This commit is contained in:
Armored-Dragon 2025-03-30 15:33:06 +00:00 committed by GitHub
commit bf6d744edc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 567 additions and 325 deletions

View file

@ -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"
];

View file

@ -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

View file

@ -1,5 +1,5 @@
//
// armored_chat.js
// domainChat.js
//
// Created by Armored Dragon, May 17th, 2024.
// Copyright 2024 Overte e.V.
@ -10,12 +10,19 @@
(() => {
("use strict");
Script.include([
"./formatting.js"
])
var appIsVisible = false;
var settings = {
external_window: false,
maximum_messages: 200,
join_notification: true
join_notification: true,
switchToInternalOnHeadsetUsed: true,
enableEmbedding: false // Prevents information leakage, default false
};
let temporaryChangeModeToVirtual = false;
// Global vars
var tablet;
@ -28,15 +35,11 @@
var palData = AvatarManager.getPalData().data;
Controller.keyPressEvent.connect(keyPressEvent);
Messages.subscribe("Chat"); // Floofchat
Messages.subscribe("chat");
Messages.messageReceived.connect(receivedMessage);
AvatarManager.avatarAddedEvent.connect((sessionId) => {
_avatarAction("connected", sessionId);
});
AvatarManager.avatarRemovedEvent.connect((sessionId) => {
_avatarAction("left", sessionId);
});
AvatarManager.avatarAddedEvent.connect((sessionId) => { _avatarAction("connected", sessionId); });
AvatarManager.avatarRemovedEvent.connect((sessionId) => { _avatarAction("left", sessionId); });
HMD.displayModeChanged.connect(_onHMDDisplayModeChanged);
startup();
@ -46,6 +49,7 @@
appButton = tablet.addButton({
icon: Script.resolvePath("./img/icon_white.png"),
activeIcon: Script.resolvePath("./img/icon_black.png"),
sortOrder: 8,
text: "CHAT",
sortOrder: 8,
isActive: appIsVisible,
@ -62,7 +66,7 @@
appButton.clicked.connect(toggleMainChatWindow);
quickMessage = new OverlayWindow({
source: Script.resolvePath("./armored_chat_quick_message.qml"),
source: Script.resolvePath("./domainChatQuick.qml"),
});
_openWindow();
@ -79,7 +83,7 @@
}
function _openWindow() {
chatOverlayWindow = new Desktop.createWindow(
Script.resolvePath("./armored_chat.qml"),
Script.resolvePath("./domainChat.qml"),
{
title: "Chat",
size: { x: 550, y: 400 },
@ -93,64 +97,36 @@
chatOverlayWindow.fromQml.connect(fromQML);
quickMessage.fromQml.connect(fromQML);
}
function receivedMessage(channel, message) {
async function receivedMessage(channel, message) {
// Is the message a chat message?
channel = channel.toLowerCase();
if (channel !== "chat") return;
message = JSON.parse(message);
if ((message = formatting.toJSON(message)) == null) return; // Make sure we are working with a JSON object we expect, otherwise kill
message = formatting.addTimeAndDateStringToPacket(message);
if (!message.channel) message.channel = "domain"; // We don't know where to put this message. Assume it is a domain wide message.
message.channel = message.channel.toLowerCase(); // Only recognize channel names as lower case.
if (!channels.includes(message.channel)) return; // Check the channel. If the channel is not one we have, do nothing.
if (message.channel == "local" && isTooFar(message.position)) return; // If message is local, and if player is too far away from location, do nothing.
let formattedMessagePacket = { ...message };
formattedMessagePacket.message = await formatting.parseMessage(message.message, settings.enableEmbedding)
// Get the message data
const currentTimestamp = _getTimestamp();
const timeArray = _formatTimestamp(currentTimestamp);
_emitEvent({ type: "show_message", ...formattedMessagePacket }); // Update qml view of to new message.
_notificationCoreMessage(message.displayName, message.message) // Show a new message on screen.
if (!message.channel) message.channel = "domain"; // We don't know where to put this message. Assume it is a domain wide message.
if (message.forApp) return; // Floofchat
// Create a new variable based on the message that will be saved.
let trimmedPacket = formatting.trimPacketToSave(message);
messageHistory.push(trimmedPacket);
// Floofchat compatibility hook
message = floofChatCompatibilityConversion(message);
message.channel = message.channel.toLowerCase();
// Check the channel. If the channel is not one we have, do nothing.
if (!channels.includes(message.channel)) return;
// If message is local, and if player is too far away from location, do nothing.
if (message.channel == "local" && isTooFar(message.position)) return;
// Format the timestamp
message.timeString = timeArray[0];
message.dateString = timeArray[1];
// Update qml view of to new message
_emitEvent({ type: "show_message", ...message });
// Show new message on screen
Messages.sendLocalMessage(
"Floof-Notif",
JSON.stringify({
sender: message.displayName,
text: message.message,
})
);
// Save message to history
let savedMessage = message;
// Remove unnecessary data.
delete savedMessage.position;
delete savedMessage.timeString;
delete savedMessage.dateString;
delete savedMessage.action;
savedMessage.timestamp = currentTimestamp;
messageHistory.push(savedMessage);
while (messageHistory.length > settings.maximum_messages) {
messageHistory.shift();
}
Settings.setValue("ArmoredChat-Messages", messageHistory);
// Check to see if the message is close enough to the user
function isTooFar(messagePosition) {
// Check to see if the message is close enough to the user
return Vec3.distance(MyAvatar.position, messagePosition) > maxLocalDistance;
}
}
@ -161,15 +137,16 @@
break;
case "setting_change":
// Set the setting value, and save the config
settings[event.setting] = event.value; // Update local settings
_saveSettings(); // Save local settings
settings[event.setting] = event.value; // Update local settings
_saveSettings(); // Save local settings
// Extra actions to preform.
switch (event.setting) {
case "external_window":
chatOverlayWindow.presentationMode = event.value
? Desktop.PresentationMode.NATIVE
: Desktop.PresentationMode.VIRTUAL;
_changePresentationMode(event.value);
break;
case "switchToInternalOnHeadsetUsed":
_onHMDDisplayModeChanged(HMD.active);
break;
}
@ -203,6 +180,24 @@
});
}
}
function _onHMDDisplayModeChanged(isHMDActive){
// If the user enabled automatic switching to internal when they put on a headset...
if (!settings.switchToInternalOnHeadsetUsed) return;
if (isHMDActive) temporaryChangeModeToVirtual = true;
else temporaryChangeModeToVirtual = false;
_changePresentationMode(settings.external_window);
}
function _changePresentationMode(changeToExternal){
if (temporaryChangeModeToVirtual) changeToExternal = false;
chatOverlayWindow.presentationMode = changeToExternal
? Desktop.PresentationMode.NATIVE
: Desktop.PresentationMode.VIRTUAL;
console.log(`Presentation mode was changed to ${chatOverlayWindow.presentationMode}`);
}
function _sendMessage(message, channel) {
if (message.length == 0) return;
@ -216,11 +211,9 @@
action: "send_chat_message",
})
);
floofChatCompatibilitySendMessage(message, channel);
}
function _avatarAction(type, sessionId) {
Script.setTimeout(() => {
Script.setTimeout(async () => {
if (type == "connected") {
palData = AvatarManager.getPalData().data;
}
@ -237,101 +230,68 @@
}
// Format the packet
let message = {};
const timeArray = _formatTimestamp(_getTimestamp());
message.timeString = timeArray[0];
message.dateString = timeArray[1];
let message = addTimeAndDateStringToPacket({});
message.message = `${displayName} ${type}`;
// Show new message on screen
if (settings.join_notification){
Messages.sendLocalMessage(
"Floof-Notif",
JSON.stringify({
sender: displayName,
text: type,
})
);
_notificationCoreMessage(displayName, type)
}
_emitEvent({ type: "notification", ...message });
// Format notification message
let formattedMessagePacket = {...message};
formattedMessagePacket.message = await formatting.parseMessage(message.message);
_emitEvent({ type: "notification", ...formattedMessagePacket });
}, 1500);
}
function _loadSettings() {
async function _loadSettings() {
settings = Settings.getValue("ArmoredChat-Config", settings);
console.log("Loading settings: ", jstr(settings));
if (messageHistory) {
// Load message history
messageHistory.forEach((message) => {
const timeArray = _formatTimestamp(_getTimestamp());
message.timeString = timeArray[0];
message.dateString = timeArray[1];
_emitEvent({ type: "show_message", ...message });
});
for (message of messageHistory) {
messagePacket = { ...message }; // Create new variable
messagePacket = formatting.addTimeAndDateStringToPacket(messagePacket); // Add timestamp
messagePacket.message = await formatting.parseMessage(messagePacket.message, settings.enableEmbedding); // Parse the message for the UI
_emitEvent({ type: "show_message", ...messagePacket }); // Send message to UI
}
}
// Send current settings to the app
_emitEvent({ type: "initial_settings", settings: settings });
_emitEvent({ type: "initial_settings", settings: settings }); // Send current settings to the app
}
function _saveSettings() {
console.log("Saving config");
console.log("Saving settings: ", jstr(settings));
Settings.setValue("ArmoredChat-Config", settings);
}
function _getTimestamp(){
return Date.now();
function _notificationCoreMessage(displayName, message){
console.log("Sending notification to notificationCore:", `Display name: ${displayName}\n Message: ${message}`);
Messages.sendLocalMessage(
"Floof-Notif",
JSON.stringify({ sender: displayName, text: message })
);
}
function _formatTimestamp(timestamp){
let timeArray = [];
timeArray.push(new Date().toLocaleTimeString(undefined, {
hour12: false,
}));
timeArray.push(new Date(timestamp).toLocaleDateString(undefined, {
year: "numeric",
month: "long",
day: "numeric",
}));
return timeArray;
}
/**
* Emit a packet to the HTML front end. Easy communication!
* @param {Object} packet - The Object packet to emit to the HTML
* @param {("show_message"|"clear_messages"|"notification"|"initial_settings")} packet.type - The type of packet it is
*/
function _emitEvent(packet = { type: "" }) {
if (packet.type == `show_message`) {
// Don't show the message contents, this is a courtesy to prevent message leakage in the logs.
let strippedPacket = {...packet};
delete strippedPacket.message
console.log("Sending packet to QML interface", jstr(strippedPacket));
}
else {
console.log("Sending packet to QML interface", jstr(packet));
}
chatOverlayWindow.sendToQml(packet);
}
//
// Floofchat compatibility functions
// Added to ease the transition between Floofchat to ArmoredChat
// These functions can be safely removed at a much later date.
function floofChatCompatibilityConversion(message) {
if (message.type === "TransmitChatMessage" && !message.forApp) {
return {
position: message.position,
message: message.message,
displayName: message.displayName,
channel: message.channel.toLowerCase(),
};
}
return message;
}
function floofChatCompatibilitySendMessage(message, channel) {
Messages.sendMessage(
"Chat",
JSON.stringify({
position: MyAvatar.position,
message: message,
displayName: MyAvatar.sessionDisplayName,
channel: channel.charAt(0).toUpperCase() + channel.slice(1),
type: "TransmitChatMessage",
forApp: "Floof",
})
);
}
// Debug and developer functions and data
const jstr = (object) => JSON.stringify(object, null, 4); // JSON Stringify function with formatting
})();

View file

@ -2,14 +2,16 @@ import QtQuick 2.7
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.3
import controlsUit 1.0 as HifiControlsUit
import "./qml_widgets"
Rectangle {
color: Qt.rgba(0.1,0.1,0.1,1)
signal sendToScript(var message);
property string pageVal: "local"
property string last_message_user: ""
property date last_message_time: new Date()
property string pageVal: "local";
property date last_message_time: new Date();
property bool initialized: false;
// When the window is created on the script side, the window starts open.
// Once the QML window is created wait, then send the initialized signal.
@ -162,18 +164,14 @@ Rectangle {
model: getChannel(pageVal)
delegate: Loader {
property int delegateIndex: model.index
property string delegateText: model.text
property var delegateText: model.message
property string delegateUsername: model.username
property string delegateDate: model.date
sourceComponent: {
if (model.type === "chat") {
return template_chat_message;
} else if (model.type === "notification") {
return template_notification;
}
if (model.type === "chat") return template_chat_message;
if (model.type === "notification") return template_notification;
}
}
}
}
@ -191,7 +189,6 @@ Rectangle {
}
}
ListModel {
id: local
}
@ -373,123 +370,60 @@ Rectangle {
}
}
}
// Switch to internal on VR Mode
Rectangle {
width: parent.width
height: 40
color: "transparent"
Text {
text: "Force Virtual window in VR"
color: "white"
font.pointSize: 12
anchors.verticalCenter: parent.verticalCenter
}
CheckBox {
id: s_force_vw_in_vr
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
onCheckedChanged: {
toScript({type: 'setting_change', setting: 'switchToInternalOnHeadsetUsed', value: checked})
}
}
}
// Toggle media embedding
Rectangle {
width: parent.width
height: 40
color: "transparent"
Text {
text: "Enable media embedding"
color: "white"
font.pointSize: 12
anchors.verticalCenter: parent.verticalCenter
}
CheckBox {
id: s_enable_embedding
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
onCheckedChanged: {
toScript({type: 'setting_change', setting: 'enableEmbedding', value: checked})
}
}
}
}
}
}
// Templates
Component {
id: template_chat_message
Rectangle {
property int index: delegateIndex
property string texttest: delegateText
property string username: delegateUsername
property string date: delegateDate
height: Math.max(65, children[1].height + 30)
color: index % 2 === 0 ? "transparent" : Qt.rgba(0.15,0.15,0.15,1)
width: listview.parent.parent.width
Layout.fillWidth: true
Item {
width: parent.width - 10
anchors.horizontalCenter: parent.horizontalCenter
height: 22
Text{
text: username
color: "lightgray"
}
Text{
anchors.right: parent.right
text: date
color: "lightgray"
}
}
TextEdit {
anchors.top: parent.children[0].bottom
x: 5
text: texttest
color:"white"
font.pointSize: 12
readOnly: true
selectByMouse: true
selectByKeyboard: true
width: parent.width * 0.8
height: contentHeight
wrapMode: Text.Wrap
textFormat: TextEdit.RichText
onLinkActivated: {
Window.openWebBrowser(link)
}
}
}
}
Component {
id: template_notification
Rectangle{
property int index: delegateIndex
property string texttest: delegateText
property string username: delegateUsername
property string date: delegateDate
color: "#171717"
width: parent.width
height: 40
Item {
width: 10
height: parent.height
Rectangle {
height: parent.height
width: 5
color: "#505186"
}
}
Item {
width: parent.width - parent.children[0].width - 5
height: parent.height
anchors.left: parent.children[0].right
TextEdit{
text: texttest
color:"white"
font.pointSize: 12
readOnly: true
width: parent.width * 0.8
selectByMouse: true
selectByKeyboard: true
height: parent.height
wrapMode: Text.Wrap
verticalAlignment: Text.AlignVCenter
font.italic: true
}
Text {
text: date
color:"white"
font.pointSize: 12
anchors.right: parent.right
height: parent.height
wrapMode: Text.Wrap
horizontalAlignment: Text.AlignRight
verticalAlignment: Text.AlignVCenter
font.italic: true
}
}
}
}
TemplateChatMessage { id: template_chat_message }
TemplateNotification { id: template_notification }
property var channels: {
"local": local,
@ -497,56 +431,32 @@ Rectangle {
}
function scrollToBottom(bypassDistanceCheck = false, extraMoveDistance = 0) {
const totalHeight = listview.height; // Total height of the content
const currentPosition = messageViewFlickable.contentY; // Current position of the view
const windowHeight = listview.parent.parent.height; // Total height of the window
const totalHeight = listview.height; // Total height of the content
const currentPosition = messageViewFlickable.contentY; // Current position of the view
const windowHeight = listview.parent.parent.height; // Total height of the window
const bottomPosition = currentPosition + windowHeight;
// Check if the view is within 300 units from the bottom
const closeEnoughToBottom = totalHeight - bottomPosition <= 300;
if (!bypassDistanceCheck && !closeEnoughToBottom) return;
if (totalHeight < windowHeight) return; // No reason to scroll, we don't have an overflow.
if (bottomPosition == totalHeight) return; // At the bottom, do nothing.
if (totalHeight < windowHeight) return; // No reason to scroll, we don't have an overflow.
if (bottomPosition == totalHeight) return; // At the bottom, do nothing.
messageViewFlickable.contentY = listview.height - listview.parent.parent.height;
messageViewFlickable.returnToBounds();
}
function addMessage(username, message, date, channel, type){
channel = getChannel(channel)
// Format content
message = formatContent(message);
message = embedImages(message);
if (type === "notification"){
channel.append({ text: message, date: date, type: "notification" });
last_message_user = "";
channel.append({ message: message, date: date, type: "notification" });
scrollToBottom(null, 30);
last_message_time = new Date();
return;
}
var current_time = new Date();
var elapsed_time = current_time - last_message_time;
var elapsed_minutes = elapsed_time / (1000 * 60);
var last_item_index = channel.count - 1;
var last_item = channel.get(last_item_index);
if (last_message_user === username && elapsed_minutes < 1 && last_item){
message = "<br>" + message
last_item.text = last_item.text += "\n" + message;
load_scroll_timer.running = true;
last_message_time = new Date();
return;
}
last_message_user = username;
last_message_time = new Date();
channel.append({ text: message, username: username, date: date, type: type });
channel.append({ message: message, username: username, date: date, type: type });
load_scroll_timer.running = true;
}
@ -554,37 +464,6 @@ Rectangle {
return channels[id];
}
function formatContent(mess) {
var arrow = /\</gi
mess = mess.replace(arrow, "&lt;");
var link = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g;
mess = mess.replace(link, (match) => {return `<a style="color:#4EBAFD" onclick='Window.openUrl("+match+")' href='` + match + `'>` + match + `</a> <a onclick='Window.openUrl(`+match+`)'>🗗</a>`});
var newline = /\n/gi;
mess = mess.replace(newline, "<br>");
return mess
}
function embedImages(mess){
var image_link = /(https?:(\/){2})[\w.-]+(?:\.[\w\.-]+)+(?:\/[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]*)(?:png|jpe?g|gif|bmp|svg|webp)/g;
var matches = mess.match(image_link);
var new_message = ""
var listed = []
var total_emeds = 0
new_message += mess
for (var i = 0; matches && matches.length > i && total_emeds < 3; i++){
if (!listed.includes(matches[i])) {
new_message += "<br><img src="+ matches[i] +" width='250' >"
listed.push(matches[i]);
total_emeds++
}
}
return new_message;
}
// Messages from script
function fromScript(message) {
@ -600,15 +479,23 @@ Rectangle {
domain.clear();
break;
case "initial_settings":
print(`Got settings:\n ${JSON.stringify(message.settings, null, 4)}`);
if (message.settings.external_window) s_external_window.checked = true;
if (message.settings.maximum_messages) s_maximum_messages.value = message.settings.maximum_messages;
if (message.settings.join_notification) s_join_notification.checked = true;
if (message.settings.switchToInternalOnHeadsetUsed) s_force_vw_in_vr.checked = true;
if (message.settings.enableEmbedding) s_enable_embedding.checked = true;
initialized = true; // Application is ready
break;
}
}
// Send message to script
function toScript(packet){
if (packet.type === "setting_change" && !initialized) return; // Don't announce a change in settings if not ready
sendToScript(packet)
}
}

View file

@ -0,0 +1,167 @@
//
// formatting.js
//
// Created by Armored Dragon, 2024.
// Copyright 2024 Overte e.V.
//
// This just does some basic formatting and minor housekeeping for the domainChat.js application
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
const formatting = {
toJSON: function(data) {
if (typeof data == "object") return data; // Already JSON
try {
const parsedData = JSON.parse(data);
return parsedData;
} catch (e) {
console.log('Failed to convert data to JSON.')
return null; // Could not convert to json, some error;
}
},
addTimeAndDateStringToPacket: function(packet) {
// Gets the current time and adds it to a given packet
const timeArray = formatting.helpers._timestampArray(packet.timestamp);
packet.timeString = timeArray[0];
packet.dateString = timeArray[1];
return packet;
},
trimPacketToSave: function(packet) {
// Takes a packet, and returns a packet containing only what is needed to save.
let newPacket = {
channel: packet.channel || "",
displayName: packet.displayName || "",
message: packet.message || "",
timestamp: packet.timestamp || formatting.helpers.getTimestamp(),
};
return newPacket;
},
parseMessage: async function(message, enableEmbedding) {
const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;
const overteLocationRegex = /hifi:\/\/[a-zA-Z0-9_-]+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+/;
let runningMessage = message; // The remaining message that will be parsed
let messageArray = []; // An array of messages that are split up by the formatting functions
const regexPatterns = [
{ type: "url", regex: urlRegex },
{ type: "overteLocation", regex: overteLocationRegex }
]
while (true) {
let firstMatch = _findFirstMatch();
if (firstMatch == null) {
// If there is no more text to parse, break out of the loop and return the message array.
// Format any remaining text as a basic 'text' type.
if (runningMessage.trim() != "") messageArray.push({type: 'text', value: runningMessage});
// Append a final 'fill width' to the message text.
messageArray.push({type: 'messageEnd'});
break;
}
_formatMessage(firstMatch);
}
// Embed images in the message array.
if (enableEmbedding) {
for (dataChunk of messageArray){
if (dataChunk.type == 'url'){
let url = dataChunk.value;
const res = await formatting.helpers.fetch(url, {method: 'GET'}); // TODO: Replace with 'HEAD' method. https://github.com/overte-org/overte/issues/1273
const contentType = res.getResponseHeader("content-type");
if (contentType.startsWith('image/')) {
messageArray.push({type: 'imageEmbed', value: url});
continue;
}
if (contentType.startsWith('video/')){
messageArray.push({type: 'videoEmbed', value: url});
continue;
}
}
}
}
return messageArray;
function _formatMessage(firstMatch){
let indexOfFirstMatch = firstMatch[0];
let regex = regexPatterns[firstMatch[1]].regex;
let foundMatch = runningMessage.match(regex)[0];
if (runningMessage.substring(0, indexOfFirstMatch) != "") messageArray.push({type: 'text', value: runningMessage.substring(0, indexOfFirstMatch)});
messageArray.push({type: regexPatterns[firstMatch[1]].type, value: runningMessage.substring(indexOfFirstMatch, indexOfFirstMatch + foundMatch.length)});
runningMessage = runningMessage.substring(indexOfFirstMatch + foundMatch.length); // Remove the part of the message we have worked with
}
function _findFirstMatch(){
let indexOfFirstMatch = Infinity;
let indexOfRegexPattern = Infinity;
for (let i = 0; regexPatterns.length > i; i++){
let indexOfMatch = runningMessage.search(regexPatterns[i].regex);
if (indexOfMatch == -1) continue; // No match found
if (indexOfMatch < indexOfFirstMatch) {
indexOfFirstMatch = indexOfMatch;
indexOfRegexPattern = i;
}
}
if (indexOfFirstMatch !== Infinity) return [indexOfFirstMatch, indexOfRegexPattern]; // If there was a found match
return null; // No found match
}
},
helpers: {
// Small functions that are used often in the other functions.
_timestampArray: function(timestamp) {
const currentDate = timestamp || formatting.helpers.getTimestamp();
let timeArray = [];
timeArray.push(new Date(currentDate).toLocaleTimeString(undefined, {
hour12: false,
}));
timeArray.push(new Date(currentDate).toLocaleDateString(undefined, {
year: "numeric",
month: "long",
day: "numeric",
}));
return timeArray;
},
getTimestamp: function(){
return Date.now();
},
fetch: function (url, options = {method: "GET"}) {
return new Promise((resolve, reject) => {
let req = new XMLHttpRequest();
req.onreadystatechange = function () {
if (req.readyState === req.DONE) {
if (req.status === 200) {
resolve(req);
} else {
console.log("Error", req.status, req.statusText);
reject();
}
}
};
req.open(options.method, url);
req.send();
});
}
}
}

View file

Before

Width:  |  Height:  |  Size: 400 B

After

Width:  |  Height:  |  Size: 400 B

View file

Before

Width:  |  Height:  |  Size: 778 B

After

Width:  |  Height:  |  Size: 778 B

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

Before

Width:  |  Height:  |  Size: 4 KiB

After

Width:  |  Height:  |  Size: 4 KiB

View file

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View file

@ -0,0 +1,187 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.3
Component {
id: template_chat_message
Rectangle {
property int index: delegateIndex
height: Math.max(65, children[1].height + 30)
color: index % 2 === 0 ? "transparent" : Qt.rgba(0.15,0.15,0.15,1)
width: listview.parent.parent.width
Layout.fillWidth: true
Item {
width: parent.width - 10
anchors.horizontalCenter: parent.horizontalCenter
height: 22
TextEdit {
text: delegateUsername;
color: "lightgray";
readOnly: true;
selectByMouse: true;
selectByKeyboard: true;
}
Text {
anchors.right: parent.right;
text: delegateDate;
color: "lightgray";
}
}
Flow {
anchors.top: parent.children[0].bottom;
width: parent.width;
x: 5
id: messageBoxFlow
Repeater {
model: delegateText;
Item {
width: parent.width;
height: children[0].contentHeight;
TextEdit {
text: model.value || ""
font.pointSize: 12
wrapMode: TextEdit.WordWrap
width: parent.width * 0.8
visible: model.type === 'text' || model.type === 'mention';
readOnly: true
selectByMouse: true
selectByKeyboard: true
color: {
switch (model.type) {
case "mention":
return "purple";
default:
return "white";
}
}
}
Flow {
width: parent.width * 0.8;
height: 20
visible: model.type === 'url';
TextEdit {
id: urlTypeTextDisplay;
text: model.value || "";
font.pointSize: 12;
wrapMode: Text.Wrap;
color: "#4EBAFD";
font.underline: true;
readOnly: true
selectByMouse: true
selectByKeyboard: true
width: Math.min(parent.width - 20, textMetrics.tightBoundingRect.width) ;
MouseArea {
anchors.fill: parent;
onClicked: {
Window.openWebBrowser(model.value);
}
}
}
TextMetrics {
id: textMetrics
font: urlTypeTextDisplay.font
text: urlTypeTextDisplay.text
}
Text {
width: 20;
text: "🗗";
font.pointSize: 10;
color: "white";
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
MouseArea {
anchors.fill: parent;
onClicked: {
Qt.openUrlExternally(model.value);
}
}
}
}
RowLayout {
visible: model.type === 'overteLocation';
width: Math.min(messageBoxFlow.width, children[0].children[1].contentWidth + 35);
height: 20;
Layout.leftMargin: 5
Layout.rightMargin: 5
Rectangle {
width: parent.width;
height: 20;
color: "lightgray"
radius: 2;
Image {
source: "../img/ui/world_black.png"
width: 18;
height: 18;
sourceSize.width: 18
sourceSize.height: 18
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 2
anchors.rightMargin: 10
}
TextEdit {
text: model.type === 'overteLocation' ? model.value.split('hifi://')[1].split('/')[0] : '';
color: "black"
font.pointSize: 12
x: parent.children[0].width + 5;
anchors.verticalCenter: parent.verticalCenter
readOnly: true
selectByMouse: true
selectByKeyboard: true
}
MouseArea {
anchors.fill: parent;
onClicked: {
Window.openUrl(model.value);
}
}
}
}
Item {
Layout.fillWidth: true;
visible: model.type === 'messageEnd';
}
Item {
visible: model.type === 'imageEmbed';
width: messageBoxFlow.width;
height: 200
AnimatedImage {
source: model.type === 'imageEmbed' ? model.value : ''
height: Math.min(sourceSize.height, 200);
fillMode: Image.PreserveAspectFit
}
}
}
}
}
}
}

View file

@ -0,0 +1,41 @@
import QtQuick 2.7
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.3
Component {
id: template_notification
Rectangle {
color: "#171717"
width: parent.width
height: 40
RowLayout {
width: parent.width
height: parent.height
Rectangle {
height: parent.height
width: 5
color: "#505186"
}
Repeater {
model: delegateText
TextEdit {
visible: model.value != undefined;
text: model.value || ""
color: "white"
font.pointSize: 12
readOnly: true
selectByMouse: true
selectByKeyboard: true
height: root.height
wrapMode: Text.Wrap
font.italic: true
}
}
}
}
}