Merge 25a9976a7f
into b38237cb8d
|
@ -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"
|
||||
];
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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
|
||||
})();
|
|
@ -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, "<");
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
167
scripts/system/domainChat/formatting.js
Normal 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 400 B After Width: | Height: | Size: 400 B |
Before Width: | Height: | Size: 778 B After Width: | Height: | Size: 778 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 4 KiB After Width: | Height: | Size: 4 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
187
scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|