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 55%
rename from scripts/communityScripts/armored-chat/armored_chat.js
rename to scripts/system/domainChat/domainChat.js
index ae46f4d8f3..1fa85ba37a 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.
@@ -10,12 +10,19 @@
 (() => {
     ("use strict");
 
+    Script.include([
+        "./formatting.js"
+    ])
+
     var appIsVisible = false;
     var settings = {
         external_window: false,
         maximum_messages: 200,
-        join_notification: true
+        join_notification: true,
+        switchToInternalOnHeadsetUsed: true,
+        enableEmbedding: false                  // Prevents information leakage, default false
     };
+    let temporaryChangeModeToVirtual = false;
 
     // Global vars
     var tablet;
@@ -28,15 +35,11 @@
     var palData = AvatarManager.getPalData().data;
 
     Controller.keyPressEvent.connect(keyPressEvent);
-    Messages.subscribe("Chat"); // Floofchat
     Messages.subscribe("chat");
     Messages.messageReceived.connect(receivedMessage);
-    AvatarManager.avatarAddedEvent.connect((sessionId) => {
-        _avatarAction("connected", sessionId);
-    });
-    AvatarManager.avatarRemovedEvent.connect((sessionId) => {
-        _avatarAction("left", sessionId);
-    });
+    AvatarManager.avatarAddedEvent.connect((sessionId) => { _avatarAction("connected", sessionId); });
+    AvatarManager.avatarRemovedEvent.connect((sessionId) => { _avatarAction("left", sessionId); });
+    HMD.displayModeChanged.connect(_onHMDDisplayModeChanged);
 
     startup();
 
@@ -46,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,
@@ -62,7 +66,7 @@
         appButton.clicked.connect(toggleMainChatWindow);
 
         quickMessage = new OverlayWindow({
-            source: Script.resolvePath("./armored_chat_quick_message.qml"),
+            source: Script.resolvePath("./domainChatQuick.qml"),
         });
 
         _openWindow();
@@ -79,7 +83,7 @@
     }
     function _openWindow() {
         chatOverlayWindow = new Desktop.createWindow(
-            Script.resolvePath("./armored_chat.qml"),
+            Script.resolvePath("./domainChat.qml"),
             {
                 title: "Chat",
                 size: { x: 550, y: 400 },
@@ -93,64 +97,36 @@
         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;
-        message = JSON.parse(message);
+        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.
+        
+        let formattedMessagePacket = { ...message };
+        formattedMessagePacket.message = await formatting.parseMessage(message.message, settings.enableEmbedding)
 
-        // Get the message data
-        const currentTimestamp = _getTimestamp();
-        const timeArray = _formatTimestamp(currentTimestamp);
+        _emitEvent({ type: "show_message", ...formattedMessagePacket });        // Update qml view of to new message.
+        _notificationCoreMessage(message.displayName, message.message)          // Show a new message on screen.
 
-        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
+        // Create a new variable based on the message that will be saved.
+        let trimmedPacket = formatting.trimPacketToSave(message);
+        messageHistory.push(trimmedPacket);
 
-        // Floofchat compatibility hook
-        message = floofChatCompatibilityConversion(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; 
-
-        // Format the timestamp 
-        message.timeString = timeArray[0];
-        message.dateString = timeArray[1];
-
-        // 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
-        let savedMessage = message;
-
-        // 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;
         }
     }
@@ -161,15 +137,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;
                 }
 
@@ -203,6 +180,24 @@
                 });
         }
     }
+    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;
+        
+        console.log(`Presentation mode was changed to ${chatOverlayWindow.presentationMode}`);
+    }
     function _sendMessage(message, channel) {
         if (message.length == 0) return;
 
@@ -216,11 +211,9 @@
                 action: "send_chat_message",
             })
         );
-
-        floofChatCompatibilitySendMessage(message, channel);
     }
     function _avatarAction(type, sessionId) {
-        Script.setTimeout(() => {
+        Script.setTimeout(async () => {
             if (type == "connected") {
                 palData = AvatarManager.getPalData().data;
             }
@@ -237,101 +230,68 @@
             }
 
             // 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
             if (settings.join_notification){
-                Messages.sendLocalMessage(
-                    "Floof-Notif",
-                    JSON.stringify({
-                        sender: displayName,
-                        text: type,
-                    })
-                );
+                _notificationCoreMessage(displayName, type)
             }
 
-            _emitEvent({ type: "notification", ...message });
+            // Format notification message
+            let formattedMessagePacket = {...message};
+            formattedMessagePacket.message = await formatting.parseMessage(message.message);
+
+            _emitEvent({ type: "notification", ...formattedMessagePacket });
         }, 1500);
     }
-    function _loadSettings() {
+    async function _loadSettings() {
         settings = Settings.getValue("ArmoredChat-Config", settings);
+        console.log("Loading settings: ", jstr(settings));
 
         if (messageHistory) {
             // Load message history
-            messageHistory.forEach((message) => {
-                const timeArray = _formatTimestamp(_getTimestamp());
-                message.timeString = timeArray[0];
-                message.dateString = timeArray[1];
-                _emitEvent({ type: "show_message", ...message });
-            });
+            for (message of messageHistory) {
+                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
+            }
         }
 
-        // 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");
+        console.log("Saving settings: ", jstr(settings));
         Settings.setValue("ArmoredChat-Config", settings);
     }
-    function _getTimestamp(){
-        return Date.now();
+    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 })
+        );
     }
-    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;
-    }
-
     /**
      * Emit a packet to the HTML front end. Easy communication!
      * @param {Object} packet - The Object packet to emit to the HTML
      * @param {("show_message"|"clear_messages"|"notification"|"initial_settings")} packet.type - The type of packet it is
      */
     function _emitEvent(packet = { type: "" }) {
+        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);
     }
 
-    //
-    // 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",
-            })
-        );
-    }
+    // Debug and developer functions and data
+    const jstr = (object) => JSON.stringify(object, null, 4);       // JSON Stringify function with formatting
 })();
diff --git a/scripts/communityScripts/armored-chat/armored_chat.qml b/scripts/system/domainChat/domainChat.qml
similarity index 69%
rename from scripts/communityScripts/armored-chat/armored_chat.qml
rename to scripts/system/domainChat/domainChat.qml
index 07eb75c626..28ec98d390 100644
--- a/scripts/communityScripts/armored-chat/armored_chat.qml
+++ b/scripts/system/domainChat/domainChat.qml
@@ -2,14 +2,16 @@ 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)
     signal sendToScript(var message);
 
-    property string pageVal: "local"
-    property string last_message_user: ""
-    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.
@@ -162,18 +164,14 @@ Rectangle {
                         model: getChannel(pageVal)
                         delegate: Loader {
                             property int delegateIndex: model.index
-                            property string delegateText: model.text
+                            property var delegateText: model.message
                             property string delegateUsername: model.username
                             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;
                             }
-                        
                         }
                     }
                 }
@@ -191,7 +189,6 @@ Rectangle {
                 }
             }
 
-
             ListModel {
                 id: local
             }
@@ -373,123 +370,60 @@ 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})
+                        }
+                    }
+                }
+                // 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})
+                        }
+                    }
+                }
             }
         }
 
     }
 
     // Templates
-    Component {
-        id: template_chat_message
-
-        Rectangle {
-            property int index: delegateIndex
-            property string texttest: delegateText
-            property string username: delegateUsername
-            property string date: delegateDate
-
-            height: Math.max(65, children[1].height + 30)
-            color: index % 2 === 0 ? "transparent" : Qt.rgba(0.15,0.15,0.15,1)
-            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: date
-                    color: "lightgray"
-                }
-            }
-
-            TextEdit {
-                anchors.top: parent.children[0].bottom
-                x: 5
-                text: texttest
-                color:"white"
-                font.pointSize: 12
-                readOnly: true
-                selectByMouse: true
-                selectByKeyboard: true
-                width: parent.width * 0.8
-                height: contentHeight
-                wrapMode: Text.Wrap
-                textFormat: TextEdit.RichText
-
-                onLinkActivated: {
-                    Window.openWebBrowser(link)
-                }
-            }
-        }
-    }
-
-    Component {
-        id: template_notification
-
-        Rectangle{
-            property int index: delegateIndex
-            property string texttest: delegateText
-            property string username: delegateUsername
-            property string date: delegateDate
-            color: "#171717"
-            width: parent.width
-            height: 40
-
-            Item {
-                width: 10
-                height: parent.height
-
-                Rectangle {
-                    height: parent.height
-                    width: 5
-                    color: "#505186"
-                }
-            }
-
-
-            Item {
-                width: parent.width - parent.children[0].width - 5
-                height: parent.height
-                anchors.left: parent.children[0].right
-
-                TextEdit{
-                    text: texttest
-                    color:"white"
-                    font.pointSize: 12
-                    readOnly: true
-                    width: parent.width * 0.8
-                    selectByMouse: true
-                    selectByKeyboard: true
-                    height: parent.height
-                    wrapMode: Text.Wrap
-                    verticalAlignment: Text.AlignVCenter
-                    font.italic: true
-                }
-
-                Text {
-                    text: date
-                    color:"white"
-                    font.pointSize: 12
-                    anchors.right: parent.right
-                    height: parent.height
-                    wrapMode: Text.Wrap
-                    horizontalAlignment: Text.AlignRight
-                    verticalAlignment: Text.AlignVCenter
-                    font.italic: true
-                }
-            }
-
-        }
-
-    }
+    TemplateChatMessage { id: template_chat_message }
+    TemplateNotification { id: template_notification }
 
     property var channels: {
         "local": local,
@@ -497,56 +431,32 @@ 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();
     }
 
-
     function addMessage(username, message, date, channel, type){
         channel = getChannel(channel)
 
         // Format content
-        message = formatContent(message);
-        message = embedImages(message);
-
         if (type === "notification"){
-            channel.append({ text: message, date: date, type: "notification" });
-            last_message_user = "";
+            channel.append({ message: message, date: date, type: "notification" });
             scrollToBottom(null, 30);
-
-            last_message_time = new Date();
             return;
         }
 
-        var current_time = new Date();
-        var elapsed_time = current_time - last_message_time;
-        var elapsed_minutes = elapsed_time / (1000 * 60); 
-
-        var last_item_index = channel.count - 1;
-        var last_item = channel.get(last_item_index);
-
-        if (last_message_user === username && elapsed_minutes < 1 && last_item){
-            message = "<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 });
+        channel.append({ message: message, username: username, date: date, type: type });
         load_scroll_timer.running = true;
     }
 
@@ -554,37 +464,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) {
 
@@ -600,15 +479,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/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/system/domainChat/formatting.js b/scripts/system/domainChat/formatting.js
new file mode 100644
index 0000000000..3cd0d9d335
--- /dev/null
+++ b/scripts/system/domainChat/formatting.js
@@ -0,0 +1,167 @@
+//
+//  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;
+    },
+    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
+
+        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.
+                if (runningMessage.trim() != "") 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.
+        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");
+
+                    if (contentType.startsWith('image/')) {
+                        messageArray.push({type: 'imageEmbed', value: url});
+                        continue;
+                    }
+                    if (contentType.startsWith('video/')){ 
+                        messageArray.push({type: 'videoEmbed', value: url});
+                        continue;
+                    }
+                }
+            }
+        }
+
+        return messageArray;
+
+        function _formatMessage(firstMatch){
+            let indexOfFirstMatch = firstMatch[0];
+            let regex = regexPatterns[firstMatch[1]].regex;
+
+            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
+        }
+
+        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.
+        _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();
+            });
+        }
+    }
+}
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
diff --git a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
new file mode 100644
index 0000000000..b97301ddf2
--- /dev/null
+++ b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
@@ -0,0 +1,187 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.3
+
+Component {
+    id: template_chat_message
+
+    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
+
+        Item {
+            width: parent.width - 10
+            anchors.horizontalCenter: parent.horizontalCenter
+            height: 22
+
+            TextEdit {
+                text: delegateUsername;
+                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;
+            x: 5
+            id: messageBoxFlow
+
+            Repeater {
+                model: delegateText;
+
+                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
+
+                        color: {
+                            switch (model.type) {
+                                case "mention":
+                                    return "purple";
+                                default:
+                                    return "white";
+                            }
+                        }
+                    }
+
+                    Flow {
+                        width: parent.width * 0.8;
+                        height: 20
+                        visible: model.type === 'url';
+
+                        TextEdit {
+                            id: urlTypeTextDisplay;
+                            text: model.value || "";
+                            font.pointSize: 12;
+                            wrapMode: Text.Wrap;
+                            color: "#4EBAFD";
+                            font.underline: true;
+                            readOnly: true
+                            selectByMouse: true
+                            selectByKeyboard: true
+                            width: Math.min(parent.width - 20, textMetrics.tightBoundingRect.width) ;
+
+                            MouseArea {
+                                anchors.fill: parent;
+
+                                onClicked: {
+                                    Window.openWebBrowser(model.value);
+                                }
+                            }
+                        }
+
+                        TextMetrics {
+                            id: textMetrics
+                            font: urlTypeTextDisplay.font
+                            text: urlTypeTextDisplay.text
+                        }
+
+                        Text {
+                            width: 20;
+                            text: "🗗";
+                            font.pointSize: 10;
+                            color: "white";
+                            horizontalAlignment: Text.AlignHCenter
+                            verticalAlignment: Text.AlignVCenter
+                            
+                            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
+                            }
+
+                            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;
+
+                                onClicked: {
+                                    Window.openUrl(model.value);
+                                }
+                            }
+                        }
+                    }
+
+                    Item {
+                        Layout.fillWidth: true;
+                        visible: model.type === 'messageEnd';
+                    }
+
+                    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
+                        }
+                    }
+
+
+                }
+            }
+        }
+    }
+}
\ 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..3c4fcdb240
--- /dev/null
+++ b/scripts/system/domainChat/qml_widgets/TemplateNotification.qml
@@ -0,0 +1,41 @@
+import QtQuick 2.7
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.3
+
+Component {
+    id: template_notification
+
+    Rectangle {
+        color: "#171717"
+        width: parent.width
+        height: 40
+
+        RowLayout {
+            width: parent.width
+            height: parent.height
+
+            Rectangle {
+                height: parent.height
+                width: 5
+                color: "#505186"
+            }
+
+            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
+                }
+            }
+        }
+    }
+}