From cb3b7711d57a2f78d74be8443be1a229060196ad Mon Sep 17 00:00:00 2001 From: duckietm Date: Thu, 8 May 2025 10:21:15 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9A=A0=EF=B8=8F=20Rebuild=20chatbaloons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/room/widgets/ChatBubbleMessage.ts | 38 ++-- .../widgets/chat/ChatWidgetMessageView.tsx | 108 +++++---- .../room/widgets/chat/ChatWidgetView.scss | 31 +-- .../room/widgets/chat/ChatWidgetView.tsx | 207 +++++++++--------- src/hooks/rooms/widgets/useChatWidget.ts | 29 +-- 5 files changed, 201 insertions(+), 212 deletions(-) diff --git a/src/api/room/widgets/ChatBubbleMessage.ts b/src/api/room/widgets/ChatBubbleMessage.ts index 10ad546..ded0423 100644 --- a/src/api/room/widgets/ChatBubbleMessage.ts +++ b/src/api/room/widgets/ChatBubbleMessage.ts @@ -1,7 +1,6 @@ import { INitroPoint } from '@nitrots/nitro-renderer'; -export class ChatBubbleMessage -{ +export class ChatBubbleMessage { public static BUBBLE_COUNTER: number = 0; public id: number = -1; @@ -12,7 +11,7 @@ export class ChatBubbleMessage private _top: number = 0; private _left: number = 0; - + constructor( public senderId: number = -1, public senderCategory: number = -1, @@ -25,35 +24,32 @@ export class ChatBubbleMessage public styleId: number = 0, public imageUrl: string = null, public color: string = null, - public chatColours: string = "" - ) - { + public chatColours: string = "" + ) { this.id = ++ChatBubbleMessage.BUBBLE_COUNTER; - this.color = color; - this.chatColours = chatColours; + this.color = color; + this.chatColours = chatColours; } - public get top(): number - { + public get top(): number { return this._top; } - public set top(value: number) - { + public set top(value: number) { this._top = value; - - if(this.elementRef) this.elementRef.style.top = (this._top + 'px'); + if (this.elementRef) { + this.elementRef.style.top = `${this._top}px`; // Always update DOM + } } - public get left(): number - { + public get left(): number { return this._left; } - public set left(value: number) - { + public set left(value: number) { this._left = value; - - if(this.elementRef) this.elementRef.style.left = (this._left + 'px'); + if (this.elementRef) { + this.elementRef.style.left = `${this._left}px`; // Always update DOM + } } -} +} \ No newline at end of file diff --git a/src/components/room/widgets/chat/ChatWidgetMessageView.tsx b/src/components/room/widgets/chat/ChatWidgetMessageView.tsx index db5899d..9854b8a 100644 --- a/src/components/room/widgets/chat/ChatWidgetMessageView.tsx +++ b/src/components/room/widgets/chat/ChatWidgetMessageView.tsx @@ -3,25 +3,39 @@ import { FC, useEffect, useMemo, useRef, useState } from 'react'; import { ChatBubbleMessage, GetRoomEngine } from '../../../../api'; import { useOnClickChat } from '../../../../hooks'; -interface ChatWidgetMessageViewProps -{ +interface ChatWidgetMessageViewProps { chat: ChatBubbleMessage; makeRoom: (chat: ChatBubbleMessage) => void; bubbleWidth?: number; - selectedEmoji?: string; + selectedEmoji?: string; } export const ChatWidgetMessageView: FC = props => { const { chat = null, makeRoom = null, bubbleWidth = RoomChatSettings.CHAT_BUBBLE_WIDTH_NORMAL, selectedEmoji } = props; - const [ isVisible, setIsVisible ] = useState(false); - const [ isReady, setIsReady ] = useState(false); + const [isVisible, setIsVisible] = useState(false); + const [isReady, setIsReady] = useState(false); const { onClickChat = null } = useOnClickChat(); const elementRef = useRef(); - const getBubbleWidth = useMemo(() => - { - switch(bubbleWidth) - { + // Memoize chat properties to prevent unnecessary re-renders + const chatMemo = useMemo(() => ({ + id: chat?.id, + locationX: chat?.location?.x, + top: chat?.top, + left: chat?.left, + styleId: chat?.styleId, + color: chat?.color, + chatColours: chat?.chatColours, + username: chat?.username, + formattedText: chat?.formattedText, + imageUrl: chat?.imageUrl, + type: chat?.type, + roomId: chat?.roomId, + senderId: chat?.senderId + }), [chat?.id, chat?.location?.x, chat?.top, chat?.left, chat?.styleId, chat?.color, chat?.chatColours, chat?.username, chat?.formattedText, chat?.imageUrl, chat?.type, chat?.roomId, chat?.senderId]); + + const getBubbleWidth = useMemo(() => { + switch (bubbleWidth) { case RoomChatSettings.CHAT_BUBBLE_WIDTH_NORMAL: return 350; case RoomChatSettings.CHAT_BUBBLE_WIDTH_THIN: @@ -29,15 +43,12 @@ export const ChatWidgetMessageView: FC = props => { case RoomChatSettings.CHAT_BUBBLE_WIDTH_WIDE: return 2000; } - }, [ bubbleWidth ]); + }, [bubbleWidth]); - useEffect(() => - { + useEffect(() => { setIsVisible(false); - const element = elementRef.current; - - if(!element) return; + if (!element) return; const width = element.offsetWidth; const height = element.offsetHeight; @@ -45,54 +56,67 @@ export const ChatWidgetMessageView: FC = props => { chat.width = width; chat.height = height; chat.elementRef = element; - + let left = chat.left; let top = chat.top; - if(!left && !top) - { - left = (chat.location.x - (width / 2)); - top = (element.parentElement.offsetHeight - height); - + // Calculate content offset based on styleId to account for margin-left in .chat-content + const contentOffset = chatMemo.styleId === 33 || chatMemo.styleId === 34 ? 35 : 27; + + // Position the bubble above the user (chat.location.x), adjusting for content offset + if (!left && !top) { + left = chatMemo.locationX ? (chatMemo.locationX - (width / 2) + contentOffset) : contentOffset; + top = element.parentElement ? (element.parentElement.offsetHeight - height) : 0; chat.left = left; chat.top = top; } + // Apply position immediately + element.style.position = 'absolute'; + element.style.left = `${left}px`; + element.style.top = `${top}px`; + setIsReady(true); - return () => - { + return () => { chat.elementRef = null; - setIsReady(false); - } - }, [ chat ]); - - useEffect(() => - { - if(!isReady || !chat || isVisible) return; - - if(makeRoom) makeRoom(chat); + }; + }, [chat, chatMemo]); + useEffect(() => { + if (!isReady || !chat || isVisible) return; + if (makeRoom) makeRoom(chat); setIsVisible(true); - }, [ chat, isReady, isVisible, makeRoom ]); + }, [chat, isReady, isVisible, makeRoom]); return ( -
GetRoomEngine().selectRoomObject(chat.roomId, chat.senderId, RoomObjectCategory.UNIT)}> +
GetRoomEngine().selectRoomObject(chatMemo.roomId, chatMemo.senderId, RoomObjectCategory.UNIT)} + > {selectedEmoji && {DOMPurify.sanitize(selectedEmoji)}} - { (chat.styleId === 0) && -
} -
+ {chatMemo.styleId === 0 && ( +
+ )} +
- { chat.imageUrl && (chat.imageUrl.length > 0) && -
} + {chatMemo.imageUrl && chatMemo.imageUrl.length > 0 && ( +
+ )}
- - onClickChat(e)} /> + + onClickChat(e)} + />
); -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/components/room/widgets/chat/ChatWidgetView.scss b/src/components/room/widgets/chat/ChatWidgetView.scss index c8dd62e..3a0f47f 100644 --- a/src/components/room/widgets/chat/ChatWidgetView.scss +++ b/src/components/room/widgets/chat/ChatWidgetView.scss @@ -1,8 +1,5 @@ .nitro-chat-widget { position: absolute; - display: flex; - justify-content: center; - align-items: center; width: 100%; top: 0; min-height: 1px; @@ -11,6 +8,7 @@ border-radius: 0; box-shadow: none; pointer-events: none; + } .chat-mention { @@ -28,17 +26,9 @@ .bubble-container { position: absolute; width: fit-content; - transition: top 0.2s ease 0s; user-select: none; pointer-events: all; - // -webkit-animation-duration: 0.2s; - // animation-duration: 0.2s; - // -webkit-animation-fill-mode: both; - // animation-fill-mode: both; - // -webkit-animation-name: bounceIn; - // animation-name: bounceIn; - .user-container-bg { position: absolute; top: -1px; @@ -74,14 +64,12 @@ } &.type-0 { - // normal .message { font-weight: 400; } } &.type-1 { - // whisper .message { font-weight: 400; font-style: italic; @@ -90,7 +78,6 @@ } &.type-2 { - // shout .message { font-weight: 700; } @@ -686,7 +673,7 @@ } } - &.bubble-39 { + &.bubble-39 { border-image-source: url("@/assets/images/chat/chatbubbles/bubble_39.png"); border-image-slice: 16 6 7 32 fill; @@ -864,7 +851,7 @@ .pointer { background: url("@/assets/images/chat/chatbubbles/bubble_53_pointer.png"); } - } + } .user-container { z-index: 3; @@ -958,7 +945,7 @@ } &.bubble-13 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_13.png'); + background-image: url('@/assets/images/chat/chatbubles/bubble_13.png'); } &.bubble-14 { @@ -1096,8 +1083,8 @@ background: url('@/assets/images/chat/chatbubbles/bubble_38_extra.png'); } } - - &.bubble-39 { + + &.bubble-39 { background-image: url("@/assets/images/chat/chatbubbles/bubble_39.png"); } @@ -1148,12 +1135,12 @@ &.bubble-51 { background-image: url("@/assets/images/chat/chatbubbles/bubble_51.png"); } - + &.bubble-52 { background-image: url("@/assets/images/chat/chatbubbles/bubble_52.png"); } - + &.bubble-53 { background-image: url("@/assets/images/chat/chatbubbles/bubble_53.png"); } -} +} \ No newline at end of file diff --git a/src/components/room/widgets/chat/ChatWidgetView.tsx b/src/components/room/widgets/chat/ChatWidgetView.tsx index 967b21f..27ccd56 100644 --- a/src/components/room/widgets/chat/ChatWidgetView.tsx +++ b/src/components/room/widgets/chat/ChatWidgetView.tsx @@ -6,156 +6,161 @@ import IntervalWebWorker from '../../../../workers/IntervalWebWorker'; import { WorkerBuilder } from '../../../../workers/WorkerBuilder'; import { ChatWidgetMessageView } from './ChatWidgetMessageView'; -export const ChatWidgetView: FC<{}> = props => -{ +export const ChatWidgetView: FC<{}> = props => { const { chatMessages = [], setChatMessages = null, chatSettings = null, getScrollSpeed = 6000 } = useChatWidget(); const elementRef = useRef(); + const lastTickTimeRef = useRef(Date.now()); + const scrollAmountPerTick = 15; // Pixels moved per tick + const workerRef = useRef(null); // Preserve worker across re-renders - const removeHiddenChats = useCallback(() => - { - setChatMessages(prevValue => - { - if(prevValue) - { + const removeHiddenChats = useCallback(() => { + setChatMessages(prevValue => { + if (prevValue) { const newMessages = prevValue.filter(chat => ((chat.top > (-(chat.height) * 2)))); - - if(newMessages.length !== prevValue.length) return newMessages; + if (newMessages.length !== prevValue.length) return newMessages; } - return prevValue; - }) - }, [ setChatMessages ]); + }); + }, [setChatMessages]); - const checkOverlappingChats = useCallback((chat: ChatBubbleMessage, moved: number, tempChats: ChatBubbleMessage[]) => - { - for(let i = (chatMessages.indexOf(chat) - 1); i >= 0; i--) - { + const checkOverlappingChats = useCallback((chat: ChatBubbleMessage, moved: number, tempChats: ChatBubbleMessage[]) => { + for (let i = (chatMessages.indexOf(chat) - 1); i >= 0; i--) { const collides = chatMessages[i]; + if (!collides || (chat === collides) || (tempChats.indexOf(collides) >= 0) || (((collides.top + collides.height) - moved) > (chat.top + chat.height))) continue; - if(!collides || (chat === collides) || (tempChats.indexOf(collides) >= 0) || (((collides.top + collides.height) - moved) > (chat.top + chat.height))) continue; - - if(DoChatsOverlap(chat, collides, -moved, 0)) - { + if (DoChatsOverlap(chat, collides, -moved, 0)) { const amount = Math.abs((collides.top + collides.height) - chat.top); - tempChats.push(collides); - collides.top -= amount; - collides.skipMovement = true; - checkOverlappingChats(collides, amount, tempChats); } } - }, [ chatMessages ]); - - const makeRoom = useCallback((chat: ChatBubbleMessage) => - { - if(chatSettings.mode === RoomChatSettings.CHAT_MODE_FREE_FLOW) - { - chat.skipMovement = true; - - checkOverlappingChats(chat, 0, [ chat ]); + }, [chatMessages]); + const makeRoom = useCallback((chat: ChatBubbleMessage) => { + if (chatSettings.mode === RoomChatSettings.CHAT_MODE_FREE_FLOW) { + checkOverlappingChats(chat, 0, [chat]); removeHiddenChats(); - } - else - { + } else { const lowestPoint = (chat.top + chat.height); const requiredSpace = chat.height; const spaceAvailable = (elementRef.current.offsetHeight - lowestPoint); const amount = (requiredSpace - spaceAvailable); - if(spaceAvailable < requiredSpace) - { - setChatMessages(prevValue => - { - prevValue.forEach(prevChat => - { - if(prevChat === chat) return; - + if (spaceAvailable < requiredSpace) { + setChatMessages(prevValue => { + prevValue.forEach(prevChat => { prevChat.top -= amount; + if (prevChat.elementRef) { + prevChat.elementRef.style.top = `${prevChat.top}px`; + } }); - return prevValue; }); - removeHiddenChats(); } } - }, [ chatSettings, checkOverlappingChats, removeHiddenChats, setChatMessages ]); + }, [chatSettings, checkOverlappingChats, removeHiddenChats, setChatMessages]); - useEffect(() => - { - const resize = (event: UIEvent = null) => - { - if(!elementRef || !elementRef.current) return; + const moveAllChatsUp = useCallback((amount: number) => { + setChatMessages(prevValue => { + prevValue.forEach(chat => { + chat.top -= amount; + if (chat.elementRef) { + chat.elementRef.style.top = `${chat.top}px`; + } + }); + return prevValue; + }); + removeHiddenChats(); + lastTickTimeRef.current = Date.now(); + }, [setChatMessages, removeHiddenChats]); + + useEffect(() => { + const resize = (event: UIEvent = null) => { + if (!elementRef || !elementRef.current) return; const currentHeight = elementRef.current.offsetHeight; const newHeight = Math.round(document.body.offsetHeight * GetConfiguration('chat.viewer.height.percentage')); - elementRef.current.style.height = `${ newHeight }px`; + elementRef.current.style.height = `${newHeight}px`; - setChatMessages(prevValue => - { - if(prevValue) - { - prevValue.forEach(chat => (chat.top -= (currentHeight - newHeight))); + setChatMessages(prevValue => { + if (prevValue) { + prevValue.forEach(chat => { + chat.top -= (currentHeight - newHeight); + if (chat.elementRef) { + chat.elementRef.style.top = `${chat.top}px`; + } + }); } - return prevValue; }); - } + }; window.addEventListener('resize', resize); - resize(); - return () => - { + return () => { window.removeEventListener('resize', resize); - } - }, [ setChatMessages ]); - - useEffect(() => - { - const moveAllChatsUp = (amount: number) => - { - setChatMessages(prevValue => - { - prevValue.forEach(chat => - { - if(chat.skipMovement) - { - chat.skipMovement = false; - - return; - } - - chat.top -= amount; - }); - - return prevValue; - }); - - removeHiddenChats(); - } + }; + }, [setChatMessages]); + useEffect(() => { const worker = new WorkerBuilder(IntervalWebWorker); - - worker.onmessage = () => moveAllChatsUp(15); - + workerRef.current = worker; + worker.onmessage = () => moveAllChatsUp(scrollAmountPerTick); worker.postMessage({ action: 'START', content: getScrollSpeed }); - return () => - { - worker.postMessage({ action: 'STOP' }); - worker.terminate(); + return () => { + if (workerRef.current) { + workerRef.current.postMessage({ action: 'STOP' }); + workerRef.current.terminate(); + workerRef.current = null; + } + }; + }, [moveAllChatsUp]); // Remove getScrollSpeed from deps to prevent re-creation + + // Update worker interval if getScrollSpeed changes + useEffect(() => { + if (workerRef.current) { + workerRef.current.postMessage({ action: 'UPDATE', content: getScrollSpeed }); } - }, [ getScrollSpeed, removeHiddenChats, setChatMessages ]); + }, [getScrollSpeed]); + + // Handle new chats and ensure immediate scrolling + useEffect(() => { + if (!chatMessages.length) return; + + const lastChat = chatMessages[chatMessages.length - 1]; + const timeSinceLastTick = Date.now() - lastTickTimeRef.current; + const ticksMissed = Math.floor(timeSinceLastTick / getScrollSpeed); + const offset = ticksMissed * scrollAmountPerTick; + + setChatMessages(prevValue => { + const newChat = prevValue[prevValue.length - 1]; + if (newChat === lastChat) { + newChat.top -= offset; + if (newChat.elementRef) { + newChat.elementRef.style.top = `${newChat.top}px`; + } + } + return prevValue; + }); + + moveAllChatsUp(scrollAmountPerTick); + }, [chatMessages, getScrollSpeed, moveAllChatsUp]); return ( -
- { chatMessages.map(chat => ) } +
+ {chatMessages.map(chat => ( + + ))}
); -} +}; \ No newline at end of file diff --git a/src/hooks/rooms/widgets/useChatWidget.ts b/src/hooks/rooms/widgets/useChatWidget.ts index 469562d..5d67025 100644 --- a/src/hooks/rooms/widgets/useChatWidget.ts +++ b/src/hooks/rooms/widgets/useChatWidget.ts @@ -40,8 +40,6 @@ const useChatWidgetState = () => const setFigureImage = (figure: string, username: string): Promise => { return new Promise((resolve) => { - console.log('setFigureImage called with figure:', figure, 'username:', username); - const avatarImage = GetAvatarRenderManager().createAvatarImage(figure, AvatarScaleType.LARGE, null, { resetFigure: figure => { if (isDisposed.current) return; @@ -51,37 +49,27 @@ const useChatWidgetState = () => disposed: false }); - console.log('avatarImage result:', avatarImage); - if (!avatarImage) { - console.log('Failed to create avatarImage for figure:', figure); - resolve('https://via.placeholder.com/40'); return; } avatarImage.getCroppedImage(AvatarSetType.HEAD).then(image => { - console.log('Cropped image:', image, 'Image src:', image?.src); if (!image || !image.src) { - console.log('Failed to get cropped image or src for figure:', figure); avatarImage.dispose(); - resolve('https://via.placeholder.com/40'); return; } const color = avatarImage.getPartColor(AvatarFigurePartType.CHEST); - console.log('Avatar color:', color, 'RGB:', color?.rgb); avatarColorCache.set(figure, ((color && color.rgb) || 16777215)); avatarImageCache.set(figure, image.src); - console.log('Cached image src:', image.src); - // Update existing chat messages for this username setChatMessages(prevValue => { const updatedMessages = prevValue.map(chat => { if (chat.username === username && chat.imageUrl !== image.src) { - chat.imageUrl = image.src; // Update in-place - return { ...chat }; // Shallow copy to trigger re-render + chat.imageUrl = image.src; + return { ...chat }; } return chat; }); @@ -91,9 +79,7 @@ const useChatWidgetState = () => avatarImage.dispose(); resolve(image.src); }).catch(error => { - console.error('Error in setFigureImage:', error); avatarImage.dispose(); - resolve('https://via.placeholder.com/40'); }); }); }; @@ -105,7 +91,7 @@ const useChatWidgetState = () => setFigureImage(figure, username).then(src => { avatarImageCache.set(figure, src); }); - return 'https://via.placeholder.com/40'; + return; } return existing; @@ -152,14 +138,6 @@ const useChatWidgetState = () => const figure = userData.figure; - console.log('Chat Event Debug:', { - userId: event.objectId, - username: userData.name, - userType, - figure, - roomObjectExists: !!roomObject - }); - switch(userType) { case RoomObjectType.PET: @@ -169,7 +147,6 @@ const useChatWidgetState = () => break; case RoomObjectType.USER: imageUrl = getUserImage(figure, userData.name); - console.log('getUserImage result:', { figure, imageUrl }); break; case RoomObjectType.RENTABLE_BOT: case RoomObjectType.BOT: