⚠️ Rebuild chatbaloons

This commit is contained in:
duckietm 2025-05-08 10:21:15 +02:00
parent 22f3aa4596
commit cb3b7711d5
5 changed files with 201 additions and 212 deletions

View File

@ -1,7 +1,6 @@
import { INitroPoint } from '@nitrots/nitro-renderer'; import { INitroPoint } from '@nitrots/nitro-renderer';
export class ChatBubbleMessage export class ChatBubbleMessage {
{
public static BUBBLE_COUNTER: number = 0; public static BUBBLE_COUNTER: number = 0;
public id: number = -1; public id: number = -1;
@ -26,34 +25,31 @@ export class ChatBubbleMessage
public imageUrl: string = null, public imageUrl: string = null,
public color: string = null, public color: string = null,
public chatColours: string = "" public chatColours: string = ""
) ) {
{
this.id = ++ChatBubbleMessage.BUBBLE_COUNTER; this.id = ++ChatBubbleMessage.BUBBLE_COUNTER;
this.color = color; this.color = color;
this.chatColours = chatColours; this.chatColours = chatColours;
} }
public get top(): number public get top(): number {
{
return this._top; return this._top;
} }
public set top(value: number) public set top(value: number) {
{
this._top = value; this._top = value;
if (this.elementRef) {
if(this.elementRef) this.elementRef.style.top = (this._top + 'px'); this.elementRef.style.top = `${this._top}px`; // Always update DOM
}
} }
public get left(): number public get left(): number {
{
return this._left; return this._left;
} }
public set left(value: number) public set left(value: number) {
{
this._left = value; this._left = value;
if (this.elementRef) {
if(this.elementRef) this.elementRef.style.left = (this._left + 'px'); this.elementRef.style.left = `${this._left}px`; // Always update DOM
}
} }
} }

View File

@ -3,8 +3,7 @@ import { FC, useEffect, useMemo, useRef, useState } from 'react';
import { ChatBubbleMessage, GetRoomEngine } from '../../../../api'; import { ChatBubbleMessage, GetRoomEngine } from '../../../../api';
import { useOnClickChat } from '../../../../hooks'; import { useOnClickChat } from '../../../../hooks';
interface ChatWidgetMessageViewProps interface ChatWidgetMessageViewProps {
{
chat: ChatBubbleMessage; chat: ChatBubbleMessage;
makeRoom: (chat: ChatBubbleMessage) => void; makeRoom: (chat: ChatBubbleMessage) => void;
bubbleWidth?: number; bubbleWidth?: number;
@ -18,10 +17,25 @@ export const ChatWidgetMessageView: FC<ChatWidgetMessageViewProps> = props => {
const { onClickChat = null } = useOnClickChat(); const { onClickChat = null } = useOnClickChat();
const elementRef = useRef<HTMLDivElement>(); const elementRef = useRef<HTMLDivElement>();
const getBubbleWidth = useMemo(() => // Memoize chat properties to prevent unnecessary re-renders
{ const chatMemo = useMemo(() => ({
switch(bubbleWidth) 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: case RoomChatSettings.CHAT_BUBBLE_WIDTH_NORMAL:
return 350; return 350;
case RoomChatSettings.CHAT_BUBBLE_WIDTH_THIN: case RoomChatSettings.CHAT_BUBBLE_WIDTH_THIN:
@ -31,12 +45,9 @@ export const ChatWidgetMessageView: FC<ChatWidgetMessageViewProps> = props => {
} }
}, [bubbleWidth]); }, [bubbleWidth]);
useEffect(() => useEffect(() => {
{
setIsVisible(false); setIsVisible(false);
const element = elementRef.current; const element = elementRef.current;
if (!element) return; if (!element) return;
const width = element.offsetWidth; const width = element.offsetWidth;
@ -49,50 +60,63 @@ export const ChatWidgetMessageView: FC<ChatWidgetMessageViewProps> = props => {
let left = chat.left; let left = chat.left;
let top = chat.top; let top = chat.top;
if(!left && !top) // Calculate content offset based on styleId to account for margin-left in .chat-content
{ const contentOffset = chatMemo.styleId === 33 || chatMemo.styleId === 34 ? 35 : 27;
left = (chat.location.x - (width / 2));
top = (element.parentElement.offsetHeight - height);
// 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.left = left;
chat.top = top; chat.top = top;
} }
// Apply position immediately
element.style.position = 'absolute';
element.style.left = `${left}px`;
element.style.top = `${top}px`;
setIsReady(true); setIsReady(true);
return () => return () => {
{
chat.elementRef = null; chat.elementRef = null;
setIsReady(false); setIsReady(false);
} };
}, [ chat ]); }, [chat, chatMemo]);
useEffect(() => useEffect(() => {
{
if (!isReady || !chat || isVisible) return; if (!isReady || !chat || isVisible) return;
if (makeRoom) makeRoom(chat); if (makeRoom) makeRoom(chat);
setIsVisible(true); setIsVisible(true);
}, [chat, isReady, isVisible, makeRoom]); }, [chat, isReady, isVisible, makeRoom]);
return ( return (
<div ref={elementRef} className={`bubble-container newbubblehe ${isVisible ? 'visible' : 'invisible'}`} onClick={event => GetRoomEngine().selectRoomObject(chat.roomId, chat.senderId, RoomObjectCategory.UNIT)}> <div
ref={elementRef}
className={`bubble-container newbubblehe ${isVisible ? 'visible' : 'invisible'}`}
onClick={event => GetRoomEngine().selectRoomObject(chatMemo.roomId, chatMemo.senderId, RoomObjectCategory.UNIT)}
>
{selectedEmoji && <span>{DOMPurify.sanitize(selectedEmoji)}</span>} {selectedEmoji && <span>{DOMPurify.sanitize(selectedEmoji)}</span>}
{ (chat.styleId === 0) && {chatMemo.styleId === 0 && (
<div className="user-container-bg" style={ { backgroundColor: chat.color } } /> } <div className="user-container-bg" style={{ backgroundColor: chatMemo.color }} />
<div className={ `chat-bubble bubble-${ chat.styleId } type-${ chat.type }` } style={ { maxWidth: getBubbleWidth } }> )}
<div className={`chat-bubble bubble-${chatMemo.styleId} type-${chatMemo.type}`} style={{ maxWidth: getBubbleWidth }}>
<div className="user-container"> <div className="user-container">
{ chat.imageUrl && (chat.imageUrl.length > 0) && {chatMemo.imageUrl && chatMemo.imageUrl.length > 0 && (
<div className="user-image" style={ { backgroundImage: `url(${ chat.imageUrl })` } } /> } <div className="user-image" style={{ backgroundImage: `url(${chatMemo.imageUrl})` }} />
)}
</div> </div>
<div className="chat-content"> <div className="chat-content">
<b className="username mr-1" dangerouslySetInnerHTML={ { __html: `${ chat.username }: ` } } /> <b className="username mr-1" dangerouslySetInnerHTML={{ __html: `${chatMemo.username}: ` }} />
<span className="message" style={{ color: chat.chatColours }} dangerouslySetInnerHTML={{ __html: `${chat.formattedText}` }} onClick={e => onClickChat(e)} /> <span
className="message"
style={{ color: chatMemo.chatColours }}
dangerouslySetInnerHTML={{ __html: `${chatMemo.formattedText}` }}
onClick={e => onClickChat(e)}
/>
</div> </div>
<div className="pointer" /> <div className="pointer" />
</div> </div>
</div> </div>
); );
} };

View File

@ -1,8 +1,5 @@
.nitro-chat-widget { .nitro-chat-widget {
position: absolute; position: absolute;
display: flex;
justify-content: center;
align-items: center;
width: 100%; width: 100%;
top: 0; top: 0;
min-height: 1px; min-height: 1px;
@ -11,6 +8,7 @@
border-radius: 0; border-radius: 0;
box-shadow: none; box-shadow: none;
pointer-events: none; pointer-events: none;
} }
.chat-mention { .chat-mention {
@ -28,17 +26,9 @@
.bubble-container { .bubble-container {
position: absolute; position: absolute;
width: fit-content; width: fit-content;
transition: top 0.2s ease 0s;
user-select: none; user-select: none;
pointer-events: all; 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 { .user-container-bg {
position: absolute; position: absolute;
top: -1px; top: -1px;
@ -74,14 +64,12 @@
} }
&.type-0 { &.type-0 {
// normal
.message { .message {
font-weight: 400; font-weight: 400;
} }
} }
&.type-1 { &.type-1 {
// whisper
.message { .message {
font-weight: 400; font-weight: 400;
font-style: italic; font-style: italic;
@ -90,7 +78,6 @@
} }
&.type-2 { &.type-2 {
// shout
.message { .message {
font-weight: 700; font-weight: 700;
} }
@ -958,7 +945,7 @@
} }
&.bubble-13 { &.bubble-13 {
background-image: url('@/assets/images/chat/chatbubbles/bubble_13.png'); background-image: url('@/assets/images/chat/chatbubles/bubble_13.png');
} }
&.bubble-14 { &.bubble-14 {

View File

@ -6,88 +6,78 @@ import IntervalWebWorker from '../../../../workers/IntervalWebWorker';
import { WorkerBuilder } from '../../../../workers/WorkerBuilder'; import { WorkerBuilder } from '../../../../workers/WorkerBuilder';
import { ChatWidgetMessageView } from './ChatWidgetMessageView'; import { ChatWidgetMessageView } from './ChatWidgetMessageView';
export const ChatWidgetView: FC<{}> = props => export const ChatWidgetView: FC<{}> = props => {
{
const { chatMessages = [], setChatMessages = null, chatSettings = null, getScrollSpeed = 6000 } = useChatWidget(); const { chatMessages = [], setChatMessages = null, chatSettings = null, getScrollSpeed = 6000 } = useChatWidget();
const elementRef = useRef<HTMLDivElement>(); const elementRef = useRef<HTMLDivElement>();
const lastTickTimeRef = useRef<number>(Date.now());
const scrollAmountPerTick = 15; // Pixels moved per tick
const workerRef = useRef<Worker | null>(null); // Preserve worker across re-renders
const removeHiddenChats = useCallback(() => const removeHiddenChats = useCallback(() => {
{ setChatMessages(prevValue => {
setChatMessages(prevValue => if (prevValue) {
{
if(prevValue)
{
const newMessages = prevValue.filter(chat => ((chat.top > (-(chat.height) * 2)))); 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; return prevValue;
}) });
}, [setChatMessages]); }, [setChatMessages]);
const checkOverlappingChats = useCallback((chat: ChatBubbleMessage, moved: number, tempChats: ChatBubbleMessage[]) => const checkOverlappingChats = useCallback((chat: ChatBubbleMessage, moved: number, tempChats: ChatBubbleMessage[]) => {
{ for (let i = (chatMessages.indexOf(chat) - 1); i >= 0; i--) {
for(let i = (chatMessages.indexOf(chat) - 1); i >= 0; i--)
{
const collides = chatMessages[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); const amount = Math.abs((collides.top + collides.height) - chat.top);
tempChats.push(collides); tempChats.push(collides);
collides.top -= amount; collides.top -= amount;
collides.skipMovement = true;
checkOverlappingChats(collides, amount, tempChats); checkOverlappingChats(collides, amount, tempChats);
} }
} }
}, [chatMessages]); }, [chatMessages]);
const makeRoom = useCallback((chat: ChatBubbleMessage) => const makeRoom = useCallback((chat: ChatBubbleMessage) => {
{ if (chatSettings.mode === RoomChatSettings.CHAT_MODE_FREE_FLOW) {
if(chatSettings.mode === RoomChatSettings.CHAT_MODE_FREE_FLOW)
{
chat.skipMovement = true;
checkOverlappingChats(chat, 0, [chat]); checkOverlappingChats(chat, 0, [chat]);
removeHiddenChats(); removeHiddenChats();
} } else {
else
{
const lowestPoint = (chat.top + chat.height); const lowestPoint = (chat.top + chat.height);
const requiredSpace = chat.height; const requiredSpace = chat.height;
const spaceAvailable = (elementRef.current.offsetHeight - lowestPoint); const spaceAvailable = (elementRef.current.offsetHeight - lowestPoint);
const amount = (requiredSpace - spaceAvailable); const amount = (requiredSpace - spaceAvailable);
if(spaceAvailable < requiredSpace) if (spaceAvailable < requiredSpace) {
{ setChatMessages(prevValue => {
setChatMessages(prevValue => prevValue.forEach(prevChat => {
{
prevValue.forEach(prevChat =>
{
if(prevChat === chat) return;
prevChat.top -= amount; prevChat.top -= amount;
if (prevChat.elementRef) {
prevChat.elementRef.style.top = `${prevChat.top}px`;
}
}); });
return prevValue; return prevValue;
}); });
removeHiddenChats(); removeHiddenChats();
} }
} }
}, [chatSettings, checkOverlappingChats, removeHiddenChats, setChatMessages]); }, [chatSettings, checkOverlappingChats, removeHiddenChats, setChatMessages]);
useEffect(() => const moveAllChatsUp = useCallback((amount: number) => {
{ setChatMessages(prevValue => {
const resize = (event: UIEvent = null) => 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; if (!elementRef || !elementRef.current) return;
const currentHeight = elementRef.current.offsetHeight; const currentHeight = elementRef.current.offsetHeight;
@ -95,67 +85,82 @@ export const ChatWidgetView: FC<{}> = props =>
elementRef.current.style.height = `${newHeight}px`; elementRef.current.style.height = `${newHeight}px`;
setChatMessages(prevValue => setChatMessages(prevValue => {
{ if (prevValue) {
if(prevValue) prevValue.forEach(chat => {
{ chat.top -= (currentHeight - newHeight);
prevValue.forEach(chat => (chat.top -= (currentHeight - newHeight))); if (chat.elementRef) {
chat.elementRef.style.top = `${chat.top}px`;
} }
return prevValue;
}); });
} }
return prevValue;
});
};
window.addEventListener('resize', resize); window.addEventListener('resize', resize);
resize(); resize();
return () => return () => {
{
window.removeEventListener('resize', resize); window.removeEventListener('resize', resize);
} };
}, [setChatMessages]); }, [setChatMessages]);
useEffect(() => useEffect(() => {
{ const worker = new WorkerBuilder(IntervalWebWorker);
const moveAllChatsUp = (amount: number) => workerRef.current = worker;
{ worker.onmessage = () => moveAllChatsUp(scrollAmountPerTick);
setChatMessages(prevValue => worker.postMessage({ action: 'START', content: getScrollSpeed });
{
prevValue.forEach(chat =>
{
if(chat.skipMovement)
{
chat.skipMovement = false;
return; return () => {
if (workerRef.current) {
workerRef.current.postMessage({ action: 'STOP' });
workerRef.current.terminate();
workerRef.current = null;
} }
};
}, [moveAllChatsUp]); // Remove getScrollSpeed from deps to prevent re-creation
chat.top -= amount; // Update worker interval if getScrollSpeed changes
}); useEffect(() => {
if (workerRef.current) {
workerRef.current.postMessage({ action: 'UPDATE', content: getScrollSpeed });
}
}, [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; return prevValue;
}); });
removeHiddenChats(); moveAllChatsUp(scrollAmountPerTick);
} }, [chatMessages, getScrollSpeed, moveAllChatsUp]);
const worker = new WorkerBuilder(IntervalWebWorker);
worker.onmessage = () => moveAllChatsUp(15);
worker.postMessage({ action: 'START', content: getScrollSpeed });
return () =>
{
worker.postMessage({ action: 'STOP' });
worker.terminate();
}
}, [ getScrollSpeed, removeHiddenChats, setChatMessages ]);
return ( return (
<div ref={elementRef} className="nitro-chat-widget"> <div ref={elementRef} className="nitro-chat-widget">
{ chatMessages.map(chat => <ChatWidgetMessageView key={ chat.id } chat={ chat } makeRoom={ makeRoom } bubbleWidth={ chatSettings.weight } />) } {chatMessages.map(chat => (
<ChatWidgetMessageView
key={chat.id}
chat={chat}
makeRoom={makeRoom}
bubbleWidth={chatSettings.weight}
/>
))}
</div> </div>
); );
} };

View File

@ -40,8 +40,6 @@ const useChatWidgetState = () =>
const setFigureImage = (figure: string, username: string): Promise<string | null> => { const setFigureImage = (figure: string, username: string): Promise<string | null> => {
return new Promise((resolve) => { return new Promise((resolve) => {
console.log('setFigureImage called with figure:', figure, 'username:', username);
const avatarImage = GetAvatarRenderManager().createAvatarImage(figure, AvatarScaleType.LARGE, null, { const avatarImage = GetAvatarRenderManager().createAvatarImage(figure, AvatarScaleType.LARGE, null, {
resetFigure: figure => { resetFigure: figure => {
if (isDisposed.current) return; if (isDisposed.current) return;
@ -51,37 +49,27 @@ const useChatWidgetState = () =>
disposed: false disposed: false
}); });
console.log('avatarImage result:', avatarImage);
if (!avatarImage) { if (!avatarImage) {
console.log('Failed to create avatarImage for figure:', figure);
resolve('https://via.placeholder.com/40');
return; return;
} }
avatarImage.getCroppedImage(AvatarSetType.HEAD).then(image => { avatarImage.getCroppedImage(AvatarSetType.HEAD).then(image => {
console.log('Cropped image:', image, 'Image src:', image?.src);
if (!image || !image.src) { if (!image || !image.src) {
console.log('Failed to get cropped image or src for figure:', figure);
avatarImage.dispose(); avatarImage.dispose();
resolve('https://via.placeholder.com/40');
return; return;
} }
const color = avatarImage.getPartColor(AvatarFigurePartType.CHEST); const color = avatarImage.getPartColor(AvatarFigurePartType.CHEST);
console.log('Avatar color:', color, 'RGB:', color?.rgb);
avatarColorCache.set(figure, ((color && color.rgb) || 16777215)); avatarColorCache.set(figure, ((color && color.rgb) || 16777215));
avatarImageCache.set(figure, image.src); avatarImageCache.set(figure, image.src);
console.log('Cached image src:', image.src);
// Update existing chat messages for this username
setChatMessages(prevValue => { setChatMessages(prevValue => {
const updatedMessages = prevValue.map(chat => { const updatedMessages = prevValue.map(chat => {
if (chat.username === username && chat.imageUrl !== image.src) { if (chat.username === username && chat.imageUrl !== image.src) {
chat.imageUrl = image.src; // Update in-place chat.imageUrl = image.src;
return { ...chat }; // Shallow copy to trigger re-render return { ...chat };
} }
return chat; return chat;
}); });
@ -91,9 +79,7 @@ const useChatWidgetState = () =>
avatarImage.dispose(); avatarImage.dispose();
resolve(image.src); resolve(image.src);
}).catch(error => { }).catch(error => {
console.error('Error in setFigureImage:', error);
avatarImage.dispose(); avatarImage.dispose();
resolve('https://via.placeholder.com/40');
}); });
}); });
}; };
@ -105,7 +91,7 @@ const useChatWidgetState = () =>
setFigureImage(figure, username).then(src => { setFigureImage(figure, username).then(src => {
avatarImageCache.set(figure, src); avatarImageCache.set(figure, src);
}); });
return 'https://via.placeholder.com/40'; return;
} }
return existing; return existing;
@ -152,14 +138,6 @@ const useChatWidgetState = () =>
const figure = userData.figure; const figure = userData.figure;
console.log('Chat Event Debug:', {
userId: event.objectId,
username: userData.name,
userType,
figure,
roomObjectExists: !!roomObject
});
switch(userType) switch(userType)
{ {
case RoomObjectType.PET: case RoomObjectType.PET:
@ -169,7 +147,6 @@ const useChatWidgetState = () =>
break; break;
case RoomObjectType.USER: case RoomObjectType.USER:
imageUrl = getUserImage(figure, userData.name); imageUrl = getUserImage(figure, userData.name);
console.log('getUserImage result:', { figure, imageUrl });
break; break;
case RoomObjectType.RENTABLE_BOT: case RoomObjectType.RENTABLE_BOT:
case RoomObjectType.BOT: case RoomObjectType.BOT: