fix up setting, url handling, images, notif sound

This commit is contained in:
Ada 2025-06-23 15:28:11 +10:00
parent aa200c4e67
commit 601fe7fd3d
6 changed files with 159 additions and 108 deletions

View file

@ -14,7 +14,8 @@
var settings = { var settings = {
external_window: false, external_window: false,
maximum_messages: 200, maximum_messages: 200,
join_notification: true join_notification: true,
use_chat_bubbles: true,
}; };
// Global vars // Global vars
@ -27,7 +28,6 @@
var maxLocalDistance = 20; // Maximum range for the local chat var maxLocalDistance = 20; // Maximum range for the local chat
var palData = AvatarManager.getPalData().data; var palData = AvatarManager.getPalData().data;
var isTyping = false; var isTyping = false;
var useChatBubbles = false;
Controller.keyPressEvent.connect(keyPressEvent); Controller.keyPressEvent.connect(keyPressEvent);
Messages.subscribe("Chat"); // Floofchat Messages.subscribe("Chat"); // Floofchat
@ -128,7 +128,7 @@
_emitEvent({ type: "show_message", ...message }); _emitEvent({ type: "show_message", ...message });
// Show new message on screen // Show new message on screen
if (message.channel !== "local" || !useChatBubbles) { if (message.channel !== "local" || !settings.use_chat_bubbles) {
Messages.sendLocalMessage( Messages.sendLocalMessage(
"Floof-Notif", "Floof-Notif",
JSON.stringify({ JSON.stringify({
@ -167,7 +167,7 @@
break; break;
case "setting_change": case "setting_change":
if (event.setting === "worldspace_chat_bubbles") { if (event.setting === "worldspace_chat_bubbles") {
useChatBubbles = event.value; settings.use_chat_bubbles = event.value;
Messages.sendLocalMessage( Messages.sendLocalMessage(
"ChatBubbles-Config", "ChatBubbles-Config",
JSON.stringify({ JSON.stringify({
@ -301,7 +301,7 @@
settings = Settings.getValue("ArmoredChat-Config", settings); settings = Settings.getValue("ArmoredChat-Config", settings);
const chatBubbleSettings = Settings.getValue("ChatBubbles-Config", { enabled: true }); const chatBubbleSettings = Settings.getValue("ChatBubbles-Config", { enabled: true });
if (chatBubbleSettings.enabled) { useChatBubbles = true; } if (chatBubbleSettings.enabled) { settings.use_chat_bubbles = true; }
if (messageHistory) { if (messageHistory) {
// Load message history // Load message history
@ -314,13 +314,7 @@
} }
// Send current settings to the app // Send current settings to the app
_emitEvent({ _emitEvent({ type: "initial_settings", settings: settings });
type: "initial_settings",
settings: {
worldspace_chat_bubbles: useChatBubbles,
...settings
}
});
} }
function _saveSettings() { function _saveSettings() {
console.log("Saving config"); console.log("Saving config");

View file

@ -641,7 +641,7 @@ Rectangle {
if (message.settings.external_window) s_external_window.checked = true; 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.maximum_messages) s_maximum_messages.value = message.settings.maximum_messages;
if (message.settings.join_notification) s_join_notification.checked = true; if (message.settings.join_notification) s_join_notification.checked = true;
if (message.settings.worldspace_chat_bubbles) s_chat_bubbles.checked = true; if (message.settings.use_chat_bubbles) s_chat_bubbles.checked = true;
break; break;
} }
} }

View file

@ -43,6 +43,14 @@ Item {
Keys.onLeftPressed: { moveLeft(); } Keys.onLeftPressed: { moveLeft(); }
Keys.onRightPressed: { moveRight(); } Keys.onRightPressed: { moveRight(); }
onTextChanged: {
if (text === "") {
toScript({type: "action", action: "end_typing"});
} else {
toScript({type: "action", action: "start_typing"});
}
}
function moveLeft(){ function moveLeft(){
if (cursorPosition > 0){ if (cursorPosition > 0){
cursorPosition-- cursorPosition--
@ -119,4 +127,4 @@ Item {
function toScript(packet){ function toScript(packet){
sendToScript(packet) sendToScript(packet)
} }
} }

Binary file not shown.

View file

@ -24,8 +24,10 @@ const BUBBLE_WIDTH_MAX_CHARS = 24; // roughly 18 ems per meter
const MAX_DISTANCE = 20; const MAX_DISTANCE = 20;
const SELF_BUBBLES = false; const SELF_BUBBLES = false;
const NOTIFY_SOUND = SoundCache.getSound(Script.resolvePath("./assets/notify.wav"));
let settings = { let settings = {
enabled: true, enabled: true,
}; };
let currentBubbles = {}; let currentBubbles = {};
@ -101,38 +103,72 @@ function ChatBubbles_SpawnBubble(data, senderID) {
const scale = AvatarList.getAvatar(senderID).scale; const scale = AvatarList.getAvatar(senderID).scale;
let link; let link;
let linkIsImage = false;
try { // only handles cases where the whole message is just a URL,
const maybeURL = data.message.trim(); // text with a URL in the middle is ignored
const maybeURL = data.message.trim();
if (maybeURL.startsWith("https://") || maybeURL.startsWith("http://")) { if (
link = maybeURL; (maybeURL.startsWith("https://") || maybeURL.startsWith("http://")) &&
!/\s+/g.test(maybeURL) &&
/[A-Za-z0-9-._~:/?#\[\]@!$&'()*+,;%=]+/g.test(maybeURL)
) {
link = maybeURL;
const chunkBeforeQuery = maybeURL.split("?", 2)[0];
if (
chunkBeforeQuery.endsWith(".jpg") ||
chunkBeforeQuery.endsWith(".png") ||
chunkBeforeQuery.endsWith(".gif") ||
chunkBeforeQuery.endsWith(".svg") ||
chunkBeforeQuery.endsWith(".webp")
) {
linkIsImage = true;
} }
} catch (e) {} }
const [text, lineCount] = ChatBubbles_WrapText(data.message); const [text, lineCount] = ChatBubbles_WrapText(data.message);
const height = lineCount * BUBBLE_LINE_HEIGHT; let height = lineCount * BUBBLE_LINE_HEIGHT;
const bubbleEntity = Entities.addEntity({ let bubbleEntity;
type: "Text", if (link !== undefined && linkIsImage) {
parentID: senderID, height = BUBBLE_WIDTH / 3;
text: text, bubbleEntity = Entities.addEntity({
unlit: true, type: "Image",
ignorePickIntersection: (link === undefined), parentID: senderID,
lineHeight: BUBBLE_LINE_HEIGHT, imageURL: link,
dimensions: [BUBBLE_WIDTH, height + 0.04, 0.01], emissive: true,
localPosition: [0, scale + (height / 2) + 0.1, 0], keepAspectRatio: true,
backgroundAlpha: 0.5, ignorePickIntersection: true,
textColor: (link === undefined) ? [255, 255, 255] : [128, 240, 255], dimensions: [BUBBLE_WIDTH, height, 0.01],
textEffect: "outline fill", localPosition: [0, scale + (height / 2) + 0.1, 0],
textEffectColor: "#000", canCastShadow: false,
textEffectThickness: 0.4, billboardMode: "yaw",
canCastShadow: false, grab: {grabbable: false},
billboardMode: "yaw", }, "local");
alignment: "center", } else {
verticalAlignment: "center", bubbleEntity = Entities.addEntity({
grab: {grabbable: false}, type: "Text",
script: (link === undefined) ? undefined : parentID: senderID,
text: text,
unlit: true,
ignorePickIntersection: (link === undefined),
lineHeight: BUBBLE_LINE_HEIGHT,
dimensions: [BUBBLE_WIDTH, height + 0.04, 0.01],
localPosition: [0, scale + (height / 2) + 0.1, 0],
backgroundAlpha: 0.5,
textColor: (link === undefined) ? [255, 255, 255] : [128, 240, 255],
textEffect: "outline fill",
textEffectColor: "#000",
textEffectThickness: 0.4,
canCastShadow: false,
billboardMode: "yaw",
alignment: "center",
verticalAlignment: "center",
grab: {grabbable: false},
script: (link === undefined && !linkIsImage) ? undefined :
`(function() { `(function() {
this.mousePressOnEntity = function(entity, event) { this.mousePressOnEntity = function(entity, event) {
if (event.isPrimaryButton) { if (event.isPrimaryButton) {
@ -141,7 +177,8 @@ function ChatBubbles_SpawnBubble(data, senderID) {
} }
}; };
})` })`
}, "local"); }, "local");
}
for (const bubble of Object.values(currentBubbles[senderID])) { for (const bubble of Object.values(currentBubbles[senderID])) {
let { localPosition } = Entities.getEntityProperties(bubble.entity, "localPosition"); let { localPosition } = Entities.getEntityProperties(bubble.entity, "localPosition");
@ -152,12 +189,16 @@ function ChatBubbles_SpawnBubble(data, senderID) {
let bubbleIndex = Uuid.generate(); let bubbleIndex = Uuid.generate();
let bubble = { let bubble = {
entity: bubbleEntity, entity: bubbleEntity,
timeout: Script.setTimeout(() => { timeout: Script.setTimeout(() => {
let fade = 1.0; let fade = 1.0;
const fadeInterval = Script.setInterval(() => { const fadeInterval = Script.setInterval(() => {
Entities.editEntity(bubble.entity, { textAlpha: fade, backgroundAlpha: fade * 0.5 }); if (linkIsImage) {
Entities.editEntity(bubble.entity, { alpha: fade });
} else {
Entities.editEntity(bubble.entity, { textAlpha: fade, backgroundAlpha: fade * 0.5 });
}
fade -= (1 / BUBBLE_ANIM_FPS) / BUBBLE_FADE_TIME; fade -= (1 / BUBBLE_ANIM_FPS) / BUBBLE_FADE_TIME;
}, 1000 / BUBBLE_ANIM_FPS); }, 1000 / BUBBLE_ANIM_FPS);
@ -166,67 +207,73 @@ function ChatBubbles_SpawnBubble(data, senderID) {
Entities.deleteEntity(bubble.entity); Entities.deleteEntity(bubble.entity);
delete currentBubbles[senderID][bubbleIndex]; delete currentBubbles[senderID][bubbleIndex];
}, BUBBLE_FADE_TIME * 1000); }, BUBBLE_FADE_TIME * 1000);
}, BUBBLE_LIFETIME_SECS * 1000), }, BUBBLE_LIFETIME_SECS * 1000),
}; };
currentBubbles[senderID][bubbleIndex] = bubble; currentBubbles[senderID][bubbleIndex] = bubble;
Audio.playSound(NOTIFY_SOUND, {
position: data.position,
volume: 0.25,
localOnly: true,
});
} }
function ChatBubbles_IndicatorTick(senderID) { function ChatBubbles_IndicatorTick(senderID) {
const data = typingIndicators[senderID]; const data = typingIndicators[senderID];
const lowColor = [128, 192, 192]; const lowColor = [128, 192, 192];
const hiColor = [255, 255, 255]; const hiColor = [255, 255, 255];
let colorFade = 0.5 + (Math.cos(data.age / 5) * 0.5); let colorFade = 0.5 + (Math.cos(data.age / 5) * 0.5);
Entities.editEntity(data.entity, {textColor: Vec3.mix(lowColor, hiColor, colorFade)}); Entities.editEntity(data.entity, {textColor: Vec3.mix(lowColor, hiColor, colorFade)});
data.age += 1; data.age += 1;
} }
function ChatBubbles_ShowTypingIndicator(senderID) { function ChatBubbles_ShowTypingIndicator(senderID) {
if (typingIndicators[senderID]) { return; } if (typingIndicators[senderID]) { return; }
const scale = AvatarList.getAvatar(senderID).scale; const scale = AvatarList.getAvatar(senderID).scale;
const indicatorEntity = Entities.addEntity({ const indicatorEntity = Entities.addEntity({
type: "Text", type: "Text",
parentID: senderID, parentID: senderID,
text: "•••", text: "•••",
unlit: true, unlit: true,
lineHeight: 0.15, lineHeight: 0.15,
dimensions: [0.18, 0.08, 0.01], dimensions: [0.18, 0.08, 0.01],
localPosition: [0, scale, 0], localPosition: [0, scale, 0],
backgroundAlpha: 0.8, backgroundAlpha: 0.8,
canCastShadow: false, canCastShadow: false,
billboardMode: "full", billboardMode: "full",
alignment: "center", alignment: "center",
verticalAlignment: "center", verticalAlignment: "center",
textEffect: "outline fill", textEffect: "outline fill",
textEffectColor: "#000", textEffectColor: "#000",
textEffectThickness: 0.3, textEffectThickness: 0.3,
topMargin: -0.06, topMargin: -0.06,
grab: {grabbable: false}, grab: {grabbable: false},
}, "local"); }, "local");
const indicatorInterval = Script.setInterval(() => ChatBubbles_IndicatorTick(senderID), 1000 / BUBBLE_ANIM_FPS); const indicatorInterval = Script.setInterval(() => ChatBubbles_IndicatorTick(senderID), 1000 / BUBBLE_ANIM_FPS);
typingIndicators[senderID] = { typingIndicators[senderID] = {
entity: indicatorEntity, entity: indicatorEntity,
interval: indicatorInterval, interval: indicatorInterval,
age: 0, age: 0,
}; };
} }
function ChatBubbles_HideTypingIndicator(senderID) { function ChatBubbles_HideTypingIndicator(senderID) {
const data = typingIndicators[senderID]; const data = typingIndicators[senderID];
if (!data) { return; } if (!data) { return; }
Entities.deleteEntity(data.entity); Entities.deleteEntity(data.entity);
Script.clearInterval(data.interval); Script.clearInterval(data.interval);
delete typingIndicators[senderID]; delete typingIndicators[senderID];
} }
function ChatBubbles_RecvMsg(channel, msg, senderID, localOnly) { function ChatBubbles_RecvMsg(channel, msg, senderID, localOnly) {
@ -243,6 +290,8 @@ function ChatBubbles_RecvMsg(channel, msg, senderID, localOnly) {
for (const [key, value] of Object.entries(data)) { for (const [key, value] of Object.entries(data)) {
settings[key] = value; settings[key] = value;
} }
Settings.setValue("ChatBubbles-Config", settings);
return; return;
} }
@ -252,43 +301,43 @@ function ChatBubbles_RecvMsg(channel, msg, senderID, localOnly) {
// don't spawn bubbles for MyAvatar if the setting is disabled // don't spawn bubbles for MyAvatar if the setting is disabled
if (!SELF_BUBBLES && (senderID === MyAvatar.sessionUUID || !MyAvatar.sessionUUID)) { return; } if (!SELF_BUBBLES && (senderID === MyAvatar.sessionUUID || !MyAvatar.sessionUUID)) { return; }
let data; let data;
try { try {
data = JSON.parse(msg); data = JSON.parse(msg);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
return; return;
} }
if (channel === TYPING_NOTIFICATION_CHANNEL) { if (channel === TYPING_NOTIFICATION_CHANNEL) {
if (data.action === "typing_start") { if (data.action === "typing_start") {
// don't spawn a bubble if they're too far away // don't spawn a bubble if they're too far away
if (Vec3.distance(MyAvatar.position, data.position) > MAX_DISTANCE) { return; } if (Vec3.distance(MyAvatar.position, data.position) > MAX_DISTANCE) { return; }
ChatBubbles_ShowTypingIndicator(senderID); ChatBubbles_ShowTypingIndicator(senderID);
} else if (data.action === "typing_stop") { } else if (data.action === "typing_stop") {
ChatBubbles_HideTypingIndicator(senderID); ChatBubbles_HideTypingIndicator(senderID);
} }
} else if (data.action === "send_chat_message" && settings.enabled) { } else if (data.action === "send_chat_message" && settings.enabled) {
// don't spawn a bubble if they're too far away // don't spawn a bubble if they're too far away
if (data.channel !== "local") { return; } if (data.channel !== "local") { return; }
if (Vec3.distance(MyAvatar.position, data.position) > MAX_DISTANCE) { return; } if (Vec3.distance(MyAvatar.position, data.position) > MAX_DISTANCE) { return; }
ChatBubbles_SpawnBubble(data, senderID); ChatBubbles_SpawnBubble(data, senderID);
} }
} }
function ChatBubbles_DeleteAll() { function ChatBubbles_DeleteAll() {
for (const [_, bubbleList] of Object.entries(currentBubbles)) { for (const [_, bubbleList] of Object.entries(currentBubbles)) {
for (const [id, bubble] of Object.entries(bubbleList)) { for (const [id, bubble] of Object.entries(bubbleList)) {
Entities.deleteEntity(bubble.entity); Entities.deleteEntity(bubble.entity);
Script.clearTimeout(bubble.timeout); Script.clearTimeout(bubble.timeout);
delete bubbleList[id]; delete bubbleList[id];
} }
} }
for (const [_, indicator] of Object.entries(typingIndicators)) { for (const [_, indicator] of Object.entries(typingIndicators)) {
Entities.deleteEntity(indicator.entity); Entities.deleteEntity(indicator.entity);
Script.clearInterval(indicator.interval); Script.clearInterval(indicator.interval);
} }
currentBubbles = {}; currentBubbles = {};
typingIndicators = {}; typingIndicators = {};
@ -326,8 +375,8 @@ Messages.messageReceived.connect(ChatBubbles_RecvMsg);
Messages.subscribe(TYPING_NOTIFICATION_CHANNEL); Messages.subscribe(TYPING_NOTIFICATION_CHANNEL);
Script.scriptEnding.connect(() => { Script.scriptEnding.connect(() => {
Settings.setValue("ChatBubbles-Config", settings); Settings.setValue("ChatBubbles-Config", settings);
Messages.messageReceived.disconnect(ChatBubbles_RecvMsg); Messages.messageReceived.disconnect(ChatBubbles_RecvMsg);
Messages.unsubscribe(TYPING_NOTIFICATION_CHANNEL); Messages.unsubscribe(TYPING_NOTIFICATION_CHANNEL);
ChatBubbles_DeleteAll(); ChatBubbles_DeleteAll();
}); });

View file

@ -47,7 +47,7 @@ var DEFAULT_SCRIPTS_SEPARATE = [
"simplifiedUI/ui/simplifiedNametag/simplifiedNametag.js", "simplifiedUI/ui/simplifiedNametag/simplifiedNametag.js",
{"stable": "system/more/app-more.js", "beta": "https://more.overte.org/more/app-more.js"}, {"stable": "system/more/app-more.js", "beta": "https://more.overte.org/more/app-more.js"},
"communityScripts/armored-chat/armored_chat.js", "communityScripts/armored-chat/armored_chat.js",
"communityScripts/chatBubbles.js", "communityScripts/chatBubbles/chatBubbles.js",
//"system/chat.js" //"system/chat.js"
]; ];