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 + } + } + } + } +}