From b155e999212cd91e304208e3d8744a8a31a345d6 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Fri, 29 Nov 2024 23:53:27 -0600 Subject: [PATCH 01/23] Moved script. Changed name. --- scripts/defaultScripts.js | 2 +- .../armored-chat => system/domainChat}/README.md | 12 ++++++------ .../domainChat/domainChat.js} | 6 +++--- .../domainChat/domainChat.qml} | 0 .../domainChat/domainChatQuick.qml} | 0 .../domainChat}/img/icon_black.png | Bin .../domainChat}/img/icon_white.png | Bin .../domainChat}/img/ui/send.svg | 0 .../domainChat}/img/ui/send_black.png | Bin .../domainChat}/img/ui/send_white.png | Bin .../domainChat}/img/ui/settings_black.png | Bin .../domainChat}/img/ui/settings_white.png | Bin .../domainChat}/img/ui/social_black.png | Bin .../domainChat}/img/ui/social_white.png | Bin .../domainChat}/img/ui/world_black.png | Bin .../domainChat}/img/ui/world_white.png | Bin 16 files changed, 10 insertions(+), 10 deletions(-) rename scripts/{communityScripts/armored-chat => system/domainChat}/README.md (93%) rename scripts/{communityScripts/armored-chat/armored_chat.js => system/domainChat/domainChat.js} (98%) rename scripts/{communityScripts/armored-chat/armored_chat.qml => system/domainChat/domainChat.qml} (100%) rename scripts/{communityScripts/armored-chat/armored_chat_quick_message.qml => system/domainChat/domainChatQuick.qml} (100%) rename scripts/{communityScripts/armored-chat => system/domainChat}/img/icon_black.png (100%) rename scripts/{communityScripts/armored-chat => system/domainChat}/img/icon_white.png (100%) rename scripts/{communityScripts/armored-chat => system/domainChat}/img/ui/send.svg (100%) rename scripts/{communityScripts/armored-chat => system/domainChat}/img/ui/send_black.png (100%) rename scripts/{communityScripts/armored-chat => system/domainChat}/img/ui/send_white.png (100%) rename scripts/{communityScripts/armored-chat => system/domainChat}/img/ui/settings_black.png (100%) rename scripts/{communityScripts/armored-chat => system/domainChat}/img/ui/settings_white.png (100%) rename scripts/{communityScripts/armored-chat => system/domainChat}/img/ui/social_black.png (100%) rename scripts/{communityScripts/armored-chat => system/domainChat}/img/ui/social_white.png (100%) rename scripts/{communityScripts/armored-chat => system/domainChat}/img/ui/world_black.png (100%) rename scripts/{communityScripts/armored-chat => system/domainChat}/img/ui/world_white.png (100%) diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 31afd6e2db..d185daadb5 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -46,7 +46,7 @@ var DEFAULT_SCRIPTS_SEPARATE = [ "communityScripts/notificationCore/notificationCore.js", "simplifiedUI/ui/simplifiedNametag/simplifiedNametag.js", {"stable": "system/more/app-more.js", "beta": "https://more.overte.org/more/app-more.js"}, - "communityScripts/armored-chat/armored_chat.js", + "system/domainChat/domainChat.js", //"system/chat.js" ]; diff --git a/scripts/communityScripts/armored-chat/README.md b/scripts/system/domainChat/README.md similarity index 93% rename from scripts/communityScripts/armored-chat/README.md rename to scripts/system/domainChat/README.md index 2385494676..8ed2e8d911 100644 --- a/scripts/communityScripts/armored-chat/README.md +++ b/scripts/system/domainChat/README.md @@ -1,15 +1,15 @@ -# Armored Chat +# Domain Chat -1. What is Armored Chat +1. What is Domain Chat 2. User manual - Installation - Settings - Usability tips 3. Development -## What is Armored Chat +## What is Domain Chat -Armored Chat is a chat application strictly made to communicate between players in the same domain. It is made using QML and to be as light weight as reasonably possible. +Domain Chat is a chat application strictly made to communicate between players in the same domain. It is made using QML and to be as light weight as reasonably possible. ### Dependencies @@ -21,7 +21,7 @@ For notifications, AC uses [notificationCore.js](https://github.com/overte-org/o ### Installation -Armored Chat is preinstalled courtesy of [defaultScripts.js](https://github.com/overte-org/overte/blob/8661e8a858663b48e8485c2cd7120dc3e2d7b87e/scripts/defaultScripts.js). +Domain Chat is preinstalled courtesy of [defaultScripts.js](https://github.com/overte-org/overte/blob/8661e8a858663b48e8485c2cd7120dc3e2d7b87e/scripts/defaultScripts.js). If AC is not preinstalled, or for some other reason it can not be automatically installed, you can install it manually by following [these instructions](https://github.com/overte-org/overte/blob/8661e8a858663b48e8485c2cd7120dc3e2d7b87e/scripts/defaultScripts.js) to open your script management application, and loading the script url: @@ -33,7 +33,7 @@ https://raw.githubusercontent.com/overte-org/overte/master/scripts/communityScri ### Settings -Armored Chat comes with basic settings for managing itself. +Domain Chat comes with basic settings for managing itself. #### External window diff --git a/scripts/communityScripts/armored-chat/armored_chat.js b/scripts/system/domainChat/domainChat.js similarity index 98% rename from scripts/communityScripts/armored-chat/armored_chat.js rename to scripts/system/domainChat/domainChat.js index 779dc3ff54..a7836859ce 100644 --- a/scripts/communityScripts/armored-chat/armored_chat.js +++ b/scripts/system/domainChat/domainChat.js @@ -1,5 +1,5 @@ // -// armored_chat.js +// domainChat.js // // Created by Armored Dragon, 2024. // Copyright 2024 Overte e.V. @@ -61,7 +61,7 @@ appButton.clicked.connect(toggleMainChatWindow); quickMessage = new OverlayWindow({ - source: Script.resolvePath("./armored_chat_quick_message.qml"), + source: Script.resolvePath("./domainChatQuick.qml"), }); _openWindow(); @@ -78,7 +78,7 @@ } function _openWindow() { chatOverlayWindow = new Desktop.createWindow( - Script.resolvePath("./armored_chat.qml"), + Script.resolvePath("./domainChat.qml"), { title: "Chat", size: { x: 550, y: 400 }, diff --git a/scripts/communityScripts/armored-chat/armored_chat.qml b/scripts/system/domainChat/domainChat.qml similarity index 100% rename from scripts/communityScripts/armored-chat/armored_chat.qml rename to scripts/system/domainChat/domainChat.qml diff --git a/scripts/communityScripts/armored-chat/armored_chat_quick_message.qml b/scripts/system/domainChat/domainChatQuick.qml similarity index 100% rename from scripts/communityScripts/armored-chat/armored_chat_quick_message.qml rename to scripts/system/domainChat/domainChatQuick.qml diff --git a/scripts/communityScripts/armored-chat/img/icon_black.png b/scripts/system/domainChat/img/icon_black.png similarity index 100% rename from scripts/communityScripts/armored-chat/img/icon_black.png rename to scripts/system/domainChat/img/icon_black.png diff --git a/scripts/communityScripts/armored-chat/img/icon_white.png b/scripts/system/domainChat/img/icon_white.png similarity index 100% rename from scripts/communityScripts/armored-chat/img/icon_white.png rename to scripts/system/domainChat/img/icon_white.png diff --git a/scripts/communityScripts/armored-chat/img/ui/send.svg b/scripts/system/domainChat/img/ui/send.svg similarity index 100% rename from scripts/communityScripts/armored-chat/img/ui/send.svg rename to scripts/system/domainChat/img/ui/send.svg diff --git a/scripts/communityScripts/armored-chat/img/ui/send_black.png b/scripts/system/domainChat/img/ui/send_black.png similarity index 100% rename from scripts/communityScripts/armored-chat/img/ui/send_black.png rename to scripts/system/domainChat/img/ui/send_black.png diff --git a/scripts/communityScripts/armored-chat/img/ui/send_white.png b/scripts/system/domainChat/img/ui/send_white.png similarity index 100% rename from scripts/communityScripts/armored-chat/img/ui/send_white.png rename to scripts/system/domainChat/img/ui/send_white.png diff --git a/scripts/communityScripts/armored-chat/img/ui/settings_black.png b/scripts/system/domainChat/img/ui/settings_black.png similarity index 100% rename from scripts/communityScripts/armored-chat/img/ui/settings_black.png rename to scripts/system/domainChat/img/ui/settings_black.png diff --git a/scripts/communityScripts/armored-chat/img/ui/settings_white.png b/scripts/system/domainChat/img/ui/settings_white.png similarity index 100% rename from scripts/communityScripts/armored-chat/img/ui/settings_white.png rename to scripts/system/domainChat/img/ui/settings_white.png diff --git a/scripts/communityScripts/armored-chat/img/ui/social_black.png b/scripts/system/domainChat/img/ui/social_black.png similarity index 100% rename from scripts/communityScripts/armored-chat/img/ui/social_black.png rename to scripts/system/domainChat/img/ui/social_black.png diff --git a/scripts/communityScripts/armored-chat/img/ui/social_white.png b/scripts/system/domainChat/img/ui/social_white.png similarity index 100% rename from scripts/communityScripts/armored-chat/img/ui/social_white.png rename to scripts/system/domainChat/img/ui/social_white.png diff --git a/scripts/communityScripts/armored-chat/img/ui/world_black.png b/scripts/system/domainChat/img/ui/world_black.png similarity index 100% rename from scripts/communityScripts/armored-chat/img/ui/world_black.png rename to scripts/system/domainChat/img/ui/world_black.png diff --git a/scripts/communityScripts/armored-chat/img/ui/world_white.png b/scripts/system/domainChat/img/ui/world_white.png similarity index 100% rename from scripts/communityScripts/armored-chat/img/ui/world_white.png rename to scripts/system/domainChat/img/ui/world_white.png From 1d47275b410e73fadf4e6489891a3d6a6938f7e2 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Sat, 30 Nov 2024 00:32:20 -0600 Subject: [PATCH 02/23] Removed floofchat compatibility. --- scripts/system/domainChat/domainChat.js | 36 ------------------------- 1 file changed, 36 deletions(-) diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js index a7836859ce..b00a424499 100644 --- a/scripts/system/domainChat/domainChat.js +++ b/scripts/system/domainChat/domainChat.js @@ -28,7 +28,6 @@ var palData = AvatarManager.getPalData().data; Controller.keyPressEvent.connect(keyPressEvent); - Messages.subscribe("Chat"); // Floofchat Messages.subscribe("chat"); Messages.messageReceived.connect(receivedMessage); AvatarManager.avatarAddedEvent.connect((sessionId) => { @@ -103,10 +102,7 @@ const timeArray = _formatTimestamp(currentTimestamp); if (!message.channel) message.channel = "domain"; // We don't know where to put this message. Assume it is a domain wide message. - if (message.forApp) return; // Floofchat - // Floofchat compatibility hook - message = floofChatCompatibilityConversion(message); message.channel = message.channel.toLowerCase(); // Check the channel. If the channel is not one we have, do nothing. @@ -215,8 +211,6 @@ action: "send_chat_message", }) ); - - floofChatCompatibilitySendMessage(message, channel); } function _avatarAction(type, sessionId) { Script.setTimeout(() => { @@ -303,34 +297,4 @@ function _emitEvent(packet = { type: "" }) { chatOverlayWindow.sendToQml(packet); } - - // - // Floofchat compatibility functions - // Added to ease the transition between Floofchat to ArmoredChat - // These functions can be safely removed at a much later date. - function floofChatCompatibilityConversion(message) { - if (message.type === "TransmitChatMessage" && !message.forApp) { - return { - position: message.position, - message: message.message, - displayName: message.displayName, - channel: message.channel.toLowerCase(), - }; - } - return message; - } - - function floofChatCompatibilitySendMessage(message, channel) { - Messages.sendMessage( - "Chat", - JSON.stringify({ - position: MyAvatar.position, - message: message, - displayName: MyAvatar.sessionDisplayName, - channel: channel.charAt(0).toUpperCase() + channel.slice(1), - type: "TransmitChatMessage", - forApp: "Floof", - }) - ); - } })(); From 777912999eba1b903dd465212f4a0553747bd119 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Sat, 30 Nov 2024 01:23:49 -0600 Subject: [PATCH 03/23] Notification organization. --- scripts/system/domainChat/domainChat.js | 45 +++++++++---------------- 1 file changed, 16 insertions(+), 29 deletions(-) diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js index b00a424499..c70a8f925f 100644 --- a/scripts/system/domainChat/domainChat.js +++ b/scripts/system/domainChat/domainChat.js @@ -101,33 +101,20 @@ const currentTimestamp = _getTimestamp(); const timeArray = _formatTimestamp(currentTimestamp); - if (!message.channel) message.channel = "domain"; // We don't know where to put this message. Assume it is a domain wide message. - - message.channel = message.channel.toLowerCase(); - - // Check the channel. If the channel is not one we have, do nothing. - if (!channels.includes(message.channel)) return; - - // If message is local, and if player is too far away from location, do nothing. - if (message.channel == "local" && isTooFar(message.position)) return; + if (!message.channel) message.channel = "domain"; // We don't know where to put this message. Assume it is a domain wide message. + message.channel = message.channel.toLowerCase(); // Only recognize channel names as lower case. + + if (!channels.includes(message.channel)) return; // Check the channel. If the channel is not one we have, do nothing. + if (message.channel == "local" && isTooFar(message.position)) return; // If message is local, and if player is too far away from location, do nothing. // Format the timestamp message.timeString = timeArray[0]; message.dateString = timeArray[1]; + + _emitEvent({ type: "show_message", ...message }); // Update qml view of to new message. + _notificationCoreMessage(message.displayName, message.message) // Show a new message on screen. - // Update qml view of to new message - _emitEvent({ type: "show_message", ...message }); - - // Show new message on screen - Messages.sendLocalMessage( - "Floof-Notif", - JSON.stringify({ - sender: message.displayName, - text: message.message, - }) - ); - - // Save message to history + // Create a new variable based on the message that will be saved. let savedMessage = message; // Remove unnecessary data. @@ -238,13 +225,7 @@ // Show new message on screen if (settings.join_notification){ - Messages.sendLocalMessage( - "Floof-Notif", - JSON.stringify({ - sender: displayName, - text: type, - }) - ); + _notificationCoreMessage(displayName, type) } _emitEvent({ type: "notification", ...message }); @@ -288,6 +269,12 @@ return timeArray; } + function _notificationCoreMessage(displayName, message){ + Messages.sendLocalMessage( + "Floof-Notif", + JSON.stringify({ sender: displayName, text: message }) + ); + } /** * Emit a packet to the HTML front end. Easy communication! From 54b4aa70b8b3dbdc6abc54a2772fa987d779f0b8 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Sat, 30 Nov 2024 07:22:28 -0600 Subject: [PATCH 04/23] Message formatting. --- scripts/system/domainChat/domainChat.js | 86 ++++++++++++- scripts/system/domainChat/domainChat.qml | 157 ++++++++++++++--------- 2 files changed, 174 insertions(+), 69 deletions(-) diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js index c70a8f925f..4e62afd061 100644 --- a/scripts/system/domainChat/domainChat.js +++ b/scripts/system/domainChat/domainChat.js @@ -7,6 +7,8 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// TODO: Message trimming + (() => { ("use strict"); @@ -110,8 +112,12 @@ // Format the timestamp message.timeString = timeArray[0]; message.dateString = timeArray[1]; - - _emitEvent({ type: "show_message", ...message }); // Update qml view of to new message. + + let formattedMessage = _parseMessage(message.message); // Format the message for viewing + let formattedMessagePacket = { ...message }; + formattedMessagePacket.message = formattedMessage + + _emitEvent({ type: "show_message", ...formattedMessagePacket }); // Update qml view of to new message. _notificationCoreMessage(message.displayName, message.message) // Show a new message on screen. // Create a new variable based on the message that will be saved. @@ -238,9 +244,15 @@ // Load message history messageHistory.forEach((message) => { const timeArray = _formatTimestamp(_getTimestamp()); - message.timeString = timeArray[0]; - message.dateString = timeArray[1]; - _emitEvent({ type: "show_message", ...message }); + messagePacket = { ...message }; + messagePacket.timeString = timeArray[0]; + messagePacket.dateString = timeArray[1]; + + let formattedMessage = _parseMessage(messagePacket.message); + let formattedMessagePacket = messagePacket; + formattedMessagePacket.message = formattedMessage; + + _emitEvent({ type: "show_message", ...formattedMessagePacket }); }); } @@ -275,6 +287,70 @@ JSON.stringify({ sender: displayName, text: message }) ); } + function _parseMessage(message){ + const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/; + const mentionRegex = /@(\w+)/; // FIXME: Remove - devcode + const overteLocationRegex = null; + + let runningMessage = message; + let messageArray = []; + + const regexPatterns = [ + { type: "url", regex: urlRegex }, + { type: "mention", regex: mentionRegex }, // FIXME: Remove - devcode + { type: "overteLocation", regex: overteLocationRegex } + ] + + // Here is a link https://www.example.com, #hashtag, and @mention. Just for some spice here is another https://exampletwo.com + + while (true) { + let firstMatch = _findFirstMatch(); + + if (firstMatch == null) { + // If there was not any matches found in the entire message, format the whole message as a single text entry. + messageArray.push({type: 'text', value: runningMessage}); + + // Append a final 'fill width' to the message text. + messageArray.push({type: 'messageEnd'}); + break; + } + + _formatMessage(firstMatch); + } + + return messageArray; + + function _formatMessage(firstMatch){ + let indexOfFirstMatch = firstMatch[0]; + let regex = regexPatterns[firstMatch[1]].regex; + + let foundMatch = runningMessage.match(regex)[0]; + + messageArray.push({type: 'text', value: runningMessage.substring(0, indexOfFirstMatch)}); + messageArray.push({type: regexPatterns[firstMatch[1]].type, value: runningMessage.substring(indexOfFirstMatch, indexOfFirstMatch + foundMatch.length)}); + + runningMessage = runningMessage.substring(indexOfFirstMatch + foundMatch.length); // Remove the part of the message we have worked with + } + + function _findFirstMatch(){ + let indexOfFirstMatch = Infinity; + let indexOfRegexPattern = Infinity; + + for (let i = 0; regexPatterns.length > i; i++){ + let indexOfMatch = runningMessage.search(regexPatterns[i].regex); + + if (indexOfMatch == -1) continue; // No match found + + if (indexOfMatch < indexOfFirstMatch) { + indexOfFirstMatch = indexOfMatch; + indexOfRegexPattern = i; + } + } + + if (indexOfFirstMatch !== Infinity) return [indexOfFirstMatch, indexOfRegexPattern]; // If there was a found match + return null; // No found match + } + } /** * Emit a packet to the HTML front end. Easy communication! diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml index 07eb75c626..405e5d5ea9 100644 --- a/scripts/system/domainChat/domainChat.qml +++ b/scripts/system/domainChat/domainChat.qml @@ -162,7 +162,7 @@ Rectangle { model: getChannel(pageVal) delegate: Loader { property int delegateIndex: model.index - property string delegateText: model.text + property var delegateText: model.text property string delegateUsername: model.username property string delegateDate: model.date @@ -384,7 +384,7 @@ Rectangle { Rectangle { property int index: delegateIndex - property string texttest: delegateText + property var texttest: delegateText property string username: delegateUsername property string date: delegateDate @@ -410,22 +410,83 @@ Rectangle { } } - TextEdit { - anchors.top: parent.children[0].bottom - x: 5 - text: texttest - color:"white" - font.pointSize: 12 - readOnly: true - selectByMouse: true - selectByKeyboard: true + Flow { + anchors.top: parent.children[0].bottom; width: parent.width * 0.8 - height: contentHeight - wrapMode: Text.Wrap - textFormat: TextEdit.RichText + x: 5 + + Repeater { + model: texttest; + + RowLayout { + width: { + switch (model.type) { + case "text": + return children[0].width; + case "url": + return children[1].width; + } + } + + Text { + text: model.value || "" + font.pointSize: 12 + wrapMode: Text.Wrap + width: Math.min(parent.parent.parent.width, contentWidth); + + visible: model.type === 'text' || model.type === 'mention'; + + color: { + switch (model.type) { + case "mention": + return "purple"; + default: + return "white"; + } + } + } + + RowLayout { + width: Math.min(parent.parent.parent.width, children[0].contentWidth); + visible: model.type === 'url'; + + Text { + text: model.value || "" + font.pointSize: 12 + wrapMode: Text.Wrap + color: "#4EBAFD"; + font.underline: true + + MouseArea { + anchors.fill: parent; + + onClicked: { + Window.openWebBrowser(model.value) + } + } + } - onLinkActivated: { - Window.openWebBrowser(link) + Text { + text: "🗗" + font.pointSize: 10 + wrapMode: Text.Wrap + color: "white" + + MouseArea { + anchors.fill: parent; + + onClicked: { + Qt.openUrlExternally(model.value) + } + } + } + } + + Item { + Layout.fillWidth: true + visible: model.type === 'messageEnd' + } + } } } } @@ -436,7 +497,7 @@ Rectangle { Rectangle{ property int index: delegateIndex - property string texttest: delegateText + property var texttest: delegateText property string username: delegateUsername property string date: delegateDate color: "#171717" @@ -517,8 +578,6 @@ Rectangle { channel = getChannel(channel) // Format content - message = formatContent(message); - message = embedImages(message); if (type === "notification"){ channel.append({ text: message, date: date, type: "notification" }); @@ -529,23 +588,24 @@ Rectangle { return; } - var current_time = new Date(); - var elapsed_time = current_time - last_message_time; - var elapsed_minutes = elapsed_time / (1000 * 60); + // TODO: Replace new time generation with time pregenerated from message + // var current_time = new Date(); + // var elapsed_time = current_time - last_message_time; + // var elapsed_minutes = elapsed_time / (1000 * 60); - var last_item_index = channel.count - 1; - var last_item = channel.get(last_item_index); + // var last_item_index = channel.count - 1; + // var last_item = channel.get(last_item_index); - if (last_message_user === username && elapsed_minutes < 1 && last_item){ - message = "
" + message - last_item.text = last_item.text += "\n" + message; - load_scroll_timer.running = true; - last_message_time = new Date(); - return; - } + // if (last_message_user === username && elapsed_minutes < 1 && last_item){ + // message = "
" + message + // last_item.text = last_item.text += "\n" + message; + // load_scroll_timer.running = true; + // last_message_time = new Date(); + // return; + // } - last_message_user = username; - last_message_time = new Date(); + // last_message_user = username; + // last_message_time = new Date(); channel.append({ text: message, username: username, date: date, type: type }); load_scroll_timer.running = true; } @@ -554,37 +614,6 @@ Rectangle { return channels[id]; } - function formatContent(mess) { - var arrow = /\ {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) { From 9174ffa4d195bf59da5ad70408966764f2db5d8f Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Sat, 30 Nov 2024 09:29:08 -0600 Subject: [PATCH 05/23] World links. --- scripts/system/domainChat/domainChat.js | 4 +- scripts/system/domainChat/domainChat.qml | 71 ++++++++++++++++++++---- 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js index 4e62afd061..6d2809f782 100644 --- a/scripts/system/domainChat/domainChat.js +++ b/scripts/system/domainChat/domainChat.js @@ -117,7 +117,7 @@ let formattedMessagePacket = { ...message }; formattedMessagePacket.message = formattedMessage - _emitEvent({ type: "show_message", ...formattedMessagePacket }); // Update qml view of to new message. + _emitEvent({ type: "show_message", ...formattedMessagePacket }); // Update qml view of to new message. _notificationCoreMessage(message.displayName, message.message) // Show a new message on screen. // Create a new variable based on the message that will be saved. @@ -290,7 +290,7 @@ function _parseMessage(message){ const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/; const mentionRegex = /@(\w+)/; // FIXME: Remove - devcode - const overteLocationRegex = null; + const overteLocationRegex = /hifi:\/\/[a-zA-Z0-9_-]+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+/; let runningMessage = message; let messageArray = []; diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml index 405e5d5ea9..46340d233a 100644 --- a/scripts/system/domainChat/domainChat.qml +++ b/scripts/system/domainChat/domainChat.qml @@ -414,6 +414,7 @@ Rectangle { anchors.top: parent.children[0].bottom; width: parent.width * 0.8 x: 5 + id: messageBoxFlow Repeater { model: texttest; @@ -432,7 +433,7 @@ Rectangle { text: model.value || "" font.pointSize: 12 wrapMode: Text.Wrap - width: Math.min(parent.parent.parent.width, contentWidth); + width: Math.min(messageBoxFlow.width, contentWidth); visible: model.type === 'text' || model.type === 'mention'; @@ -447,41 +448,87 @@ Rectangle { } RowLayout { - width: Math.min(parent.parent.parent.width, children[0].contentWidth); + width: Math.min(messageBoxFlow.width, children[0].contentWidth); visible: model.type === 'url'; Text { - text: model.value || "" - font.pointSize: 12 - wrapMode: Text.Wrap + text: model.value || ""; + font.pointSize: 12; + wrapMode: Text.Wrap; color: "#4EBAFD"; - font.underline: true + font.underline: true; + width: parent.width; MouseArea { anchors.fill: parent; onClicked: { - Window.openWebBrowser(model.value) + Window.openWebBrowser(model.value); } } } Text { - text: "🗗" - font.pointSize: 10 - wrapMode: Text.Wrap - color: "white" + text: "🗗"; + font.pointSize: 10; + wrapMode: Text.Wrap; + color: "white"; MouseArea { anchors.fill: parent; onClicked: { - Qt.openUrlExternally(model.value) + Qt.openUrlExternally(model.value); } } } } + RowLayout { + visible: model.type === 'overteLocation'; + width: Math.min(messageBoxFlow.width, children[0].children[1].contentWidth + 35); + height: 20; + Layout.leftMargin: 5 + Layout.rightMargin: 5 + + Rectangle { + width: parent.width; + height: 20; + color: "lightgray" + radius: 2; + + Image { + source: "./img/ui/world_black.png" + width: 18; + height: 18; + sourceSize.width: 18 + sourceSize.height: 18 + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 2 + anchors.rightMargin: 10 + } + + Text { + text: model.value.split('hifi://')[1].split('/')[0]; + color: "black" + font.pointSize: 12 + x: parent.children[0].width + 5; + anchors.verticalCenter: parent.verticalCenter + } + + MouseArea { + anchors.fill: parent; + + onClicked: { + Window.openUrl(model.value); + } + } + + } + } + + Item { Layout.fillWidth: true visible: model.type === 'messageEnd' From e470cb23b44ff45d9c713b7c0303b6d4c64b36f9 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Tue, 17 Dec 2024 18:13:49 -0600 Subject: [PATCH 06/23] Message media embedding. --- scripts/system/domainChat/domainChat.js | 60 +++++++++++++++++++----- scripts/system/domainChat/domainChat.qml | 15 +++++- 2 files changed, 63 insertions(+), 12 deletions(-) diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js index 6d2809f782..34cd128bce 100644 --- a/scripts/system/domainChat/domainChat.js +++ b/scripts/system/domainChat/domainChat.js @@ -93,7 +93,7 @@ chatOverlayWindow.fromQml.connect(fromQML); quickMessage.fromQml.connect(fromQML); } - function receivedMessage(channel, message) { + async function receivedMessage(channel, message) { // Is the message a chat message? channel = channel.toLowerCase(); if (channel !== "chat") return; @@ -113,9 +113,8 @@ message.timeString = timeArray[0]; message.dateString = timeArray[1]; - let formattedMessage = _parseMessage(message.message); // Format the message for viewing let formattedMessagePacket = { ...message }; - formattedMessagePacket.message = formattedMessage + formattedMessagePacket.message = await _parseMessage(message.message) _emitEvent({ type: "show_message", ...formattedMessagePacket }); // Update qml view of to new message. _notificationCoreMessage(message.displayName, message.message) // Show a new message on screen. @@ -237,23 +236,22 @@ _emitEvent({ type: "notification", ...message }); }, 1500); } - function _loadSettings() { + async function _loadSettings() { settings = Settings.getValue("ArmoredChat-Config", settings); if (messageHistory) { // Load message history - messageHistory.forEach((message) => { + for (message of messageHistory) { const timeArray = _formatTimestamp(_getTimestamp()); messagePacket = { ...message }; messagePacket.timeString = timeArray[0]; messagePacket.dateString = timeArray[1]; - let formattedMessage = _parseMessage(messagePacket.message); - let formattedMessagePacket = messagePacket; - formattedMessagePacket.message = formattedMessage; + let formattedMessagePacket = {...messagePacket}; + formattedMessagePacket.message = await _parseMessage(messagePacket.message); _emitEvent({ type: "show_message", ...formattedMessagePacket }); - }); + } } // Send current settings to the app @@ -287,7 +285,7 @@ JSON.stringify({ sender: displayName, text: message }) ); } - function _parseMessage(message){ + async function _parseMessage(message){ const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/; const mentionRegex = /@(\w+)/; // FIXME: Remove - devcode const overteLocationRegex = /hifi:\/\/[a-zA-Z0-9_-]+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+/; @@ -308,7 +306,9 @@ if (firstMatch == null) { // If there was not any matches found in the entire message, format the whole message as a single text entry. - messageArray.push({type: 'text', value: runningMessage}); + if (messageArray.length == 0) { + messageArray.push({type: 'text', value: runningMessage}); + } // Append a final 'fill width' to the message text. messageArray.push({type: 'messageEnd'}); @@ -318,6 +318,20 @@ _formatMessage(firstMatch); } + for (dataChunk of messageArray){ + if (dataChunk.type == 'url'){ + let url = dataChunk.value; + + const res = await fetch(url, {method: 'GET'}); // TODO: Replace with 'HEAD' method. https://github.com/overte-org/overte/issues/1273 + const contentType = res.getResponseHeader("content-type"); + + // TODO: Add support for other media types + if (contentType.startsWith('image/')) { + messageArray.push({type: 'imageEmbed', value: url}); + } + } + } + return messageArray; function _formatMessage(firstMatch){ @@ -360,4 +374,28 @@ function _emitEvent(packet = { type: "" }) { chatOverlayWindow.sendToQml(packet); } + + function fetch(url, options = {method: "GET"}) { + return new Promise((resolve, reject) => { + let req = new XMLHttpRequest(); + + req.onreadystatechange = function () { + + if (req.readyState === req.DONE) { + if (req.status === 200) { + console.log("Content type:", req.getResponseHeader("content-type")); + resolve(req); + + } else { + console.log("Error", req.status, req.statusText); + reject(); + } + } + }; + + req.open(options.method, url); + req.send(); + }); + } + })(); diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml index 46340d233a..f0350e972a 100644 --- a/scripts/system/domainChat/domainChat.qml +++ b/scripts/system/domainChat/domainChat.qml @@ -510,7 +510,7 @@ Rectangle { } Text { - text: model.value.split('hifi://')[1].split('/')[0]; + text: model.type === 'overteLocation' ? model.value.split('hifi://')[1].split('/')[0] : ''; color: "black" font.pointSize: 12 x: parent.children[0].width + 5; @@ -533,6 +533,18 @@ Rectangle { Layout.fillWidth: true visible: model.type === 'messageEnd' } + + Item { + visible: model.type === 'imageEmbed'; + width: messageBoxFlow.width; + height: 200 + + Image { + source: model.type === 'imageEmbed' ? model.value : '' + sourceSize.width: 400 + sourceSize.height: 200 + } + } } } } @@ -653,6 +665,7 @@ Rectangle { // last_message_user = username; // last_message_time = new Date(); + channel.append({ text: message, username: username, date: date, type: type }); load_scroll_timer.running = true; } From 353f7ec30d8de5e03582ff2d0a2ac3dafeeaa34d Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Wed, 18 Dec 2024 05:04:23 -0600 Subject: [PATCH 07/23] Fix domain notifications. --- scripts/system/domainChat/domainChat.qml | 76 ++++++------------------ 1 file changed, 18 insertions(+), 58 deletions(-) diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml index f0350e972a..2220127f98 100644 --- a/scripts/system/domainChat/domainChat.qml +++ b/scripts/system/domainChat/domainChat.qml @@ -167,11 +167,8 @@ Rectangle { property string delegateDate: model.date sourceComponent: { - if (model.type === "chat") { - return template_chat_message; - } else if (model.type === "notification") { - return template_notification; - } + if (model.type === "chat") return template_chat_message; + if (model.type === "notification") return template_notification; } } @@ -384,9 +381,7 @@ Rectangle { Rectangle { property int index: delegateIndex - property var texttest: delegateText property string username: delegateUsername - property string date: delegateDate height: Math.max(65, children[1].height + 30) color: index % 2 === 0 ? "transparent" : Qt.rgba(0.15,0.15,0.15,1) @@ -405,7 +400,7 @@ Rectangle { Text{ anchors.right: parent.right - text: date + text: delegateDate color: "lightgray" } } @@ -417,24 +412,14 @@ Rectangle { id: messageBoxFlow Repeater { - model: texttest; + model: delegateText; RowLayout { - width: { - switch (model.type) { - case "text": - return children[0].width; - case "url": - return children[1].width; - } - } - Text { text: model.value || "" font.pointSize: 12 wrapMode: Text.Wrap - width: Math.min(messageBoxFlow.width, contentWidth); - + width: model.type === 'text' || model.type === 'mention' ? Math.min(messageBoxFlow.width, contentWidth) : 0; visible: model.type === 'text' || model.type === 'mention'; color: { @@ -448,7 +433,7 @@ Rectangle { } RowLayout { - width: Math.min(messageBoxFlow.width, children[0].contentWidth); + width: children[0].contentWidth; visible: model.type === 'url'; Text { @@ -524,11 +509,9 @@ Rectangle { Window.openUrl(model.value); } } - } } - Item { Layout.fillWidth: true visible: model.type === 'messageEnd' @@ -554,11 +537,10 @@ Rectangle { Component { id: template_notification - Rectangle{ + Rectangle { property int index: delegateIndex - property var texttest: delegateText property string username: delegateUsername - property string date: delegateDate + color: "#171717" width: parent.width height: 40 @@ -574,15 +556,14 @@ Rectangle { } } - Item { width: parent.width - parent.children[0].width - 5 height: parent.height anchors.left: parent.children[0].right - TextEdit{ - text: texttest - color:"white" + TextEdit { + text: delegateText + color: "white" font.pointSize: 12 readOnly: true width: parent.width * 0.8 @@ -595,8 +576,8 @@ Rectangle { } Text { - text: date - color:"white" + text: delegateDate + color: "white" font.pointSize: 12 anchors.right: parent.right height: parent.height @@ -617,16 +598,16 @@ Rectangle { } function scrollToBottom(bypassDistanceCheck = false, extraMoveDistance = 0) { - const totalHeight = listview.height; // Total height of the content - const currentPosition = messageViewFlickable.contentY; // Current position of the view - const windowHeight = listview.parent.parent.height; // Total height of the window + const totalHeight = listview.height; // Total height of the content + const currentPosition = messageViewFlickable.contentY; // Current position of the view + const windowHeight = listview.parent.parent.height; // Total height of the window const bottomPosition = currentPosition + windowHeight; // Check if the view is within 300 units from the bottom const closeEnoughToBottom = totalHeight - bottomPosition <= 300; if (!bypassDistanceCheck && !closeEnoughToBottom) return; - if (totalHeight < windowHeight) return; // No reason to scroll, we don't have an overflow. - if (bottomPosition == totalHeight) return; // At the bottom, do nothing. + if (totalHeight < windowHeight) return; // No reason to scroll, we don't have an overflow. + if (bottomPosition == totalHeight) return; // At the bottom, do nothing. messageViewFlickable.contentY = listview.height - listview.parent.parent.height; messageViewFlickable.returnToBounds(); @@ -640,32 +621,11 @@ Rectangle { if (type === "notification"){ channel.append({ text: message, date: date, type: "notification" }); - last_message_user = ""; scrollToBottom(null, 30); - last_message_time = new Date(); return; } - // TODO: Replace new time generation with time pregenerated from message - // var current_time = new Date(); - // var elapsed_time = current_time - last_message_time; - // var elapsed_minutes = elapsed_time / (1000 * 60); - - // var last_item_index = channel.count - 1; - // var last_item = channel.get(last_item_index); - - // if (last_message_user === username && elapsed_minutes < 1 && last_item){ - // message = "
" + message - // last_item.text = last_item.text += "\n" + message; - // load_scroll_timer.running = true; - // last_message_time = new Date(); - // return; - // } - - // last_message_user = username; - // last_message_time = new Date(); - channel.append({ text: message, username: username, date: date, type: type }); load_scroll_timer.running = true; } From 392a1446a9faa26042445c6ab54262eb2ff13c02 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Wed, 18 Dec 2024 17:38:52 -0600 Subject: [PATCH 08/23] Broke out templates into their own files. --- scripts/system/domainChat/domainChat.qml | 218 +----------------- .../qml_widgets/TemplateChatMessage.qml | 161 +++++++++++++ .../qml_widgets/TemplateNotification.qml | 59 +++++ 3 files changed, 223 insertions(+), 215 deletions(-) create mode 100644 scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml create mode 100644 scripts/system/domainChat/qml_widgets/TemplateNotification.qml diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml index 2220127f98..f40121ee71 100644 --- a/scripts/system/domainChat/domainChat.qml +++ b/scripts/system/domainChat/domainChat.qml @@ -2,6 +2,7 @@ import QtQuick 2.7 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.3 import controlsUit 1.0 as HifiControlsUit +import "./qml_widgets" Rectangle { color: Qt.rgba(0.1,0.1,0.1,1) @@ -376,221 +377,8 @@ Rectangle { } // Templates - Component { - id: template_chat_message - - Rectangle { - property int index: delegateIndex - property string username: delegateUsername - - height: Math.max(65, children[1].height + 30) - color: index % 2 === 0 ? "transparent" : Qt.rgba(0.15,0.15,0.15,1) - width: listview.parent.parent.width - Layout.fillWidth: true - - Item { - width: parent.width - 10 - anchors.horizontalCenter: parent.horizontalCenter - height: 22 - - Text{ - text: username - color: "lightgray" - } - - Text{ - anchors.right: parent.right - text: delegateDate - color: "lightgray" - } - } - - Flow { - anchors.top: parent.children[0].bottom; - width: parent.width * 0.8 - x: 5 - id: messageBoxFlow - - Repeater { - model: delegateText; - - RowLayout { - Text { - text: model.value || "" - font.pointSize: 12 - wrapMode: Text.Wrap - width: model.type === 'text' || model.type === 'mention' ? Math.min(messageBoxFlow.width, contentWidth) : 0; - visible: model.type === 'text' || model.type === 'mention'; - - color: { - switch (model.type) { - case "mention": - return "purple"; - default: - return "white"; - } - } - } - - RowLayout { - width: children[0].contentWidth; - visible: model.type === 'url'; - - Text { - text: model.value || ""; - font.pointSize: 12; - wrapMode: Text.Wrap; - color: "#4EBAFD"; - font.underline: true; - width: parent.width; - - MouseArea { - anchors.fill: parent; - - onClicked: { - Window.openWebBrowser(model.value); - } - } - } - - Text { - text: "🗗"; - font.pointSize: 10; - wrapMode: Text.Wrap; - color: "white"; - - MouseArea { - anchors.fill: parent; - - onClicked: { - Qt.openUrlExternally(model.value); - } - } - } - } - - RowLayout { - visible: model.type === 'overteLocation'; - width: Math.min(messageBoxFlow.width, children[0].children[1].contentWidth + 35); - height: 20; - Layout.leftMargin: 5 - Layout.rightMargin: 5 - - Rectangle { - width: parent.width; - height: 20; - color: "lightgray" - radius: 2; - - Image { - source: "./img/ui/world_black.png" - width: 18; - height: 18; - sourceSize.width: 18 - sourceSize.height: 18 - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 2 - anchors.rightMargin: 10 - } - - Text { - text: model.type === 'overteLocation' ? model.value.split('hifi://')[1].split('/')[0] : ''; - color: "black" - font.pointSize: 12 - x: parent.children[0].width + 5; - anchors.verticalCenter: parent.verticalCenter - } - - MouseArea { - anchors.fill: parent; - - onClicked: { - Window.openUrl(model.value); - } - } - } - } - - Item { - Layout.fillWidth: true - visible: model.type === 'messageEnd' - } - - Item { - visible: model.type === 'imageEmbed'; - width: messageBoxFlow.width; - height: 200 - - Image { - source: model.type === 'imageEmbed' ? model.value : '' - sourceSize.width: 400 - sourceSize.height: 200 - } - } - } - } - } - } - } - - Component { - id: template_notification - - Rectangle { - property int index: delegateIndex - property string username: delegateUsername - - color: "#171717" - width: parent.width - height: 40 - - Item { - width: 10 - height: parent.height - - Rectangle { - height: parent.height - width: 5 - color: "#505186" - } - } - - Item { - width: parent.width - parent.children[0].width - 5 - height: parent.height - anchors.left: parent.children[0].right - - TextEdit { - text: delegateText - color: "white" - font.pointSize: 12 - readOnly: true - width: parent.width * 0.8 - selectByMouse: true - selectByKeyboard: true - height: parent.height - wrapMode: Text.Wrap - verticalAlignment: Text.AlignVCenter - font.italic: true - } - - Text { - text: delegateDate - color: "white" - font.pointSize: 12 - anchors.right: parent.right - height: parent.height - wrapMode: Text.Wrap - horizontalAlignment: Text.AlignRight - verticalAlignment: Text.AlignVCenter - font.italic: true - } - } - - } - - } + TemplateChatMessage { id: template_chat_message } + TemplateNotification { id: template_notification } property var channels: { "local": local, diff --git a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml new file mode 100644 index 0000000000..4165e8d37c --- /dev/null +++ b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml @@ -0,0 +1,161 @@ +import QtQuick 2.7 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.3 + +Component { + id: template_chat_message + + Rectangle { + property int index: delegateIndex + property string username: delegateUsername + + height: Math.max(65, children[1].height + 30) + color: index % 2 === 0 ? "transparent" : Qt.rgba(0.15,0.15,0.15,1) + width: listview.parent.parent.width + Layout.fillWidth: true + + Item { + width: parent.width - 10 + anchors.horizontalCenter: parent.horizontalCenter + height: 22 + + Text{ + text: username + color: "lightgray" + } + + Text{ + anchors.right: parent.right + text: delegateDate + color: "lightgray" + } + } + + Flow { + anchors.top: parent.children[0].bottom; + width: parent.width * 0.8 + x: 5 + id: messageBoxFlow + + Repeater { + model: delegateText; + + RowLayout { + Text { + text: model.value || "" + font.pointSize: 12 + wrapMode: Text.Wrap + width: model.type === 'text' || model.type === 'mention' ? Math.min(messageBoxFlow.width, contentWidth) : 0; + visible: model.type === 'text' || model.type === 'mention'; + + color: { + switch (model.type) { + case "mention": + return "purple"; + default: + return "white"; + } + } + } + + RowLayout { + width: children[0].contentWidth; + visible: model.type === 'url'; + + Text { + text: model.value || ""; + font.pointSize: 12; + wrapMode: Text.Wrap; + color: "#4EBAFD"; + font.underline: true; + width: parent.width; + + MouseArea { + anchors.fill: parent; + + onClicked: { + Window.openWebBrowser(model.value); + } + } + } + + Text { + text: "🗗"; + font.pointSize: 10; + wrapMode: Text.Wrap; + color: "white"; + + MouseArea { + anchors.fill: parent; + + onClicked: { + Qt.openUrlExternally(model.value); + } + } + } + } + + RowLayout { + visible: model.type === 'overteLocation'; + width: Math.min(messageBoxFlow.width, children[0].children[1].contentWidth + 35); + height: 20; + Layout.leftMargin: 5 + Layout.rightMargin: 5 + + Rectangle { + width: parent.width; + height: 20; + color: "lightgray" + radius: 2; + + Image { + source: "./img/ui/world_black.png" + width: 18; + height: 18; + sourceSize.width: 18 + sourceSize.height: 18 + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 2 + anchors.rightMargin: 10 + } + + Text { + text: model.type === 'overteLocation' ? model.value.split('hifi://')[1].split('/')[0] : ''; + color: "black" + font.pointSize: 12 + x: parent.children[0].width + 5; + anchors.verticalCenter: parent.verticalCenter + } + + MouseArea { + anchors.fill: parent; + + onClicked: { + Window.openUrl(model.value); + } + } + } + } + + Item { + Layout.fillWidth: true + visible: model.type === 'messageEnd' + } + + Item { + visible: model.type === 'imageEmbed'; + width: messageBoxFlow.width; + height: 200 + + Image { + source: model.type === 'imageEmbed' ? model.value : '' + sourceSize.width: 400 + sourceSize.height: 200 + } + } + } + } + } + } +} \ No newline at end of file diff --git a/scripts/system/domainChat/qml_widgets/TemplateNotification.qml b/scripts/system/domainChat/qml_widgets/TemplateNotification.qml new file mode 100644 index 0000000000..e522d58c54 --- /dev/null +++ b/scripts/system/domainChat/qml_widgets/TemplateNotification.qml @@ -0,0 +1,59 @@ +import QtQuick 2.7 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.3 + +Component { + id: template_notification + + Rectangle { + property int index: delegateIndex + property string username: delegateUsername + + color: "#171717" + width: parent.width + height: 40 + + Item { + width: 10 + height: parent.height + + Rectangle { + height: parent.height + width: 5 + color: "#505186" + } + } + + Item { + width: parent.width - parent.children[0].width - 5 + height: parent.height + anchors.left: parent.children[0].right + + TextEdit { + text: delegateText + color: "white" + font.pointSize: 12 + readOnly: true + width: parent.width * 0.8 + selectByMouse: true + selectByKeyboard: true + height: parent.height + wrapMode: Text.Wrap + verticalAlignment: Text.AlignVCenter + font.italic: true + } + + Text { + text: delegateDate + color: "white" + font.pointSize: 12 + anchors.right: parent.right + height: parent.height + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + font.italic: true + } + } + } +} \ No newline at end of file From 60284015c26f6dc58da27f79fbbd46c3ed489286 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Thu, 19 Dec 2024 04:41:49 -0600 Subject: [PATCH 09/23] Actually fix notifications. --- scripts/system/domainChat/domainChat.js | 16 +++--- scripts/system/domainChat/domainChat.qml | 10 ++-- .../qml_widgets/TemplateChatMessage.qml | 9 ++-- .../qml_widgets/TemplateNotification.qml | 54 +++++++------------ 4 files changed, 33 insertions(+), 56 deletions(-) diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js index 34cd128bce..2d80688d22 100644 --- a/scripts/system/domainChat/domainChat.js +++ b/scripts/system/domainChat/domainChat.js @@ -32,12 +32,8 @@ Controller.keyPressEvent.connect(keyPressEvent); Messages.subscribe("chat"); Messages.messageReceived.connect(receivedMessage); - AvatarManager.avatarAddedEvent.connect((sessionId) => { - _avatarAction("connected", sessionId); - }); - AvatarManager.avatarRemovedEvent.connect((sessionId) => { - _avatarAction("left", sessionId); - }); + AvatarManager.avatarAddedEvent.connect((sessionId) => { _avatarAction("connected", sessionId); }); + AvatarManager.avatarRemovedEvent.connect((sessionId) => { _avatarAction("left", sessionId); }); startup(); @@ -205,7 +201,7 @@ ); } function _avatarAction(type, sessionId) { - Script.setTimeout(() => { + Script.setTimeout(async () => { if (type == "connected") { palData = AvatarManager.getPalData().data; } @@ -233,7 +229,11 @@ _notificationCoreMessage(displayName, type) } - _emitEvent({ type: "notification", ...message }); + // Format notification message + let formattedMessagePacket = {...message}; + formattedMessagePacket.message = await _parseMessage(message.message); + + _emitEvent({ type: "notification", ...formattedMessagePacket }); }, 1500); } async function _loadSettings() { diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml index f40121ee71..6a79cb7e2e 100644 --- a/scripts/system/domainChat/domainChat.qml +++ b/scripts/system/domainChat/domainChat.qml @@ -9,7 +9,6 @@ Rectangle { signal sendToScript(var message); property string pageVal: "local" - property string last_message_user: "" property date last_message_time: new Date() // When the window is created on the script side, the window starts open. @@ -163,7 +162,7 @@ Rectangle { model: getChannel(pageVal) delegate: Loader { property int delegateIndex: model.index - property var delegateText: model.text + property var delegateText: model.message property string delegateUsername: model.username property string delegateDate: model.date @@ -171,7 +170,6 @@ Rectangle { if (model.type === "chat") return template_chat_message; if (model.type === "notification") return template_notification; } - } } } @@ -406,15 +404,13 @@ Rectangle { channel = getChannel(channel) // Format content - if (type === "notification"){ - channel.append({ text: message, date: date, type: "notification" }); + channel.append({ message: message, date: date, type: "notification" }); scrollToBottom(null, 30); - return; } - channel.append({ text: message, username: username, date: date, type: type }); + channel.append({ message: message, username: username, date: date, type: type }); load_scroll_timer.running = true; } diff --git a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml index 4165e8d37c..a9c366ce76 100644 --- a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml +++ b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml @@ -7,7 +7,6 @@ Component { Rectangle { property int index: delegateIndex - property string username: delegateUsername height: Math.max(65, children[1].height + 30) color: index % 2 === 0 ? "transparent" : Qt.rgba(0.15,0.15,0.15,1) @@ -19,12 +18,12 @@ Component { anchors.horizontalCenter: parent.horizontalCenter height: 22 - Text{ - text: username + Text { + text: delegateUsername color: "lightgray" } - Text{ + Text { anchors.right: parent.right text: delegateDate color: "lightgray" @@ -109,7 +108,7 @@ Component { radius: 2; Image { - source: "./img/ui/world_black.png" + source: "../img/ui/world_black.png" width: 18; height: 18; sourceSize.width: 18 diff --git a/scripts/system/domainChat/qml_widgets/TemplateNotification.qml b/scripts/system/domainChat/qml_widgets/TemplateNotification.qml index e522d58c54..4b9797d7f4 100644 --- a/scripts/system/domainChat/qml_widgets/TemplateNotification.qml +++ b/scripts/system/domainChat/qml_widgets/TemplateNotification.qml @@ -6,15 +6,12 @@ Component { id: template_notification Rectangle { - property int index: delegateIndex - property string username: delegateUsername - color: "#171717" width: parent.width height: 40 - Item { - width: 10 + RowLayout { + width: parent.width height: parent.height Rectangle { @@ -22,38 +19,23 @@ Component { width: 5 color: "#505186" } - } - - Item { - width: parent.width - parent.children[0].width - 5 - height: parent.height - anchors.left: parent.children[0].right - - TextEdit { - text: delegateText - color: "white" - font.pointSize: 12 - readOnly: true - width: parent.width * 0.8 - selectByMouse: true - selectByKeyboard: true - height: parent.height - wrapMode: Text.Wrap - verticalAlignment: Text.AlignVCenter - font.italic: true - } - Text { - text: delegateDate - color: "white" - font.pointSize: 12 - anchors.right: parent.right - height: parent.height - wrapMode: Text.Wrap - horizontalAlignment: Text.AlignRight - verticalAlignment: Text.AlignVCenter - font.italic: true + 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 + } } } } -} \ No newline at end of file +} From 6dbd63ebe9c5444e5e3e3bce39b0c5cb5ab1a407 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Thu, 19 Dec 2024 04:48:56 -0600 Subject: [PATCH 10/23] Fix formatting sometimes not formatting the last part of a message. --- scripts/system/domainChat/domainChat.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js index 2d80688d22..08be9822d9 100644 --- a/scripts/system/domainChat/domainChat.js +++ b/scripts/system/domainChat/domainChat.js @@ -305,10 +305,8 @@ let firstMatch = _findFirstMatch(); if (firstMatch == null) { - // If there was not any matches found in the entire message, format the whole message as a single text entry. - if (messageArray.length == 0) { - messageArray.push({type: 'text', value: runningMessage}); - } + // Format any remaining text as a basic 'text' type. + messageArray.push({type: 'text', value: runningMessage}); // Append a final 'fill width' to the message text. messageArray.push({type: 'messageEnd'}); From 27fa5da04883b0560141c57a4a72b4ed3f358c2e Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Thu, 19 Dec 2024 21:48:05 -0600 Subject: [PATCH 11/23] Housekeeping: data formatting --- scripts/system/domainChat/domainChat.js | 72 +++++++------------------ scripts/system/domainChat/formatting.js | 64 ++++++++++++++++++++++ 2 files changed, 82 insertions(+), 54 deletions(-) create mode 100644 scripts/system/domainChat/formatting.js diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js index 08be9822d9..03aeadd789 100644 --- a/scripts/system/domainChat/domainChat.js +++ b/scripts/system/domainChat/domainChat.js @@ -12,6 +12,10 @@ (() => { ("use strict"); + Script.include([ + "./formatting.js" + ]) + var appIsVisible = false; var settings = { external_window: false, @@ -93,22 +97,16 @@ // Is the message a chat message? channel = channel.toLowerCase(); if (channel !== "chat") return; - message = JSON.parse(message); - - // Get the message data - const currentTimestamp = _getTimestamp(); - const timeArray = _formatTimestamp(currentTimestamp); - + + if ((message = formatting.toJSON(message)) == null) return; // Make sure we are working with a JSON object we expect, otherwise kill + message = formatting.addTimeAndDateStringToPacket(message); + if (!message.channel) message.channel = "domain"; // We don't know where to put this message. Assume it is a domain wide message. message.channel = message.channel.toLowerCase(); // Only recognize channel names as lower case. if (!channels.includes(message.channel)) return; // Check the channel. If the channel is not one we have, do nothing. if (message.channel == "local" && isTooFar(message.position)) return; // If message is local, and if player is too far away from location, do nothing. - - // Format the timestamp - message.timeString = timeArray[0]; - message.dateString = timeArray[1]; - + let formattedMessagePacket = { ...message }; formattedMessagePacket.message = await _parseMessage(message.message) @@ -116,24 +114,16 @@ _notificationCoreMessage(message.displayName, message.message) // Show a new message on screen. // Create a new variable based on the message that will be saved. - let savedMessage = message; + let trimmedPacket = formatting.trimPacketToSave(message); + messageHistory.push(trimmedPacket); - // Remove unnecessary data. - delete savedMessage.position; - delete savedMessage.timeString; - delete savedMessage.dateString; - delete savedMessage.action; - - savedMessage.timestamp = currentTimestamp; - - messageHistory.push(savedMessage); while (messageHistory.length > settings.maximum_messages) { messageHistory.shift(); } Settings.setValue("ArmoredChat-Messages", messageHistory); - // Check to see if the message is close enough to the user function isTooFar(messagePosition) { + // Check to see if the message is close enough to the user return Vec3.distance(MyAvatar.position, messagePosition) > maxLocalDistance; } } @@ -218,10 +208,7 @@ } // Format the packet - let message = {}; - const timeArray = _formatTimestamp(_getTimestamp()); - message.timeString = timeArray[0]; - message.dateString = timeArray[1]; + let message = addTimeAndDateStringToPacket({}); message.message = `${displayName} ${type}`; // Show new message on screen @@ -242,43 +229,20 @@ if (messageHistory) { // Load message history for (message of messageHistory) { - const timeArray = _formatTimestamp(_getTimestamp()); - messagePacket = { ...message }; - messagePacket.timeString = timeArray[0]; - messagePacket.dateString = timeArray[1]; + messagePacket = { ...message }; // Create new variable + messagePacket = formatting.addTimeAndDateStringToPacket(messagePacket); // Add timestamp + messagePacket.message = await _parseMessage(messagePacket.message); // Parse the message for the UI - let formattedMessagePacket = {...messagePacket}; - formattedMessagePacket.message = await _parseMessage(messagePacket.message); - - _emitEvent({ type: "show_message", ...formattedMessagePacket }); + _emitEvent({ type: "show_message", ...messagePacket }); // Send message to UI } } - // Send current settings to the app - _emitEvent({ type: "initial_settings", settings: settings }); + _emitEvent({ type: "initial_settings", settings: settings }); // Send current settings to the app } function _saveSettings() { console.log("Saving config"); Settings.setValue("ArmoredChat-Config", settings); } - function _getTimestamp(){ - return Date.now(); - } - function _formatTimestamp(timestamp){ - let timeArray = []; - - timeArray.push(new Date().toLocaleTimeString(undefined, { - hour12: false, - })); - - timeArray.push(new Date(timestamp).toLocaleDateString(undefined, { - year: "numeric", - month: "long", - day: "numeric", - })); - - return timeArray; - } function _notificationCoreMessage(displayName, message){ Messages.sendLocalMessage( "Floof-Notif", diff --git a/scripts/system/domainChat/formatting.js b/scripts/system/domainChat/formatting.js new file mode 100644 index 0000000000..b523f30d2d --- /dev/null +++ b/scripts/system/domainChat/formatting.js @@ -0,0 +1,64 @@ +// +// formatting.js +// +// Created by Armored Dragon, 2024. +// Copyright 2024 Overte e.V. +// +// This just does some basic formatting and minor housekeeping for the domainChat.js application +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +const formatting = { + toJSON: function(data) { + if (typeof data == "object") return data; // Already JSON + + try { + const parsedData = JSON.parse(data); + return parsedData; + } catch (e) { + console.log('Failed to convert data to JSON.') + return null; // Could not convert to json, some error; + } + }, + addTimeAndDateStringToPacket: function(packet) { + // Gets the current time and adds it to a given packet + const timeArray = formatting.helpers._timestampArray(packet.timestamp); + packet.timeString = timeArray[0]; + packet.dateString = timeArray[1]; + return packet; + }, + trimPacketToSave: function(packet) { + // Takes a packet, and returns a packet containing only what is needed to save. + let newPacket = { + channel: packet.channel || "", + displayName: packet.displayName || "", + message: packet.message || "", + timestamp: packet.timestamp || formatting.helpers.getTimestamp(), + }; + return newPacket; + }, + + helpers: { + // Small functions that are used often in the other functions. + _timestampArray: function(timestamp) { + const currentDate = timestamp || formatting.helpers.getTimestamp(); + let timeArray = []; + + timeArray.push(new Date(currentDate).toLocaleTimeString(undefined, { + hour12: false, + })); + + timeArray.push(new Date(currentDate).toLocaleDateString(undefined, { + year: "numeric", + month: "long", + day: "numeric", + })); + + return timeArray; + }, + getTimestamp: function(){ + return Date.now(); + } + } +} From 891d533bd8b8323921e6d74127f2b1172981d026 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Fri, 20 Dec 2024 05:32:17 -0600 Subject: [PATCH 12/23] Move message parsing to formatting file. --- scripts/system/domainChat/domainChat.js | 114 ++---------------------- scripts/system/domainChat/formatting.js | 98 ++++++++++++++++++++ 2 files changed, 105 insertions(+), 107 deletions(-) diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js index 03aeadd789..6d75556081 100644 --- a/scripts/system/domainChat/domainChat.js +++ b/scripts/system/domainChat/domainChat.js @@ -108,7 +108,7 @@ if (message.channel == "local" && isTooFar(message.position)) return; // If message is local, and if player is too far away from location, do nothing. let formattedMessagePacket = { ...message }; - formattedMessagePacket.message = await _parseMessage(message.message) + formattedMessagePacket.message = await formatting.parseMessage(message.message) _emitEvent({ type: "show_message", ...formattedMessagePacket }); // Update qml view of to new message. _notificationCoreMessage(message.displayName, message.message) // Show a new message on screen. @@ -218,7 +218,7 @@ // Format notification message let formattedMessagePacket = {...message}; - formattedMessagePacket.message = await _parseMessage(message.message); + formattedMessagePacket.message = await formatting.parseMessage(message.message); _emitEvent({ type: "notification", ...formattedMessagePacket }); }, 1500); @@ -229,15 +229,15 @@ if (messageHistory) { // Load message history for (message of messageHistory) { - messagePacket = { ...message }; // Create new variable - messagePacket = formatting.addTimeAndDateStringToPacket(messagePacket); // Add timestamp - messagePacket.message = await _parseMessage(messagePacket.message); // Parse the message for the UI + messagePacket = { ...message }; // Create new variable + messagePacket = formatting.addTimeAndDateStringToPacket(messagePacket); // Add timestamp + messagePacket.message = await formatting.parseMessage(messagePacket.message); // Parse the message for the UI - _emitEvent({ type: "show_message", ...messagePacket }); // Send message to UI + _emitEvent({ type: "show_message", ...messagePacket }); // Send message to UI } } - _emitEvent({ type: "initial_settings", settings: settings }); // Send current settings to the app + _emitEvent({ type: "initial_settings", settings: settings }); // Send current settings to the app } function _saveSettings() { console.log("Saving config"); @@ -249,85 +249,6 @@ JSON.stringify({ sender: displayName, text: message }) ); } - async function _parseMessage(message){ - const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/; - const mentionRegex = /@(\w+)/; // FIXME: Remove - devcode - const overteLocationRegex = /hifi:\/\/[a-zA-Z0-9_-]+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+/; - - let runningMessage = message; - let messageArray = []; - - const regexPatterns = [ - { type: "url", regex: urlRegex }, - { type: "mention", regex: mentionRegex }, // FIXME: Remove - devcode - { type: "overteLocation", regex: overteLocationRegex } - ] - - // Here is a link https://www.example.com, #hashtag, and @mention. Just for some spice here is another https://exampletwo.com - - while (true) { - let firstMatch = _findFirstMatch(); - - if (firstMatch == null) { - // Format any remaining text as a basic 'text' type. - messageArray.push({type: 'text', value: runningMessage}); - - // Append a final 'fill width' to the message text. - messageArray.push({type: 'messageEnd'}); - break; - } - - _formatMessage(firstMatch); - } - - for (dataChunk of messageArray){ - if (dataChunk.type == 'url'){ - let url = dataChunk.value; - - const res = await fetch(url, {method: 'GET'}); // TODO: Replace with 'HEAD' method. https://github.com/overte-org/overte/issues/1273 - const contentType = res.getResponseHeader("content-type"); - - // TODO: Add support for other media types - if (contentType.startsWith('image/')) { - messageArray.push({type: 'imageEmbed', value: url}); - } - } - } - - return messageArray; - - function _formatMessage(firstMatch){ - let indexOfFirstMatch = firstMatch[0]; - let regex = regexPatterns[firstMatch[1]].regex; - - let foundMatch = runningMessage.match(regex)[0]; - - messageArray.push({type: 'text', value: runningMessage.substring(0, indexOfFirstMatch)}); - messageArray.push({type: regexPatterns[firstMatch[1]].type, value: runningMessage.substring(indexOfFirstMatch, indexOfFirstMatch + foundMatch.length)}); - - runningMessage = runningMessage.substring(indexOfFirstMatch + foundMatch.length); // Remove the part of the message we have worked with - } - - function _findFirstMatch(){ - let indexOfFirstMatch = Infinity; - let indexOfRegexPattern = Infinity; - - for (let i = 0; regexPatterns.length > i; i++){ - let indexOfMatch = runningMessage.search(regexPatterns[i].regex); - - if (indexOfMatch == -1) continue; // No match found - - if (indexOfMatch < indexOfFirstMatch) { - indexOfFirstMatch = indexOfMatch; - indexOfRegexPattern = i; - } - } - - if (indexOfFirstMatch !== Infinity) return [indexOfFirstMatch, indexOfRegexPattern]; // If there was a found match - return null; // No found match - } - } - /** * Emit a packet to the HTML front end. Easy communication! * @param {Object} packet - The Object packet to emit to the HTML @@ -337,27 +258,6 @@ chatOverlayWindow.sendToQml(packet); } - function fetch(url, options = {method: "GET"}) { - return new Promise((resolve, reject) => { - let req = new XMLHttpRequest(); - req.onreadystatechange = function () { - - if (req.readyState === req.DONE) { - if (req.status === 200) { - console.log("Content type:", req.getResponseHeader("content-type")); - resolve(req); - - } else { - console.log("Error", req.status, req.statusText); - reject(); - } - } - }; - - req.open(options.method, url); - req.send(); - }); - } })(); diff --git a/scripts/system/domainChat/formatting.js b/scripts/system/domainChat/formatting.js index b523f30d2d..05d3bf4cfa 100644 --- a/scripts/system/domainChat/formatting.js +++ b/scripts/system/domainChat/formatting.js @@ -38,6 +38,82 @@ const formatting = { }; return newPacket; }, + parseMessage: async function(message) { + const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/; + const overteLocationRegex = /hifi:\/\/[a-zA-Z0-9_-]+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+/; + + let runningMessage = message; // The remaining message that will be parsed + let messageArray = []; // An array of messages that are split up by the formatting functions + + const regexPatterns = [ + { type: "url", regex: urlRegex }, + { type: "overteLocation", regex: overteLocationRegex } + ] + + while (true) { + let firstMatch = _findFirstMatch(); + + if (firstMatch == null) { + // If there is no more text to parse, break out of the loop and return the message array. + // Format any remaining text as a basic 'text' type. + messageArray.push({type: 'text', value: runningMessage}); + + // Append a final 'fill width' to the message text. + messageArray.push({type: 'messageEnd'}); + break; + } + + _formatMessage(firstMatch); + } + + // Embed images in the message array. + for (dataChunk of messageArray){ + if (dataChunk.type == 'url'){ + let url = dataChunk.value; + + const res = await formatting.helpers.fetch(url, {method: 'GET'}); // TODO: Replace with 'HEAD' method. https://github.com/overte-org/overte/issues/1273 + const contentType = res.getResponseHeader("content-type"); + + // TODO: Add support for other media types + if (contentType.startsWith('image/')) { + messageArray.push({type: 'imageEmbed', value: url}); + } + } + } + + return messageArray; + + function _formatMessage(firstMatch){ + let indexOfFirstMatch = firstMatch[0]; + let regex = regexPatterns[firstMatch[1]].regex; + + let foundMatch = runningMessage.match(regex)[0]; + + messageArray.push({type: 'text', value: runningMessage.substring(0, indexOfFirstMatch)}); + messageArray.push({type: regexPatterns[firstMatch[1]].type, value: runningMessage.substring(indexOfFirstMatch, indexOfFirstMatch + foundMatch.length)}); + + runningMessage = runningMessage.substring(indexOfFirstMatch + foundMatch.length); // Remove the part of the message we have worked with + } + + function _findFirstMatch(){ + let indexOfFirstMatch = Infinity; + let indexOfRegexPattern = Infinity; + + for (let i = 0; regexPatterns.length > i; i++){ + let indexOfMatch = runningMessage.search(regexPatterns[i].regex); + + if (indexOfMatch == -1) continue; // No match found + + if (indexOfMatch < indexOfFirstMatch) { + indexOfFirstMatch = indexOfMatch; + indexOfRegexPattern = i; + } + } + + if (indexOfFirstMatch !== Infinity) return [indexOfFirstMatch, indexOfRegexPattern]; // If there was a found match + return null; // No found match + } + }, helpers: { // Small functions that are used often in the other functions. @@ -59,6 +135,28 @@ const formatting = { }, getTimestamp: function(){ return Date.now(); + }, + fetch: function (url, options = {method: "GET"}) { + return new Promise((resolve, reject) => { + let req = new XMLHttpRequest(); + + req.onreadystatechange = function () { + + if (req.readyState === req.DONE) { + if (req.status === 200) { + console.log("Content type:", req.getResponseHeader("content-type")); + resolve(req); + + } else { + console.log("Error", req.status, req.statusText); + reject(); + } + } + }; + + req.open(options.method, url); + req.send(); + }); } } } From 6976432574a0a266d6a6ebcfa13fb948c4253ded Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Sat, 21 Dec 2024 05:45:59 -0600 Subject: [PATCH 13/23] Support animated images. --- .../domainChat/qml_widgets/TemplateChatMessage.qml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml index a9c366ce76..7829ee791d 100644 --- a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml +++ b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml @@ -1,4 +1,4 @@ -import QtQuick 2.7 +import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.3 @@ -147,10 +147,11 @@ Component { width: messageBoxFlow.width; height: 200 - Image { + AnimatedImage { source: model.type === 'imageEmbed' ? model.value : '' - sourceSize.width: 400 - sourceSize.height: 200 + height: Math.min(sourceSize.height, 200); + onStatusChanged: playing = (status == AnimatedImage.Ready) + fillMode: Image.PreserveAspectFit } } } From eb6530136705887471fe1f2474dad19ca70dd751 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Sat, 21 Dec 2024 06:44:23 -0600 Subject: [PATCH 14/23] Basic video support. --- scripts/system/domainChat/formatting.js | 7 +++- .../qml_widgets/TemplateChatMessage.qml | 42 ++++++++++++++++++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/scripts/system/domainChat/formatting.js b/scripts/system/domainChat/formatting.js index 05d3bf4cfa..5e1458db8d 100644 --- a/scripts/system/domainChat/formatting.js +++ b/scripts/system/domainChat/formatting.js @@ -74,10 +74,14 @@ const formatting = { const res = await formatting.helpers.fetch(url, {method: 'GET'}); // TODO: Replace with 'HEAD' method. https://github.com/overte-org/overte/issues/1273 const contentType = res.getResponseHeader("content-type"); - // TODO: Add support for other media types if (contentType.startsWith('image/')) { messageArray.push({type: 'imageEmbed', value: url}); + continue; } + if (contentType.startsWith('video/')){ + messageArray.push({type: 'videoEmbed', value: url}); + continue; + } } } @@ -144,7 +148,6 @@ const formatting = { if (req.readyState === req.DONE) { if (req.status === 200) { - console.log("Content type:", req.getResponseHeader("content-type")); resolve(req); } else { diff --git a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml index 7829ee791d..9e33a0503e 100644 --- a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml +++ b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml @@ -1,6 +1,7 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.3 +import QtMultimedia 5.15 Component { id: template_chat_message @@ -150,10 +151,49 @@ Component { AnimatedImage { source: model.type === 'imageEmbed' ? model.value : '' height: Math.min(sourceSize.height, 200); - onStatusChanged: playing = (status == AnimatedImage.Ready) fillMode: Image.PreserveAspectFit } } + + Item { + visible: model.type === 'videoEmbed'; + width: messageBoxFlow.width; + height: 200; + + Video { + id: videoPlayer + source: model.type === 'videoEmbed' ? model.value : '' + height: 200; + width: 400; + fillMode: Image.PreserveAspectFit + autoLoad: false; + + onStatusChanged: { + if (status === 7) { + // Weird hack to make the video restart when it's over + // Ideally you'd want to use the seek function to restart the video but it doesn't work? + // Will need to make a more refined solution for this later. in the form of a more advanced media player. + // For now, this is sufficient. -AD + let originalURL = videoPlayer.source; + videoPlayer.source = ""; + videoPlayer.source = originalURL; + } + } + + MouseArea { + anchors.fill: parent + onClicked: { + const videoIsOver = videoPlayer.position == videoPlayer.duration + if (videoPlayer.playbackState == MediaPlayer.PlayingState) { + videoPlayer.pause(); + } + else { + parent.play(); + } + } + } + } + } } } } From cf4ec3fb572b06b6c6364e486adbcc36291052e1 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Tue, 31 Dec 2024 16:22:55 -0600 Subject: [PATCH 15/23] Add setting to force virtual window in VR. --- scripts/system/domainChat/domainChat.js | 34 ++++++++++++++++++------ scripts/system/domainChat/domainChat.qml | 25 +++++++++++++++++ 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js index 6d75556081..e1e73bd6cb 100644 --- a/scripts/system/domainChat/domainChat.js +++ b/scripts/system/domainChat/domainChat.js @@ -7,8 +7,6 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// TODO: Message trimming - (() => { ("use strict"); @@ -20,8 +18,10 @@ var settings = { external_window: false, maximum_messages: 200, - join_notification: true + join_notification: true, + switchToInternalOnHeadsetUsed: true }; + let temporaryChangeModeToVirtual = false; // Global vars var tablet; @@ -38,6 +38,7 @@ Messages.messageReceived.connect(receivedMessage); AvatarManager.avatarAddedEvent.connect((sessionId) => { _avatarAction("connected", sessionId); }); AvatarManager.avatarRemovedEvent.connect((sessionId) => { _avatarAction("left", sessionId); }); + HMD.displayModeChanged.connect(_onHMDDisplayModeChanged); startup(); @@ -134,15 +135,16 @@ break; case "setting_change": // Set the setting value, and save the config - settings[event.setting] = event.value; // Update local settings - _saveSettings(); // Save local settings + settings[event.setting] = event.value; // Update local settings + _saveSettings(); // Save local settings // Extra actions to preform. switch (event.setting) { case "external_window": - chatOverlayWindow.presentationMode = event.value - ? Desktop.PresentationMode.NATIVE - : Desktop.PresentationMode.VIRTUAL; + _changePresentationMode(event.value); + break; + case "switchToInternalOnHeadsetUsed": + _onHMDDisplayModeChanged(HMD.active); break; } @@ -176,6 +178,22 @@ }); } } + function _onHMDDisplayModeChanged(isHMDActive){ + // If the user enabled automatic switching to internal when they put on a headset... + if (!settings.switchToInternalOnHeadsetUsed) return; + + if (isHMDActive) temporaryChangeModeToVirtual = true; + else temporaryChangeModeToVirtual = false; + + _changePresentationMode(settings.external_window); + } + function _changePresentationMode(changeToExternal){ + if (temporaryChangeModeToVirtual) changeToExternal = false; + + chatOverlayWindow.presentationMode = changeToExternal + ? Desktop.PresentationMode.NATIVE + : Desktop.PresentationMode.VIRTUAL; + } function _sendMessage(message, channel) { if (message.length == 0) return; diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml index 6a79cb7e2e..287bb1a825 100644 --- a/scripts/system/domainChat/domainChat.qml +++ b/scripts/system/domainChat/domainChat.qml @@ -369,6 +369,29 @@ Rectangle { } } } + // Switch to internal on VR Mode + Rectangle { + width: parent.width + height: 40 + color: "transparent" + + Text { + text: "Force Virtual window in VR" + color: "white" + font.pointSize: 12 + anchors.verticalCenter: parent.verticalCenter + } + + CheckBox { + id: s_force_vw_in_vr + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + onCheckedChanged: { + toScript({type: 'setting_change', setting: 'switchToInternalOnHeadsetUsed', value: checked}) + } + } + } } } @@ -433,9 +456,11 @@ Rectangle { domain.clear(); break; case "initial_settings": + print(JSON.stringify(message.settings)); if (message.settings.external_window) s_external_window.checked = true; if (message.settings.maximum_messages) s_maximum_messages.value = message.settings.maximum_messages; if (message.settings.join_notification) s_join_notification.checked = true; + if (message.settings.switchToInternalOnHeadsetUsed) s_force_vw_in_vr.checked = true; break; } } From 37cdbacf684665eebe0a47cbe25d3aa534a00a39 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Wed, 1 Jan 2025 07:35:04 -0600 Subject: [PATCH 16/23] Message embedding toggle. --- scripts/system/domainChat/domainChat.js | 11 ++++---- scripts/system/domainChat/domainChat.qml | 25 ++++++++++++++++++ scripts/system/domainChat/formatting.js | 32 +++++++++++++----------- 3 files changed, 48 insertions(+), 20 deletions(-) diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js index e1e73bd6cb..5a97c4dee9 100644 --- a/scripts/system/domainChat/domainChat.js +++ b/scripts/system/domainChat/domainChat.js @@ -19,7 +19,8 @@ external_window: false, maximum_messages: 200, join_notification: true, - switchToInternalOnHeadsetUsed: true + switchToInternalOnHeadsetUsed: true, + enableEmbedding: false // Prevents information leakage, default false }; let temporaryChangeModeToVirtual = false; @@ -109,7 +110,7 @@ if (message.channel == "local" && isTooFar(message.position)) return; // If message is local, and if player is too far away from location, do nothing. let formattedMessagePacket = { ...message }; - formattedMessagePacket.message = await formatting.parseMessage(message.message) + formattedMessagePacket.message = await formatting.parseMessage(message.message, settings.enableEmbedding) _emitEvent({ type: "show_message", ...formattedMessagePacket }); // Update qml view of to new message. _notificationCoreMessage(message.displayName, message.message) // Show a new message on screen. @@ -247,9 +248,9 @@ if (messageHistory) { // Load message history for (message of messageHistory) { - messagePacket = { ...message }; // Create new variable - messagePacket = formatting.addTimeAndDateStringToPacket(messagePacket); // Add timestamp - messagePacket.message = await formatting.parseMessage(messagePacket.message); // Parse the message for the UI + messagePacket = { ...message }; // Create new variable + messagePacket = formatting.addTimeAndDateStringToPacket(messagePacket); // Add timestamp + messagePacket.message = await formatting.parseMessage(messagePacket.message, settings.enableEmbedding); // Parse the message for the UI _emitEvent({ type: "show_message", ...messagePacket }); // Send message to UI } diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml index 287bb1a825..ef5860587a 100644 --- a/scripts/system/domainChat/domainChat.qml +++ b/scripts/system/domainChat/domainChat.qml @@ -392,6 +392,29 @@ Rectangle { } } } + // Toggle media embedding + Rectangle { + width: parent.width + height: 40 + color: "transparent" + + Text { + text: "Enable media embedding" + color: "white" + font.pointSize: 12 + anchors.verticalCenter: parent.verticalCenter + } + + CheckBox { + id: s_enable_embedding + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + onCheckedChanged: { + toScript({type: 'setting_change', setting: 'enableEmbedding', value: checked}) + } + } + } } } @@ -456,11 +479,13 @@ Rectangle { domain.clear(); break; case "initial_settings": + print(JSON.stringify(message.settings)); if (message.settings.external_window) s_external_window.checked = true; if (message.settings.maximum_messages) s_maximum_messages.value = message.settings.maximum_messages; if (message.settings.join_notification) s_join_notification.checked = true; if (message.settings.switchToInternalOnHeadsetUsed) s_force_vw_in_vr.checked = true; + if (message.settings.enableEmbedding) s_enable_embedding.checked = true; break; } } diff --git a/scripts/system/domainChat/formatting.js b/scripts/system/domainChat/formatting.js index 5e1458db8d..24662e4ff1 100644 --- a/scripts/system/domainChat/formatting.js +++ b/scripts/system/domainChat/formatting.js @@ -38,7 +38,7 @@ const formatting = { }; return newPacket; }, - parseMessage: async function(message) { + parseMessage: async function(message, enableEmbedding) { const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/; const overteLocationRegex = /hifi:\/\/[a-zA-Z0-9_-]+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+/; @@ -67,23 +67,25 @@ const formatting = { } // Embed images in the message array. - for (dataChunk of messageArray){ - if (dataChunk.type == 'url'){ - let url = dataChunk.value; + if (enableEmbedding) { + for (dataChunk of messageArray){ + if (dataChunk.type == 'url'){ + let url = dataChunk.value; - const res = await formatting.helpers.fetch(url, {method: 'GET'}); // TODO: Replace with 'HEAD' method. https://github.com/overte-org/overte/issues/1273 - const contentType = res.getResponseHeader("content-type"); + const res = await formatting.helpers.fetch(url, {method: 'GET'}); // TODO: Replace with 'HEAD' method. https://github.com/overte-org/overte/issues/1273 + const contentType = res.getResponseHeader("content-type"); - if (contentType.startsWith('image/')) { - messageArray.push({type: 'imageEmbed', value: url}); - continue; - } - if (contentType.startsWith('video/')){ - messageArray.push({type: 'videoEmbed', value: url}); - continue; + if (contentType.startsWith('image/')) { + messageArray.push({type: 'imageEmbed', value: url}); + continue; + } + if (contentType.startsWith('video/')){ + messageArray.push({type: 'videoEmbed', value: url}); + continue; + } } - } - } + } + } return messageArray; From fae1fdd7e045e0ded4174c0d9da9a8747470e5e2 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Wed, 1 Jan 2025 11:41:17 -0600 Subject: [PATCH 17/23] Remove lingering print function. --- scripts/system/domainChat/domainChat.qml | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml index ef5860587a..9ac69f71ac 100644 --- a/scripts/system/domainChat/domainChat.qml +++ b/scripts/system/domainChat/domainChat.qml @@ -479,8 +479,6 @@ Rectangle { domain.clear(); break; case "initial_settings": - - print(JSON.stringify(message.settings)); if (message.settings.external_window) s_external_window.checked = true; if (message.settings.maximum_messages) s_maximum_messages.value = message.settings.maximum_messages; if (message.settings.join_notification) s_join_notification.checked = true; From 1c9174ff8f06a8fa290dfd3b22a5f8b46cc2ff23 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Wed, 15 Jan 2025 11:46:50 -0600 Subject: [PATCH 18/23] Fix text highlighting. --- .../qml_widgets/TemplateChatMessage.qml | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml index 9e33a0503e..8a8606c0ff 100644 --- a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml +++ b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml @@ -19,15 +19,21 @@ Component { anchors.horizontalCenter: parent.horizontalCenter height: 22 - Text { + TextEdit { text: delegateUsername color: "lightgray" + readOnly: true + selectByMouse: true + selectByKeyboard: true } - Text { + TextEdit { anchors.right: parent.right text: delegateDate color: "lightgray" + readOnly: true + selectByMouse: true + selectByKeyboard: true } } @@ -41,12 +47,15 @@ Component { model: delegateText; RowLayout { - Text { + TextEdit { text: model.value || "" font.pointSize: 12 wrapMode: Text.Wrap width: model.type === 'text' || model.type === 'mention' ? Math.min(messageBoxFlow.width, contentWidth) : 0; visible: model.type === 'text' || model.type === 'mention'; + readOnly: true + selectByMouse: true + selectByKeyboard: true color: { switch (model.type) { @@ -62,13 +71,16 @@ Component { width: children[0].contentWidth; visible: model.type === 'url'; - Text { + TextEdit { text: model.value || ""; font.pointSize: 12; wrapMode: Text.Wrap; color: "#4EBAFD"; font.underline: true; width: parent.width; + readOnly: true + selectByMouse: true + selectByKeyboard: true MouseArea { anchors.fill: parent; @@ -120,12 +132,15 @@ Component { anchors.rightMargin: 10 } - Text { + TextEdit { text: model.type === 'overteLocation' ? model.value.split('hifi://')[1].split('/')[0] : ''; color: "black" font.pointSize: 12 x: parent.children[0].width + 5; anchors.verticalCenter: parent.verticalCenter + readOnly: true + selectByMouse: true + selectByKeyboard: true } MouseArea { From e67526cdf5ddfbff7931794c34dcfd3eaeb6617c Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Wed, 15 Jan 2025 13:53:16 -0600 Subject: [PATCH 19/23] Fix binding loops. --- .../qml_widgets/TemplateChatMessage.qml | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml index 8a8606c0ff..b5b82a7d79 100644 --- a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml +++ b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml @@ -20,26 +20,23 @@ Component { height: 22 TextEdit { - text: delegateUsername - color: "lightgray" - readOnly: true - selectByMouse: true - selectByKeyboard: true + text: delegateUsername; + color: "lightgray"; + readOnly: true; + selectByMouse: true; + selectByKeyboard: true; } - TextEdit { - anchors.right: parent.right - text: delegateDate - color: "lightgray" - readOnly: true - selectByMouse: true - selectByKeyboard: true + Text { + anchors.right: parent.right; + text: delegateDate; + color: "lightgray"; } } Flow { anchors.top: parent.children[0].bottom; - width: parent.width * 0.8 + width: parent.width; x: 5 id: messageBoxFlow @@ -47,11 +44,13 @@ Component { model: delegateText; RowLayout { + width: parent.width; + TextEdit { text: model.value || "" font.pointSize: 12 wrapMode: Text.Wrap - width: model.type === 'text' || model.type === 'mention' ? Math.min(messageBoxFlow.width, contentWidth) : 0; + width: messageBoxFlow.width; visible: model.type === 'text' || model.type === 'mention'; readOnly: true selectByMouse: true @@ -68,10 +67,11 @@ Component { } RowLayout { - width: children[0].contentWidth; + width: urlTypeTextDisplay.width; visible: model.type === 'url'; TextEdit { + id: urlTypeTextDisplay; text: model.value || ""; font.pointSize: 12; wrapMode: Text.Wrap; @@ -154,8 +154,8 @@ Component { } Item { - Layout.fillWidth: true - visible: model.type === 'messageEnd' + Layout.fillWidth: true; + visible: model.type === 'messageEnd'; } Item { From dbf5af052b59a7467d7d1ac2a73a4ded88bd7ae8 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Thu, 16 Jan 2025 14:53:54 -0600 Subject: [PATCH 20/23] Remove video embeds. Added debug logs. Don't save settings when initializing the application. --- scripts/system/domainChat/domainChat.js | 21 ++++- scripts/system/domainChat/domainChat.qml | 13 ++- .../qml_widgets/TemplateChatMessage.qml | 79 +++++++++---------- 3 files changed, 66 insertions(+), 47 deletions(-) diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js index 5a97c4dee9..0034f73093 100644 --- a/scripts/system/domainChat/domainChat.js +++ b/scripts/system/domainChat/domainChat.js @@ -99,7 +99,6 @@ // Is the message a chat message? channel = channel.toLowerCase(); if (channel !== "chat") return; - if ((message = formatting.toJSON(message)) == null) return; // Make sure we are working with a JSON object we expect, otherwise kill message = formatting.addTimeAndDateStringToPacket(message); @@ -194,6 +193,8 @@ chatOverlayWindow.presentationMode = changeToExternal ? Desktop.PresentationMode.NATIVE : Desktop.PresentationMode.VIRTUAL; + + console.log(`Presentation mode was changed to ${chatOverlayWindow.presentationMode}`); } function _sendMessage(message, channel) { if (message.length == 0) return; @@ -244,6 +245,7 @@ } async function _loadSettings() { settings = Settings.getValue("ArmoredChat-Config", settings); + console.log("Loading settings: ", jstr(settings)); if (messageHistory) { // Load message history @@ -259,10 +261,11 @@ _emitEvent({ type: "initial_settings", settings: settings }); // Send current settings to the app } function _saveSettings() { - console.log("Saving config"); + console.log("Saving settings: ", jstr(settings)); Settings.setValue("ArmoredChat-Config", settings); } function _notificationCoreMessage(displayName, message){ + console.log("Sending notification to notificationCore:", `Display name: ${displayName}\n Message: ${message}`); Messages.sendLocalMessage( "Floof-Notif", JSON.stringify({ sender: displayName, text: message }) @@ -274,9 +277,19 @@ * @param {("show_message"|"clear_messages"|"notification"|"initial_settings")} packet.type - The type of packet it is */ function _emitEvent(packet = { type: "" }) { + if (packet.type == `show_message`) { + // Don't show the message contents, this is a courtesy to prevent message leakage in the logs. + let strippedPacket = {...packet}; + delete strippedPacket.message + console.log("Sending packet to QML interface", jstr(strippedPacket)); + } + else { + console.log("Sending packet to QML interface", jstr(packet)); + } + chatOverlayWindow.sendToQml(packet); } - - + // Debug and developer functions and data + const jstr = (object) => JSON.stringify(object, null, 4); // JSON Stringify function with formatting })(); diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml index 9ac69f71ac..be4b6d4f79 100644 --- a/scripts/system/domainChat/domainChat.qml +++ b/scripts/system/domainChat/domainChat.qml @@ -8,8 +8,10 @@ Rectangle { color: Qt.rgba(0.1,0.1,0.1,1) signal sendToScript(var message); - property string pageVal: "local" - property date last_message_time: new Date() + property string pageVal: "local"; + property date last_message_time: new Date(); + property bool initialized: false; + // When the window is created on the script side, the window starts open. // Once the QML window is created wait, then send the initialized signal. @@ -445,7 +447,6 @@ Rectangle { messageViewFlickable.returnToBounds(); } - function addMessage(username, message, date, channel, type){ channel = getChannel(channel) @@ -479,17 +480,23 @@ Rectangle { domain.clear(); break; case "initial_settings": + print(`Got settings:\n ${JSON.stringify(message.settings, null, 4)}`); if (message.settings.external_window) s_external_window.checked = true; if (message.settings.maximum_messages) s_maximum_messages.value = message.settings.maximum_messages; if (message.settings.join_notification) s_join_notification.checked = true; if (message.settings.switchToInternalOnHeadsetUsed) s_force_vw_in_vr.checked = true; if (message.settings.enableEmbedding) s_enable_embedding.checked = true; + + initialized = true; // Application is ready + break; } } // Send message to script function toScript(packet){ + if (packet.type === "setting_change" && !initialized) return; // Don't announce a change in settings if not ready + sendToScript(packet) } } diff --git a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml index b5b82a7d79..641a0ca0b3 100644 --- a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml +++ b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml @@ -1,7 +1,7 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.3 -import QtMultimedia 5.15 +// import QtMultimedia 5.15 Component { id: template_chat_message @@ -170,45 +170,44 @@ Component { } } - Item { - visible: model.type === 'videoEmbed'; - width: messageBoxFlow.width; - height: 200; - - 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(); - } - } - } - } - } + // Item { + // visible: model.type === 'videoEmbed'; + // width: messageBoxFlow.width; + // height: 200; + + // Video { + // id: videoPlayer + // source: model.type === 'videoEmbed' ? model.value : '' + // height: 200; + // width: 400; + // fillMode: Image.PreserveAspectFit + // autoLoad: false; + + // onStatusChanged: { + // if (status === 7) { + // // Weird hack to make the video restart when it's over + // // Ideally you'd want to use the seek function to restart the video but it doesn't work? + // // Will need to make a more refined solution for this later. in the form of a more advanced media player. + // // For now, this is sufficient. -AD + // let originalURL = videoPlayer.source; + // videoPlayer.source = ""; + // videoPlayer.source = originalURL; + // } + // } + + // MouseArea { + // anchors.fill: parent + // onClicked: { + // const videoIsOver = videoPlayer.position == videoPlayer.duration + // if (videoPlayer.playbackState == MediaPlayer.PlayingState) { + // videoPlayer.pause(); + // } + // else { + // parent.play(); + // } + // } + // } + // } } } } From cf8a21c794a9c230e335a635c0967cd9b234c290 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Wed, 22 Jan 2025 23:12:03 -0600 Subject: [PATCH 21/23] Fix spacing on messages with links. --- scripts/system/domainChat/domainChat.qml | 1 - scripts/system/domainChat/formatting.js | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml index be4b6d4f79..28ec98d390 100644 --- a/scripts/system/domainChat/domainChat.qml +++ b/scripts/system/domainChat/domainChat.qml @@ -189,7 +189,6 @@ Rectangle { } } - ListModel { id: local } diff --git a/scripts/system/domainChat/formatting.js b/scripts/system/domainChat/formatting.js index 24662e4ff1..c534f3a720 100644 --- a/scripts/system/domainChat/formatting.js +++ b/scripts/system/domainChat/formatting.js @@ -56,7 +56,7 @@ const formatting = { if (firstMatch == null) { // If there is no more text to parse, break out of the loop and return the message array. // Format any remaining text as a basic 'text' type. - messageArray.push({type: 'text', value: runningMessage}); + if (runningMessage.trim() != "") messageArray.push({type: 'text', value: runningMessage}); // Append a final 'fill width' to the message text. messageArray.push({type: 'messageEnd'}); @@ -95,7 +95,7 @@ const formatting = { let foundMatch = runningMessage.match(regex)[0]; - messageArray.push({type: 'text', value: runningMessage.substring(0, indexOfFirstMatch)}); + if (runningMessage.substring(0, indexOfFirstMatch) != "") messageArray.push({type: 'text', value: runningMessage.substring(0, indexOfFirstMatch)}); messageArray.push({type: regexPatterns[firstMatch[1]].type, value: runningMessage.substring(indexOfFirstMatch, indexOfFirstMatch + foundMatch.length)}); runningMessage = runningMessage.substring(indexOfFirstMatch + foundMatch.length); // Remove the part of the message we have worked with From aacdf64bf6809a15b36a6ed079d7802535ff3f5d Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Mon, 27 Jan 2025 23:53:58 -0600 Subject: [PATCH 22/23] Fix wrapping. Remove unused video embed code. --- .../qml_widgets/TemplateChatMessage.qml | 47 ++----------------- 1 file changed, 5 insertions(+), 42 deletions(-) diff --git a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml index 641a0ca0b3..0f97a614ae 100644 --- a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml +++ b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml @@ -1,7 +1,6 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.3 -// import QtMultimedia 5.15 Component { id: template_chat_message @@ -43,14 +42,15 @@ Component { Repeater { model: delegateText; - RowLayout { + Item { width: parent.width; + height: children[0].contentHeight; TextEdit { text: model.value || "" font.pointSize: 12 - wrapMode: Text.Wrap - width: messageBoxFlow.width; + wrapMode: TextEdit.WordWrap + width: parent.width * 0.8 visible: model.type === 'text' || model.type === 'mention'; readOnly: true selectByMouse: true @@ -170,44 +170,7 @@ Component { } } - // Item { - // visible: model.type === 'videoEmbed'; - // width: messageBoxFlow.width; - // height: 200; - - // Video { - // id: videoPlayer - // source: model.type === 'videoEmbed' ? model.value : '' - // height: 200; - // width: 400; - // fillMode: Image.PreserveAspectFit - // autoLoad: false; - - // onStatusChanged: { - // if (status === 7) { - // // Weird hack to make the video restart when it's over - // // Ideally you'd want to use the seek function to restart the video but it doesn't work? - // // Will need to make a more refined solution for this later. in the form of a more advanced media player. - // // For now, this is sufficient. -AD - // let originalURL = videoPlayer.source; - // videoPlayer.source = ""; - // videoPlayer.source = originalURL; - // } - // } - - // MouseArea { - // anchors.fill: parent - // onClicked: { - // const videoIsOver = videoPlayer.position == videoPlayer.duration - // if (videoPlayer.playbackState == MediaPlayer.PlayingState) { - // videoPlayer.pause(); - // } - // else { - // parent.play(); - // } - // } - // } - // } + } } } From 25fe4850b99c655b7fa3bc3943e6c40925572c95 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Tue, 28 Jan 2025 00:00:11 -0600 Subject: [PATCH 23/23] Added a sortOrder. https://github.com/overte-org/overte/pull/1276 --- scripts/system/domainChat/domainChat.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js index 0034f73093..74de8d1b9b 100644 --- a/scripts/system/domainChat/domainChat.js +++ b/scripts/system/domainChat/domainChat.js @@ -49,6 +49,7 @@ appButton = tablet.addButton({ icon: Script.resolvePath("./img/icon_white.png"), activeIcon: Script.resolvePath("./img/icon_black.png"), + sortOrder: 8, text: "CHAT", isActive: appIsVisible, });