diff --git a/src/api/catalog/CatalogUtilities.ts b/src/api/catalog/CatalogUtilities.ts index c2e1d5e..9cb8866 100644 --- a/src/api/catalog/CatalogUtilities.ts +++ b/src/api/catalog/CatalogUtilities.ts @@ -13,6 +13,8 @@ export const GetSubscriptionProductIcon = (id: number) => export const GetOfferNodes = (offerNodes: Map, offerId: number) => { + if(!offerNodes) return []; + const nodes = offerNodes.get(offerId); const allowedNodes: ICatalogNode[] = []; @@ -20,7 +22,7 @@ export const GetOfferNodes = (offerNodes: Map, offerId: { for(const node of nodes) { - if(!node.isVisible) continue; + if(!node || !node.isVisible) continue; allowedNodes.push(node); } @@ -31,6 +33,8 @@ export const GetOfferNodes = (offerNodes: Map, offerId: export const FilterCatalogNode = (search: string, furniLines: string[], node: ICatalogNode, nodes: ICatalogNode[]) => { + if(!node) return; + if(node.isVisible && (node.pageId > 0)) { let nodeAdded = false; diff --git a/src/api/catalog/FurnitureOffer.ts b/src/api/catalog/FurnitureOffer.ts index 367f247..5b68c11 100644 --- a/src/api/catalog/FurnitureOffer.ts +++ b/src/api/catalog/FurnitureOffer.ts @@ -105,12 +105,12 @@ export class FurnitureOffer implements IPurchasableOffer public get localizationName(): string { - return this._furniData.name; + return this._furniData?.name ?? ''; } public get localizationDescription(): string { - return this._furniData.description; + return this._furniData?.description ?? ''; } public get isLazy(): boolean diff --git a/src/common/layout/LayoutAvatarImageView.tsx b/src/common/layout/LayoutAvatarImageView.tsx index 2b3b156..fdde1ac 100644 --- a/src/common/layout/LayoutAvatarImageView.tsx +++ b/src/common/layout/LayoutAvatarImageView.tsx @@ -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 { Base, BaseProps } from '../Base'; +import { useNitroEvent } from '../../hooks/events'; const AVATAR_IMAGE_CACHE: Map = new Map(); @@ -18,7 +19,18 @@ export const LayoutAvatarImageView: FC = props => const { figure = '', gender = '', headOnly = false, direction = 0, scale = 1, classNames = [], style = {}, ...rest } = props; const [ avatarUrl, setAvatarUrl ] = useState(null); const [ isReady, setIsReady ] = useState(false); + const [ updateId, setUpdateId ] = useState(0); const isDisposed = useRef(false); + const figureKeyRef = useRef(null); + + useNitroEvent(NitroEventType.AVATAR_ASSET_LOADED, () => + { + if(figureKeyRef.current) + { + AVATAR_IMAGE_CACHE.delete(figureKeyRef.current); + setUpdateId(prev => prev + 1); + } + }); const getClassNames = useMemo(() => { @@ -53,39 +65,44 @@ export const LayoutAvatarImageView: FC = props => const figureKey = [ figure, gender, direction, headOnly ].join('-'); + figureKeyRef.current = figureKey; + if(AVATAR_IMAGE_CACHE.has(figureKey)) { setAvatarUrl(AVATAR_IMAGE_CACHE.get(figureKey)); } else { - const resetFigure = (_figure: string) => - { - if(isDisposed.current) return; - - const avatarImage = GetAvatarRenderManager().createAvatarImage(_figure, AvatarScaleType.LARGE, gender, { resetFigure: (figure: string) => resetFigure(figure), dispose: null, disposed: false }); - - let setType = AvatarSetType.FULL; - - if(headOnly) setType = AvatarSetType.HEAD; - - avatarImage.setDirection(setType, direction); - - const imageUrl = avatarImage.processAsImageUrl(setType); - - if(imageUrl && !isDisposed.current) + const avatarImage = GetAvatarRenderManager().createAvatarImage(figure, AvatarScaleType.LARGE, gender, { + resetFigure: (figure: string) => { - if(!avatarImage.isPlaceholder()) AVATAR_IMAGE_CACHE.set(figureKey, imageUrl); + if(isDisposed.current) return; - setAvatarUrl(imageUrl); - } + AVATAR_IMAGE_CACHE.delete(figureKey); + setUpdateId(prev => prev + 1); + }, + dispose: null, + disposed: false + }); - avatarImage.dispose(); - }; + let setType = AvatarSetType.FULL; - resetFigure(figure); + if(headOnly) setType = AvatarSetType.HEAD; + + avatarImage.setDirection(setType, direction); + + const imageUrl = avatarImage.processAsImageUrl(setType); + + if(imageUrl && !isDisposed.current) + { + if(!avatarImage.isPlaceholder()) AVATAR_IMAGE_CACHE.set(figureKey, imageUrl); + + setAvatarUrl(imageUrl); + } + + avatarImage.dispose(); } - }, [ figure, gender, direction, headOnly, isReady ]); + }, [ figure, gender, direction, headOnly, isReady, updateId ]); useEffect(() => { diff --git a/src/components/avatar-editor/figure-set/AvatarEditorFigureSetItemView.tsx b/src/components/avatar-editor/figure-set/AvatarEditorFigureSetItemView.tsx index 789dc74..7b5d65c 100644 --- a/src/components/avatar-editor/figure-set/AvatarEditorFigureSetItemView.tsx +++ b/src/components/avatar-editor/figure-set/AvatarEditorFigureSetItemView.tsx @@ -1,7 +1,9 @@ +import { NitroEventType } from '@nitrots/nitro-renderer'; import { FC, useEffect, useState } from 'react'; import { AvatarEditorThumbnailsHelper, GetClubMemberLevel, GetConfigurationValue, IAvatarEditorCategoryPartItem } from '../../../api'; import { LayoutCurrencyIcon, LayoutGridItemProps } from '../../../common'; import { useAvatarEditor } from '../../../hooks'; +import { useNitroEvent } from '../../../hooks/events'; import { InfiniteGrid } from '../../../layout'; import { AvatarEditorIcon } from '../AvatarEditorIcon'; @@ -14,6 +16,7 @@ export const AvatarEditorFigureSetItemView: FC<{ { const { setType = null, partItem = null, isSelected = false, width = '100%', ...rest } = props; const [ assetUrl, setAssetUrl ] = useState(''); + const [ retryId, setRetryId ] = useState(0); const { selectedColorParts = null, getFigureStringWithFace = null } = useAvatarEditor(); const clubLevel = partItem.partSet?.clubLevel ?? 0; @@ -21,6 +24,15 @@ export const AvatarEditorFigureSetItemView: FC<{ const isLocked = isHC && (GetClubMemberLevel() < clubLevel); const isSellableNotOwned = partItem.isSellableNotOwned ?? false; + useNitroEvent(NitroEventType.AVATAR_ASSET_LOADED, () => + { + if(!assetUrl || !assetUrl.length) + { + AvatarEditorThumbnailsHelper.clearCache(); + setRetryId(prev => prev + 1); + } + }); + useEffect(() => { setAssetUrl(''); @@ -54,7 +66,7 @@ export const AvatarEditorFigureSetItemView: FC<{ }; loadImage(); - }, [ setType, partItem, selectedColorParts, getFigureStringWithFace, isSellableNotOwned ]); + }, [ setType, partItem, selectedColorParts, getFigureStringWithFace, isSellableNotOwned, retryId ]); if(!partItem) return null; diff --git a/src/components/catalog/views/page/common/CatalogGridOfferView.tsx b/src/components/catalog/views/page/common/CatalogGridOfferView.tsx index a3fcedd..fd7bba4 100644 --- a/src/components/catalog/views/page/common/CatalogGridOfferView.tsx +++ b/src/components/catalog/views/page/common/CatalogGridOfferView.tsx @@ -18,16 +18,18 @@ export const CatalogGridOfferView: FC = props => const { requestOfferToMover = null } = useCatalog(); const { isVisible = false } = useInventoryFurni(); const { isFavoriteOffer, toggleFavoriteOffer } = useCatalogFavorites(); - const isFav = isFavoriteOffer(offer.offerId); + const isFav = offer ? isFavoriteOffer(offer.offerId) : false; const iconUrl = useMemo(() => { + if(!offer) return null; + if(offer.pricingModel === Offer.PRICING_MODEL_BUNDLE) { return null; } - return offer.product.getIconUrl(offer); + return offer.product?.getIconUrl(offer) ?? null; }, [ offer ]); const onMouseEvent = (event: MouseEvent) => @@ -49,6 +51,8 @@ export const CatalogGridOfferView: FC = props => } }; + if(!offer) return null; + const product = offer.product; if(!product) return null; diff --git a/src/components/catalog/views/page/common/CatalogSearchView.tsx b/src/components/catalog/views/page/common/CatalogSearchView.tsx index 7cc30c7..295abe3 100644 --- a/src/components/catalog/views/page/common/CatalogSearchView.tsx +++ b/src/components/catalog/views/page/common/CatalogSearchView.tsx @@ -22,6 +22,8 @@ export const CatalogSearchView: FC<{}> = () => const timeout = setTimeout(() => { + if(!offersToNodes || !rootNode) return; + const furnitureDatas = GetSessionDataManager().getAllFurnitureData(); if(!furnitureDatas || !furnitureDatas.length) return; @@ -31,11 +33,13 @@ export const CatalogSearchView: FC<{}> = () => for(const furniture of furnitureDatas) { + if(!furniture) continue; + if((currentType === CatalogType.BUILDER) && !furniture.availableForBuildersClub) 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)) { diff --git a/src/components/room/widgets/furniture/FurnitureYoutubeDisplayView.tsx b/src/components/room/widgets/furniture/FurnitureYoutubeDisplayView.tsx index 0d8dd5e..f2375ca 100644 --- a/src/components/room/widgets/furniture/FurnitureYoutubeDisplayView.tsx +++ b/src/components/room/widgets/furniture/FurnitureYoutubeDisplayView.tsx @@ -17,42 +17,48 @@ export const FurnitureYoutubeDisplayView: FC<{}> = FurnitureYoutubeDisplayViewPr const onStateChange = (event: { target: YouTubePlayer; data: number }) => { - setPlayer(event.target); - - if(objectId === -1) return; - - switch(event.target.getPlayerState()) + try { - case -1: - case 1: - if(currentVideoState === 2) - { - //event.target.pauseVideo(); - } + setPlayer(event.target); - if(currentVideoState !== 1) play(); - return; - case 2: - if(currentVideoState !== 2) pause(); + if(objectId === -1) return; + + switch(event.target.getPlayerState()) + { + case -1: + case 1: + if(currentVideoState !== 1) play(); + return; + case 2: + if(currentVideoState !== 2) pause(); + } } + catch(err) {} }; useEffect(() => { if((currentVideoState === null) || !player) return; - if((currentVideoState === YoutubeVideoPlaybackStateEnum.PLAYING) && (player.getPlayerState() !== YoutubeVideoPlaybackStateEnum.PLAYING)) + try { - player.playVideo(); + if((currentVideoState === YoutubeVideoPlaybackStateEnum.PLAYING) && (player.getPlayerState() !== YoutubeVideoPlaybackStateEnum.PLAYING)) + { + player.playVideo(); - return; + return; + } + + if((currentVideoState === YoutubeVideoPlaybackStateEnum.PAUSED) && (player.getPlayerState() !== YoutubeVideoPlaybackStateEnum.PAUSED)) + { + player.pauseVideo(); + + return; + } } - - if((currentVideoState === YoutubeVideoPlaybackStateEnum.PAUSED) && (player.getPlayerState() !== YoutubeVideoPlaybackStateEnum.PAUSED)) + catch(err) { - player.pauseVideo(); - - return; + setPlayer(null); } }, [ currentVideoState, player ]); diff --git a/src/components/user-profile/UserContainerView.tsx b/src/components/user-profile/UserContainerView.tsx index a6b3a00..88d3150 100644 --- a/src/components/user-profile/UserContainerView.tsx +++ b/src/components/user-profile/UserContainerView.tsx @@ -8,8 +8,11 @@ export const UserContainerView: FC<{ }> = props => { const { userProfile = null } = props; + const [ requestSent, setRequestSent ] = useState(userProfile.requestSent); + const isOwnProfile = (userProfile.id === GetSessionDataManager().userId); + const canSendFriendRequest = !requestSent && (!isOwnProfile && !userProfile.isMyFriend && !userProfile.requestSent); const infostandBackgroundClass = `background-${userProfile.backgroundId ?? 'default'}`; @@ -35,36 +38,75 @@ export const UserContainerView: FC<{
+

{ userProfile.username }

{ userProfile.motto }

+
-

-

-

- { LocalizeText('extendedprofile.friends.count') } { userProfile.friendsCount } -

+

+ +

+ +

+

{ LocalizeText('extendedprofile.achievementscore') } { userProfile.achievementPoints }

+
{ userProfile.isOnline && } + { !userProfile.isOnline && } +
{ canSendFriendRequest && - { LocalizeText('extendedprofile.addasafriend') } } + + { LocalizeText('extendedprofile.addasafriend') } + } + { !canSendFriendRequest && <> + { isOwnProfile &&

{ LocalizeText('extendedprofile.me') }

} + { userProfile.isMyFriend &&

{ LocalizeText('extendedprofile.friend') }

} + { (requestSent || userProfile.requestSent) &&

{ LocalizeText('extendedprofile.friendrequestsent') }

} } @@ -73,4 +115,4 @@ export const UserContainerView: FC<{
); -}; +}; \ No newline at end of file diff --git a/src/components/user-profile/UserProfileView.tsx b/src/components/user-profile/UserProfileView.tsx index eb8f09e..a4d39c3 100644 --- a/src/components/user-profile/UserProfileView.tsx +++ b/src/components/user-profile/UserProfileView.tsx @@ -9,7 +9,7 @@ import { FriendsContainerView } from './FriendsContainerView'; import { GroupsContainerView } from './GroupsContainerView'; import { UserContainerView } from './UserContainerView'; -type ProfileTab = 'badge' | 'amici' | 'stanze' | 'gruppi'; +type ProfileTab = 'badge' | 'friends' | 'rooms' | 'groups'; export const UserProfileView: FC<{}> = props => { @@ -39,7 +39,7 @@ export const UserProfileView: FC<{}> = props => { setActiveTab(tab); - if(tab === 'stanze' && !userRooms && userProfile) + if(tab === 'rooms' && !userRooms && userProfile) { SendMessageComposer(new NavigatorSearchComposer('hotel_view', `owner:${ userProfile.username }`)); } @@ -99,7 +99,7 @@ export const UserProfileView: FC<{}> = props => useMessageEvent(NavigatorSearchEvent, event => { - if(!userProfile || activeTab !== 'stanze') return; + if(!userProfile || activeTab !== 'rooms') return; const parser = event.getParser(); const result = parser.result; @@ -145,16 +145,16 @@ export const UserProfileView: FC<{}> = props =>
onTabClick('badge') }> - { LocalizeText('extendedprofile.tab.badge') } + { LocalizeText('levelinfo.category.badge') } - onTabClick('amici') }> - { LocalizeText('extendedprofile.tab.friends') } + onTabClick('friends') }> + { LocalizeText('navigator.tab.3') } - onTabClick('stanze') }> - { LocalizeText('extendedprofile.tab.rooms') } + onTabClick('rooms') }> + { LocalizeText('navigator.tab.2') } - onTabClick('gruppi') }> - { LocalizeText('extendedprofile.tab.groups') } + onTabClick('groups') }> + { LocalizeText('navigator.searchcode.title.groups') }
@@ -172,7 +172,7 @@ export const UserProfileView: FC<{}> = props => }
) } - { activeTab === 'amici' && ( + { activeTab === 'friends' && (
{ userRelationships ? ( @@ -183,7 +183,7 @@ export const UserProfileView: FC<{}> = props => ) }
) } - { activeTab === 'stanze' && ( + { activeTab === 'rooms' && (
{ !userRooms && ( @@ -206,7 +206,7 @@ export const UserProfileView: FC<{}> = props => )) }
) } - { activeTab === 'gruppi' && ( + { activeTab === 'groups' && (
diff --git a/src/hooks/furni-editor/useFurniEditor.ts b/src/hooks/furni-editor/useFurniEditor.ts index 936eb4f..cd95ec4 100644 --- a/src/hooks/furni-editor/useFurniEditor.ts +++ b/src/hooks/furni-editor/useFurniEditor.ts @@ -60,7 +60,7 @@ export const useFurniEditor = () => const [ catalogItems, setCatalogItems ] = useState([]); const [ interactions, setInteractions ] = useState([]); const [ furniDataEntry, setFurniDataEntry ] = useState | null>(null); - const pendingActionRef = useRef(null); + const pendingActionRef = useRef<{ action: string; itemId: number } | null>(null); const { simpleAlert = null } = useNotification(); const clearError = useCallback(() => setError(null), []); @@ -161,7 +161,9 @@ export const useFurniEditor = () => useMessageEvent(FurniEditorResultEvent, (event: FurniEditorResultEvent) => { 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; setLoading(false); @@ -182,10 +184,10 @@ export const useFurniEditor = () => if(action === 'update') { - // Auto-reload detail after update - if(selectedItem) + // Auto-reload detail after update using the ID from the original request + if(actionItemId) { - SendMessageComposer(new FurniEditorDetailComposer(selectedItem.id)); + SendMessageComposer(new FurniEditorDetailComposer(actionItemId)); } if(simpleAlert) @@ -231,7 +233,7 @@ export const useFurniEditor = () => { setLoading(true); setError(null); - pendingActionRef.current = 'update'; + pendingActionRef.current = { action: 'update', itemId: id }; SendMessageComposer(new FurniEditorUpdateComposer(id, JSON.stringify(fields))); }, []); @@ -239,7 +241,7 @@ export const useFurniEditor = () => { setLoading(true); setError(null); - pendingActionRef.current = 'delete'; + pendingActionRef.current = { action: 'delete', itemId: id }; SendMessageComposer(new FurniEditorDeleteComposer(id)); }, []); diff --git a/src/hooks/rooms/widgets/furniture/useFurnitureYoutubeWidget.ts b/src/hooks/rooms/widgets/furniture/useFurnitureYoutubeWidget.ts index 7f91c1e..0069063 100644 --- a/src/hooks/rooms/widgets/furniture/useFurnitureYoutubeWidget.ts +++ b/src/hooks/rooms/widgets/furniture/useFurnitureYoutubeWidget.ts @@ -1,5 +1,5 @@ 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 { useMessageEvent, useNitroEvent } from '../../../events'; import { useFurniRemovedEvent } from '../../engine'; @@ -13,6 +13,7 @@ const useFurnitureYoutubeWidgetState = () => { const [ objectId, setObjectId ] = useState(-1); const [ category, setCategory ] = useState(-1); + const objectIdRef = useRef(-1); const [ videoId, setVideoId ] = useState(null); const [ videoStart, setVideoStart ] = useState(null); const [ videoEnd, setVideoEnd ] = useState(null); @@ -23,6 +24,7 @@ const useFurnitureYoutubeWidgetState = () => const onClose = () => { + objectIdRef.current = -1; setObjectId(-1); setCategory(-1); setVideoId(null); @@ -64,6 +66,7 @@ const useFurnitureYoutubeWidgetState = () => if(!roomObject) return; + objectIdRef.current = event.objectId; setObjectId(event.objectId); setCategory(event.category); setHasControl(GetSessionDataManager().hasSecurity(SecurityLevel.EMPLOYEE) || IsOwnerOfFurniture(roomObject)); @@ -74,8 +77,9 @@ const useFurnitureYoutubeWidgetState = () => useMessageEvent(YoutubeDisplayVideoMessageEvent, event => { 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); setVideoStart(parser.startAtSeconds); @@ -86,8 +90,9 @@ const useFurnitureYoutubeWidgetState = () => useMessageEvent(YoutubeDisplayPlaylistsEvent, event => { 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); setSelectedVideo(parser.selectedPlaylistId); @@ -100,8 +105,9 @@ const useFurnitureYoutubeWidgetState = () => useMessageEvent(YoutubeControlVideoMessageEvent, event => { 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) {