⚠️ Chat has been completed

This commit is contained in:
duckietm 2025-05-08 13:40:38 +02:00
parent cb3b7711d5
commit 68bd7ed148
4 changed files with 96 additions and 50 deletions

View File

@ -1,4 +1,4 @@
import { ILinkEventTracker } from '@nitrots/nitro-renderer'; import { ILinkEventTracker, RoomObjectType } from '@nitrots/nitro-renderer';
import { FC, useEffect, useMemo, useRef, useState } from 'react'; import { FC, useEffect, useMemo, useRef, useState } from 'react';
import { AddEventLinkTracker, ChatEntryType, LocalizeText, RemoveLinkEventTracker } from '../../api'; import { AddEventLinkTracker, ChatEntryType, LocalizeText, RemoveLinkEventTracker } from '../../api';
import { Column, Flex, InfiniteScroll, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text, Button } from '../../common'; import { Column, Flex, InfiniteScroll, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text, Button } from '../../common';
@ -87,6 +87,17 @@ export const ChatHistoryView: FC<{}> = props =>
scrollToBottom={true} scrollToBottom={true}
rowRender={row => rowRender={row =>
{ {
// Define styles where imageUrl should not be loaded
const stylesWithoutImage = [1, 2, 8, 28, 30, 32, 33, 34, 35, 36, 38];
const shouldDisplayImage = !stylesWithoutImage.includes(row.style);
// Determine image dimensions and positioning based on entityType
const isPet = row.entityType === RoomObjectType.PET;
const imageWidth = isPet ? '41px' : '90px'; // Double for users to account for transform: scale(0.5)
const imageHeight = isPet ? '54px' : '130px'; // Double for users to account for transform: scale(0.5)
const imageTop = isPet ? '-15px' : '-50px'; // Adjusted to shift user image up
const imageLeft = isPet ? '-9.25px' : '-31px'; // Adjusted to shift user image left
return ( return (
<Flex alignItems="center" className="p-1" gap={2}> <Flex alignItems="center" className="p-1" gap={2}>
<Text variant="muted">{row.timestamp}</Text> <Text variant="muted">{row.timestamp}</Text>
@ -96,8 +107,19 @@ export const ChatHistoryView: FC<{}> = props =>
<div className="user-container-bg" style={{ backgroundColor: row.color }} /> } <div className="user-container-bg" style={{ backgroundColor: row.color }} /> }
<div className={`chat-bubble bubble-${row.style} type-${row.chatType}`} style={{ maxWidth: '100%' }}> <div className={`chat-bubble bubble-${row.style} type-${row.chatType}`} style={{ maxWidth: '100%' }}>
<div className="user-container"> <div className="user-container">
{ row.imageUrl && (row.imageUrl.length > 0) && { shouldDisplayImage && row.imageUrl && (row.imageUrl.length > 0) &&
<div className="user-image" style={{ backgroundImage: `url(${row.imageUrl})` }} /> } <div
className="user-image"
style={{
backgroundImage: `url(${row.imageUrl})`,
width: imageWidth,
height: imageHeight,
top: imageTop,
left: imageLeft,
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
/> }
</div> </div>
<div className="chat-content"> <div className="chat-content">
<b className="username mr-1" dangerouslySetInnerHTML={{ __html: `${row.name}: ` }} /> <b className="username mr-1" dangerouslySetInnerHTML={{ __html: `${row.name}: ` }} />

View File

@ -1,4 +1,4 @@
import { RoomChatSettings, RoomObjectCategory } from '@nitrots/nitro-renderer'; import { RoomChatSettings, RoomObjectCategory, RoomObjectType } from '@nitrots/nitro-renderer';
import { FC, useEffect, useMemo, useRef, useState } from 'react'; 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';
@ -17,7 +17,6 @@ export const ChatWidgetMessageView: FC<ChatWidgetMessageViewProps> = props => {
const { onClickChat = null } = useOnClickChat(); const { onClickChat = null } = useOnClickChat();
const elementRef = useRef<HTMLDivElement>(); const elementRef = useRef<HTMLDivElement>();
// Memoize chat properties to prevent unnecessary re-renders
const chatMemo = useMemo(() => ({ const chatMemo = useMemo(() => ({
id: chat?.id, id: chat?.id,
locationX: chat?.location?.x, locationX: chat?.location?.x,
@ -31,8 +30,9 @@ export const ChatWidgetMessageView: FC<ChatWidgetMessageViewProps> = props => {
imageUrl: chat?.imageUrl, imageUrl: chat?.imageUrl,
type: chat?.type, type: chat?.type,
roomId: chat?.roomId, roomId: chat?.roomId,
senderId: chat?.senderId 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]); userType: (chat as any)?.userType || 0
}), [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, (chat as any)?.userType]);
const getBubbleWidth = useMemo(() => { const getBubbleWidth = useMemo(() => {
switch (bubbleWidth) { switch (bubbleWidth) {
@ -60,10 +60,8 @@ export const ChatWidgetMessageView: FC<ChatWidgetMessageViewProps> = props => {
let left = chat.left; let left = chat.left;
let top = chat.top; let top = chat.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; 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) { if (!left && !top) {
left = chatMemo.locationX ? (chatMemo.locationX - (width / 2) + contentOffset) : contentOffset; left = chatMemo.locationX ? (chatMemo.locationX - (width / 2) + contentOffset) : contentOffset;
top = element.parentElement ? (element.parentElement.offsetHeight - height) : 0; top = element.parentElement ? (element.parentElement.offsetHeight - height) : 0;
@ -71,7 +69,6 @@ export const ChatWidgetMessageView: FC<ChatWidgetMessageViewProps> = props => {
chat.top = top; chat.top = top;
} }
// Apply position immediately
element.style.position = 'absolute'; element.style.position = 'absolute';
element.style.left = `${left}px`; element.style.left = `${left}px`;
element.style.top = `${top}px`; element.style.top = `${top}px`;
@ -90,6 +87,16 @@ export const ChatWidgetMessageView: FC<ChatWidgetMessageViewProps> = props => {
setIsVisible(true); setIsVisible(true);
}, [chat, isReady, isVisible, makeRoom]); }, [chat, isReady, isVisible, makeRoom]);
const isPet = chatMemo.userType === RoomObjectType.PET;
const imageWidth = isPet ? '41px' : '90px';
const imageHeight = isPet ? '54px' : '130px';
const imageTop = isPet ? '-15px' : '-50px';
const imageLeft = isPet ? '-9.25px' : '-31px';
// Define styles where imageUrl should not be loaded
const stylesWithoutImage = [1, 2, 8, 28, 30, 32, 33, 34, 35, 36, 38];
const shouldDisplayImage = !stylesWithoutImage.includes(chatMemo.styleId);
return ( return (
<div <div
ref={elementRef} ref={elementRef}
@ -101,11 +108,23 @@ export const ChatWidgetMessageView: FC<ChatWidgetMessageViewProps> = props => {
<div className="user-container-bg" style={{ backgroundColor: chatMemo.color }} /> <div className="user-container-bg" style={{ backgroundColor: chatMemo.color }} />
)} )}
<div className={`chat-bubble bubble-${chatMemo.styleId} type-${chatMemo.type}`} style={{ maxWidth: getBubbleWidth }}> <div className={`chat-bubble bubble-${chatMemo.styleId} type-${chatMemo.type}`} style={{ maxWidth: getBubbleWidth }}>
<div className="user-container"> {shouldDisplayImage && chatMemo.imageUrl && typeof chatMemo.imageUrl === 'string' && chatMemo.imageUrl.length > 0 && (
{chatMemo.imageUrl && chatMemo.imageUrl.length > 0 && ( <div className="user-container" style={{ display: 'block' }}>
<div className="user-image" style={{ backgroundImage: `url(${chatMemo.imageUrl})` }} /> <div
)} className="user-image"
style={{
backgroundImage: `url(${chatMemo.imageUrl})`,
width: imageWidth,
height: imageHeight,
top: imageTop,
left: imageLeft,
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
onError={() => console.log(`Failed to load image for Chat ID ${chatMemo.id}: ${chatMemo.imageUrl}`)}
/>
</div> </div>
)}
<div className="chat-content"> <div className="chat-content">
<b className="username mr-1" dangerouslySetInnerHTML={{ __html: `${chatMemo.username}: ` }} /> <b className="username mr-1" dangerouslySetInnerHTML={{ __html: `${chatMemo.username}: ` }} />
<span <span

View File

@ -10,8 +10,8 @@ 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 lastTickTimeRef = useRef<number>(Date.now());
const scrollAmountPerTick = 15; // Pixels moved per tick const scrollAmountPerTick = 15;
const workerRef = useRef<Worker | null>(null); // Preserve worker across re-renders const workerRef = useRef<Worker | null>(null);
const removeHiddenChats = useCallback(() => { const removeHiddenChats = useCallback(() => {
setChatMessages(prevValue => { setChatMessages(prevValue => {
@ -119,16 +119,14 @@ export const ChatWidgetView: FC<{}> = props => {
workerRef.current = null; workerRef.current = null;
} }
}; };
}, [moveAllChatsUp]); // Remove getScrollSpeed from deps to prevent re-creation }, [moveAllChatsUp]);
// Update worker interval if getScrollSpeed changes
useEffect(() => { useEffect(() => {
if (workerRef.current) { if (workerRef.current) {
workerRef.current.postMessage({ action: 'UPDATE', content: getScrollSpeed }); workerRef.current.postMessage({ action: 'UPDATE', content: getScrollSpeed });
} }
}, [getScrollSpeed]); }, [getScrollSpeed]);
// Handle new chats and ensure immediate scrolling
useEffect(() => { useEffect(() => {
if (!chatMessages.length) return; if (!chatMessages.length) return;

View File

@ -1,4 +1,4 @@
import { AvatarFigurePartType, AvatarScaleType, AvatarSetType, GetGuestRoomResultEvent, NitroPoint, PetFigureData, RoomChatSettings, RoomChatSettingsEvent, RoomDragEvent, RoomObjectCategory, RoomObjectType, RoomObjectVariable, RoomSessionChatEvent, RoomUserData, SystemChatStyleEnum, TextureUtils, Vector3d } from '@nitrots/nitro-renderer'; import { AvatarFigurePartType, AvatarScaleType, AvatarSetType, GetGuestRoomResultEvent, NitroPoint, PetFigureData, RoomChatSettings, RoomChatSettingsEvent, RoomDragEvent, RoomObjectCategory, RoomObjectType, RoomObjectVariable, RoomSessionChatEvent, SystemChatStyleEnum, TextureUtils, Vector3d } from '@nitrots/nitro-renderer';
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { ChatBubbleMessage, ChatEntryType, ChatHistoryCurrentDate, GetAvatarRenderManager, GetConfiguration, GetRoomEngine, GetRoomObjectScreenLocation, IRoomChatSettings, LocalizeText, PlaySound, RoomChatFormatter } from '../../../api'; import { ChatBubbleMessage, ChatEntryType, ChatHistoryCurrentDate, GetAvatarRenderManager, GetConfiguration, GetRoomEngine, GetRoomObjectScreenLocation, IRoomChatSettings, LocalizeText, PlaySound, RoomChatFormatter } from '../../../api';
import { useMessageEvent, useRoomEngineEvent, useRoomSessionManagerEvent } from '../../events'; import { useMessageEvent, useRoomEngineEvent, useRoomSessionManagerEvent } from '../../events';
@ -50,13 +50,14 @@ const useChatWidgetState = () =>
}); });
if (!avatarImage) { if (!avatarImage) {
resolve(null);
return; return;
} }
avatarImage.getCroppedImage(AvatarSetType.HEAD).then(image => { avatarImage.getCroppedImage(AvatarSetType.HEAD).then(image => {
if (!image || !image.src) { if (!image || !image.src) {
avatarImage.dispose(); avatarImage.dispose();
resolve(null);
return; return;
} }
@ -80,43 +81,57 @@ const useChatWidgetState = () =>
resolve(image.src); resolve(image.src);
}).catch(error => { }).catch(error => {
avatarImage.dispose(); avatarImage.dispose();
resolve(null);
}); });
}); });
}; };
const getUserImage = (figure: string, username: string): string | null => { const getUserImage = async (figure: string, username: string): Promise<string | null> => {
let existing = avatarImageCache.get(figure); let existing = avatarImageCache.get(figure);
if (!existing) { if (!existing) {
setFigureImage(figure, username).then(src => { const src = await setFigureImage(figure, username);
if (src) {
avatarImageCache.set(figure, src); avatarImageCache.set(figure, src);
}); return src;
return; }
return null;
} }
return existing; return existing;
}; };
const getPetImage = (figure: string, direction: number, _arg_3: boolean, scale: number = 64, posture: string = null) => const getPetImage = async (figure: string, direction: number, _arg_3: boolean, scale: number = 64, posture: string = null): Promise<string | null> => {
{ const cacheKey = (figure + posture);
let existing = petImageCache.get((figure + posture)); let existing = petImageCache.get(cacheKey);
if(existing) return existing; if (existing) return existing;
const figureData = new PetFigureData(figure); const figureData = new PetFigureData(figure);
const typeId = figureData.typeId; const typeId = figureData.typeId;
const image = GetRoomEngine().getRoomObjectPetImage(typeId, figureData.paletteId, figureData.color, new Vector3d((direction * 45)), scale, null, false, 0, figureData.customParts, posture);
if(image) let image = GetRoomEngine().getRoomObjectPetImage(typeId, figureData.paletteId, figureData.color, new Vector3d((direction * 45)), scale, null, false, 0, figureData.customParts, posture);
{
existing = TextureUtils.generateImageUrl(image.data); if (!image) {
petImageCache.set((figure + posture), existing); image = GetRoomEngine().getRoomObjectPetImage(typeId, figureData.paletteId, figureData.color, new Vector3d(90), 64, null, false, 0, figureData.customParts, null);
} }
return existing; if (image) {
const imageUrl = await TextureUtils.generateImageUrl(image.data);
if (imageUrl) {
petImageCache.set(cacheKey, imageUrl);
return imageUrl;
} else {
petImageCache.delete(cacheKey);
}
} else {
petImageCache.delete(cacheKey);
}
return null;
}; };
useRoomSessionManagerEvent<RoomSessionChatEvent>(RoomSessionChatEvent.CHAT_EVENT, event => useRoomSessionManagerEvent<RoomSessionChatEvent>(RoomSessionChatEvent.CHAT_EVENT, async event =>
{ {
const roomObject = GetRoomEngine().getRoomObject(roomSession.roomId, event.objectId, RoomObjectCategory.UNIT); const roomObject = GetRoomEngine().getRoomObject(roomSession.roomId, event.objectId, RoomObjectCategory.UNIT);
const bubbleLocation = roomObject ? GetRoomObjectScreenLocation(roomSession.roomId, roomObject?.id, RoomObjectCategory.UNIT) : new NitroPoint(); const bubbleLocation = roomObject ? GetRoomObjectScreenLocation(roomSession.roomId, roomObject?.id, RoomObjectCategory.UNIT) : new NitroPoint();
@ -135,18 +150,17 @@ const useChatWidgetState = () =>
if(userData) if(userData)
{ {
userType = userData.type; userType = userData.type;
const figure = userData.figure; const figure = userData.figure;
switch(userType) switch(userType)
{ {
case RoomObjectType.PET: case RoomObjectType.PET:
imageUrl = getPetImage(figure, 2, true, 64, roomObject.model.getValue<string>(RoomObjectVariable.FIGURE_POSTURE)); imageUrl = await getPetImage(figure, 2, true, 64, roomObject.model.getValue<string>(RoomObjectVariable.FIGURE_POSTURE));
petType = new PetFigureData(figure).typeId; petType = new PetFigureData(figure).typeId;
chatColours = "black"; chatColours = "black";
break; break;
case RoomObjectType.USER: case RoomObjectType.USER:
imageUrl = getUserImage(figure, userData.name); imageUrl = await getUserImage(figure, userData.name);
break; break;
case RoomObjectType.RENTABLE_BOT: case RoomObjectType.RENTABLE_BOT:
case RoomObjectType.BOT: case RoomObjectType.BOT:
@ -163,36 +177,27 @@ const useChatWidgetState = () =>
{ {
case RoomSessionChatEvent.CHAT_TYPE_RESPECT: case RoomSessionChatEvent.CHAT_TYPE_RESPECT:
text = LocalizeText('widgets.chatbubble.respect', [ 'username' ], [ username ]); text = LocalizeText('widgets.chatbubble.respect', [ 'username' ], [ username ]);
if(GetConfiguration('respect.options')['enabled']) PlaySound(GetConfiguration('respect.options')['sound']); if(GetConfiguration('respect.options')['enabled']) PlaySound(GetConfiguration('respect.options')['sound']);
break; break;
case RoomSessionChatEvent.CHAT_TYPE_PETREVIVE: case RoomSessionChatEvent.CHAT_TYPE_PETREVIVE:
case RoomSessionChatEvent.CHAT_TYPE_PET_REBREED_FERTILIZE: case RoomSessionChatEvent.CHAT_TYPE_PET_REBREED_FERTILIZE:
case RoomSessionChatEvent.CHAT_TYPE_PET_SPEED_FERTILIZE: { case RoomSessionChatEvent.CHAT_TYPE_PET_SPEED_FERTILIZE: {
let textKey = 'widget.chatbubble.petrevived'; let textKey = 'widget.chatbubble.petrevived';
if(chatType === RoomSessionChatEvent.CHAT_TYPE_PET_REBREED_FERTILIZE) if(chatType === RoomSessionChatEvent.CHAT_TYPE_PET_REBREED_FERTILIZE)
{ {
textKey = 'widget.chatbubble.petrefertilized;'; textKey = 'widget.chatbubble.petrefertilized;';
} }
else if(chatType === RoomSessionChatEvent.CHAT_TYPE_PET_SPEED_FERTILIZE) else if(chatType === RoomSessionChatEvent.CHAT_TYPE_PET_SPEED_FERTILIZE)
{ {
textKey = 'widget.chatbubble.petspeedfertilized'; textKey = 'widget.chatbubble.petspeedfertilized';
} }
let targetUserName: string = null; let targetUserName: string = null;
const newRoomObject = GetRoomEngine().getRoomObject(roomSession.roomId, event.extraParam, RoomObjectCategory.UNIT); const newRoomObject = GetRoomEngine().getRoomObject(roomSession.roomId, event.extraParam, RoomObjectCategory.UNIT);
if(newRoomObject) if(newRoomObject)
{ {
const newUserData = roomSession.userDataManager.getUserDataByIndex(roomObject.id); const newUserData = roomSession.userDataManager.getUserDataByIndex(roomObject.id);
if(newUserData) targetUserName = newUserData.name; if(newUserData) targetUserName = newUserData.name;
} }
text = LocalizeText(textKey, [ 'petName', 'userName' ], [ username, targetUserName ]); text = LocalizeText(textKey, [ 'petName', 'userName' ], [ username, targetUserName ]);
break; break;
} }
@ -209,7 +214,6 @@ const useChatWidgetState = () =>
const hours = ((event.extraParam > 0) ? Math.floor((event.extraParam / 3600)) : 0).toString(); const hours = ((event.extraParam > 0) ? Math.floor((event.extraParam / 3600)) : 0).toString();
const minutes = ((event.extraParam > 0) ? Math.floor((event.extraParam % 3600) / 60) : 0).toString(); const minutes = ((event.extraParam > 0) ? Math.floor((event.extraParam % 3600) / 60) : 0).toString();
const seconds = (event.extraParam % 60).toString(); const seconds = (event.extraParam % 60).toString();
text = LocalizeText('widget.chatbubble.mutetime', [ 'hours', 'minutes', 'seconds' ], [ hours, minutes, seconds ]); text = LocalizeText('widget.chatbubble.mutetime', [ 'hours', 'minutes', 'seconds' ], [ hours, minutes, seconds ]);
break; break;
} }
@ -233,6 +237,9 @@ const useChatWidgetState = () =>
chatColours chatColours
); );
// Add userType to ChatBubbleMessage as a dynamic property
(chatMessage as any).userType = userType;
setChatMessages(prevValue => [ ...prevValue, chatMessage ]); setChatMessages(prevValue => [ ...prevValue, chatMessage ]);
addChatEntry({ id: -1, webId: userData.webID, entityId: userData.roomIndex, name: username, imageUrl, style: styleId, chatType: chatType, entityType: userData.type, message: formattedText, timestamp: ChatHistoryCurrentDate(), type: ChatEntryType.TYPE_CHAT, roomId: roomSession.roomId, color, chatColours }); addChatEntry({ id: -1, webId: userData.webID, entityId: userData.roomIndex, name: username, imageUrl, style: styleId, chatType: chatType, entityType: userData.type, message: formattedText, timestamp: ChatHistoryCurrentDate(), type: ChatEntryType.TYPE_CHAT, roomId: roomSession.roomId, color, chatColours });
}); });