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 57%
rename from scripts/communityScripts/armored-chat/armored_chat.js
rename to scripts/system/domainChat/domainChat.js
index 779dc3ff54..5a97c4dee9 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.
@@ -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();
@@ -61,7 +64,7 @@
appButton.clicked.connect(toggleMainChatWindow);
quickMessage = new OverlayWindow({
- source: Script.resolvePath("./armored_chat_quick_message.qml"),
+ source: Script.resolvePath("./domainChatQuick.qml"),
});
_openWindow();
@@ -78,7 +81,7 @@
}
function _openWindow() {
chatOverlayWindow = new Desktop.createWindow(
- Script.resolvePath("./armored_chat.qml"),
+ Script.resolvePath("./domainChat.qml"),
{
title: "Chat",
size: { x: 550, y: 400 },
@@ -92,64 +95,37 @@
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);
-
- // Get the message data
- 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.
- 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.
- 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 = 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)
+
+ _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.
+ let trimmedPacket = formatting.trimPacketToSave(message);
+ messageHistory.push(trimmedPacket);
- // 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;
}
}
@@ -160,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;
}
@@ -202,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;
@@ -215,11 +208,9 @@
action: "send_chat_message",
})
);
-
- floofChatCompatibilitySendMessage(message, channel);
}
function _avatarAction(type, sessionId) {
- Script.setTimeout(() => {
+ Script.setTimeout(async () => {
if (type == "connected") {
palData = AvatarManager.getPalData().data;
}
@@ -236,65 +227,47 @@
}
// 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);
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");
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",
+ JSON.stringify({ sender: displayName, text: message })
+ );
}
-
/**
* Emit a packet to the HTML front end. Easy communication!
* @param {Object} packet - The Object packet to emit to the HTML
@@ -304,33 +277,6 @@
chatOverlayWindow.sendToQml(packet);
}
- //
- // Floofchat compatibility functions
- // Added to ease the transition between Floofchat to ArmoredChat
- // These functions can be safely removed at a much later date.
- function floofChatCompatibilityConversion(message) {
- if (message.type === "TransmitChatMessage" && !message.forApp) {
- return {
- position: message.position,
- message: message.message,
- displayName: message.displayName,
- channel: message.channel.toLowerCase(),
- };
- }
- return message;
- }
- function floofChatCompatibilitySendMessage(message, channel) {
- Messages.sendMessage(
- "Chat",
- JSON.stringify({
- position: MyAvatar.position,
- message: message,
- displayName: MyAvatar.sessionDisplayName,
- channel: channel.charAt(0).toUpperCase() + channel.slice(1),
- type: "TransmitChatMessage",
- forApp: "Floof",
- })
- );
- }
+
})();
diff --git a/scripts/communityScripts/armored-chat/armored_chat.qml b/scripts/system/domainChat/domainChat.qml
similarity index 70%
rename from scripts/communityScripts/armored-chat/armored_chat.qml
rename to scripts/system/domainChat/domainChat.qml
index 07eb75c626..9ac69f71ac 100644
--- a/scripts/communityScripts/armored-chat/armored_chat.qml
+++ b/scripts/system/domainChat/domainChat.qml
@@ -2,13 +2,13 @@ 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()
// When the window is created on the script side, the window starts open.
@@ -162,18 +162,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;
}
-
}
}
}
@@ -373,140 +369,77 @@ Rectangle {
}
}
}
- }
- }
-
- }
-
- // 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
+ // Switch to internal on VR Mode
+ Rectangle {
+ width: parent.width
+ height: 40
+ color: "transparent"
- Text{
- text: username
- color: "lightgray"
- }
+ Text {
+ text: "Force Virtual window in VR"
+ color: "white"
+ font.pointSize: 12
+ anchors.verticalCenter: parent.verticalCenter
+ }
- Text{
- anchors.right: parent.right
- text: date
- color: "lightgray"
- }
- }
+ CheckBox {
+ id: s_force_vw_in_vr
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
- 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)
+ onCheckedChanged: {
+ toScript({type: 'setting_change', setting: 'switchToInternalOnHeadsetUsed', value: checked})
+ }
+ }
}
- }
- }
- }
-
- 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
-
+ // Toggle media embedding
Rectangle {
- height: parent.height
- width: 5
- color: "#505186"
- }
- }
+ width: parent.width
+ height: 40
+ color: "transparent"
+ Text {
+ text: "Enable media embedding"
+ color: "white"
+ font.pointSize: 12
+ anchors.verticalCenter: parent.verticalCenter
+ }
- 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
- }
+ CheckBox {
+ id: s_enable_embedding
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
- 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
+ onCheckedChanged: {
+ toScript({type: 'setting_change', setting: 'enableEmbedding', value: checked})
+ }
+ }
}
}
-
}
}
+ // Templates
+ TemplateChatMessage { id: template_chat_message }
+ TemplateNotification { id: template_notification }
+
property var channels: {
"local": local,
"domain": domain,
}
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();
@@ -517,36 +450,13 @@ Rectangle {
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 = "
" + 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 = /\ {return `` + match + ` 🗗`});
-
- var newline = /\n/gi;
- mess = mess.replace(newline, "
");
- return mess
- }
-
- function embedImages(mess){
- var image_link = /(https?:(\/){2})[\w.-]+(?:\.[\w\.-]+)+(?:\/[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]*)(?:png|jpe?g|gif|bmp|svg|webp)/g;
- var matches = mess.match(image_link);
- var new_message = ""
- var listed = []
- var total_emeds = 0
-
- new_message += mess
-
- for (var i = 0; matches && matches.length > i && total_emeds < 3; i++){
- if (!listed.includes(matches[i])) {
- new_message += "
"
- listed.push(matches[i]);
- total_emeds++
- }
- }
- return new_message;
- }
-
// Messages from script
function fromScript(message) {
@@ -603,6 +482,8 @@ Rectangle {
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/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..24662e4ff1
--- /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.
+ 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];
+
+ 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..9e33a0503e
--- /dev/null
+++ b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
@@ -0,0 +1,201 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.3
+import QtMultimedia 5.15
+
+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
+
+ Text {
+ text: delegateUsername
+ 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
+
+ AnimatedImage {
+ source: model.type === 'imageEmbed' ? model.value : ''
+ height: Math.min(sourceSize.height, 200);
+ 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();
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ 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..4b9797d7f4
--- /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
+ }
+ }
+ }
+ }
+}