Merge pull request #68 from Armored-Dragon/armored-chat

Chat-ng
This commit is contained in:
ksuprynowicz 2024-02-24 21:29:18 +01:00 committed by GitHub
commit 63a894f996
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 1106 additions and 0 deletions

View file

@ -0,0 +1,27 @@
# Armored Chat
Armored Chat is a light-weight alternative chat application that extends the existing chat features.
## Features
- (wip) Drop-in replacement for Fluffy chat
- (wip) E2EE Direct messages
- (wip) Group chats
- (?) Message signing
## Encryption
TODO:
- Algorithm
- Key exchange
- When and where
- How
## Group chats
TODO:
- How
- Limitations

View file

@ -0,0 +1,234 @@
//
// armored_chat.js
//
// Created by Armored Dragon, 2024.
// Copyright 2024 Overte e.V.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
(function () {
"use strict";
// TODO: Encryption + PMs
// TODO: Find window init event method
var app_is_visible = false;
var settings = {
max_history: 250,
compact_chat: false,
external_window: false,
};
var app_data = { current_page: "domain" };
// Global vars
var ac_tablet;
var chat_overlay_window;
var app_button;
const channels = ["domain", "local", "system"];
var max_local_distance = 20; // Maximum range for the local chat
startup();
function startup() {
ac_tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
app_button = ac_tablet.addButton({
icon: Script.resolvePath("./img/icon.png"),
text: "CHAT",
isActive: app_is_visible,
});
// When script ends, remove itself from tablet
Script.scriptEnding.connect(function () {
console.log("Shutting Down");
ac_tablet.removeButton(app_button);
chat_overlay_window.close();
});
// Overlay button toggle
app_button.clicked.connect(toggleMainChatWindow);
_openWindow();
}
function toggleMainChatWindow() {
app_is_visible = !app_is_visible;
console.log(`App is now ${app_is_visible ? "visible" : "hidden"}`);
app_button.editProperties({ isActive: app_is_visible });
chat_overlay_window.visible = app_is_visible;
// External window was closed; the window does not exist anymore
if (chat_overlay_window.title == "" && app_is_visible) {
_openWindow();
}
}
function _openWindow() {
chat_overlay_window = new Desktop.createWindow(Script.resourcesPath() + "qml/hifi/tablet/DynamicWebview.qml", {
title: "Overte Chat",
size: { x: 550, y: 400 },
additionalFlags: Desktop.ALWAYS_ON_TOP,
visible: app_is_visible, // FIXME Invalid?
presentationMode: Desktop.PresentationMode.VIRTUAL,
});
chat_overlay_window.visible = app_is_visible; // The "visible" field in the Desktop.createWindow does not seem to work. Force set it to false
chat_overlay_window.closed.connect(toggleMainChatWindow);
chat_overlay_window.sendToQml({ url: Script.resolvePath("./index.html") });
// FIXME: Loadsettings need to happen after the window is initialized?
// Script.setTimeout(_loadSettings, 1000);
chat_overlay_window.webEventReceived.connect(onWebEventReceived);
}
// Initialize default message subscriptions
Messages.subscribe("chat");
// Messages.subscribe("system");
Messages.messageReceived.connect(receivedMessage);
function receivedMessage(channel, message) {
console.log(`Received message:\n${message}`);
var message = JSON.parse(message);
channel = channel.toLowerCase();
if (channel !== "chat") return;
message.channel = message.channel.toLowerCase();
// For now, while we are working on superseding Floof, we will allow compatibility with it.
// If for_app exists, it came from us and we are just sending the message so Floof can read it.
// We don't need to listen to this message.
if (message.for_app) return;
// Check the channel is valid
if (!channels.includes(message.channel)) return;
// FIXME: Not doing distance check?
// If message is local, and if player is too far away from location, don't do anything
if (channel === "local" && Vec3.distance(MyAvatar.position, message.position) < max_local_distance) return;
// Floof chat compatibility.
if (message.type) delete message.type;
// Update web view of to new message
_emitEvent({ type: "show_message", ...message });
// Display on popup chat area
_overlayMessage({ sender: message.displayName, message: message });
}
function onWebEventReceived(event) {
console.log(`New web event:\n${event}`);
// FIXME: Lazy!
// Checks to see if the event is a JSON object
if (!event.includes("{")) return;
var parsed = JSON.parse(event);
// Not our app? Not our problem!
// if (parsed.app !== "ArmoredChat") return;
switch (parsed.type) {
case "page_update":
app_data.current_page = parsed.page;
break;
case "send_message":
_sendMessage(parsed.message);
break;
case "open_url":
Window.openUrl(parsed.message.toString());
break;
case "setting_update":
// Update local settings
settings[parsed.setting_name] = parsed.setting_value;
// Save local settings
_saveSettings();
switch (parsed.setting_name) {
case "external_window":
console.log(parsed.setting_value);
chat_overlay_window.presentationMode = parsed.setting_value ? Desktop.PresentationMode.NATIVE : Desktop.PresentationMode.VIRTUAL;
break;
}
break;
case "initialized":
_loadSettings();
break;
}
}
//
// Sending messages
// These functions just shout out their messages. We are listening to messages in an other function, and will record all heard messages there
function _sendMessage(message) {
Messages.sendMessage(
"chat",
JSON.stringify({
position: MyAvatar.position,
message: message,
displayName: MyAvatar.sessionDisplayName,
channel: app_data.current_page,
action: "send_chat_message",
})
);
// FloofyChat Compatibility
Messages.sendMessage(
"Chat",
JSON.stringify({
position: MyAvatar.position,
message: message,
displayName: MyAvatar.sessionDisplayName,
channel: app_data.current_page.charAt(0).toUpperCase() + app_data.current_page.slice(1),
type: "TransmitChatMessage",
for_app: "Floof",
})
);
// Show overlay of the message you sent
_overlayMessage({ sender: MyAvatar.sessionDisplayName, message: message });
}
function _overlayMessage(message) {
// Floofchat compatibility
// This makes it so that our own messages are not rendered.
// For now, Floofchat has priority over notifications as they use a strange system I don't want to touch yet.
if (!message.action) return;
Messages.sendLocalMessage(
"Floof-Notif",
JSON.stringify({
sender: message.sender,
text: message.message,
color: { red: 122, green: 122, blue: 122 },
})
);
}
function _loadSettings() {
console.log("Loading config");
settings = Settings.getValue("ArmoredChat-Config", settings);
console.log("\nSettings follow:");
console.log(JSON.stringify(settings, " ", 4));
// Compact chat
if (settings.compact_chat) {
_emitEvent({ type: "setting_update", setting_name: "compact_chat", setting_value: true });
}
// External Window
if (settings.external_window) {
chat_overlay_window.presentationMode = settings.external_window ? Desktop.PresentationMode.NATIVE : Desktop.PresentationMode.VIRTUAL;
_emitEvent({ type: "setting_update", setting_name: "external_window", setting_value: true });
}
}
function _saveSettings() {
console.log("Saving config");
Settings.setValue("ArmoredChat-Config", settings);
}
/**
* Emit a packet to the HTML front end. Easy communication!
* @param {Object} packet - The Object packet to emit to the HTML
* @param {("setting_update"|"show_message")} packet.type - The type of packet it is
*/
function _emitEvent(packet = { type: "" }) {
chat_overlay_window.emitScriptEvent(JSON.stringify(packet));
}
})();

View file

@ -0,0 +1,22 @@
body .page .content.message-list .message {
display: grid;
box-sizing: border-box;
grid-template-columns: 1fr 1fr;
grid-gap: inherit;
padding: 2px;
margin-bottom: 5px;
}
body .page .content.message-list .message .pfp {
display: none !important;
}
body .page .content.message-list .message .name {
color: #dbdbdb;
}
body .page .content.message-list .message .timestamp {
text-align: right;
color: #dbdbdb;
}
body .page .content.message-list .message .body {
grid-column-start: 1;
grid-column-end: 3;
}

View file

@ -0,0 +1,29 @@
body {
.page {
.content.message-list {
.message {
display: grid;
box-sizing: border-box;
grid-template-columns: 1fr 1fr;
grid-gap: inherit;
padding: 2px;
margin-bottom: 5px;
.pfp {
display: none !important;
}
.name {
color: #dbdbdb;
}
.timestamp {
text-align: right;
color: #dbdbdb;
}
.body {
grid-column-start: 1;
grid-column-end: 3;
}
}
}
}
}

View file

@ -0,0 +1,22 @@
(function () {
// TODO: Sign messages
// TODO: Verify signatures
let rsa = forge.pki.rsa;
let keypair;
function newKeyPair() {
// 2048 bits. Not the most super-duper secure length of 4096.
// This value must remain low to ensure lower-power machines can use.
// We will generate new keys automatically every so often and will also allow user to refresh keys.
keypair = rsa.generateKeyPair({ bits: 2048, workers: -1 });
}
function encrypt(message) {
if (!keypair) return null;
return keypair.publicKey.encrypt("Test message");
}
function decrypt(message) {
if (!keypair) return null;
return keypair.privateKey.decrypt(encrypted);
}
})();

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 B

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="32"
viewBox="0 -960 760 640"
width="38"
version="1.1"
id="svg4"
sodipodi:docname="send.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
inkscape:export-filename="send_black.png"
inkscape:export-xdpi="300"
inkscape:export-ydpi="300"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<sodipodi:namedview
id="namedview6"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
showgrid="false"
inkscape:zoom="21.395833"
inkscape:cx="17.363194"
inkscape:cy="16.031159"
inkscape:window-width="2560"
inkscape:window-height="1366"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<path
d="m 0,-320 v -640 l 760,320 z M 60,-413 604,-640 60,-870 v 168 l 242,62 -242,60 z m 0,0 v -457 z"
id="path2"
style="fill:#000000" />
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="31.049999"
viewBox="0 -960 640 620.99998"
width="32"
version="1.1"
id="svg4"
sodipodi:docname="user.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
inkscape:export-filename="user_white.png"
inkscape:export-xdpi="309.17874"
inkscape:export-ydpi="309.17874"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<sodipodi:namedview
id="namedview6"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
showgrid="false"
inkscape:zoom="21.395833"
inkscape:cx="15.306719"
inkscape:cy="15.119766"
inkscape:window-width="1826"
inkscape:window-height="1233"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg4" />
<path
d="m 320,-660 q -66,0 -108,-42 -42,-42 -42,-108 0,-66 42,-108 42,-42 108,-42 66,0 108,42 42,42 42,108 0,66 -42,108 -42,42 -108,42 z M 0,-339 v -94 q 0,-38 19,-65 19,-27 49,-41 67,-30 128.5,-45 61.5,-15 123.5,-15 62,0 123,15.5 61,15.5 127.921,44.694 31.301,14.126 50.19,40.966 Q 640,-471 640,-433 v 94 z m 60,-60 h 520 v -34 q 0,-16 -9.5,-30.5 Q 561,-478 547,-485 483,-516 430,-527.5 377,-539 320,-539 q -57,0 -111,11.5 -54,11.5 -117,42.5 -14,7 -23,21.5 -9,14.5 -9,30.5 z m 260,-321 q 39,0 64.5,-25.5 Q 410,-771 410,-810 410,-849 384.5,-874.5 359,-900 320,-900 q -39,0 -64.5,25.5 -25.5,25.5 -25.5,64.5 0,39 25.5,64.5 25.5,25.5 64.5,25.5 z m 0,-90 z m 0,411 z"
id="path2"
style="fill:#ffffff" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View file

@ -0,0 +1,190 @@
body {
background-color: black;
color: white;
margin: 0;
height: 100vh;
width: 100vw;
font-family: Verdana, Geneva, Tahoma, sans-serif;
display: flex;
flex-direction: column;
}
body .header {
width: 100%;
height: 40px;
display: flex;
flex-direction: row;
}
body .header button {
width: 60px;
height: 100%;
border-radius: 0;
border: 0;
margin: 0;
padding: 0;
box-sizing: border-box;
transition: width ease-in-out 0.2s;
}
body .header button.active {
background-color: #6667ab;
color: white;
width: 100px;
}
body .header .left {
margin: 0 auto 0 0;
}
body .header .right {
margin: 0 0 0 auto;
}
body .page {
display: flex;
flex-direction: column;
flex-grow: 1;
overflow-y: auto;
width: 100%;
}
body .page .content {
width: 100%;
background-color: #111;
flex-grow: 1;
}
body .page .content.message-list {
overflow-y: auto;
overflow-x: hidden;
width: 100%;
}
body .page .content.message-list .message:nth-child(even) {
background-color: #1a1a1a;
}
body .page .content.message-list .message {
display: grid;
box-sizing: border-box;
grid-template-columns: 80px 5fr;
grid-gap: 0.75rem;
padding: 0.8rem 0.15rem;
width: 100%;
overflow-y: auto;
}
body .page .content.message-list .message .pfp {
height: 30px;
width: auto;
display: flex;
}
body .page .content.message-list .message .pfp img {
height: 100%;
width: auto;
margin: auto;
border-radius: 50px;
background-color: black;
}
body .page .content.message-list .message .name {
font-size: 1.15rem;
color: #dbdbdb;
}
body .page .content.message-list .message .body {
width: 100%;
word-wrap: anywhere;
overflow-x: hidden;
}
body .page .content.message-list .message .body .image-container {
width: 100%;
max-width: 400px;
max-height: 300px;
}
body .page .content.message-list .message .body .image-container img {
width: auto;
height: 100%;
max-width: 400px;
max-height: 300px;
}
body .page .content.message-list .message .timestamp {
text-align: center;
color: #dbdbdb;
}
body .page .content.settings .setting {
width: 100%;
display: flex;
padding: 0.5rem 0.25rem;
box-sizing: border-box;
}
body .page .content.settings .setting-toggle input {
margin: auto 0 auto auto;
width: 20px;
height: 20px;
}
body .page .content.settings .setting:nth-child(even) {
background-color: #1a1a1a;
}
body .footer {
width: 100%;
height: 40px;
}
body .footer.text-entry {
display: Flex;
flex-direction: row;
}
body .footer.text-entry input {
flex-grow: 1;
margin-right: 0;
border: 0;
font-size: 1.3rem;
min-width: 0;
}
body .footer.text-entry button {
width: 75px;
border: 0;
border-radius: 0;
}
body .hidden {
display: none !important;
}
button {
width: 100px;
height: 100%;
background-color: white;
border-radius: 2px;
border: 0;
cursor: pointer;
outline: none;
}
button span {
width: 100%;
height: 100%;
}
button span img {
max-height: 70%;
width: auto;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
button:focus {
filter: brightness(50%);
}
input {
outline: none;
}
input:focus {
background-color: lightgray;
}
@media screen and (max-width: 400px) {
body .header {
height: 30px;
}
body .header button {
width: 50px;
}
body .page .content.message-list .message {
grid-template-columns: 80px 5fr;
grid-gap: 0.75rem;
padding: 0.8rem 0.15rem;
}
body .page .content.message-list .message .pfp {
height: 30px;
}
}

View file

@ -0,0 +1,267 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdn.jsdelivr.net/npm/node-forge@1.0.0/dist/forge.min.js"></script>
<link rel="stylesheet" href="index.css" />
<!-- Compact messages -->
<!-- <link id="compact-messages" rel="stylesheet" href="compact-messages.css" /> -->
</head>
<body>
<!-- The primary page. This is where people will chat -->
<div class="header">
<div class="left">
<button data-target="domain-chat" class="active">
<span><img src="./img/ui/world_white.png" /></span>
</button>
<button data-target="local-chat">
<span><img src="./img/ui/social_black.png" /></span>
</button>
<!-- <button data-target="pm-chat">
<span><img src="./img/ui/user_black.png" /></span>
</button> -->
</div>
<div class="right">
<button data-target="settings">
<span><img src="./img/ui/settings_black.png" /></span>
</button>
</div>
</div>
<div id="domain-chat" class="page chat">
<div class="content message-list"></div>
</div>
<!-- Local Chat -->
<div id="local-chat" class="page hidden">
<div class="content message-list"></div>
</div>
<!-- DM page. -->
<div id="pm-chat" class="page hidden">
<div class="content">PM content</div>
</div>
<!-- Settings page. Adjust the chat here -->
<div id="settings" class="page hidden">
<div class="content settings">
<!-- <div class="setting setting-toggle">
<span>Show typing indicator</span>
<input id="typing-indicator-toggle" type="checkbox" />
</div> -->
<!-- <div class="setting setting-toggle">
<span>Show speech bubbles</span>
<input id="speech-bubble-toggle" type="checkbox" />
</div> -->
<div class="setting setting-toggle">
<span>Compact Mode</span>
<input id="compact-message-toggle" type="checkbox" />
</div>
<div class="setting setting-toggle">
<span>External Window</span>
<input id="external-window-toggle" type="checkbox" />
</div>
</div>
</div>
<div class="footer text-entry">
<input id="message-entry" type="text" placeholder="Enter message here..." />
<button id="send-message">
<span><img src="./img/ui/send_black.png" /></span>
</button>
</div>
</body>
</html>
<script>
"use strict";
_emitEvent({ type: "initialized" }); // Tell the script we are ready
const qs = (target) => document.querySelector(target);
const qsa = (target) => document.querySelectorAll(target);
var scroll_distance = 100000; // The scroll distance for the chat window to automatically scroll down with.
var compact_mode = false; // Compact messages
var external_window = false;
// Start listening for new messages
EventBridge.scriptEventReceived.connect(_newScriptEvent);
// HTML Event listeners
qs("#send-message").addEventListener("click", _sendMessage);
qs("#message-entry").addEventListener("keyup", (event) => {
if (event.keyCode === 13) _sendMessage(); // Enter key, send message
});
// Add event listeners to all nav-buttons
qsa(".header button").forEach((button) => {
button.addEventListener("click", () => {
_switchPage(button.dataset.target, button);
if (button.dataset.target === "settings") {
qs(".footer.text-entry").classList.add("hidden");
} else {
qs(".footer.text-entry").classList.remove("hidden");
}
});
});
function _switchPage(target, button) {
// Hide all pages
qsa(".page").forEach((page) => page.classList.add("hidden"));
// Deactivate all nav-buttons
qsa(".header button").forEach((button) => deactivateButton(button));
// Show target page
qs(`#${target}`).classList.remove("hidden");
// Sets active for target button
activateButton(button);
function deactivateButton(button) {
button.querySelector("img").src = button.querySelector("img").src.replace("_white", "_black");
button.classList.remove("active");
}
function activateButton(button) {
button.querySelector("img").src = button.querySelector("img").src.replace("_black", "_white");
button.classList.add("active");
button.blur();
// Tell script where we are at
_emitEvent({ type: "page_update", page: button.dataset.target.replace("-chat", "") });
}
}
function _sendMessage() {
qs("#send-message").blur();
// Don't send empty messages.
if (qs("#message-entry").value.length === 0) return;
// Send what is in the text area as a message
_emitEvent({ type: "send_message", message: qs("#message-entry").value });
// Clear the message area
qs("#message-entry").value = "";
}
function _newScriptEvent(message) {
message = JSON.parse(message);
switch (message.type) {
case "show_message":
_showMessage(message);
break;
case "setting_update":
_newSetting(message);
break;
}
}
// Settings
qs("#compact-message-toggle").addEventListener("change", function () {
_toggleCompactMode();
_emitEvent({ type: "setting_update", setting_name: "compact_chat", setting_value: compact_mode });
});
function _toggleCompactMode() {
compact_mode = !compact_mode;
if (compact_mode) {
// Add the stylesheet to the head
document.head.insertAdjacentHTML("beforeend", '<link id="compact-messages-ss" rel="stylesheet" href="compact-messages.css" />');
} else {
// Remove the compact messages stylesheet
qs("#compact-messages-ss").remove();
}
}
qs("#external-window-toggle").addEventListener("change", function () {
external_window = !external_window;
_emitEvent({ type: "setting_update", setting_name: "external_window", setting_value: external_window });
});
// TODO: Limit embeds to 3.
function _showMessage(message) {
var target = message.channel + "-chat";
// Clone template message
let message_template = qs("#message-listing");
let message_clone = message_template.content.cloneNode(true);
// Youtube embeds
let yt_url = message.message.match(/(https?:\/\/)?(www\.)?(youtube\.com\/watch\?v=|youtu\.be\/)([^& \n<]+)(?:[^ \n<]+)?/g);
if (yt_url) {
message.message = message.message.replace(
yt_url,
`${yt_url}
<br>
<iframe class="z-depth-2" width='420' height='236' src='https://www.youtube.com/embed/${yt_url.toString().split("/")[3]}' frameborder='0'></iframe>`
);
}
// Image embeds
let image_link = message.message.match(/.+.(png|jpg|jpeg|webp)/g);
if (image_link) {
message.message = message.message.replace(image_link, `${image_link}<br><span class='image-container'><img src='${image_link}'></span>`);
}
// Linkify links
let link_regex = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$/gm;
if (message.message.match(link_regex)) {
message.message = message.message.replace(link_regex, (match) => {
return `<a href='#' onclick='_emitEvent({type:"open_url", url: match.toString()})'>${match}</a>`;
});
}
// Update template data to message data
message_clone.querySelector(".name").innerText = message.displayName;
message_clone.querySelector(".timestamp").innerText = new Date().toLocaleTimeString(undefined, { hour12: false });
message_clone.querySelector(".timestamp").title = new Date().toLocaleDateString(undefined, {
month: "long",
day: "numeric",
});
message_clone.querySelector(".body").innerHTML = message.message;
// Append to the message list
qs("#" + target + " .message-list").appendChild(message_clone);
// Scroll to the bottom of the page
qs("#" + target + " .message-list").scrollTop = scroll_distance;
// Increase scroll distance so it will continue to work for future messages.
scroll_distance = scroll_distance + 100000;
}
/**
* Called when the script is initialized and we are loading the user settings
*/
function _newSetting(message) {
switch (message.setting_name) {
case "compact_chat":
qs("#compact-message-toggle").checked = true;
_toggleCompactMode();
break;
case "external_window":
qs("#external-window-toggle").checked = true;
external_window = true;
break;
}
}
/**
* Emit a packet to the HTML front end. Easy communication!
* @param {Object} packet - The Object packet to emit to the HTML
* @param {("setting_update"|"send_message"|"page_update"|"open_url"|"initialized")} packet.type - The type of packet it is
*/
function _emitEvent(packet = { type: "" }) {
EventBridge.emitWebEvent(JSON.stringify(packet));
}
</script>
<template id="message-listing">
<div class="message">
<div class="pfp"><img src="./img/ui/user_white.png" /></div>
<div class="name">[NAME]</div>
<div class="timestamp" title="[DATE]">[TIMESTAMP]</div>
<div class="body">[CONTENT]</div>
</div>
</template>
<!-- <script src="encrpytion.js"></script> -->

View file

@ -0,0 +1,222 @@
body {
background-color: black;
color: white;
margin: 0;
height: 100vh;
width: 100vw;
font-family: Verdana, Geneva, Tahoma, sans-serif;
display: flex;
flex-direction: column;
.header {
width: 100%;
height: 40px;
display: flex;
flex-direction: row;
button {
width: 60px;
height: 100%;
border-radius: 0;
border: 0;
margin: 0;
padding: 0;
box-sizing: border-box;
transition: width ease-in-out 0.2s;
}
button.active {
background-color: #6667ab;
color: white;
width: 100px;
}
.left {
margin: 0 auto 0 0;
}
.right {
margin: 0 0 0 auto;
}
}
.page {
display: flex;
flex-direction: column;
flex-grow: 1;
overflow-y: auto;
width: 100%;
.content {
width: 100%;
background-color: #111;
flex-grow: 1;
}
.content.message-list {
overflow-y: auto;
overflow-x: hidden;
width: 100%;
.message:nth-child(even) {
background-color: #1a1a1a;
}
.message {
display: grid;
box-sizing: border-box;
grid-template-columns: 80px 5fr;
grid-gap: 0.75rem;
padding: 0.8rem 0.15rem;
width: 100%;
overflow-y: auto;
.pfp {
height: 30px;
width: auto;
display: flex;
img {
height: 100%;
width: auto;
margin: auto;
border-radius: 50px;
background-color: black;
}
}
.name {
font-size: 1.15rem;
color: #dbdbdb;
}
.body {
width: 100%;
word-wrap: anywhere;
overflow-x: hidden;
.image-container {
width: 100%;
max-width: 400px;
max-height: 300px;
img {
width: auto;
height: 100%;
max-width: 400px;
max-height: 300px;
}
}
}
.timestamp {
text-align: center;
color: #dbdbdb;
}
}
}
.content.settings {
.setting {
width: 100%;
display: flex;
padding: 0.5rem 0.25rem;
box-sizing: border-box;
}
.setting-toggle {
input {
margin: auto 0 auto auto;
width: 20px;
height: 20px;
}
}
.setting:nth-child(even) {
background-color: #1a1a1a;
}
}
}
.footer {
width: 100%;
height: 40px;
}
.footer.text-entry {
display: Flex;
flex-direction: row;
input {
flex-grow: 1;
margin-right: 0;
border: 0;
font-size: 1.3rem;
min-width: 0;
}
button {
width: 75px;
border: 0;
border-radius: 0;
}
}
.hidden {
display: none !important;
}
}
button {
width: 100px;
height: 100%;
background-color: white;
border-radius: 2px;
border: 0;
cursor: pointer;
outline: none;
span {
width: 100%;
height: 100%;
img {
max-height: 70%;
width: auto;
user-select: none;
-webkit-user-drag: none;
}
}
}
button:focus {
filter: brightness(50%);
}
input {
outline: none;
}
input:focus {
background-color: lightgray;
}
@media screen and (max-width: 400px) {
body {
.header {
height: 30px;
button {
width: 50px;
}
}
.page {
.content.message-list {
.message {
grid-template-columns: 80px 5fr;
grid-gap: 0.75rem;
padding: 0.8rem 0.15rem;
.pfp {
height: 30px;
}
}
}
}
}
}

View file

@ -314,6 +314,15 @@ var metadata = { "applications":
"jsfile": "replica/replica-app.js",
"icon": "replica/replica-i.png",
"caption": "REPLICA"
},
{
"isActive": true,
"directory": "armored-chat",
"name": "Chat",
"description": "Chat application",
"jsfile": "armored-chat/armored_chat.js",
"icon": "armored-chat/img/icon.png",
"caption": "CHAT"
}
]
};