+
+
+ { canModify &&
+ <>
+
+ { STICKIE_COLORS.map(color =>
+ {
+ return
setColor(color) } />;
+ }) }
+ > }
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/Coolui v3 test/src/components/room/widgets/furniture/FurnitureStackHeightView.tsx b/Coolui v3 test/src/components/room/widgets/furniture/FurnitureStackHeightView.tsx
new file mode 100644
index 0000000000..741a35eee8
--- /dev/null
+++ b/Coolui v3 test/src/components/room/widgets/furniture/FurnitureStackHeightView.tsx
@@ -0,0 +1,58 @@
+import { FurnitureStackHeightComposer } from '@nitrots/nitro-renderer';
+import { FC, useEffect, useState } from 'react';
+import ReactSlider from 'react-slider';
+import { LocalizeText, SendMessageComposer } from '../../../../api';
+import { Button, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
+import { useFurnitureStackHeightWidget } from '../../../../hooks';
+
+export const FurnitureStackHeightView: FC<{}> = props =>
+{
+ const { objectId = -1, height = 0, maxHeight = 40, onClose = null, updateHeight = null } = useFurnitureStackHeightWidget();
+ const [ tempHeight, setTempHeight ] = useState('');
+
+ const updateTempHeight = (value: string) =>
+ {
+ setTempHeight(value);
+
+ const newValue = parseFloat(value);
+
+ if(isNaN(newValue) || (newValue === height)) return;
+
+ updateHeight(newValue);
+ };
+
+ useEffect(() =>
+ {
+ setTempHeight(height.toString());
+ }, [ height ]);
+
+ if(objectId === -1) return null;
+
+ return (
+
+
+
+ { LocalizeText('widget.custom.stack.height.text') }
+
+
+ SendMessageComposer(new FurnitureStackHeightComposer(objectId, -100)) }>
+ { LocalizeText('furniture.above.stack') }
+
+ SendMessageComposer(new FurnitureStackHeightComposer(objectId, 0)) }>
+ { LocalizeText('furniture.floor.level') }
+
+
+
+
+ );
+};
diff --git a/Coolui v3 test/src/components/room/widgets/furniture/FurnitureStickieView.tsx b/Coolui v3 test/src/components/room/widgets/furniture/FurnitureStickieView.tsx
new file mode 100644
index 0000000000..fec4845484
--- /dev/null
+++ b/Coolui v3 test/src/components/room/widgets/furniture/FurnitureStickieView.tsx
@@ -0,0 +1,66 @@
+import { FC, useEffect, useState } from 'react';
+import { ColorUtils } from '../../../../api';
+import { DraggableWindow, DraggableWindowPosition } from '../../../../common';
+import { useFurnitureStickieWidget } from '../../../../hooks';
+
+const STICKIE_COLORS = [ '9CCEFF', 'FF9CFF', '9CFF9C', 'FFFF33' ];
+const STICKIE_COLOR_NAMES = [ 'blue', 'pink', 'green', 'yellow' ];
+const STICKIE_TYPES = [ 'post_it', 'post_it_shakesp', 'post_it_dreams', 'post_it_xmas', 'post_it_vd', 'post_it_juninas' ];
+const STICKIE_TYPE_NAMES = [ 'post_it', 'shakesp', 'dreams', 'christmas', 'heart', 'juninas' ];
+
+const getStickieColorName = (color: string) =>
+{
+ let index = STICKIE_COLORS.indexOf(color);
+
+ if(index === -1) index = 0;
+
+ return STICKIE_COLOR_NAMES[index];
+};
+
+const getStickieTypeName = (type: string) =>
+{
+ let index = STICKIE_TYPES.indexOf(type);
+
+ if(index === -1) index = 0;
+
+ return STICKIE_TYPE_NAMES[index];
+};
+
+export const FurnitureStickieView: FC<{}> = props =>
+{
+ const { objectId = -1, color = '0', text = '', type = '', canModify = false, updateColor = null, updateText = null, trash = null, onClose = null } = useFurnitureStickieWidget();
+ const [ isEditing, setIsEditing ] = useState(false);
+
+ useEffect(() =>
+ {
+ setIsEditing(false);
+ }, [ objectId, color, text, type ]);
+
+ if(objectId === -1) return null;
+
+ return (
+
+
+
+
+ { canModify &&
+ <>
+
+ { type == 'post_it' &&
+ <>
+ { STICKIE_COLORS.map(color =>
+ {
+ return
updateColor(color) } />;
+ }) }
+ > }
+ > }
+
+
+
+
+ { (!isEditing || !canModify) ?
(canModify && setIsEditing(true)) }>{ text }
:
}
+
+
+
+ );
+};
diff --git a/Coolui v3 test/src/components/room/widgets/furniture/FurnitureTrophyView.tsx b/Coolui v3 test/src/components/room/widgets/furniture/FurnitureTrophyView.tsx
new file mode 100644
index 0000000000..2e08af0658
--- /dev/null
+++ b/Coolui v3 test/src/components/room/widgets/furniture/FurnitureTrophyView.tsx
@@ -0,0 +1,12 @@
+import { FC } from 'react';
+import { LayoutTrophyView } from '../../../../common';
+import { useFurnitureTrophyWidget } from '../../../../hooks';
+
+export const FurnitureTrophyView: FC<{}> = props =>
+{
+ const { objectId = -1, color = '1', senderName = '', date = '', message = '', onClose = null } = useFurnitureTrophyWidget();
+
+ if(objectId === -1) return null;
+
+ return
;
+};
diff --git a/Coolui v3 test/src/components/room/widgets/furniture/FurnitureWidgetsView.tsx b/Coolui v3 test/src/components/room/widgets/furniture/FurnitureWidgetsView.tsx
new file mode 100644
index 0000000000..8c6dba25e6
--- /dev/null
+++ b/Coolui v3 test/src/components/room/widgets/furniture/FurnitureWidgetsView.tsx
@@ -0,0 +1,47 @@
+import { FC } from 'react';
+import { FurnitureBackgroundColorView } from './FurnitureBackgroundColorView';
+import { FurnitureBadgeDisplayView } from './FurnitureBadgeDisplayView';
+import { FurnitureCraftingView } from './FurnitureCraftingView';
+import { FurnitureDimmerView } from './FurnitureDimmerView';
+import { FurnitureExchangeCreditView } from './FurnitureExchangeCreditView';
+import { FurnitureExternalImageView } from './FurnitureExternalImageView';
+import { FurnitureFriendFurniView } from './FurnitureFriendFurniView';
+import { FurnitureGiftOpeningView } from './FurnitureGiftOpeningView';
+import { FurnitureHighScoreView } from './FurnitureHighScoreView';
+import { FurnitureInternalLinkView } from './FurnitureInternalLinkView';
+import { FurnitureMannequinView } from './FurnitureMannequinView';
+import { FurnitureRoomLinkView } from './FurnitureRoomLinkView';
+import { FurnitureSpamWallPostItView } from './FurnitureSpamWallPostItView';
+import { FurnitureStackHeightView } from './FurnitureStackHeightView';
+import { FurnitureStickieView } from './FurnitureStickieView';
+import { FurnitureTrophyView } from './FurnitureTrophyView';
+import { FurnitureYoutubeDisplayView } from './FurnitureYoutubeDisplayView';
+import { FurnitureContextMenuView } from './context-menu/FurnitureContextMenuView';
+import { FurniturePlaylistEditorWidgetView } from './playlist-editor/FurniturePlaylistEditorWidgetView';
+
+export const FurnitureWidgetsView: FC<{}> = props =>
+{
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/Coolui v3 test/src/components/room/widgets/furniture/FurnitureYoutubeDisplayView.tsx b/Coolui v3 test/src/components/room/widgets/furniture/FurnitureYoutubeDisplayView.tsx
new file mode 100644
index 0000000000..0d8dd5e32d
--- /dev/null
+++ b/Coolui v3 test/src/components/room/widgets/furniture/FurnitureYoutubeDisplayView.tsx
@@ -0,0 +1,109 @@
+import { FC, useEffect, useState } from 'react';
+import YouTube, { Options } from 'react-youtube';
+import { YouTubePlayer } from 'youtube-player/dist/types';
+import { LocalizeText, YoutubeVideoPlaybackStateEnum } from '../../../../api';
+import { AutoGrid, AutoGridProps, LayoutGridItem, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common';
+import { useFurnitureYoutubeWidget } from '../../../../hooks';
+
+interface FurnitureYoutubeDisplayViewProps extends AutoGridProps
+{
+
+}
+
+export const FurnitureYoutubeDisplayView: FC<{}> = FurnitureYoutubeDisplayViewProps =>
+{
+ const [ player, setPlayer ] = useState
(null);
+ 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 }) =>
+ {
+ setPlayer(event.target);
+
+ if(objectId === -1) return;
+
+ switch(event.target.getPlayerState())
+ {
+ case -1:
+ case 1:
+ if(currentVideoState === 2)
+ {
+ //event.target.pauseVideo();
+ }
+
+ if(currentVideoState !== 1) play();
+ return;
+ case 2:
+ if(currentVideoState !== 2) pause();
+ }
+ };
+
+ useEffect(() =>
+ {
+ if((currentVideoState === null) || !player) return;
+
+ if((currentVideoState === YoutubeVideoPlaybackStateEnum.PLAYING) && (player.getPlayerState() !== YoutubeVideoPlaybackStateEnum.PLAYING))
+ {
+ player.playVideo();
+
+ return;
+ }
+
+ if((currentVideoState === YoutubeVideoPlaybackStateEnum.PAUSED) && (player.getPlayerState() !== YoutubeVideoPlaybackStateEnum.PAUSED))
+ {
+ player.pauseVideo();
+
+ return;
+ }
+ }, [ currentVideoState, player ]);
+
+ if(objectId === -1) return null;
+
+ const youtubeOptions: Options = {
+ height: '375',
+ width: '500',
+ playerVars: {
+ autoplay: 1,
+ disablekb: 1,
+ controls: 0,
+ origin: window.origin,
+ modestbranding: 1,
+ start: videoStart,
+ end: videoEnd
+ }
+ };
+
+ return (
+
+
+
+
+
+ { (videoId && videoId.length > 0) &&
+
setPlayer(event.target) } onStateChange={ onStateChange } />
+ }
+ { (!videoId || videoId.length === 0) &&
+ { LocalizeText('widget.furni.video_viewer.no_videos') }
+ }
+
+
+
+
+
+
+
{ LocalizeText('widget.furni.video_viewer.playlists') }
+
+ { playlists && playlists.map((entry, index) =>
+ {
+ return (
+ selectVideo(entry.video) }>
+ { entry.title }
+
+ );
+ }) }
+
+
+
+
+
+ );
+};
diff --git a/Coolui v3 test/src/components/room/widgets/furniture/context-menu/EffectBoxConfirmView.tsx b/Coolui v3 test/src/components/room/widgets/furniture/context-menu/EffectBoxConfirmView.tsx
new file mode 100644
index 0000000000..a818152b3f
--- /dev/null
+++ b/Coolui v3 test/src/components/room/widgets/furniture/context-menu/EffectBoxConfirmView.tsx
@@ -0,0 +1,40 @@
+import { FC } from 'react';
+import { LocalizeText } from '../../../../../api';
+import { Button, Column, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../../common';
+import { useRoom } from '../../../../../hooks';
+
+interface EffectBoxConfirmViewProps
+{
+ objectId: number;
+ onClose: () => void;
+}
+
+export const EffectBoxConfirmView: FC = props =>
+{
+ const { objectId = -1, onClose = null } = props;
+ const { roomSession = null } = useRoom();
+
+ const useProduct = () =>
+ {
+ roomSession.useMultistateItem(objectId);
+
+ onClose();
+ };
+
+ return (
+
+
+
+
+
+ { LocalizeText('effectbox.header.description') }
+
+ { LocalizeText('generic.cancel') }
+ { LocalizeText('generic.ok') }
+
+
+
+
+
+ );
+};
diff --git a/Coolui v3 test/src/components/room/widgets/furniture/context-menu/FurnitureContextMenuView.tsx b/Coolui v3 test/src/components/room/widgets/furniture/context-menu/FurnitureContextMenuView.tsx
new file mode 100644
index 0000000000..7976d99ce9
--- /dev/null
+++ b/Coolui v3 test/src/components/room/widgets/furniture/context-menu/FurnitureContextMenuView.tsx
@@ -0,0 +1,130 @@
+import { ContextMenuEnum, CustomUserNotificationMessageEvent, GetSessionDataManager, RoomObjectCategory } from '@nitrots/nitro-renderer';
+import { FC } from 'react';
+import { GetGroupInformation, LocalizeText } from '../../../../../api';
+import { EFFECTBOX_OPEN, GROUP_FURNITURE, MONSTERPLANT_SEED_CONFIRMATION, MYSTERYTROPHY_OPEN_DIALOG, PURCHASABLE_CLOTHING_CONFIRMATION, useFurnitureContextMenuWidget, useMessageEvent, useNotification } from '../../../../../hooks';
+import { ContextMenuHeaderView } from '../../context-menu/ContextMenuHeaderView';
+import { ContextMenuListItemView } from '../../context-menu/ContextMenuListItemView';
+import { ContextMenuView } from '../../context-menu/ContextMenuView';
+import { FurnitureMysteryBoxOpenDialogView } from '../FurnitureMysteryBoxOpenDialogView';
+import { FurnitureMysteryTrophyOpenDialogView } from '../FurnitureMysteryTrophyOpenDialogView';
+import { EffectBoxConfirmView } from './EffectBoxConfirmView';
+import { MonsterPlantSeedConfirmView } from './MonsterPlantSeedConfirmView';
+import { PurchasableClothingConfirmView } from './PurchasableClothingConfirmView';
+
+export const FurnitureContextMenuView: FC<{}> = props =>
+{
+ const { closeConfirm = null, processAction = null, onClose = null, objectId = -1, mode = null, confirmMode = null, confirmingObjectId = -1, groupData = null, isGroupMember = false, objectOwnerId = -1 } = useFurnitureContextMenuWidget();
+ const { simpleAlert = null } = useNotification();
+
+ useMessageEvent(CustomUserNotificationMessageEvent, event =>
+ {
+ const parser = event.getParser();
+
+ if(!parser) return;
+
+ // HOPPER_NO_COSTUME = 1; HOPPER_NO_HC = 2; GATE_NO_HC = 3; STARS_NOT_CANDIDATE = 4 (not coded in Emulator); STARS_NOT_ENOUGH_USERS = 5 (not coded in Emulator);
+
+ switch(parser.count)
+ {
+ case 1:
+ simpleAlert(LocalizeText('costumehopper.costumerequired.bodytext'), null, 'catalog/open/temporary_effects' , LocalizeText('costumehopper.costumerequired.buy'), LocalizeText('costumehopper.costumerequired.header'), null);
+ break;
+ case 2:
+ simpleAlert(LocalizeText('viphopper.viprequired.bodytext'), null, 'catalog/open/habbo_club' , LocalizeText('viprequired.buy.vip'), LocalizeText('viprequired.header'), null);
+ break;
+ case 3:
+ simpleAlert(LocalizeText('gate.viprequired.bodytext'), null, 'catalog/open/habbo_club' , LocalizeText('viprequired.buy.vip'), LocalizeText('gate.viprequired.title'), null);
+ break;
+ }
+ });
+
+ const isOwner = GetSessionDataManager().userId === objectOwnerId;
+
+ return (
+ <>
+ { (confirmMode === MONSTERPLANT_SEED_CONFIRMATION) &&
+ }
+ { (confirmMode === PURCHASABLE_CLOTHING_CONFIRMATION) &&
+ }
+ { (confirmMode === EFFECTBOX_OPEN) &&
+ }
+ { (confirmMode === MYSTERYTROPHY_OPEN_DIALOG) &&
+ }
+
+ { (objectId >= 0) && mode &&
+
+ { (mode === ContextMenuEnum.FRIEND_FURNITURE) &&
+ <>
+
+ { LocalizeText('friendfurni.context.title') }
+
+ processAction('use_friend_furni') }>
+ { LocalizeText('friendfurni.context.use') }
+
+ > }
+ { (mode === ContextMenuEnum.MONSTERPLANT_SEED) &&
+ <>
+
+ { LocalizeText('furni.mnstr_seed.name') }
+
+ processAction('use_monsterplant_seed') }>
+ { LocalizeText('widget.monsterplant_seed.button.use') }
+
+ > }
+ { (mode === ContextMenuEnum.RANDOM_TELEPORT) &&
+ <>
+
+ { LocalizeText('furni.random_teleport.name') }
+
+ processAction('use_random_teleport') }>
+ { LocalizeText('widget.random_teleport.button.use') }
+
+ > }
+ { (mode === ContextMenuEnum.PURCHASABLE_CLOTHING) &&
+ <>
+
+ { LocalizeText('furni.generic_usable.name') }
+
+ processAction('use_purchaseable_clothing') }>
+ { LocalizeText('widget.generic_usable.button.use') }
+
+ > }
+ { (mode === ContextMenuEnum.MYSTERY_BOX) &&
+ <>
+
+ { LocalizeText('mysterybox.context.title') }
+
+ processAction('use_mystery_box') }>
+ { LocalizeText('mysterybox.context.' + ((isOwner) ? 'owner' : 'other') + '.use') }
+
+ > }
+ { (mode === ContextMenuEnum.MYSTERY_TROPHY) &&
+ <>
+
+ { LocalizeText('mysterytrophy.header.title') }
+
+ processAction('use_mystery_trophy') }>
+ { LocalizeText('friendfurni.context.use') }
+
+ > }
+ { (mode === GROUP_FURNITURE) && groupData &&
+ <>
+ GetGroupInformation(groupData.guildId) }>
+ { groupData.guildName }
+
+ { !isGroupMember &&
+ processAction('join_group') }>
+ { LocalizeText('widget.furniture.button.join.group') }
+ }
+ processAction('go_to_group_homeroom') }>
+ { LocalizeText('widget.furniture.button.go.to.group.home.room') }
+
+ { groupData.guildHasReadableForum &&
+ processAction('open_forum') }>
+ { LocalizeText('widget.furniture.button.open_group_forum') }
+ }
+ > }
+ }
+ >
+ );
+};
diff --git a/Coolui v3 test/src/components/room/widgets/furniture/context-menu/MonsterPlantSeedConfirmView.tsx b/Coolui v3 test/src/components/room/widgets/furniture/context-menu/MonsterPlantSeedConfirmView.tsx
new file mode 100644
index 0000000000..4a5ed984ee
--- /dev/null
+++ b/Coolui v3 test/src/components/room/widgets/furniture/context-menu/MonsterPlantSeedConfirmView.tsx
@@ -0,0 +1,85 @@
+import { IFurnitureData, RoomObjectCategory } from '@nitrots/nitro-renderer';
+import { FC, useEffect, useState } from 'react';
+import { FurniCategory, GetFurnitureDataForRoomObject, LocalizeText } from '../../../../../api';
+import { Button, Column, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../../common';
+import { useRoom } from '../../../../../hooks';
+
+interface MonsterPlantSeedConfirmViewProps
+{
+ objectId: number;
+ onClose: () => void;
+}
+
+const MODE_DEFAULT: number = -1;
+const MODE_MONSTERPLANT_SEED: number = 0;
+
+export const MonsterPlantSeedConfirmView: FC = props =>
+{
+ const { objectId = -1, onClose = null } = props;
+ const [ furniData, setFurniData ] = useState(null);
+ const [ mode, setMode ] = useState(MODE_DEFAULT);
+ const { roomSession = null } = useRoom();
+
+ const useProduct = () =>
+ {
+ roomSession.useMultistateItem(objectId);
+
+ onClose();
+ };
+
+ useEffect(() =>
+ {
+ if(!roomSession || (objectId === -1)) return;
+
+ const furniData = GetFurnitureDataForRoomObject(roomSession.roomId, objectId, RoomObjectCategory.FLOOR);
+
+ if(!furniData) return;
+
+ setFurniData(furniData);
+
+ let mode = MODE_DEFAULT;
+
+ switch(furniData.specialType)
+ {
+ case FurniCategory.MONSTERPLANT_SEED:
+ mode = MODE_MONSTERPLANT_SEED;
+ break;
+ }
+
+ if(mode === MODE_DEFAULT)
+ {
+ onClose();
+
+ return;
+ }
+
+ setMode(mode);
+ }, [ roomSession, objectId, onClose ]);
+
+ if(mode === MODE_DEFAULT) return null;
+
+ return (
+
+
+
+
+
+
+
+ { LocalizeText('useproduct.widget.text.plant_seed', [ 'productName' ], [ furniData.name ]) }
+ { LocalizeText('useproduct.widget.info.plant_seed') }
+
+
+ { LocalizeText('useproduct.widget.cancel') }
+ { LocalizeText('widget.monsterplant_seed.button.use') }
+
+
+
+
+
+ );
+};
diff --git a/Coolui v3 test/src/components/room/widgets/furniture/context-menu/PurchasableClothingConfirmView.tsx b/Coolui v3 test/src/components/room/widgets/furniture/context-menu/PurchasableClothingConfirmView.tsx
new file mode 100644
index 0000000000..85a6f8062e
--- /dev/null
+++ b/Coolui v3 test/src/components/room/widgets/furniture/context-menu/PurchasableClothingConfirmView.tsx
@@ -0,0 +1,104 @@
+import { AvatarFigurePartType, GetAvatarRenderManager, GetSessionDataManager, RedeemItemClothingComposer, RoomObjectCategory, UserFigureComposer } from '@nitrots/nitro-renderer';
+import { FC, useEffect, useState } from 'react';
+import { FurniCategory, GetFurnitureDataForRoomObject, LocalizeText, SendMessageComposer } from '../../../../../api';
+import { Button, Column, LayoutAvatarImageView, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../../common';
+import { useRoom } from '../../../../../hooks';
+
+interface PurchasableClothingConfirmViewProps
+{
+ objectId: number;
+ onClose: () => void;
+}
+
+const MODE_DEFAULT: number = -1;
+const MODE_PURCHASABLE_CLOTHING: number = 0;
+
+export const PurchasableClothingConfirmView: FC = props =>
+{
+ const { objectId = -1, onClose = null } = props;
+ const [ mode, setMode ] = useState(MODE_DEFAULT);
+ const [ gender, setGender ] = useState(AvatarFigurePartType.MALE);
+ const [ newFigure, setNewFigure ] = useState(null);
+ const { roomSession = null } = useRoom();
+
+ const useProduct = () =>
+ {
+ SendMessageComposer(new RedeemItemClothingComposer(objectId));
+ SendMessageComposer(new UserFigureComposer(gender, newFigure));
+
+ onClose();
+ };
+
+ useEffect(() =>
+ {
+ let mode = MODE_DEFAULT;
+
+ const figure = GetSessionDataManager().figure;
+ const gender = GetSessionDataManager().gender;
+ const validSets: number[] = [];
+
+ if(roomSession && (objectId >= 0))
+ {
+ const furniData = GetFurnitureDataForRoomObject(roomSession.roomId, objectId, RoomObjectCategory.FLOOR);
+
+ if(furniData)
+ {
+ switch(furniData.specialType)
+ {
+ case FurniCategory.FIGURE_PURCHASABLE_SET:
+ mode = MODE_PURCHASABLE_CLOTHING;
+
+ const setIds = furniData.customParams.split(',').map(part => parseInt(part));
+
+ for(const setId of setIds)
+ {
+ if(GetAvatarRenderManager().isValidFigureSetForGender(setId, gender)) validSets.push(setId);
+ }
+
+ break;
+ }
+ }
+ }
+
+ if(mode === MODE_DEFAULT)
+ {
+ onClose();
+
+ return;
+ }
+
+ setGender(gender);
+ setNewFigure(GetAvatarRenderManager().getFigureStringWithFigureIds(figure, gender, validSets));
+
+ // if owns clothing, change to it
+
+ setMode(mode);
+ }, [ roomSession, objectId, onClose ]);
+
+ if(mode === MODE_DEFAULT) return null;
+
+ return (
+
+
+
+
+
+
+
+ { LocalizeText('useproduct.widget.text.bind_clothing') }
+ { LocalizeText('useproduct.widget.info.bind_clothing') }
+
+
+ { LocalizeText('useproduct.widget.cancel') }
+ { LocalizeText('useproduct.widget.bind_clothing') }
+
+
+
+
+
+ );
+};
diff --git a/Coolui v3 test/src/components/room/widgets/furniture/playlist-editor/DiskInventoryView.tsx b/Coolui v3 test/src/components/room/widgets/furniture/playlist-editor/DiskInventoryView.tsx
new file mode 100644
index 0000000000..7550af9bf4
--- /dev/null
+++ b/Coolui v3 test/src/components/room/widgets/furniture/playlist-editor/DiskInventoryView.tsx
@@ -0,0 +1,94 @@
+import { CreateLinkEvent, GetSoundManager, IAdvancedMap, MusicPriorities } from '@nitrots/nitro-renderer';
+import { FC, MouseEvent, useCallback, useEffect, useState } from 'react';
+import { CatalogPageName, GetConfigurationValue, GetDiskColor, LocalizeText } from '../../../../../api';
+import { AutoGrid, Button, Flex, LayoutGridItem, Text } from '../../../../../common';
+
+export interface DiskInventoryViewProps
+{
+ diskInventory: IAdvancedMap;
+ addToPlaylist: (diskId: number, slotNumber: number) => void;
+}
+
+export const DiskInventoryView: FC = props =>
+{
+ const { diskInventory = null, addToPlaylist = null } = props;
+ const [ selectedItem, setSelectedItem ] = useState(-1);
+ const [ previewSongId, setPreviewSongId ] = useState(-1);
+
+ const previewSong = useCallback((event: MouseEvent, songId: number) =>
+ {
+ event.stopPropagation();
+
+ setPreviewSongId(prevValue => (prevValue === songId) ? -1 : songId);
+ }, []);
+
+ const addSong = useCallback((event: MouseEvent, diskId: number) =>
+ {
+ event.stopPropagation();
+
+ addToPlaylist(diskId, GetSoundManager().musicController?.getRoomItemPlaylist()?.length);
+ }, [ addToPlaylist ]);
+
+ const openCatalogPage = () =>
+ {
+ CreateLinkEvent('catalog/open/' + CatalogPageName.TRAX_SONGS);
+ };
+
+ useEffect(() =>
+ {
+ if(previewSongId === -1) return;
+
+ GetSoundManager().musicController?.playSong(previewSongId, MusicPriorities.PRIORITY_SONG_PLAY, 0, 0, 0, 0);
+
+ return () =>
+ {
+ GetSoundManager().musicController?.stop(MusicPriorities.PRIORITY_SONG_PLAY);
+ };
+ }, [ previewSongId ]);
+
+ useEffect(() =>
+ {
+ return () => setPreviewSongId(-1);
+ }, []);
+
+ return (<>
+
+
+
{ LocalizeText('playlist.editor.my.music') }
+
+
+
+ { diskInventory && diskInventory.getKeys().map((key, index) =>
+ {
+ const diskId = diskInventory.getKey(index);
+ const songId = diskInventory.getWithIndex(index);
+ const songInfo = GetSoundManager().musicController?.getSongInfo(songId);
+
+ return (
+ setSelectedItem(prev => prev === index ? -1 : index) }>
+
+
+ { songInfo?.name }
+ { (selectedItem === index) &&
+
+ previewSong(event, songId) }>
+
+
+ addSong(event, diskId) }>
+
+
+
+ }
+ );
+ }) }
+
+
+
+
{ LocalizeText('playlist.editor.text.get.more.music') }
+
{ LocalizeText('playlist.editor.text.you.have.no.songdisks.available') }
+
{ LocalizeText('playlist.editor.text.you.can.buy.some.from.the.catalogue') }
+
openCatalogPage() }>{ LocalizeText('playlist.editor.button.open.catalogue') }
+
+
+ >);
+};
diff --git a/Coolui v3 test/src/components/room/widgets/furniture/playlist-editor/FurniturePlaylistEditorWidgetView.tsx b/Coolui v3 test/src/components/room/widgets/furniture/playlist-editor/FurniturePlaylistEditorWidgetView.tsx
new file mode 100644
index 0000000000..611eba2195
--- /dev/null
+++ b/Coolui v3 test/src/components/room/widgets/furniture/playlist-editor/FurniturePlaylistEditorWidgetView.tsx
@@ -0,0 +1,29 @@
+import { FC } from 'react';
+import { LocalizeText } from '../../../../../api';
+import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../../common';
+import { useFurniturePlaylistEditorWidget } from '../../../../../hooks';
+import { DiskInventoryView } from './DiskInventoryView';
+import { SongPlaylistView } from './SongPlaylistView';
+
+export const FurniturePlaylistEditorWidgetView: FC<{}> = props =>
+{
+ const { objectId = -1, currentPlayingIndex = -1, playlist = null, diskInventory = null, onClose = null, togglePlayPause = null, removeFromPlaylist = null, addToPlaylist = null } = useFurniturePlaylistEditorWidget();
+
+ if(objectId === -1) return null;
+
+ return (
+
+
+
+
+
+
+ );
+};
diff --git a/Coolui v3 test/src/components/room/widgets/furniture/playlist-editor/SongPlaylistView.tsx b/Coolui v3 test/src/components/room/widgets/furniture/playlist-editor/SongPlaylistView.tsx
new file mode 100644
index 0000000000..95289a7266
--- /dev/null
+++ b/Coolui v3 test/src/components/room/widgets/furniture/playlist-editor/SongPlaylistView.tsx
@@ -0,0 +1,78 @@
+import { ISongInfo } from '@nitrots/nitro-renderer';
+import { FC, useState } from 'react';
+import { GetConfigurationValue, GetDiskColor, LocalizeText } from '../../../../../api';
+import { Button, Text } from '../../../../../common';
+
+export interface SongPlaylistViewProps
+{
+ furniId: number;
+ playlist: ISongInfo[];
+ currentPlayingIndex: number;
+ removeFromPlaylist(slotNumber: number): void;
+ togglePlayPause(furniId: number, position: number): void;
+}
+
+export const SongPlaylistView: FC = props =>
+{
+ const { furniId = -1, playlist = null, currentPlayingIndex = -1, removeFromPlaylist = null, togglePlayPause = null } = props;
+ const [ selectedItem, setSelectedItem ] = useState(-1);
+
+ const action = (index: number) =>
+ {
+ if(selectedItem === index) removeFromPlaylist(index);
+ };
+
+ const playPause = (furniId: number, selectedItem: number) =>
+ {
+ togglePlayPause(furniId, selectedItem !== -1 ? selectedItem : 0);
+ };
+
+ return (<>
+
+
+
{ LocalizeText('playlist.editor.playlist') }
+
+
+
+ { playlist && playlist.map((songInfo, index) =>
+ {
+ return
setSelectedItem(prev => prev === index ? -1 : index) }>
+
action(index) } />
+ { songInfo.name }
+
;
+ }) }
+
+
+
+ { (!playlist || playlist.length === 0) &&
+ <>
+
{ LocalizeText('playlist.editor.add.songs.to.your.playlist') }
+
{ LocalizeText('playlist.editor.text.click.song.to.choose.click.again.to.move') }
+
+
>
+ }
+ { (playlist && playlist.length > 0) &&
+ <>
+ { (currentPlayingIndex === -1) &&
+
playPause(furniId, selectedItem) }>
+ { LocalizeText('playlist.editor.button.play.now') }
+
+ }
+ { (currentPlayingIndex !== -1) &&
+
+
playPause(furniId, selectedItem) }>
+
+
+
+ { LocalizeText('playlist.editor.text.now.playing.in.your.room') }
+
+ { playlist[currentPlayingIndex]?.name + ' - ' + playlist[currentPlayingIndex]?.creator }
+
+
+
+ }
+ >
+ }
+
+ >);
+};
diff --git a/Coolui v3 test/src/components/room/widgets/mysterybox/MysteryBoxExtensionView.tsx b/Coolui v3 test/src/components/room/widgets/mysterybox/MysteryBoxExtensionView.tsx
new file mode 100644
index 0000000000..1e1828627b
--- /dev/null
+++ b/Coolui v3 test/src/components/room/widgets/mysterybox/MysteryBoxExtensionView.tsx
@@ -0,0 +1,67 @@
+import { MysteryBoxKeysUpdateEvent } from '@nitrots/nitro-renderer';
+import { FC, useState } from 'react';
+import { FaChevronDown, FaChevronUp } from 'react-icons/fa';
+import { ColorUtils, LocalizeText } from '../../../../api';
+import { Flex, LayoutGridItem, Text } from '../../../../common';
+import { useNitroEvent } from '../../../../hooks';
+
+const colorMap = {
+ 'purple': 9452386,
+ 'blue': 3891856,
+ 'green': 6459451,
+ 'yellow': 10658089,
+ 'lilac': 6897548,
+ 'orange': 10841125,
+ 'turquoise': 2661026,
+ 'red': 10104881
+};
+
+export const MysteryBoxExtensionView: FC<{}> = props =>
+{
+ const [ isOpen, setIsOpen ] = useState
(true);
+ const [ keyColor, setKeyColor ] = useState('');
+ const [ boxColor, setBoxColor ] = useState('');
+
+ useNitroEvent(MysteryBoxKeysUpdateEvent.MYSTERY_BOX_KEYS_UPDATE, event =>
+ {
+ setKeyColor(event.keyColor);
+ setBoxColor(event.boxColor);
+ });
+
+ const getRgbColor = (color: string) =>
+ {
+ const colorInt = colorMap[color];
+
+ return ColorUtils.int2rgb(colorInt);
+ };
+
+ if(keyColor === '' && boxColor === '') return null;
+
+ return (
+
+
+
setIsOpen(value => !value) }>
+ { LocalizeText('mysterybox.tracker.title') }
+ { isOpen && }
+ { !isOpen && }
+
+ { isOpen &&
+ <>
+
{ LocalizeText('mysterybox.tracker.description') }
+
+ > }
+
+
+ );
+};
diff --git a/Coolui v3 test/src/components/room/widgets/object-location/ObjectLocationView.tsx b/Coolui v3 test/src/components/room/widgets/object-location/ObjectLocationView.tsx
new file mode 100644
index 0000000000..0bd1a4837f
--- /dev/null
+++ b/Coolui v3 test/src/components/room/widgets/object-location/ObjectLocationView.tsx
@@ -0,0 +1,61 @@
+import { GetTicker } from '@nitrots/nitro-renderer';
+import { FC, useEffect, useRef, useState } from 'react';
+import { GetRoomObjectBounds, GetRoomSession } from '../../../../api';
+import { BaseProps } from '../../../../common';
+
+interface ObjectLocationViewProps extends BaseProps
+{
+ objectId: number;
+ category: number;
+ noFollow?: boolean;
+}
+
+export const ObjectLocationView: FC = props =>
+{
+ const { objectId = -1, category = -1, noFollow = false, ...rest } = props;
+ const [ pos, setPos ] = useState<{ x: number, y: number }>({ x: -1, y: -1 });
+ const elementRef = useRef();
+
+ useEffect(() =>
+ {
+ let remove = false;
+
+ const getObjectLocation = () =>
+ {
+ const roomSession = GetRoomSession();
+ const objectBounds = GetRoomObjectBounds(roomSession.roomId, objectId, category, 1);
+
+ return objectBounds;
+ };
+
+ const updatePosition = () =>
+ {
+ const bounds = getObjectLocation();
+
+ if(!bounds || !elementRef.current) return;
+
+ setPos({
+ x: Math.round(((bounds.left + (bounds.width / 2)) - (elementRef.current.offsetWidth / 2))),
+ y: Math.round((bounds.top - elementRef.current.offsetHeight) + 10)
+ });
+ };
+
+ if(noFollow)
+ {
+ updatePosition();
+ }
+ else
+ {
+ remove = true;
+
+ GetTicker().add(updatePosition);
+ }
+
+ return () =>
+ {
+ if(remove) GetTicker().remove(updatePosition);
+ };
+ }, [ objectId, category, noFollow ]);
+
+ return -1) ? 'visible' : 'hidden' } } { ...rest } />;
+};
diff --git a/Coolui v3 test/src/components/room/widgets/pet-package/PetPackageWidgetView.tsx b/Coolui v3 test/src/components/room/widgets/pet-package/PetPackageWidgetView.tsx
new file mode 100644
index 0000000000..9b4ff4a166
--- /dev/null
+++ b/Coolui v3 test/src/components/room/widgets/pet-package/PetPackageWidgetView.tsx
@@ -0,0 +1,41 @@
+import { FC } from 'react';
+import { GetConfigurationValue, LocalizeText } from '../../../../api';
+import { Button, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
+import { usePetPackageWidget } from '../../../../hooks';
+
+export const PetPackageWidgetView: FC<{}> = props =>
+{
+ const { isVisible = false, errorResult = null, petName = null, objectType = null, onChangePetName = null, onConfirm = null, onClose = null } = usePetPackageWidget();
+
+ return (
+ <>
+ { isVisible &&
+
+ onClose() } />
+
+
+
+
+ { objectType === 'gnome_box' ? LocalizeText('widgets.gnomepackage.name.title') : LocalizeText('furni.petpackage') }
+
+
+
+
+
+
onChangePetName(event.target.value) } />
+
+
+ { (errorResult.length > 0) &&
+
{ errorResult }
}
+
+ onClose() }>{ LocalizeText('cancel') }
+ onConfirm() }>{ objectType === 'gnome_box' ? LocalizeText('widgets.gnomepackage.name.pick') : LocalizeText('furni.petpackage.confirm') }
+
+
+
+
+
+ }
+ >
+ );
+};
diff --git a/Coolui v3 test/src/components/room/widgets/room-filter-words/RoomFilterWordsWidgetView.tsx b/Coolui v3 test/src/components/room/widgets/room-filter-words/RoomFilterWordsWidgetView.tsx
new file mode 100644
index 0000000000..bbed44b1f8
--- /dev/null
+++ b/Coolui v3 test/src/components/room/widgets/room-filter-words/RoomFilterWordsWidgetView.tsx
@@ -0,0 +1,75 @@
+import { UpdateRoomFilterMessageComposer } from '@nitrots/nitro-renderer';
+import { FC, useState } from 'react';
+import { LocalizeText, SendMessageComposer } from '../../../../api';
+import { Button, Column, Flex, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
+import { useFilterWordsWidget, useNavigator } from '../../../../hooks';
+import { NitroInput, classNames } from '../../../../layout';
+
+export const RoomFilterWordsWidgetView: FC<{}> = props =>
+{
+ const [ word, setWord ] = useState
('bobba');
+ const [ selectedWord, setSelectedWord ] = useState('');
+ const [ isSelectingWord, setIsSelectingWord ] = useState(false);
+ const { wordsFilter = [], isVisible = null, setWordsFilter, onClose = null } = useFilterWordsWidget();
+ const { navigatorData = null } = useNavigator();
+
+ const processAction = (isAddingWord: boolean) =>
+ {
+ if((isSelectingWord) ? (!selectedWord) : (!word)) return;
+
+ SendMessageComposer(new UpdateRoomFilterMessageComposer(navigatorData.enteredGuestRoom.roomId, isAddingWord, (isSelectingWord ? selectedWord : word)));
+ setSelectedWord('');
+ setWord('bobba');
+ setIsSelectingWord(false);
+
+ if(isAddingWord && wordsFilter.includes((isSelectingWord ? selectedWord : word))) return;
+
+ setWordsFilter(prevValue =>
+ {
+ const newWords = [ ...prevValue ];
+
+ isAddingWord ? newWords.push((isSelectingWord ? selectedWord : word)) : newWords.splice(newWords.indexOf((isSelectingWord ? selectedWord : word)), 1);
+
+ return newWords;
+ });
+ };
+
+ const onTyping = (word: string) =>
+ {
+ setWord(word);
+ setIsSelectingWord(false);
+ };
+
+ const onSelectedWord = (word: string) =>
+ {
+ setSelectedWord(word);
+ setIsSelectingWord(true);
+ };
+
+ if(!isVisible) return null;
+
+ return (
+
+ onClose() } />
+
+
+ onTyping(event.target.value) } />
+ processAction(true) }>{ LocalizeText('navigator.roomsettings.roomfilter.addword') }
+
+
+ { wordsFilter && (wordsFilter.length > 0) && wordsFilter.map((word, index) =>
+ {
+ return (
+ onSelectedWord(word) }>
+ { word }
+
+ );
+ }) }
+
+
+ processAction(false) }>{ LocalizeText('navigator.roomsettings.roomfilter.removeword') }
+
+
+
+ );
+};
diff --git a/Coolui v3 test/src/components/room/widgets/room-promotes/RoomPromotesWidgetView.tsx b/Coolui v3 test/src/components/room/widgets/room-promotes/RoomPromotesWidgetView.tsx
new file mode 100644
index 0000000000..07cf84185d
--- /dev/null
+++ b/Coolui v3 test/src/components/room/widgets/room-promotes/RoomPromotesWidgetView.tsx
@@ -0,0 +1,55 @@
+import { DesktopViewEvent, GetSessionDataManager } from '@nitrots/nitro-renderer';
+import { FC, useState } from 'react';
+import { FaChevronDown, FaChevronUp } from 'react-icons/fa';
+import { Flex, Text } from '../../../../common';
+import { useMessageEvent, useRoomPromote } from '../../../../hooks';
+import { RoomPromoteEditWidgetView, RoomPromoteMyOwnEventWidgetView, RoomPromoteOtherEventWidgetView } from './views';
+
+export const RoomPromotesWidgetView: FC<{}> = props =>
+{
+ const [ isEditingPromote, setIsEditingPromote ] = useState(false);
+ const [ isOpen, setIsOpen ] = useState(true);
+ const { promoteInformation, setPromoteInformation } = useRoomPromote();
+
+ useMessageEvent(DesktopViewEvent, event =>
+ {
+ setPromoteInformation(null);
+ });
+
+ if(!promoteInformation) return null;
+
+ return (
+ <>
+ { promoteInformation.data.adId !== -1 &&
+
+
+ setIsOpen(value => !value) }>
+ { promoteInformation.data.eventName }
+ { isOpen && }
+ { !isOpen && }
+
+ { (isOpen && GetSessionDataManager().userId !== promoteInformation.data.ownerAvatarId) &&
+
+ }
+ { (isOpen && GetSessionDataManager().userId === promoteInformation.data.ownerAvatarId) &&
+ setIsEditingPromote(true) }
+ />
+ }
+ { isEditingPromote &&
+ setIsEditingPromote(false) }
+ />
+ }
+
+
+ }
+ >
+ );
+};
diff --git a/Coolui v3 test/src/components/room/widgets/room-promotes/views/RoomPromoteEditWidgetView.tsx b/Coolui v3 test/src/components/room/widgets/room-promotes/views/RoomPromoteEditWidgetView.tsx
new file mode 100644
index 0000000000..bb3e6102d2
--- /dev/null
+++ b/Coolui v3 test/src/components/room/widgets/room-promotes/views/RoomPromoteEditWidgetView.tsx
@@ -0,0 +1,45 @@
+import { EditEventMessageComposer } from '@nitrots/nitro-renderer';
+import { FC, useState } from 'react';
+import { LocalizeText, SendMessageComposer } from '../../../../../api';
+import { Button, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../../common';
+import { NitroInput } from '../../../../../layout';
+
+interface RoomPromoteEditWidgetViewProps
+{
+ eventId: number;
+ eventName: string;
+ eventDescription: string;
+ setIsEditingPromote: (value: boolean) => void;
+}
+
+export const RoomPromoteEditWidgetView: FC = props =>
+{
+ const { eventId = -1, eventName = '', eventDescription = '', setIsEditingPromote = null } = props;
+ const [ newEventName, setNewEventName ] = useState(eventName);
+ const [ newEventDescription, setNewEventDescription ] = useState(eventDescription);
+
+ const updatePromote = () =>
+ {
+ SendMessageComposer(new EditEventMessageComposer(eventId, newEventName, newEventDescription));
+ setIsEditingPromote(false);
+ };
+
+ return (
+
+ setIsEditingPromote(false) } />
+
+
+ { LocalizeText('navigator.eventsettings.name') }
+ setNewEventName(event.target.value) } />
+
+
+ { LocalizeText('navigator.eventsettings.desc') }
+
+
+
+ updatePromote() }>{ LocalizeText('navigator.eventsettings.edit') }
+
+
+
+ );
+};
diff --git a/Coolui v3 test/src/components/room/widgets/room-promotes/views/RoomPromoteMyOwnEventWidgetView.tsx b/Coolui v3 test/src/components/room/widgets/room-promotes/views/RoomPromoteMyOwnEventWidgetView.tsx
new file mode 100644
index 0000000000..e53497d036
--- /dev/null
+++ b/Coolui v3 test/src/components/room/widgets/room-promotes/views/RoomPromoteMyOwnEventWidgetView.tsx
@@ -0,0 +1,36 @@
+import { CreateLinkEvent } from '@nitrots/nitro-renderer';
+import { FC } from 'react';
+import { LocalizeText } from '../../../../../api';
+import { Button, Flex, Grid, Text } from '../../../../../common';
+import { useRoomPromote } from '../../../../../hooks';
+
+interface RoomPromoteMyOwnEventWidgetViewProps
+{
+ eventDescription: string;
+ setIsEditingPromote: (value: boolean) => void;
+}
+
+export const RoomPromoteMyOwnEventWidgetView: FC = props =>
+{
+ const { eventDescription = '', setIsEditingPromote = null } = props;
+ const { setIsExtended } = useRoomPromote();
+
+ const extendPromote = () =>
+ {
+ setIsExtended(true);
+ CreateLinkEvent('catalog/open/room_event');
+ };
+
+ return (
+ <>
+
+ { eventDescription }
+
+
+
+ setIsEditingPromote(true) }>{ LocalizeText('navigator.roominfo.editevent') }
+ extendPromote() }>{ LocalizeText('roomad.extend.event') }
+
+ >
+ );
+};
diff --git a/Coolui v3 test/src/components/room/widgets/room-promotes/views/RoomPromoteOtherEventWidgetView.tsx b/Coolui v3 test/src/components/room/widgets/room-promotes/views/RoomPromoteOtherEventWidgetView.tsx
new file mode 100644
index 0000000000..3a3ed08af4
--- /dev/null
+++ b/Coolui v3 test/src/components/room/widgets/room-promotes/views/RoomPromoteOtherEventWidgetView.tsx
@@ -0,0 +1,30 @@
+import { FC } from 'react';
+import { LocalizeText } from '../../../../../api';
+import { Column, Flex, Text } from '../../../../../common';
+
+interface RoomPromoteOtherEventWidgetViewProps
+{
+ eventDescription: string;
+}
+
+export const RoomPromoteOtherEventWidgetView: FC = props =>
+{
+ const { eventDescription = '' } = props;
+
+ return (
+ <>
+
+ { eventDescription }
+
+
+
+
+
+ { LocalizeText('navigator.eventinprogress') }
+
+
+
+
+ >
+ );
+};
diff --git a/Coolui v3 test/src/components/room/widgets/room-promotes/views/index.ts b/Coolui v3 test/src/components/room/widgets/room-promotes/views/index.ts
new file mode 100644
index 0000000000..da746917f6
--- /dev/null
+++ b/Coolui v3 test/src/components/room/widgets/room-promotes/views/index.ts
@@ -0,0 +1,3 @@
+export * from './RoomPromoteEditWidgetView';
+export * from './RoomPromoteMyOwnEventWidgetView';
+export * from './RoomPromoteOtherEventWidgetView';
diff --git a/Coolui v3 test/src/components/room/widgets/room-thumbnail/RoomThumbnailWidgetView.tsx b/Coolui v3 test/src/components/room/widgets/room-thumbnail/RoomThumbnailWidgetView.tsx
new file mode 100644
index 0000000000..a14744c259
--- /dev/null
+++ b/Coolui v3 test/src/components/room/widgets/room-thumbnail/RoomThumbnailWidgetView.tsx
@@ -0,0 +1,41 @@
+import { GetRoomEngine, NitroRenderTexture } from '@nitrots/nitro-renderer';
+import { FC, useState } from 'react';
+import { LayoutMiniCameraView } from '../../../../common';
+import { RoomWidgetThumbnailEvent } from '../../../../events';
+import { useRoom, useUiEvent } from '../../../../hooks';
+
+export const RoomThumbnailWidgetView: FC<{}> = props =>
+{
+ const [ isVisible, setIsVisible ] = useState(false);
+ const { roomSession = null } = useRoom();
+
+ useUiEvent([
+ RoomWidgetThumbnailEvent.SHOW_THUMBNAIL,
+ RoomWidgetThumbnailEvent.HIDE_THUMBNAIL,
+ RoomWidgetThumbnailEvent.TOGGLE_THUMBNAIL ], event =>
+ {
+ switch(event.type)
+ {
+ case RoomWidgetThumbnailEvent.SHOW_THUMBNAIL:
+ setIsVisible(true);
+ return;
+ case RoomWidgetThumbnailEvent.HIDE_THUMBNAIL:
+ setIsVisible(false);
+ return;
+ case RoomWidgetThumbnailEvent.TOGGLE_THUMBNAIL:
+ setIsVisible(value => !value);
+ return;
+ }
+ });
+
+ const receiveTexture = async (texture: NitroRenderTexture) =>
+ {
+ await GetRoomEngine().saveTextureAsScreenshot(texture, true);
+
+ setIsVisible(false);
+ };
+
+ if(!isVisible) return null;
+
+ return setIsVisible(false) } />;
+};
diff --git a/Coolui v3 test/src/components/room/widgets/room-tools/RoomToolsWidgetView.tsx b/Coolui v3 test/src/components/room/widgets/room-tools/RoomToolsWidgetView.tsx
new file mode 100644
index 0000000000..19d382ac8f
--- /dev/null
+++ b/Coolui v3 test/src/components/room/widgets/room-tools/RoomToolsWidgetView.tsx
@@ -0,0 +1,162 @@
+import { CreateLinkEvent, GetGuestRoomResultEvent, GetRoomEngine, NavigatorSearchComposer, RateFlatMessageComposer } from '@nitrots/nitro-renderer';
+import { AnimatePresence, motion } from 'framer-motion';
+import { classNames } from '../../../../layout';
+import { FC, useEffect, useState } from 'react';
+import { GetConfigurationValue, LocalizeText, SendMessageComposer, SetLocalStorage, TryVisitRoom } from '../../../../api';
+import { Text } from '../../../../common';
+import { useMessageEvent, useNavigator, useRoom } from '../../../../hooks';
+
+export const RoomToolsWidgetView: FC<{}> = props => {
+ const [areBubblesMuted, setAreBubblesMuted] = useState(false);
+ const [isZoomedIn, setIsZoomedIn] = useState(false);
+ const [roomName, setRoomName] = useState(null);
+ const [roomOwner, setRoomOwner] = useState(null);
+ const [roomTags, setRoomTags] = useState(null);
+ const [isOpen, setIsOpen] = useState(false);
+ const [isOpenHistory, setIsOpenHistory] = useState(false);
+ const [roomHistory, setRoomHistory] = useState<{ roomId: number, roomName: string }[]>([]);
+ const { navigatorData = null } = useNavigator();
+ const { roomSession = null } = useRoom();
+
+ const handleToolClick = (action: string, value?: string) => {
+ if (!roomSession) return;
+
+ switch (action) {
+ case 'settings':
+ CreateLinkEvent('navigator/toggle-room-info');
+ return;
+ case 'zoom':
+ setIsZoomedIn(prevValue => {
+ if (GetConfigurationValue('room.zoom.enabled', true)) {
+ const scale = GetRoomEngine().getRoomInstanceRenderingCanvasScale(roomSession.roomId, 1);
+ GetRoomEngine().setRoomInstanceRenderingCanvasScale(roomSession.roomId, 1, scale === 1 ? 0.5 : 1);
+ } else {
+ const geometry = GetRoomEngine().getRoomInstanceGeometry(roomSession.roomId, 1);
+ if (geometry) geometry.performZoom();
+ }
+ return !prevValue;
+ });
+ return;
+ case 'chat_history':
+ CreateLinkEvent('chat-history/toggle');
+ return;
+ case 'hiddenbubbles':
+ CreateLinkEvent('nitrobubblehidden/toggle');
+ setAreBubblesMuted(prev => !prev);
+ return;
+ case 'like_room':
+ SendMessageComposer(new RateFlatMessageComposer(1));
+ return;
+ case 'toggle_room_link':
+ CreateLinkEvent('navigator/toggle-room-link');
+ return;
+ case 'navigator_search_tag':
+ CreateLinkEvent(`navigator/search/${value}`);
+ SendMessageComposer(new NavigatorSearchComposer('hotel_view', `tag:${value}`));
+ return;
+ case 'room_history':
+ if (roomHistory.length > 0) setIsOpenHistory(prev => !prev);
+ return;
+ case 'room_history_back':
+ const prevIndex = roomHistory.findIndex(room => room.roomId === navigatorData.currentRoomId) - 1;
+ if (prevIndex >= 0) TryVisitRoom(roomHistory[prevIndex].roomId);
+ return;
+ case 'room_history_next':
+ const nextIndex = roomHistory.findIndex(room => room.roomId === navigatorData.currentRoomId) + 1;
+ if (nextIndex < roomHistory.length) TryVisitRoom(roomHistory[nextIndex].roomId);
+ return;
+ }
+ };
+
+ const onChangeRoomHistory = (roomId: number, roomName: string) => {
+ let newStorage = JSON.parse(window.localStorage.getItem('nitro.room.history') || '[]');
+ if (newStorage.some((room: { roomId: number }) => room.roomId === roomId)) return;
+
+ if (newStorage.length >= 10) newStorage.shift();
+ newStorage = [...newStorage, { roomId, roomName }];
+
+ setRoomHistory(newStorage);
+ SetLocalStorage('nitro.room.history', newStorage);
+ };
+
+ useMessageEvent(GetGuestRoomResultEvent, event => {
+ const parser = event.getParser();
+ if (!parser.roomEnter || (parser.data.roomId !== roomSession.roomId)) return;
+
+ if (roomName !== parser.data.roomName) setRoomName(parser.data.roomName);
+ if (roomOwner !== parser.data.ownerName) setRoomOwner(parser.data.ownerName);
+ if (roomTags !== parser.data.tags) setRoomTags(parser.data.tags);
+ onChangeRoomHistory(parser.data.roomId, parser.data.roomName);
+ });
+
+ useEffect(() => {
+ setIsOpen(true);
+ const timeout = setTimeout(() => setIsOpen(false), 5000);
+ return () => clearTimeout(timeout);
+ }, [roomName, roomOwner, roomTags]);
+
+ useEffect(() => {
+ setRoomHistory(JSON.parse(window.localStorage.getItem('nitro.room.history') || '[]'));
+ }, []);
+
+ useEffect(() => {
+ const handleTabClose = () => {
+ window.localStorage.removeItem('nitro.room.history');
+ };
+ window.addEventListener('beforeunload', handleTabClose);
+ return () => window.removeEventListener('beforeunload', handleTabClose);
+ }, []);
+
+ return (
+
+
+
handleToolClick('settings')} />
+
handleToolClick('zoom')} />
+
handleToolClick('chat_history')} />
+
handleToolClick('hiddenbubbles')} />
+
+ {navigatorData.canRate && (
+
handleToolClick('like_room')} />
+ )}
+
handleToolClick('toggle_room_link')} />
+
handleToolClick('room_history')} />
+
+
+