Merge upstream/main into feature/checkpoint-20260403

This commit is contained in:
Lorenzune
2026-04-03 05:26:19 +02:00
11 changed files with 181 additions and 84 deletions
+5 -1
View File
@@ -13,6 +13,8 @@ export const GetSubscriptionProductIcon = (id: number) =>
export const GetOfferNodes = (offerNodes: Map<number, ICatalogNode[]>, offerId: number) => export const GetOfferNodes = (offerNodes: Map<number, ICatalogNode[]>, offerId: number) =>
{ {
if(!offerNodes) return [];
const nodes = offerNodes.get(offerId); const nodes = offerNodes.get(offerId);
const allowedNodes: ICatalogNode[] = []; const allowedNodes: ICatalogNode[] = [];
@@ -20,7 +22,7 @@ export const GetOfferNodes = (offerNodes: Map<number, ICatalogNode[]>, offerId:
{ {
for(const node of nodes) for(const node of nodes)
{ {
if(!node.isVisible) continue; if(!node || !node.isVisible) continue;
allowedNodes.push(node); allowedNodes.push(node);
} }
@@ -31,6 +33,8 @@ export const GetOfferNodes = (offerNodes: Map<number, ICatalogNode[]>, offerId:
export const FilterCatalogNode = (search: string, furniLines: string[], node: ICatalogNode, nodes: ICatalogNode[]) => export const FilterCatalogNode = (search: string, furniLines: string[], node: ICatalogNode, nodes: ICatalogNode[]) =>
{ {
if(!node) return;
if(node.isVisible && (node.pageId > 0)) if(node.isVisible && (node.pageId > 0))
{ {
let nodeAdded = false; let nodeAdded = false;
+2 -2
View File
@@ -105,12 +105,12 @@ export class FurnitureOffer implements IPurchasableOffer
public get localizationName(): string public get localizationName(): string
{ {
return this._furniData.name; return this._furniData?.name ?? '';
} }
public get localizationDescription(): string public get localizationDescription(): string
{ {
return this._furniData.description; return this._furniData?.description ?? '';
} }
public get isLazy(): boolean public get isLazy(): boolean
+24 -7
View File
@@ -1,6 +1,7 @@
import { AvatarScaleType, AvatarSetType, GetAvatarRenderManager } from '@nitrots/nitro-renderer'; import { AvatarScaleType, AvatarSetType, GetAvatarRenderManager, NitroEventType } from '@nitrots/nitro-renderer';
import { CSSProperties, FC, useEffect, useMemo, useRef, useState } from 'react'; import { CSSProperties, FC, useEffect, useMemo, useRef, useState } from 'react';
import { Base, BaseProps } from '../Base'; import { Base, BaseProps } from '../Base';
import { useNitroEvent } from '../../hooks/events';
const AVATAR_IMAGE_CACHE: Map<string, string> = new Map(); const AVATAR_IMAGE_CACHE: Map<string, string> = new Map();
@@ -18,7 +19,18 @@ export const LayoutAvatarImageView: FC<LayoutAvatarImageViewProps> = props =>
const { figure = '', gender = '', headOnly = false, direction = 0, scale = 1, classNames = [], style = {}, ...rest } = props; const { figure = '', gender = '', headOnly = false, direction = 0, scale = 1, classNames = [], style = {}, ...rest } = props;
const [ avatarUrl, setAvatarUrl ] = useState<string>(null); const [ avatarUrl, setAvatarUrl ] = useState<string>(null);
const [ isReady, setIsReady ] = useState<boolean>(false); const [ isReady, setIsReady ] = useState<boolean>(false);
const [ updateId, setUpdateId ] = useState<number>(0);
const isDisposed = useRef(false); const isDisposed = useRef(false);
const figureKeyRef = useRef<string>(null);
useNitroEvent(NitroEventType.AVATAR_ASSET_LOADED, () =>
{
if(figureKeyRef.current)
{
AVATAR_IMAGE_CACHE.delete(figureKeyRef.current);
setUpdateId(prev => prev + 1);
}
});
const getClassNames = useMemo(() => const getClassNames = useMemo(() =>
{ {
@@ -53,17 +65,25 @@ export const LayoutAvatarImageView: FC<LayoutAvatarImageViewProps> = props =>
const figureKey = [ figure, gender, direction, headOnly ].join('-'); const figureKey = [ figure, gender, direction, headOnly ].join('-');
figureKeyRef.current = figureKey;
if(AVATAR_IMAGE_CACHE.has(figureKey)) if(AVATAR_IMAGE_CACHE.has(figureKey))
{ {
setAvatarUrl(AVATAR_IMAGE_CACHE.get(figureKey)); setAvatarUrl(AVATAR_IMAGE_CACHE.get(figureKey));
} }
else else
{ {
const resetFigure = (_figure: string) => const avatarImage = GetAvatarRenderManager().createAvatarImage(figure, AvatarScaleType.LARGE, gender, {
resetFigure: (figure: string) =>
{ {
if(isDisposed.current) return; if(isDisposed.current) return;
const avatarImage = GetAvatarRenderManager().createAvatarImage(_figure, AvatarScaleType.LARGE, gender, { resetFigure: (figure: string) => resetFigure(figure), dispose: null, disposed: false }); AVATAR_IMAGE_CACHE.delete(figureKey);
setUpdateId(prev => prev + 1);
},
dispose: null,
disposed: false
});
let setType = AvatarSetType.FULL; let setType = AvatarSetType.FULL;
@@ -81,11 +101,8 @@ export const LayoutAvatarImageView: FC<LayoutAvatarImageViewProps> = props =>
} }
avatarImage.dispose(); avatarImage.dispose();
};
resetFigure(figure);
} }
}, [ figure, gender, direction, headOnly, isReady ]); }, [ figure, gender, direction, headOnly, isReady, updateId ]);
useEffect(() => useEffect(() =>
{ {
@@ -1,7 +1,9 @@
import { NitroEventType } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react'; import { FC, useEffect, useState } from 'react';
import { AvatarEditorThumbnailsHelper, GetClubMemberLevel, GetConfigurationValue, IAvatarEditorCategoryPartItem } from '../../../api'; import { AvatarEditorThumbnailsHelper, GetClubMemberLevel, GetConfigurationValue, IAvatarEditorCategoryPartItem } from '../../../api';
import { LayoutCurrencyIcon, LayoutGridItemProps } from '../../../common'; import { LayoutCurrencyIcon, LayoutGridItemProps } from '../../../common';
import { useAvatarEditor } from '../../../hooks'; import { useAvatarEditor } from '../../../hooks';
import { useNitroEvent } from '../../../hooks/events';
import { InfiniteGrid } from '../../../layout'; import { InfiniteGrid } from '../../../layout';
import { AvatarEditorIcon } from '../AvatarEditorIcon'; import { AvatarEditorIcon } from '../AvatarEditorIcon';
@@ -14,6 +16,7 @@ export const AvatarEditorFigureSetItemView: FC<{
{ {
const { setType = null, partItem = null, isSelected = false, width = '100%', ...rest } = props; const { setType = null, partItem = null, isSelected = false, width = '100%', ...rest } = props;
const [ assetUrl, setAssetUrl ] = useState<string>(''); const [ assetUrl, setAssetUrl ] = useState<string>('');
const [ retryId, setRetryId ] = useState<number>(0);
const { selectedColorParts = null, getFigureStringWithFace = null } = useAvatarEditor(); const { selectedColorParts = null, getFigureStringWithFace = null } = useAvatarEditor();
const clubLevel = partItem.partSet?.clubLevel ?? 0; const clubLevel = partItem.partSet?.clubLevel ?? 0;
@@ -21,6 +24,15 @@ export const AvatarEditorFigureSetItemView: FC<{
const isLocked = isHC && (GetClubMemberLevel() < clubLevel); const isLocked = isHC && (GetClubMemberLevel() < clubLevel);
const isSellableNotOwned = partItem.isSellableNotOwned ?? false; const isSellableNotOwned = partItem.isSellableNotOwned ?? false;
useNitroEvent(NitroEventType.AVATAR_ASSET_LOADED, () =>
{
if(!assetUrl || !assetUrl.length)
{
AvatarEditorThumbnailsHelper.clearCache();
setRetryId(prev => prev + 1);
}
});
useEffect(() => useEffect(() =>
{ {
setAssetUrl(''); setAssetUrl('');
@@ -54,7 +66,7 @@ export const AvatarEditorFigureSetItemView: FC<{
}; };
loadImage(); loadImage();
}, [ setType, partItem, selectedColorParts, getFigureStringWithFace, isSellableNotOwned ]); }, [ setType, partItem, selectedColorParts, getFigureStringWithFace, isSellableNotOwned, retryId ]);
if(!partItem) return null; if(!partItem) return null;
@@ -18,16 +18,18 @@ export const CatalogGridOfferView: FC<CatalogGridOfferViewProps> = props =>
const { requestOfferToMover = null } = useCatalog(); const { requestOfferToMover = null } = useCatalog();
const { isVisible = false } = useInventoryFurni(); const { isVisible = false } = useInventoryFurni();
const { isFavoriteOffer, toggleFavoriteOffer } = useCatalogFavorites(); const { isFavoriteOffer, toggleFavoriteOffer } = useCatalogFavorites();
const isFav = isFavoriteOffer(offer.offerId); const isFav = offer ? isFavoriteOffer(offer.offerId) : false;
const iconUrl = useMemo(() => const iconUrl = useMemo(() =>
{ {
if(!offer) return null;
if(offer.pricingModel === Offer.PRICING_MODEL_BUNDLE) if(offer.pricingModel === Offer.PRICING_MODEL_BUNDLE)
{ {
return null; return null;
} }
return offer.product.getIconUrl(offer); return offer.product?.getIconUrl(offer) ?? null;
}, [ offer ]); }, [ offer ]);
const onMouseEvent = (event: MouseEvent) => const onMouseEvent = (event: MouseEvent) =>
@@ -49,6 +51,8 @@ export const CatalogGridOfferView: FC<CatalogGridOfferViewProps> = props =>
} }
}; };
if(!offer) return null;
const product = offer.product; const product = offer.product;
if(!product) return null; if(!product) return null;
@@ -22,6 +22,8 @@ export const CatalogSearchView: FC<{}> = () =>
const timeout = setTimeout(() => const timeout = setTimeout(() =>
{ {
if(!offersToNodes || !rootNode) return;
const furnitureDatas = GetSessionDataManager().getAllFurnitureData(); const furnitureDatas = GetSessionDataManager().getAllFurnitureData();
if(!furnitureDatas || !furnitureDatas.length) return; if(!furnitureDatas || !furnitureDatas.length) return;
@@ -31,11 +33,13 @@ export const CatalogSearchView: FC<{}> = () =>
for(const furniture of furnitureDatas) for(const furniture of furnitureDatas)
{ {
if(!furniture) continue;
if((currentType === CatalogType.BUILDER) && !furniture.availableForBuildersClub) continue; if((currentType === CatalogType.BUILDER) && !furniture.availableForBuildersClub) continue;
if((currentType === CatalogType.NORMAL) && furniture.excludeDynamic) continue; if((currentType === CatalogType.NORMAL) && furniture.excludeDynamic) continue;
const searchValues = [ furniture.className, furniture.name, furniture.description ].join(' ').replace(/ /gi, '').toLowerCase(); const searchValues = [ furniture.className || '', furniture.name || '', furniture.description || '' ].join(' ').replace(/ /gi, '').toLowerCase();
if((currentType === CatalogType.BUILDER) && (furniture.purchaseOfferId === -1) && (furniture.rentOfferId === -1)) if((currentType === CatalogType.BUILDER) && (furniture.purchaseOfferId === -1) && (furniture.rentOfferId === -1))
{ {
@@ -16,6 +16,8 @@ export const FurnitureYoutubeDisplayView: FC<{}> = FurnitureYoutubeDisplayViewPr
const { objectId = -1, videoId = null, videoStart = 0, videoEnd = 0, currentVideoState = null, selectedVideo = null, playlists = [], onClose = null, previous = null, next = null, pause = null, play = null, selectVideo = null } = useFurnitureYoutubeWidget(); const { objectId = -1, videoId = null, videoStart = 0, videoEnd = 0, currentVideoState = null, selectedVideo = null, playlists = [], onClose = null, previous = null, next = null, pause = null, play = null, selectVideo = null } = useFurnitureYoutubeWidget();
const onStateChange = (event: { target: YouTubePlayer; data: number }) => const onStateChange = (event: { target: YouTubePlayer; data: number }) =>
{
try
{ {
setPlayer(event.target); setPlayer(event.target);
@@ -25,22 +27,21 @@ export const FurnitureYoutubeDisplayView: FC<{}> = FurnitureYoutubeDisplayViewPr
{ {
case -1: case -1:
case 1: case 1:
if(currentVideoState === 2)
{
//event.target.pauseVideo();
}
if(currentVideoState !== 1) play(); if(currentVideoState !== 1) play();
return; return;
case 2: case 2:
if(currentVideoState !== 2) pause(); if(currentVideoState !== 2) pause();
} }
}
catch(err) {}
}; };
useEffect(() => useEffect(() =>
{ {
if((currentVideoState === null) || !player) return; if((currentVideoState === null) || !player) return;
try
{
if((currentVideoState === YoutubeVideoPlaybackStateEnum.PLAYING) && (player.getPlayerState() !== YoutubeVideoPlaybackStateEnum.PLAYING)) if((currentVideoState === YoutubeVideoPlaybackStateEnum.PLAYING) && (player.getPlayerState() !== YoutubeVideoPlaybackStateEnum.PLAYING))
{ {
player.playVideo(); player.playVideo();
@@ -54,6 +55,11 @@ export const FurnitureYoutubeDisplayView: FC<{}> = FurnitureYoutubeDisplayViewPr
return; return;
} }
}
catch(err)
{
setPlayer(null);
}
}, [ currentVideoState, player ]); }, [ currentVideoState, player ]);
if(objectId === -1) return null; if(objectId === -1) return null;
@@ -8,8 +8,11 @@ export const UserContainerView: FC<{
}> = props => }> = props =>
{ {
const { userProfile = null } = props; const { userProfile = null } = props;
const [ requestSent, setRequestSent ] = useState(userProfile.requestSent); const [ requestSent, setRequestSent ] = useState(userProfile.requestSent);
const isOwnProfile = (userProfile.id === GetSessionDataManager().userId); const isOwnProfile = (userProfile.id === GetSessionDataManager().userId);
const canSendFriendRequest = !requestSent && (!isOwnProfile && !userProfile.isMyFriend && !userProfile.requestSent); const canSendFriendRequest = !requestSent && (!isOwnProfile && !userProfile.isMyFriend && !userProfile.requestSent);
const infostandBackgroundClass = `background-${userProfile.backgroundId ?? 'default'}`; const infostandBackgroundClass = `background-${userProfile.backgroundId ?? 'default'}`;
@@ -35,36 +38,75 @@ export const UserContainerView: FC<{
<LayoutAvatarImageView direction={ 2 } figure={ userProfile.figure } /> <LayoutAvatarImageView direction={ 2 } figure={ userProfile.figure } />
<div className={`absolute inset-0 profile-overlay ${infostandOverlayClass}`} /> <div className={`absolute inset-0 profile-overlay ${infostandOverlayClass}`} />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="flex flex-col gap-0"> <div className="flex flex-col gap-0">
<p className="leading-tight font-bold">{ userProfile.username }</p> <p className="leading-tight font-bold">{ userProfile.username }</p>
<p className="text-sm italic leading-tight">{ userProfile.motto }</p> <p className="text-sm italic leading-tight">{ userProfile.motto }</p>
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<p className="text-sm leading-none" dangerouslySetInnerHTML={{ __html: LocalizeText('extendedprofile.created', ['created'], [userProfile.registration]) }} /> <p
<p className="text-sm leading-none" dangerouslySetInnerHTML={{ __html: LocalizeText('extendedprofile.last.login', ['lastlogin'], [FriendlyTime.format(userProfile.secondsSinceLastVisit, '.ago', 2)]) }} /> className="text-sm leading-none"
<p className="text-sm leading-none"> dangerouslySetInnerHTML={{
<b>{ LocalizeText('extendedprofile.friends.count') }</b> { userProfile.friendsCount } __html: LocalizeText(
</p> 'extendedprofile.created',
['created'],
[userProfile.registration]
)
}}
/>
<p
className="text-sm leading-none"
dangerouslySetInnerHTML={{
__html: LocalizeText(
'extendedprofile.last.login',
['lastlogin'],
[FriendlyTime.format(userProfile.secondsSinceLastVisit, '.ago', 2)]
)
}}
/>
<p
className="text-sm leading-none"
dangerouslySetInnerHTML={{
__html: LocalizeText(
'extendedprofile.friends.count',
['count'],
[userProfile.friendsCount]
)
}}
/>
<p className="text-sm leading-none"> <p className="text-sm leading-none">
<b>{ LocalizeText('extendedprofile.achievementscore') }</b> { userProfile.achievementPoints } <b>{ LocalizeText('extendedprofile.achievementscore') }</b> { userProfile.achievementPoints }
</p> </p>
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
{ userProfile.isOnline && { userProfile.isOnline &&
<i className="nitro-icon icon-pf-online" /> } <i className="nitro-icon icon-pf-online" /> }
{ !userProfile.isOnline && { !userProfile.isOnline &&
<i className="nitro-icon icon-pf-offline" /> } <i className="nitro-icon icon-pf-offline" /> }
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
{ canSendFriendRequest && { canSendFriendRequest &&
<Text pointer small underline onClick={ addFriend }>{ LocalizeText('extendedprofile.addasafriend') }</Text> } <Text pointer small underline onClick={ addFriend }>
{ LocalizeText('extendedprofile.addasafriend') }
</Text> }
{ !canSendFriendRequest && { !canSendFriendRequest &&
<> <>
<i className="nitro-icon icon-pf-tick" /> <i className="nitro-icon icon-pf-tick" />
{ isOwnProfile && { isOwnProfile &&
<p>{ LocalizeText('extendedprofile.me') }</p> } <p>{ LocalizeText('extendedprofile.me') }</p> }
{ userProfile.isMyFriend && { userProfile.isMyFriend &&
<p>{ LocalizeText('extendedprofile.friend') }</p> } <p>{ LocalizeText('extendedprofile.friend') }</p> }
{ (requestSent || userProfile.requestSent) && { (requestSent || userProfile.requestSent) &&
<p>{ LocalizeText('extendedprofile.friendrequestsent') }</p> } <p>{ LocalizeText('extendedprofile.friendrequestsent') }</p> }
</> } </> }
+13 -13
View File
@@ -9,7 +9,7 @@ import { FriendsContainerView } from './FriendsContainerView';
import { GroupsContainerView } from './GroupsContainerView'; import { GroupsContainerView } from './GroupsContainerView';
import { UserContainerView } from './UserContainerView'; import { UserContainerView } from './UserContainerView';
type ProfileTab = 'badge' | 'amici' | 'stanze' | 'gruppi'; type ProfileTab = 'badge' | 'friends' | 'rooms' | 'groups';
export const UserProfileView: FC<{}> = props => export const UserProfileView: FC<{}> = props =>
{ {
@@ -39,7 +39,7 @@ export const UserProfileView: FC<{}> = props =>
{ {
setActiveTab(tab); setActiveTab(tab);
if(tab === 'stanze' && !userRooms && userProfile) if(tab === 'rooms' && !userRooms && userProfile)
{ {
SendMessageComposer(new NavigatorSearchComposer('hotel_view', `owner:${ userProfile.username }`)); SendMessageComposer(new NavigatorSearchComposer('hotel_view', `owner:${ userProfile.username }`));
} }
@@ -99,7 +99,7 @@ export const UserProfileView: FC<{}> = props =>
useMessageEvent<NavigatorSearchEvent>(NavigatorSearchEvent, event => useMessageEvent<NavigatorSearchEvent>(NavigatorSearchEvent, event =>
{ {
if(!userProfile || activeTab !== 'stanze') return; if(!userProfile || activeTab !== 'rooms') return;
const parser = event.getParser(); const parser = event.getParser();
const result = parser.result; const result = parser.result;
@@ -145,16 +145,16 @@ export const UserProfileView: FC<{}> = props =>
</div> </div>
<NitroCard.Tabs> <NitroCard.Tabs>
<NitroCard.TabItem isActive={ activeTab === 'badge' } count={ userBadges.length } onClick={ () => onTabClick('badge') }> <NitroCard.TabItem isActive={ activeTab === 'badge' } count={ userBadges.length } onClick={ () => onTabClick('badge') }>
{ LocalizeText('extendedprofile.tab.badge') } { LocalizeText('levelinfo.category.badge') }
</NitroCard.TabItem> </NitroCard.TabItem>
<NitroCard.TabItem isActive={ activeTab === 'amici' } count={ userProfile.friendsCount } onClick={ () => onTabClick('amici') }> <NitroCard.TabItem isActive={ activeTab === 'friends' } count={ userProfile.friendsCount } onClick={ () => onTabClick('friends') }>
{ LocalizeText('extendedprofile.tab.friends') } { LocalizeText('navigator.tab.3') }
</NitroCard.TabItem> </NitroCard.TabItem>
<NitroCard.TabItem isActive={ activeTab === 'stanze' } onClick={ () => onTabClick('stanze') }> <NitroCard.TabItem isActive={ activeTab === 'rooms' } onClick={ () => onTabClick('rooms') }>
{ LocalizeText('extendedprofile.tab.rooms') } { LocalizeText('navigator.tab.2') }
</NitroCard.TabItem> </NitroCard.TabItem>
<NitroCard.TabItem isActive={ activeTab === 'gruppi' } count={ userProfile.groups?.length } onClick={ () => onTabClick('gruppi') }> <NitroCard.TabItem isActive={ activeTab === 'groups' } count={ userProfile.groups?.length } onClick={ () => onTabClick('groups') }>
{ LocalizeText('extendedprofile.tab.groups') } { LocalizeText('navigator.searchcode.title.groups') }
</NitroCard.TabItem> </NitroCard.TabItem>
</NitroCard.Tabs> </NitroCard.Tabs>
<div className="flex-1 overflow-auto p-2"> <div className="flex-1 overflow-auto p-2">
@@ -172,7 +172,7 @@ export const UserProfileView: FC<{}> = props =>
} }
</div> </div>
) } ) }
{ activeTab === 'amici' && ( { activeTab === 'friends' && (
<div className="flex flex-col gap-2 h-full"> <div className="flex flex-col gap-2 h-full">
{ userRelationships ? ( { userRelationships ? (
<FriendsContainerView friendsCount={ userProfile.friendsCount } relationships={ userRelationships } /> <FriendsContainerView friendsCount={ userProfile.friendsCount } relationships={ userRelationships } />
@@ -183,7 +183,7 @@ export const UserProfileView: FC<{}> = props =>
) } ) }
</div> </div>
) } ) }
{ activeTab === 'stanze' && ( { activeTab === 'rooms' && (
<div className="flex flex-col gap-1 h-full"> <div className="flex flex-col gap-1 h-full">
{ !userRooms && ( { !userRooms && (
<Flex center className="h-full"> <Flex center className="h-full">
@@ -206,7 +206,7 @@ export const UserProfileView: FC<{}> = props =>
)) } )) }
</div> </div>
) } ) }
{ activeTab === 'gruppi' && ( { activeTab === 'groups' && (
<div className="h-full"> <div className="h-full">
<GroupsContainerView fullWidth groups={ userProfile.groups } itsMe={ userProfile.id === GetSessionDataManager().userId } onLeaveGroup={ onLeaveGroup } /> <GroupsContainerView fullWidth groups={ userProfile.groups } itsMe={ userProfile.id === GetSessionDataManager().userId } onLeaveGroup={ onLeaveGroup } />
</div> </div>
+9 -7
View File
@@ -60,7 +60,7 @@ export const useFurniEditor = () =>
const [ catalogItems, setCatalogItems ] = useState<CatalogRef[]>([]); const [ catalogItems, setCatalogItems ] = useState<CatalogRef[]>([]);
const [ interactions, setInteractions ] = useState<string[]>([]); const [ interactions, setInteractions ] = useState<string[]>([]);
const [ furniDataEntry, setFurniDataEntry ] = useState<Record<string, unknown> | null>(null); const [ furniDataEntry, setFurniDataEntry ] = useState<Record<string, unknown> | null>(null);
const pendingActionRef = useRef<string | null>(null); const pendingActionRef = useRef<{ action: string; itemId: number } | null>(null);
const { simpleAlert = null } = useNotification(); const { simpleAlert = null } = useNotification();
const clearError = useCallback(() => setError(null), []); const clearError = useCallback(() => setError(null), []);
@@ -161,7 +161,9 @@ export const useFurniEditor = () =>
useMessageEvent(FurniEditorResultEvent, (event: FurniEditorResultEvent) => useMessageEvent(FurniEditorResultEvent, (event: FurniEditorResultEvent) =>
{ {
const parser = event.getParser(); const parser = event.getParser();
const action = pendingActionRef.current; const pending = pendingActionRef.current;
const action = pending?.action ?? null;
const actionItemId = pending?.itemId ?? null;
pendingActionRef.current = null; pendingActionRef.current = null;
setLoading(false); setLoading(false);
@@ -182,10 +184,10 @@ export const useFurniEditor = () =>
if(action === 'update') if(action === 'update')
{ {
// Auto-reload detail after update // Auto-reload detail after update using the ID from the original request
if(selectedItem) if(actionItemId)
{ {
SendMessageComposer(new FurniEditorDetailComposer(selectedItem.id)); SendMessageComposer(new FurniEditorDetailComposer(actionItemId));
} }
if(simpleAlert) if(simpleAlert)
@@ -231,7 +233,7 @@ export const useFurniEditor = () =>
{ {
setLoading(true); setLoading(true);
setError(null); setError(null);
pendingActionRef.current = 'update'; pendingActionRef.current = { action: 'update', itemId: id };
SendMessageComposer(new FurniEditorUpdateComposer(id, JSON.stringify(fields))); SendMessageComposer(new FurniEditorUpdateComposer(id, JSON.stringify(fields)));
}, []); }, []);
@@ -239,7 +241,7 @@ export const useFurniEditor = () =>
{ {
setLoading(true); setLoading(true);
setError(null); setError(null);
pendingActionRef.current = 'delete'; pendingActionRef.current = { action: 'delete', itemId: id };
SendMessageComposer(new FurniEditorDeleteComposer(id)); SendMessageComposer(new FurniEditorDeleteComposer(id));
}, []); }, []);
@@ -1,5 +1,5 @@
import { ControlYoutubeDisplayPlaybackMessageComposer, GetRoomEngine, GetSessionDataManager, GetYoutubeDisplayStatusMessageComposer, RoomEngineTriggerWidgetEvent, RoomId, SecurityLevel, SetYoutubeDisplayPlaylistMessageComposer, YoutubeControlVideoMessageEvent, YoutubeDisplayPlaylist, YoutubeDisplayPlaylistsEvent, YoutubeDisplayVideoMessageEvent } from '@nitrots/nitro-renderer'; import { ControlYoutubeDisplayPlaybackMessageComposer, GetRoomEngine, GetSessionDataManager, GetYoutubeDisplayStatusMessageComposer, RoomEngineTriggerWidgetEvent, RoomId, SecurityLevel, SetYoutubeDisplayPlaylistMessageComposer, YoutubeControlVideoMessageEvent, YoutubeDisplayPlaylist, YoutubeDisplayPlaylistsEvent, YoutubeDisplayVideoMessageEvent } from '@nitrots/nitro-renderer';
import { useState } from 'react'; import { useRef, useState } from 'react';
import { IsOwnerOfFurniture, SendMessageComposer, YoutubeVideoPlaybackStateEnum } from '../../../../api'; import { IsOwnerOfFurniture, SendMessageComposer, YoutubeVideoPlaybackStateEnum } from '../../../../api';
import { useMessageEvent, useNitroEvent } from '../../../events'; import { useMessageEvent, useNitroEvent } from '../../../events';
import { useFurniRemovedEvent } from '../../engine'; import { useFurniRemovedEvent } from '../../engine';
@@ -13,6 +13,7 @@ const useFurnitureYoutubeWidgetState = () =>
{ {
const [ objectId, setObjectId ] = useState(-1); const [ objectId, setObjectId ] = useState(-1);
const [ category, setCategory ] = useState(-1); const [ category, setCategory ] = useState(-1);
const objectIdRef = useRef(-1);
const [ videoId, setVideoId ] = useState<string>(null); const [ videoId, setVideoId ] = useState<string>(null);
const [ videoStart, setVideoStart ] = useState<number>(null); const [ videoStart, setVideoStart ] = useState<number>(null);
const [ videoEnd, setVideoEnd ] = useState<number>(null); const [ videoEnd, setVideoEnd ] = useState<number>(null);
@@ -23,6 +24,7 @@ const useFurnitureYoutubeWidgetState = () =>
const onClose = () => const onClose = () =>
{ {
objectIdRef.current = -1;
setObjectId(-1); setObjectId(-1);
setCategory(-1); setCategory(-1);
setVideoId(null); setVideoId(null);
@@ -64,6 +66,7 @@ const useFurnitureYoutubeWidgetState = () =>
if(!roomObject) return; if(!roomObject) return;
objectIdRef.current = event.objectId;
setObjectId(event.objectId); setObjectId(event.objectId);
setCategory(event.category); setCategory(event.category);
setHasControl(GetSessionDataManager().hasSecurity(SecurityLevel.EMPLOYEE) || IsOwnerOfFurniture(roomObject)); setHasControl(GetSessionDataManager().hasSecurity(SecurityLevel.EMPLOYEE) || IsOwnerOfFurniture(roomObject));
@@ -74,8 +77,9 @@ const useFurnitureYoutubeWidgetState = () =>
useMessageEvent<YoutubeDisplayVideoMessageEvent>(YoutubeDisplayVideoMessageEvent, event => useMessageEvent<YoutubeDisplayVideoMessageEvent>(YoutubeDisplayVideoMessageEvent, event =>
{ {
const parser = event.getParser(); const parser = event.getParser();
const currentObjectId = objectIdRef.current;
if((objectId === -1) || (objectId !== parser.furniId)) return; if((currentObjectId === -1) || (currentObjectId !== parser.furniId)) return;
setVideoId(parser.videoId); setVideoId(parser.videoId);
setVideoStart(parser.startAtSeconds); setVideoStart(parser.startAtSeconds);
@@ -86,8 +90,9 @@ const useFurnitureYoutubeWidgetState = () =>
useMessageEvent<YoutubeDisplayPlaylistsEvent>(YoutubeDisplayPlaylistsEvent, event => useMessageEvent<YoutubeDisplayPlaylistsEvent>(YoutubeDisplayPlaylistsEvent, event =>
{ {
const parser = event.getParser(); const parser = event.getParser();
const currentObjectId = objectIdRef.current;
if((objectId === -1) || (objectId !== parser.furniId)) return; if((currentObjectId === -1) || (currentObjectId !== parser.furniId)) return;
setPlaylists(parser.playlists); setPlaylists(parser.playlists);
setSelectedVideo(parser.selectedPlaylistId); setSelectedVideo(parser.selectedPlaylistId);
@@ -100,8 +105,9 @@ const useFurnitureYoutubeWidgetState = () =>
useMessageEvent<YoutubeControlVideoMessageEvent>(YoutubeControlVideoMessageEvent, event => useMessageEvent<YoutubeControlVideoMessageEvent>(YoutubeControlVideoMessageEvent, event =>
{ {
const parser = event.getParser(); const parser = event.getParser();
const currentObjectId = objectIdRef.current;
if((objectId === -1) || (objectId !== parser.furniId)) return; if((currentObjectId === -1) || (currentObjectId !== parser.furniId)) return;
switch(parser.commandId) switch(parser.commandId)
{ {