From b155e999212cd91e304208e3d8744a8a31a345d6 Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Fri, 29 Nov 2024 23:53:27 -0600
Subject: [PATCH 01/49] Moved script. Changed name.

---
 scripts/defaultScripts.js                           |   2 +-
 .../armored-chat => system/domainChat}/README.md    |  12 ++++++------
 .../domainChat/domainChat.js}                       |   6 +++---
 .../domainChat/domainChat.qml}                      |   0
 .../domainChat/domainChatQuick.qml}                 |   0
 .../domainChat}/img/icon_black.png                  | Bin
 .../domainChat}/img/icon_white.png                  | Bin
 .../domainChat}/img/ui/send.svg                     |   0
 .../domainChat}/img/ui/send_black.png               | Bin
 .../domainChat}/img/ui/send_white.png               | Bin
 .../domainChat}/img/ui/settings_black.png           | Bin
 .../domainChat}/img/ui/settings_white.png           | Bin
 .../domainChat}/img/ui/social_black.png             | Bin
 .../domainChat}/img/ui/social_white.png             | Bin
 .../domainChat}/img/ui/world_black.png              | Bin
 .../domainChat}/img/ui/world_white.png              | Bin
 16 files changed, 10 insertions(+), 10 deletions(-)
 rename scripts/{communityScripts/armored-chat => system/domainChat}/README.md (93%)
 rename scripts/{communityScripts/armored-chat/armored_chat.js => system/domainChat/domainChat.js} (98%)
 rename scripts/{communityScripts/armored-chat/armored_chat.qml => system/domainChat/domainChat.qml} (100%)
 rename scripts/{communityScripts/armored-chat/armored_chat_quick_message.qml => system/domainChat/domainChatQuick.qml} (100%)
 rename scripts/{communityScripts/armored-chat => system/domainChat}/img/icon_black.png (100%)
 rename scripts/{communityScripts/armored-chat => system/domainChat}/img/icon_white.png (100%)
 rename scripts/{communityScripts/armored-chat => system/domainChat}/img/ui/send.svg (100%)
 rename scripts/{communityScripts/armored-chat => system/domainChat}/img/ui/send_black.png (100%)
 rename scripts/{communityScripts/armored-chat => system/domainChat}/img/ui/send_white.png (100%)
 rename scripts/{communityScripts/armored-chat => system/domainChat}/img/ui/settings_black.png (100%)
 rename scripts/{communityScripts/armored-chat => system/domainChat}/img/ui/settings_white.png (100%)
 rename scripts/{communityScripts/armored-chat => system/domainChat}/img/ui/social_black.png (100%)
 rename scripts/{communityScripts/armored-chat => system/domainChat}/img/ui/social_white.png (100%)
 rename scripts/{communityScripts/armored-chat => system/domainChat}/img/ui/world_black.png (100%)
 rename scripts/{communityScripts/armored-chat => system/domainChat}/img/ui/world_white.png (100%)

diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js
index 31afd6e2db..d185daadb5 100644
--- a/scripts/defaultScripts.js
+++ b/scripts/defaultScripts.js
@@ -46,7 +46,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/armored-chat/armored_chat.js",
+    "system/domainChat/domainChat.js",
     //"system/chat.js"
 ];
 
diff --git a/scripts/communityScripts/armored-chat/README.md b/scripts/system/domainChat/README.md
similarity index 93%
rename from scripts/communityScripts/armored-chat/README.md
rename to scripts/system/domainChat/README.md
index 2385494676..8ed2e8d911 100644
--- a/scripts/communityScripts/armored-chat/README.md
+++ b/scripts/system/domainChat/README.md
@@ -1,15 +1,15 @@
-# Armored Chat
+# Domain Chat
 
-1. What is Armored Chat
+1. What is Domain Chat
 2. User manual
     - Installation
     - Settings
     - Usability tips
 3. Development
 
-## What is Armored Chat
+## What is Domain 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.
+Domain 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
 
@@ -21,7 +21,7 @@ For notifications, AC uses [notificationCore.js](https://github.com/overte-org/o
 
 ### Installation
 
-Armored Chat is preinstalled courtesy of [defaultScripts.js](https://github.com/overte-org/overte/blob/8661e8a858663b48e8485c2cd7120dc3e2d7b87e/scripts/defaultScripts.js).
+Domain 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:
 
@@ -33,7 +33,7 @@ https://raw.githubusercontent.com/overte-org/overte/master/scripts/communityScri
 
 ### Settings
 
-Armored Chat comes with basic settings for managing itself.
+Domain Chat comes with basic settings for managing itself.
 
 #### External window
 
diff --git a/scripts/communityScripts/armored-chat/armored_chat.js b/scripts/system/domainChat/domainChat.js
similarity index 98%
rename from scripts/communityScripts/armored-chat/armored_chat.js
rename to scripts/system/domainChat/domainChat.js
index 779dc3ff54..a7836859ce 100644
--- a/scripts/communityScripts/armored-chat/armored_chat.js
+++ b/scripts/system/domainChat/domainChat.js
@@ -1,5 +1,5 @@
 //
-//  armored_chat.js
+//  domainChat.js
 //
 //  Created by Armored Dragon, 2024.
 //  Copyright 2024 Overte e.V.
@@ -61,7 +61,7 @@
         appButton.clicked.connect(toggleMainChatWindow);
 
         quickMessage = new OverlayWindow({
-            source: Script.resolvePath("./armored_chat_quick_message.qml"),
+            source: Script.resolvePath("./domainChatQuick.qml"),
         });
 
         _openWindow();
@@ -78,7 +78,7 @@
     }
     function _openWindow() {
         chatOverlayWindow = new Desktop.createWindow(
-            Script.resolvePath("./armored_chat.qml"),
+            Script.resolvePath("./domainChat.qml"),
             {
                 title: "Chat",
                 size: { x: 550, y: 400 },
diff --git a/scripts/communityScripts/armored-chat/armored_chat.qml b/scripts/system/domainChat/domainChat.qml
similarity index 100%
rename from scripts/communityScripts/armored-chat/armored_chat.qml
rename to scripts/system/domainChat/domainChat.qml
diff --git a/scripts/communityScripts/armored-chat/armored_chat_quick_message.qml b/scripts/system/domainChat/domainChatQuick.qml
similarity index 100%
rename from scripts/communityScripts/armored-chat/armored_chat_quick_message.qml
rename to scripts/system/domainChat/domainChatQuick.qml
diff --git a/scripts/communityScripts/armored-chat/img/icon_black.png b/scripts/system/domainChat/img/icon_black.png
similarity index 100%
rename from scripts/communityScripts/armored-chat/img/icon_black.png
rename to scripts/system/domainChat/img/icon_black.png
diff --git a/scripts/communityScripts/armored-chat/img/icon_white.png b/scripts/system/domainChat/img/icon_white.png
similarity index 100%
rename from scripts/communityScripts/armored-chat/img/icon_white.png
rename to scripts/system/domainChat/img/icon_white.png
diff --git a/scripts/communityScripts/armored-chat/img/ui/send.svg b/scripts/system/domainChat/img/ui/send.svg
similarity index 100%
rename from scripts/communityScripts/armored-chat/img/ui/send.svg
rename to scripts/system/domainChat/img/ui/send.svg
diff --git a/scripts/communityScripts/armored-chat/img/ui/send_black.png b/scripts/system/domainChat/img/ui/send_black.png
similarity index 100%
rename from scripts/communityScripts/armored-chat/img/ui/send_black.png
rename to scripts/system/domainChat/img/ui/send_black.png
diff --git a/scripts/communityScripts/armored-chat/img/ui/send_white.png b/scripts/system/domainChat/img/ui/send_white.png
similarity index 100%
rename from scripts/communityScripts/armored-chat/img/ui/send_white.png
rename to scripts/system/domainChat/img/ui/send_white.png
diff --git a/scripts/communityScripts/armored-chat/img/ui/settings_black.png b/scripts/system/domainChat/img/ui/settings_black.png
similarity index 100%
rename from scripts/communityScripts/armored-chat/img/ui/settings_black.png
rename to scripts/system/domainChat/img/ui/settings_black.png
diff --git a/scripts/communityScripts/armored-chat/img/ui/settings_white.png b/scripts/system/domainChat/img/ui/settings_white.png
similarity index 100%
rename from scripts/communityScripts/armored-chat/img/ui/settings_white.png
rename to scripts/system/domainChat/img/ui/settings_white.png
diff --git a/scripts/communityScripts/armored-chat/img/ui/social_black.png b/scripts/system/domainChat/img/ui/social_black.png
similarity index 100%
rename from scripts/communityScripts/armored-chat/img/ui/social_black.png
rename to scripts/system/domainChat/img/ui/social_black.png
diff --git a/scripts/communityScripts/armored-chat/img/ui/social_white.png b/scripts/system/domainChat/img/ui/social_white.png
similarity index 100%
rename from scripts/communityScripts/armored-chat/img/ui/social_white.png
rename to scripts/system/domainChat/img/ui/social_white.png
diff --git a/scripts/communityScripts/armored-chat/img/ui/world_black.png b/scripts/system/domainChat/img/ui/world_black.png
similarity index 100%
rename from scripts/communityScripts/armored-chat/img/ui/world_black.png
rename to scripts/system/domainChat/img/ui/world_black.png
diff --git a/scripts/communityScripts/armored-chat/img/ui/world_white.png b/scripts/system/domainChat/img/ui/world_white.png
similarity index 100%
rename from scripts/communityScripts/armored-chat/img/ui/world_white.png
rename to scripts/system/domainChat/img/ui/world_white.png

From 1d47275b410e73fadf4e6489891a3d6a6938f7e2 Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Sat, 30 Nov 2024 00:32:20 -0600
Subject: [PATCH 02/49] Removed floofchat compatibility.

---
 scripts/system/domainChat/domainChat.js | 36 -------------------------
 1 file changed, 36 deletions(-)

diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js
index a7836859ce..b00a424499 100644
--- a/scripts/system/domainChat/domainChat.js
+++ b/scripts/system/domainChat/domainChat.js
@@ -28,7 +28,6 @@
     var palData = AvatarManager.getPalData().data;
 
     Controller.keyPressEvent.connect(keyPressEvent);
-    Messages.subscribe("Chat"); // Floofchat
     Messages.subscribe("chat");
     Messages.messageReceived.connect(receivedMessage);
     AvatarManager.avatarAddedEvent.connect((sessionId) => {
@@ -103,10 +102,7 @@
         const timeArray = _formatTimestamp(currentTimestamp);
 
         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();
 
         // Check the channel. If the channel is not one we have, do nothing.
@@ -215,8 +211,6 @@
                 action: "send_chat_message",
             })
         );
-
-        floofChatCompatibilitySendMessage(message, channel);
     }
     function _avatarAction(type, sessionId) {
         Script.setTimeout(() => {
@@ -303,34 +297,4 @@
     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",
-            })
-        );
-    }
 })();

From 777912999eba1b903dd465212f4a0553747bd119 Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Sat, 30 Nov 2024 01:23:49 -0600
Subject: [PATCH 03/49] Notification organization.

---
 scripts/system/domainChat/domainChat.js | 45 +++++++++----------------
 1 file changed, 16 insertions(+), 29 deletions(-)

diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js
index b00a424499..c70a8f925f 100644
--- a/scripts/system/domainChat/domainChat.js
+++ b/scripts/system/domainChat/domainChat.js
@@ -101,33 +101,20 @@
         const currentTimestamp = _getTimestamp();
         const timeArray = _formatTimestamp(currentTimestamp);
 
-        if (!message.channel) message.channel = "domain"; // We don't know where to put this message. Assume it is a domain wide message.
-
-        message.channel = message.channel.toLowerCase();
-
-        // Check the channel. If the channel is not one we have, do nothing.
-        if (!channels.includes(message.channel)) return;
-
-        // If message is local, and if player is too far away from location, do nothing.
-        if (message.channel == "local" && isTooFar(message.position)) return; 
+        if (!message.channel) message.channel = "domain";                       // We don't know where to put this message. Assume it is a domain wide message.
+        message.channel = message.channel.toLowerCase();                        // Only recognize channel names as lower case. 
+        
+        if (!channels.includes(message.channel)) return;                        // Check the channel. If the channel is not one we have, do nothing.
+        if (message.channel == "local" && isTooFar(message.position)) return;   // If message is local, and if player is too far away from location, do nothing.
 
         // Format the timestamp 
         message.timeString = timeArray[0];
         message.dateString = timeArray[1];
+        
+        _emitEvent({ type: "show_message", ...message });                       // Update qml view of to new message.
+        _notificationCoreMessage(message.displayName, message.message)          // Show a new message on screen.
 
-        // Update qml view of to new message
-        _emitEvent({ type: "show_message", ...message });
-
-        // Show new message on screen
-        Messages.sendLocalMessage(
-            "Floof-Notif",
-            JSON.stringify({
-                sender: message.displayName,
-                text: message.message,
-            })
-        );
-
-        // Save message to history
+        // Create a new variable based on the message that will be saved.
         let savedMessage = message;
 
         // Remove unnecessary data.
@@ -238,13 +225,7 @@
 
             // Show new message on screen
             if (settings.join_notification){
-                Messages.sendLocalMessage(
-                    "Floof-Notif",
-                    JSON.stringify({
-                        sender: displayName,
-                        text: type,
-                    })
-                );
+                _notificationCoreMessage(displayName, type)
             }
 
             _emitEvent({ type: "notification", ...message });
@@ -288,6 +269,12 @@
 
         return timeArray;
     }
+    function _notificationCoreMessage(displayName, message){
+        Messages.sendLocalMessage(
+            "Floof-Notif",
+            JSON.stringify({ sender: displayName, text: message })
+        );
+    }
 
     /**
      * Emit a packet to the HTML front end. Easy communication!

From 54b4aa70b8b3dbdc6abc54a2772fa987d779f0b8 Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Sat, 30 Nov 2024 07:22:28 -0600
Subject: [PATCH 04/49] Message formatting.

---
 scripts/system/domainChat/domainChat.js  |  86 ++++++++++++-
 scripts/system/domainChat/domainChat.qml | 157 ++++++++++++++---------
 2 files changed, 174 insertions(+), 69 deletions(-)

diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js
index c70a8f925f..4e62afd061 100644
--- a/scripts/system/domainChat/domainChat.js
+++ b/scripts/system/domainChat/domainChat.js
@@ -7,6 +7,8 @@
 //  Distributed under the Apache License, Version 2.0.
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
 
+// TODO: Message trimming
+
 (() => {
     ("use strict");
 
@@ -110,8 +112,12 @@
         // Format the timestamp 
         message.timeString = timeArray[0];
         message.dateString = timeArray[1];
-        
-        _emitEvent({ type: "show_message", ...message });                       // Update qml view of to new message.
+
+        let formattedMessage = _parseMessage(message.message);                  // Format the message for viewing
+        let formattedMessagePacket = { ...message };
+        formattedMessagePacket.message = formattedMessage
+
+        _emitEvent({ type: "show_message", ...formattedMessagePacket });           // Update qml view of to new message.
         _notificationCoreMessage(message.displayName, message.message)          // Show a new message on screen.
 
         // Create a new variable based on the message that will be saved.
@@ -238,9 +244,15 @@
             // Load message history
             messageHistory.forEach((message) => {
                 const timeArray = _formatTimestamp(_getTimestamp());
-                message.timeString = timeArray[0];
-                message.dateString = timeArray[1];
-                _emitEvent({ type: "show_message", ...message });
+                messagePacket = { ...message };
+                messagePacket.timeString = timeArray[0];
+                messagePacket.dateString = timeArray[1];
+
+                let formattedMessage = _parseMessage(messagePacket.message);
+                let formattedMessagePacket = messagePacket;
+                formattedMessagePacket.message = formattedMessage;
+
+                _emitEvent({ type: "show_message", ...formattedMessagePacket });
             });
         }
 
@@ -275,6 +287,70 @@
             JSON.stringify({ sender: displayName, text: message })
         );
     }
+    function _parseMessage(message){
+        const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;
+        const mentionRegex = /@(\w+)/; // FIXME: Remove - devcode
+        const overteLocationRegex = null;
+
+        let runningMessage = message;
+        let messageArray = [];
+
+        const regexPatterns = [
+            { type: "url", regex: urlRegex },
+            { type: "mention", regex: mentionRegex }, // FIXME: Remove - devcode
+            { type: "overteLocation", regex: overteLocationRegex }
+        ]
+
+        // Here is a link https://www.example.com, #hashtag, and @mention. Just for some spice here is another https://exampletwo.com
+
+        while (true) {
+            let firstMatch = _findFirstMatch();
+
+            if (firstMatch == null) {
+                // If there was not any matches found in the entire message, format the whole message as a single text entry.
+                messageArray.push({type: 'text', value: runningMessage});
+
+                // Append a final 'fill width' to the message text.
+                messageArray.push({type: 'messageEnd'});
+                break;
+            }
+
+            _formatMessage(firstMatch);
+        }
+
+        return messageArray;
+
+        function _formatMessage(firstMatch){
+            let indexOfFirstMatch = firstMatch[0];
+            let regex = regexPatterns[firstMatch[1]].regex;
+
+            let foundMatch = runningMessage.match(regex)[0];
+
+            messageArray.push({type: 'text', value: runningMessage.substring(0, indexOfFirstMatch)});
+            messageArray.push({type: regexPatterns[firstMatch[1]].type, value: runningMessage.substring(indexOfFirstMatch, indexOfFirstMatch + foundMatch.length)});
+            
+            runningMessage = runningMessage.substring(indexOfFirstMatch + foundMatch.length);   // Remove the part of the message we have worked with
+        }
+
+        function _findFirstMatch(){
+            let indexOfFirstMatch = Infinity;
+            let indexOfRegexPattern = Infinity;
+
+            for (let i = 0; regexPatterns.length > i; i++){
+                let indexOfMatch = runningMessage.search(regexPatterns[i].regex);
+
+                if (indexOfMatch == -1) continue;                                              // No match found
+
+                if (indexOfMatch < indexOfFirstMatch) {
+                    indexOfFirstMatch = indexOfMatch;
+                    indexOfRegexPattern = i;
+                }
+            }
+
+            if (indexOfFirstMatch !== Infinity) return [indexOfFirstMatch, indexOfRegexPattern];    // If there was a found match
+            return null;                                                                            // No found match
+        }
+    }
 
     /**
      * Emit a packet to the HTML front end. Easy communication!
diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml
index 07eb75c626..405e5d5ea9 100644
--- a/scripts/system/domainChat/domainChat.qml
+++ b/scripts/system/domainChat/domainChat.qml
@@ -162,7 +162,7 @@ Rectangle {
                         model: getChannel(pageVal)
                         delegate: Loader {
                             property int delegateIndex: model.index
-                            property string delegateText: model.text
+                            property var delegateText: model.text
                             property string delegateUsername: model.username
                             property string delegateDate: model.date
 
@@ -384,7 +384,7 @@ Rectangle {
 
         Rectangle {
             property int index: delegateIndex
-            property string texttest: delegateText
+            property var texttest: delegateText
             property string username: delegateUsername
             property string date: delegateDate
 
@@ -410,22 +410,83 @@ Rectangle {
                 }
             }
 
-            TextEdit {
-                anchors.top: parent.children[0].bottom
-                x: 5
-                text: texttest
-                color:"white"
-                font.pointSize: 12
-                readOnly: true
-                selectByMouse: true
-                selectByKeyboard: true
+            Flow {
+                anchors.top: parent.children[0].bottom;
                 width: parent.width * 0.8
-                height: contentHeight
-                wrapMode: Text.Wrap
-                textFormat: TextEdit.RichText
+                x: 5
 
-                onLinkActivated: {
-                    Window.openWebBrowser(link)
+                Repeater {
+                    model: texttest;
+
+                    RowLayout {
+                        width: {
+                            switch (model.type) {
+                                case "text":
+                                    return children[0].width;
+                                case "url":
+                                    return children[1].width;
+                            }
+                        }
+
+                        Text {
+                            text: model.value || ""
+                            font.pointSize: 12
+                            wrapMode: Text.Wrap
+                            width: Math.min(parent.parent.parent.width, contentWidth);
+
+                            visible: model.type === 'text' || model.type === 'mention';
+
+                            color: {
+                                switch (model.type) {
+                                    case "mention":
+                                        return "purple";
+                                    default:
+                                        return "white";
+                                }
+                            }
+                        }
+
+                        RowLayout {
+                            width: Math.min(parent.parent.parent.width, children[0].contentWidth);
+                            visible: model.type === 'url';
+
+                            Text {
+                                text: model.value || ""
+                                font.pointSize: 12
+                                wrapMode: Text.Wrap
+                                color: "#4EBAFD";
+                                font.underline: true
+
+                                MouseArea {
+                                    anchors.fill: parent;
+
+                                    onClicked: {
+                                        Window.openWebBrowser(model.value)
+                                    }
+                                }
+                            }
+
+                            Text {
+                                text: "🗗"
+                                font.pointSize: 10
+                                wrapMode: Text.Wrap
+                                color: "white"
+
+                                MouseArea {
+                                    anchors.fill: parent;
+
+                                    onClicked: {
+                                        Qt.openUrlExternally(model.value)
+                                    }
+                                }
+                            }
+                        }
+
+                        Item {
+                            Layout.fillWidth: true
+                            visible: model.type === 'messageEnd'
+                        }
+                    }
                 }
             }
         }
@@ -436,7 +497,7 @@ Rectangle {
 
         Rectangle{
             property int index: delegateIndex
-            property string texttest: delegateText
+            property var texttest: delegateText
             property string username: delegateUsername
             property string date: delegateDate
             color: "#171717"
@@ -517,8 +578,6 @@ Rectangle {
         channel = getChannel(channel)
 
         // Format content
-        message = formatContent(message);
-        message = embedImages(message);
 
         if (type === "notification"){
             channel.append({ text: message, date: date, type: "notification" });
@@ -529,23 +588,24 @@ Rectangle {
             return;
         }
 
-        var current_time = new Date();
-        var elapsed_time = current_time - last_message_time;
-        var elapsed_minutes = elapsed_time / (1000 * 60); 
+        // TODO: Replace new time generation with time pregenerated from message
+        // 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);
+        // 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 = "<br>" + message 
-            last_item.text = last_item.text += "\n" + message;
-            load_scroll_timer.running = true;
-            last_message_time = new Date();
-            return;
-        }
+        // if (last_message_user === username && elapsed_minutes < 1 && last_item){
+        //     message = "<br>" + message 
+        //     last_item.text = last_item.text += "\n" + message;
+        //     load_scroll_timer.running = true;
+        //     last_message_time = new Date();
+        //     return;
+        // }
 
-        last_message_user = username;
-        last_message_time = new Date();
+        // last_message_user = username;
+        // last_message_time = new Date();
         channel.append({ text: message, username: username, date: date, type: type });
         load_scroll_timer.running = true;
     }
@@ -554,37 +614,6 @@ Rectangle {
         return channels[id];
     }
 
-    function formatContent(mess) {
-        var arrow = /\</gi
-        mess = mess.replace(arrow, "&lt;");
-
-        var link = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g;
-        mess = mess.replace(link, (match) => {return `<a style="color:#4EBAFD" onclick='Window.openUrl("+match+")' href='` + match + `'>` + match + `</a> <a onclick='Window.openUrl(`+match+`)'>🗗</a>`});
-
-        var newline = /\n/gi;
-        mess = mess.replace(newline, "<br>");
-        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 += "<br><img src="+ matches[i] +" width='250' >"
-                listed.push(matches[i]);
-                total_emeds++
-            } 
-        }
-        return new_message;
-    }
-
     // Messages from script
     function fromScript(message) {
 

From 9174ffa4d195bf59da5ad70408966764f2db5d8f Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Sat, 30 Nov 2024 09:29:08 -0600
Subject: [PATCH 05/49] World links.

---
 scripts/system/domainChat/domainChat.js  |  4 +-
 scripts/system/domainChat/domainChat.qml | 71 ++++++++++++++++++++----
 2 files changed, 61 insertions(+), 14 deletions(-)

diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js
index 4e62afd061..6d2809f782 100644
--- a/scripts/system/domainChat/domainChat.js
+++ b/scripts/system/domainChat/domainChat.js
@@ -117,7 +117,7 @@
         let formattedMessagePacket = { ...message };
         formattedMessagePacket.message = formattedMessage
 
-        _emitEvent({ type: "show_message", ...formattedMessagePacket });           // Update qml view of to new message.
+        _emitEvent({ type: "show_message", ...formattedMessagePacket });        // Update qml view of to new message.
         _notificationCoreMessage(message.displayName, message.message)          // Show a new message on screen.
 
         // Create a new variable based on the message that will be saved.
@@ -290,7 +290,7 @@
     function _parseMessage(message){
         const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;
         const mentionRegex = /@(\w+)/; // FIXME: Remove - devcode
-        const overteLocationRegex = null;
+        const overteLocationRegex = /hifi:\/\/[a-zA-Z0-9_-]+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+/;
 
         let runningMessage = message;
         let messageArray = [];
diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml
index 405e5d5ea9..46340d233a 100644
--- a/scripts/system/domainChat/domainChat.qml
+++ b/scripts/system/domainChat/domainChat.qml
@@ -414,6 +414,7 @@ Rectangle {
                 anchors.top: parent.children[0].bottom;
                 width: parent.width * 0.8
                 x: 5
+                id: messageBoxFlow
 
                 Repeater {
                     model: texttest;
@@ -432,7 +433,7 @@ Rectangle {
                             text: model.value || ""
                             font.pointSize: 12
                             wrapMode: Text.Wrap
-                            width: Math.min(parent.parent.parent.width, contentWidth);
+                            width: Math.min(messageBoxFlow.width, contentWidth);
 
                             visible: model.type === 'text' || model.type === 'mention';
 
@@ -447,41 +448,87 @@ Rectangle {
                         }
 
                         RowLayout {
-                            width: Math.min(parent.parent.parent.width, children[0].contentWidth);
+                            width: Math.min(messageBoxFlow.width, children[0].contentWidth);
                             visible: model.type === 'url';
 
                             Text {
-                                text: model.value || ""
-                                font.pointSize: 12
-                                wrapMode: Text.Wrap
+                                text: model.value || "";
+                                font.pointSize: 12;
+                                wrapMode: Text.Wrap;
                                 color: "#4EBAFD";
-                                font.underline: true
+                                font.underline: true;
+                                width: parent.width;
 
                                 MouseArea {
                                     anchors.fill: parent;
 
                                     onClicked: {
-                                        Window.openWebBrowser(model.value)
+                                        Window.openWebBrowser(model.value);
                                     }
                                 }
                             }
 
                             Text {
-                                text: "🗗"
-                                font.pointSize: 10
-                                wrapMode: Text.Wrap
-                                color: "white"
+                                text: "🗗";
+                                font.pointSize: 10;
+                                wrapMode: Text.Wrap;
+                                color: "white";
 
                                 MouseArea {
                                     anchors.fill: parent;
 
                                     onClicked: {
-                                        Qt.openUrlExternally(model.value)
+                                        Qt.openUrlExternally(model.value);
                                     }
                                 }
                             }
                         }
 
+                        RowLayout {
+                            visible: model.type === 'overteLocation';
+                            width: Math.min(messageBoxFlow.width, children[0].children[1].contentWidth + 35);
+                            height: 20;
+                            Layout.leftMargin: 5
+                            Layout.rightMargin: 5
+
+                            Rectangle {
+                                width: parent.width;
+                                height: 20;
+                                color: "lightgray"
+                                radius: 2;
+
+                                Image {
+                                    source: "./img/ui/world_black.png"
+                                    width: 18;
+                                    height: 18;
+                                    sourceSize.width: 18
+                                    sourceSize.height: 18
+                                    anchors.left: parent.left
+                                    anchors.verticalCenter: parent.verticalCenter 
+                                    anchors.leftMargin: 2
+                                    anchors.rightMargin: 10
+                                }
+
+                                Text {
+                                    text: model.value.split('hifi://')[1].split('/')[0];
+                                    color: "black"
+                                    font.pointSize: 12
+                                    x: parent.children[0].width + 5;
+                                    anchors.verticalCenter: parent.verticalCenter 
+                                }
+
+                                MouseArea {
+                                    anchors.fill: parent;
+
+                                    onClicked: {
+                                        Window.openUrl(model.value);
+                                    }
+                                }
+
+                            }
+                        }
+
+
                         Item {
                             Layout.fillWidth: true
                             visible: model.type === 'messageEnd'

From e470cb23b44ff45d9c713b7c0303b6d4c64b36f9 Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Tue, 17 Dec 2024 18:13:49 -0600
Subject: [PATCH 06/49] Message media embedding.

---
 scripts/system/domainChat/domainChat.js  | 60 +++++++++++++++++++-----
 scripts/system/domainChat/domainChat.qml | 15 +++++-
 2 files changed, 63 insertions(+), 12 deletions(-)

diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js
index 6d2809f782..34cd128bce 100644
--- a/scripts/system/domainChat/domainChat.js
+++ b/scripts/system/domainChat/domainChat.js
@@ -93,7 +93,7 @@
         chatOverlayWindow.fromQml.connect(fromQML);
         quickMessage.fromQml.connect(fromQML);
     }
-    function receivedMessage(channel, message) {
+    async function receivedMessage(channel, message) {
         // Is the message a chat message?
         channel = channel.toLowerCase();
         if (channel !== "chat") return;
@@ -113,9 +113,8 @@
         message.timeString = timeArray[0];
         message.dateString = timeArray[1];
 
-        let formattedMessage = _parseMessage(message.message);                  // Format the message for viewing
         let formattedMessagePacket = { ...message };
-        formattedMessagePacket.message = formattedMessage
+        formattedMessagePacket.message = await _parseMessage(message.message)
 
         _emitEvent({ type: "show_message", ...formattedMessagePacket });        // Update qml view of to new message.
         _notificationCoreMessage(message.displayName, message.message)          // Show a new message on screen.
@@ -237,23 +236,22 @@
             _emitEvent({ type: "notification", ...message });
         }, 1500);
     }
-    function _loadSettings() {
+    async function _loadSettings() {
         settings = Settings.getValue("ArmoredChat-Config", settings);
 
         if (messageHistory) {
             // Load message history
-            messageHistory.forEach((message) => {
+            for (message of messageHistory) {
                 const timeArray = _formatTimestamp(_getTimestamp());
                 messagePacket = { ...message };
                 messagePacket.timeString = timeArray[0];
                 messagePacket.dateString = timeArray[1];
 
-                let formattedMessage = _parseMessage(messagePacket.message);
-                let formattedMessagePacket = messagePacket;
-                formattedMessagePacket.message = formattedMessage;
+                let formattedMessagePacket = {...messagePacket};
+                formattedMessagePacket.message = await _parseMessage(messagePacket.message);
 
                 _emitEvent({ type: "show_message", ...formattedMessagePacket });
-            });
+            }
         }
 
         // Send current settings to the app
@@ -287,7 +285,7 @@
             JSON.stringify({ sender: displayName, text: message })
         );
     }
-    function _parseMessage(message){
+    async function _parseMessage(message){
         const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;
         const mentionRegex = /@(\w+)/; // FIXME: Remove - devcode
         const overteLocationRegex = /hifi:\/\/[a-zA-Z0-9_-]+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+/;
@@ -308,7 +306,9 @@
 
             if (firstMatch == null) {
                 // If there was not any matches found in the entire message, format the whole message as a single text entry.
-                messageArray.push({type: 'text', value: runningMessage});
+                if (messageArray.length == 0) {
+                    messageArray.push({type: 'text', value: runningMessage});
+                }
 
                 // Append a final 'fill width' to the message text.
                 messageArray.push({type: 'messageEnd'});
@@ -318,6 +318,20 @@
             _formatMessage(firstMatch);
         }
 
+        for (dataChunk of messageArray){
+            if (dataChunk.type == 'url'){
+                let url = dataChunk.value;
+
+                const res = await fetch(url, {method: 'GET'});      // TODO: Replace with 'HEAD' method. https://github.com/overte-org/overte/issues/1273
+                const contentType = res.getResponseHeader("content-type");
+
+                // TODO: Add support for other media types
+                if (contentType.startsWith('image/')) {
+                    messageArray.push({type: 'imageEmbed', value: url});
+                }
+            }
+        }
+
         return messageArray;
 
         function _formatMessage(firstMatch){
@@ -360,4 +374,28 @@
     function _emitEvent(packet = { type: "" }) {
         chatOverlayWindow.sendToQml(packet);
     }
+
+    function fetch(url, options = {method: "GET"}) {
+        return new Promise((resolve, reject) => {
+            let req = new XMLHttpRequest();
+
+            req.onreadystatechange = function () {
+
+                if (req.readyState === req.DONE) {
+                    if (req.status === 200) {
+                        console.log("Content type:", req.getResponseHeader("content-type"));
+                        resolve(req);
+            
+                    } else {
+                        console.log("Error", req.status, req.statusText);
+                        reject();
+                    }
+                }
+            };
+
+            req.open(options.method, url);
+            req.send();
+        });
+    }
+
 })();
diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml
index 46340d233a..f0350e972a 100644
--- a/scripts/system/domainChat/domainChat.qml
+++ b/scripts/system/domainChat/domainChat.qml
@@ -510,7 +510,7 @@ Rectangle {
                                 }
 
                                 Text {
-                                    text: model.value.split('hifi://')[1].split('/')[0];
+                                    text: model.type === 'overteLocation' ? model.value.split('hifi://')[1].split('/')[0] : '';
                                     color: "black"
                                     font.pointSize: 12
                                     x: parent.children[0].width + 5;
@@ -533,6 +533,18 @@ Rectangle {
                             Layout.fillWidth: true
                             visible: model.type === 'messageEnd'
                         }
+
+                        Item {
+                            visible: model.type === 'imageEmbed';
+                            width: messageBoxFlow.width;
+                            height: 200
+
+                            Image {
+                                source: model.type === 'imageEmbed' ? model.value : ''
+                                sourceSize.width: 400
+                                sourceSize.height: 200
+                            }
+                        }
                     }
                 }
             }
@@ -653,6 +665,7 @@ Rectangle {
 
         // last_message_user = username;
         // last_message_time = new Date();
+
         channel.append({ text: message, username: username, date: date, type: type });
         load_scroll_timer.running = true;
     }

From 353f7ec30d8de5e03582ff2d0a2ac3dafeeaa34d Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Wed, 18 Dec 2024 05:04:23 -0600
Subject: [PATCH 07/49] Fix domain notifications.

---
 scripts/system/domainChat/domainChat.qml | 76 ++++++------------------
 1 file changed, 18 insertions(+), 58 deletions(-)

diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml
index f0350e972a..2220127f98 100644
--- a/scripts/system/domainChat/domainChat.qml
+++ b/scripts/system/domainChat/domainChat.qml
@@ -167,11 +167,8 @@ Rectangle {
                             property string delegateDate: model.date
 
                             sourceComponent: {
-                                if (model.type === "chat") {
-                                    return template_chat_message;
-                                } else if (model.type === "notification") {
-                                    return template_notification;
-                                }
+                                if (model.type === "chat") return template_chat_message;
+                                if (model.type === "notification") return template_notification;
                             }
                         
                         }
@@ -384,9 +381,7 @@ Rectangle {
 
         Rectangle {
             property int index: delegateIndex
-            property var 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)
@@ -405,7 +400,7 @@ Rectangle {
 
                 Text{
                     anchors.right: parent.right
-                    text: date
+                    text: delegateDate
                     color: "lightgray"
                 }
             }
@@ -417,24 +412,14 @@ Rectangle {
                 id: messageBoxFlow
 
                 Repeater {
-                    model: texttest;
+                    model: delegateText;
 
                     RowLayout {
-                        width: {
-                            switch (model.type) {
-                                case "text":
-                                    return children[0].width;
-                                case "url":
-                                    return children[1].width;
-                            }
-                        }
-
                         Text {
                             text: model.value || ""
                             font.pointSize: 12
                             wrapMode: Text.Wrap
-                            width: Math.min(messageBoxFlow.width, contentWidth);
-
+                            width: model.type === 'text' || model.type === 'mention' ? Math.min(messageBoxFlow.width, contentWidth) : 0;
                             visible: model.type === 'text' || model.type === 'mention';
 
                             color: {
@@ -448,7 +433,7 @@ Rectangle {
                         }
 
                         RowLayout {
-                            width: Math.min(messageBoxFlow.width, children[0].contentWidth);
+                            width: children[0].contentWidth;
                             visible: model.type === 'url';
 
                             Text {
@@ -524,11 +509,9 @@ Rectangle {
                                         Window.openUrl(model.value);
                                     }
                                 }
-
                             }
                         }
 
-
                         Item {
                             Layout.fillWidth: true
                             visible: model.type === 'messageEnd'
@@ -554,11 +537,10 @@ Rectangle {
     Component {
         id: template_notification
 
-        Rectangle{
+        Rectangle {
             property int index: delegateIndex
-            property var texttest: delegateText
             property string username: delegateUsername
-            property string date: delegateDate
+
             color: "#171717"
             width: parent.width
             height: 40
@@ -574,15 +556,14 @@ Rectangle {
                 }
             }
 
-
             Item {
                 width: parent.width - parent.children[0].width - 5
                 height: parent.height
                 anchors.left: parent.children[0].right
 
-                TextEdit{
-                    text: texttest
-                    color:"white"
+                TextEdit {
+                    text: delegateText
+                    color: "white"
                     font.pointSize: 12
                     readOnly: true
                     width: parent.width * 0.8
@@ -595,8 +576,8 @@ Rectangle {
                 }
 
                 Text {
-                    text: date
-                    color:"white"
+                    text: delegateDate
+                    color: "white"
                     font.pointSize: 12
                     anchors.right: parent.right
                     height: parent.height
@@ -617,16 +598,16 @@ Rectangle {
     }
 
     function scrollToBottom(bypassDistanceCheck = false, extraMoveDistance = 0) {
-        const totalHeight = listview.height; // Total height of the content
-        const currentPosition = messageViewFlickable.contentY; // Current position of the view
-        const windowHeight = listview.parent.parent.height; // Total height of the window
+        const totalHeight = listview.height;                    // Total height of the content
+        const currentPosition = messageViewFlickable.contentY;  // Current position of the view
+        const windowHeight = listview.parent.parent.height;     // Total height of the window
         const bottomPosition = currentPosition + windowHeight;
 
         // Check if the view is within 300 units from the bottom
         const closeEnoughToBottom = totalHeight - bottomPosition <= 300;
         if (!bypassDistanceCheck && !closeEnoughToBottom) return;
-        if (totalHeight < windowHeight) return; // No reason to scroll, we don't have an overflow.
-        if (bottomPosition == totalHeight) return; // At the bottom, do nothing.
+        if (totalHeight < windowHeight) return;                 // No reason to scroll, we don't have an overflow.
+        if (bottomPosition == totalHeight) return;              // At the bottom, do nothing.
 
         messageViewFlickable.contentY = listview.height - listview.parent.parent.height;
         messageViewFlickable.returnToBounds();
@@ -640,32 +621,11 @@ Rectangle {
 
         if (type === "notification"){
             channel.append({ text: message, date: date, type: "notification" });
-            last_message_user = "";
             scrollToBottom(null, 30);
 
-            last_message_time = new Date();
             return;
         }
 
-        // TODO: Replace new time generation with time pregenerated from message
-        // 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 = "<br>" + message 
-        //     last_item.text = last_item.text += "\n" + message;
-        //     load_scroll_timer.running = true;
-        //     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 });
         load_scroll_timer.running = true;
     }

From 392a1446a9faa26042445c6ab54262eb2ff13c02 Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Wed, 18 Dec 2024 17:38:52 -0600
Subject: [PATCH 08/49] Broke out templates into their own files.

---
 scripts/system/domainChat/domainChat.qml      | 218 +-----------------
 .../qml_widgets/TemplateChatMessage.qml       | 161 +++++++++++++
 .../qml_widgets/TemplateNotification.qml      |  59 +++++
 3 files changed, 223 insertions(+), 215 deletions(-)
 create mode 100644 scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
 create mode 100644 scripts/system/domainChat/qml_widgets/TemplateNotification.qml

diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml
index 2220127f98..f40121ee71 100644
--- a/scripts/system/domainChat/domainChat.qml
+++ b/scripts/system/domainChat/domainChat.qml
@@ -2,6 +2,7 @@ import QtQuick 2.7
 import QtQuick.Controls 2.15
 import QtQuick.Layouts 1.3
 import controlsUit 1.0 as HifiControlsUit
+import "./qml_widgets"
 
 Rectangle {
     color: Qt.rgba(0.1,0.1,0.1,1)
@@ -376,221 +377,8 @@ Rectangle {
     }
 
     // Templates
-    Component {
-        id: template_chat_message
-
-        Rectangle {
-            property int index: delegateIndex
-            property string username: delegateUsername
-
-            height: Math.max(65, children[1].height + 30)
-            color: index % 2 === 0 ? "transparent" : Qt.rgba(0.15,0.15,0.15,1)
-            width: listview.parent.parent.width
-            Layout.fillWidth: true
-
-            Item {
-                width: parent.width - 10
-                anchors.horizontalCenter: parent.horizontalCenter
-                height: 22
-
-                Text{
-                    text: username
-                    color: "lightgray"
-                }
-
-                Text{
-                    anchors.right: parent.right
-                    text: delegateDate
-                    color: "lightgray"
-                }
-            }
-
-            Flow {
-                anchors.top: parent.children[0].bottom;
-                width: parent.width * 0.8
-                x: 5
-                id: messageBoxFlow
-
-                Repeater {
-                    model: delegateText;
-
-                    RowLayout {
-                        Text {
-                            text: model.value || ""
-                            font.pointSize: 12
-                            wrapMode: Text.Wrap
-                            width: model.type === 'text' || model.type === 'mention' ? Math.min(messageBoxFlow.width, contentWidth) : 0;
-                            visible: model.type === 'text' || model.type === 'mention';
-
-                            color: {
-                                switch (model.type) {
-                                    case "mention":
-                                        return "purple";
-                                    default:
-                                        return "white";
-                                }
-                            }
-                        }
-
-                        RowLayout {
-                            width: children[0].contentWidth;
-                            visible: model.type === 'url';
-
-                            Text {
-                                text: model.value || "";
-                                font.pointSize: 12;
-                                wrapMode: Text.Wrap;
-                                color: "#4EBAFD";
-                                font.underline: true;
-                                width: parent.width;
-
-                                MouseArea {
-                                    anchors.fill: parent;
-
-                                    onClicked: {
-                                        Window.openWebBrowser(model.value);
-                                    }
-                                }
-                            }
-
-                            Text {
-                                text: "🗗";
-                                font.pointSize: 10;
-                                wrapMode: Text.Wrap;
-                                color: "white";
-
-                                MouseArea {
-                                    anchors.fill: parent;
-
-                                    onClicked: {
-                                        Qt.openUrlExternally(model.value);
-                                    }
-                                }
-                            }
-                        }
-
-                        RowLayout {
-                            visible: model.type === 'overteLocation';
-                            width: Math.min(messageBoxFlow.width, children[0].children[1].contentWidth + 35);
-                            height: 20;
-                            Layout.leftMargin: 5
-                            Layout.rightMargin: 5
-
-                            Rectangle {
-                                width: parent.width;
-                                height: 20;
-                                color: "lightgray"
-                                radius: 2;
-
-                                Image {
-                                    source: "./img/ui/world_black.png"
-                                    width: 18;
-                                    height: 18;
-                                    sourceSize.width: 18
-                                    sourceSize.height: 18
-                                    anchors.left: parent.left
-                                    anchors.verticalCenter: parent.verticalCenter 
-                                    anchors.leftMargin: 2
-                                    anchors.rightMargin: 10
-                                }
-
-                                Text {
-                                    text: model.type === 'overteLocation' ? model.value.split('hifi://')[1].split('/')[0] : '';
-                                    color: "black"
-                                    font.pointSize: 12
-                                    x: parent.children[0].width + 5;
-                                    anchors.verticalCenter: parent.verticalCenter 
-                                }
-
-                                MouseArea {
-                                    anchors.fill: parent;
-
-                                    onClicked: {
-                                        Window.openUrl(model.value);
-                                    }
-                                }
-                            }
-                        }
-
-                        Item {
-                            Layout.fillWidth: true
-                            visible: model.type === 'messageEnd'
-                        }
-
-                        Item {
-                            visible: model.type === 'imageEmbed';
-                            width: messageBoxFlow.width;
-                            height: 200
-
-                            Image {
-                                source: model.type === 'imageEmbed' ? model.value : ''
-                                sourceSize.width: 400
-                                sourceSize.height: 200
-                            }
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    Component {
-        id: template_notification
-
-        Rectangle {
-            property int index: delegateIndex
-            property string username: delegateUsername
-
-            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: delegateText
-                    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: delegateDate
-                    color: "white"
-                    font.pointSize: 12
-                    anchors.right: parent.right
-                    height: parent.height
-                    wrapMode: Text.Wrap
-                    horizontalAlignment: Text.AlignRight
-                    verticalAlignment: Text.AlignVCenter
-                    font.italic: true
-                }
-            }
-
-        }
-
-    }
+    TemplateChatMessage { id: template_chat_message }
+    TemplateNotification { id: template_notification }
 
     property var channels: {
         "local": local,
diff --git a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
new file mode 100644
index 0000000000..4165e8d37c
--- /dev/null
+++ b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
@@ -0,0 +1,161 @@
+import QtQuick 2.7
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.3
+
+Component {
+	id: template_chat_message
+
+	Rectangle {
+		property int index: delegateIndex
+		property string username: delegateUsername
+
+		height: Math.max(65, children[1].height + 30)
+		color: index % 2 === 0 ? "transparent" : Qt.rgba(0.15,0.15,0.15,1)
+		width: listview.parent.parent.width
+		Layout.fillWidth: true
+
+		Item {
+			width: parent.width - 10
+			anchors.horizontalCenter: parent.horizontalCenter
+			height: 22
+
+			Text{
+				text: username
+				color: "lightgray"
+			}
+
+			Text{
+				anchors.right: parent.right
+				text: delegateDate
+				color: "lightgray"
+			}
+		}
+
+		Flow {
+			anchors.top: parent.children[0].bottom;
+			width: parent.width * 0.8
+			x: 5
+			id: messageBoxFlow
+
+			Repeater {
+				model: delegateText;
+
+				RowLayout {
+					Text {
+						text: model.value || ""
+						font.pointSize: 12
+						wrapMode: Text.Wrap
+						width: model.type === 'text' || model.type === 'mention' ? Math.min(messageBoxFlow.width, contentWidth) : 0;
+						visible: model.type === 'text' || model.type === 'mention';
+
+						color: {
+							switch (model.type) {
+								case "mention":
+									return "purple";
+								default:
+									return "white";
+							}
+						}
+					}
+
+					RowLayout {
+						width: children[0].contentWidth;
+						visible: model.type === 'url';
+
+						Text {
+							text: model.value || "";
+							font.pointSize: 12;
+							wrapMode: Text.Wrap;
+							color: "#4EBAFD";
+							font.underline: true;
+							width: parent.width;
+
+							MouseArea {
+								anchors.fill: parent;
+
+								onClicked: {
+									Window.openWebBrowser(model.value);
+								}
+							}
+						}
+
+						Text {
+							text: "🗗";
+							font.pointSize: 10;
+							wrapMode: Text.Wrap;
+							color: "white";
+
+							MouseArea {
+								anchors.fill: parent;
+
+								onClicked: {
+									Qt.openUrlExternally(model.value);
+								}
+							}
+						}
+					}
+
+					RowLayout {
+						visible: model.type === 'overteLocation';
+						width: Math.min(messageBoxFlow.width, children[0].children[1].contentWidth + 35);
+						height: 20;
+						Layout.leftMargin: 5
+						Layout.rightMargin: 5
+
+						Rectangle {
+							width: parent.width;
+							height: 20;
+							color: "lightgray"
+							radius: 2;
+
+							Image {
+								source: "./img/ui/world_black.png"
+								width: 18;
+								height: 18;
+								sourceSize.width: 18
+								sourceSize.height: 18
+								anchors.left: parent.left
+								anchors.verticalCenter: parent.verticalCenter 
+								anchors.leftMargin: 2
+								anchors.rightMargin: 10
+							}
+
+							Text {
+								text: model.type === 'overteLocation' ? model.value.split('hifi://')[1].split('/')[0] : '';
+								color: "black"
+								font.pointSize: 12
+								x: parent.children[0].width + 5;
+								anchors.verticalCenter: parent.verticalCenter 
+							}
+
+							MouseArea {
+								anchors.fill: parent;
+
+								onClicked: {
+									Window.openUrl(model.value);
+								}
+							}
+						}
+					}
+
+					Item {
+						Layout.fillWidth: true
+						visible: model.type === 'messageEnd'
+					}
+
+					Item {
+						visible: model.type === 'imageEmbed';
+						width: messageBoxFlow.width;
+						height: 200
+
+						Image {
+							source: model.type === 'imageEmbed' ? model.value : ''
+							sourceSize.width: 400
+							sourceSize.height: 200
+						}
+					}
+				}
+			}
+		}
+	}
+}
\ No newline at end of file
diff --git a/scripts/system/domainChat/qml_widgets/TemplateNotification.qml b/scripts/system/domainChat/qml_widgets/TemplateNotification.qml
new file mode 100644
index 0000000000..e522d58c54
--- /dev/null
+++ b/scripts/system/domainChat/qml_widgets/TemplateNotification.qml
@@ -0,0 +1,59 @@
+import QtQuick 2.7
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.3
+
+Component {
+	id: template_notification
+
+	Rectangle {
+		property int index: delegateIndex
+		property string username: delegateUsername
+
+		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: delegateText
+				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: delegateDate
+				color: "white"
+				font.pointSize: 12
+				anchors.right: parent.right
+				height: parent.height
+				wrapMode: Text.Wrap
+				horizontalAlignment: Text.AlignRight
+				verticalAlignment: Text.AlignVCenter
+				font.italic: true
+			}
+		}
+	}
+}
\ No newline at end of file

From 60284015c26f6dc58da27f79fbbd46c3ed489286 Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Thu, 19 Dec 2024 04:41:49 -0600
Subject: [PATCH 09/49] Actually fix notifications.

---
 scripts/system/domainChat/domainChat.js       | 16 +++---
 scripts/system/domainChat/domainChat.qml      | 10 ++--
 .../qml_widgets/TemplateChatMessage.qml       |  9 ++--
 .../qml_widgets/TemplateNotification.qml      | 52 ++++++-------------
 4 files changed, 32 insertions(+), 55 deletions(-)

diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js
index 34cd128bce..2d80688d22 100644
--- a/scripts/system/domainChat/domainChat.js
+++ b/scripts/system/domainChat/domainChat.js
@@ -32,12 +32,8 @@
     Controller.keyPressEvent.connect(keyPressEvent);
     Messages.subscribe("chat");
     Messages.messageReceived.connect(receivedMessage);
-    AvatarManager.avatarAddedEvent.connect((sessionId) => {
-        _avatarAction("connected", sessionId);
-    });
-    AvatarManager.avatarRemovedEvent.connect((sessionId) => {
-        _avatarAction("left", sessionId);
-    });
+    AvatarManager.avatarAddedEvent.connect((sessionId) => { _avatarAction("connected", sessionId); });
+    AvatarManager.avatarRemovedEvent.connect((sessionId) => { _avatarAction("left", sessionId); });
 
     startup();
 
@@ -205,7 +201,7 @@
         );
     }
     function _avatarAction(type, sessionId) {
-        Script.setTimeout(() => {
+        Script.setTimeout(async () => {
             if (type == "connected") {
                 palData = AvatarManager.getPalData().data;
             }
@@ -233,7 +229,11 @@
                 _notificationCoreMessage(displayName, type)
             }
 
-            _emitEvent({ type: "notification", ...message });
+            // Format notification message
+            let formattedMessagePacket = {...message};
+            formattedMessagePacket.message = await _parseMessage(message.message);
+
+            _emitEvent({ type: "notification", ...formattedMessagePacket });
         }, 1500);
     }
     async function _loadSettings() {
diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml
index f40121ee71..6a79cb7e2e 100644
--- a/scripts/system/domainChat/domainChat.qml
+++ b/scripts/system/domainChat/domainChat.qml
@@ -9,7 +9,6 @@ Rectangle {
     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.
@@ -163,7 +162,7 @@ Rectangle {
                         model: getChannel(pageVal)
                         delegate: Loader {
                             property int delegateIndex: model.index
-                            property var delegateText: model.text
+                            property var delegateText: model.message
                             property string delegateUsername: model.username
                             property string delegateDate: model.date
 
@@ -171,7 +170,6 @@ Rectangle {
                                 if (model.type === "chat") return template_chat_message;
                                 if (model.type === "notification") return template_notification;
                             }
-                        
                         }
                     }
                 }
@@ -406,15 +404,13 @@ Rectangle {
         channel = getChannel(channel)
 
         // Format content
-
         if (type === "notification"){
-            channel.append({ text: message, date: date, type: "notification" });
+            channel.append({ message: message, date: date, type: "notification" });
             scrollToBottom(null, 30);
-
             return;
         }
 
-        channel.append({ text: message, username: username, date: date, type: type });
+        channel.append({ message: message, username: username, date: date, type: type });
         load_scroll_timer.running = true;
     }
 
diff --git a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
index 4165e8d37c..a9c366ce76 100644
--- a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
+++ b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
@@ -7,7 +7,6 @@ Component {
 
 	Rectangle {
 		property int index: delegateIndex
-		property string username: delegateUsername
 
 		height: Math.max(65, children[1].height + 30)
 		color: index % 2 === 0 ? "transparent" : Qt.rgba(0.15,0.15,0.15,1)
@@ -19,12 +18,12 @@ Component {
 			anchors.horizontalCenter: parent.horizontalCenter
 			height: 22
 
-			Text{
-				text: username
+			Text {
+				text: delegateUsername
 				color: "lightgray"
 			}
 
-			Text{
+			Text {
 				anchors.right: parent.right
 				text: delegateDate
 				color: "lightgray"
@@ -109,7 +108,7 @@ Component {
 							radius: 2;
 
 							Image {
-								source: "./img/ui/world_black.png"
+								source: "../img/ui/world_black.png"
 								width: 18;
 								height: 18;
 								sourceSize.width: 18
diff --git a/scripts/system/domainChat/qml_widgets/TemplateNotification.qml b/scripts/system/domainChat/qml_widgets/TemplateNotification.qml
index e522d58c54..4b9797d7f4 100644
--- a/scripts/system/domainChat/qml_widgets/TemplateNotification.qml
+++ b/scripts/system/domainChat/qml_widgets/TemplateNotification.qml
@@ -6,15 +6,12 @@ Component {
 	id: template_notification
 
 	Rectangle {
-		property int index: delegateIndex
-		property string username: delegateUsername
-
 		color: "#171717"
 		width: parent.width
 		height: 40
 
-		Item {
-			width: 10
+		RowLayout {
+			width: parent.width
 			height: parent.height
 
 			Rectangle {
@@ -22,38 +19,23 @@ Component {
 				width: 5
 				color: "#505186"
 			}
-		}
 
-		Item {
-			width: parent.width - parent.children[0].width - 5
-			height: parent.height
-			anchors.left: parent.children[0].right
+			Repeater {
+				model: delegateText
 
-			TextEdit {
-				text: delegateText
-				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: delegateDate
-				color: "white"
-				font.pointSize: 12
-				anchors.right: parent.right
-				height: parent.height
-				wrapMode: Text.Wrap
-				horizontalAlignment: Text.AlignRight
-				verticalAlignment: Text.AlignVCenter
-				font.italic: true
+				TextEdit {
+					visible: model.value != undefined;
+					text: model.value || ""
+					color: "white"
+					font.pointSize: 12
+					readOnly: true
+					selectByMouse: true
+					selectByKeyboard: true
+					height: root.height
+					wrapMode: Text.Wrap
+					font.italic: true
+				}
 			}
 		}
 	}
-}
\ No newline at end of file
+}

From 6dbd63ebe9c5444e5e3e3bce39b0c5cb5ab1a407 Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Thu, 19 Dec 2024 04:48:56 -0600
Subject: [PATCH 10/49] Fix formatting sometimes not formatting the last part
 of a message.

---
 scripts/system/domainChat/domainChat.js | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js
index 2d80688d22..08be9822d9 100644
--- a/scripts/system/domainChat/domainChat.js
+++ b/scripts/system/domainChat/domainChat.js
@@ -305,10 +305,8 @@
             let firstMatch = _findFirstMatch();
 
             if (firstMatch == null) {
-                // If there was not any matches found in the entire message, format the whole message as a single text entry.
-                if (messageArray.length == 0) {
-                    messageArray.push({type: 'text', value: runningMessage});
-                }
+                // Format any remaining text as a basic 'text' type.
+                messageArray.push({type: 'text', value: runningMessage});
 
                 // Append a final 'fill width' to the message text.
                 messageArray.push({type: 'messageEnd'});

From 27fa5da04883b0560141c57a4a72b4ed3f358c2e Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Thu, 19 Dec 2024 21:48:05 -0600
Subject: [PATCH 11/49] Housekeeping: data formatting

---
 scripts/system/domainChat/domainChat.js | 72 +++++++------------------
 scripts/system/domainChat/formatting.js | 64 ++++++++++++++++++++++
 2 files changed, 82 insertions(+), 54 deletions(-)
 create mode 100644 scripts/system/domainChat/formatting.js

diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js
index 08be9822d9..03aeadd789 100644
--- a/scripts/system/domainChat/domainChat.js
+++ b/scripts/system/domainChat/domainChat.js
@@ -12,6 +12,10 @@
 (() => {
     ("use strict");
 
+    Script.include([
+        "./formatting.js"
+    ])
+
     var appIsVisible = false;
     var settings = {
         external_window: false,
@@ -93,22 +97,16 @@
         // Is the message a chat message?
         channel = channel.toLowerCase();
         if (channel !== "chat") return;
-        message = JSON.parse(message);
-
-        // Get the message data
-        const currentTimestamp = _getTimestamp();
-        const timeArray = _formatTimestamp(currentTimestamp);
-
+        
+        if ((message = formatting.toJSON(message)) == null) return;             // Make sure we are working with a JSON object we expect, otherwise kill
+        message = formatting.addTimeAndDateStringToPacket(message);  
+        
         if (!message.channel) message.channel = "domain";                       // We don't know where to put this message. Assume it is a domain wide message.
         message.channel = message.channel.toLowerCase();                        // Only recognize channel names as lower case. 
         
         if (!channels.includes(message.channel)) return;                        // Check the channel. If the channel is not one we have, do nothing.
         if (message.channel == "local" && isTooFar(message.position)) return;   // If message is local, and if player is too far away from location, do nothing.
-
-        // Format the timestamp 
-        message.timeString = timeArray[0];
-        message.dateString = timeArray[1];
-
+        
         let formattedMessagePacket = { ...message };
         formattedMessagePacket.message = await _parseMessage(message.message)
 
@@ -116,24 +114,16 @@
         _notificationCoreMessage(message.displayName, message.message)          // Show a new message on screen.
 
         // Create a new variable based on the message that will be saved.
-        let savedMessage = message;
+        let trimmedPacket = formatting.trimPacketToSave(message);
+        messageHistory.push(trimmedPacket);
 
-        // Remove unnecessary data.
-        delete savedMessage.position;
-        delete savedMessage.timeString;
-        delete savedMessage.dateString;
-        delete savedMessage.action;
-
-        savedMessage.timestamp = currentTimestamp;
-
-        messageHistory.push(savedMessage);
         while (messageHistory.length > settings.maximum_messages) {
             messageHistory.shift();
         }
         Settings.setValue("ArmoredChat-Messages", messageHistory);
 
-        // Check to see if the message is close enough to the user
         function isTooFar(messagePosition) {
+            // Check to see if the message is close enough to the user
             return Vec3.distance(MyAvatar.position, messagePosition) > maxLocalDistance;
         }
     }
@@ -218,10 +208,7 @@
             }
 
             // Format the packet
-            let message = {};
-            const timeArray = _formatTimestamp(_getTimestamp());
-            message.timeString = timeArray[0];
-            message.dateString = timeArray[1];
+            let message = addTimeAndDateStringToPacket({});
             message.message = `${displayName} ${type}`;
 
             // Show new message on screen
@@ -242,43 +229,20 @@
         if (messageHistory) {
             // Load message history
             for (message of messageHistory) {
-                const timeArray = _formatTimestamp(_getTimestamp());
-                messagePacket = { ...message };
-                messagePacket.timeString = timeArray[0];
-                messagePacket.dateString = timeArray[1];
+                messagePacket = { ...message };                                             // Create new variable
+                messagePacket = formatting.addTimeAndDateStringToPacket(messagePacket);     // Add timestamp
+                messagePacket.message = await _parseMessage(messagePacket.message);         // Parse the message for the UI
 
-                let formattedMessagePacket = {...messagePacket};
-                formattedMessagePacket.message = await _parseMessage(messagePacket.message);
-
-                _emitEvent({ type: "show_message", ...formattedMessagePacket });
+                _emitEvent({ type: "show_message", ...messagePacket });                     // Send message to UI
             }
         }
 
-        // Send current settings to the app
-        _emitEvent({ type: "initial_settings", settings: settings });
+        _emitEvent({ type: "initial_settings", settings: settings });                       // Send current settings to the app
     }
     function _saveSettings() {
         console.log("Saving config");
         Settings.setValue("ArmoredChat-Config", settings);
     }
-    function _getTimestamp(){
-        return Date.now();
-    }
-    function _formatTimestamp(timestamp){
-        let timeArray = [];
-
-        timeArray.push(new Date().toLocaleTimeString(undefined, {
-            hour12: false,
-        }));
-
-        timeArray.push(new Date(timestamp).toLocaleDateString(undefined, {
-                year: "numeric",
-                month: "long",
-                day: "numeric",
-        }));
-
-        return timeArray;
-    }
     function _notificationCoreMessage(displayName, message){
         Messages.sendLocalMessage(
             "Floof-Notif",
diff --git a/scripts/system/domainChat/formatting.js b/scripts/system/domainChat/formatting.js
new file mode 100644
index 0000000000..b523f30d2d
--- /dev/null
+++ b/scripts/system/domainChat/formatting.js
@@ -0,0 +1,64 @@
+//
+//  formatting.js
+//
+//  Created by Armored Dragon, 2024.
+//  Copyright 2024 Overte e.V.
+//
+//	This just does some basic formatting and minor housekeeping for the domainChat.js application
+//
+//  Distributed under the Apache License, Version 2.0.
+//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+
+const formatting = {
+	toJSON: function(data) {
+		if (typeof data == "object") return data; // Already JSON
+		
+		try {
+			const parsedData = JSON.parse(data);
+			return parsedData;
+		} catch (e) {
+			console.log('Failed to convert data to JSON.')
+			return null; // Could not convert to json, some error;
+		}
+	},
+	addTimeAndDateStringToPacket: function(packet) {
+		// Gets the current time and adds it to a given packet
+		const timeArray = formatting.helpers._timestampArray(packet.timestamp);
+		packet.timeString = timeArray[0];
+        packet.dateString = timeArray[1];
+		return packet;
+	},
+	trimPacketToSave: function(packet) {
+		// Takes a packet, and returns a packet containing only what is needed to save.
+		let newPacket = {
+			channel: packet.channel || "",
+			displayName: packet.displayName || "",
+			message: packet.message || "",
+			timestamp: packet.timestamp || formatting.helpers.getTimestamp(),
+		};
+		return newPacket;
+	},
+
+	helpers: {
+		// Small functions that are used often in the other functions.
+		_timestampArray: function(timestamp) {
+			const currentDate = timestamp || formatting.helpers.getTimestamp();
+			let timeArray = [];
+
+			timeArray.push(new Date(currentDate).toLocaleTimeString(undefined, {
+				hour12: false,
+			}));
+	
+			timeArray.push(new Date(currentDate).toLocaleDateString(undefined, {
+				year: "numeric",
+				month: "long",
+				day: "numeric",
+			}));
+	
+			return timeArray;
+		},
+		getTimestamp: function(){
+			return Date.now();
+		}
+	}
+}

From 891d533bd8b8323921e6d74127f2b1172981d026 Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Fri, 20 Dec 2024 05:32:17 -0600
Subject: [PATCH 12/49] Move message parsing to formatting file.

---
 scripts/system/domainChat/domainChat.js | 114 ++----------------------
 scripts/system/domainChat/formatting.js |  98 ++++++++++++++++++++
 2 files changed, 105 insertions(+), 107 deletions(-)

diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js
index 03aeadd789..6d75556081 100644
--- a/scripts/system/domainChat/domainChat.js
+++ b/scripts/system/domainChat/domainChat.js
@@ -108,7 +108,7 @@
         if (message.channel == "local" && isTooFar(message.position)) return;   // If message is local, and if player is too far away from location, do nothing.
         
         let formattedMessagePacket = { ...message };
-        formattedMessagePacket.message = await _parseMessage(message.message)
+        formattedMessagePacket.message = await formatting.parseMessage(message.message)
 
         _emitEvent({ type: "show_message", ...formattedMessagePacket });        // Update qml view of to new message.
         _notificationCoreMessage(message.displayName, message.message)          // Show a new message on screen.
@@ -218,7 +218,7 @@
 
             // Format notification message
             let formattedMessagePacket = {...message};
-            formattedMessagePacket.message = await _parseMessage(message.message);
+            formattedMessagePacket.message = await formatting.parseMessage(message.message);
 
             _emitEvent({ type: "notification", ...formattedMessagePacket });
         }, 1500);
@@ -229,15 +229,15 @@
         if (messageHistory) {
             // Load message history
             for (message of messageHistory) {
-                messagePacket = { ...message };                                             // Create new variable
-                messagePacket = formatting.addTimeAndDateStringToPacket(messagePacket);     // Add timestamp
-                messagePacket.message = await _parseMessage(messagePacket.message);         // Parse the message for the UI
+                messagePacket = { ...message };                                                 // Create new variable
+                messagePacket = formatting.addTimeAndDateStringToPacket(messagePacket);         // Add timestamp
+                messagePacket.message = await formatting.parseMessage(messagePacket.message);   // Parse the message for the UI
 
-                _emitEvent({ type: "show_message", ...messagePacket });                     // Send message to UI
+                _emitEvent({ type: "show_message", ...messagePacket });                         // Send message to UI
             }
         }
 
-        _emitEvent({ type: "initial_settings", settings: settings });                       // Send current settings to the app
+        _emitEvent({ type: "initial_settings", settings: settings });                           // Send current settings to the app
     }
     function _saveSettings() {
         console.log("Saving config");
@@ -249,85 +249,6 @@
             JSON.stringify({ sender: displayName, text: message })
         );
     }
-    async function _parseMessage(message){
-        const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;
-        const mentionRegex = /@(\w+)/; // FIXME: Remove - devcode
-        const overteLocationRegex = /hifi:\/\/[a-zA-Z0-9_-]+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+/;
-
-        let runningMessage = message;
-        let messageArray = [];
-
-        const regexPatterns = [
-            { type: "url", regex: urlRegex },
-            { type: "mention", regex: mentionRegex }, // FIXME: Remove - devcode
-            { type: "overteLocation", regex: overteLocationRegex }
-        ]
-
-        // Here is a link https://www.example.com, #hashtag, and @mention. Just for some spice here is another https://exampletwo.com
-
-        while (true) {
-            let firstMatch = _findFirstMatch();
-
-            if (firstMatch == null) {
-                // Format any remaining text as a basic 'text' type.
-                messageArray.push({type: 'text', value: runningMessage});
-
-                // Append a final 'fill width' to the message text.
-                messageArray.push({type: 'messageEnd'});
-                break;
-            }
-
-            _formatMessage(firstMatch);
-        }
-
-        for (dataChunk of messageArray){
-            if (dataChunk.type == 'url'){
-                let url = dataChunk.value;
-
-                const res = await fetch(url, {method: 'GET'});      // TODO: Replace with 'HEAD' method. https://github.com/overte-org/overte/issues/1273
-                const contentType = res.getResponseHeader("content-type");
-
-                // TODO: Add support for other media types
-                if (contentType.startsWith('image/')) {
-                    messageArray.push({type: 'imageEmbed', value: url});
-                }
-            }
-        }
-
-        return messageArray;
-
-        function _formatMessage(firstMatch){
-            let indexOfFirstMatch = firstMatch[0];
-            let regex = regexPatterns[firstMatch[1]].regex;
-
-            let foundMatch = runningMessage.match(regex)[0];
-
-            messageArray.push({type: 'text', value: runningMessage.substring(0, indexOfFirstMatch)});
-            messageArray.push({type: regexPatterns[firstMatch[1]].type, value: runningMessage.substring(indexOfFirstMatch, indexOfFirstMatch + foundMatch.length)});
-            
-            runningMessage = runningMessage.substring(indexOfFirstMatch + foundMatch.length);   // Remove the part of the message we have worked with
-        }
-
-        function _findFirstMatch(){
-            let indexOfFirstMatch = Infinity;
-            let indexOfRegexPattern = Infinity;
-
-            for (let i = 0; regexPatterns.length > i; i++){
-                let indexOfMatch = runningMessage.search(regexPatterns[i].regex);
-
-                if (indexOfMatch == -1) continue;                                              // No match found
-
-                if (indexOfMatch < indexOfFirstMatch) {
-                    indexOfFirstMatch = indexOfMatch;
-                    indexOfRegexPattern = i;
-                }
-            }
-
-            if (indexOfFirstMatch !== Infinity) return [indexOfFirstMatch, indexOfRegexPattern];    // If there was a found match
-            return null;                                                                            // No found match
-        }
-    }
-
     /**
      * Emit a packet to the HTML front end. Easy communication!
      * @param {Object} packet - The Object packet to emit to the HTML
@@ -337,27 +258,6 @@
         chatOverlayWindow.sendToQml(packet);
     }
 
-    function fetch(url, options = {method: "GET"}) {
-        return new Promise((resolve, reject) => {
-            let req = new XMLHttpRequest();
 
-            req.onreadystatechange = function () {
-
-                if (req.readyState === req.DONE) {
-                    if (req.status === 200) {
-                        console.log("Content type:", req.getResponseHeader("content-type"));
-                        resolve(req);
-            
-                    } else {
-                        console.log("Error", req.status, req.statusText);
-                        reject();
-                    }
-                }
-            };
-
-            req.open(options.method, url);
-            req.send();
-        });
-    }
 
 })();
diff --git a/scripts/system/domainChat/formatting.js b/scripts/system/domainChat/formatting.js
index b523f30d2d..05d3bf4cfa 100644
--- a/scripts/system/domainChat/formatting.js
+++ b/scripts/system/domainChat/formatting.js
@@ -38,6 +38,82 @@ const formatting = {
 		};
 		return newPacket;
 	},
+	parseMessage: async function(message) {
+		const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;
+        const overteLocationRegex = /hifi:\/\/[a-zA-Z0-9_-]+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+/;
+
+		let runningMessage = message;			// The remaining message that will be parsed
+        let messageArray = [];					// An array of messages that are split up by the formatting functions
+
+		const regexPatterns = [
+            { type: "url", regex: urlRegex },
+            { type: "overteLocation", regex: overteLocationRegex }
+        ]
+
+		while (true) {
+            let firstMatch = _findFirstMatch();
+
+            if (firstMatch == null) {
+				// If there is no more text to parse, break out of the loop and return the message array.
+                // Format any remaining text as a basic 'text' type.
+                messageArray.push({type: 'text', value: runningMessage});
+
+                // Append a final 'fill width' to the message text.
+                messageArray.push({type: 'messageEnd'});
+                break;
+            }
+
+            _formatMessage(firstMatch);
+        }
+
+		// Embed images in the message array.
+		for (dataChunk of messageArray){
+            if (dataChunk.type == 'url'){
+                let url = dataChunk.value;
+
+                const res = await formatting.helpers.fetch(url, {method: 'GET'});      // TODO: Replace with 'HEAD' method. https://github.com/overte-org/overte/issues/1273
+                const contentType = res.getResponseHeader("content-type");
+
+                // TODO: Add support for other media types
+                if (contentType.startsWith('image/')) {
+                    messageArray.push({type: 'imageEmbed', value: url});
+                }
+            }
+        }
+
+		return messageArray;
+
+		function _formatMessage(firstMatch){
+            let indexOfFirstMatch = firstMatch[0];
+            let regex = regexPatterns[firstMatch[1]].regex;
+
+            let foundMatch = runningMessage.match(regex)[0];
+
+            messageArray.push({type: 'text', value: runningMessage.substring(0, indexOfFirstMatch)});
+            messageArray.push({type: regexPatterns[firstMatch[1]].type, value: runningMessage.substring(indexOfFirstMatch, indexOfFirstMatch + foundMatch.length)});
+            
+            runningMessage = runningMessage.substring(indexOfFirstMatch + foundMatch.length);   // Remove the part of the message we have worked with
+        }
+
+        function _findFirstMatch(){
+            let indexOfFirstMatch = Infinity;
+            let indexOfRegexPattern = Infinity;
+
+            for (let i = 0; regexPatterns.length > i; i++){
+                let indexOfMatch = runningMessage.search(regexPatterns[i].regex);
+
+                if (indexOfMatch == -1) continue;                                              // No match found
+
+                if (indexOfMatch < indexOfFirstMatch) {
+                    indexOfFirstMatch = indexOfMatch;
+                    indexOfRegexPattern = i;
+                }
+            }
+
+            if (indexOfFirstMatch !== Infinity) return [indexOfFirstMatch, indexOfRegexPattern];    // If there was a found match
+            return null;                                                                            // No found match
+        }
+	},
 
 	helpers: {
 		// Small functions that are used often in the other functions.
@@ -59,6 +135,28 @@ const formatting = {
 		},
 		getTimestamp: function(){
 			return Date.now();
+		},
+		fetch: function (url, options = {method: "GET"}) {
+			return new Promise((resolve, reject) => {
+				let req = new XMLHttpRequest();
+	
+				req.onreadystatechange = function () {
+	
+					if (req.readyState === req.DONE) {
+						if (req.status === 200) {
+							console.log("Content type:", req.getResponseHeader("content-type"));
+							resolve(req);
+				
+						} else {
+							console.log("Error", req.status, req.statusText);
+							reject();
+						}
+					}
+				};
+	
+				req.open(options.method, url);
+				req.send();
+			});
 		}
 	}
 }

From 6976432574a0a266d6a6ebcfa13fb948c4253ded Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Sat, 21 Dec 2024 05:45:59 -0600
Subject: [PATCH 13/49] Support animated images.

---
 .../domainChat/qml_widgets/TemplateChatMessage.qml       | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
index a9c366ce76..7829ee791d 100644
--- a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
+++ b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
@@ -1,4 +1,4 @@
-import QtQuick 2.7
+import QtQuick 2.15
 import QtQuick.Controls 2.15
 import QtQuick.Layouts 1.3
 
@@ -147,10 +147,11 @@ Component {
 						width: messageBoxFlow.width;
 						height: 200
 
-						Image {
+						AnimatedImage {
 							source: model.type === 'imageEmbed' ? model.value : ''
-							sourceSize.width: 400
-							sourceSize.height: 200
+							height: Math.min(sourceSize.height, 200);
+							onStatusChanged: playing = (status == AnimatedImage.Ready)
+							fillMode: Image.PreserveAspectFit
 						}
 					}
 				}

From eb6530136705887471fe1f2474dad19ca70dd751 Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Sat, 21 Dec 2024 06:44:23 -0600
Subject: [PATCH 14/49] Basic video support.

---
 scripts/system/domainChat/formatting.js       |  7 +++-
 .../qml_widgets/TemplateChatMessage.qml       | 42 ++++++++++++++++++-
 2 files changed, 46 insertions(+), 3 deletions(-)

diff --git a/scripts/system/domainChat/formatting.js b/scripts/system/domainChat/formatting.js
index 05d3bf4cfa..5e1458db8d 100644
--- a/scripts/system/domainChat/formatting.js
+++ b/scripts/system/domainChat/formatting.js
@@ -74,10 +74,14 @@ const formatting = {
                 const res = await formatting.helpers.fetch(url, {method: 'GET'});      // TODO: Replace with 'HEAD' method. https://github.com/overte-org/overte/issues/1273
                 const contentType = res.getResponseHeader("content-type");
 
-                // TODO: Add support for other media types
                 if (contentType.startsWith('image/')) {
                     messageArray.push({type: 'imageEmbed', value: url});
+					continue;
                 }
+				if (contentType.startsWith('video/')){ 
+                    messageArray.push({type: 'videoEmbed', value: url});
+					continue;
+				}
             }
         }
 
@@ -144,7 +148,6 @@ const formatting = {
 	
 					if (req.readyState === req.DONE) {
 						if (req.status === 200) {
-							console.log("Content type:", req.getResponseHeader("content-type"));
 							resolve(req);
 				
 						} else {
diff --git a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
index 7829ee791d..9e33a0503e 100644
--- a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
+++ b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
@@ -1,6 +1,7 @@
 import QtQuick 2.15
 import QtQuick.Controls 2.15
 import QtQuick.Layouts 1.3
+import QtMultimedia 5.15
 
 Component {
 	id: template_chat_message
@@ -150,10 +151,49 @@ Component {
 						AnimatedImage {
 							source: model.type === 'imageEmbed' ? model.value : ''
 							height: Math.min(sourceSize.height, 200);
-							onStatusChanged: playing = (status == AnimatedImage.Ready)
 							fillMode: Image.PreserveAspectFit
 						}
 					}
+
+					Item {
+						visible: model.type === 'videoEmbed';
+						width: messageBoxFlow.width;
+						height: 200;
+
+						Video {
+							id: videoPlayer
+							source: model.type === 'videoEmbed' ? model.value : ''
+							height: 200;
+							width: 400;
+							fillMode: Image.PreserveAspectFit
+							autoLoad: false;
+
+							onStatusChanged: {
+								if (status === 7) {
+									// Weird hack to make the video restart when it's over
+									// Ideally you'd want to use the seek function to restart the video but it doesn't work?
+									// Will need to make a more refined solution for this later. in the form of a more advanced media player.
+									// For now, this is sufficient. -AD
+									let originalURL = videoPlayer.source;
+									videoPlayer.source = "";
+									videoPlayer.source = originalURL;
+								}
+							}
+
+							MouseArea {
+								anchors.fill: parent
+								onClicked: {
+									const videoIsOver = videoPlayer.position == videoPlayer.duration
+									if (videoPlayer.playbackState == MediaPlayer.PlayingState) {
+										videoPlayer.pause();
+									} 
+									else {
+										parent.play();
+									}
+								}
+							}
+						}
+					}
 				}
 			}
 		}

From cf4ec3fb572b06b6c6364e486adbcc36291052e1 Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Tue, 31 Dec 2024 16:22:55 -0600
Subject: [PATCH 15/49] Add setting to force virtual window in VR.

---
 scripts/system/domainChat/domainChat.js  | 34 ++++++++++++++++++------
 scripts/system/domainChat/domainChat.qml | 25 +++++++++++++++++
 2 files changed, 51 insertions(+), 8 deletions(-)

diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js
index 6d75556081..e1e73bd6cb 100644
--- a/scripts/system/domainChat/domainChat.js
+++ b/scripts/system/domainChat/domainChat.js
@@ -7,8 +7,6 @@
 //  Distributed under the Apache License, Version 2.0.
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
 
-// TODO: Message trimming
-
 (() => {
     ("use strict");
 
@@ -20,8 +18,10 @@
     var settings = {
         external_window: false,
         maximum_messages: 200,
-        join_notification: true
+        join_notification: true,
+        switchToInternalOnHeadsetUsed: true
     };
+    let temporaryChangeModeToVirtual = false;
 
     // Global vars
     var tablet;
@@ -38,6 +38,7 @@
     Messages.messageReceived.connect(receivedMessage);
     AvatarManager.avatarAddedEvent.connect((sessionId) => { _avatarAction("connected", sessionId); });
     AvatarManager.avatarRemovedEvent.connect((sessionId) => { _avatarAction("left", sessionId); });
+    HMD.displayModeChanged.connect(_onHMDDisplayModeChanged);
 
     startup();
 
@@ -134,15 +135,16 @@
                 break;
             case "setting_change":
                 // Set the setting value, and save the config
-                settings[event.setting] = event.value; // Update local settings
-                _saveSettings(); // Save local settings
+                settings[event.setting] = event.value;  // Update local settings
+                _saveSettings();                        // Save local settings
 
                 // Extra actions to preform. 
                 switch (event.setting) {
                     case "external_window":
-                        chatOverlayWindow.presentationMode = event.value
-                            ? Desktop.PresentationMode.NATIVE
-                            : Desktop.PresentationMode.VIRTUAL;
+                        _changePresentationMode(event.value);
+                        break;
+                    case "switchToInternalOnHeadsetUsed":
+                        _onHMDDisplayModeChanged(HMD.active);
                         break;
                 }
 
@@ -176,6 +178,22 @@
                 });
         }
     }
+    function _onHMDDisplayModeChanged(isHMDActive){
+        // If the user enabled automatic switching to internal when they put on a headset...
+        if (!settings.switchToInternalOnHeadsetUsed) return;
+
+        if (isHMDActive) temporaryChangeModeToVirtual = true;
+        else temporaryChangeModeToVirtual = false;
+
+        _changePresentationMode(settings.external_window);
+    }
+    function _changePresentationMode(changeToExternal){
+        if (temporaryChangeModeToVirtual) changeToExternal = false;
+        
+        chatOverlayWindow.presentationMode = changeToExternal
+            ? Desktop.PresentationMode.NATIVE
+            : Desktop.PresentationMode.VIRTUAL;
+    }
     function _sendMessage(message, channel) {
         if (message.length == 0) return;
 
diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml
index 6a79cb7e2e..287bb1a825 100644
--- a/scripts/system/domainChat/domainChat.qml
+++ b/scripts/system/domainChat/domainChat.qml
@@ -369,6 +369,29 @@ Rectangle {
                         }
                     }
                 }
+                // Switch to internal on VR Mode
+                Rectangle {
+                    width: parent.width
+                    height: 40
+                    color: "transparent"
+
+                    Text {
+                        text: "Force Virtual window in VR"
+                        color: "white"
+                        font.pointSize: 12
+                        anchors.verticalCenter: parent.verticalCenter
+                    }
+
+                    CheckBox {
+                        id: s_force_vw_in_vr
+                        anchors.right: parent.right
+                        anchors.verticalCenter: parent.verticalCenter
+
+                        onCheckedChanged: {
+                            toScript({type: 'setting_change', setting: 'switchToInternalOnHeadsetUsed', value: checked})
+                        }
+                    }
+                }
             }
         }
 
@@ -433,9 +456,11 @@ Rectangle {
                 domain.clear();
                 break;
             case "initial_settings":
+                print(JSON.stringify(message.settings));
                 if (message.settings.external_window) s_external_window.checked = true;
                 if (message.settings.maximum_messages) s_maximum_messages.value = message.settings.maximum_messages;
                 if (message.settings.join_notification) s_join_notification.checked = true;
+                if (message.settings.switchToInternalOnHeadsetUsed) s_force_vw_in_vr.checked = true;
                 break;
         }
     }

From 37cdbacf684665eebe0a47cbe25d3aa534a00a39 Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Wed, 1 Jan 2025 07:35:04 -0600
Subject: [PATCH 16/49] Message embedding toggle.

---
 scripts/system/domainChat/domainChat.js  | 11 ++++----
 scripts/system/domainChat/domainChat.qml | 25 ++++++++++++++++++
 scripts/system/domainChat/formatting.js  | 32 +++++++++++++-----------
 3 files changed, 48 insertions(+), 20 deletions(-)

diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js
index e1e73bd6cb..5a97c4dee9 100644
--- a/scripts/system/domainChat/domainChat.js
+++ b/scripts/system/domainChat/domainChat.js
@@ -19,7 +19,8 @@
         external_window: false,
         maximum_messages: 200,
         join_notification: true,
-        switchToInternalOnHeadsetUsed: true
+        switchToInternalOnHeadsetUsed: true,
+        enableEmbedding: false                  // Prevents information leakage, default false
     };
     let temporaryChangeModeToVirtual = false;
 
@@ -109,7 +110,7 @@
         if (message.channel == "local" && isTooFar(message.position)) return;   // If message is local, and if player is too far away from location, do nothing.
         
         let formattedMessagePacket = { ...message };
-        formattedMessagePacket.message = await formatting.parseMessage(message.message)
+        formattedMessagePacket.message = await formatting.parseMessage(message.message, settings.enableEmbedding)
 
         _emitEvent({ type: "show_message", ...formattedMessagePacket });        // Update qml view of to new message.
         _notificationCoreMessage(message.displayName, message.message)          // Show a new message on screen.
@@ -247,9 +248,9 @@
         if (messageHistory) {
             // Load message history
             for (message of messageHistory) {
-                messagePacket = { ...message };                                                 // Create new variable
-                messagePacket = formatting.addTimeAndDateStringToPacket(messagePacket);         // Add timestamp
-                messagePacket.message = await formatting.parseMessage(messagePacket.message);   // Parse the message for the UI
+                messagePacket = { ...message };                                                                             // Create new variable
+                messagePacket = formatting.addTimeAndDateStringToPacket(messagePacket);                                     // Add timestamp
+                messagePacket.message = await formatting.parseMessage(messagePacket.message, settings.enableEmbedding);     // Parse the message for the UI
 
                 _emitEvent({ type: "show_message", ...messagePacket });                         // Send message to UI
             }
diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml
index 287bb1a825..ef5860587a 100644
--- a/scripts/system/domainChat/domainChat.qml
+++ b/scripts/system/domainChat/domainChat.qml
@@ -392,6 +392,29 @@ Rectangle {
                         }
                     }
                 }
+                // Toggle media embedding
+                Rectangle {
+                    width: parent.width
+                    height: 40
+                    color: "transparent"
+
+                    Text {
+                        text: "Enable media embedding"
+                        color: "white"
+                        font.pointSize: 12
+                        anchors.verticalCenter: parent.verticalCenter
+                    }
+
+                    CheckBox {
+                        id: s_enable_embedding
+                        anchors.right: parent.right
+                        anchors.verticalCenter: parent.verticalCenter
+
+                        onCheckedChanged: {
+                            toScript({type: 'setting_change', setting: 'enableEmbedding', value: checked})
+                        }
+                    }
+                }
             }
         }
 
@@ -456,11 +479,13 @@ Rectangle {
                 domain.clear();
                 break;
             case "initial_settings":
+
                 print(JSON.stringify(message.settings));
                 if (message.settings.external_window) s_external_window.checked = true;
                 if (message.settings.maximum_messages) s_maximum_messages.value = message.settings.maximum_messages;
                 if (message.settings.join_notification) s_join_notification.checked = true;
                 if (message.settings.switchToInternalOnHeadsetUsed) s_force_vw_in_vr.checked = true;
+                if (message.settings.enableEmbedding) s_enable_embedding.checked = true;
                 break;
         }
     }
diff --git a/scripts/system/domainChat/formatting.js b/scripts/system/domainChat/formatting.js
index 5e1458db8d..24662e4ff1 100644
--- a/scripts/system/domainChat/formatting.js
+++ b/scripts/system/domainChat/formatting.js
@@ -38,7 +38,7 @@ const formatting = {
 		};
 		return newPacket;
 	},
-	parseMessage: async function(message) {
+	parseMessage: async function(message, enableEmbedding) {
 		const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;
         const overteLocationRegex = /hifi:\/\/[a-zA-Z0-9_-]+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+/;
 
@@ -67,23 +67,25 @@ const formatting = {
         }
 
 		// Embed images in the message array.
-		for (dataChunk of messageArray){
-            if (dataChunk.type == 'url'){
-                let url = dataChunk.value;
+		if (enableEmbedding) {
+			for (dataChunk of messageArray){
+				if (dataChunk.type == 'url'){
+					let url = dataChunk.value;
 
-                const res = await formatting.helpers.fetch(url, {method: 'GET'});      // TODO: Replace with 'HEAD' method. https://github.com/overte-org/overte/issues/1273
-                const contentType = res.getResponseHeader("content-type");
+					const res = await formatting.helpers.fetch(url, {method: 'GET'});      // TODO: Replace with 'HEAD' method. https://github.com/overte-org/overte/issues/1273
+					const contentType = res.getResponseHeader("content-type");
 
-                if (contentType.startsWith('image/')) {
-                    messageArray.push({type: 'imageEmbed', value: url});
-					continue;
-                }
-				if (contentType.startsWith('video/')){ 
-                    messageArray.push({type: 'videoEmbed', value: url});
-					continue;
+					if (contentType.startsWith('image/')) {
+						messageArray.push({type: 'imageEmbed', value: url});
+						continue;
+					}
+					if (contentType.startsWith('video/')){ 
+						messageArray.push({type: 'videoEmbed', value: url});
+						continue;
+					}
 				}
-            }
-        }
+			}
+		}
 
 		return messageArray;
 

From fae1fdd7e045e0ded4174c0d9da9a8747470e5e2 Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Wed, 1 Jan 2025 11:41:17 -0600
Subject: [PATCH 17/49] Remove lingering print function.

---
 scripts/system/domainChat/domainChat.qml | 2 --
 1 file changed, 2 deletions(-)

diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml
index ef5860587a..9ac69f71ac 100644
--- a/scripts/system/domainChat/domainChat.qml
+++ b/scripts/system/domainChat/domainChat.qml
@@ -479,8 +479,6 @@ Rectangle {
                 domain.clear();
                 break;
             case "initial_settings":
-
-                print(JSON.stringify(message.settings));
                 if (message.settings.external_window) s_external_window.checked = true;
                 if (message.settings.maximum_messages) s_maximum_messages.value = message.settings.maximum_messages;
                 if (message.settings.join_notification) s_join_notification.checked = true;

From 1c9174ff8f06a8fa290dfd3b22a5f8b46cc2ff23 Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Wed, 15 Jan 2025 11:46:50 -0600
Subject: [PATCH 18/49] Fix text highlighting.

---
 .../qml_widgets/TemplateChatMessage.qml       | 25 +++++++++++++++----
 1 file changed, 20 insertions(+), 5 deletions(-)

diff --git a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
index 9e33a0503e..8a8606c0ff 100644
--- a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
+++ b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
@@ -19,15 +19,21 @@ Component {
 			anchors.horizontalCenter: parent.horizontalCenter
 			height: 22
 
-			Text {
+			TextEdit {
 				text: delegateUsername
 				color: "lightgray"
+				readOnly: true
+				selectByMouse: true
+				selectByKeyboard: true
 			}
 
-			Text {
+			TextEdit {
 				anchors.right: parent.right
 				text: delegateDate
 				color: "lightgray"
+				readOnly: true
+				selectByMouse: true
+				selectByKeyboard: true
 			}
 		}
 
@@ -41,12 +47,15 @@ Component {
 				model: delegateText;
 
 				RowLayout {
-					Text {
+					TextEdit {
 						text: model.value || ""
 						font.pointSize: 12
 						wrapMode: Text.Wrap
 						width: model.type === 'text' || model.type === 'mention' ? Math.min(messageBoxFlow.width, contentWidth) : 0;
 						visible: model.type === 'text' || model.type === 'mention';
+						readOnly: true
+						selectByMouse: true
+						selectByKeyboard: true
 
 						color: {
 							switch (model.type) {
@@ -62,13 +71,16 @@ Component {
 						width: children[0].contentWidth;
 						visible: model.type === 'url';
 
-						Text {
+						TextEdit {
 							text: model.value || "";
 							font.pointSize: 12;
 							wrapMode: Text.Wrap;
 							color: "#4EBAFD";
 							font.underline: true;
 							width: parent.width;
+							readOnly: true
+							selectByMouse: true
+							selectByKeyboard: true
 
 							MouseArea {
 								anchors.fill: parent;
@@ -120,12 +132,15 @@ Component {
 								anchors.rightMargin: 10
 							}
 
-							Text {
+							TextEdit {
 								text: model.type === 'overteLocation' ? model.value.split('hifi://')[1].split('/')[0] : '';
 								color: "black"
 								font.pointSize: 12
 								x: parent.children[0].width + 5;
 								anchors.verticalCenter: parent.verticalCenter 
+								readOnly: true
+								selectByMouse: true
+								selectByKeyboard: true
 							}
 
 							MouseArea {

From e67526cdf5ddfbff7931794c34dcfd3eaeb6617c Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Wed, 15 Jan 2025 13:53:16 -0600
Subject: [PATCH 19/49] Fix binding loops.

---
 .../qml_widgets/TemplateChatMessage.qml       | 34 +++++++++----------
 1 file changed, 17 insertions(+), 17 deletions(-)

diff --git a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
index 8a8606c0ff..b5b82a7d79 100644
--- a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
+++ b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
@@ -20,26 +20,23 @@ Component {
 			height: 22
 
 			TextEdit {
-				text: delegateUsername
-				color: "lightgray"
-				readOnly: true
-				selectByMouse: true
-				selectByKeyboard: true
+				text: delegateUsername;
+				color: "lightgray";
+				readOnly: true;
+				selectByMouse: true;
+				selectByKeyboard: true;
 			}
 
-			TextEdit {
-				anchors.right: parent.right
-				text: delegateDate
-				color: "lightgray"
-				readOnly: true
-				selectByMouse: true
-				selectByKeyboard: true
+			Text {
+				anchors.right: parent.right;
+				text: delegateDate;
+				color: "lightgray";
 			}
 		}
 
 		Flow {
 			anchors.top: parent.children[0].bottom;
-			width: parent.width * 0.8
+			width: parent.width;
 			x: 5
 			id: messageBoxFlow
 
@@ -47,11 +44,13 @@ Component {
 				model: delegateText;
 
 				RowLayout {
+					width: parent.width;
+
 					TextEdit {
 						text: model.value || ""
 						font.pointSize: 12
 						wrapMode: Text.Wrap
-						width: model.type === 'text' || model.type === 'mention' ? Math.min(messageBoxFlow.width, contentWidth) : 0;
+						width: messageBoxFlow.width;
 						visible: model.type === 'text' || model.type === 'mention';
 						readOnly: true
 						selectByMouse: true
@@ -68,10 +67,11 @@ Component {
 					}
 
 					RowLayout {
-						width: children[0].contentWidth;
+						width: urlTypeTextDisplay.width;
 						visible: model.type === 'url';
 
 						TextEdit {
+							id: urlTypeTextDisplay;
 							text: model.value || "";
 							font.pointSize: 12;
 							wrapMode: Text.Wrap;
@@ -154,8 +154,8 @@ Component {
 					}
 
 					Item {
-						Layout.fillWidth: true
-						visible: model.type === 'messageEnd'
+						Layout.fillWidth: true;
+						visible: model.type === 'messageEnd';
 					}
 
 					Item {

From dbf5af052b59a7467d7d1ac2a73a4ded88bd7ae8 Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Thu, 16 Jan 2025 14:53:54 -0600
Subject: [PATCH 20/49] Remove video embeds. Added debug logs. Don't save
 settings when initializing the application.

---
 scripts/system/domainChat/domainChat.js       | 21 +++++-
 scripts/system/domainChat/domainChat.qml      | 13 +++-
 .../qml_widgets/TemplateChatMessage.qml       | 73 +++++++++----------
 3 files changed, 63 insertions(+), 44 deletions(-)

diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js
index 5a97c4dee9..0034f73093 100644
--- a/scripts/system/domainChat/domainChat.js
+++ b/scripts/system/domainChat/domainChat.js
@@ -99,7 +99,6 @@
         // Is the message a chat message?
         channel = channel.toLowerCase();
         if (channel !== "chat") return;
-        
         if ((message = formatting.toJSON(message)) == null) return;             // Make sure we are working with a JSON object we expect, otherwise kill
         message = formatting.addTimeAndDateStringToPacket(message);  
         
@@ -194,6 +193,8 @@
         chatOverlayWindow.presentationMode = changeToExternal
             ? Desktop.PresentationMode.NATIVE
             : Desktop.PresentationMode.VIRTUAL;
+        
+        console.log(`Presentation mode was changed to ${chatOverlayWindow.presentationMode}`);
     }
     function _sendMessage(message, channel) {
         if (message.length == 0) return;
@@ -244,6 +245,7 @@
     }
     async function _loadSettings() {
         settings = Settings.getValue("ArmoredChat-Config", settings);
+        console.log("Loading settings: ", jstr(settings));
 
         if (messageHistory) {
             // Load message history
@@ -259,10 +261,11 @@
         _emitEvent({ type: "initial_settings", settings: settings });                           // Send current settings to the app
     }
     function _saveSettings() {
-        console.log("Saving config");
+        console.log("Saving settings: ", jstr(settings));
         Settings.setValue("ArmoredChat-Config", settings);
     }
     function _notificationCoreMessage(displayName, message){
+        console.log("Sending notification to notificationCore:", `Display name: ${displayName}\n Message: ${message}`);
         Messages.sendLocalMessage(
             "Floof-Notif",
             JSON.stringify({ sender: displayName, text: message })
@@ -274,9 +277,19 @@
      * @param {("show_message"|"clear_messages"|"notification"|"initial_settings")} packet.type - The type of packet it is
      */
     function _emitEvent(packet = { type: "" }) {
+        if (packet.type == `show_message`) {
+            // Don't show the message contents, this is a courtesy to prevent message leakage in the logs.
+            let strippedPacket = {...packet};
+            delete strippedPacket.message
+            console.log("Sending packet to QML interface", jstr(strippedPacket));
+        }
+        else {
+            console.log("Sending packet to QML interface", jstr(packet));
+        }
+
         chatOverlayWindow.sendToQml(packet);
     }
 
-
-
+    // Debug and developer functions and data
+    const jstr = (object) => JSON.stringify(object, null, 4);       // JSON Stringify function with formatting
 })();
diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml
index 9ac69f71ac..be4b6d4f79 100644
--- a/scripts/system/domainChat/domainChat.qml
+++ b/scripts/system/domainChat/domainChat.qml
@@ -8,8 +8,10 @@ Rectangle {
     color: Qt.rgba(0.1,0.1,0.1,1)
     signal sendToScript(var message);
 
-    property string pageVal: "local"
-    property date last_message_time: new Date()
+    property string pageVal: "local";
+    property date last_message_time: new Date();
+    property bool initialized: false;
+
 
     // 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.
@@ -445,7 +447,6 @@ Rectangle {
         messageViewFlickable.returnToBounds();
     }
 
-
     function addMessage(username, message, date, channel, type){
         channel = getChannel(channel)
 
@@ -479,17 +480,23 @@ Rectangle {
                 domain.clear();
                 break;
             case "initial_settings":
+                print(`Got settings:\n ${JSON.stringify(message.settings, null, 4)}`);
                 if (message.settings.external_window) s_external_window.checked = true;
                 if (message.settings.maximum_messages) s_maximum_messages.value = message.settings.maximum_messages;
                 if (message.settings.join_notification) s_join_notification.checked = true;
                 if (message.settings.switchToInternalOnHeadsetUsed) s_force_vw_in_vr.checked = true;
                 if (message.settings.enableEmbedding) s_enable_embedding.checked = true;
+
+                initialized = true;     // Application is ready
+
                 break;
         }
     }
 
     // Send message to script
     function toScript(packet){
+        if (packet.type === "setting_change" && !initialized) return;   // Don't announce a change in settings if not ready
+
         sendToScript(packet)
     }
 }
diff --git a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
index b5b82a7d79..641a0ca0b3 100644
--- a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
+++ b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
@@ -1,7 +1,7 @@
 import QtQuick 2.15
 import QtQuick.Controls 2.15
 import QtQuick.Layouts 1.3
-import QtMultimedia 5.15
+// import QtMultimedia 5.15
 
 Component {
 	id: template_chat_message
@@ -170,45 +170,44 @@ Component {
 						}
 					}
 
-					Item {
-						visible: model.type === 'videoEmbed';
-						width: messageBoxFlow.width;
-						height: 200;
+					// Item {
+					// 	visible: model.type === 'videoEmbed';
+					// 	width: messageBoxFlow.width;
+					// 	height: 200;
 
-						Video {
-							id: videoPlayer
-							source: model.type === 'videoEmbed' ? model.value : ''
-							height: 200;
-							width: 400;
-							fillMode: Image.PreserveAspectFit
-							autoLoad: false;
+					// 	Video {
+					// 		id: videoPlayer
+					// 		source: model.type === 'videoEmbed' ? model.value : ''
+					// 		height: 200;
+					// 		width: 400;
+					// 		fillMode: Image.PreserveAspectFit
+					// 		autoLoad: false;
 
-							onStatusChanged: {
-								if (status === 7) {
-									// Weird hack to make the video restart when it's over
-									// Ideally you'd want to use the seek function to restart the video but it doesn't work?
-									// Will need to make a more refined solution for this later. in the form of a more advanced media player.
-									// For now, this is sufficient. -AD
-									let originalURL = videoPlayer.source;
-									videoPlayer.source = "";
-									videoPlayer.source = originalURL;
-								}
-							}
+					// 		onStatusChanged: {
+					// 			if (status === 7) {
+					// 				// Weird hack to make the video restart when it's over
+					// 				// Ideally you'd want to use the seek function to restart the video but it doesn't work?
+					// 				// Will need to make a more refined solution for this later. in the form of a more advanced media player.
+					// 				// For now, this is sufficient. -AD
+					// 				let originalURL = videoPlayer.source;
+					// 				videoPlayer.source = "";
+					// 				videoPlayer.source = originalURL;
+					// 			}
+					// 		}
 
-							MouseArea {
-								anchors.fill: parent
-								onClicked: {
-									const videoIsOver = videoPlayer.position == videoPlayer.duration
-									if (videoPlayer.playbackState == MediaPlayer.PlayingState) {
-										videoPlayer.pause();
-									} 
-									else {
-										parent.play();
-									}
-								}
-							}
-						}
-					}
+					// 		MouseArea {
+					// 			anchors.fill: parent
+					// 			onClicked: {
+					// 				const videoIsOver = videoPlayer.position == videoPlayer.duration
+					// 				if (videoPlayer.playbackState == MediaPlayer.PlayingState) {
+					// 					videoPlayer.pause();
+					// 				} 
+					// 				else {
+					// 					parent.play();
+					// 				}
+					// 			}
+					// 		}
+					// 	}
 				}
 			}
 		}

From cf8a21c794a9c230e335a635c0967cd9b234c290 Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Wed, 22 Jan 2025 23:12:03 -0600
Subject: [PATCH 21/49] Fix spacing on messages with links.

---
 scripts/system/domainChat/domainChat.qml | 1 -
 scripts/system/domainChat/formatting.js  | 4 ++--
 2 files changed, 2 insertions(+), 3 deletions(-)

diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml
index be4b6d4f79..28ec98d390 100644
--- a/scripts/system/domainChat/domainChat.qml
+++ b/scripts/system/domainChat/domainChat.qml
@@ -189,7 +189,6 @@ Rectangle {
                 }
             }
 
-
             ListModel {
                 id: local
             }
diff --git a/scripts/system/domainChat/formatting.js b/scripts/system/domainChat/formatting.js
index 24662e4ff1..c534f3a720 100644
--- a/scripts/system/domainChat/formatting.js
+++ b/scripts/system/domainChat/formatting.js
@@ -56,7 +56,7 @@ const formatting = {
             if (firstMatch == null) {
 				// If there is no more text to parse, break out of the loop and return the message array.
                 // Format any remaining text as a basic 'text' type.
-                messageArray.push({type: 'text', value: runningMessage});
+                if (runningMessage.trim() != "") messageArray.push({type: 'text', value: runningMessage});
 
                 // Append a final 'fill width' to the message text.
                 messageArray.push({type: 'messageEnd'});
@@ -95,7 +95,7 @@ const formatting = {
 
             let foundMatch = runningMessage.match(regex)[0];
 
-            messageArray.push({type: 'text', value: runningMessage.substring(0, indexOfFirstMatch)});
+			if (runningMessage.substring(0, indexOfFirstMatch) != "") messageArray.push({type: 'text', value: runningMessage.substring(0, indexOfFirstMatch)});
             messageArray.push({type: regexPatterns[firstMatch[1]].type, value: runningMessage.substring(indexOfFirstMatch, indexOfFirstMatch + foundMatch.length)});
             
             runningMessage = runningMessage.substring(indexOfFirstMatch + foundMatch.length);   // Remove the part of the message we have worked with

From aacdf64bf6809a15b36a6ed079d7802535ff3f5d Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Mon, 27 Jan 2025 23:53:58 -0600
Subject: [PATCH 22/49] Fix wrapping. Remove unused video embed code.

---
 .../qml_widgets/TemplateChatMessage.qml       | 45 ++-----------------
 1 file changed, 4 insertions(+), 41 deletions(-)

diff --git a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
index 641a0ca0b3..0f97a614ae 100644
--- a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
+++ b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
@@ -1,7 +1,6 @@
 import QtQuick 2.15
 import QtQuick.Controls 2.15
 import QtQuick.Layouts 1.3
-// import QtMultimedia 5.15
 
 Component {
 	id: template_chat_message
@@ -43,14 +42,15 @@ Component {
 			Repeater {
 				model: delegateText;
 
-				RowLayout {
+				Item {
 					width: parent.width;
+					height: children[0].contentHeight;
 
 					TextEdit {
 						text: model.value || ""
 						font.pointSize: 12
-						wrapMode: Text.Wrap
-						width: messageBoxFlow.width;
+						wrapMode: TextEdit.WordWrap
+						width: parent.width * 0.8
 						visible: model.type === 'text' || model.type === 'mention';
 						readOnly: true
 						selectByMouse: true
@@ -170,44 +170,7 @@ Component {
 						}
 					}
 
-					// Item {
-					// 	visible: model.type === 'videoEmbed';
-					// 	width: messageBoxFlow.width;
-					// 	height: 200;
 
-					// 	Video {
-					// 		id: videoPlayer
-					// 		source: model.type === 'videoEmbed' ? model.value : ''
-					// 		height: 200;
-					// 		width: 400;
-					// 		fillMode: Image.PreserveAspectFit
-					// 		autoLoad: false;
-
-					// 		onStatusChanged: {
-					// 			if (status === 7) {
-					// 				// Weird hack to make the video restart when it's over
-					// 				// Ideally you'd want to use the seek function to restart the video but it doesn't work?
-					// 				// Will need to make a more refined solution for this later. in the form of a more advanced media player.
-					// 				// For now, this is sufficient. -AD
-					// 				let originalURL = videoPlayer.source;
-					// 				videoPlayer.source = "";
-					// 				videoPlayer.source = originalURL;
-					// 			}
-					// 		}
-
-					// 		MouseArea {
-					// 			anchors.fill: parent
-					// 			onClicked: {
-					// 				const videoIsOver = videoPlayer.position == videoPlayer.duration
-					// 				if (videoPlayer.playbackState == MediaPlayer.PlayingState) {
-					// 					videoPlayer.pause();
-					// 				} 
-					// 				else {
-					// 					parent.play();
-					// 				}
-					// 			}
-					// 		}
-					// 	}
 				}
 			}
 		}

From 25fe4850b99c655b7fa3bc3943e6c40925572c95 Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Tue, 28 Jan 2025 00:00:11 -0600
Subject: [PATCH 23/49] Added a sortOrder.
 https://github.com/overte-org/overte/pull/1276

---
 scripts/system/domainChat/domainChat.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js
index 0034f73093..74de8d1b9b 100644
--- a/scripts/system/domainChat/domainChat.js
+++ b/scripts/system/domainChat/domainChat.js
@@ -49,6 +49,7 @@
         appButton = tablet.addButton({
             icon: Script.resolvePath("./img/icon_white.png"),
             activeIcon: Script.resolvePath("./img/icon_black.png"),
+            sortOrder: 8,
             text: "CHAT",
             isActive: appIsVisible,
         });

From 6e4899f2c8600d49daca8af989875f9174bd6f00 Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Fri, 29 Nov 2024 23:53:27 -0600
Subject: [PATCH 24/49] Moved script. Changed name.

---
 scripts/defaultScripts.js                           |   2 +-
 .../armored-chat => system/domainChat}/README.md    |  12 ++++++------
 .../domainChat/domainChat.js}                       |   6 +++---
 .../domainChat/domainChat.qml}                      |   0
 .../domainChat/domainChatQuick.qml}                 |   0
 .../domainChat}/img/icon_black.png                  | Bin
 .../domainChat}/img/icon_white.png                  | Bin
 .../domainChat}/img/ui/send.svg                     |   0
 .../domainChat}/img/ui/send_black.png               | Bin
 .../domainChat}/img/ui/send_white.png               | Bin
 .../domainChat}/img/ui/settings_black.png           | Bin
 .../domainChat}/img/ui/settings_white.png           | Bin
 .../domainChat}/img/ui/social_black.png             | Bin
 .../domainChat}/img/ui/social_white.png             | Bin
 .../domainChat}/img/ui/world_black.png              | Bin
 .../domainChat}/img/ui/world_white.png              | Bin
 16 files changed, 10 insertions(+), 10 deletions(-)
 rename scripts/{communityScripts/armored-chat => system/domainChat}/README.md (93%)
 rename scripts/{communityScripts/armored-chat/armored_chat.js => system/domainChat/domainChat.js} (98%)
 rename scripts/{communityScripts/armored-chat/armored_chat.qml => system/domainChat/domainChat.qml} (100%)
 rename scripts/{communityScripts/armored-chat/armored_chat_quick_message.qml => system/domainChat/domainChatQuick.qml} (100%)
 rename scripts/{communityScripts/armored-chat => system/domainChat}/img/icon_black.png (100%)
 rename scripts/{communityScripts/armored-chat => system/domainChat}/img/icon_white.png (100%)
 rename scripts/{communityScripts/armored-chat => system/domainChat}/img/ui/send.svg (100%)
 rename scripts/{communityScripts/armored-chat => system/domainChat}/img/ui/send_black.png (100%)
 rename scripts/{communityScripts/armored-chat => system/domainChat}/img/ui/send_white.png (100%)
 rename scripts/{communityScripts/armored-chat => system/domainChat}/img/ui/settings_black.png (100%)
 rename scripts/{communityScripts/armored-chat => system/domainChat}/img/ui/settings_white.png (100%)
 rename scripts/{communityScripts/armored-chat => system/domainChat}/img/ui/social_black.png (100%)
 rename scripts/{communityScripts/armored-chat => system/domainChat}/img/ui/social_white.png (100%)
 rename scripts/{communityScripts/armored-chat => system/domainChat}/img/ui/world_black.png (100%)
 rename scripts/{communityScripts/armored-chat => system/domainChat}/img/ui/world_white.png (100%)

diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js
index 31afd6e2db..d185daadb5 100644
--- a/scripts/defaultScripts.js
+++ b/scripts/defaultScripts.js
@@ -46,7 +46,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/armored-chat/armored_chat.js",
+    "system/domainChat/domainChat.js",
     //"system/chat.js"
 ];
 
diff --git a/scripts/communityScripts/armored-chat/README.md b/scripts/system/domainChat/README.md
similarity index 93%
rename from scripts/communityScripts/armored-chat/README.md
rename to scripts/system/domainChat/README.md
index 2385494676..8ed2e8d911 100644
--- a/scripts/communityScripts/armored-chat/README.md
+++ b/scripts/system/domainChat/README.md
@@ -1,15 +1,15 @@
-# Armored Chat
+# Domain Chat
 
-1. What is Armored Chat
+1. What is Domain Chat
 2. User manual
     - Installation
     - Settings
     - Usability tips
 3. Development
 
-## What is Armored Chat
+## What is Domain 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.
+Domain 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
 
@@ -21,7 +21,7 @@ For notifications, AC uses [notificationCore.js](https://github.com/overte-org/o
 
 ### Installation
 
-Armored Chat is preinstalled courtesy of [defaultScripts.js](https://github.com/overte-org/overte/blob/8661e8a858663b48e8485c2cd7120dc3e2d7b87e/scripts/defaultScripts.js).
+Domain 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:
 
@@ -33,7 +33,7 @@ https://raw.githubusercontent.com/overte-org/overte/master/scripts/communityScri
 
 ### Settings
 
-Armored Chat comes with basic settings for managing itself.
+Domain Chat comes with basic settings for managing itself.
 
 #### External window
 
diff --git a/scripts/communityScripts/armored-chat/armored_chat.js b/scripts/system/domainChat/domainChat.js
similarity index 98%
rename from scripts/communityScripts/armored-chat/armored_chat.js
rename to scripts/system/domainChat/domainChat.js
index ae46f4d8f3..8d64f35e16 100644
--- a/scripts/communityScripts/armored-chat/armored_chat.js
+++ b/scripts/system/domainChat/domainChat.js
@@ -1,5 +1,5 @@
 //
-//  armored_chat.js
+//  domainChat.js
 //
 //  Created by Armored Dragon, May 17th, 2024.
 //  Copyright 2024 Overte e.V.
@@ -62,7 +62,7 @@
         appButton.clicked.connect(toggleMainChatWindow);
 
         quickMessage = new OverlayWindow({
-            source: Script.resolvePath("./armored_chat_quick_message.qml"),
+            source: Script.resolvePath("./domainChatQuick.qml"),
         });
 
         _openWindow();
@@ -79,7 +79,7 @@
     }
     function _openWindow() {
         chatOverlayWindow = new Desktop.createWindow(
-            Script.resolvePath("./armored_chat.qml"),
+            Script.resolvePath("./domainChat.qml"),
             {
                 title: "Chat",
                 size: { x: 550, y: 400 },
diff --git a/scripts/communityScripts/armored-chat/armored_chat.qml b/scripts/system/domainChat/domainChat.qml
similarity index 100%
rename from scripts/communityScripts/armored-chat/armored_chat.qml
rename to scripts/system/domainChat/domainChat.qml
diff --git a/scripts/communityScripts/armored-chat/armored_chat_quick_message.qml b/scripts/system/domainChat/domainChatQuick.qml
similarity index 100%
rename from scripts/communityScripts/armored-chat/armored_chat_quick_message.qml
rename to scripts/system/domainChat/domainChatQuick.qml
diff --git a/scripts/communityScripts/armored-chat/img/icon_black.png b/scripts/system/domainChat/img/icon_black.png
similarity index 100%
rename from scripts/communityScripts/armored-chat/img/icon_black.png
rename to scripts/system/domainChat/img/icon_black.png
diff --git a/scripts/communityScripts/armored-chat/img/icon_white.png b/scripts/system/domainChat/img/icon_white.png
similarity index 100%
rename from scripts/communityScripts/armored-chat/img/icon_white.png
rename to scripts/system/domainChat/img/icon_white.png
diff --git a/scripts/communityScripts/armored-chat/img/ui/send.svg b/scripts/system/domainChat/img/ui/send.svg
similarity index 100%
rename from scripts/communityScripts/armored-chat/img/ui/send.svg
rename to scripts/system/domainChat/img/ui/send.svg
diff --git a/scripts/communityScripts/armored-chat/img/ui/send_black.png b/scripts/system/domainChat/img/ui/send_black.png
similarity index 100%
rename from scripts/communityScripts/armored-chat/img/ui/send_black.png
rename to scripts/system/domainChat/img/ui/send_black.png
diff --git a/scripts/communityScripts/armored-chat/img/ui/send_white.png b/scripts/system/domainChat/img/ui/send_white.png
similarity index 100%
rename from scripts/communityScripts/armored-chat/img/ui/send_white.png
rename to scripts/system/domainChat/img/ui/send_white.png
diff --git a/scripts/communityScripts/armored-chat/img/ui/settings_black.png b/scripts/system/domainChat/img/ui/settings_black.png
similarity index 100%
rename from scripts/communityScripts/armored-chat/img/ui/settings_black.png
rename to scripts/system/domainChat/img/ui/settings_black.png
diff --git a/scripts/communityScripts/armored-chat/img/ui/settings_white.png b/scripts/system/domainChat/img/ui/settings_white.png
similarity index 100%
rename from scripts/communityScripts/armored-chat/img/ui/settings_white.png
rename to scripts/system/domainChat/img/ui/settings_white.png
diff --git a/scripts/communityScripts/armored-chat/img/ui/social_black.png b/scripts/system/domainChat/img/ui/social_black.png
similarity index 100%
rename from scripts/communityScripts/armored-chat/img/ui/social_black.png
rename to scripts/system/domainChat/img/ui/social_black.png
diff --git a/scripts/communityScripts/armored-chat/img/ui/social_white.png b/scripts/system/domainChat/img/ui/social_white.png
similarity index 100%
rename from scripts/communityScripts/armored-chat/img/ui/social_white.png
rename to scripts/system/domainChat/img/ui/social_white.png
diff --git a/scripts/communityScripts/armored-chat/img/ui/world_black.png b/scripts/system/domainChat/img/ui/world_black.png
similarity index 100%
rename from scripts/communityScripts/armored-chat/img/ui/world_black.png
rename to scripts/system/domainChat/img/ui/world_black.png
diff --git a/scripts/communityScripts/armored-chat/img/ui/world_white.png b/scripts/system/domainChat/img/ui/world_white.png
similarity index 100%
rename from scripts/communityScripts/armored-chat/img/ui/world_white.png
rename to scripts/system/domainChat/img/ui/world_white.png

From 8882468f3eeb916999978ebdef549f2e079d4a65 Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Sat, 30 Nov 2024 00:32:20 -0600
Subject: [PATCH 25/49] Removed floofchat compatibility.

---
 scripts/system/domainChat/domainChat.js | 36 -------------------------
 1 file changed, 36 deletions(-)

diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js
index 8d64f35e16..22e28f0f51 100644
--- a/scripts/system/domainChat/domainChat.js
+++ b/scripts/system/domainChat/domainChat.js
@@ -28,7 +28,6 @@
     var palData = AvatarManager.getPalData().data;
 
     Controller.keyPressEvent.connect(keyPressEvent);
-    Messages.subscribe("Chat"); // Floofchat
     Messages.subscribe("chat");
     Messages.messageReceived.connect(receivedMessage);
     AvatarManager.avatarAddedEvent.connect((sessionId) => {
@@ -104,10 +103,7 @@
         const timeArray = _formatTimestamp(currentTimestamp);
 
         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();
 
         // Check the channel. If the channel is not one we have, do nothing.
@@ -216,8 +212,6 @@
                 action: "send_chat_message",
             })
         );
-
-        floofChatCompatibilitySendMessage(message, channel);
     }
     function _avatarAction(type, sessionId) {
         Script.setTimeout(() => {
@@ -304,34 +298,4 @@
     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",
-            })
-        );
-    }
 })();

From 8db5772a37aef3d187c7844bbdcc51b330fb1a13 Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Sat, 30 Nov 2024 01:23:49 -0600
Subject: [PATCH 26/49] Notification organization.

---
 scripts/system/domainChat/domainChat.js | 45 +++++++++----------------
 1 file changed, 16 insertions(+), 29 deletions(-)

diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js
index 22e28f0f51..d8341289ec 100644
--- a/scripts/system/domainChat/domainChat.js
+++ b/scripts/system/domainChat/domainChat.js
@@ -102,33 +102,20 @@
         const currentTimestamp = _getTimestamp();
         const timeArray = _formatTimestamp(currentTimestamp);
 
-        if (!message.channel) message.channel = "domain"; // We don't know where to put this message. Assume it is a domain wide message.
-
-        message.channel = message.channel.toLowerCase();
-
-        // Check the channel. If the channel is not one we have, do nothing.
-        if (!channels.includes(message.channel)) return;
-
-        // If message is local, and if player is too far away from location, do nothing.
-        if (message.channel == "local" && isTooFar(message.position)) return; 
+        if (!message.channel) message.channel = "domain";                       // We don't know where to put this message. Assume it is a domain wide message.
+        message.channel = message.channel.toLowerCase();                        // Only recognize channel names as lower case. 
+        
+        if (!channels.includes(message.channel)) return;                        // Check the channel. If the channel is not one we have, do nothing.
+        if (message.channel == "local" && isTooFar(message.position)) return;   // If message is local, and if player is too far away from location, do nothing.
 
         // Format the timestamp 
         message.timeString = timeArray[0];
         message.dateString = timeArray[1];
+        
+        _emitEvent({ type: "show_message", ...message });                       // Update qml view of to new message.
+        _notificationCoreMessage(message.displayName, message.message)          // Show a new message on screen.
 
-        // Update qml view of to new message
-        _emitEvent({ type: "show_message", ...message });
-
-        // Show new message on screen
-        Messages.sendLocalMessage(
-            "Floof-Notif",
-            JSON.stringify({
-                sender: message.displayName,
-                text: message.message,
-            })
-        );
-
-        // Save message to history
+        // Create a new variable based on the message that will be saved.
         let savedMessage = message;
 
         // Remove unnecessary data.
@@ -239,13 +226,7 @@
 
             // Show new message on screen
             if (settings.join_notification){
-                Messages.sendLocalMessage(
-                    "Floof-Notif",
-                    JSON.stringify({
-                        sender: displayName,
-                        text: type,
-                    })
-                );
+                _notificationCoreMessage(displayName, type)
             }
 
             _emitEvent({ type: "notification", ...message });
@@ -289,6 +270,12 @@
 
         return timeArray;
     }
+    function _notificationCoreMessage(displayName, message){
+        Messages.sendLocalMessage(
+            "Floof-Notif",
+            JSON.stringify({ sender: displayName, text: message })
+        );
+    }
 
     /**
      * Emit a packet to the HTML front end. Easy communication!

From 01542d7ea6b29c55cd8bc09528daa0cce2089ff4 Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Sat, 30 Nov 2024 07:22:28 -0600
Subject: [PATCH 27/49] Message formatting.

---
 scripts/system/domainChat/domainChat.js  |  86 ++++++++++++-
 scripts/system/domainChat/domainChat.qml | 157 ++++++++++++++---------
 2 files changed, 174 insertions(+), 69 deletions(-)

diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js
index d8341289ec..6bf42d5a5e 100644
--- a/scripts/system/domainChat/domainChat.js
+++ b/scripts/system/domainChat/domainChat.js
@@ -7,6 +7,8 @@
 //  Distributed under the Apache License, Version 2.0.
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
 
+// TODO: Message trimming
+
 (() => {
     ("use strict");
 
@@ -111,8 +113,12 @@
         // Format the timestamp 
         message.timeString = timeArray[0];
         message.dateString = timeArray[1];
-        
-        _emitEvent({ type: "show_message", ...message });                       // Update qml view of to new message.
+
+        let formattedMessage = _parseMessage(message.message);                  // Format the message for viewing
+        let formattedMessagePacket = { ...message };
+        formattedMessagePacket.message = formattedMessage
+
+        _emitEvent({ type: "show_message", ...formattedMessagePacket });           // Update qml view of to new message.
         _notificationCoreMessage(message.displayName, message.message)          // Show a new message on screen.
 
         // Create a new variable based on the message that will be saved.
@@ -239,9 +245,15 @@
             // Load message history
             messageHistory.forEach((message) => {
                 const timeArray = _formatTimestamp(_getTimestamp());
-                message.timeString = timeArray[0];
-                message.dateString = timeArray[1];
-                _emitEvent({ type: "show_message", ...message });
+                messagePacket = { ...message };
+                messagePacket.timeString = timeArray[0];
+                messagePacket.dateString = timeArray[1];
+
+                let formattedMessage = _parseMessage(messagePacket.message);
+                let formattedMessagePacket = messagePacket;
+                formattedMessagePacket.message = formattedMessage;
+
+                _emitEvent({ type: "show_message", ...formattedMessagePacket });
             });
         }
 
@@ -276,6 +288,70 @@
             JSON.stringify({ sender: displayName, text: message })
         );
     }
+    function _parseMessage(message){
+        const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;
+        const mentionRegex = /@(\w+)/; // FIXME: Remove - devcode
+        const overteLocationRegex = null;
+
+        let runningMessage = message;
+        let messageArray = [];
+
+        const regexPatterns = [
+            { type: "url", regex: urlRegex },
+            { type: "mention", regex: mentionRegex }, // FIXME: Remove - devcode
+            { type: "overteLocation", regex: overteLocationRegex }
+        ]
+
+        // Here is a link https://www.example.com, #hashtag, and @mention. Just for some spice here is another https://exampletwo.com
+
+        while (true) {
+            let firstMatch = _findFirstMatch();
+
+            if (firstMatch == null) {
+                // If there was not any matches found in the entire message, format the whole message as a single text entry.
+                messageArray.push({type: 'text', value: runningMessage});
+
+                // Append a final 'fill width' to the message text.
+                messageArray.push({type: 'messageEnd'});
+                break;
+            }
+
+            _formatMessage(firstMatch);
+        }
+
+        return messageArray;
+
+        function _formatMessage(firstMatch){
+            let indexOfFirstMatch = firstMatch[0];
+            let regex = regexPatterns[firstMatch[1]].regex;
+
+            let foundMatch = runningMessage.match(regex)[0];
+
+            messageArray.push({type: 'text', value: runningMessage.substring(0, indexOfFirstMatch)});
+            messageArray.push({type: regexPatterns[firstMatch[1]].type, value: runningMessage.substring(indexOfFirstMatch, indexOfFirstMatch + foundMatch.length)});
+            
+            runningMessage = runningMessage.substring(indexOfFirstMatch + foundMatch.length);   // Remove the part of the message we have worked with
+        }
+
+        function _findFirstMatch(){
+            let indexOfFirstMatch = Infinity;
+            let indexOfRegexPattern = Infinity;
+
+            for (let i = 0; regexPatterns.length > i; i++){
+                let indexOfMatch = runningMessage.search(regexPatterns[i].regex);
+
+                if (indexOfMatch == -1) continue;                                              // No match found
+
+                if (indexOfMatch < indexOfFirstMatch) {
+                    indexOfFirstMatch = indexOfMatch;
+                    indexOfRegexPattern = i;
+                }
+            }
+
+            if (indexOfFirstMatch !== Infinity) return [indexOfFirstMatch, indexOfRegexPattern];    // If there was a found match
+            return null;                                                                            // No found match
+        }
+    }
 
     /**
      * Emit a packet to the HTML front end. Easy communication!
diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml
index 07eb75c626..405e5d5ea9 100644
--- a/scripts/system/domainChat/domainChat.qml
+++ b/scripts/system/domainChat/domainChat.qml
@@ -162,7 +162,7 @@ Rectangle {
                         model: getChannel(pageVal)
                         delegate: Loader {
                             property int delegateIndex: model.index
-                            property string delegateText: model.text
+                            property var delegateText: model.text
                             property string delegateUsername: model.username
                             property string delegateDate: model.date
 
@@ -384,7 +384,7 @@ Rectangle {
 
         Rectangle {
             property int index: delegateIndex
-            property string texttest: delegateText
+            property var texttest: delegateText
             property string username: delegateUsername
             property string date: delegateDate
 
@@ -410,22 +410,83 @@ Rectangle {
                 }
             }
 
-            TextEdit {
-                anchors.top: parent.children[0].bottom
-                x: 5
-                text: texttest
-                color:"white"
-                font.pointSize: 12
-                readOnly: true
-                selectByMouse: true
-                selectByKeyboard: true
+            Flow {
+                anchors.top: parent.children[0].bottom;
                 width: parent.width * 0.8
-                height: contentHeight
-                wrapMode: Text.Wrap
-                textFormat: TextEdit.RichText
+                x: 5
 
-                onLinkActivated: {
-                    Window.openWebBrowser(link)
+                Repeater {
+                    model: texttest;
+
+                    RowLayout {
+                        width: {
+                            switch (model.type) {
+                                case "text":
+                                    return children[0].width;
+                                case "url":
+                                    return children[1].width;
+                            }
+                        }
+
+                        Text {
+                            text: model.value || ""
+                            font.pointSize: 12
+                            wrapMode: Text.Wrap
+                            width: Math.min(parent.parent.parent.width, contentWidth);
+
+                            visible: model.type === 'text' || model.type === 'mention';
+
+                            color: {
+                                switch (model.type) {
+                                    case "mention":
+                                        return "purple";
+                                    default:
+                                        return "white";
+                                }
+                            }
+                        }
+
+                        RowLayout {
+                            width: Math.min(parent.parent.parent.width, children[0].contentWidth);
+                            visible: model.type === 'url';
+
+                            Text {
+                                text: model.value || ""
+                                font.pointSize: 12
+                                wrapMode: Text.Wrap
+                                color: "#4EBAFD";
+                                font.underline: true
+
+                                MouseArea {
+                                    anchors.fill: parent;
+
+                                    onClicked: {
+                                        Window.openWebBrowser(model.value)
+                                    }
+                                }
+                            }
+
+                            Text {
+                                text: "🗗"
+                                font.pointSize: 10
+                                wrapMode: Text.Wrap
+                                color: "white"
+
+                                MouseArea {
+                                    anchors.fill: parent;
+
+                                    onClicked: {
+                                        Qt.openUrlExternally(model.value)
+                                    }
+                                }
+                            }
+                        }
+
+                        Item {
+                            Layout.fillWidth: true
+                            visible: model.type === 'messageEnd'
+                        }
+                    }
                 }
             }
         }
@@ -436,7 +497,7 @@ Rectangle {
 
         Rectangle{
             property int index: delegateIndex
-            property string texttest: delegateText
+            property var texttest: delegateText
             property string username: delegateUsername
             property string date: delegateDate
             color: "#171717"
@@ -517,8 +578,6 @@ Rectangle {
         channel = getChannel(channel)
 
         // Format content
-        message = formatContent(message);
-        message = embedImages(message);
 
         if (type === "notification"){
             channel.append({ text: message, date: date, type: "notification" });
@@ -529,23 +588,24 @@ Rectangle {
             return;
         }
 
-        var current_time = new Date();
-        var elapsed_time = current_time - last_message_time;
-        var elapsed_minutes = elapsed_time / (1000 * 60); 
+        // TODO: Replace new time generation with time pregenerated from message
+        // 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);
+        // 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 = "<br>" + message 
-            last_item.text = last_item.text += "\n" + message;
-            load_scroll_timer.running = true;
-            last_message_time = new Date();
-            return;
-        }
+        // if (last_message_user === username && elapsed_minutes < 1 && last_item){
+        //     message = "<br>" + message 
+        //     last_item.text = last_item.text += "\n" + message;
+        //     load_scroll_timer.running = true;
+        //     last_message_time = new Date();
+        //     return;
+        // }
 
-        last_message_user = username;
-        last_message_time = new Date();
+        // last_message_user = username;
+        // last_message_time = new Date();
         channel.append({ text: message, username: username, date: date, type: type });
         load_scroll_timer.running = true;
     }
@@ -554,37 +614,6 @@ Rectangle {
         return channels[id];
     }
 
-    function formatContent(mess) {
-        var arrow = /\</gi
-        mess = mess.replace(arrow, "&lt;");
-
-        var link = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g;
-        mess = mess.replace(link, (match) => {return `<a style="color:#4EBAFD" onclick='Window.openUrl("+match+")' href='` + match + `'>` + match + `</a> <a onclick='Window.openUrl(`+match+`)'>🗗</a>`});
-
-        var newline = /\n/gi;
-        mess = mess.replace(newline, "<br>");
-        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 += "<br><img src="+ matches[i] +" width='250' >"
-                listed.push(matches[i]);
-                total_emeds++
-            } 
-        }
-        return new_message;
-    }
-
     // Messages from script
     function fromScript(message) {
 

From e340741dcef9b41328f5db5badc22038254f6795 Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Sat, 30 Nov 2024 09:29:08 -0600
Subject: [PATCH 28/49] World links.

---
 scripts/system/domainChat/domainChat.js  |  4 +-
 scripts/system/domainChat/domainChat.qml | 71 ++++++++++++++++++++----
 2 files changed, 61 insertions(+), 14 deletions(-)

diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js
index 6bf42d5a5e..aefe04dd53 100644
--- a/scripts/system/domainChat/domainChat.js
+++ b/scripts/system/domainChat/domainChat.js
@@ -118,7 +118,7 @@
         let formattedMessagePacket = { ...message };
         formattedMessagePacket.message = formattedMessage
 
-        _emitEvent({ type: "show_message", ...formattedMessagePacket });           // Update qml view of to new message.
+        _emitEvent({ type: "show_message", ...formattedMessagePacket });        // Update qml view of to new message.
         _notificationCoreMessage(message.displayName, message.message)          // Show a new message on screen.
 
         // Create a new variable based on the message that will be saved.
@@ -291,7 +291,7 @@
     function _parseMessage(message){
         const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;
         const mentionRegex = /@(\w+)/; // FIXME: Remove - devcode
-        const overteLocationRegex = null;
+        const overteLocationRegex = /hifi:\/\/[a-zA-Z0-9_-]+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+/;
 
         let runningMessage = message;
         let messageArray = [];
diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml
index 405e5d5ea9..46340d233a 100644
--- a/scripts/system/domainChat/domainChat.qml
+++ b/scripts/system/domainChat/domainChat.qml
@@ -414,6 +414,7 @@ Rectangle {
                 anchors.top: parent.children[0].bottom;
                 width: parent.width * 0.8
                 x: 5
+                id: messageBoxFlow
 
                 Repeater {
                     model: texttest;
@@ -432,7 +433,7 @@ Rectangle {
                             text: model.value || ""
                             font.pointSize: 12
                             wrapMode: Text.Wrap
-                            width: Math.min(parent.parent.parent.width, contentWidth);
+                            width: Math.min(messageBoxFlow.width, contentWidth);
 
                             visible: model.type === 'text' || model.type === 'mention';
 
@@ -447,41 +448,87 @@ Rectangle {
                         }
 
                         RowLayout {
-                            width: Math.min(parent.parent.parent.width, children[0].contentWidth);
+                            width: Math.min(messageBoxFlow.width, children[0].contentWidth);
                             visible: model.type === 'url';
 
                             Text {
-                                text: model.value || ""
-                                font.pointSize: 12
-                                wrapMode: Text.Wrap
+                                text: model.value || "";
+                                font.pointSize: 12;
+                                wrapMode: Text.Wrap;
                                 color: "#4EBAFD";
-                                font.underline: true
+                                font.underline: true;
+                                width: parent.width;
 
                                 MouseArea {
                                     anchors.fill: parent;
 
                                     onClicked: {
-                                        Window.openWebBrowser(model.value)
+                                        Window.openWebBrowser(model.value);
                                     }
                                 }
                             }
 
                             Text {
-                                text: "🗗"
-                                font.pointSize: 10
-                                wrapMode: Text.Wrap
-                                color: "white"
+                                text: "🗗";
+                                font.pointSize: 10;
+                                wrapMode: Text.Wrap;
+                                color: "white";
 
                                 MouseArea {
                                     anchors.fill: parent;
 
                                     onClicked: {
-                                        Qt.openUrlExternally(model.value)
+                                        Qt.openUrlExternally(model.value);
                                     }
                                 }
                             }
                         }
 
+                        RowLayout {
+                            visible: model.type === 'overteLocation';
+                            width: Math.min(messageBoxFlow.width, children[0].children[1].contentWidth + 35);
+                            height: 20;
+                            Layout.leftMargin: 5
+                            Layout.rightMargin: 5
+
+                            Rectangle {
+                                width: parent.width;
+                                height: 20;
+                                color: "lightgray"
+                                radius: 2;
+
+                                Image {
+                                    source: "./img/ui/world_black.png"
+                                    width: 18;
+                                    height: 18;
+                                    sourceSize.width: 18
+                                    sourceSize.height: 18
+                                    anchors.left: parent.left
+                                    anchors.verticalCenter: parent.verticalCenter 
+                                    anchors.leftMargin: 2
+                                    anchors.rightMargin: 10
+                                }
+
+                                Text {
+                                    text: model.value.split('hifi://')[1].split('/')[0];
+                                    color: "black"
+                                    font.pointSize: 12
+                                    x: parent.children[0].width + 5;
+                                    anchors.verticalCenter: parent.verticalCenter 
+                                }
+
+                                MouseArea {
+                                    anchors.fill: parent;
+
+                                    onClicked: {
+                                        Window.openUrl(model.value);
+                                    }
+                                }
+
+                            }
+                        }
+
+
                         Item {
                             Layout.fillWidth: true
                             visible: model.type === 'messageEnd'

From 907e9775086dc2ec135b5137a31729a70b160c26 Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Tue, 17 Dec 2024 18:13:49 -0600
Subject: [PATCH 29/49] Message media embedding.

---
 scripts/system/domainChat/domainChat.js  | 60 +++++++++++++++++++-----
 scripts/system/domainChat/domainChat.qml | 15 +++++-
 2 files changed, 63 insertions(+), 12 deletions(-)

diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js
index aefe04dd53..57fe1241c4 100644
--- a/scripts/system/domainChat/domainChat.js
+++ b/scripts/system/domainChat/domainChat.js
@@ -94,7 +94,7 @@
         chatOverlayWindow.fromQml.connect(fromQML);
         quickMessage.fromQml.connect(fromQML);
     }
-    function receivedMessage(channel, message) {
+    async function receivedMessage(channel, message) {
         // Is the message a chat message?
         channel = channel.toLowerCase();
         if (channel !== "chat") return;
@@ -114,9 +114,8 @@
         message.timeString = timeArray[0];
         message.dateString = timeArray[1];
 
-        let formattedMessage = _parseMessage(message.message);                  // Format the message for viewing
         let formattedMessagePacket = { ...message };
-        formattedMessagePacket.message = formattedMessage
+        formattedMessagePacket.message = await _parseMessage(message.message)
 
         _emitEvent({ type: "show_message", ...formattedMessagePacket });        // Update qml view of to new message.
         _notificationCoreMessage(message.displayName, message.message)          // Show a new message on screen.
@@ -238,23 +237,22 @@
             _emitEvent({ type: "notification", ...message });
         }, 1500);
     }
-    function _loadSettings() {
+    async function _loadSettings() {
         settings = Settings.getValue("ArmoredChat-Config", settings);
 
         if (messageHistory) {
             // Load message history
-            messageHistory.forEach((message) => {
+            for (message of messageHistory) {
                 const timeArray = _formatTimestamp(_getTimestamp());
                 messagePacket = { ...message };
                 messagePacket.timeString = timeArray[0];
                 messagePacket.dateString = timeArray[1];
 
-                let formattedMessage = _parseMessage(messagePacket.message);
-                let formattedMessagePacket = messagePacket;
-                formattedMessagePacket.message = formattedMessage;
+                let formattedMessagePacket = {...messagePacket};
+                formattedMessagePacket.message = await _parseMessage(messagePacket.message);
 
                 _emitEvent({ type: "show_message", ...formattedMessagePacket });
-            });
+            }
         }
 
         // Send current settings to the app
@@ -288,7 +286,7 @@
             JSON.stringify({ sender: displayName, text: message })
         );
     }
-    function _parseMessage(message){
+    async function _parseMessage(message){
         const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;
         const mentionRegex = /@(\w+)/; // FIXME: Remove - devcode
         const overteLocationRegex = /hifi:\/\/[a-zA-Z0-9_-]+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+/;
@@ -309,7 +307,9 @@
 
             if (firstMatch == null) {
                 // If there was not any matches found in the entire message, format the whole message as a single text entry.
-                messageArray.push({type: 'text', value: runningMessage});
+                if (messageArray.length == 0) {
+                    messageArray.push({type: 'text', value: runningMessage});
+                }
 
                 // Append a final 'fill width' to the message text.
                 messageArray.push({type: 'messageEnd'});
@@ -319,6 +319,20 @@
             _formatMessage(firstMatch);
         }
 
+        for (dataChunk of messageArray){
+            if (dataChunk.type == 'url'){
+                let url = dataChunk.value;
+
+                const res = await fetch(url, {method: 'GET'});      // TODO: Replace with 'HEAD' method. https://github.com/overte-org/overte/issues/1273
+                const contentType = res.getResponseHeader("content-type");
+
+                // TODO: Add support for other media types
+                if (contentType.startsWith('image/')) {
+                    messageArray.push({type: 'imageEmbed', value: url});
+                }
+            }
+        }
+
         return messageArray;
 
         function _formatMessage(firstMatch){
@@ -361,4 +375,28 @@
     function _emitEvent(packet = { type: "" }) {
         chatOverlayWindow.sendToQml(packet);
     }
+
+    function fetch(url, options = {method: "GET"}) {
+        return new Promise((resolve, reject) => {
+            let req = new XMLHttpRequest();
+
+            req.onreadystatechange = function () {
+
+                if (req.readyState === req.DONE) {
+                    if (req.status === 200) {
+                        console.log("Content type:", req.getResponseHeader("content-type"));
+                        resolve(req);
+            
+                    } else {
+                        console.log("Error", req.status, req.statusText);
+                        reject();
+                    }
+                }
+            };
+
+            req.open(options.method, url);
+            req.send();
+        });
+    }
+
 })();
diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml
index 46340d233a..f0350e972a 100644
--- a/scripts/system/domainChat/domainChat.qml
+++ b/scripts/system/domainChat/domainChat.qml
@@ -510,7 +510,7 @@ Rectangle {
                                 }
 
                                 Text {
-                                    text: model.value.split('hifi://')[1].split('/')[0];
+                                    text: model.type === 'overteLocation' ? model.value.split('hifi://')[1].split('/')[0] : '';
                                     color: "black"
                                     font.pointSize: 12
                                     x: parent.children[0].width + 5;
@@ -533,6 +533,18 @@ Rectangle {
                             Layout.fillWidth: true
                             visible: model.type === 'messageEnd'
                         }
+
+                        Item {
+                            visible: model.type === 'imageEmbed';
+                            width: messageBoxFlow.width;
+                            height: 200
+
+                            Image {
+                                source: model.type === 'imageEmbed' ? model.value : ''
+                                sourceSize.width: 400
+                                sourceSize.height: 200
+                            }
+                        }
                     }
                 }
             }
@@ -653,6 +665,7 @@ Rectangle {
 
         // last_message_user = username;
         // last_message_time = new Date();
+
         channel.append({ text: message, username: username, date: date, type: type });
         load_scroll_timer.running = true;
     }

From 00451ca1f70a52aa8c227120491dcd59202de5f8 Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Wed, 18 Dec 2024 05:04:23 -0600
Subject: [PATCH 30/49] Fix domain notifications.

---
 scripts/system/domainChat/domainChat.qml | 76 ++++++------------------
 1 file changed, 18 insertions(+), 58 deletions(-)

diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml
index f0350e972a..2220127f98 100644
--- a/scripts/system/domainChat/domainChat.qml
+++ b/scripts/system/domainChat/domainChat.qml
@@ -167,11 +167,8 @@ Rectangle {
                             property string delegateDate: model.date
 
                             sourceComponent: {
-                                if (model.type === "chat") {
-                                    return template_chat_message;
-                                } else if (model.type === "notification") {
-                                    return template_notification;
-                                }
+                                if (model.type === "chat") return template_chat_message;
+                                if (model.type === "notification") return template_notification;
                             }
                         
                         }
@@ -384,9 +381,7 @@ Rectangle {
 
         Rectangle {
             property int index: delegateIndex
-            property var 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)
@@ -405,7 +400,7 @@ Rectangle {
 
                 Text{
                     anchors.right: parent.right
-                    text: date
+                    text: delegateDate
                     color: "lightgray"
                 }
             }
@@ -417,24 +412,14 @@ Rectangle {
                 id: messageBoxFlow
 
                 Repeater {
-                    model: texttest;
+                    model: delegateText;
 
                     RowLayout {
-                        width: {
-                            switch (model.type) {
-                                case "text":
-                                    return children[0].width;
-                                case "url":
-                                    return children[1].width;
-                            }
-                        }
-
                         Text {
                             text: model.value || ""
                             font.pointSize: 12
                             wrapMode: Text.Wrap
-                            width: Math.min(messageBoxFlow.width, contentWidth);
-
+                            width: model.type === 'text' || model.type === 'mention' ? Math.min(messageBoxFlow.width, contentWidth) : 0;
                             visible: model.type === 'text' || model.type === 'mention';
 
                             color: {
@@ -448,7 +433,7 @@ Rectangle {
                         }
 
                         RowLayout {
-                            width: Math.min(messageBoxFlow.width, children[0].contentWidth);
+                            width: children[0].contentWidth;
                             visible: model.type === 'url';
 
                             Text {
@@ -524,11 +509,9 @@ Rectangle {
                                         Window.openUrl(model.value);
                                     }
                                 }
-
                             }
                         }
 
-
                         Item {
                             Layout.fillWidth: true
                             visible: model.type === 'messageEnd'
@@ -554,11 +537,10 @@ Rectangle {
     Component {
         id: template_notification
 
-        Rectangle{
+        Rectangle {
             property int index: delegateIndex
-            property var texttest: delegateText
             property string username: delegateUsername
-            property string date: delegateDate
+
             color: "#171717"
             width: parent.width
             height: 40
@@ -574,15 +556,14 @@ Rectangle {
                 }
             }
 
-
             Item {
                 width: parent.width - parent.children[0].width - 5
                 height: parent.height
                 anchors.left: parent.children[0].right
 
-                TextEdit{
-                    text: texttest
-                    color:"white"
+                TextEdit {
+                    text: delegateText
+                    color: "white"
                     font.pointSize: 12
                     readOnly: true
                     width: parent.width * 0.8
@@ -595,8 +576,8 @@ Rectangle {
                 }
 
                 Text {
-                    text: date
-                    color:"white"
+                    text: delegateDate
+                    color: "white"
                     font.pointSize: 12
                     anchors.right: parent.right
                     height: parent.height
@@ -617,16 +598,16 @@ Rectangle {
     }
 
     function scrollToBottom(bypassDistanceCheck = false, extraMoveDistance = 0) {
-        const totalHeight = listview.height; // Total height of the content
-        const currentPosition = messageViewFlickable.contentY; // Current position of the view
-        const windowHeight = listview.parent.parent.height; // Total height of the window
+        const totalHeight = listview.height;                    // Total height of the content
+        const currentPosition = messageViewFlickable.contentY;  // Current position of the view
+        const windowHeight = listview.parent.parent.height;     // Total height of the window
         const bottomPosition = currentPosition + windowHeight;
 
         // Check if the view is within 300 units from the bottom
         const closeEnoughToBottom = totalHeight - bottomPosition <= 300;
         if (!bypassDistanceCheck && !closeEnoughToBottom) return;
-        if (totalHeight < windowHeight) return; // No reason to scroll, we don't have an overflow.
-        if (bottomPosition == totalHeight) return; // At the bottom, do nothing.
+        if (totalHeight < windowHeight) return;                 // No reason to scroll, we don't have an overflow.
+        if (bottomPosition == totalHeight) return;              // At the bottom, do nothing.
 
         messageViewFlickable.contentY = listview.height - listview.parent.parent.height;
         messageViewFlickable.returnToBounds();
@@ -640,32 +621,11 @@ Rectangle {
 
         if (type === "notification"){
             channel.append({ text: message, date: date, type: "notification" });
-            last_message_user = "";
             scrollToBottom(null, 30);
 
-            last_message_time = new Date();
             return;
         }
 
-        // TODO: Replace new time generation with time pregenerated from message
-        // 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 = "<br>" + message 
-        //     last_item.text = last_item.text += "\n" + message;
-        //     load_scroll_timer.running = true;
-        //     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 });
         load_scroll_timer.running = true;
     }

From 48c690a2df7c45957839d658e7c94f5392150360 Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Wed, 18 Dec 2024 17:38:52 -0600
Subject: [PATCH 31/49] Broke out templates into their own files.

---
 scripts/system/domainChat/domainChat.qml      | 218 +-----------------
 .../qml_widgets/TemplateChatMessage.qml       | 161 +++++++++++++
 .../qml_widgets/TemplateNotification.qml      |  59 +++++
 3 files changed, 223 insertions(+), 215 deletions(-)
 create mode 100644 scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
 create mode 100644 scripts/system/domainChat/qml_widgets/TemplateNotification.qml

diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml
index 2220127f98..f40121ee71 100644
--- a/scripts/system/domainChat/domainChat.qml
+++ b/scripts/system/domainChat/domainChat.qml
@@ -2,6 +2,7 @@ import QtQuick 2.7
 import QtQuick.Controls 2.15
 import QtQuick.Layouts 1.3
 import controlsUit 1.0 as HifiControlsUit
+import "./qml_widgets"
 
 Rectangle {
     color: Qt.rgba(0.1,0.1,0.1,1)
@@ -376,221 +377,8 @@ Rectangle {
     }
 
     // Templates
-    Component {
-        id: template_chat_message
-
-        Rectangle {
-            property int index: delegateIndex
-            property string username: delegateUsername
-
-            height: Math.max(65, children[1].height + 30)
-            color: index % 2 === 0 ? "transparent" : Qt.rgba(0.15,0.15,0.15,1)
-            width: listview.parent.parent.width
-            Layout.fillWidth: true
-
-            Item {
-                width: parent.width - 10
-                anchors.horizontalCenter: parent.horizontalCenter
-                height: 22
-
-                Text{
-                    text: username
-                    color: "lightgray"
-                }
-
-                Text{
-                    anchors.right: parent.right
-                    text: delegateDate
-                    color: "lightgray"
-                }
-            }
-
-            Flow {
-                anchors.top: parent.children[0].bottom;
-                width: parent.width * 0.8
-                x: 5
-                id: messageBoxFlow
-
-                Repeater {
-                    model: delegateText;
-
-                    RowLayout {
-                        Text {
-                            text: model.value || ""
-                            font.pointSize: 12
-                            wrapMode: Text.Wrap
-                            width: model.type === 'text' || model.type === 'mention' ? Math.min(messageBoxFlow.width, contentWidth) : 0;
-                            visible: model.type === 'text' || model.type === 'mention';
-
-                            color: {
-                                switch (model.type) {
-                                    case "mention":
-                                        return "purple";
-                                    default:
-                                        return "white";
-                                }
-                            }
-                        }
-
-                        RowLayout {
-                            width: children[0].contentWidth;
-                            visible: model.type === 'url';
-
-                            Text {
-                                text: model.value || "";
-                                font.pointSize: 12;
-                                wrapMode: Text.Wrap;
-                                color: "#4EBAFD";
-                                font.underline: true;
-                                width: parent.width;
-
-                                MouseArea {
-                                    anchors.fill: parent;
-
-                                    onClicked: {
-                                        Window.openWebBrowser(model.value);
-                                    }
-                                }
-                            }
-
-                            Text {
-                                text: "🗗";
-                                font.pointSize: 10;
-                                wrapMode: Text.Wrap;
-                                color: "white";
-
-                                MouseArea {
-                                    anchors.fill: parent;
-
-                                    onClicked: {
-                                        Qt.openUrlExternally(model.value);
-                                    }
-                                }
-                            }
-                        }
-
-                        RowLayout {
-                            visible: model.type === 'overteLocation';
-                            width: Math.min(messageBoxFlow.width, children[0].children[1].contentWidth + 35);
-                            height: 20;
-                            Layout.leftMargin: 5
-                            Layout.rightMargin: 5
-
-                            Rectangle {
-                                width: parent.width;
-                                height: 20;
-                                color: "lightgray"
-                                radius: 2;
-
-                                Image {
-                                    source: "./img/ui/world_black.png"
-                                    width: 18;
-                                    height: 18;
-                                    sourceSize.width: 18
-                                    sourceSize.height: 18
-                                    anchors.left: parent.left
-                                    anchors.verticalCenter: parent.verticalCenter 
-                                    anchors.leftMargin: 2
-                                    anchors.rightMargin: 10
-                                }
-
-                                Text {
-                                    text: model.type === 'overteLocation' ? model.value.split('hifi://')[1].split('/')[0] : '';
-                                    color: "black"
-                                    font.pointSize: 12
-                                    x: parent.children[0].width + 5;
-                                    anchors.verticalCenter: parent.verticalCenter 
-                                }
-
-                                MouseArea {
-                                    anchors.fill: parent;
-
-                                    onClicked: {
-                                        Window.openUrl(model.value);
-                                    }
-                                }
-                            }
-                        }
-
-                        Item {
-                            Layout.fillWidth: true
-                            visible: model.type === 'messageEnd'
-                        }
-
-                        Item {
-                            visible: model.type === 'imageEmbed';
-                            width: messageBoxFlow.width;
-                            height: 200
-
-                            Image {
-                                source: model.type === 'imageEmbed' ? model.value : ''
-                                sourceSize.width: 400
-                                sourceSize.height: 200
-                            }
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    Component {
-        id: template_notification
-
-        Rectangle {
-            property int index: delegateIndex
-            property string username: delegateUsername
-
-            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: delegateText
-                    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: delegateDate
-                    color: "white"
-                    font.pointSize: 12
-                    anchors.right: parent.right
-                    height: parent.height
-                    wrapMode: Text.Wrap
-                    horizontalAlignment: Text.AlignRight
-                    verticalAlignment: Text.AlignVCenter
-                    font.italic: true
-                }
-            }
-
-        }
-
-    }
+    TemplateChatMessage { id: template_chat_message }
+    TemplateNotification { id: template_notification }
 
     property var channels: {
         "local": local,
diff --git a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
new file mode 100644
index 0000000000..4165e8d37c
--- /dev/null
+++ b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
@@ -0,0 +1,161 @@
+import QtQuick 2.7
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.3
+
+Component {
+	id: template_chat_message
+
+	Rectangle {
+		property int index: delegateIndex
+		property string username: delegateUsername
+
+		height: Math.max(65, children[1].height + 30)
+		color: index % 2 === 0 ? "transparent" : Qt.rgba(0.15,0.15,0.15,1)
+		width: listview.parent.parent.width
+		Layout.fillWidth: true
+
+		Item {
+			width: parent.width - 10
+			anchors.horizontalCenter: parent.horizontalCenter
+			height: 22
+
+			Text{
+				text: username
+				color: "lightgray"
+			}
+
+			Text{
+				anchors.right: parent.right
+				text: delegateDate
+				color: "lightgray"
+			}
+		}
+
+		Flow {
+			anchors.top: parent.children[0].bottom;
+			width: parent.width * 0.8
+			x: 5
+			id: messageBoxFlow
+
+			Repeater {
+				model: delegateText;
+
+				RowLayout {
+					Text {
+						text: model.value || ""
+						font.pointSize: 12
+						wrapMode: Text.Wrap
+						width: model.type === 'text' || model.type === 'mention' ? Math.min(messageBoxFlow.width, contentWidth) : 0;
+						visible: model.type === 'text' || model.type === 'mention';
+
+						color: {
+							switch (model.type) {
+								case "mention":
+									return "purple";
+								default:
+									return "white";
+							}
+						}
+					}
+
+					RowLayout {
+						width: children[0].contentWidth;
+						visible: model.type === 'url';
+
+						Text {
+							text: model.value || "";
+							font.pointSize: 12;
+							wrapMode: Text.Wrap;
+							color: "#4EBAFD";
+							font.underline: true;
+							width: parent.width;
+
+							MouseArea {
+								anchors.fill: parent;
+
+								onClicked: {
+									Window.openWebBrowser(model.value);
+								}
+							}
+						}
+
+						Text {
+							text: "🗗";
+							font.pointSize: 10;
+							wrapMode: Text.Wrap;
+							color: "white";
+
+							MouseArea {
+								anchors.fill: parent;
+
+								onClicked: {
+									Qt.openUrlExternally(model.value);
+								}
+							}
+						}
+					}
+
+					RowLayout {
+						visible: model.type === 'overteLocation';
+						width: Math.min(messageBoxFlow.width, children[0].children[1].contentWidth + 35);
+						height: 20;
+						Layout.leftMargin: 5
+						Layout.rightMargin: 5
+
+						Rectangle {
+							width: parent.width;
+							height: 20;
+							color: "lightgray"
+							radius: 2;
+
+							Image {
+								source: "./img/ui/world_black.png"
+								width: 18;
+								height: 18;
+								sourceSize.width: 18
+								sourceSize.height: 18
+								anchors.left: parent.left
+								anchors.verticalCenter: parent.verticalCenter 
+								anchors.leftMargin: 2
+								anchors.rightMargin: 10
+							}
+
+							Text {
+								text: model.type === 'overteLocation' ? model.value.split('hifi://')[1].split('/')[0] : '';
+								color: "black"
+								font.pointSize: 12
+								x: parent.children[0].width + 5;
+								anchors.verticalCenter: parent.verticalCenter 
+							}
+
+							MouseArea {
+								anchors.fill: parent;
+
+								onClicked: {
+									Window.openUrl(model.value);
+								}
+							}
+						}
+					}
+
+					Item {
+						Layout.fillWidth: true
+						visible: model.type === 'messageEnd'
+					}
+
+					Item {
+						visible: model.type === 'imageEmbed';
+						width: messageBoxFlow.width;
+						height: 200
+
+						Image {
+							source: model.type === 'imageEmbed' ? model.value : ''
+							sourceSize.width: 400
+							sourceSize.height: 200
+						}
+					}
+				}
+			}
+		}
+	}
+}
\ No newline at end of file
diff --git a/scripts/system/domainChat/qml_widgets/TemplateNotification.qml b/scripts/system/domainChat/qml_widgets/TemplateNotification.qml
new file mode 100644
index 0000000000..e522d58c54
--- /dev/null
+++ b/scripts/system/domainChat/qml_widgets/TemplateNotification.qml
@@ -0,0 +1,59 @@
+import QtQuick 2.7
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.3
+
+Component {
+	id: template_notification
+
+	Rectangle {
+		property int index: delegateIndex
+		property string username: delegateUsername
+
+		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: delegateText
+				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: delegateDate
+				color: "white"
+				font.pointSize: 12
+				anchors.right: parent.right
+				height: parent.height
+				wrapMode: Text.Wrap
+				horizontalAlignment: Text.AlignRight
+				verticalAlignment: Text.AlignVCenter
+				font.italic: true
+			}
+		}
+	}
+}
\ No newline at end of file

From 7d789acaa6a4ebdd413225da29440f9010e28140 Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Thu, 19 Dec 2024 04:41:49 -0600
Subject: [PATCH 32/49] Actually fix notifications.

---
 scripts/system/domainChat/domainChat.js       | 16 +++---
 scripts/system/domainChat/domainChat.qml      | 10 ++--
 .../qml_widgets/TemplateChatMessage.qml       |  9 ++--
 .../qml_widgets/TemplateNotification.qml      | 52 ++++++-------------
 4 files changed, 32 insertions(+), 55 deletions(-)

diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js
index 57fe1241c4..780879f8c1 100644
--- a/scripts/system/domainChat/domainChat.js
+++ b/scripts/system/domainChat/domainChat.js
@@ -32,12 +32,8 @@
     Controller.keyPressEvent.connect(keyPressEvent);
     Messages.subscribe("chat");
     Messages.messageReceived.connect(receivedMessage);
-    AvatarManager.avatarAddedEvent.connect((sessionId) => {
-        _avatarAction("connected", sessionId);
-    });
-    AvatarManager.avatarRemovedEvent.connect((sessionId) => {
-        _avatarAction("left", sessionId);
-    });
+    AvatarManager.avatarAddedEvent.connect((sessionId) => { _avatarAction("connected", sessionId); });
+    AvatarManager.avatarRemovedEvent.connect((sessionId) => { _avatarAction("left", sessionId); });
 
     startup();
 
@@ -206,7 +202,7 @@
         );
     }
     function _avatarAction(type, sessionId) {
-        Script.setTimeout(() => {
+        Script.setTimeout(async () => {
             if (type == "connected") {
                 palData = AvatarManager.getPalData().data;
             }
@@ -234,7 +230,11 @@
                 _notificationCoreMessage(displayName, type)
             }
 
-            _emitEvent({ type: "notification", ...message });
+            // Format notification message
+            let formattedMessagePacket = {...message};
+            formattedMessagePacket.message = await _parseMessage(message.message);
+
+            _emitEvent({ type: "notification", ...formattedMessagePacket });
         }, 1500);
     }
     async function _loadSettings() {
diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml
index f40121ee71..6a79cb7e2e 100644
--- a/scripts/system/domainChat/domainChat.qml
+++ b/scripts/system/domainChat/domainChat.qml
@@ -9,7 +9,6 @@ Rectangle {
     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.
@@ -163,7 +162,7 @@ Rectangle {
                         model: getChannel(pageVal)
                         delegate: Loader {
                             property int delegateIndex: model.index
-                            property var delegateText: model.text
+                            property var delegateText: model.message
                             property string delegateUsername: model.username
                             property string delegateDate: model.date
 
@@ -171,7 +170,6 @@ Rectangle {
                                 if (model.type === "chat") return template_chat_message;
                                 if (model.type === "notification") return template_notification;
                             }
-                        
                         }
                     }
                 }
@@ -406,15 +404,13 @@ Rectangle {
         channel = getChannel(channel)
 
         // Format content
-
         if (type === "notification"){
-            channel.append({ text: message, date: date, type: "notification" });
+            channel.append({ message: message, date: date, type: "notification" });
             scrollToBottom(null, 30);
-
             return;
         }
 
-        channel.append({ text: message, username: username, date: date, type: type });
+        channel.append({ message: message, username: username, date: date, type: type });
         load_scroll_timer.running = true;
     }
 
diff --git a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
index 4165e8d37c..a9c366ce76 100644
--- a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
+++ b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
@@ -7,7 +7,6 @@ Component {
 
 	Rectangle {
 		property int index: delegateIndex
-		property string username: delegateUsername
 
 		height: Math.max(65, children[1].height + 30)
 		color: index % 2 === 0 ? "transparent" : Qt.rgba(0.15,0.15,0.15,1)
@@ -19,12 +18,12 @@ Component {
 			anchors.horizontalCenter: parent.horizontalCenter
 			height: 22
 
-			Text{
-				text: username
+			Text {
+				text: delegateUsername
 				color: "lightgray"
 			}
 
-			Text{
+			Text {
 				anchors.right: parent.right
 				text: delegateDate
 				color: "lightgray"
@@ -109,7 +108,7 @@ Component {
 							radius: 2;
 
 							Image {
-								source: "./img/ui/world_black.png"
+								source: "../img/ui/world_black.png"
 								width: 18;
 								height: 18;
 								sourceSize.width: 18
diff --git a/scripts/system/domainChat/qml_widgets/TemplateNotification.qml b/scripts/system/domainChat/qml_widgets/TemplateNotification.qml
index e522d58c54..4b9797d7f4 100644
--- a/scripts/system/domainChat/qml_widgets/TemplateNotification.qml
+++ b/scripts/system/domainChat/qml_widgets/TemplateNotification.qml
@@ -6,15 +6,12 @@ Component {
 	id: template_notification
 
 	Rectangle {
-		property int index: delegateIndex
-		property string username: delegateUsername
-
 		color: "#171717"
 		width: parent.width
 		height: 40
 
-		Item {
-			width: 10
+		RowLayout {
+			width: parent.width
 			height: parent.height
 
 			Rectangle {
@@ -22,38 +19,23 @@ Component {
 				width: 5
 				color: "#505186"
 			}
-		}
 
-		Item {
-			width: parent.width - parent.children[0].width - 5
-			height: parent.height
-			anchors.left: parent.children[0].right
+			Repeater {
+				model: delegateText
 
-			TextEdit {
-				text: delegateText
-				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: delegateDate
-				color: "white"
-				font.pointSize: 12
-				anchors.right: parent.right
-				height: parent.height
-				wrapMode: Text.Wrap
-				horizontalAlignment: Text.AlignRight
-				verticalAlignment: Text.AlignVCenter
-				font.italic: true
+				TextEdit {
+					visible: model.value != undefined;
+					text: model.value || ""
+					color: "white"
+					font.pointSize: 12
+					readOnly: true
+					selectByMouse: true
+					selectByKeyboard: true
+					height: root.height
+					wrapMode: Text.Wrap
+					font.italic: true
+				}
 			}
 		}
 	}
-}
\ No newline at end of file
+}

From 0c3fbbbeb9ae63fd0bc8c9dd7e21f6bab18fa8d2 Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Thu, 19 Dec 2024 04:48:56 -0600
Subject: [PATCH 33/49] Fix formatting sometimes not formatting the last part
 of a message.

---
 scripts/system/domainChat/domainChat.js | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js
index 780879f8c1..411e6bd278 100644
--- a/scripts/system/domainChat/domainChat.js
+++ b/scripts/system/domainChat/domainChat.js
@@ -306,10 +306,8 @@
             let firstMatch = _findFirstMatch();
 
             if (firstMatch == null) {
-                // If there was not any matches found in the entire message, format the whole message as a single text entry.
-                if (messageArray.length == 0) {
-                    messageArray.push({type: 'text', value: runningMessage});
-                }
+                // Format any remaining text as a basic 'text' type.
+                messageArray.push({type: 'text', value: runningMessage});
 
                 // Append a final 'fill width' to the message text.
                 messageArray.push({type: 'messageEnd'});

From ae56632c644c7cfec0ab145371fe6e89ee4d5460 Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Thu, 19 Dec 2024 21:48:05 -0600
Subject: [PATCH 34/49] Housekeeping: data formatting

---
 scripts/system/domainChat/domainChat.js | 72 +++++++------------------
 scripts/system/domainChat/formatting.js | 64 ++++++++++++++++++++++
 2 files changed, 82 insertions(+), 54 deletions(-)
 create mode 100644 scripts/system/domainChat/formatting.js

diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js
index 411e6bd278..9428ef51fa 100644
--- a/scripts/system/domainChat/domainChat.js
+++ b/scripts/system/domainChat/domainChat.js
@@ -12,6 +12,10 @@
 (() => {
     ("use strict");
 
+    Script.include([
+        "./formatting.js"
+    ])
+
     var appIsVisible = false;
     var settings = {
         external_window: false,
@@ -94,22 +98,16 @@
         // Is the message a chat message?
         channel = channel.toLowerCase();
         if (channel !== "chat") return;
-        message = JSON.parse(message);
-
-        // Get the message data
-        const currentTimestamp = _getTimestamp();
-        const timeArray = _formatTimestamp(currentTimestamp);
-
+        
+        if ((message = formatting.toJSON(message)) == null) return;             // Make sure we are working with a JSON object we expect, otherwise kill
+        message = formatting.addTimeAndDateStringToPacket(message);  
+        
         if (!message.channel) message.channel = "domain";                       // We don't know where to put this message. Assume it is a domain wide message.
         message.channel = message.channel.toLowerCase();                        // Only recognize channel names as lower case. 
         
         if (!channels.includes(message.channel)) return;                        // Check the channel. If the channel is not one we have, do nothing.
         if (message.channel == "local" && isTooFar(message.position)) return;   // If message is local, and if player is too far away from location, do nothing.
-
-        // Format the timestamp 
-        message.timeString = timeArray[0];
-        message.dateString = timeArray[1];
-
+        
         let formattedMessagePacket = { ...message };
         formattedMessagePacket.message = await _parseMessage(message.message)
 
@@ -117,24 +115,16 @@
         _notificationCoreMessage(message.displayName, message.message)          // Show a new message on screen.
 
         // Create a new variable based on the message that will be saved.
-        let savedMessage = message;
+        let trimmedPacket = formatting.trimPacketToSave(message);
+        messageHistory.push(trimmedPacket);
 
-        // Remove unnecessary data.
-        delete savedMessage.position;
-        delete savedMessage.timeString;
-        delete savedMessage.dateString;
-        delete savedMessage.action;
-
-        savedMessage.timestamp = currentTimestamp;
-
-        messageHistory.push(savedMessage);
         while (messageHistory.length > settings.maximum_messages) {
             messageHistory.shift();
         }
         Settings.setValue("ArmoredChat-Messages", messageHistory);
 
-        // Check to see if the message is close enough to the user
         function isTooFar(messagePosition) {
+            // Check to see if the message is close enough to the user
             return Vec3.distance(MyAvatar.position, messagePosition) > maxLocalDistance;
         }
     }
@@ -219,10 +209,7 @@
             }
 
             // Format the packet
-            let message = {};
-            const timeArray = _formatTimestamp(_getTimestamp());
-            message.timeString = timeArray[0];
-            message.dateString = timeArray[1];
+            let message = addTimeAndDateStringToPacket({});
             message.message = `${displayName} ${type}`;
 
             // Show new message on screen
@@ -243,43 +230,20 @@
         if (messageHistory) {
             // Load message history
             for (message of messageHistory) {
-                const timeArray = _formatTimestamp(_getTimestamp());
-                messagePacket = { ...message };
-                messagePacket.timeString = timeArray[0];
-                messagePacket.dateString = timeArray[1];
+                messagePacket = { ...message };                                             // Create new variable
+                messagePacket = formatting.addTimeAndDateStringToPacket(messagePacket);     // Add timestamp
+                messagePacket.message = await _parseMessage(messagePacket.message);         // Parse the message for the UI
 
-                let formattedMessagePacket = {...messagePacket};
-                formattedMessagePacket.message = await _parseMessage(messagePacket.message);
-
-                _emitEvent({ type: "show_message", ...formattedMessagePacket });
+                _emitEvent({ type: "show_message", ...messagePacket });                     // Send message to UI
             }
         }
 
-        // Send current settings to the app
-        _emitEvent({ type: "initial_settings", settings: settings });
+        _emitEvent({ type: "initial_settings", settings: settings });                       // Send current settings to the app
     }
     function _saveSettings() {
         console.log("Saving config");
         Settings.setValue("ArmoredChat-Config", settings);
     }
-    function _getTimestamp(){
-        return Date.now();
-    }
-    function _formatTimestamp(timestamp){
-        let timeArray = [];
-
-        timeArray.push(new Date().toLocaleTimeString(undefined, {
-            hour12: false,
-        }));
-
-        timeArray.push(new Date(timestamp).toLocaleDateString(undefined, {
-                year: "numeric",
-                month: "long",
-                day: "numeric",
-        }));
-
-        return timeArray;
-    }
     function _notificationCoreMessage(displayName, message){
         Messages.sendLocalMessage(
             "Floof-Notif",
diff --git a/scripts/system/domainChat/formatting.js b/scripts/system/domainChat/formatting.js
new file mode 100644
index 0000000000..b523f30d2d
--- /dev/null
+++ b/scripts/system/domainChat/formatting.js
@@ -0,0 +1,64 @@
+//
+//  formatting.js
+//
+//  Created by Armored Dragon, 2024.
+//  Copyright 2024 Overte e.V.
+//
+//	This just does some basic formatting and minor housekeeping for the domainChat.js application
+//
+//  Distributed under the Apache License, Version 2.0.
+//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+
+const formatting = {
+	toJSON: function(data) {
+		if (typeof data == "object") return data; // Already JSON
+		
+		try {
+			const parsedData = JSON.parse(data);
+			return parsedData;
+		} catch (e) {
+			console.log('Failed to convert data to JSON.')
+			return null; // Could not convert to json, some error;
+		}
+	},
+	addTimeAndDateStringToPacket: function(packet) {
+		// Gets the current time and adds it to a given packet
+		const timeArray = formatting.helpers._timestampArray(packet.timestamp);
+		packet.timeString = timeArray[0];
+        packet.dateString = timeArray[1];
+		return packet;
+	},
+	trimPacketToSave: function(packet) {
+		// Takes a packet, and returns a packet containing only what is needed to save.
+		let newPacket = {
+			channel: packet.channel || "",
+			displayName: packet.displayName || "",
+			message: packet.message || "",
+			timestamp: packet.timestamp || formatting.helpers.getTimestamp(),
+		};
+		return newPacket;
+	},
+
+	helpers: {
+		// Small functions that are used often in the other functions.
+		_timestampArray: function(timestamp) {
+			const currentDate = timestamp || formatting.helpers.getTimestamp();
+			let timeArray = [];
+
+			timeArray.push(new Date(currentDate).toLocaleTimeString(undefined, {
+				hour12: false,
+			}));
+	
+			timeArray.push(new Date(currentDate).toLocaleDateString(undefined, {
+				year: "numeric",
+				month: "long",
+				day: "numeric",
+			}));
+	
+			return timeArray;
+		},
+		getTimestamp: function(){
+			return Date.now();
+		}
+	}
+}

From 0d6af1e90c329eddceddbcc21377513ef557bf83 Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Fri, 20 Dec 2024 05:32:17 -0600
Subject: [PATCH 35/49] Move message parsing to formatting file.

---
 scripts/system/domainChat/domainChat.js | 114 ++----------------------
 scripts/system/domainChat/formatting.js |  98 ++++++++++++++++++++
 2 files changed, 105 insertions(+), 107 deletions(-)

diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js
index 9428ef51fa..30b6081083 100644
--- a/scripts/system/domainChat/domainChat.js
+++ b/scripts/system/domainChat/domainChat.js
@@ -109,7 +109,7 @@
         if (message.channel == "local" && isTooFar(message.position)) return;   // If message is local, and if player is too far away from location, do nothing.
         
         let formattedMessagePacket = { ...message };
-        formattedMessagePacket.message = await _parseMessage(message.message)
+        formattedMessagePacket.message = await formatting.parseMessage(message.message)
 
         _emitEvent({ type: "show_message", ...formattedMessagePacket });        // Update qml view of to new message.
         _notificationCoreMessage(message.displayName, message.message)          // Show a new message on screen.
@@ -219,7 +219,7 @@
 
             // Format notification message
             let formattedMessagePacket = {...message};
-            formattedMessagePacket.message = await _parseMessage(message.message);
+            formattedMessagePacket.message = await formatting.parseMessage(message.message);
 
             _emitEvent({ type: "notification", ...formattedMessagePacket });
         }, 1500);
@@ -230,15 +230,15 @@
         if (messageHistory) {
             // Load message history
             for (message of messageHistory) {
-                messagePacket = { ...message };                                             // Create new variable
-                messagePacket = formatting.addTimeAndDateStringToPacket(messagePacket);     // Add timestamp
-                messagePacket.message = await _parseMessage(messagePacket.message);         // Parse the message for the UI
+                messagePacket = { ...message };                                                 // Create new variable
+                messagePacket = formatting.addTimeAndDateStringToPacket(messagePacket);         // Add timestamp
+                messagePacket.message = await formatting.parseMessage(messagePacket.message);   // Parse the message for the UI
 
-                _emitEvent({ type: "show_message", ...messagePacket });                     // Send message to UI
+                _emitEvent({ type: "show_message", ...messagePacket });                         // Send message to UI
             }
         }
 
-        _emitEvent({ type: "initial_settings", settings: settings });                       // Send current settings to the app
+        _emitEvent({ type: "initial_settings", settings: settings });                           // Send current settings to the app
     }
     function _saveSettings() {
         console.log("Saving config");
@@ -250,85 +250,6 @@
             JSON.stringify({ sender: displayName, text: message })
         );
     }
-    async function _parseMessage(message){
-        const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;
-        const mentionRegex = /@(\w+)/; // FIXME: Remove - devcode
-        const overteLocationRegex = /hifi:\/\/[a-zA-Z0-9_-]+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+/;
-
-        let runningMessage = message;
-        let messageArray = [];
-
-        const regexPatterns = [
-            { type: "url", regex: urlRegex },
-            { type: "mention", regex: mentionRegex }, // FIXME: Remove - devcode
-            { type: "overteLocation", regex: overteLocationRegex }
-        ]
-
-        // Here is a link https://www.example.com, #hashtag, and @mention. Just for some spice here is another https://exampletwo.com
-
-        while (true) {
-            let firstMatch = _findFirstMatch();
-
-            if (firstMatch == null) {
-                // Format any remaining text as a basic 'text' type.
-                messageArray.push({type: 'text', value: runningMessage});
-
-                // Append a final 'fill width' to the message text.
-                messageArray.push({type: 'messageEnd'});
-                break;
-            }
-
-            _formatMessage(firstMatch);
-        }
-
-        for (dataChunk of messageArray){
-            if (dataChunk.type == 'url'){
-                let url = dataChunk.value;
-
-                const res = await fetch(url, {method: 'GET'});      // TODO: Replace with 'HEAD' method. https://github.com/overte-org/overte/issues/1273
-                const contentType = res.getResponseHeader("content-type");
-
-                // TODO: Add support for other media types
-                if (contentType.startsWith('image/')) {
-                    messageArray.push({type: 'imageEmbed', value: url});
-                }
-            }
-        }
-
-        return messageArray;
-
-        function _formatMessage(firstMatch){
-            let indexOfFirstMatch = firstMatch[0];
-            let regex = regexPatterns[firstMatch[1]].regex;
-
-            let foundMatch = runningMessage.match(regex)[0];
-
-            messageArray.push({type: 'text', value: runningMessage.substring(0, indexOfFirstMatch)});
-            messageArray.push({type: regexPatterns[firstMatch[1]].type, value: runningMessage.substring(indexOfFirstMatch, indexOfFirstMatch + foundMatch.length)});
-            
-            runningMessage = runningMessage.substring(indexOfFirstMatch + foundMatch.length);   // Remove the part of the message we have worked with
-        }
-
-        function _findFirstMatch(){
-            let indexOfFirstMatch = Infinity;
-            let indexOfRegexPattern = Infinity;
-
-            for (let i = 0; regexPatterns.length > i; i++){
-                let indexOfMatch = runningMessage.search(regexPatterns[i].regex);
-
-                if (indexOfMatch == -1) continue;                                              // No match found
-
-                if (indexOfMatch < indexOfFirstMatch) {
-                    indexOfFirstMatch = indexOfMatch;
-                    indexOfRegexPattern = i;
-                }
-            }
-
-            if (indexOfFirstMatch !== Infinity) return [indexOfFirstMatch, indexOfRegexPattern];    // If there was a found match
-            return null;                                                                            // No found match
-        }
-    }
-
     /**
      * Emit a packet to the HTML front end. Easy communication!
      * @param {Object} packet - The Object packet to emit to the HTML
@@ -338,27 +259,6 @@
         chatOverlayWindow.sendToQml(packet);
     }
 
-    function fetch(url, options = {method: "GET"}) {
-        return new Promise((resolve, reject) => {
-            let req = new XMLHttpRequest();
 
-            req.onreadystatechange = function () {
-
-                if (req.readyState === req.DONE) {
-                    if (req.status === 200) {
-                        console.log("Content type:", req.getResponseHeader("content-type"));
-                        resolve(req);
-            
-                    } else {
-                        console.log("Error", req.status, req.statusText);
-                        reject();
-                    }
-                }
-            };
-
-            req.open(options.method, url);
-            req.send();
-        });
-    }
 
 })();
diff --git a/scripts/system/domainChat/formatting.js b/scripts/system/domainChat/formatting.js
index b523f30d2d..05d3bf4cfa 100644
--- a/scripts/system/domainChat/formatting.js
+++ b/scripts/system/domainChat/formatting.js
@@ -38,6 +38,82 @@ const formatting = {
 		};
 		return newPacket;
 	},
+	parseMessage: async function(message) {
+		const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;
+        const overteLocationRegex = /hifi:\/\/[a-zA-Z0-9_-]+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+/;
+
+		let runningMessage = message;			// The remaining message that will be parsed
+        let messageArray = [];					// An array of messages that are split up by the formatting functions
+
+		const regexPatterns = [
+            { type: "url", regex: urlRegex },
+            { type: "overteLocation", regex: overteLocationRegex }
+        ]
+
+		while (true) {
+            let firstMatch = _findFirstMatch();
+
+            if (firstMatch == null) {
+				// If there is no more text to parse, break out of the loop and return the message array.
+                // Format any remaining text as a basic 'text' type.
+                messageArray.push({type: 'text', value: runningMessage});
+
+                // Append a final 'fill width' to the message text.
+                messageArray.push({type: 'messageEnd'});
+                break;
+            }
+
+            _formatMessage(firstMatch);
+        }
+
+		// Embed images in the message array.
+		for (dataChunk of messageArray){
+            if (dataChunk.type == 'url'){
+                let url = dataChunk.value;
+
+                const res = await formatting.helpers.fetch(url, {method: 'GET'});      // TODO: Replace with 'HEAD' method. https://github.com/overte-org/overte/issues/1273
+                const contentType = res.getResponseHeader("content-type");
+
+                // TODO: Add support for other media types
+                if (contentType.startsWith('image/')) {
+                    messageArray.push({type: 'imageEmbed', value: url});
+                }
+            }
+        }
+
+		return messageArray;
+
+		function _formatMessage(firstMatch){
+            let indexOfFirstMatch = firstMatch[0];
+            let regex = regexPatterns[firstMatch[1]].regex;
+
+            let foundMatch = runningMessage.match(regex)[0];
+
+            messageArray.push({type: 'text', value: runningMessage.substring(0, indexOfFirstMatch)});
+            messageArray.push({type: regexPatterns[firstMatch[1]].type, value: runningMessage.substring(indexOfFirstMatch, indexOfFirstMatch + foundMatch.length)});
+            
+            runningMessage = runningMessage.substring(indexOfFirstMatch + foundMatch.length);   // Remove the part of the message we have worked with
+        }
+
+        function _findFirstMatch(){
+            let indexOfFirstMatch = Infinity;
+            let indexOfRegexPattern = Infinity;
+
+            for (let i = 0; regexPatterns.length > i; i++){
+                let indexOfMatch = runningMessage.search(regexPatterns[i].regex);
+
+                if (indexOfMatch == -1) continue;                                              // No match found
+
+                if (indexOfMatch < indexOfFirstMatch) {
+                    indexOfFirstMatch = indexOfMatch;
+                    indexOfRegexPattern = i;
+                }
+            }
+
+            if (indexOfFirstMatch !== Infinity) return [indexOfFirstMatch, indexOfRegexPattern];    // If there was a found match
+            return null;                                                                            // No found match
+        }
+	},
 
 	helpers: {
 		// Small functions that are used often in the other functions.
@@ -59,6 +135,28 @@ const formatting = {
 		},
 		getTimestamp: function(){
 			return Date.now();
+		},
+		fetch: function (url, options = {method: "GET"}) {
+			return new Promise((resolve, reject) => {
+				let req = new XMLHttpRequest();
+	
+				req.onreadystatechange = function () {
+	
+					if (req.readyState === req.DONE) {
+						if (req.status === 200) {
+							console.log("Content type:", req.getResponseHeader("content-type"));
+							resolve(req);
+				
+						} else {
+							console.log("Error", req.status, req.statusText);
+							reject();
+						}
+					}
+				};
+	
+				req.open(options.method, url);
+				req.send();
+			});
 		}
 	}
 }

From 8ac598886f629a5661ecd2142c5799d9630b4e6a Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Sat, 21 Dec 2024 05:45:59 -0600
Subject: [PATCH 36/49] Support animated images.

---
 .../domainChat/qml_widgets/TemplateChatMessage.qml       | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
index a9c366ce76..7829ee791d 100644
--- a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
+++ b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
@@ -1,4 +1,4 @@
-import QtQuick 2.7
+import QtQuick 2.15
 import QtQuick.Controls 2.15
 import QtQuick.Layouts 1.3
 
@@ -147,10 +147,11 @@ Component {
 						width: messageBoxFlow.width;
 						height: 200
 
-						Image {
+						AnimatedImage {
 							source: model.type === 'imageEmbed' ? model.value : ''
-							sourceSize.width: 400
-							sourceSize.height: 200
+							height: Math.min(sourceSize.height, 200);
+							onStatusChanged: playing = (status == AnimatedImage.Ready)
+							fillMode: Image.PreserveAspectFit
 						}
 					}
 				}

From 71c85d388980a79affe970caec76a2427407c60a Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Sat, 21 Dec 2024 06:44:23 -0600
Subject: [PATCH 37/49] Basic video support.

---
 scripts/system/domainChat/formatting.js       |  7 +++-
 .../qml_widgets/TemplateChatMessage.qml       | 42 ++++++++++++++++++-
 2 files changed, 46 insertions(+), 3 deletions(-)

diff --git a/scripts/system/domainChat/formatting.js b/scripts/system/domainChat/formatting.js
index 05d3bf4cfa..5e1458db8d 100644
--- a/scripts/system/domainChat/formatting.js
+++ b/scripts/system/domainChat/formatting.js
@@ -74,10 +74,14 @@ const formatting = {
                 const res = await formatting.helpers.fetch(url, {method: 'GET'});      // TODO: Replace with 'HEAD' method. https://github.com/overte-org/overte/issues/1273
                 const contentType = res.getResponseHeader("content-type");
 
-                // TODO: Add support for other media types
                 if (contentType.startsWith('image/')) {
                     messageArray.push({type: 'imageEmbed', value: url});
+					continue;
                 }
+				if (contentType.startsWith('video/')){ 
+                    messageArray.push({type: 'videoEmbed', value: url});
+					continue;
+				}
             }
         }
 
@@ -144,7 +148,6 @@ const formatting = {
 	
 					if (req.readyState === req.DONE) {
 						if (req.status === 200) {
-							console.log("Content type:", req.getResponseHeader("content-type"));
 							resolve(req);
 				
 						} else {
diff --git a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
index 7829ee791d..9e33a0503e 100644
--- a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
+++ b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
@@ -1,6 +1,7 @@
 import QtQuick 2.15
 import QtQuick.Controls 2.15
 import QtQuick.Layouts 1.3
+import QtMultimedia 5.15
 
 Component {
 	id: template_chat_message
@@ -150,10 +151,49 @@ Component {
 						AnimatedImage {
 							source: model.type === 'imageEmbed' ? model.value : ''
 							height: Math.min(sourceSize.height, 200);
-							onStatusChanged: playing = (status == AnimatedImage.Ready)
 							fillMode: Image.PreserveAspectFit
 						}
 					}
+
+					Item {
+						visible: model.type === 'videoEmbed';
+						width: messageBoxFlow.width;
+						height: 200;
+
+						Video {
+							id: videoPlayer
+							source: model.type === 'videoEmbed' ? model.value : ''
+							height: 200;
+							width: 400;
+							fillMode: Image.PreserveAspectFit
+							autoLoad: false;
+
+							onStatusChanged: {
+								if (status === 7) {
+									// Weird hack to make the video restart when it's over
+									// Ideally you'd want to use the seek function to restart the video but it doesn't work?
+									// Will need to make a more refined solution for this later. in the form of a more advanced media player.
+									// For now, this is sufficient. -AD
+									let originalURL = videoPlayer.source;
+									videoPlayer.source = "";
+									videoPlayer.source = originalURL;
+								}
+							}
+
+							MouseArea {
+								anchors.fill: parent
+								onClicked: {
+									const videoIsOver = videoPlayer.position == videoPlayer.duration
+									if (videoPlayer.playbackState == MediaPlayer.PlayingState) {
+										videoPlayer.pause();
+									} 
+									else {
+										parent.play();
+									}
+								}
+							}
+						}
+					}
 				}
 			}
 		}

From 624ceefc244c62b70f1400a146512bc138ee150e Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Tue, 31 Dec 2024 16:22:55 -0600
Subject: [PATCH 38/49] Add setting to force virtual window in VR.

---
 scripts/system/domainChat/domainChat.js  | 34 ++++++++++++++++++------
 scripts/system/domainChat/domainChat.qml | 25 +++++++++++++++++
 2 files changed, 51 insertions(+), 8 deletions(-)

diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js
index 30b6081083..f6d8a10b3f 100644
--- a/scripts/system/domainChat/domainChat.js
+++ b/scripts/system/domainChat/domainChat.js
@@ -7,8 +7,6 @@
 //  Distributed under the Apache License, Version 2.0.
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
 
-// TODO: Message trimming
-
 (() => {
     ("use strict");
 
@@ -20,8 +18,10 @@
     var settings = {
         external_window: false,
         maximum_messages: 200,
-        join_notification: true
+        join_notification: true,
+        switchToInternalOnHeadsetUsed: true
     };
+    let temporaryChangeModeToVirtual = false;
 
     // Global vars
     var tablet;
@@ -38,6 +38,7 @@
     Messages.messageReceived.connect(receivedMessage);
     AvatarManager.avatarAddedEvent.connect((sessionId) => { _avatarAction("connected", sessionId); });
     AvatarManager.avatarRemovedEvent.connect((sessionId) => { _avatarAction("left", sessionId); });
+    HMD.displayModeChanged.connect(_onHMDDisplayModeChanged);
 
     startup();
 
@@ -135,15 +136,16 @@
                 break;
             case "setting_change":
                 // Set the setting value, and save the config
-                settings[event.setting] = event.value; // Update local settings
-                _saveSettings(); // Save local settings
+                settings[event.setting] = event.value;  // Update local settings
+                _saveSettings();                        // Save local settings
 
                 // Extra actions to preform. 
                 switch (event.setting) {
                     case "external_window":
-                        chatOverlayWindow.presentationMode = event.value
-                            ? Desktop.PresentationMode.NATIVE
-                            : Desktop.PresentationMode.VIRTUAL;
+                        _changePresentationMode(event.value);
+                        break;
+                    case "switchToInternalOnHeadsetUsed":
+                        _onHMDDisplayModeChanged(HMD.active);
                         break;
                 }
 
@@ -177,6 +179,22 @@
                 });
         }
     }
+    function _onHMDDisplayModeChanged(isHMDActive){
+        // If the user enabled automatic switching to internal when they put on a headset...
+        if (!settings.switchToInternalOnHeadsetUsed) return;
+
+        if (isHMDActive) temporaryChangeModeToVirtual = true;
+        else temporaryChangeModeToVirtual = false;
+
+        _changePresentationMode(settings.external_window);
+    }
+    function _changePresentationMode(changeToExternal){
+        if (temporaryChangeModeToVirtual) changeToExternal = false;
+        
+        chatOverlayWindow.presentationMode = changeToExternal
+            ? Desktop.PresentationMode.NATIVE
+            : Desktop.PresentationMode.VIRTUAL;
+    }
     function _sendMessage(message, channel) {
         if (message.length == 0) return;
 
diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml
index 6a79cb7e2e..287bb1a825 100644
--- a/scripts/system/domainChat/domainChat.qml
+++ b/scripts/system/domainChat/domainChat.qml
@@ -369,6 +369,29 @@ Rectangle {
                         }
                     }
                 }
+                // Switch to internal on VR Mode
+                Rectangle {
+                    width: parent.width
+                    height: 40
+                    color: "transparent"
+
+                    Text {
+                        text: "Force Virtual window in VR"
+                        color: "white"
+                        font.pointSize: 12
+                        anchors.verticalCenter: parent.verticalCenter
+                    }
+
+                    CheckBox {
+                        id: s_force_vw_in_vr
+                        anchors.right: parent.right
+                        anchors.verticalCenter: parent.verticalCenter
+
+                        onCheckedChanged: {
+                            toScript({type: 'setting_change', setting: 'switchToInternalOnHeadsetUsed', value: checked})
+                        }
+                    }
+                }
             }
         }
 
@@ -433,9 +456,11 @@ Rectangle {
                 domain.clear();
                 break;
             case "initial_settings":
+                print(JSON.stringify(message.settings));
                 if (message.settings.external_window) s_external_window.checked = true;
                 if (message.settings.maximum_messages) s_maximum_messages.value = message.settings.maximum_messages;
                 if (message.settings.join_notification) s_join_notification.checked = true;
+                if (message.settings.switchToInternalOnHeadsetUsed) s_force_vw_in_vr.checked = true;
                 break;
         }
     }

From 30bfe0d03885f806b9a39b354c21170977f9e248 Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Wed, 1 Jan 2025 07:35:04 -0600
Subject: [PATCH 39/49] Message embedding toggle.

---
 scripts/system/domainChat/domainChat.js  | 11 ++++----
 scripts/system/domainChat/domainChat.qml | 25 ++++++++++++++++++
 scripts/system/domainChat/formatting.js  | 32 +++++++++++++-----------
 3 files changed, 48 insertions(+), 20 deletions(-)

diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js
index f6d8a10b3f..2758ac9c54 100644
--- a/scripts/system/domainChat/domainChat.js
+++ b/scripts/system/domainChat/domainChat.js
@@ -19,7 +19,8 @@
         external_window: false,
         maximum_messages: 200,
         join_notification: true,
-        switchToInternalOnHeadsetUsed: true
+        switchToInternalOnHeadsetUsed: true,
+        enableEmbedding: false                  // Prevents information leakage, default false
     };
     let temporaryChangeModeToVirtual = false;
 
@@ -110,7 +111,7 @@
         if (message.channel == "local" && isTooFar(message.position)) return;   // If message is local, and if player is too far away from location, do nothing.
         
         let formattedMessagePacket = { ...message };
-        formattedMessagePacket.message = await formatting.parseMessage(message.message)
+        formattedMessagePacket.message = await formatting.parseMessage(message.message, settings.enableEmbedding)
 
         _emitEvent({ type: "show_message", ...formattedMessagePacket });        // Update qml view of to new message.
         _notificationCoreMessage(message.displayName, message.message)          // Show a new message on screen.
@@ -248,9 +249,9 @@
         if (messageHistory) {
             // Load message history
             for (message of messageHistory) {
-                messagePacket = { ...message };                                                 // Create new variable
-                messagePacket = formatting.addTimeAndDateStringToPacket(messagePacket);         // Add timestamp
-                messagePacket.message = await formatting.parseMessage(messagePacket.message);   // Parse the message for the UI
+                messagePacket = { ...message };                                                                             // Create new variable
+                messagePacket = formatting.addTimeAndDateStringToPacket(messagePacket);                                     // Add timestamp
+                messagePacket.message = await formatting.parseMessage(messagePacket.message, settings.enableEmbedding);     // Parse the message for the UI
 
                 _emitEvent({ type: "show_message", ...messagePacket });                         // Send message to UI
             }
diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml
index 287bb1a825..ef5860587a 100644
--- a/scripts/system/domainChat/domainChat.qml
+++ b/scripts/system/domainChat/domainChat.qml
@@ -392,6 +392,29 @@ Rectangle {
                         }
                     }
                 }
+                // Toggle media embedding
+                Rectangle {
+                    width: parent.width
+                    height: 40
+                    color: "transparent"
+
+                    Text {
+                        text: "Enable media embedding"
+                        color: "white"
+                        font.pointSize: 12
+                        anchors.verticalCenter: parent.verticalCenter
+                    }
+
+                    CheckBox {
+                        id: s_enable_embedding
+                        anchors.right: parent.right
+                        anchors.verticalCenter: parent.verticalCenter
+
+                        onCheckedChanged: {
+                            toScript({type: 'setting_change', setting: 'enableEmbedding', value: checked})
+                        }
+                    }
+                }
             }
         }
 
@@ -456,11 +479,13 @@ Rectangle {
                 domain.clear();
                 break;
             case "initial_settings":
+
                 print(JSON.stringify(message.settings));
                 if (message.settings.external_window) s_external_window.checked = true;
                 if (message.settings.maximum_messages) s_maximum_messages.value = message.settings.maximum_messages;
                 if (message.settings.join_notification) s_join_notification.checked = true;
                 if (message.settings.switchToInternalOnHeadsetUsed) s_force_vw_in_vr.checked = true;
+                if (message.settings.enableEmbedding) s_enable_embedding.checked = true;
                 break;
         }
     }
diff --git a/scripts/system/domainChat/formatting.js b/scripts/system/domainChat/formatting.js
index 5e1458db8d..24662e4ff1 100644
--- a/scripts/system/domainChat/formatting.js
+++ b/scripts/system/domainChat/formatting.js
@@ -38,7 +38,7 @@ const formatting = {
 		};
 		return newPacket;
 	},
-	parseMessage: async function(message) {
+	parseMessage: async function(message, enableEmbedding) {
 		const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;
         const overteLocationRegex = /hifi:\/\/[a-zA-Z0-9_-]+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+/;
 
@@ -67,23 +67,25 @@ const formatting = {
         }
 
 		// Embed images in the message array.
-		for (dataChunk of messageArray){
-            if (dataChunk.type == 'url'){
-                let url = dataChunk.value;
+		if (enableEmbedding) {
+			for (dataChunk of messageArray){
+				if (dataChunk.type == 'url'){
+					let url = dataChunk.value;
 
-                const res = await formatting.helpers.fetch(url, {method: 'GET'});      // TODO: Replace with 'HEAD' method. https://github.com/overte-org/overte/issues/1273
-                const contentType = res.getResponseHeader("content-type");
+					const res = await formatting.helpers.fetch(url, {method: 'GET'});      // TODO: Replace with 'HEAD' method. https://github.com/overte-org/overte/issues/1273
+					const contentType = res.getResponseHeader("content-type");
 
-                if (contentType.startsWith('image/')) {
-                    messageArray.push({type: 'imageEmbed', value: url});
-					continue;
-                }
-				if (contentType.startsWith('video/')){ 
-                    messageArray.push({type: 'videoEmbed', value: url});
-					continue;
+					if (contentType.startsWith('image/')) {
+						messageArray.push({type: 'imageEmbed', value: url});
+						continue;
+					}
+					if (contentType.startsWith('video/')){ 
+						messageArray.push({type: 'videoEmbed', value: url});
+						continue;
+					}
 				}
-            }
-        }
+			}
+		}
 
 		return messageArray;
 

From 7acd44a4464d6628991f73dd23b11541ec30e38e Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Wed, 1 Jan 2025 11:41:17 -0600
Subject: [PATCH 40/49] Remove lingering print function.

---
 scripts/system/domainChat/domainChat.qml | 2 --
 1 file changed, 2 deletions(-)

diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml
index ef5860587a..9ac69f71ac 100644
--- a/scripts/system/domainChat/domainChat.qml
+++ b/scripts/system/domainChat/domainChat.qml
@@ -479,8 +479,6 @@ Rectangle {
                 domain.clear();
                 break;
             case "initial_settings":
-
-                print(JSON.stringify(message.settings));
                 if (message.settings.external_window) s_external_window.checked = true;
                 if (message.settings.maximum_messages) s_maximum_messages.value = message.settings.maximum_messages;
                 if (message.settings.join_notification) s_join_notification.checked = true;

From 6c631bf3611f52548264522553719feb327e3d2a Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Wed, 15 Jan 2025 11:46:50 -0600
Subject: [PATCH 41/49] Fix text highlighting.

---
 .../qml_widgets/TemplateChatMessage.qml       | 25 +++++++++++++++----
 1 file changed, 20 insertions(+), 5 deletions(-)

diff --git a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
index 9e33a0503e..8a8606c0ff 100644
--- a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
+++ b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
@@ -19,15 +19,21 @@ Component {
 			anchors.horizontalCenter: parent.horizontalCenter
 			height: 22
 
-			Text {
+			TextEdit {
 				text: delegateUsername
 				color: "lightgray"
+				readOnly: true
+				selectByMouse: true
+				selectByKeyboard: true
 			}
 
-			Text {
+			TextEdit {
 				anchors.right: parent.right
 				text: delegateDate
 				color: "lightgray"
+				readOnly: true
+				selectByMouse: true
+				selectByKeyboard: true
 			}
 		}
 
@@ -41,12 +47,15 @@ Component {
 				model: delegateText;
 
 				RowLayout {
-					Text {
+					TextEdit {
 						text: model.value || ""
 						font.pointSize: 12
 						wrapMode: Text.Wrap
 						width: model.type === 'text' || model.type === 'mention' ? Math.min(messageBoxFlow.width, contentWidth) : 0;
 						visible: model.type === 'text' || model.type === 'mention';
+						readOnly: true
+						selectByMouse: true
+						selectByKeyboard: true
 
 						color: {
 							switch (model.type) {
@@ -62,13 +71,16 @@ Component {
 						width: children[0].contentWidth;
 						visible: model.type === 'url';
 
-						Text {
+						TextEdit {
 							text: model.value || "";
 							font.pointSize: 12;
 							wrapMode: Text.Wrap;
 							color: "#4EBAFD";
 							font.underline: true;
 							width: parent.width;
+							readOnly: true
+							selectByMouse: true
+							selectByKeyboard: true
 
 							MouseArea {
 								anchors.fill: parent;
@@ -120,12 +132,15 @@ Component {
 								anchors.rightMargin: 10
 							}
 
-							Text {
+							TextEdit {
 								text: model.type === 'overteLocation' ? model.value.split('hifi://')[1].split('/')[0] : '';
 								color: "black"
 								font.pointSize: 12
 								x: parent.children[0].width + 5;
 								anchors.verticalCenter: parent.verticalCenter 
+								readOnly: true
+								selectByMouse: true
+								selectByKeyboard: true
 							}
 
 							MouseArea {

From f59f39ecb452935275c0a61388dafc2d6a92d47c Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Wed, 15 Jan 2025 13:53:16 -0600
Subject: [PATCH 42/49] Fix binding loops.

---
 .../qml_widgets/TemplateChatMessage.qml       | 34 +++++++++----------
 1 file changed, 17 insertions(+), 17 deletions(-)

diff --git a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
index 8a8606c0ff..b5b82a7d79 100644
--- a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
+++ b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
@@ -20,26 +20,23 @@ Component {
 			height: 22
 
 			TextEdit {
-				text: delegateUsername
-				color: "lightgray"
-				readOnly: true
-				selectByMouse: true
-				selectByKeyboard: true
+				text: delegateUsername;
+				color: "lightgray";
+				readOnly: true;
+				selectByMouse: true;
+				selectByKeyboard: true;
 			}
 
-			TextEdit {
-				anchors.right: parent.right
-				text: delegateDate
-				color: "lightgray"
-				readOnly: true
-				selectByMouse: true
-				selectByKeyboard: true
+			Text {
+				anchors.right: parent.right;
+				text: delegateDate;
+				color: "lightgray";
 			}
 		}
 
 		Flow {
 			anchors.top: parent.children[0].bottom;
-			width: parent.width * 0.8
+			width: parent.width;
 			x: 5
 			id: messageBoxFlow
 
@@ -47,11 +44,13 @@ Component {
 				model: delegateText;
 
 				RowLayout {
+					width: parent.width;
+
 					TextEdit {
 						text: model.value || ""
 						font.pointSize: 12
 						wrapMode: Text.Wrap
-						width: model.type === 'text' || model.type === 'mention' ? Math.min(messageBoxFlow.width, contentWidth) : 0;
+						width: messageBoxFlow.width;
 						visible: model.type === 'text' || model.type === 'mention';
 						readOnly: true
 						selectByMouse: true
@@ -68,10 +67,11 @@ Component {
 					}
 
 					RowLayout {
-						width: children[0].contentWidth;
+						width: urlTypeTextDisplay.width;
 						visible: model.type === 'url';
 
 						TextEdit {
+							id: urlTypeTextDisplay;
 							text: model.value || "";
 							font.pointSize: 12;
 							wrapMode: Text.Wrap;
@@ -154,8 +154,8 @@ Component {
 					}
 
 					Item {
-						Layout.fillWidth: true
-						visible: model.type === 'messageEnd'
+						Layout.fillWidth: true;
+						visible: model.type === 'messageEnd';
 					}
 
 					Item {

From 5586df1a9344a36a12b0b32d9e4990e0f3b484b9 Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Thu, 16 Jan 2025 14:53:54 -0600
Subject: [PATCH 43/49] Remove video embeds. Added debug logs. Don't save
 settings when initializing the application.

---
 scripts/system/domainChat/domainChat.js       | 21 +++++-
 scripts/system/domainChat/domainChat.qml      | 13 +++-
 .../qml_widgets/TemplateChatMessage.qml       | 73 +++++++++----------
 3 files changed, 63 insertions(+), 44 deletions(-)

diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js
index 2758ac9c54..8cc42153eb 100644
--- a/scripts/system/domainChat/domainChat.js
+++ b/scripts/system/domainChat/domainChat.js
@@ -100,7 +100,6 @@
         // Is the message a chat message?
         channel = channel.toLowerCase();
         if (channel !== "chat") return;
-        
         if ((message = formatting.toJSON(message)) == null) return;             // Make sure we are working with a JSON object we expect, otherwise kill
         message = formatting.addTimeAndDateStringToPacket(message);  
         
@@ -195,6 +194,8 @@
         chatOverlayWindow.presentationMode = changeToExternal
             ? Desktop.PresentationMode.NATIVE
             : Desktop.PresentationMode.VIRTUAL;
+        
+        console.log(`Presentation mode was changed to ${chatOverlayWindow.presentationMode}`);
     }
     function _sendMessage(message, channel) {
         if (message.length == 0) return;
@@ -245,6 +246,7 @@
     }
     async function _loadSettings() {
         settings = Settings.getValue("ArmoredChat-Config", settings);
+        console.log("Loading settings: ", jstr(settings));
 
         if (messageHistory) {
             // Load message history
@@ -260,10 +262,11 @@
         _emitEvent({ type: "initial_settings", settings: settings });                           // Send current settings to the app
     }
     function _saveSettings() {
-        console.log("Saving config");
+        console.log("Saving settings: ", jstr(settings));
         Settings.setValue("ArmoredChat-Config", settings);
     }
     function _notificationCoreMessage(displayName, message){
+        console.log("Sending notification to notificationCore:", `Display name: ${displayName}\n Message: ${message}`);
         Messages.sendLocalMessage(
             "Floof-Notif",
             JSON.stringify({ sender: displayName, text: message })
@@ -275,9 +278,19 @@
      * @param {("show_message"|"clear_messages"|"notification"|"initial_settings")} packet.type - The type of packet it is
      */
     function _emitEvent(packet = { type: "" }) {
+        if (packet.type == `show_message`) {
+            // Don't show the message contents, this is a courtesy to prevent message leakage in the logs.
+            let strippedPacket = {...packet};
+            delete strippedPacket.message
+            console.log("Sending packet to QML interface", jstr(strippedPacket));
+        }
+        else {
+            console.log("Sending packet to QML interface", jstr(packet));
+        }
+
         chatOverlayWindow.sendToQml(packet);
     }
 
-
-
+    // Debug and developer functions and data
+    const jstr = (object) => JSON.stringify(object, null, 4);       // JSON Stringify function with formatting
 })();
diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml
index 9ac69f71ac..be4b6d4f79 100644
--- a/scripts/system/domainChat/domainChat.qml
+++ b/scripts/system/domainChat/domainChat.qml
@@ -8,8 +8,10 @@ Rectangle {
     color: Qt.rgba(0.1,0.1,0.1,1)
     signal sendToScript(var message);
 
-    property string pageVal: "local"
-    property date last_message_time: new Date()
+    property string pageVal: "local";
+    property date last_message_time: new Date();
+    property bool initialized: false;
+
 
     // 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.
@@ -445,7 +447,6 @@ Rectangle {
         messageViewFlickable.returnToBounds();
     }
 
-
     function addMessage(username, message, date, channel, type){
         channel = getChannel(channel)
 
@@ -479,17 +480,23 @@ Rectangle {
                 domain.clear();
                 break;
             case "initial_settings":
+                print(`Got settings:\n ${JSON.stringify(message.settings, null, 4)}`);
                 if (message.settings.external_window) s_external_window.checked = true;
                 if (message.settings.maximum_messages) s_maximum_messages.value = message.settings.maximum_messages;
                 if (message.settings.join_notification) s_join_notification.checked = true;
                 if (message.settings.switchToInternalOnHeadsetUsed) s_force_vw_in_vr.checked = true;
                 if (message.settings.enableEmbedding) s_enable_embedding.checked = true;
+
+                initialized = true;     // Application is ready
+
                 break;
         }
     }
 
     // Send message to script
     function toScript(packet){
+        if (packet.type === "setting_change" && !initialized) return;   // Don't announce a change in settings if not ready
+
         sendToScript(packet)
     }
 }
diff --git a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
index b5b82a7d79..641a0ca0b3 100644
--- a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
+++ b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
@@ -1,7 +1,7 @@
 import QtQuick 2.15
 import QtQuick.Controls 2.15
 import QtQuick.Layouts 1.3
-import QtMultimedia 5.15
+// import QtMultimedia 5.15
 
 Component {
 	id: template_chat_message
@@ -170,45 +170,44 @@ Component {
 						}
 					}
 
-					Item {
-						visible: model.type === 'videoEmbed';
-						width: messageBoxFlow.width;
-						height: 200;
+					// Item {
+					// 	visible: model.type === 'videoEmbed';
+					// 	width: messageBoxFlow.width;
+					// 	height: 200;
 
-						Video {
-							id: videoPlayer
-							source: model.type === 'videoEmbed' ? model.value : ''
-							height: 200;
-							width: 400;
-							fillMode: Image.PreserveAspectFit
-							autoLoad: false;
+					// 	Video {
+					// 		id: videoPlayer
+					// 		source: model.type === 'videoEmbed' ? model.value : ''
+					// 		height: 200;
+					// 		width: 400;
+					// 		fillMode: Image.PreserveAspectFit
+					// 		autoLoad: false;
 
-							onStatusChanged: {
-								if (status === 7) {
-									// Weird hack to make the video restart when it's over
-									// Ideally you'd want to use the seek function to restart the video but it doesn't work?
-									// Will need to make a more refined solution for this later. in the form of a more advanced media player.
-									// For now, this is sufficient. -AD
-									let originalURL = videoPlayer.source;
-									videoPlayer.source = "";
-									videoPlayer.source = originalURL;
-								}
-							}
+					// 		onStatusChanged: {
+					// 			if (status === 7) {
+					// 				// Weird hack to make the video restart when it's over
+					// 				// Ideally you'd want to use the seek function to restart the video but it doesn't work?
+					// 				// Will need to make a more refined solution for this later. in the form of a more advanced media player.
+					// 				// For now, this is sufficient. -AD
+					// 				let originalURL = videoPlayer.source;
+					// 				videoPlayer.source = "";
+					// 				videoPlayer.source = originalURL;
+					// 			}
+					// 		}
 
-							MouseArea {
-								anchors.fill: parent
-								onClicked: {
-									const videoIsOver = videoPlayer.position == videoPlayer.duration
-									if (videoPlayer.playbackState == MediaPlayer.PlayingState) {
-										videoPlayer.pause();
-									} 
-									else {
-										parent.play();
-									}
-								}
-							}
-						}
-					}
+					// 		MouseArea {
+					// 			anchors.fill: parent
+					// 			onClicked: {
+					// 				const videoIsOver = videoPlayer.position == videoPlayer.duration
+					// 				if (videoPlayer.playbackState == MediaPlayer.PlayingState) {
+					// 					videoPlayer.pause();
+					// 				} 
+					// 				else {
+					// 					parent.play();
+					// 				}
+					// 			}
+					// 		}
+					// 	}
 				}
 			}
 		}

From d518c12cf9343d7b411ebace8fc2d436e320031a Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Wed, 22 Jan 2025 23:12:03 -0600
Subject: [PATCH 44/49] Fix spacing on messages with links.

---
 scripts/system/domainChat/domainChat.qml | 1 -
 scripts/system/domainChat/formatting.js  | 4 ++--
 2 files changed, 2 insertions(+), 3 deletions(-)

diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml
index be4b6d4f79..28ec98d390 100644
--- a/scripts/system/domainChat/domainChat.qml
+++ b/scripts/system/domainChat/domainChat.qml
@@ -189,7 +189,6 @@ Rectangle {
                 }
             }
 
-
             ListModel {
                 id: local
             }
diff --git a/scripts/system/domainChat/formatting.js b/scripts/system/domainChat/formatting.js
index 24662e4ff1..c534f3a720 100644
--- a/scripts/system/domainChat/formatting.js
+++ b/scripts/system/domainChat/formatting.js
@@ -56,7 +56,7 @@ const formatting = {
             if (firstMatch == null) {
 				// If there is no more text to parse, break out of the loop and return the message array.
                 // Format any remaining text as a basic 'text' type.
-                messageArray.push({type: 'text', value: runningMessage});
+                if (runningMessage.trim() != "") messageArray.push({type: 'text', value: runningMessage});
 
                 // Append a final 'fill width' to the message text.
                 messageArray.push({type: 'messageEnd'});
@@ -95,7 +95,7 @@ const formatting = {
 
             let foundMatch = runningMessage.match(regex)[0];
 
-            messageArray.push({type: 'text', value: runningMessage.substring(0, indexOfFirstMatch)});
+			if (runningMessage.substring(0, indexOfFirstMatch) != "") messageArray.push({type: 'text', value: runningMessage.substring(0, indexOfFirstMatch)});
             messageArray.push({type: regexPatterns[firstMatch[1]].type, value: runningMessage.substring(indexOfFirstMatch, indexOfFirstMatch + foundMatch.length)});
             
             runningMessage = runningMessage.substring(indexOfFirstMatch + foundMatch.length);   // Remove the part of the message we have worked with

From 6b766e4daa9e40e1440164c1fc929b9bb81ed262 Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Mon, 27 Jan 2025 23:53:58 -0600
Subject: [PATCH 45/49] Fix wrapping. Remove unused video embed code.

---
 .../qml_widgets/TemplateChatMessage.qml       | 45 ++-----------------
 1 file changed, 4 insertions(+), 41 deletions(-)

diff --git a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
index 641a0ca0b3..0f97a614ae 100644
--- a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
+++ b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
@@ -1,7 +1,6 @@
 import QtQuick 2.15
 import QtQuick.Controls 2.15
 import QtQuick.Layouts 1.3
-// import QtMultimedia 5.15
 
 Component {
 	id: template_chat_message
@@ -43,14 +42,15 @@ Component {
 			Repeater {
 				model: delegateText;
 
-				RowLayout {
+				Item {
 					width: parent.width;
+					height: children[0].contentHeight;
 
 					TextEdit {
 						text: model.value || ""
 						font.pointSize: 12
-						wrapMode: Text.Wrap
-						width: messageBoxFlow.width;
+						wrapMode: TextEdit.WordWrap
+						width: parent.width * 0.8
 						visible: model.type === 'text' || model.type === 'mention';
 						readOnly: true
 						selectByMouse: true
@@ -170,44 +170,7 @@ Component {
 						}
 					}
 
-					// Item {
-					// 	visible: model.type === 'videoEmbed';
-					// 	width: messageBoxFlow.width;
-					// 	height: 200;
 
-					// 	Video {
-					// 		id: videoPlayer
-					// 		source: model.type === 'videoEmbed' ? model.value : ''
-					// 		height: 200;
-					// 		width: 400;
-					// 		fillMode: Image.PreserveAspectFit
-					// 		autoLoad: false;
-
-					// 		onStatusChanged: {
-					// 			if (status === 7) {
-					// 				// Weird hack to make the video restart when it's over
-					// 				// Ideally you'd want to use the seek function to restart the video but it doesn't work?
-					// 				// Will need to make a more refined solution for this later. in the form of a more advanced media player.
-					// 				// For now, this is sufficient. -AD
-					// 				let originalURL = videoPlayer.source;
-					// 				videoPlayer.source = "";
-					// 				videoPlayer.source = originalURL;
-					// 			}
-					// 		}
-
-					// 		MouseArea {
-					// 			anchors.fill: parent
-					// 			onClicked: {
-					// 				const videoIsOver = videoPlayer.position == videoPlayer.duration
-					// 				if (videoPlayer.playbackState == MediaPlayer.PlayingState) {
-					// 					videoPlayer.pause();
-					// 				} 
-					// 				else {
-					// 					parent.play();
-					// 				}
-					// 			}
-					// 		}
-					// 	}
 				}
 			}
 		}

From 9000d3a5ec14af111974d672eed423d465cd2890 Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Tue, 28 Jan 2025 00:00:11 -0600
Subject: [PATCH 46/49] Added a sortOrder.
 https://github.com/overte-org/overte/pull/1276

---
 scripts/system/domainChat/domainChat.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js
index 8cc42153eb..1fa85ba37a 100644
--- a/scripts/system/domainChat/domainChat.js
+++ b/scripts/system/domainChat/domainChat.js
@@ -49,6 +49,7 @@
         appButton = tablet.addButton({
             icon: Script.resolvePath("./img/icon_white.png"),
             activeIcon: Script.resolvePath("./img/icon_black.png"),
+            sortOrder: 8,
             text: "CHAT",
             sortOrder: 8,
             isActive: appIsVisible,

From b1e8019e2a40553e0161b81d2c90432eeff1fa5e Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Fri, 14 Feb 2025 08:34:59 -0600
Subject: [PATCH 47/49] Convert indentation spaces to tabs.

---
 scripts/system/domainChat/formatting.js | 74 ++++++++++++-------------
 1 file changed, 37 insertions(+), 37 deletions(-)

diff --git a/scripts/system/domainChat/formatting.js b/scripts/system/domainChat/formatting.js
index c534f3a720..22ae3ee203 100644
--- a/scripts/system/domainChat/formatting.js
+++ b/scripts/system/domainChat/formatting.js
@@ -25,7 +25,7 @@ const formatting = {
 		// Gets the current time and adds it to a given packet
 		const timeArray = formatting.helpers._timestampArray(packet.timestamp);
 		packet.timeString = timeArray[0];
-        packet.dateString = timeArray[1];
+		packet.dateString = timeArray[1];
 		return packet;
 	},
 	trimPacketToSave: function(packet) {
@@ -40,31 +40,31 @@ const formatting = {
 	},
 	parseMessage: async function(message, enableEmbedding) {
 		const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;
-        const overteLocationRegex = /hifi:\/\/[a-zA-Z0-9_-]+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+/;
+		const overteLocationRegex = /hifi:\/\/[a-zA-Z0-9_-]+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+/;
 
 		let runningMessage = message;			// The remaining message that will be parsed
-        let messageArray = [];					// An array of messages that are split up by the formatting functions
+		let messageArray = [];					// An array of messages that are split up by the formatting functions
 
 		const regexPatterns = [
-            { type: "url", regex: urlRegex },
-            { type: "overteLocation", regex: overteLocationRegex }
-        ]
+			{ type: "url", regex: urlRegex },
+			{ type: "overteLocation", regex: overteLocationRegex }
+		]
 
 		while (true) {
-            let firstMatch = _findFirstMatch();
+			let firstMatch = _findFirstMatch();
 
-            if (firstMatch == null) {
+			if (firstMatch == null) {
 				// If there is no more text to parse, break out of the loop and return the message array.
-                // Format any remaining text as a basic 'text' type.
-                if (runningMessage.trim() != "") messageArray.push({type: 'text', value: runningMessage});
+				// Format any remaining text as a basic 'text' type.
+				if (runningMessage.trim() != "") messageArray.push({type: 'text', value: runningMessage});
 
-                // Append a final 'fill width' to the message text.
-                messageArray.push({type: 'messageEnd'});
-                break;
-            }
+				// Append a final 'fill width' to the message text.
+				messageArray.push({type: 'messageEnd'});
+				break;
+			}
 
-            _formatMessage(firstMatch);
-        }
+			_formatMessage(firstMatch);
+		}
 
 		// Embed images in the message array.
 		if (enableEmbedding) {
@@ -90,35 +90,35 @@ const formatting = {
 		return messageArray;
 
 		function _formatMessage(firstMatch){
-            let indexOfFirstMatch = firstMatch[0];
-            let regex = regexPatterns[firstMatch[1]].regex;
+			let indexOfFirstMatch = firstMatch[0];
+			let regex = regexPatterns[firstMatch[1]].regex;
 
-            let foundMatch = runningMessage.match(regex)[0];
+			let foundMatch = runningMessage.match(regex)[0];
 
 			if (runningMessage.substring(0, indexOfFirstMatch) != "") messageArray.push({type: 'text', value: runningMessage.substring(0, indexOfFirstMatch)});
-            messageArray.push({type: regexPatterns[firstMatch[1]].type, value: runningMessage.substring(indexOfFirstMatch, indexOfFirstMatch + foundMatch.length)});
-            
-            runningMessage = runningMessage.substring(indexOfFirstMatch + foundMatch.length);   // Remove the part of the message we have worked with
-        }
+			messageArray.push({type: regexPatterns[firstMatch[1]].type, value: runningMessage.substring(indexOfFirstMatch, indexOfFirstMatch + foundMatch.length)});
+			
+			runningMessage = runningMessage.substring(indexOfFirstMatch + foundMatch.length);   // Remove the part of the message we have worked with
+		}
 
-        function _findFirstMatch(){
-            let indexOfFirstMatch = Infinity;
-            let indexOfRegexPattern = Infinity;
+		function _findFirstMatch(){
+			let indexOfFirstMatch = Infinity;
+			let indexOfRegexPattern = Infinity;
 
-            for (let i = 0; regexPatterns.length > i; i++){
-                let indexOfMatch = runningMessage.search(regexPatterns[i].regex);
+			for (let i = 0; regexPatterns.length > i; i++){
+				let indexOfMatch = runningMessage.search(regexPatterns[i].regex);
 
-                if (indexOfMatch == -1) continue;                                              // No match found
+				if (indexOfMatch == -1) continue;                                              // No match found
 
-                if (indexOfMatch < indexOfFirstMatch) {
-                    indexOfFirstMatch = indexOfMatch;
-                    indexOfRegexPattern = i;
-                }
-            }
+				if (indexOfMatch < indexOfFirstMatch) {
+					indexOfFirstMatch = indexOfMatch;
+					indexOfRegexPattern = i;
+				}
+			}
 
-            if (indexOfFirstMatch !== Infinity) return [indexOfFirstMatch, indexOfRegexPattern];    // If there was a found match
-            return null;                                                                            // No found match
-        }
+			if (indexOfFirstMatch !== Infinity) return [indexOfFirstMatch, indexOfRegexPattern];    // If there was a found match
+			return null;                                                                            // No found match
+		}
 	},
 
 	helpers: {

From 3d16617bba16442fd0753d1e0083eb08925c06a0 Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Fri, 14 Feb 2025 17:34:04 -0600
Subject: [PATCH 48/49] Convert tab indentation to spaces. Derp.

---
 scripts/system/domainChat/formatting.js       | 268 ++++++++--------
 .../qml_widgets/TemplateChatMessage.qml       | 288 +++++++++---------
 .../qml_widgets/TemplateNotification.qml      |  60 ++--
 3 files changed, 308 insertions(+), 308 deletions(-)

diff --git a/scripts/system/domainChat/formatting.js b/scripts/system/domainChat/formatting.js
index 22ae3ee203..3cd0d9d335 100644
--- a/scripts/system/domainChat/formatting.js
+++ b/scripts/system/domainChat/formatting.js
@@ -10,158 +10,158 @@
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
 
 const formatting = {
-	toJSON: function(data) {
-		if (typeof data == "object") return data; // Already JSON
-		
-		try {
-			const parsedData = JSON.parse(data);
-			return parsedData;
-		} catch (e) {
-			console.log('Failed to convert data to JSON.')
-			return null; // Could not convert to json, some error;
-		}
-	},
-	addTimeAndDateStringToPacket: function(packet) {
-		// Gets the current time and adds it to a given packet
-		const timeArray = formatting.helpers._timestampArray(packet.timestamp);
-		packet.timeString = timeArray[0];
-		packet.dateString = timeArray[1];
-		return packet;
-	},
-	trimPacketToSave: function(packet) {
-		// Takes a packet, and returns a packet containing only what is needed to save.
-		let newPacket = {
-			channel: packet.channel || "",
-			displayName: packet.displayName || "",
-			message: packet.message || "",
-			timestamp: packet.timestamp || formatting.helpers.getTimestamp(),
-		};
-		return newPacket;
-	},
-	parseMessage: async function(message, enableEmbedding) {
-		const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;
-		const overteLocationRegex = /hifi:\/\/[a-zA-Z0-9_-]+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+/;
+    toJSON: function(data) {
+        if (typeof data == "object") return data; // Already JSON
+        
+        try {
+            const parsedData = JSON.parse(data);
+            return parsedData;
+        } catch (e) {
+            console.log('Failed to convert data to JSON.')
+            return null; // Could not convert to json, some error;
+        }
+    },
+    addTimeAndDateStringToPacket: function(packet) {
+        // Gets the current time and adds it to a given packet
+        const timeArray = formatting.helpers._timestampArray(packet.timestamp);
+        packet.timeString = timeArray[0];
+        packet.dateString = timeArray[1];
+        return packet;
+    },
+    trimPacketToSave: function(packet) {
+        // Takes a packet, and returns a packet containing only what is needed to save.
+        let newPacket = {
+            channel: packet.channel || "",
+            displayName: packet.displayName || "",
+            message: packet.message || "",
+            timestamp: packet.timestamp || formatting.helpers.getTimestamp(),
+        };
+        return newPacket;
+    },
+    parseMessage: async function(message, enableEmbedding) {
+        const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;
+        const overteLocationRegex = /hifi:\/\/[a-zA-Z0-9_-]+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+/;
 
-		let runningMessage = message;			// The remaining message that will be parsed
-		let messageArray = [];					// An array of messages that are split up by the formatting functions
+        let runningMessage = message;			// The remaining message that will be parsed
+        let messageArray = [];					// An array of messages that are split up by the formatting functions
 
-		const regexPatterns = [
-			{ type: "url", regex: urlRegex },
-			{ type: "overteLocation", regex: overteLocationRegex }
-		]
+        const regexPatterns = [
+            { type: "url", regex: urlRegex },
+            { type: "overteLocation", regex: overteLocationRegex }
+        ]
 
-		while (true) {
-			let firstMatch = _findFirstMatch();
+        while (true) {
+            let firstMatch = _findFirstMatch();
 
-			if (firstMatch == null) {
-				// If there is no more text to parse, break out of the loop and return the message array.
-				// Format any remaining text as a basic 'text' type.
-				if (runningMessage.trim() != "") messageArray.push({type: 'text', value: runningMessage});
+            if (firstMatch == null) {
+                // If there is no more text to parse, break out of the loop and return the message array.
+                // Format any remaining text as a basic 'text' type.
+                if (runningMessage.trim() != "") messageArray.push({type: 'text', value: runningMessage});
 
-				// Append a final 'fill width' to the message text.
-				messageArray.push({type: 'messageEnd'});
-				break;
-			}
+                // Append a final 'fill width' to the message text.
+                messageArray.push({type: 'messageEnd'});
+                break;
+            }
 
-			_formatMessage(firstMatch);
-		}
+            _formatMessage(firstMatch);
+        }
 
-		// Embed images in the message array.
-		if (enableEmbedding) {
-			for (dataChunk of messageArray){
-				if (dataChunk.type == 'url'){
-					let url = dataChunk.value;
+        // Embed images in the message array.
+        if (enableEmbedding) {
+            for (dataChunk of messageArray){
+                if (dataChunk.type == 'url'){
+                    let url = dataChunk.value;
 
-					const res = await formatting.helpers.fetch(url, {method: 'GET'});      // TODO: Replace with 'HEAD' method. https://github.com/overte-org/overte/issues/1273
-					const contentType = res.getResponseHeader("content-type");
+                    const res = await formatting.helpers.fetch(url, {method: 'GET'});      // TODO: Replace with 'HEAD' method. https://github.com/overte-org/overte/issues/1273
+                    const contentType = res.getResponseHeader("content-type");
 
-					if (contentType.startsWith('image/')) {
-						messageArray.push({type: 'imageEmbed', value: url});
-						continue;
-					}
-					if (contentType.startsWith('video/')){ 
-						messageArray.push({type: 'videoEmbed', value: url});
-						continue;
-					}
-				}
-			}
-		}
+                    if (contentType.startsWith('image/')) {
+                        messageArray.push({type: 'imageEmbed', value: url});
+                        continue;
+                    }
+                    if (contentType.startsWith('video/')){ 
+                        messageArray.push({type: 'videoEmbed', value: url});
+                        continue;
+                    }
+                }
+            }
+        }
 
-		return messageArray;
+        return messageArray;
 
-		function _formatMessage(firstMatch){
-			let indexOfFirstMatch = firstMatch[0];
-			let regex = regexPatterns[firstMatch[1]].regex;
+        function _formatMessage(firstMatch){
+            let indexOfFirstMatch = firstMatch[0];
+            let regex = regexPatterns[firstMatch[1]].regex;
 
-			let foundMatch = runningMessage.match(regex)[0];
+            let foundMatch = runningMessage.match(regex)[0];
 
-			if (runningMessage.substring(0, indexOfFirstMatch) != "") messageArray.push({type: 'text', value: runningMessage.substring(0, indexOfFirstMatch)});
-			messageArray.push({type: regexPatterns[firstMatch[1]].type, value: runningMessage.substring(indexOfFirstMatch, indexOfFirstMatch + foundMatch.length)});
-			
-			runningMessage = runningMessage.substring(indexOfFirstMatch + foundMatch.length);   // Remove the part of the message we have worked with
-		}
+            if (runningMessage.substring(0, indexOfFirstMatch) != "") messageArray.push({type: 'text', value: runningMessage.substring(0, indexOfFirstMatch)});
+            messageArray.push({type: regexPatterns[firstMatch[1]].type, value: runningMessage.substring(indexOfFirstMatch, indexOfFirstMatch + foundMatch.length)});
+            
+            runningMessage = runningMessage.substring(indexOfFirstMatch + foundMatch.length);   // Remove the part of the message we have worked with
+        }
 
-		function _findFirstMatch(){
-			let indexOfFirstMatch = Infinity;
-			let indexOfRegexPattern = Infinity;
+        function _findFirstMatch(){
+            let indexOfFirstMatch = Infinity;
+            let indexOfRegexPattern = Infinity;
 
-			for (let i = 0; regexPatterns.length > i; i++){
-				let indexOfMatch = runningMessage.search(regexPatterns[i].regex);
+            for (let i = 0; regexPatterns.length > i; i++){
+                let indexOfMatch = runningMessage.search(regexPatterns[i].regex);
 
-				if (indexOfMatch == -1) continue;                                              // No match found
+                if (indexOfMatch == -1) continue;                                              // No match found
 
-				if (indexOfMatch < indexOfFirstMatch) {
-					indexOfFirstMatch = indexOfMatch;
-					indexOfRegexPattern = i;
-				}
-			}
+                if (indexOfMatch < indexOfFirstMatch) {
+                    indexOfFirstMatch = indexOfMatch;
+                    indexOfRegexPattern = i;
+                }
+            }
 
-			if (indexOfFirstMatch !== Infinity) return [indexOfFirstMatch, indexOfRegexPattern];    // If there was a found match
-			return null;                                                                            // No found match
-		}
-	},
+            if (indexOfFirstMatch !== Infinity) return [indexOfFirstMatch, indexOfRegexPattern];    // If there was a found match
+            return null;                                                                            // No found match
+        }
+    },
 
-	helpers: {
-		// Small functions that are used often in the other functions.
-		_timestampArray: function(timestamp) {
-			const currentDate = timestamp || formatting.helpers.getTimestamp();
-			let timeArray = [];
+    helpers: {
+        // Small functions that are used often in the other functions.
+        _timestampArray: function(timestamp) {
+            const currentDate = timestamp || formatting.helpers.getTimestamp();
+            let timeArray = [];
 
-			timeArray.push(new Date(currentDate).toLocaleTimeString(undefined, {
-				hour12: false,
-			}));
-	
-			timeArray.push(new Date(currentDate).toLocaleDateString(undefined, {
-				year: "numeric",
-				month: "long",
-				day: "numeric",
-			}));
-	
-			return timeArray;
-		},
-		getTimestamp: function(){
-			return Date.now();
-		},
-		fetch: function (url, options = {method: "GET"}) {
-			return new Promise((resolve, reject) => {
-				let req = new XMLHttpRequest();
-	
-				req.onreadystatechange = function () {
-	
-					if (req.readyState === req.DONE) {
-						if (req.status === 200) {
-							resolve(req);
-				
-						} else {
-							console.log("Error", req.status, req.statusText);
-							reject();
-						}
-					}
-				};
-	
-				req.open(options.method, url);
-				req.send();
-			});
-		}
-	}
+            timeArray.push(new Date(currentDate).toLocaleTimeString(undefined, {
+                hour12: false,
+            }));
+    
+            timeArray.push(new Date(currentDate).toLocaleDateString(undefined, {
+                year: "numeric",
+                month: "long",
+                day: "numeric",
+            }));
+    
+            return timeArray;
+        },
+        getTimestamp: function(){
+            return Date.now();
+        },
+        fetch: function (url, options = {method: "GET"}) {
+            return new Promise((resolve, reject) => {
+                let req = new XMLHttpRequest();
+    
+                req.onreadystatechange = function () {
+    
+                    if (req.readyState === req.DONE) {
+                        if (req.status === 200) {
+                            resolve(req);
+                
+                        } else {
+                            console.log("Error", req.status, req.statusText);
+                            reject();
+                        }
+                    }
+                };
+    
+                req.open(options.method, url);
+                req.send();
+            });
+        }
+    }
 }
diff --git a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
index 0f97a614ae..124f1888c6 100644
--- a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
+++ b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
@@ -3,176 +3,176 @@ import QtQuick.Controls 2.15
 import QtQuick.Layouts 1.3
 
 Component {
-	id: template_chat_message
+    id: template_chat_message
 
-	Rectangle {
-		property int index: delegateIndex
+    Rectangle {
+        property int index: delegateIndex
 
-		height: Math.max(65, children[1].height + 30)
-		color: index % 2 === 0 ? "transparent" : Qt.rgba(0.15,0.15,0.15,1)
-		width: listview.parent.parent.width
-		Layout.fillWidth: true
+        height: Math.max(65, children[1].height + 30)
+        color: index % 2 === 0 ? "transparent" : Qt.rgba(0.15,0.15,0.15,1)
+        width: listview.parent.parent.width
+        Layout.fillWidth: true
 
-		Item {
-			width: parent.width - 10
-			anchors.horizontalCenter: parent.horizontalCenter
-			height: 22
+        Item {
+            width: parent.width - 10
+            anchors.horizontalCenter: parent.horizontalCenter
+            height: 22
 
-			TextEdit {
-				text: delegateUsername;
-				color: "lightgray";
-				readOnly: true;
-				selectByMouse: true;
-				selectByKeyboard: true;
-			}
+            TextEdit {
+                text: delegateUsername;
+                color: "lightgray";
+                readOnly: true;
+                selectByMouse: true;
+                selectByKeyboard: true;
+            }
 
-			Text {
-				anchors.right: parent.right;
-				text: delegateDate;
-				color: "lightgray";
-			}
-		}
+            Text {
+                anchors.right: parent.right;
+                text: delegateDate;
+                color: "lightgray";
+            }
+        }
 
-		Flow {
-			anchors.top: parent.children[0].bottom;
-			width: parent.width;
-			x: 5
-			id: messageBoxFlow
+        Flow {
+            anchors.top: parent.children[0].bottom;
+            width: parent.width;
+            x: 5
+            id: messageBoxFlow
 
-			Repeater {
-				model: delegateText;
+            Repeater {
+                model: delegateText;
 
-				Item {
-					width: parent.width;
-					height: children[0].contentHeight;
+                Item {
+                    width: parent.width;
+                    height: children[0].contentHeight;
 
-					TextEdit {
-						text: model.value || ""
-						font.pointSize: 12
-						wrapMode: TextEdit.WordWrap
-						width: parent.width * 0.8
-						visible: model.type === 'text' || model.type === 'mention';
-						readOnly: true
-						selectByMouse: true
-						selectByKeyboard: true
+                    TextEdit {
+                        text: model.value || ""
+                        font.pointSize: 12
+                        wrapMode: TextEdit.WordWrap
+                        width: parent.width * 0.8
+                        visible: model.type === 'text' || model.type === 'mention';
+                        readOnly: true
+                        selectByMouse: true
+                        selectByKeyboard: true
 
-						color: {
-							switch (model.type) {
-								case "mention":
-									return "purple";
-								default:
-									return "white";
-							}
-						}
-					}
+                        color: {
+                            switch (model.type) {
+                                case "mention":
+                                    return "purple";
+                                default:
+                                    return "white";
+                            }
+                        }
+                    }
 
-					RowLayout {
-						width: urlTypeTextDisplay.width;
-						visible: model.type === 'url';
+                    RowLayout {
+                        width: urlTypeTextDisplay.width;
+                        visible: model.type === 'url';
 
-						TextEdit {
-							id: urlTypeTextDisplay;
-							text: model.value || "";
-							font.pointSize: 12;
-							wrapMode: Text.Wrap;
-							color: "#4EBAFD";
-							font.underline: true;
-							width: parent.width;
-							readOnly: true
-							selectByMouse: true
-							selectByKeyboard: true
+                        TextEdit {
+                            id: urlTypeTextDisplay;
+                            text: model.value || "";
+                            font.pointSize: 12;
+                            wrapMode: Text.Wrap;
+                            color: "#4EBAFD";
+                            font.underline: true;
+                            width: parent.width;
+                            readOnly: true
+                            selectByMouse: true
+                            selectByKeyboard: true
 
-							MouseArea {
-								anchors.fill: parent;
+                            MouseArea {
+                                anchors.fill: parent;
 
-								onClicked: {
-									Window.openWebBrowser(model.value);
-								}
-							}
-						}
+                                onClicked: {
+                                    Window.openWebBrowser(model.value);
+                                }
+                            }
+                        }
 
-						Text {
-							text: "🗗";
-							font.pointSize: 10;
-							wrapMode: Text.Wrap;
-							color: "white";
+                        Text {
+                            text: "🗗";
+                            font.pointSize: 10;
+                            wrapMode: Text.Wrap;
+                            color: "white";
 
-							MouseArea {
-								anchors.fill: parent;
+                            MouseArea {
+                                anchors.fill: parent;
 
-								onClicked: {
-									Qt.openUrlExternally(model.value);
-								}
-							}
-						}
-					}
+                                onClicked: {
+                                    Qt.openUrlExternally(model.value);
+                                }
+                            }
+                        }
+                    }
 
-					RowLayout {
-						visible: model.type === 'overteLocation';
-						width: Math.min(messageBoxFlow.width, children[0].children[1].contentWidth + 35);
-						height: 20;
-						Layout.leftMargin: 5
-						Layout.rightMargin: 5
+                    RowLayout {
+                        visible: model.type === 'overteLocation';
+                        width: Math.min(messageBoxFlow.width, children[0].children[1].contentWidth + 35);
+                        height: 20;
+                        Layout.leftMargin: 5
+                        Layout.rightMargin: 5
 
-						Rectangle {
-							width: parent.width;
-							height: 20;
-							color: "lightgray"
-							radius: 2;
+                        Rectangle {
+                            width: parent.width;
+                            height: 20;
+                            color: "lightgray"
+                            radius: 2;
 
-							Image {
-								source: "../img/ui/world_black.png"
-								width: 18;
-								height: 18;
-								sourceSize.width: 18
-								sourceSize.height: 18
-								anchors.left: parent.left
-								anchors.verticalCenter: parent.verticalCenter 
-								anchors.leftMargin: 2
-								anchors.rightMargin: 10
-							}
+                            Image {
+                                source: "../img/ui/world_black.png"
+                                width: 18;
+                                height: 18;
+                                sourceSize.width: 18
+                                sourceSize.height: 18
+                                anchors.left: parent.left
+                                anchors.verticalCenter: parent.verticalCenter 
+                                anchors.leftMargin: 2
+                                anchors.rightMargin: 10
+                            }
 
-							TextEdit {
-								text: model.type === 'overteLocation' ? model.value.split('hifi://')[1].split('/')[0] : '';
-								color: "black"
-								font.pointSize: 12
-								x: parent.children[0].width + 5;
-								anchors.verticalCenter: parent.verticalCenter 
-								readOnly: true
-								selectByMouse: true
-								selectByKeyboard: true
-							}
+                            TextEdit {
+                                text: model.type === 'overteLocation' ? model.value.split('hifi://')[1].split('/')[0] : '';
+                                color: "black"
+                                font.pointSize: 12
+                                x: parent.children[0].width + 5;
+                                anchors.verticalCenter: parent.verticalCenter 
+                                readOnly: true
+                                selectByMouse: true
+                                selectByKeyboard: true
+                            }
 
-							MouseArea {
-								anchors.fill: parent;
+                            MouseArea {
+                                anchors.fill: parent;
 
-								onClicked: {
-									Window.openUrl(model.value);
-								}
-							}
-						}
-					}
+                                onClicked: {
+                                    Window.openUrl(model.value);
+                                }
+                            }
+                        }
+                    }
 
-					Item {
-						Layout.fillWidth: true;
-						visible: model.type === 'messageEnd';
-					}
+                    Item {
+                        Layout.fillWidth: true;
+                        visible: model.type === 'messageEnd';
+                    }
 
-					Item {
-						visible: model.type === 'imageEmbed';
-						width: messageBoxFlow.width;
-						height: 200
+                    Item {
+                        visible: model.type === 'imageEmbed';
+                        width: messageBoxFlow.width;
+                        height: 200
 
-						AnimatedImage {
-							source: model.type === 'imageEmbed' ? model.value : ''
-							height: Math.min(sourceSize.height, 200);
-							fillMode: Image.PreserveAspectFit
-						}
-					}
+                        AnimatedImage {
+                            source: model.type === 'imageEmbed' ? model.value : ''
+                            height: Math.min(sourceSize.height, 200);
+                            fillMode: Image.PreserveAspectFit
+                        }
+                    }
 
 
-				}
-			}
-		}
-	}
+                }
+            }
+        }
+    }
 }
\ No newline at end of file
diff --git a/scripts/system/domainChat/qml_widgets/TemplateNotification.qml b/scripts/system/domainChat/qml_widgets/TemplateNotification.qml
index 4b9797d7f4..3c4fcdb240 100644
--- a/scripts/system/domainChat/qml_widgets/TemplateNotification.qml
+++ b/scripts/system/domainChat/qml_widgets/TemplateNotification.qml
@@ -3,39 +3,39 @@ import QtQuick.Controls 2.15
 import QtQuick.Layouts 1.3
 
 Component {
-	id: template_notification
+    id: template_notification
 
-	Rectangle {
-		color: "#171717"
-		width: parent.width
-		height: 40
+    Rectangle {
+        color: "#171717"
+        width: parent.width
+        height: 40
 
-		RowLayout {
-			width: parent.width
-			height: parent.height
+        RowLayout {
+            width: parent.width
+            height: parent.height
 
-			Rectangle {
-				height: parent.height
-				width: 5
-				color: "#505186"
-			}
+            Rectangle {
+                height: parent.height
+                width: 5
+                color: "#505186"
+            }
 
-			Repeater {
-				model: delegateText
+            Repeater {
+                model: delegateText
 
-				TextEdit {
-					visible: model.value != undefined;
-					text: model.value || ""
-					color: "white"
-					font.pointSize: 12
-					readOnly: true
-					selectByMouse: true
-					selectByKeyboard: true
-					height: root.height
-					wrapMode: Text.Wrap
-					font.italic: true
-				}
-			}
-		}
-	}
+                TextEdit {
+                    visible: model.value != undefined;
+                    text: model.value || ""
+                    color: "white"
+                    font.pointSize: 12
+                    readOnly: true
+                    selectByMouse: true
+                    selectByKeyboard: true
+                    height: root.height
+                    wrapMode: Text.Wrap
+                    font.italic: true
+                }
+            }
+        }
+    }
 }

From 25a9976a7ff54a5b53297e0e192b37552992685f Mon Sep 17 00:00:00 2001
From: armored-dragon <publicmail@armoreddragon.com>
Date: Tue, 18 Feb 2025 13:57:54 -0600
Subject: [PATCH 49/49] Fix links not wrapping.

---
 .../qml_widgets/TemplateChatMessage.qml       | 19 ++++++++++++++-----
 1 file changed, 14 insertions(+), 5 deletions(-)

diff --git a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
index 124f1888c6..b97301ddf2 100644
--- a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
+++ b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
@@ -66,8 +66,9 @@ Component {
                         }
                     }
 
-                    RowLayout {
-                        width: urlTypeTextDisplay.width;
+                    Flow {
+                        width: parent.width * 0.8;
+                        height: 20
                         visible: model.type === 'url';
 
                         TextEdit {
@@ -77,10 +78,10 @@ Component {
                             wrapMode: Text.Wrap;
                             color: "#4EBAFD";
                             font.underline: true;
-                            width: parent.width;
                             readOnly: true
                             selectByMouse: true
                             selectByKeyboard: true
+                            width: Math.min(parent.width - 20, textMetrics.tightBoundingRect.width) ;
 
                             MouseArea {
                                 anchors.fill: parent;
@@ -91,12 +92,20 @@ Component {
                             }
                         }
 
+                        TextMetrics {
+                            id: textMetrics
+                            font: urlTypeTextDisplay.font
+                            text: urlTypeTextDisplay.text
+                        }
+
                         Text {
+                            width: 20;
                             text: "🗗";
                             font.pointSize: 10;
-                            wrapMode: Text.Wrap;
                             color: "white";
-
+                            horizontalAlignment: Text.AlignHCenter
+                            verticalAlignment: Text.AlignVCenter
+                            
                             MouseArea {
                                 anchors.fill: parent;