Merge pull request #961 from Armored-Dragon/ArmoredChat

Replace Floofchat with ArmoredChat
This commit is contained in:
ksuprynowicz 2024-07-11 21:27:36 +02:00 committed by GitHub
commit 1d1a680678
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 1220 additions and 4 deletions

View file

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View file

@ -4,7 +4,7 @@
//
// By Don Hopkins (dhopkins@donhopkins.com) on May 5th, 2017
// Copyright 2017 High Fidelity, Inc.
// Copyright 2023 Overte e.V.
// Copyright 2024 Overte e.V.
//
//
// Distributed under the Apache License, Version 2.0.
@ -13,7 +13,7 @@
(function() {
var webPageURL = Script.resolvePath("html/ChatPage.html"); // URL of tablet web page.
var webPageURL = Script.resolvePath("ChatPage.html"); // URL of tablet web page.
var randomizeWebPageURL = true; // Set to true for debugging.
var lastWebPageURL = ""; // Last random URL of tablet web page.
var onChatPage = false; // True when chat web page is opened.

View file

@ -0,0 +1,205 @@
# Armored Chat
1. What is Armored Chat
2. User manual
- Installation
- Settings
- Usability tips
3. Development
## What is Armored 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.
### Dependencies
AC uses the Overte [Messages](https://apidocs.overte.org/Messages.html) API to communicate.
For notifications, AC uses [notificationCore.js](https://github.com/overte-org/overte/blob/bb8bac43eadd3b20956a2ff7b0b21c28844b0f77/scripts/communityScripts/notificationCore/notificationCore.js).
## User manual
### Installation
Armored 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:
```
https://raw.githubusercontent.com/overte-org/overte/master/scripts/communityScripts/armored-chat/armored_chat.js
```
---
### Settings
Armored Chat comes with basic settings for managing itself.
#### External window
This boolean setting toggles whether AC will be a in-game overlay window, or whether AC will be a external floating window.
Default is `false`.
#### Maximum saved messages
This integer represents the amount of messages to save in the AC history. More messages may be present if AC is left on long enough. This setting only sets the number of saved messages and not the maximum amount of messages that can be viewed at any time.
This means if you set the value to `5`, your history will save a maximum of 5 messages, however you will still be able to see a longer history in the session should you receive more. Once AC completely closes and fetches your message history as it initializes, you will only see the last 5 messages.
Default value is `200`
#### Erase chat history
This action immediately clears the AC history and the session. Functionally this will set the message list to a empty Array.
### Usage
AC has two chat modes: Local, and Domain. Local chat displays all other local chat messages that are within 20 units of you. Domain chat will display all other Domain messages sent though that channel regardless of distance.
AC also handles link embedding. When you send an HTTP(S) link, it will automatically parse it using Qt RichText and allow everyone to click on the message. Next to the link you will also see a "⮺" symbol. Clicking on this symbol will open the link in an external window.
### Usability tips
#### Navigation
You can scroll quickly using kinetic scrolling! Try "grabbing" the right side of messages, where the timestamp is, and flinging yourself in a direction.
#### Formatting
You can format messages using basic HTML elements. Try `<div style="color: red"> Red text! </div>` to color your text red.
Find the full list of Qt rich text tags [here](https://doc.qt.io/qt-6/richtext-html-subset.html). Please note that some of these tags may be intentionally restricted.
#### Media embedding
Images can be embedded when linked directly.
Try it out by linking to the Overte logo! `https://github.com/overte-org/overte/raw/master/interface/resources/images/brand-banner.svg`
In order for images to be embedded, URLs must end in a image filetype.
Supported filetypes are:
- `.png`
- `.jpg`
- `.jpeg`
- `.gif`
- `.bmp`
- `.svg`
- `.webp`
## Development
### To QML communication
Here are the signals needed to communicate from the JavaScript core to the QML interface.
AC calls a `_emitEvent()` function that also includes a `type` key in the object. This `type` tells the QML and/or the JS core what the packet is for.
When you call the `_emitEvent()` function be sure to include the following signals as a `type`. In the examples below, the `type` is being excluded for brevity.
Example:
```json
{ type: "show_message", displayName: "username", ...}
```
#### "show_message"
This signal tells the QML to add a new message to the ListView element list.
Supply a `JSON` object.
```json
{
"displayName": "username",
"message": "chat message",
"channel": "domain", // Channel to send message on. By default it should only be "domain" or "local".
"date": "[ time and date string ]" // Optional, defaults to current time and date.
}
```
#### "clear_messages"
Clear all messages displayed in the ListView elements. Note this does not clear the history and this is only a visual erasure.
No payload required.
#### "notification"
Renders a notification to the domain channel.
The intended use is to provide updates about the domain and make the notifications accessible.
Supply a `JSON` object.
```json
{
"message": "notification message" // Notification to render
}
```
#### "initial_settings"
Visually set the settings in the QML interface based on the supplied object.
Supply a `JSON` object.
```json
{
"settings": {
// JSON object of current AC settings
"external_window": false,
"maximum_messages": 200
}
}
```
### To JS communication
Here are the signals needed to communicate from the QML interface to the JavaScript core. AC is developed in a way that all actions that are not style related are preformed though the JavaScript core.
This means that what ever action you want to preform must go though the JavaScript core for processing.
This is formatted the same was as the communication packets to the QML interface. Supply the following entries as "type"s in your packet.
#### "send_message"
Tell AC to broadcast a message to the domain.
Supply a `JSON` object.
```json
{
"message": "message content", // The contents of the message to send.
"channel": "domain" // Channel to emit the message to.
}
```
#### "setting_change"
Tell AC to change a setting. Exercise caution when using this as you can add new settings unintentionally if you are not careful.
Supply a `JSON` object
```json
{
"setting": "external_window", // The name of the setting to change
"value": true // The value to change the setting to
}
```
#### "action"
Tell AC to preform a generic action. This is normally reserved for functions that would get called on a button onClicked event in the QML.
Supply a `JSON` object
```json
{
"action": "erase_history" // The action to preform
}
```
#### "initialized"
Tell AC the QML overlay has loaded successfully.
This is called to hide the overlay on creation.
No payload required.

View file

@ -0,0 +1,291 @@
//
// armored_chat.js
//
// Created by Armored Dragon, 2024.
// Copyright 2024 Overte e.V.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
(() => {
("use strict");
var appIsVisible = false;
var settings = {
external_window: false,
maximum_messages: 200,
};
// Global vars
var tablet;
var chatOverlayWindow;
var appButton;
var quickMessage;
const channels = ["domain", "local"];
var messageHistory = Settings.getValue("ArmoredChat-Messages", []) || [];
var maxLocalDistance = 20; // Maximum range for the local chat
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);
});
startup();
function startup() {
tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
appButton = tablet.addButton({
icon: Script.resolvePath("./img/icon_white.png"),
activeIcon: Script.resolvePath("./img/icon_black.png"),
text: "CHAT",
isActive: appIsVisible,
});
// When script ends, remove itself from tablet
Script.scriptEnding.connect(function () {
console.log("Shutting Down");
tablet.removeButton(appButton);
chatOverlayWindow.close();
});
// Overlay button toggle
appButton.clicked.connect(toggleMainChatWindow);
quickMessage = new OverlayWindow({
source: Script.resolvePath("./armored_chat_quick_message.qml"),
});
_openWindow();
}
function toggleMainChatWindow() {
appIsVisible = !appIsVisible;
appButton.editProperties({ isActive: appIsVisible });
chatOverlayWindow.visible = appIsVisible;
// External window was closed; the window does not exist anymore
if (chatOverlayWindow.title == "" && appIsVisible) {
_openWindow();
}
}
function _openWindow() {
chatOverlayWindow = new Desktop.createWindow(
Script.resolvePath("./armored_chat.qml"),
{
title: "Chat",
size: { x: 550, y: 400 },
additionalFlags: Desktop.ALWAYS_ON_TOP,
visible: appIsVisible,
presentationMode: Desktop.PresentationMode.VIRTUAL,
}
);
chatOverlayWindow.closed.connect(toggleMainChatWindow);
chatOverlayWindow.fromQml.connect(fromQML);
quickMessage.fromQml.connect(fromQML);
}
function receivedMessage(channel, message) {
// Is the message a chat message?
channel = channel.toLowerCase();
if (channel !== "chat") return;
message = JSON.parse(message);
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(); // Make sure the "local", "domain", etc. is formatted consistently
if (!channels.includes(message.channel)) return; // Check the channel
if (
message.channel == "local" &&
Vec3.distance(MyAvatar.position, message.position) >
maxLocalDistance
)
return; // If message is local, and if player is too far away from location, don't do anything
// Update qml view of to new message
_emitEvent({ type: "show_message", ...message });
Messages.sendLocalMessage(
"Floof-Notif",
JSON.stringify({
sender: message.displayName,
text: message.message,
})
);
// Save message to history
let savedMessage = message;
delete savedMessage.position;
savedMessage.timeString = new Date().toLocaleTimeString(undefined, {
hour12: false,
});
savedMessage.dateString = new Date().toLocaleDateString(undefined, {
year: "numeric",
month: "long",
day: "numeric",
});
messageHistory.push(savedMessage);
while (messageHistory.length > settings.maximum_messages) {
messageHistory.shift();
}
Settings.setValue("ArmoredChat-Messages", messageHistory);
}
function fromQML(event) {
switch (event.type) {
case "send_message":
_sendMessage(event.message, event.channel);
break;
case "setting_change":
settings[event.setting] = event.value; // Update local settings
_saveSettings(); // Save local settings
switch (event.setting) {
case "external_window":
chatOverlayWindow.presentationMode = event.value
? Desktop.PresentationMode.NATIVE
: Desktop.PresentationMode.VIRTUAL;
break;
case "maximum_messages":
// Do nothing
break;
}
break;
case "action":
switch (event.action) {
case "erase_history":
Settings.setValue("ArmoredChat-Messages", []);
_emitEvent({
type: "clear_messages",
});
break;
}
break;
case "initialized":
// https://github.com/overte-org/overte/issues/824
chatOverlayWindow.visible = appIsVisible; // The "visible" field in the Desktop.createWindow does not seem to work. Force set it to the initial state (false)
_loadSettings();
break;
}
}
function keyPressEvent(event) {
switch (JSON.stringify(event.key)) {
case "16777220": // Enter key
if (HMD.active) return; // Don't allow in VR
quickMessage.sendToQml({
type: "change_visibility",
value: true,
});
}
}
function _sendMessage(message, channel) {
if (message.length == 0) return;
Messages.sendMessage(
"chat",
JSON.stringify({
position: MyAvatar.position,
message: message,
displayName: MyAvatar.sessionDisplayName,
channel: channel,
action: "send_chat_message",
})
);
floofChatCompatibilitySendMessage(message, channel);
}
function _avatarAction(type, sessionId) {
Script.setTimeout(() => {
if (type == "connected") {
palData = AvatarManager.getPalData().data;
}
// Get the display name of the user
let displayName = "";
displayName =
AvatarManager.getPalData([sessionId])?.data[0]
?.sessionDisplayName || null;
if (displayName == null) {
for (let i = 0; i < palData.length; i++) {
if (palData[i].sessionUUID == sessionId) {
displayName = palData[i].sessionDisplayName;
}
}
}
// Format the packet
let message = {};
message.message = `${displayName} ${type}`;
_emitEvent({ type: "notification", ...message });
}, 1500);
}
function _loadSettings() {
settings = Settings.getValue("ArmoredChat-Config", settings);
if (messageHistory) {
// Load message history
messageHistory.forEach((message) => {
delete message.action;
_emitEvent({ type: "show_message", ...message });
});
}
// Send current settings to the app
_emitEvent({ type: "initial_settings", settings: settings });
}
function _saveSettings() {
console.log("Saving config");
Settings.setValue("ArmoredChat-Config", settings);
}
/**
* 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: "" }) {
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",
})
);
}
})();

View file

@ -0,0 +1,566 @@
import QtQuick 2.7
import QtQuick.Controls 2.0
import QtQuick.Layouts 1.3
import controlsUit 1.0 as HifiControlsUit
Rectangle {
color: Qt.rgba(0.1,0.1,0.1,1)
signal sendToScript(var message);
property string pageVal: "local"
property string last_message_user: ""
property date last_message_time: new Date()
// When the window is created on the script side, the window starts open.
// Once the QML window is created wait, then send the initialized signal.
// This signal is mostly used to close the "Desktop overlay window" script side
// https://github.com/overte-org/overte/issues/824
Timer {
interval: 10
running: true
repeat: false
onTriggered: {
toScript({type: "initialized"});
load_scroll_timer.running = true
}
}
Timer {
id: load_scroll_timer
interval: 100
running: false
repeat: false
onTriggered: {
scrollToBottom();
}
}
// User view
Item {
anchors.fill: parent
// Navigation Bar
Rectangle {
id: navigation_bar
width: parent.width
height: 40
color:Qt.rgba(0,0,0,1)
Item {
height: parent.height
width: parent.width
anchors.fill: parent
Rectangle {
width: pageVal === "local" ? 100 : 60
height: parent.height
color: pageVal === "local" ? "#505186" : "white"
id: local_page
Image {
source: "./img/ui/" + (pageVal === "local" ? "social_white.png" : "social_black.png")
sourceSize.width: 40
sourceSize.height: 40
anchors.centerIn: parent
}
Behavior on width {
NumberAnimation {
duration: 50
}
}
MouseArea {
anchors.fill: parent
onClicked: {
pageVal = "local";
load_scroll_timer.running = true;
}
}
}
Rectangle {
width: pageVal === "domain" ? 100 : 60
height: parent.height
color: pageVal === "domain" ? "#505186" : "white"
anchors.left: local_page.right
anchors.leftMargin: 5
id: domain_page
Image {
source: "./img/ui/" + (pageVal === "domain" ? "world_white.png" : "world_black.png")
sourceSize.width: 30
sourceSize.height: 30
anchors.centerIn: parent
}
Behavior on width {
NumberAnimation {
duration: 50
}
}
MouseArea {
anchors.fill: parent
onClicked: {
pageVal = "domain"
load_scroll_timer.running = true;
}
}
}
Rectangle {
width: pageVal === "settings" ? 100 : 60
height: parent.height
color: pageVal === "settings" ? "#505186" : "white"
anchors.right: parent.right
id: settings_page
Image {
source: "./img/ui/" + (pageVal === "settings" ? "settings_white.png" : "settings_black.png")
sourceSize.width: 30
sourceSize.height: 30
anchors.centerIn: parent
}
Behavior on width {
NumberAnimation {
duration: 50
}
}
MouseArea {
anchors.fill: parent
onClicked: {
pageVal = "settings"
}
}
}
}
}
// Pages
Item {
width: parent.width
height: parent.height - 40
anchors.top: navigation_bar.bottom
visible: ["local", "domain"].includes(pageVal) ? true : false
// Chat Message History
ListView {
width: parent.width
height: parent.height - 40
clip: true
interactive: true
spacing: 5
id: listview
delegate: Loader {
property int delegateIndex: index
property string delegateText: model.text
property string delegateUsername: model.username
property string delegateDate: model.date
width: listview.width
sourceComponent: {
if (model.type === "chat") {
return template_chat_message;
} else if (model.type === "notification") {
return template_notification;
}
}
}
ScrollBar.vertical: ScrollBar {
id: chat_scrollbar
height: 100
size: 0.05
}
model: getChannel(pageVal)
}
ListModel {
id: local
}
ListModel {
id: domain
}
// Chat Entry
Rectangle {
width: parent.width
height: 40
color: Qt.rgba(0.9,0.9,0.9,1)
anchors.bottom: parent.bottom
Row {
width: parent.width
height: parent.height
TextField {
width: parent.width - 60
height: parent.height
placeholderText: pageVal.charAt(0).toUpperCase() + pageVal.slice(1) + " chat message..."
clip: false
Keys.onPressed: {
if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && !(event.modifiers & Qt.ShiftModifier)) {
event.accepted = true;
toScript({type: "send_message", message: text, channel: pageVal});
text = ""
}
}
onFocusChanged: {
if (!HMD.active) return;
if (focus) return ApplicationInterface.showVRKeyboardForHudUI(true);
ApplicationInterface.showVRKeyboardForHudUI(false);
}
}
Button {
width: 60
height:parent.height
Image {
source: "./img/ui/send_black.png"
sourceSize.width: 30
sourceSize.height: 30
anchors.centerIn: parent
}
onClicked: {
toScript({type: "send_message", message: parent.children[0].text, channel: pageVal});
parent.children[0].text = ""
}
Keys.onPressed: {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
toScript({type: "send_message", message: parent.children[0].text, channel: pageVal});
parent.children[0].text = ""
}
}
}
}
}
}
Item {
width: parent.width
height: parent.height - 40
anchors.top: navigation_bar.bottom
visible: ["local", "domain"].includes(pageVal) ? false : true
Column {
width: parent.width - 10
height: parent.height - 10
anchors.centerIn: parent
spacing: 10
// External Window
Rectangle {
width: parent.width
height: 40
color: "transparent"
Text{
text: "External window"
color: "white"
font.pointSize: 12
anchors.verticalCenter: parent.verticalCenter
}
CheckBox{
id: s_external_window
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
onCheckedChanged: {
toScript({type: 'setting_change', setting: 'external_window', value: checked})
}
}
}
// Maximum saved messages
Rectangle {
width: parent.width
height: 40
color: "transparent"
Text{
text: "Maximum saved messages"
color: "white"
font.pointSize: 12
anchors.verticalCenter: parent.verticalCenter
}
HifiControlsUit.SpinBox {
id: s_maximum_messages
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
decimals: 0
width: 100
height: parent.height
realFrom: 1
realTo: 1000
backgroundColor: "#cccccc"
onValueChanged: {
toScript({type: 'setting_change', setting: 'maximum_messages', value: value})
}
}
}
// Erase History
Rectangle {
width: parent.width
height: 40
color: Qt.rgba(0.15,0.15,0.15,1);
Text{
text: "Erase chat history"
color: "white"
font.pointSize: 12
anchors.verticalCenter: parent.verticalCenter
}
Button {
anchors.right: parent.right
text: "Erase"
height: parent.height
anchors.verticalCenter: parent.verticalCenter
onClicked: {
toScript({type: "action", action: "erase_history"})
}
}
}
}
}
}
// 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)
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
}
}
}
}
property var channels: {
"local": local,
"domain": domain,
}
function scrollToBottom() {
if (listview.count == 0) return;
listview.positionViewAtEnd();
}
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 = "";
scrollToBottom();
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;
scrollToBottom()
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 });
scrollToBottom();
}
function getChannel(id) {
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 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) {
let time = new Date().toLocaleTimeString(undefined, { hour12: false });
let date = new Date().toLocaleDateString(undefined, { year: "numeric", month: "long", day: "numeric", });
switch (message.type){
case "show_message":
addMessage(message.displayName, message.message, `[ ${message.timeString || time} - ${message.dateString || date} ]`, message.channel, "chat");
break;
case "notification":
addMessage("SYSTEM", message.message, `[ ${time} - ${date} ]`, "domain", "notification");
break;
case "clear_messages":
local.clear();
domain.clear();
break;
case "initial_settings":
if (message.settings.external_window) s_external_window.checked = true;
if (message.settings.maximum_messages) s_maximum_messages.value = message.settings.maximum_messages;
break;
}
}
// Send message to script
function toScript(packet){
sendToScript(packet)
}
}

View file

@ -0,0 +1,112 @@
import QtQuick 2.5
import QtQuick.Controls 1.4
Rectangle {
id: root
property var window
Binding { target: root; property:'window'; value: parent.parent; when: Boolean(parent.parent) }
Binding { target: window; property: 'shown'; value: false; when: Boolean(window) }
Component.onDestruction: chat_bar && chat_bar.destroy()
property alias chat_bar: chat_bar
Rectangle {
id: chat_bar
parent: desktop
x: 0
y: parent.height - height
width: parent.width
height: 50
z: 99
visible: false
TextArea {
id: textArea
x: 0
width: parent.width
height: parent.height
text:""
textColor: "#ffffff"
clip: false
font.pointSize: 18
Keys.onReturnPressed: { _onEnterPressed(); }
Keys.onEnterPressed: { _onEnterPressed(); }
Keys.onLeftPressed: { moveLeft(); }
Keys.onRightPressed: { moveRight(); }
function moveLeft(){
if (cursorPosition > 0){
cursorPosition--
}
}
function moveRight(){
if (cursorPosition < text.length){
cursorPosition++
}
}
}
Text {
text: "Local message..."
font.pointSize: 16
color: "gray"
x: 0
width: parent.width
anchors.verticalCenter: parent.verticalCenter
visible: textArea.text == ""
}
Button {
id: button
x: parent.width - width
y: 0
width: 64
height: parent.height
clip: false
visible: true
Image {
id: image
width: 30
height: 30
fillMode: Image.PreserveAspectFit
visible: true
anchors.centerIn: parent
source: "./img/ui/send_white.png"
}
onClicked: {
_onEnterPressed();
}
}
}
function _onEnterPressed() {
changeVisibility(false)
toScript({type: "send_message", message: textArea.text, channel: "local"})
textArea.text = "";
}
function changeVisibility(state){
chat_bar.visible = state
if (state) textArea.forceActiveFocus();
else root.parent.forceActiveFocus();
}
// Messages from script
function fromScript(message) {
switch (message.type){
case "change_visibility":
changeVisibility(message.value)
break;
}
}
// Send message to script
function toScript(packet){
sendToScript(packet)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 B

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="32"
viewBox="0 -960 760 640"
width="38"
version="1.1"
id="svg4"
sodipodi:docname="send.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
inkscape:export-filename="send_black.png"
inkscape:export-xdpi="300"
inkscape:export-ydpi="300"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<sodipodi:namedview
id="namedview6"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
showgrid="false"
inkscape:zoom="21.395833"
inkscape:cx="17.363194"
inkscape:cy="16.031159"
inkscape:window-width="2560"
inkscape:window-height="1366"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<path
d="m 0,-320 v -640 l 760,320 z M 60,-413 604,-640 60,-870 v 168 l 242,62 -242,60 z m 0,0 v -457 z"
id="path2"
style="fill:#000000" />
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View file

@ -7,7 +7,7 @@
//
// Copyright 2014 High Fidelity, Inc.
// Copyright 2020 Vircadia contributors.
// Copyright 2022 Overte e.V.
// Copyright 2024 Overte e.V.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
@ -47,7 +47,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/chat/FloofChat.js",
"communityScripts/armored-chat/armored_chat.js",
//"system/chat.js"
];