diff --git a/scripts/communityScripts/chat/FloofChat.html b/script-archive/floofChat/FloofChat.html
similarity index 100%
rename from scripts/communityScripts/chat/FloofChat.html
rename to script-archive/floofChat/FloofChat.html
diff --git a/scripts/communityScripts/chat/FloofChat.js b/script-archive/floofChat/FloofChat.js
similarity index 100%
rename from scripts/communityScripts/chat/FloofChat.js
rename to script-archive/floofChat/FloofChat.js
diff --git a/scripts/communityScripts/chat/FloofChat.qml b/script-archive/floofChat/FloofChat.qml
similarity index 100%
rename from scripts/communityScripts/chat/FloofChat.qml
rename to script-archive/floofChat/FloofChat.qml
diff --git a/scripts/communityScripts/chat/chat.png b/script-archive/floofChat/chat.png
similarity index 100%
rename from scripts/communityScripts/chat/chat.png
rename to script-archive/floofChat/chat.png
diff --git a/scripts/communityScripts/chat/css/FloofChat.css b/script-archive/floofChat/css/FloofChat.css
similarity index 100%
rename from scripts/communityScripts/chat/css/FloofChat.css
rename to script-archive/floofChat/css/FloofChat.css
diff --git a/scripts/communityScripts/chat/css/materialize.css b/script-archive/floofChat/css/materialize.css
similarity index 100%
rename from scripts/communityScripts/chat/css/materialize.css
rename to script-archive/floofChat/css/materialize.css
diff --git a/scripts/communityScripts/chat/js/materialize.min.js b/script-archive/floofChat/js/materialize.min.js
similarity index 100%
rename from scripts/communityScripts/chat/js/materialize.min.js
rename to script-archive/floofChat/js/materialize.min.js
diff --git a/scripts/communityScripts/chat/resources/bubblepop.wav b/script-archive/floofChat/resources/bubblepop.wav
similarity index 100%
rename from scripts/communityScripts/chat/resources/bubblepop.wav
rename to script-archive/floofChat/resources/bubblepop.wav
diff --git a/scripts/system/html/ChatPage.html b/script-archive/systemChat/ChatPage.html
similarity index 100%
rename from scripts/system/html/ChatPage.html
rename to script-archive/systemChat/ChatPage.html
diff --git a/scripts/system/chat.js b/script-archive/systemChat/chat.js
similarity index 99%
rename from scripts/system/chat.js
rename to script-archive/systemChat/chat.js
index 94a26dc6320..14696946b38 100644
--- a/scripts/system/chat.js
+++ b/script-archive/systemChat/chat.js
@@ -4,7 +4,7 @@
//
// By Don Hopkins (dhopkins@donhopkins.com) on May 5th, 2017
// Copyright 2017 High Fidelity, Inc.
-// Copyright 2023 Overte e.V.
+// Copyright 2024 Overte e.V.
//
//
// Distributed under the Apache License, Version 2.0.
@@ -13,7 +13,7 @@
(function() {
- var webPageURL = Script.resolvePath("html/ChatPage.html"); // URL of tablet web page.
+ var webPageURL = Script.resolvePath("ChatPage.html"); // URL of tablet web page.
var randomizeWebPageURL = true; // Set to true for debugging.
var lastWebPageURL = ""; // Last random URL of tablet web page.
var onChatPage = false; // True when chat web page is opened.
diff --git a/scripts/communityScripts/armored-chat/README.md b/scripts/communityScripts/armored-chat/README.md
new file mode 100644
index 00000000000..23854946761
--- /dev/null
+++ b/scripts/communityScripts/armored-chat/README.md
@@ -0,0 +1,205 @@
+# Armored Chat
+
+1. What is Armored Chat
+2. User manual
+ - Installation
+ - Settings
+ - Usability tips
+3. Development
+
+## What is Armored Chat
+
+Armored Chat is a chat application strictly made to communicate between players in the same domain. It is made using QML and to be as light weight as reasonably possible.
+
+### Dependencies
+
+AC uses the Overte [Messages](https://apidocs.overte.org/Messages.html) API to communicate.
+
+For notifications, AC uses [notificationCore.js](https://github.com/overte-org/overte/blob/bb8bac43eadd3b20956a2ff7b0b21c28844b0f77/scripts/communityScripts/notificationCore/notificationCore.js).
+
+## User manual
+
+### Installation
+
+Armored Chat is preinstalled courtesy of [defaultScripts.js](https://github.com/overte-org/overte/blob/8661e8a858663b48e8485c2cd7120dc3e2d7b87e/scripts/defaultScripts.js).
+
+If AC is not preinstalled, or for some other reason it can not be automatically installed, you can install it manually by following [these instructions](https://github.com/overte-org/overte/blob/8661e8a858663b48e8485c2cd7120dc3e2d7b87e/scripts/defaultScripts.js) to open your script management application, and loading the script url:
+
+```
+https://raw.githubusercontent.com/overte-org/overte/master/scripts/communityScripts/armored-chat/armored_chat.js
+```
+
+---
+
+### Settings
+
+Armored Chat comes with basic settings for managing itself.
+
+#### External window
+
+This boolean setting toggles whether AC will be a in-game overlay window, or whether AC will be a external floating window.
+
+Default is `false`.
+
+#### Maximum saved messages
+
+This integer represents the amount of messages to save in the AC history. More messages may be present if AC is left on long enough. This setting only sets the number of saved messages and not the maximum amount of messages that can be viewed at any time.
+
+This means if you set the value to `5`, your history will save a maximum of 5 messages, however you will still be able to see a longer history in the session should you receive more. Once AC completely closes and fetches your message history as it initializes, you will only see the last 5 messages.
+
+Default value is `200`
+
+#### Erase chat history
+
+This action immediately clears the AC history and the session. Functionally this will set the message list to a empty Array.
+
+### Usage
+
+AC has two chat modes: Local, and Domain. Local chat displays all other local chat messages that are within 20 units of you. Domain chat will display all other Domain messages sent though that channel regardless of distance.
+
+AC also handles link embedding. When you send an HTTP(S) link, it will automatically parse it using Qt RichText and allow everyone to click on the message. Next to the link you will also see a "⮺" symbol. Clicking on this symbol will open the link in an external window.
+
+### Usability tips
+
+#### Navigation
+
+You can scroll quickly using kinetic scrolling! Try "grabbing" the right side of messages, where the timestamp is, and flinging yourself in a direction.
+
+#### Formatting
+
+You can format messages using basic HTML elements. Try `
Red text!
` to color your text red.
+Find the full list of Qt rich text tags [here](https://doc.qt.io/qt-6/richtext-html-subset.html). Please note that some of these tags may be intentionally restricted.
+
+#### Media embedding
+
+Images can be embedded when linked directly.
+
+Try it out by linking to the Overte logo! `https://github.com/overte-org/overte/raw/master/interface/resources/images/brand-banner.svg`
+
+In order for images to be embedded, URLs must end in a image filetype.
+Supported filetypes are:
+
+- `.png`
+- `.jpg`
+- `.jpeg`
+- `.gif`
+- `.bmp`
+- `.svg`
+- `.webp`
+
+## Development
+
+### To QML communication
+
+Here are the signals needed to communicate from the JavaScript core to the QML interface.
+
+AC calls a `_emitEvent()` function that also includes a `type` key in the object. This `type` tells the QML and/or the JS core what the packet is for.
+When you call the `_emitEvent()` function be sure to include the following signals as a `type`. In the examples below, the `type` is being excluded for brevity.
+
+Example:
+
+```json
+{ type: "show_message", displayName: "username", ...}
+```
+
+#### "show_message"
+
+This signal tells the QML to add a new message to the ListView element list.
+
+Supply a `JSON` object.
+
+```json
+{
+ "displayName": "username",
+ "message": "chat message",
+ "channel": "domain", // Channel to send message on. By default it should only be "domain" or "local".
+ "date": "[ time and date string ]" // Optional, defaults to current time and date.
+}
+```
+
+#### "clear_messages"
+
+Clear all messages displayed in the ListView elements. Note this does not clear the history and this is only a visual erasure.
+
+No payload required.
+
+#### "notification"
+
+Renders a notification to the domain channel.
+The intended use is to provide updates about the domain and make the notifications accessible.
+
+Supply a `JSON` object.
+
+```json
+{
+ "message": "notification message" // Notification to render
+}
+```
+
+#### "initial_settings"
+
+Visually set the settings in the QML interface based on the supplied object.
+
+Supply a `JSON` object.
+
+```json
+{
+ "settings": {
+ // JSON object of current AC settings
+ "external_window": false,
+ "maximum_messages": 200
+ }
+}
+```
+
+### To JS communication
+
+Here are the signals needed to communicate from the QML interface to the JavaScript core. AC is developed in a way that all actions that are not style related are preformed though the JavaScript core.
+This means that what ever action you want to preform must go though the JavaScript core for processing.
+
+This is formatted the same was as the communication packets to the QML interface. Supply the following entries as "type"s in your packet.
+
+#### "send_message"
+
+Tell AC to broadcast a message to the domain.
+
+Supply a `JSON` object.
+
+```json
+{
+ "message": "message content", // The contents of the message to send.
+ "channel": "domain" // Channel to emit the message to.
+}
+```
+
+#### "setting_change"
+
+Tell AC to change a setting. Exercise caution when using this as you can add new settings unintentionally if you are not careful.
+
+Supply a `JSON` object
+
+```json
+{
+ "setting": "external_window", // The name of the setting to change
+ "value": true // The value to change the setting to
+}
+```
+
+#### "action"
+
+Tell AC to preform a generic action. This is normally reserved for functions that would get called on a button onClicked event in the QML.
+
+Supply a `JSON` object
+
+```json
+{
+ "action": "erase_history" // The action to preform
+}
+```
+
+#### "initialized"
+
+Tell AC the QML overlay has loaded successfully.
+This is called to hide the overlay on creation.
+
+No payload required.
diff --git a/scripts/communityScripts/armored-chat/armored_chat.js b/scripts/communityScripts/armored-chat/armored_chat.js
new file mode 100644
index 00000000000..fe66da7a549
--- /dev/null
+++ b/scripts/communityScripts/armored-chat/armored_chat.js
@@ -0,0 +1,291 @@
+//
+// 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
+
+(() => {
+ ("use strict");
+
+ var appIsVisible = false;
+ var settings = {
+ external_window: false,
+ maximum_messages: 200,
+ };
+
+ // Global vars
+ var tablet;
+ var chatOverlayWindow;
+ var appButton;
+ var quickMessage;
+ const channels = ["domain", "local"];
+ var messageHistory = Settings.getValue("ArmoredChat-Messages", []) || [];
+ var maxLocalDistance = 20; // Maximum range for the local chat
+ var palData = AvatarManager.getPalData().data;
+
+ Controller.keyPressEvent.connect(keyPressEvent);
+ Messages.subscribe("Chat"); // Floofchat
+ Messages.subscribe("chat");
+ Messages.messageReceived.connect(receivedMessage);
+ AvatarManager.avatarAddedEvent.connect((sessionId) => {
+ _avatarAction("connected", sessionId);
+ });
+ AvatarManager.avatarRemovedEvent.connect((sessionId) => {
+ _avatarAction("left", sessionId);
+ });
+
+ startup();
+
+ function startup() {
+ tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
+
+ appButton = tablet.addButton({
+ icon: Script.resolvePath("./img/icon_white.png"),
+ activeIcon: Script.resolvePath("./img/icon_black.png"),
+ text: "CHAT",
+ isActive: appIsVisible,
+ });
+
+ // When script ends, remove itself from tablet
+ Script.scriptEnding.connect(function () {
+ console.log("Shutting Down");
+ tablet.removeButton(appButton);
+ chatOverlayWindow.close();
+ });
+
+ // Overlay button toggle
+ appButton.clicked.connect(toggleMainChatWindow);
+
+ quickMessage = new OverlayWindow({
+ source: Script.resolvePath("./armored_chat_quick_message.qml"),
+ });
+
+ _openWindow();
+ }
+ function toggleMainChatWindow() {
+ appIsVisible = !appIsVisible;
+ appButton.editProperties({ isActive: appIsVisible });
+ chatOverlayWindow.visible = appIsVisible;
+
+ // External window was closed; the window does not exist anymore
+ if (chatOverlayWindow.title == "" && appIsVisible) {
+ _openWindow();
+ }
+ }
+ function _openWindow() {
+ chatOverlayWindow = new Desktop.createWindow(
+ Script.resolvePath("./armored_chat.qml"),
+ {
+ title: "Chat",
+ size: { x: 550, y: 400 },
+ additionalFlags: Desktop.ALWAYS_ON_TOP,
+ visible: appIsVisible,
+ presentationMode: Desktop.PresentationMode.VIRTUAL,
+ }
+ );
+
+ chatOverlayWindow.closed.connect(toggleMainChatWindow);
+ chatOverlayWindow.fromQml.connect(fromQML);
+ quickMessage.fromQml.connect(fromQML);
+ }
+ function receivedMessage(channel, message) {
+ // Is the message a chat message?
+ channel = channel.toLowerCase();
+ if (channel !== "chat") return;
+ message = JSON.parse(message);
+
+ if (!message.channel) message.channel = "domain"; // We don't know where to put this message. Assume it is a domain wide message.
+ if (message.forApp) return; // Floofchat
+
+ // Floofchat compatibility hook
+ message = floofChatCompatibilityConversion(message);
+ message.channel = message.channel.toLowerCase(); // Make sure the "local", "domain", etc. is formatted consistently
+
+ if (!channels.includes(message.channel)) return; // Check the channel
+ if (
+ message.channel == "local" &&
+ Vec3.distance(MyAvatar.position, message.position) >
+ maxLocalDistance
+ )
+ return; // If message is local, and if player is too far away from location, don't do anything
+
+ // Update qml view of to new message
+ _emitEvent({ type: "show_message", ...message });
+
+ Messages.sendLocalMessage(
+ "Floof-Notif",
+ JSON.stringify({
+ sender: message.displayName,
+ text: message.message,
+ })
+ );
+
+ // Save message to history
+ let savedMessage = message;
+ delete savedMessage.position;
+ savedMessage.timeString = new Date().toLocaleTimeString(undefined, {
+ hour12: false,
+ });
+ savedMessage.dateString = new Date().toLocaleDateString(undefined, {
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ });
+ messageHistory.push(savedMessage);
+ while (messageHistory.length > settings.maximum_messages) {
+ messageHistory.shift();
+ }
+ Settings.setValue("ArmoredChat-Messages", messageHistory);
+ }
+ function fromQML(event) {
+ switch (event.type) {
+ case "send_message":
+ _sendMessage(event.message, event.channel);
+ break;
+ case "setting_change":
+ settings[event.setting] = event.value; // Update local settings
+ _saveSettings(); // Save local settings
+
+ switch (event.setting) {
+ case "external_window":
+ chatOverlayWindow.presentationMode = event.value
+ ? Desktop.PresentationMode.NATIVE
+ : Desktop.PresentationMode.VIRTUAL;
+ break;
+ case "maximum_messages":
+ // Do nothing
+ break;
+ }
+
+ break;
+ case "action":
+ switch (event.action) {
+ case "erase_history":
+ Settings.setValue("ArmoredChat-Messages", []);
+ _emitEvent({
+ type: "clear_messages",
+ });
+ break;
+ }
+ break;
+ case "initialized":
+ // https://github.com/overte-org/overte/issues/824
+ chatOverlayWindow.visible = appIsVisible; // The "visible" field in the Desktop.createWindow does not seem to work. Force set it to the initial state (false)
+ _loadSettings();
+ break;
+ }
+ }
+ function keyPressEvent(event) {
+ switch (JSON.stringify(event.key)) {
+ case "16777220": // Enter key
+ if (HMD.active) return; // Don't allow in VR
+
+ quickMessage.sendToQml({
+ type: "change_visibility",
+ value: true,
+ });
+ }
+ }
+ function _sendMessage(message, channel) {
+ if (message.length == 0) return;
+
+ Messages.sendMessage(
+ "chat",
+ JSON.stringify({
+ position: MyAvatar.position,
+ message: message,
+ displayName: MyAvatar.sessionDisplayName,
+ channel: channel,
+ action: "send_chat_message",
+ })
+ );
+
+ floofChatCompatibilitySendMessage(message, channel);
+ }
+ function _avatarAction(type, sessionId) {
+ Script.setTimeout(() => {
+ if (type == "connected") {
+ palData = AvatarManager.getPalData().data;
+ }
+
+ // Get the display name of the user
+ let displayName = "";
+ displayName =
+ AvatarManager.getPalData([sessionId])?.data[0]
+ ?.sessionDisplayName || null;
+ if (displayName == null) {
+ for (let i = 0; i < palData.length; i++) {
+ if (palData[i].sessionUUID == sessionId) {
+ displayName = palData[i].sessionDisplayName;
+ }
+ }
+ }
+
+ // Format the packet
+ let message = {};
+ message.message = `${displayName} ${type}`;
+
+ _emitEvent({ type: "notification", ...message });
+ }, 1500);
+ }
+ function _loadSettings() {
+ settings = Settings.getValue("ArmoredChat-Config", settings);
+
+ if (messageHistory) {
+ // Load message history
+ messageHistory.forEach((message) => {
+ delete message.action;
+ _emitEvent({ type: "show_message", ...message });
+ });
+ }
+
+ // Send current settings to the app
+ _emitEvent({ type: "initial_settings", settings: settings });
+ }
+ 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 {("show_message"|"clear_messages"|"notification"|"initial_settings")} packet.type - The type of packet it is
+ */
+ function _emitEvent(packet = { type: "" }) {
+ chatOverlayWindow.sendToQml(packet);
+ }
+
+ //
+ // Floofchat compatibility functions
+ // Added to ease the transition between Floofchat to ArmoredChat
+ // These functions can be safely removed at a much later date.
+ function floofChatCompatibilityConversion(message) {
+ if (message.type === "TransmitChatMessage" && !message.forApp) {
+ return {
+ position: message.position,
+ message: message.message,
+ displayName: message.displayName,
+ channel: message.channel.toLowerCase(),
+ };
+ }
+ return message;
+ }
+
+ function floofChatCompatibilitySendMessage(message, channel) {
+ Messages.sendMessage(
+ "Chat",
+ JSON.stringify({
+ position: MyAvatar.position,
+ message: message,
+ displayName: MyAvatar.sessionDisplayName,
+ channel: channel.charAt(0).toUpperCase() + channel.slice(1),
+ type: "TransmitChatMessage",
+ forApp: "Floof",
+ })
+ );
+ }
+})();
diff --git a/scripts/communityScripts/armored-chat/armored_chat.qml b/scripts/communityScripts/armored-chat/armored_chat.qml
new file mode 100644
index 00000000000..13506c226a5
--- /dev/null
+++ b/scripts/communityScripts/armored-chat/armored_chat.qml
@@ -0,0 +1,566 @@
+import QtQuick 2.7
+import QtQuick.Controls 2.0
+import QtQuick.Layouts 1.3
+import controlsUit 1.0 as HifiControlsUit
+
+Rectangle {
+ color: Qt.rgba(0.1,0.1,0.1,1)
+ signal sendToScript(var message);
+
+ property string pageVal: "local"
+ property string last_message_user: ""
+ property date last_message_time: new Date()
+
+ // When the window is created on the script side, the window starts open.
+ // Once the QML window is created wait, then send the initialized signal.
+ // This signal is mostly used to close the "Desktop overlay window" script side
+ // https://github.com/overte-org/overte/issues/824
+ Timer {
+ interval: 10
+ running: true
+ repeat: false
+ onTriggered: {
+ toScript({type: "initialized"});
+ load_scroll_timer.running = true
+ }
+ }
+ Timer {
+ id: load_scroll_timer
+ interval: 100
+ running: false
+ repeat: false
+ onTriggered: {
+ scrollToBottom();
+ }
+ }
+
+ // User view
+ Item {
+ anchors.fill: parent
+
+ // Navigation Bar
+ Rectangle {
+ id: navigation_bar
+ width: parent.width
+ height: 40
+ color:Qt.rgba(0,0,0,1)
+
+ Item {
+ height: parent.height
+ width: parent.width
+ anchors.fill: parent
+
+ Rectangle {
+ width: pageVal === "local" ? 100 : 60
+ height: parent.height
+ color: pageVal === "local" ? "#505186" : "white"
+ id: local_page
+
+ Image {
+ source: "./img/ui/" + (pageVal === "local" ? "social_white.png" : "social_black.png")
+ sourceSize.width: 40
+ sourceSize.height: 40
+ anchors.centerIn: parent
+ }
+
+ Behavior on width {
+ NumberAnimation {
+ duration: 50
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ pageVal = "local";
+ load_scroll_timer.running = true;
+ }
+ }
+ }
+ Rectangle {
+ width: pageVal === "domain" ? 100 : 60
+ height: parent.height
+ color: pageVal === "domain" ? "#505186" : "white"
+ anchors.left: local_page.right
+ anchors.leftMargin: 5
+ id: domain_page
+
+ Image {
+ source: "./img/ui/" + (pageVal === "domain" ? "world_white.png" : "world_black.png")
+ sourceSize.width: 30
+ sourceSize.height: 30
+ anchors.centerIn: parent
+ }
+
+ Behavior on width {
+ NumberAnimation {
+ duration: 50
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ pageVal = "domain"
+ load_scroll_timer.running = true;
+ }
+ }
+ }
+
+ Rectangle {
+ width: pageVal === "settings" ? 100 : 60
+ height: parent.height
+ color: pageVal === "settings" ? "#505186" : "white"
+ anchors.right: parent.right
+ id: settings_page
+
+ Image {
+ source: "./img/ui/" + (pageVal === "settings" ? "settings_white.png" : "settings_black.png")
+ sourceSize.width: 30
+ sourceSize.height: 30
+ anchors.centerIn: parent
+ }
+
+ Behavior on width {
+ NumberAnimation {
+ duration: 50
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ pageVal = "settings"
+ }
+ }
+ }
+ }
+
+ }
+
+ // Pages
+ Item {
+ width: parent.width
+ height: parent.height - 40
+ anchors.top: navigation_bar.bottom
+ visible: ["local", "domain"].includes(pageVal) ? true : false
+
+
+ // Chat Message History
+ ListView {
+ width: parent.width
+ height: parent.height - 40
+ clip: true
+ interactive: true
+ spacing: 5
+ id: listview
+
+ delegate: Loader {
+ property int delegateIndex: index
+ property string delegateText: model.text
+ property string delegateUsername: model.username
+ property string delegateDate: model.date
+ width: listview.width
+
+ sourceComponent: {
+ if (model.type === "chat") {
+ return template_chat_message;
+ } else if (model.type === "notification") {
+ return template_notification;
+ }
+ }
+ }
+
+ ScrollBar.vertical: ScrollBar {
+ id: chat_scrollbar
+ height: 100
+ size: 0.05
+ }
+
+ model: getChannel(pageVal)
+
+ }
+
+ ListModel {
+ id: local
+ }
+
+ ListModel {
+ id: domain
+ }
+
+ // Chat Entry
+ Rectangle {
+ width: parent.width
+ height: 40
+ color: Qt.rgba(0.9,0.9,0.9,1)
+ anchors.bottom: parent.bottom
+
+ Row {
+ width: parent.width
+ height: parent.height
+
+ TextField {
+ width: parent.width - 60
+ height: parent.height
+ placeholderText: pageVal.charAt(0).toUpperCase() + pageVal.slice(1) + " chat message..."
+ clip: false
+ Keys.onPressed: {
+ if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && !(event.modifiers & Qt.ShiftModifier)) {
+ event.accepted = true;
+ toScript({type: "send_message", message: text, channel: pageVal});
+ text = ""
+ }
+ }
+ onFocusChanged: {
+ if (!HMD.active) return;
+ if (focus) return ApplicationInterface.showVRKeyboardForHudUI(true);
+ ApplicationInterface.showVRKeyboardForHudUI(false);
+ }
+ }
+ Button {
+ width: 60
+ height:parent.height
+
+ Image {
+ source: "./img/ui/send_black.png"
+ sourceSize.width: 30
+ sourceSize.height: 30
+ anchors.centerIn: parent
+ }
+
+ onClicked: {
+ toScript({type: "send_message", message: parent.children[0].text, channel: pageVal});
+ parent.children[0].text = ""
+ }
+ Keys.onPressed: {
+ if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
+ toScript({type: "send_message", message: parent.children[0].text, channel: pageVal});
+ parent.children[0].text = ""
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Item {
+ width: parent.width
+ height: parent.height - 40
+ anchors.top: navigation_bar.bottom
+ visible: ["local", "domain"].includes(pageVal) ? false : true
+
+ Column {
+ width: parent.width - 10
+ height: parent.height - 10
+ anchors.centerIn: parent
+ spacing: 10
+
+ // External Window
+ Rectangle {
+ width: parent.width
+ height: 40
+ color: "transparent"
+
+ Text{
+ text: "External window"
+ color: "white"
+ font.pointSize: 12
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ CheckBox{
+ id: s_external_window
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+
+ onCheckedChanged: {
+ toScript({type: 'setting_change', setting: 'external_window', value: checked})
+ }
+ }
+ }
+
+ // Maximum saved messages
+ Rectangle {
+ width: parent.width
+ height: 40
+ color: "transparent"
+
+ Text{
+ text: "Maximum saved messages"
+ color: "white"
+ font.pointSize: 12
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+
+ HifiControlsUit.SpinBox {
+ id: s_maximum_messages
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ decimals: 0
+ width: 100
+ height: parent.height
+ realFrom: 1
+ realTo: 1000
+ backgroundColor: "#cccccc"
+
+ onValueChanged: {
+ toScript({type: 'setting_change', setting: 'maximum_messages', value: value})
+ }
+ }
+ }
+
+ // Erase History
+ Rectangle {
+ width: parent.width
+ height: 40
+ color: Qt.rgba(0.15,0.15,0.15,1);
+
+ Text{
+ text: "Erase chat history"
+ color: "white"
+ font.pointSize: 12
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Button {
+ anchors.right: parent.right
+ text: "Erase"
+ height: parent.height
+ anchors.verticalCenter: parent.verticalCenter
+
+ onClicked: {
+ toScript({type: "action", action: "erase_history"})
+ }
+ }
+ }
+ }
+ }
+
+ }
+
+ // Templates
+ Component {
+ id: template_chat_message
+
+ Rectangle{
+ property int index: delegateIndex
+ property string texttest: delegateText
+ property string username: delegateUsername
+ property string date: delegateDate
+
+ height: Math.max(65, children[1].height + 30)
+ color: index % 2 === 0 ? "transparent" : Qt.rgba(0.15,0.15,0.15,1)
+
+ Item {
+ width: parent.width - 10
+ anchors.horizontalCenter: parent.horizontalCenter
+ height: 22
+
+ Text{
+ text: username
+ color: "lightgray"
+ }
+
+ Text{
+ anchors.right: parent.right
+ text: date
+ color: "lightgray"
+ }
+ }
+
+ TextEdit{
+ anchors.top: parent.children[0].bottom
+ x: 5
+ text: texttest
+ color:"white"
+ font.pointSize: 12
+ readOnly: true
+ selectByMouse: true
+ selectByKeyboard: true
+ width: parent.width * 0.8
+ height: contentHeight
+ wrapMode: Text.Wrap
+ textFormat: TextEdit.RichText
+
+ onLinkActivated: {
+ Window.openWebBrowser(link)
+ }
+ }
+ }
+ }
+
+ Component {
+ id: template_notification
+
+ Rectangle{
+ property int index: delegateIndex
+ property string texttest: delegateText
+ property string username: delegateUsername
+ property string date: delegateDate
+ color: "#171717"
+ width: parent.width
+ height: 40
+
+ Item {
+ width: 10
+ height: parent.height
+
+ Rectangle {
+ height: parent.height
+ width: 5
+ color: "#505186"
+ }
+ }
+
+
+ Item {
+ width: parent.width - parent.children[0].width - 5
+ height: parent.height
+ anchors.left: parent.children[0].right
+
+ TextEdit{
+ text: texttest
+ color:"white"
+ font.pointSize: 12
+ readOnly: true
+ width: parent.width * 0.8
+ selectByMouse: true
+ selectByKeyboard: true
+ height: parent.height
+ wrapMode: Text.Wrap
+ verticalAlignment: Text.AlignVCenter
+ font.italic: true
+ }
+
+ Text {
+ text: date
+ color:"white"
+ font.pointSize: 12
+ anchors.right: parent.right
+ height: parent.height
+ wrapMode: Text.Wrap
+ horizontalAlignment: Text.AlignRight
+ verticalAlignment: Text.AlignVCenter
+ font.italic: true
+ }
+ }
+
+ }
+
+ }
+
+
+
+ property var channels: {
+ "local": local,
+ "domain": domain,
+ }
+
+ function scrollToBottom() {
+ if (listview.count == 0) return;
+ listview.positionViewAtEnd();
+ }
+
+
+ function addMessage(username, message, date, channel, type){
+ channel = getChannel(channel)
+
+ // Format content
+ message = formatContent(message);
+
+ message = embedImages(message);
+
+ if (type === "notification"){
+ channel.append({ text: message, date: date, type: "notification" });
+ last_message_user = "";
+ scrollToBottom();
+ last_message_time = new Date();
+ return;
+ }
+
+ var current_time = new Date();
+ var elapsed_time = current_time - last_message_time;
+ var elapsed_minutes = elapsed_time / (1000 * 60);
+
+ var last_item_index = channel.count - 1;
+ var last_item = channel.get(last_item_index);
+
+ if (last_message_user === username && elapsed_minutes < 1 && last_item){
+ message = " " + message
+ last_item.text = last_item.text += "\n" + message;
+ scrollToBottom()
+ last_message_time = new Date();
+ return;
+ }
+
+ last_message_user = username;
+ last_message_time = new Date();
+ channel.append({ text: message, username: username, date: date, type: type });
+ scrollToBottom();
+ }
+
+ function getChannel(id) {
+ return channels[id];
+ }
+
+ function formatContent(mess) {
+ var arrow = /\ {return "" + match + "⮺"});
+
+ var newline = /\n/gi;
+ mess = mess.replace(newline, " ");
+ return mess
+ }
+
+ function embedImages(mess){
+ var image_link = /(https?:(\/){2})[\w.-]+(?:\.[\w\.-]+)+(?:\/[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]*)(?:png|jpe?g|gif|bmp|svg|webp)/g;
+ var matches = mess.match(image_link);
+ var new_message = ""
+ var listed = []
+ var total_emeds = 0
+
+ new_message += mess
+
+ for (var i = 0; matches && matches.length > i && total_emeds < 3; i++){
+ if (!listed.includes(matches[i])) {
+ new_message += " "
+ listed.push(matches[i]);
+ total_emeds++
+ }
+ }
+ return new_message;
+ }
+
+ // Messages from script
+ function fromScript(message) {
+ let time = new Date().toLocaleTimeString(undefined, { hour12: false });
+ let date = new Date().toLocaleDateString(undefined, { year: "numeric", month: "long", day: "numeric", });
+
+ switch (message.type){
+ case "show_message":
+ addMessage(message.displayName, message.message, `[ ${message.timeString || time} - ${message.dateString || date} ]`, message.channel, "chat");
+ break;
+ case "notification":
+ addMessage("SYSTEM", message.message, `[ ${time} - ${date} ]`, "domain", "notification");
+ break;
+ case "clear_messages":
+ local.clear();
+ domain.clear();
+ break;
+ case "initial_settings":
+ if (message.settings.external_window) s_external_window.checked = true;
+ if (message.settings.maximum_messages) s_maximum_messages.value = message.settings.maximum_messages;
+ break;
+ }
+ }
+
+ // Send message to script
+ function toScript(packet){
+ sendToScript(packet)
+ }
+}
diff --git a/scripts/communityScripts/armored-chat/armored_chat_quick_message.qml b/scripts/communityScripts/armored-chat/armored_chat_quick_message.qml
new file mode 100644
index 00000000000..ae110db379e
--- /dev/null
+++ b/scripts/communityScripts/armored-chat/armored_chat_quick_message.qml
@@ -0,0 +1,112 @@
+import QtQuick 2.5
+import QtQuick.Controls 1.4
+
+Rectangle {
+ id: root
+ property var window
+
+ Binding { target: root; property:'window'; value: parent.parent; when: Boolean(parent.parent) }
+ Binding { target: window; property: 'shown'; value: false; when: Boolean(window) }
+ Component.onDestruction: chat_bar && chat_bar.destroy()
+
+ property alias chat_bar: chat_bar
+
+ Rectangle {
+ id: chat_bar
+ parent: desktop
+ x: 0
+ y: parent.height - height
+ width: parent.width
+ height: 50
+ z: 99
+ visible: false
+
+ TextArea {
+ id: textArea
+ x: 0
+ width: parent.width
+ height: parent.height
+ text:""
+ textColor: "#ffffff"
+ clip: false
+ font.pointSize: 18
+
+ Keys.onReturnPressed: { _onEnterPressed(); }
+ Keys.onEnterPressed: { _onEnterPressed(); }
+ Keys.onLeftPressed: { moveLeft(); }
+ Keys.onRightPressed: { moveRight(); }
+
+ function moveLeft(){
+ if (cursorPosition > 0){
+ cursorPosition--
+ }
+ }
+ function moveRight(){
+ if (cursorPosition < text.length){
+ cursorPosition++
+ }
+ }
+ }
+
+ Text {
+ text: "Local message..."
+ font.pointSize: 16
+ color: "gray"
+ x: 0
+ width: parent.width
+ anchors.verticalCenter: parent.verticalCenter
+ visible: textArea.text == ""
+ }
+
+ Button {
+ id: button
+ x: parent.width - width
+ y: 0
+ width: 64
+ height: parent.height
+ clip: false
+ visible: true
+
+ Image {
+ id: image
+ width: 30
+ height: 30
+ fillMode: Image.PreserveAspectFit
+ visible: true
+ anchors.centerIn: parent
+ source: "./img/ui/send_white.png"
+ }
+
+ onClicked: {
+ _onEnterPressed();
+ }
+ }
+
+ }
+
+ function _onEnterPressed() {
+ changeVisibility(false)
+ toScript({type: "send_message", message: textArea.text, channel: "local"})
+ textArea.text = "";
+ }
+
+ function changeVisibility(state){
+ chat_bar.visible = state
+ if (state) textArea.forceActiveFocus();
+ else root.parent.forceActiveFocus();
+ }
+
+ // Messages from script
+ function fromScript(message) {
+ switch (message.type){
+ case "change_visibility":
+ changeVisibility(message.value)
+ break;
+ }
+ }
+
+ // Send message to script
+ function toScript(packet){
+ sendToScript(packet)
+ }
+}
\ No newline at end of file
diff --git a/scripts/communityScripts/armored-chat/img/icon_black.png b/scripts/communityScripts/armored-chat/img/icon_black.png
new file mode 100644
index 00000000000..410dc40b593
Binary files /dev/null and b/scripts/communityScripts/armored-chat/img/icon_black.png differ
diff --git a/scripts/communityScripts/armored-chat/img/icon_white.png b/scripts/communityScripts/armored-chat/img/icon_white.png
new file mode 100644
index 00000000000..e29bf997064
Binary files /dev/null and b/scripts/communityScripts/armored-chat/img/icon_white.png differ
diff --git a/scripts/communityScripts/armored-chat/img/ui/send.svg b/scripts/communityScripts/armored-chat/img/ui/send.svg
new file mode 100644
index 00000000000..82c70a6dafe
--- /dev/null
+++ b/scripts/communityScripts/armored-chat/img/ui/send.svg
@@ -0,0 +1,42 @@
+
+
diff --git a/scripts/communityScripts/armored-chat/img/ui/send_black.png b/scripts/communityScripts/armored-chat/img/ui/send_black.png
new file mode 100644
index 00000000000..bc9ece7a118
Binary files /dev/null and b/scripts/communityScripts/armored-chat/img/ui/send_black.png differ
diff --git a/scripts/communityScripts/armored-chat/img/ui/send_white.png b/scripts/communityScripts/armored-chat/img/ui/send_white.png
new file mode 100644
index 00000000000..2730d2f84c7
Binary files /dev/null and b/scripts/communityScripts/armored-chat/img/ui/send_white.png differ
diff --git a/scripts/communityScripts/armored-chat/img/ui/settings_black.png b/scripts/communityScripts/armored-chat/img/ui/settings_black.png
new file mode 100644
index 00000000000..f6481a85f8f
Binary files /dev/null and b/scripts/communityScripts/armored-chat/img/ui/settings_black.png differ
diff --git a/scripts/communityScripts/armored-chat/img/ui/settings_white.png b/scripts/communityScripts/armored-chat/img/ui/settings_white.png
new file mode 100644
index 00000000000..12a35ad58c9
Binary files /dev/null and b/scripts/communityScripts/armored-chat/img/ui/settings_white.png differ
diff --git a/scripts/communityScripts/armored-chat/img/ui/social_black.png b/scripts/communityScripts/armored-chat/img/ui/social_black.png
new file mode 100644
index 00000000000..16777af4629
Binary files /dev/null and b/scripts/communityScripts/armored-chat/img/ui/social_black.png differ
diff --git a/scripts/communityScripts/armored-chat/img/ui/social_white.png b/scripts/communityScripts/armored-chat/img/ui/social_white.png
new file mode 100644
index 00000000000..7677bd54696
Binary files /dev/null and b/scripts/communityScripts/armored-chat/img/ui/social_white.png differ
diff --git a/scripts/communityScripts/armored-chat/img/ui/world_black.png b/scripts/communityScripts/armored-chat/img/ui/world_black.png
new file mode 100644
index 00000000000..c983e5df28d
Binary files /dev/null and b/scripts/communityScripts/armored-chat/img/ui/world_black.png differ
diff --git a/scripts/communityScripts/armored-chat/img/ui/world_white.png b/scripts/communityScripts/armored-chat/img/ui/world_white.png
new file mode 100644
index 00000000000..1f152b47b2c
Binary files /dev/null and b/scripts/communityScripts/armored-chat/img/ui/world_white.png differ
diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js
index 1cbadadb67e..a9bc2be591e 100644
--- a/scripts/defaultScripts.js
+++ b/scripts/defaultScripts.js
@@ -7,7 +7,7 @@
//
// Copyright 2014 High Fidelity, Inc.
// Copyright 2020 Vircadia contributors.
-// Copyright 2022 Overte e.V.
+// 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
@@ -47,7 +47,7 @@ var DEFAULT_SCRIPTS_SEPARATE = [
"communityScripts/notificationCore/notificationCore.js",
"simplifiedUI/ui/simplifiedNametag/simplifiedNametag.js",
{"stable": "system/more/app-more.js", "beta": "https://more.overte.org/more/app-more.js"},
- "communityScripts/chat/FloofChat.js",
+ "communityScripts/armored-chat/armored_chat.js",
//"system/chat.js"
];