diff --git a/applications/armored-chat/README.md b/applications/armored-chat/README.md new file mode 100644 index 0000000..cd7d682 --- /dev/null +++ b/applications/armored-chat/README.md @@ -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 diff --git a/applications/armored-chat/armored_chat.js b/applications/armored-chat/armored_chat.js new file mode 100644 index 0000000..76a38f1 --- /dev/null +++ b/applications/armored-chat/armored_chat.js @@ -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)); + } +})(); diff --git a/applications/armored-chat/compact-messages.css b/applications/armored-chat/compact-messages.css new file mode 100644 index 0000000..b5f0eef --- /dev/null +++ b/applications/armored-chat/compact-messages.css @@ -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; +} \ No newline at end of file diff --git a/applications/armored-chat/compact-messages.scss b/applications/armored-chat/compact-messages.scss new file mode 100644 index 0000000..ed40c1e --- /dev/null +++ b/applications/armored-chat/compact-messages.scss @@ -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; + } + } + } + } +} diff --git a/applications/armored-chat/encrpytion.js b/applications/armored-chat/encrpytion.js new file mode 100644 index 0000000..fa2233f --- /dev/null +++ b/applications/armored-chat/encrpytion.js @@ -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); + } +})(); diff --git a/applications/armored-chat/img/icon.png b/applications/armored-chat/img/icon.png new file mode 100644 index 0000000..410dc40 Binary files /dev/null and b/applications/armored-chat/img/icon.png differ diff --git a/applications/armored-chat/img/ui/send.svg b/applications/armored-chat/img/ui/send.svg new file mode 100644 index 0000000..82c70a6 --- /dev/null +++ b/applications/armored-chat/img/ui/send.svg @@ -0,0 +1,42 @@ + + + + + + diff --git a/applications/armored-chat/img/ui/send_black.png b/applications/armored-chat/img/ui/send_black.png new file mode 100644 index 0000000..bc9ece7 Binary files /dev/null and b/applications/armored-chat/img/ui/send_black.png differ diff --git a/applications/armored-chat/img/ui/send_white.png b/applications/armored-chat/img/ui/send_white.png new file mode 100644 index 0000000..2730d2f Binary files /dev/null and b/applications/armored-chat/img/ui/send_white.png differ diff --git a/applications/armored-chat/img/ui/settings_black.png b/applications/armored-chat/img/ui/settings_black.png new file mode 100644 index 0000000..f6481a8 Binary files /dev/null and b/applications/armored-chat/img/ui/settings_black.png differ diff --git a/applications/armored-chat/img/ui/settings_white.png b/applications/armored-chat/img/ui/settings_white.png new file mode 100644 index 0000000..12a35ad Binary files /dev/null and b/applications/armored-chat/img/ui/settings_white.png differ diff --git a/applications/armored-chat/img/ui/social_black.png b/applications/armored-chat/img/ui/social_black.png new file mode 100644 index 0000000..16777af Binary files /dev/null and b/applications/armored-chat/img/ui/social_black.png differ diff --git a/applications/armored-chat/img/ui/social_white.png b/applications/armored-chat/img/ui/social_white.png new file mode 100644 index 0000000..7677bd5 Binary files /dev/null and b/applications/armored-chat/img/ui/social_white.png differ diff --git a/applications/armored-chat/img/ui/user.svg b/applications/armored-chat/img/ui/user.svg new file mode 100644 index 0000000..110e522 --- /dev/null +++ b/applications/armored-chat/img/ui/user.svg @@ -0,0 +1,42 @@ + + + + + + diff --git a/applications/armored-chat/img/ui/user_black.png b/applications/armored-chat/img/ui/user_black.png new file mode 100644 index 0000000..b3ca6e0 Binary files /dev/null and b/applications/armored-chat/img/ui/user_black.png differ diff --git a/applications/armored-chat/img/ui/user_white.png b/applications/armored-chat/img/ui/user_white.png new file mode 100644 index 0000000..74ec794 Binary files /dev/null and b/applications/armored-chat/img/ui/user_white.png differ diff --git a/applications/armored-chat/img/ui/world_black.png b/applications/armored-chat/img/ui/world_black.png new file mode 100644 index 0000000..c983e5d Binary files /dev/null and b/applications/armored-chat/img/ui/world_black.png differ diff --git a/applications/armored-chat/img/ui/world_white.png b/applications/armored-chat/img/ui/world_white.png new file mode 100644 index 0000000..1f152b4 Binary files /dev/null and b/applications/armored-chat/img/ui/world_white.png differ diff --git a/applications/armored-chat/index.css b/applications/armored-chat/index.css new file mode 100644 index 0000000..2f73fe3 --- /dev/null +++ b/applications/armored-chat/index.css @@ -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; + } +} \ No newline at end of file diff --git a/applications/armored-chat/index.html b/applications/armored-chat/index.html new file mode 100644 index 0000000..a314d1f --- /dev/null +++ b/applications/armored-chat/index.html @@ -0,0 +1,267 @@ + + + + + + + + + + + + + +
+
+ + + +
+
+ +
+
+
+
+
+ + + + + + + + + + +
+ + +
+ + + + + + + + diff --git a/applications/armored-chat/index.scss b/applications/armored-chat/index.scss new file mode 100644 index 0000000..5b98ae4 --- /dev/null +++ b/applications/armored-chat/index.scss @@ -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; + } + } + } + } + } +} diff --git a/applications/metadata.js b/applications/metadata.js index 538591e..5ceac29 100644 --- a/applications/metadata.js +++ b/applications/metadata.js @@ -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" } ] }; \ No newline at end of file