66//
77// Distributed under the Apache License, Version 2.0.
88// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
9+ //
10+ // SPDX-License-Identifier: Apache-2.0
11+ //
912"use strict" ;
1013
1114const CHAT_CHANNEL = "chat" ;
1215
1316const TYPING_NOTIFICATION_CHANNEL = "Chat-Typing" ;
1417const CONFIG_UPDATE_CHANNEL = "ChatBubbles-Config" ;
1518
16- const BUBBLE_LIFETIME_SECS = 10 ;
19+ const BUBBLE_MIN_LIFETIME_SECS = 10 ;
20+ const BUBBLE_MAX_LIFETIME_SECS = 60 ; // failsafe to prevent really long perma-messages
21+ const BUBBLE_LINE_LIFETIME_SECS = 3.5 ;
22+ const BUBBLE_IMAGE_LIFETIME_SECS = 10 ;
1723const BUBBLE_FADE_TIME = 1 ;
1824const BUBBLE_ANIM_FPS = 15 ;
1925const BUBBLE_LINE_HEIGHT = 0.07 ;
2026const BUBBLE_WIDTH = 1.3 ;
21- const BUBBLE_WIDTH_MAX_CHARS = 24 ; // roughly 18 ems per meter
27+ const BUBBLE_WIDTH_MAX_CHARS = 36 ;
28+ const BUBBLE_BG_ALPHA = 0.6 ;
2229const MAX_DISTANCE = 20 ;
30+ const TYPING_NOTIF_HEAD_OFFSET_HEIGHT = 0.48 ;
31+ const MSG_HEAD_OFFSET_HEIGHT = 0.6 ;
2332const SELF_BUBBLES = false ;
2433
2534const NOTIFY_SOUND = SoundCache . getSound ( Script . resolvePath ( "./assets/notify.wav" ) ) ;
@@ -98,7 +107,10 @@ function ChatBubbles_SpawnBubble(data, senderID) {
98107 currentBubbles [ senderID ] = { } ;
99108 }
100109
101- const scale = AvatarList . getAvatar ( senderID ) . scale ;
110+ const avatar = AvatarList . getAvatar ( senderID ) ;
111+ const scale = avatar . scale ;
112+ const headIndex = avatar . getJointIndex ( "Head" ) ;
113+ const headPos = avatar . getAbsoluteJointTranslationInObjectFrame ( headIndex ) ;
102114
103115 let link ;
104116 let linkIsImage = false ;
@@ -109,19 +121,19 @@ function ChatBubbles_SpawnBubble(data, senderID) {
109121
110122 if (
111123 ( maybeURL . startsWith ( "https://" ) || maybeURL . startsWith ( "http://" ) ) &&
112- ! / \s + / g. test ( maybeURL ) &&
113- / [ A - Z a - z 0 - 9 - ._ ~ : / ? # \[ \] @ ! $ & ' ( ) * + , ; % = ] + / g. test ( maybeURL )
124+ ! / \s + / g. test ( maybeURL ) &&
125+ / [ A - Z a - z 0 - 9 - ._ ~ : / ? # \[ \] @ ! $ & ' ( ) * + , ; % = ] + / g. test ( maybeURL )
114126 ) {
115127 link = maybeURL ;
116128
117129 const chunkBeforeQuery = maybeURL . split ( "?" , 2 ) [ 0 ] ;
118130
119131 if (
120132 chunkBeforeQuery . endsWith ( ".jpg" ) ||
121- chunkBeforeQuery . endsWith ( ".png" ) ||
122- chunkBeforeQuery . endsWith ( ".gif" ) ||
123- chunkBeforeQuery . endsWith ( ".svg" ) ||
124- chunkBeforeQuery . endsWith ( ".webp" )
133+ chunkBeforeQuery . endsWith ( ".png" ) ||
134+ chunkBeforeQuery . endsWith ( ".gif" ) ||
135+ chunkBeforeQuery . endsWith ( ".svg" ) ||
136+ chunkBeforeQuery . endsWith ( ".webp" )
125137 ) {
126138 linkIsImage = true ;
127139 }
@@ -131,32 +143,40 @@ function ChatBubbles_SpawnBubble(data, senderID) {
131143 let height = lineCount * BUBBLE_LINE_HEIGHT ;
132144
133145 let bubbleEntity ;
146+ let lifetime ;
147+
134148 if ( link !== undefined && linkIsImage ) {
135149 height = BUBBLE_WIDTH / 3 ;
150+ height *= scale ;
151+ lifetime = BUBBLE_IMAGE_LIFETIME_SECS ;
136152 bubbleEntity = Entities . addEntity ( {
137153 type : "Image" ,
138154 parentID : senderID ,
139155 imageURL : link ,
140156 emissive : true ,
141157 keepAspectRatio : true ,
142158 ignorePickIntersection : true ,
143- dimensions : [ BUBBLE_WIDTH , height , 0.01 ] ,
144- localPosition : [ 0 , scale + ( height / 2 ) + 0.1 , 0 ] ,
159+ dimensions : Vec3 . multiply ( scale , [ BUBBLE_WIDTH , height , 0.01 ] ) ,
160+ localPosition : [ 0 , MSG_HEAD_OFFSET_HEIGHT + headPos . y + ( height / 2 ) , 0 ] ,
145161 canCastShadow : false ,
146162 billboardMode : "yaw" ,
147163 grab : { grabbable : false } ,
164+ renderLayer : "front" ,
148165 } , "local" ) ;
149166 } else {
167+ height *= scale ;
168+ lifetime = BUBBLE_LINE_LIFETIME_SECS * lineCount ;
150169 bubbleEntity = Entities . addEntity ( {
151170 type : "Text" ,
152171 parentID : senderID ,
172+ visible : false ,
153173 text : text ,
154174 unlit : true ,
155175 ignorePickIntersection : ( link === undefined ) ,
156- lineHeight : BUBBLE_LINE_HEIGHT ,
157- dimensions : [ BUBBLE_WIDTH , height + 0.04 , 0.01 ] ,
158- localPosition : [ 0 , scale + ( height / 2 ) + 0.1 , 0 ] ,
159- backgroundAlpha : 0.5 ,
176+ lineHeight : BUBBLE_LINE_HEIGHT * scale ,
177+ dimensions : Vec3 . multiply ( scale , [ BUBBLE_WIDTH , height + 0.04 , 0.01 ] ) ,
178+ localPosition : [ 0 , MSG_HEAD_OFFSET_HEIGHT + headPos . y + ( height / 2 ) , 0 ] ,
179+ backgroundAlpha : BUBBLE_BG_ALPHA ,
160180 textColor : ( link === undefined ) ? [ 255 , 255 , 255 ] : [ 128 , 240 , 255 ] ,
161181 textEffect : "outline fill" ,
162182 textEffectColor : "#000" ,
@@ -165,7 +185,9 @@ function ChatBubbles_SpawnBubble(data, senderID) {
165185 billboardMode : "yaw" ,
166186 alignment : "center" ,
167187 verticalAlignment : "center" ,
188+ topMargin : - 0.004 * scale , // center isn't exactly centered?
168189 grab : { grabbable : false } ,
190+ renderLayer : "front" ,
169191 script : ( link === undefined && ! linkIsImage ) ? undefined :
170192`(function() {
171193 this.mousePressOnEntity = function(entity, event) {
@@ -176,11 +198,21 @@ function ChatBubbles_SpawnBubble(data, senderID) {
176198 };
177199})`
178200 } , "local" ) ;
201+
202+ Script . setTimeout ( ( ) => {
203+ const size = Entities . textSize ( bubbleEntity , text ) ;
204+ Entities . editEntity ( bubbleEntity , {
205+ visible : true ,
206+ dimensions : [ size . width + ( 0.06 * scale ) , size . height + ( 0.04 * scale ) , 0.01 ] ,
207+ } ) ;
208+ } , 100 ) ;
209+ // this wait time is annoyingly inconsistent,
210+ // so it needs a decent chunk of time to work properly on all messages
179211 }
180212
181213 for ( const bubble of Object . values ( currentBubbles [ senderID ] ) ) {
182214 let { localPosition } = Entities . getEntityProperties ( bubble . entity , "localPosition" ) ;
183- localPosition = Vec3 . sum ( localPosition , [ 0 , height + 0.05 , 0 ] ) ;
215+ localPosition = Vec3 . sum ( localPosition , [ 0 , height + ( 0.05 * scale ) , 0 ] ) ;
184216 Entities . editEntity ( bubble . entity , { localPosition : localPosition } ) ;
185217 }
186218
@@ -195,7 +227,7 @@ function ChatBubbles_SpawnBubble(data, senderID) {
195227 if ( linkIsImage ) {
196228 Entities . editEntity ( bubble . entity , { alpha : fade } ) ;
197229 } else {
198- Entities . editEntity ( bubble . entity , { textAlpha : fade , backgroundAlpha : fade * 0.5 } ) ;
230+ Entities . editEntity ( bubble . entity , { textAlpha : fade , backgroundAlpha : fade * BUBBLE_BG_ALPHA } ) ;
199231 }
200232 fade -= ( 1 / BUBBLE_ANIM_FPS ) / BUBBLE_FADE_TIME ;
201233 } , 1000 / BUBBLE_ANIM_FPS ) ;
@@ -205,7 +237,7 @@ function ChatBubbles_SpawnBubble(data, senderID) {
205237 Entities . deleteEntity ( bubble . entity ) ;
206238 delete currentBubbles [ senderID ] [ bubbleIndex ] ;
207239 } , BUBBLE_FADE_TIME * 1000 ) ;
208- } , BUBBLE_LIFETIME_SECS * 1000 ) ,
240+ } , Math . min ( BUBBLE_MAX_LIFETIME_SECS , Math . max ( BUBBLE_MIN_LIFETIME_SECS , lifetime ) ) * 1000 ) ,
209241 } ;
210242
211243 currentBubbles [ senderID ] [ bubbleIndex ] = bubble ;
@@ -233,26 +265,31 @@ function ChatBubbles_IndicatorTick(senderID) {
233265function ChatBubbles_ShowTypingIndicator ( senderID ) {
234266 if ( typingIndicators [ senderID ] ) { return ; }
235267
236- const scale = AvatarList . getAvatar ( senderID ) . scale ;
268+ const avatar = AvatarList . getAvatar ( senderID ) ;
269+ const scale = avatar . scale ;
270+ const headIndex = avatar . getJointIndex ( "Head" ) ;
271+ const headPos = avatar . getAbsoluteJointTranslationInObjectFrame ( headIndex ) ;
237272
238273 const indicatorEntity = Entities . addEntity ( {
239274 type : "Text" ,
240275 parentID : senderID ,
241276 text : "•••" ,
242277 unlit : true ,
243- lineHeight : 0.15 ,
244- dimensions : [ 0.18 , 0.08 , 0.01 ] ,
245- localPosition : [ 0 , scale , 0 ] ,
246- backgroundAlpha : 0.8 ,
278+ lineHeight : 0.15 * scale ,
279+ localDimensions : Vec3 . multiply ( scale , [ 0.18 , 0.08 , 0.01 ] ) ,
280+ localPosition : [ 0 , ( TYPING_NOTIF_HEAD_OFFSET_HEIGHT * scale ) + headPos . y , 0 ] ,
281+ backgroundAlpha : BUBBLE_BG_ALPHA ,
247282 canCastShadow : false ,
248283 billboardMode : "full" ,
249284 alignment : "center" ,
250285 verticalAlignment : "center" ,
251286 textEffect : "outline fill" ,
252287 textEffectColor : "#000" ,
253288 textEffectThickness : 0.3 ,
254- topMargin : - 0.06 ,
289+ topMargin : - 0.06 * scale ,
255290 grab : { grabbable : false } ,
291+ ignorePickIntersection : true ,
292+ renderLayer : "front" ,
256293 } , "local" ) ;
257294
258295 const indicatorInterval = Script . setInterval ( ( ) => ChatBubbles_IndicatorTick ( senderID ) , 1000 / BUBBLE_ANIM_FPS ) ;
0 commit comments