🆙 Init V3

This commit is contained in:
DuckieTM
2026-01-31 09:10:52 +01:00
commit 7feb10ab15
1733 changed files with 53405 additions and 0 deletions
@@ -0,0 +1,174 @@
import { GetRoomEngine, RoomEngineObjectEvent, RoomEngineRoomAdEvent, RoomEngineTriggerWidgetEvent, RoomEngineUseProductEvent, RoomId, RoomSessionErrorMessageEvent, RoomZoomEvent } from '@nitrots/nitro-renderer';
import { FC } from 'react';
import { DispatchUiEvent, LocalizeText, NotificationAlertType, RoomWidgetUpdateRoomObjectEvent } from '../../../api';
import { useNitroEvent, useNotification, useRoom } from '../../../hooks';
import { AvatarInfoWidgetView } from './avatar-info/AvatarInfoWidgetView';
import { ChatInputView } from './chat-input/ChatInputView';
import { ChatWidgetView } from './chat/ChatWidgetView';
import { FurniChooserWidgetView } from './choosers/FurniChooserWidgetView';
import { UserChooserWidgetView } from './choosers/UserChooserWidgetView';
import { DoorbellWidgetView } from './doorbell/DoorbellWidgetView';
import { FriendRequestWidgetView } from './friend-request/FriendRequestWidgetView';
import { FurnitureWidgetsView } from './furniture/FurnitureWidgetsView';
import { PetPackageWidgetView } from './pet-package/PetPackageWidgetView';
import { RoomFilterWordsWidgetView } from './room-filter-words/RoomFilterWordsWidgetView';
import { RoomThumbnailWidgetView } from './room-thumbnail/RoomThumbnailWidgetView';
import { RoomToolsWidgetView } from './room-tools/RoomToolsWidgetView';
import { WordQuizWidgetView } from './word-quiz/WordQuizWidgetView';
export const RoomWidgetsView: FC<{}> = props =>
{
const { roomSession = null } = useRoom();
const { simpleAlert = null } = useNotification();
useNitroEvent<RoomZoomEvent>(RoomZoomEvent.ROOM_ZOOM, event => GetRoomEngine().setRoomInstanceRenderingCanvasScale(event.roomId, 1, (((event.level)<1) ? 0.5 : (1 << (Math.floor(event.level) - 1))), null, null, event.isFlipForced));
useNitroEvent<RoomEngineObjectEvent>(
[
RoomEngineTriggerWidgetEvent.REQUEST_TEASER,
RoomEngineTriggerWidgetEvent.REQUEST_ECOTRONBOX,
RoomEngineTriggerWidgetEvent.REQUEST_CLOTHING_CHANGE,
RoomEngineTriggerWidgetEvent.REQUEST_PLAYLIST_EDITOR,
RoomEngineTriggerWidgetEvent.OPEN_WIDGET,
RoomEngineTriggerWidgetEvent.CLOSE_WIDGET,
RoomEngineRoomAdEvent.FURNI_CLICK,
RoomEngineRoomAdEvent.FURNI_DOUBLE_CLICK,
RoomEngineRoomAdEvent.TOOLTIP_SHOW,
RoomEngineRoomAdEvent.TOOLTIP_HIDE,
], event =>
{
if(!roomSession) return;
const objectId = event.objectId;
const category = event.category;
let updateEvent: RoomWidgetUpdateRoomObjectEvent = null;
switch(event.type)
{
case RoomEngineTriggerWidgetEvent.REQUEST_TEASER:
//widgetHandler.processWidgetMessage(new RoomWidgetFurniToWidgetMessage(RoomWidgetFurniToWidgetMessage.REQUEST_TEASER, objectId, category, event.roomId));
break;
case RoomEngineTriggerWidgetEvent.REQUEST_ECOTRONBOX:
//widgetHandler.processWidgetMessage(new RoomWidgetFurniToWidgetMessage(RoomWidgetFurniToWidgetMessage.REQUEST_ECOTRONBOX, objectId, category, event.roomId));
break;
case RoomEngineTriggerWidgetEvent.REQUEST_PLACEHOLDER:
//widgetHandler.processWidgetMessage(new RoomWidgetFurniToWidgetMessage(RoomWidgetFurniToWidgetMessage.REQUEST_PLACEHOLDER, objectId, category, event.roomId));
break;
case RoomEngineTriggerWidgetEvent.REQUEST_CLOTHING_CHANGE:
//widgetHandler.processWidgetMessage(new RoomWidgetFurniToWidgetMessage(RoomWidgetFurniToWidgetMessage.REQUEST_CLOTHING_CHANGE, objectId, category, event.roomId));
break;
case RoomEngineTriggerWidgetEvent.REQUEST_PLAYLIST_EDITOR:
//widgetHandler.processWidgetMessage(new RoomWidgetFurniToWidgetMessage(RoomWidgetFurniToWidgetMessage.REQUEST_PLAYLIST_EDITOR, objectId, category, event.roomId));
break;
case RoomEngineTriggerWidgetEvent.OPEN_WIDGET:
case RoomEngineTriggerWidgetEvent.CLOSE_WIDGET:
case RoomEngineUseProductEvent.USE_PRODUCT_FROM_ROOM:
//widgetHandler.processEvent(event);
break;
case RoomEngineRoomAdEvent.FURNI_CLICK:
case RoomEngineRoomAdEvent.FURNI_DOUBLE_CLICK:
//handleRoomAdClick(event);
break;
case RoomEngineRoomAdEvent.TOOLTIP_SHOW:
case RoomEngineRoomAdEvent.TOOLTIP_HIDE:
//handleRoomAdTooltip(event);
break;
}
if(!updateEvent) return;
let dispatchEvent = true;
if(RoomId.isRoomPreviewerId(updateEvent.roomId)) return;
if(updateEvent instanceof RoomWidgetUpdateRoomObjectEvent) dispatchEvent = (!RoomId.isRoomPreviewerId(updateEvent.roomId));
if(dispatchEvent) DispatchUiEvent(updateEvent);
});
useNitroEvent<RoomSessionErrorMessageEvent>(
[
RoomSessionErrorMessageEvent.RSEME_KICKED,
RoomSessionErrorMessageEvent.RSEME_PETS_FORBIDDEN_IN_HOTEL,
RoomSessionErrorMessageEvent.RSEME_PETS_FORBIDDEN_IN_FLAT,
RoomSessionErrorMessageEvent.RSEME_MAX_PETS,
RoomSessionErrorMessageEvent.RSEME_MAX_NUMBER_OF_OWN_PETS,
RoomSessionErrorMessageEvent.RSEME_NO_FREE_TILES_FOR_PET,
RoomSessionErrorMessageEvent.RSEME_SELECTED_TILE_NOT_FREE_FOR_PET,
RoomSessionErrorMessageEvent.RSEME_BOTS_FORBIDDEN_IN_HOTEL,
RoomSessionErrorMessageEvent.RSEME_BOTS_FORBIDDEN_IN_FLAT,
RoomSessionErrorMessageEvent.RSEME_BOT_LIMIT_REACHED,
RoomSessionErrorMessageEvent.RSEME_SELECTED_TILE_NOT_FREE_FOR_BOT,
RoomSessionErrorMessageEvent.RSEME_BOT_NAME_NOT_ACCEPTED,
], event =>
{
let errorTitle = LocalizeText('error.title');
let errorMessage: string = '';
switch(event.type)
{
case RoomSessionErrorMessageEvent.RSEME_MAX_PETS:
errorMessage = LocalizeText('room.error.max_pets');
break;
case RoomSessionErrorMessageEvent.RSEME_MAX_NUMBER_OF_OWN_PETS:
errorMessage = LocalizeText('room.error.max_own_pets');
break;
case RoomSessionErrorMessageEvent.RSEME_KICKED:
errorMessage = LocalizeText('room.error.kicked');
errorTitle = LocalizeText('generic.alert.title');
break;
case RoomSessionErrorMessageEvent.RSEME_PETS_FORBIDDEN_IN_HOTEL:
errorMessage = LocalizeText('room.error.pets.forbidden_in_hotel');
break;
case RoomSessionErrorMessageEvent.RSEME_PETS_FORBIDDEN_IN_FLAT:
errorMessage = LocalizeText('room.error.pets.forbidden_in_flat');
break;
case RoomSessionErrorMessageEvent.RSEME_NO_FREE_TILES_FOR_PET:
errorMessage = LocalizeText('room.error.pets.no_free_tiles');
break;
case RoomSessionErrorMessageEvent.RSEME_SELECTED_TILE_NOT_FREE_FOR_PET:
errorMessage = LocalizeText('room.error.pets.selected_tile_not_free');
break;
case RoomSessionErrorMessageEvent.RSEME_BOTS_FORBIDDEN_IN_HOTEL:
errorMessage = LocalizeText('room.error.bots.forbidden_in_hotel');
break;
case RoomSessionErrorMessageEvent.RSEME_BOTS_FORBIDDEN_IN_FLAT:
errorMessage = LocalizeText('room.error.bots.forbidden_in_flat');
break;
case RoomSessionErrorMessageEvent.RSEME_BOT_LIMIT_REACHED:
errorMessage = LocalizeText('room.error.max_bots');
break;
case RoomSessionErrorMessageEvent.RSEME_SELECTED_TILE_NOT_FREE_FOR_BOT:
errorMessage = LocalizeText('room.error.bots.selected_tile_not_free');
break;
case RoomSessionErrorMessageEvent.RSEME_BOT_NAME_NOT_ACCEPTED:
errorMessage = LocalizeText('room.error.bots.name.not.accepted');
break;
default:
return;
}
simpleAlert(errorMessage, NotificationAlertType.DEFAULT, null, null, errorTitle);
});
return (
<>
<div className="absolute top-0 left-0 pointer-events-none size-full">
<FurnitureWidgetsView />
</div>
<AvatarInfoWidgetView />
<ChatWidgetView />
<ChatInputView />
<DoorbellWidgetView />
<RoomToolsWidgetView />
<RoomFilterWordsWidgetView />
<RoomThumbnailWidgetView />
<FurniChooserWidgetView />
<PetPackageWidgetView />
<UserChooserWidgetView />
<WordQuizWidgetView />
<FriendRequestWidgetView />
</>
);
};
@@ -0,0 +1,59 @@
import { IRoomUserData, PetTrainingMessageParser, PetTrainingPanelMessageEvent } from '@nitrots/nitro-renderer';
import { FC, useState } from 'react';
import { LocalizeText } from '../../../../api';
import { Button, Column, Flex, Grid, LayoutPetImageView, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
import { useMessageEvent, useRoom, useSessionInfo } from '../../../../hooks';
export const AvatarInfoPetTrainingPanelView: FC<{}> = props =>
{
const [ petData, setPetData ] = useState<IRoomUserData>(null);
const [ petTrainInformation, setPetTrainInformation ] = useState<PetTrainingMessageParser>(null);
const { chatStyleId = 0 } = useSessionInfo();
const { roomSession = null } = useRoom();
useMessageEvent<PetTrainingPanelMessageEvent>(PetTrainingPanelMessageEvent, event =>
{
const parser = event.getParser();
if(!parser) return;
const roomPetData = roomSession.userDataManager.getPetData(parser.petId);
if(!roomPetData) return;
setPetData(roomPetData);
setPetTrainInformation(parser);
});
const processPetAction = (petName: string, commandName: string) =>
{
if(!petName || !commandName) return;
roomSession?.sendChatMessage(`${ petName } ${ commandName }`, chatStyleId);
};
if(!petData || !petTrainInformation) return null;
return (
<NitroCardView className="user-settings-window no-resize" theme="primary-slim" uniqueKey="user-settings">
<NitroCardHeaderView headerText={ LocalizeText('widgets.pet.commands.title') } onCloseClick={ () => setPetTrainInformation(null) } />
<NitroCardContentView className="text-black">
<Flex alignItems="center" gap={ 2 } justifyContent="center">
<Grid columnCount={ 2 }>
<Column fullWidth className="body-image pet p-1" overflow="hidden">
<LayoutPetImageView direction={ 2 } figure={ petData.figure } posture={ 'std' } />
</Column>
<Text small wrap variant="black">{ petData.name }</Text>
</Grid>
</Flex>
<Grid columnCount={ 2 }>
{
(petTrainInformation.commands && petTrainInformation.commands.length > 0) && petTrainInformation.commands.map((command, index) =>
<Button key={ index } disabled={ !petTrainInformation.enabledCommands.includes(command) } onClick={ () => processPetAction(petData.name, LocalizeText(`pet.command.${ command }`)) }>{ LocalizeText(`pet.command.${ command }`) }</Button>
)
}
</Grid>
</NitroCardContentView>
</NitroCardView>
);
};
@@ -0,0 +1,69 @@
import { BotSkillSaveComposer } from '@nitrots/nitro-renderer';
import { FC, useMemo, useState } from 'react';
import { BotSkillsEnum, GetRoomObjectBounds, GetRoomSession, LocalizeText, RoomWidgetUpdateRentableBotChatEvent, SendMessageComposer } from '../../../../api';
import { Button, Column, DraggableWindow, DraggableWindowPosition, Flex, Text } from '../../../../common';
import { NitroInput } from '../../../../layout';
import { ContextMenuHeaderView } from '../context-menu/ContextMenuHeaderView';
interface AvatarInfoRentableBotChatViewProps
{
chatEvent: RoomWidgetUpdateRentableBotChatEvent;
onClose(): void;
}
export const AvatarInfoRentableBotChatView: FC<AvatarInfoRentableBotChatViewProps> = props =>
{
const { chatEvent = null, onClose = null } = props;
const [ newText, setNewText ] = useState<string>(chatEvent.chat === '${bot.skill.chatter.configuration.text.placeholder}' ? '' : chatEvent.chat);
const [ automaticChat, setAutomaticChat ] = useState<boolean>(chatEvent.automaticChat);
const [ mixSentences, setMixSentences ] = useState<boolean>(chatEvent.mixSentences);
const [ chatDelay, setChatDelay ] = useState<number>(chatEvent.chatDelay);
const getObjectLocation = useMemo(() => GetRoomObjectBounds(GetRoomSession().roomId, chatEvent.objectId, chatEvent.category, 1), [ chatEvent ]);
const formatChatString = (value: string) => value.replace(/;#;/g, ' ').replace(/\r\n|\r|\n/g, '\r');
const save = () =>
{
const chatConfiguration = formatChatString(newText) + ';#;' + automaticChat + ';#;' + chatDelay + ';#;' + mixSentences;
SendMessageComposer(new BotSkillSaveComposer(chatEvent.botId, BotSkillsEnum.SETUP_CHAT, chatConfiguration));
onClose();
};
return (
<DraggableWindow dragStyle={ { top: getObjectLocation.y, left: getObjectLocation.x } } handleSelector=".drag-handler" windowPosition={ DraggableWindowPosition.NOTHING }>
<div className="nitro-context-menu bot-chat">
<ContextMenuHeaderView className="drag-handler">
{ LocalizeText('bot.skill.chatter.configuration.title') }
</ContextMenuHeaderView>
<Column className="p-1">
<div className="flex flex-col gap-1">
<Text variant="white">{ LocalizeText('bot.skill.chatter.configuration.chat.text') }</Text>
<textarea className="min-h-[calc(1.5em+ .5rem+2px)] px-[.5rem] py-[.25rem] rounded-[.2rem] form-control-sm" placeholder={ LocalizeText('bot.skill.chatter.configuration.text.placeholder') } rows={ 7 } value={ newText } onChange={ e => setNewText(e.target.value) } />
</div>
<div className="flex flex-col gap-1">
<Flex alignItems="center" gap={ 1 } justifyContent="between">
<Text fullWidth variant="white">{ LocalizeText('bot.skill.chatter.configuration.automatic.chat') }</Text>
<input checked={ automaticChat } className="form-check-input" type="checkbox" onChange={ event => setAutomaticChat(event.target.checked) } />
</Flex>
<Flex alignItems="center" gap={ 1 } justifyContent="between">
<Text fullWidth variant="white">{ LocalizeText('bot.skill.chatter.configuration.markov') }</Text>
<input checked={ mixSentences } className="form-check-input" type="checkbox" onChange={ event => setMixSentences(event.target.checked) } />
</Flex>
<Flex alignItems="center" gap={ 1 } justifyContent="between">
<Text fullWidth variant="white">{ LocalizeText('bot.skill.chatter.configuration.chat.delay') }</Text>
<NitroInput type="number" value={ chatDelay } onChange={ event => setChatDelay(event.target.valueAsNumber) } />
</Flex>
</div>
<Flex alignItems="center" gap={ 1 } justifyContent="between">
<Button fullWidth variant="primary" onClick={ onClose }>{ LocalizeText('cancel') }</Button>
<Button fullWidth variant="success" onClick={ save }>{ LocalizeText('save') }</Button>
</Flex>
</Column>
</div>
</DraggableWindow>
);
};
@@ -0,0 +1,281 @@
import { GetRoomEngine, IFurnitureData, IPetCustomPart, IRoomUserData, PetCustomPart, PetFigureData, RoomObjectCategory, RoomObjectVariable } from '@nitrots/nitro-renderer';
import { FC, useEffect, useMemo, useState } from 'react';
import { FurniCategory, GetFurnitureDataForRoomObject, LocalizeText, UseProductItem } from '../../../../api';
import { Button, Column, Flex, LayoutPetImageView, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
import { useRoom } from '../../../../hooks';
interface AvatarInfoUseProductConfirmViewProps
{
item: UseProductItem;
onClose: () => void;
}
const PRODUCT_PAGE_UKNOWN: number = -1;
const PRODUCT_PAGE_SHAMPOO: number = 0;
const PRODUCT_PAGE_CUSTOM_PART: number = 1;
const PRODUCT_PAGE_CUSTOM_PART_SHAMPOO: number = 2;
const PRODUCT_PAGE_SADDLE: number = 3;
const PRODUCT_PAGE_REVIVE: number = 4;
const PRODUCT_PAGE_REBREED: number = 5;
const PRODUCT_PAGE_FERTILIZE: number = 6;
export const AvatarInfoUseProductConfirmView: FC<AvatarInfoUseProductConfirmViewProps> = props =>
{
const { item = null, onClose = null } = props;
const [ mode, setMode ] = useState(PRODUCT_PAGE_UKNOWN);
const [ petData, setPetData ] = useState<IRoomUserData>(null);
const [ furniData, setFurniData ] = useState<IFurnitureData>(null);
const { roomSession = null } = useRoom();
const selectRoomObject = () =>
{
if(!petData) return;
GetRoomEngine().selectRoomObject(roomSession.roomId, petData.roomIndex, RoomObjectCategory.UNIT);
};
const useProduct = () =>
{
roomSession.usePetProduct(item.requestRoomObjectId, petData.webID);
onClose();
};
const getPetImage = useMemo(() =>
{
if(!petData || !furniData) return null;
const petFigureData = new PetFigureData(petData.figure);
const customParts = furniData.customParams.split(' ');
const petIndex = parseInt(customParts[0]);
switch(furniData.specialType)
{
case FurniCategory.PET_SHAMPOO: {
if(customParts.length < 2) return null;
const currentPalette = GetRoomEngine().getPetColorResult(petIndex, petFigureData.paletteId);
const possiblePalettes = GetRoomEngine().getPetColorResultsForTag(petIndex, customParts[1]);
let paletteId = -1;
for(const result of possiblePalettes)
{
if(result.breed === currentPalette.breed)
{
paletteId = parseInt(result.id);
break;
}
}
return <LayoutPetImageView customParts={ petFigureData.customParts } direction={ 2 } paletteId={ paletteId } petColor={ petFigureData.color } typeId={ petFigureData.typeId } />;
}
case FurniCategory.PET_CUSTOM_PART: {
if(customParts.length < 4) return null;
const newCustomParts: IPetCustomPart[] = [];
const _local_6 = customParts[1].split(',').map(piece => parseInt(piece));
const _local_7 = customParts[2].split(',').map(piece => parseInt(piece));
const _local_8 = customParts[3].split(',').map(piece => parseInt(piece));
let _local_10 = 0;
while(_local_10 < _local_6.length)
{
const _local_13 = _local_6[_local_10];
const _local_15 = petFigureData.getCustomPart(_local_13);
let _local_12 = _local_8[_local_10];
if(_local_15 != null) _local_12 = _local_15.paletteId;
newCustomParts.push(new PetCustomPart(_local_13, _local_7[_local_10], _local_12));
_local_10++;
}
return <LayoutPetImageView customParts={ newCustomParts } direction={ 2 } paletteId={ petFigureData.paletteId } petColor={ petFigureData.color } typeId={ petFigureData.typeId } />;
}
case FurniCategory.PET_CUSTOM_PART_SHAMPOO: {
if(customParts.length < 3) return null;
const newCustomParts: IPetCustomPart[] = [];
const _local_6 = customParts[1].split(',').map(piece => parseInt(piece));
const _local_8 = customParts[2].split(',').map(piece => parseInt(piece));
let _local_10 = 0;
while(_local_10 < _local_6.length)
{
const _local_13 = _local_6[_local_10];
const _local_15 = petFigureData.getCustomPart(_local_13);
let _local_14 = -1;
if(_local_15 != null) _local_14 = _local_15.partId;
newCustomParts.push(new PetCustomPart(_local_6[_local_10], _local_14, _local_8[_local_10]));
_local_10++;
}
return <LayoutPetImageView customParts={ newCustomParts } direction={ 2 } paletteId={ petFigureData.paletteId } petColor={ petFigureData.color } typeId={ petFigureData.typeId } />;
}
case FurniCategory.PET_SADDLE: {
if(customParts.length < 4) return null;
const newCustomParts: IPetCustomPart[] = [];
const _local_6 = customParts[1].split(',').map(piece => parseInt(piece));
const _local_7 = customParts[2].split(',').map(piece => parseInt(piece));
const _local_8 = customParts[3].split(',').map(piece => parseInt(piece));
let _local_10 = 0;
while(_local_10 < _local_6.length)
{
newCustomParts.push(new PetCustomPart(_local_6[_local_10], _local_7[_local_10], _local_8[_local_10]));
_local_10++;
}
for(const _local_21 of petFigureData.customParts)
{
if(_local_6.indexOf(_local_21.layerId) === -1)
{
newCustomParts.push(_local_21);
}
}
return <LayoutPetImageView customParts={ newCustomParts } direction={ 2 } paletteId={ petFigureData.paletteId } petColor={ petFigureData.color } typeId={ petFigureData.typeId } />;
}
case FurniCategory.MONSTERPLANT_REBREED:
case FurniCategory.MONSTERPLANT_REVIVAL:
case FurniCategory.MONSTERPLANT_FERTILIZE: {
let posture = 'rip';
const roomObject = GetRoomEngine().getRoomObject(roomSession.roomId, petData.roomIndex, RoomObjectCategory.UNIT);
if(roomObject)
{
posture = roomObject.model.getValue<string>(RoomObjectVariable.FIGURE_POSTURE);
if(posture === 'rip')
{
const level = petData.petLevel;
if(level < 7) posture = `grw${ level }`;
else posture = 'std';
}
}
return <LayoutPetImageView customParts={ petFigureData.customParts } direction={ 2 } paletteId={ petFigureData.paletteId } petColor={ petFigureData.color } posture={ posture } typeId={ petFigureData.typeId } />;
}
}
}, [ petData, furniData, roomSession ]);
useEffect(() =>
{
const userData = roomSession.userDataManager.getUserDataByIndex(item.id);
setPetData(userData);
const furniData = GetFurnitureDataForRoomObject(roomSession.roomId, item.requestRoomObjectId, RoomObjectCategory.FLOOR);
if(!furniData) return;
setFurniData(furniData);
let mode = PRODUCT_PAGE_UKNOWN;
switch(furniData.specialType)
{
case FurniCategory.PET_SHAMPOO:
mode = PRODUCT_PAGE_SHAMPOO;
break;
case FurniCategory.PET_CUSTOM_PART:
mode = PRODUCT_PAGE_CUSTOM_PART;
break;
case FurniCategory.PET_CUSTOM_PART_SHAMPOO:
mode = PRODUCT_PAGE_CUSTOM_PART_SHAMPOO;
break;
case FurniCategory.PET_SADDLE:
mode = PRODUCT_PAGE_SADDLE;
break;
case FurniCategory.MONSTERPLANT_REVIVAL:
mode = PRODUCT_PAGE_REVIVE;
break;
case FurniCategory.MONSTERPLANT_REBREED:
mode = PRODUCT_PAGE_REBREED;
break;
case FurniCategory.MONSTERPLANT_FERTILIZE:
mode = PRODUCT_PAGE_FERTILIZE;
break;
}
setMode(mode);
}, [ roomSession, item ]);
if(!petData) return null;
return (
<NitroCardView className="nitro-use-product-confirmation">
<NitroCardHeaderView headerText={ LocalizeText('useproduct.widget.title', [ 'name' ], [ petData.name ]) } onCloseClick={ onClose } />
<NitroCardContentView center>
<Flex gap={ 2 } overflow="hidden">
<div className="flex flex-col">
<div className="product-preview cursor-pointer" onClick={ selectRoomObject }>
{ getPetImage }
</div>
</div>
<Column justifyContent="between" overflow="auto">
<Column gap={ 2 }>
{ (mode === PRODUCT_PAGE_SHAMPOO) &&
<>
<Text>{ LocalizeText('useproduct.widget.text.shampoo', [ 'productName' ], [ furniData.name ]) }</Text>
<Text>{ LocalizeText('useproduct.widget.info.shampoo') }</Text>
</> }
{ (mode === PRODUCT_PAGE_CUSTOM_PART) &&
<>
<Text>{ LocalizeText('useproduct.widget.text.custompart', [ 'productName' ], [ furniData.name ]) }</Text>
<Text>{ LocalizeText('useproduct.widget.info.custompart') }</Text>
</> }
{ (mode === PRODUCT_PAGE_CUSTOM_PART_SHAMPOO) &&
<>
<Text>{ LocalizeText('useproduct.widget.text.custompartshampoo', [ 'productName' ], [ furniData.name ]) }</Text>
<Text>{ LocalizeText('useproduct.widget.info.custompartshampoo') }</Text>
</> }
{ (mode === PRODUCT_PAGE_SADDLE) &&
<>
<Text>{ LocalizeText('useproduct.widget.text.saddle', [ 'productName' ], [ furniData.name ]) }</Text>
<Text>{ LocalizeText('useproduct.widget.info.saddle') }</Text>
</> }
{ (mode === PRODUCT_PAGE_REVIVE) &&
<>
<Text>{ LocalizeText('useproduct.widget.text.revive_monsterplant', [ 'productName' ], [ furniData.name ]) }</Text>
<Text>{ LocalizeText('useproduct.widget.info.revive_monsterplant') }</Text>
</> }
{ (mode === PRODUCT_PAGE_REBREED) &&
<>
<Text>{ LocalizeText('useproduct.widget.text.rebreed_monsterplant', [ 'productName' ], [ furniData.name ]) }</Text>
<Text>{ LocalizeText('useproduct.widget.info.rebreed_monsterplant') }</Text>
</> }
{ (mode === PRODUCT_PAGE_FERTILIZE) &&
<>
<Text>{ LocalizeText('useproduct.widget.text.fertilize_monsterplant', [ 'productName' ], [ furniData.name ]) }</Text>
<Text>{ LocalizeText('useproduct.widget.info.fertilize_monsterplant') }</Text>
</> }
</Column>
<div className="flex items-center justify-between">
<Button variant="danger" onClick={ onClose }>{ LocalizeText('useproduct.widget.cancel') }</Button>
<Button variant="success" onClick={ useProduct }>{ LocalizeText('useproduct.widget.use') }</Button>
</div>
</Column>
</Flex>
</NitroCardContentView>
</NitroCardView>
);
};
@@ -0,0 +1,135 @@
import { RoomObjectCategory, RoomObjectType } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { FurniCategory, GetFurnitureDataForRoomObject, LocalizeText, UseProductItem } from '../../../../api';
import { useRoom } from '../../../../hooks';
import { ContextMenuHeaderView } from '../context-menu/ContextMenuHeaderView';
import { ContextMenuListItemView } from '../context-menu/ContextMenuListItemView';
import { ContextMenuView } from '../context-menu/ContextMenuView';
interface AvatarInfoUseProductViewProps
{
item: UseProductItem;
updateConfirmingProduct: (product: UseProductItem) => void;
onClose: () => void;
}
const PRODUCT_PAGE_UKNOWN: number = 0;
const PRODUCT_PAGE_SHAMPOO: number = 1;
const PRODUCT_PAGE_CUSTOM_PART: number = 2;
const PRODUCT_PAGE_CUSTOM_PART_SHAMPOO: number = 3;
const PRODUCT_PAGE_SADDLE: number = 4;
const PRODUCT_PAGE_REVIVE: number = 5;
const PRODUCT_PAGE_REBREED: number = 6;
const PRODUCT_PAGE_FERTILIZE: number = 7;
export const AvatarInfoUseProductView: FC<AvatarInfoUseProductViewProps> = props =>
{
const { item = null, updateConfirmingProduct = null, onClose = null } = props;
const [ mode, setMode ] = useState(0);
const { roomSession = null } = useRoom();
const processAction = (name: string) =>
{
if(!name) return;
switch(name)
{
case 'use_product':
case 'use_product_shampoo':
case 'use_product_custom_part':
case 'use_product_custom_part_shampoo':
case 'use_product_saddle':
case 'replace_product_saddle':
case 'revive_monsterplant':
case 'rebreed_monsterplant':
case 'fertilize_monsterplant':
updateConfirmingProduct(item);
break;
}
};
useEffect(() =>
{
if(!item) return;
const furniData = GetFurnitureDataForRoomObject(roomSession.roomId, item.requestRoomObjectId, RoomObjectCategory.FLOOR);
if(!furniData) return;
let mode = PRODUCT_PAGE_UKNOWN;
switch(furniData.specialType)
{
case FurniCategory.PET_SHAMPOO:
mode = PRODUCT_PAGE_SHAMPOO;
break;
case FurniCategory.PET_CUSTOM_PART:
mode = PRODUCT_PAGE_CUSTOM_PART;
break;
case FurniCategory.PET_CUSTOM_PART_SHAMPOO:
mode = PRODUCT_PAGE_CUSTOM_PART_SHAMPOO;
break;
case FurniCategory.PET_SADDLE:
mode = PRODUCT_PAGE_SADDLE;
break;
case FurniCategory.MONSTERPLANT_REVIVAL:
mode = PRODUCT_PAGE_REVIVE;
break;
case FurniCategory.MONSTERPLANT_REBREED:
mode = PRODUCT_PAGE_REBREED;
break;
case FurniCategory.MONSTERPLANT_FERTILIZE:
mode = PRODUCT_PAGE_FERTILIZE;
break;
}
setMode(mode);
}, [ roomSession, item ]);
return (
<ContextMenuView category={ RoomObjectCategory.UNIT } collapsable={ true } objectId={ item.id } userType={ RoomObjectType.PET } onClose={ onClose }>
<ContextMenuHeaderView>
{ item.name }
</ContextMenuHeaderView>
{ (mode === PRODUCT_PAGE_UKNOWN) &&
<ContextMenuListItemView onClick={ event => processAction('use_product') }>
{ LocalizeText('infostand.button.useproduct') }
</ContextMenuListItemView> }
{ (mode === PRODUCT_PAGE_SHAMPOO) &&
<ContextMenuListItemView onClick={ event => processAction('use_product_shampoo') }>
{ LocalizeText('infostand.button.useproduct_shampoo') }
</ContextMenuListItemView> }
{ (mode === PRODUCT_PAGE_CUSTOM_PART) &&
<ContextMenuListItemView onClick={ event => processAction('use_product_custom_part') }>
{ LocalizeText('infostand.button.useproduct_custom_part') }
</ContextMenuListItemView> }
{ (mode === PRODUCT_PAGE_CUSTOM_PART_SHAMPOO) &&
<ContextMenuListItemView onClick={ event => processAction('use_product_custom_part_shampoo') }>
{ LocalizeText('infostand.button.useproduct_custom_part_shampoo') }
</ContextMenuListItemView> }
{ (mode === PRODUCT_PAGE_SADDLE) &&
<>
{ item.replace &&
<ContextMenuListItemView onClick={ event => processAction('replace_product_saddle') }>
{ LocalizeText('infostand.button.replaceproduct_saddle') }
</ContextMenuListItemView> }
{ !item.replace &&
<ContextMenuListItemView onClick={ event => processAction('use_product_saddle') }>
{ LocalizeText('infostand.button.useproduct_saddle') }
</ContextMenuListItemView> }
</> }
{ (mode === PRODUCT_PAGE_REVIVE) &&
<ContextMenuListItemView onClick={ event => processAction('revive_monsterplant') }>
{ LocalizeText('infostand.button.revive_monsterplant') }
</ContextMenuListItemView> }
{ (mode === PRODUCT_PAGE_REBREED) &&
<ContextMenuListItemView onClick={ event => processAction('rebreed_monsterplant') }>
{ LocalizeText('infostand.button.rebreed_monsterplant') }
</ContextMenuListItemView> }
{ (mode === PRODUCT_PAGE_FERTILIZE) &&
<ContextMenuListItemView onClick={ event => processAction('fertilize_monsterplant') }>
{ LocalizeText('infostand.button.fertilize_monsterplant') }
</ContextMenuListItemView> }
</ContextMenuView>
);
};
@@ -0,0 +1,139 @@
import { GetSessionDataManager, RoomEngineEvent, RoomEnterEffect, RoomSessionDanceEvent } from '@nitrots/nitro-renderer';
import { FC, useState } from 'react';
import { AvatarInfoFurni, AvatarInfoPet, AvatarInfoRentableBot, AvatarInfoUser, GetConfigurationValue, RoomWidgetUpdateRentableBotChatEvent } from '../../../../api';
import { Column } from '../../../../common';
import { useAvatarInfoWidget, useNitroEvent, useRoom, useUiEvent } from '../../../../hooks';
import { AvatarInfoPetTrainingPanelView } from './AvatarInfoPetTrainingPanelView';
import { AvatarInfoRentableBotChatView } from './AvatarInfoRentableBotChatView';
import { AvatarInfoUseProductConfirmView } from './AvatarInfoUseProductConfirmView';
import { AvatarInfoUseProductView } from './AvatarInfoUseProductView';
import { InfoStandWidgetBotView } from './infostand/InfoStandWidgetBotView';
import { InfoStandWidgetFurniView } from './infostand/InfoStandWidgetFurniView';
import { InfoStandWidgetPetView } from './infostand/InfoStandWidgetPetView';
import { InfoStandWidgetRentableBotView } from './infostand/InfoStandWidgetRentableBotView';
import { InfoStandWidgetUserView } from './infostand/InfoStandWidgetUserView';
import { AvatarInfoWidgetAvatarView } from './menu/AvatarInfoWidgetAvatarView';
import { AvatarInfoWidgetDecorateView } from './menu/AvatarInfoWidgetDecorateView';
import { AvatarInfoWidgetFurniView } from './menu/AvatarInfoWidgetFurniView';
import { AvatarInfoWidgetNameView } from './menu/AvatarInfoWidgetNameView';
import { AvatarInfoWidgetOwnAvatarView } from './menu/AvatarInfoWidgetOwnAvatarView';
import { AvatarInfoWidgetOwnPetView } from './menu/AvatarInfoWidgetOwnPetView';
import { AvatarInfoWidgetPetView } from './menu/AvatarInfoWidgetPetView';
import { AvatarInfoWidgetRentableBotView } from './menu/AvatarInfoWidgetRentableBotView';
export const AvatarInfoWidgetView: FC<{}> = props =>
{
const [ isGameMode, setGameMode ] = useState(false);
const [ isDancing, setIsDancing ] = useState(false);
const [ rentableBotChatEvent, setRentableBotChatEvent ] = useState<RoomWidgetUpdateRentableBotChatEvent>(null);
const { avatarInfo = null, setAvatarInfo = null, activeNameBubble = null, setActiveNameBubble = null, nameBubbles = [], removeNameBubble = null, productBubbles = [], confirmingProduct = null, updateConfirmingProduct = null, removeProductBubble = null, isDecorating = false, setIsDecorating = null } = useAvatarInfoWidget();
const { roomSession = null } = useRoom();
useNitroEvent<RoomEngineEvent>(RoomEngineEvent.NORMAL_MODE, event =>
{
if(isGameMode) setGameMode(false);
});
useNitroEvent<RoomEngineEvent>(RoomEngineEvent.GAME_MODE, event =>
{
if(!isGameMode) setGameMode(true);
});
useNitroEvent<RoomSessionDanceEvent>(RoomSessionDanceEvent.RSDE_DANCE, event =>
{
if(event.roomIndex !== roomSession.ownRoomIndex) return;
setIsDancing((event.danceId !== 0));
});
useUiEvent<RoomWidgetUpdateRentableBotChatEvent>(RoomWidgetUpdateRentableBotChatEvent.UPDATE_CHAT, event => setRentableBotChatEvent(event));
const getMenuView = () =>
{
if(!roomSession || isGameMode) return null;
if(activeNameBubble) return <AvatarInfoWidgetNameView nameInfo={ activeNameBubble } onClose={ () => setActiveNameBubble(null) } />;
if(avatarInfo)
{
switch(avatarInfo.type)
{
case AvatarInfoFurni.FURNI: {
const info = (avatarInfo as AvatarInfoFurni);
if(!isDecorating) return null;
return <AvatarInfoWidgetFurniView avatarInfo={ info } onClose={ () => setAvatarInfo(null) } />;
}
case AvatarInfoUser.OWN_USER:
case AvatarInfoUser.PEER: {
const info = (avatarInfo as AvatarInfoUser);
if(GetConfigurationValue('user.tags.enabled')) GetSessionDataManager().getUserTags(info.roomIndex);
if(info.isSpectatorMode) return null;
if(info.isOwnUser)
{
if(RoomEnterEffect.isRunning()) return null;
return <AvatarInfoWidgetOwnAvatarView avatarInfo={ info } isDancing={ isDancing } setIsDecorating={ setIsDecorating } onClose={ () => setAvatarInfo(null) } />;
}
return <AvatarInfoWidgetAvatarView avatarInfo={ info } onClose={ () => setAvatarInfo(null) } />;
}
case AvatarInfoPet.PET_INFO: {
const info = (avatarInfo as AvatarInfoPet);
if(info.isOwner) return <AvatarInfoWidgetOwnPetView avatarInfo={ info } onClose={ () => setAvatarInfo(null) } />;
return <AvatarInfoWidgetPetView avatarInfo={ info } onClose={ () => setAvatarInfo(null) } />;
}
case AvatarInfoRentableBot.RENTABLE_BOT: {
return <AvatarInfoWidgetRentableBotView avatarInfo={ (avatarInfo as AvatarInfoRentableBot) } onClose={ () => setAvatarInfo(null) } />;
}
}
}
return null;
};
const getInfostandView = () =>
{
if(!avatarInfo) return null;
switch(avatarInfo.type)
{
case AvatarInfoFurni.FURNI:
return <InfoStandWidgetFurniView avatarInfo={ (avatarInfo as AvatarInfoFurni) } onClose={ () => setAvatarInfo(null) } />;
case AvatarInfoUser.OWN_USER:
case AvatarInfoUser.PEER:
return <InfoStandWidgetUserView avatarInfo={ (avatarInfo as AvatarInfoUser) } setAvatarInfo={ setAvatarInfo } onClose={ () => setAvatarInfo(null) } />;
case AvatarInfoUser.BOT:
return <InfoStandWidgetBotView avatarInfo={ (avatarInfo as AvatarInfoUser) } onClose={ () => setAvatarInfo(null) } />;
case AvatarInfoRentableBot.RENTABLE_BOT:
return <InfoStandWidgetRentableBotView avatarInfo={ (avatarInfo as AvatarInfoRentableBot) } onClose={ () => setAvatarInfo(null) } />;
case AvatarInfoPet.PET_INFO:
return <InfoStandWidgetPetView avatarInfo={ (avatarInfo as AvatarInfoPet) } onClose={ () => setAvatarInfo(null) } />;
}
};
return (
<>
{ isDecorating &&
<AvatarInfoWidgetDecorateView roomIndex={ roomSession.ownRoomIndex } setIsDecorating={ setIsDecorating } userId={ GetSessionDataManager().userId } userName={ GetSessionDataManager().userName } /> }
{ getMenuView() }
{ avatarInfo &&
<Column alignItems="end" className="absolute right-[10px] bottom-[65px] pointer-events-none z-30 text-white">
{ getInfostandView() }
</Column> }
{ (nameBubbles.length > 0) && nameBubbles.map((name, index) => <AvatarInfoWidgetNameView key={ index } nameInfo={ name } onClose={ () => removeNameBubble(index) } />) }
{ (productBubbles.length > 0) && productBubbles.map((item, index) =>
{
return <AvatarInfoUseProductView key={ item.id } item={ item } updateConfirmingProduct={ updateConfirmingProduct } onClose={ () => removeProductBubble(index) } />;
}) }
{ rentableBotChatEvent && <AvatarInfoRentableBotChatView chatEvent={ rentableBotChatEvent } onClose={ () => setRentableBotChatEvent(null) } /> }
{ confirmingProduct && <AvatarInfoUseProductConfirmView item={ confirmingProduct } onClose={ () => updateConfirmingProduct(null) } /> }
<AvatarInfoPetTrainingPanelView />
</>
);
};
@@ -0,0 +1,55 @@
import { FC } from 'react';
import { FaTimes } from 'react-icons/fa';
import { AvatarInfoUser, LocalizeText } from '../../../../../api';
import { Column, Flex, LayoutAvatarImageView, LayoutBadgeImageView, Text } from '../../../../../common';
interface InfoStandWidgetBotViewProps
{
avatarInfo: AvatarInfoUser;
onClose: () => void;
}
export const InfoStandWidgetBotView: FC<InfoStandWidgetBotViewProps> = props =>
{
const { avatarInfo = null, onClose = null } = props;
if(!avatarInfo) return null;
return (
<Column className="nitro-infostand rounded">
<Column className="container-fluid content-area" gap={ 1 } overflow="visible">
<div className="flex flex-col gap-1">
<Flex alignItems="center" gap={ 1 } justifyContent="between">
<Text small wrap variant="white">{ avatarInfo.name }</Text>
<FaTimes className="cursor-pointer fa-icon" onClick={ onClose } />
</Flex>
<hr className="m-0" />
</div>
<div className="flex flex-col gap-1">
<div className="flex gap-1">
<Column fullWidth className="body-image bot">
<LayoutAvatarImageView direction={ 4 } figure={ avatarInfo.figure } />
</Column>
<Column center grow gap={ 0 }>
{ (avatarInfo.badges.length > 0) && avatarInfo.badges.map(result =>
{
return <LayoutBadgeImageView key={ result } badgeCode={ result } showInfo={ true } />;
}) }
</Column>
</div>
<hr className="m-0" />
</div>
<Flex alignItems="center" className="bg-light-dark rounded py-1 px-2">
<Text fullWidth small textBreak wrap className="min-h-[18px]" variant="white">{ avatarInfo.motto }</Text>
</Flex>
{ (avatarInfo.carryItem > 0) &&
<div className="flex flex-col gap-1">
<hr className="m-0" />
<Text small wrap variant="white">
{ LocalizeText('infostand.text.handitem', [ 'item' ], [ LocalizeText('handitem' + avatarInfo.carryItem) ]) }
</Text>
</div> }
</Column>
</Column>
);
};
@@ -0,0 +1,475 @@
import { CrackableDataType, CreateLinkEvent, GetRoomEngine, GetSoundManager, GroupInformationComposer, GroupInformationEvent, NowPlayingEvent, RoomControllerLevel, RoomObjectCategory, RoomObjectOperationType, RoomObjectVariable, RoomWidgetEnumItemExtradataParameter, RoomWidgetFurniInfoUsagePolicyEnum, SetObjectDataMessageComposer, SongInfoReceivedEvent, StringDataType } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react';
import { FaTimes } from 'react-icons/fa';
import { AvatarInfoFurni, GetGroupInformation, LocalizeText, SendMessageComposer } from '../../../../../api';
import { Button, Column, Flex, LayoutBadgeImageView, LayoutLimitedEditionCompactPlateView, LayoutRarityLevelView, LayoutRoomObjectImageView, Text, UserProfileIconView } from '../../../../../common';
import { useMessageEvent, useNitroEvent, useRoom } from '../../../../../hooks';
import { NitroInput } from '../../../../../layout';
interface InfoStandWidgetFurniViewProps
{
avatarInfo: AvatarInfoFurni;
onClose: () => void;
}
const PICKUP_MODE_NONE: number = 0;
const PICKUP_MODE_EJECT: number = 1;
const PICKUP_MODE_FULL: number = 2;
export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props =>
{
const { avatarInfo = null, onClose = null } = props;
const { roomSession = null } = useRoom();
const [ pickupMode, setPickupMode ] = useState(0);
const [ canMove, setCanMove ] = useState(false);
const [ canRotate, setCanRotate ] = useState(false);
const [ canUse, setCanUse ] = useState(false);
const [ furniKeys, setFurniKeys ] = useState<string[]>([]);
const [ furniValues, setFurniValues ] = useState<string[]>([]);
const [ customKeys, setCustomKeys ] = useState<string[]>([]);
const [ customValues, setCustomValues ] = useState<string[]>([]);
const [ isCrackable, setIsCrackable ] = useState(false);
const [ crackableHits, setCrackableHits ] = useState(0);
const [ crackableTarget, setCrackableTarget ] = useState(0);
const [ godMode, setGodMode ] = useState(false);
const [ canSeeFurniId, setCanSeeFurniId ] = useState(false);
const [ groupName, setGroupName ] = useState<string>(null);
const [ isJukeBox, setIsJukeBox ] = useState<boolean>(false);
const [ isSongDisk, setIsSongDisk ] = useState<boolean>(false);
const [ songId, setSongId ] = useState<number>(-1);
const [ songName, setSongName ] = useState<string>('');
const [ songCreator, setSongCreator ] = useState<string>('');
useNitroEvent<NowPlayingEvent>(NowPlayingEvent.NPE_SONG_CHANGED, event =>
{
setSongId(event.id);
}, (isJukeBox || isSongDisk));
useNitroEvent<NowPlayingEvent>(SongInfoReceivedEvent.SIR_TRAX_SONG_INFO_RECEIVED, event =>
{
if(event.id !== songId) return;
const songInfo = GetSoundManager().musicController.getSongInfo(event.id);
if(!songInfo) return;
setSongName(songInfo.name);
setSongCreator(songInfo.creator);
}, (isJukeBox || isSongDisk));
useEffect(() =>
{
let pickupMode = PICKUP_MODE_NONE;
let canMove = false;
let canRotate = false;
let canUse = false;
let furniKeyss: string[] = [];
let furniValuess: string[] = [];
let customKeyss: string[] = [];
let customValuess: string[] = [];
let isCrackable = false;
let crackableHits = 0;
let crackableTarget = 0;
let godMode = false;
let canSeeFurniId = false;
let furniIsJukebox = false;
let furniIsSongDisk = false;
let furniSongId = -1;
const isValidController = (avatarInfo.roomControllerLevel >= RoomControllerLevel.GUEST);
if(isValidController || avatarInfo.isOwner || avatarInfo.isRoomOwner || avatarInfo.isAnyRoomController)
{
canMove = true;
canRotate = !avatarInfo.isWallItem;
if(avatarInfo.roomControllerLevel >= RoomControllerLevel.MODERATOR) godMode = true;
}
if(avatarInfo.isAnyRoomController)
{
canSeeFurniId = true;
}
if((((avatarInfo.usagePolicy === RoomWidgetFurniInfoUsagePolicyEnum.EVERYBODY) || ((avatarInfo.usagePolicy === RoomWidgetFurniInfoUsagePolicyEnum.CONTROLLER) && isValidController)) || ((avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.JUKEBOX) && isValidController)) || ((avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.USABLE_PRODUCT) && isValidController)) canUse = true;
if(avatarInfo.extraParam)
{
if(avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.CRACKABLE_FURNI)
{
const stuffData = (avatarInfo.stuffData as CrackableDataType);
canUse = true;
isCrackable = true;
crackableHits = stuffData.hits;
crackableTarget = stuffData.target;
}
else if(avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.JUKEBOX)
{
const playlist = GetSoundManager().musicController.getRoomItemPlaylist();
if(playlist)
{
furniSongId = playlist.currentSongId;
}
furniIsJukebox = true;
}
else if(avatarInfo.extraParam.indexOf(RoomWidgetEnumItemExtradataParameter.SONGDISK) === 0)
{
furniSongId = parseInt(avatarInfo.extraParam.substr(RoomWidgetEnumItemExtradataParameter.SONGDISK.length));
furniIsSongDisk = true;
}
if(godMode)
{
const extraParam = avatarInfo.extraParam.substr(RoomWidgetEnumItemExtradataParameter.BRANDING_OPTIONS.length);
if(extraParam)
{
const parts = extraParam.split('\t');
for(const part of parts)
{
const value = part.split('=');
if(value && (value.length === 2))
{
furniKeyss.push(value[0]);
furniValuess.push(value[1]);
}
}
}
}
}
if(godMode)
{
const roomObject = GetRoomEngine().getRoomObject(roomSession.roomId, avatarInfo.id, (avatarInfo.isWallItem) ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR);
if(roomObject)
{
const customVariables = roomObject.model.getValue<string[]>(RoomObjectVariable.FURNITURE_CUSTOM_VARIABLES);
const furnitureData = roomObject.model.getValue<{ [index: string]: string }>(RoomObjectVariable.FURNITURE_DATA);
if(customVariables && customVariables.length)
{
for(const customVariable of customVariables)
{
customKeyss.push(customVariable);
customValuess.push((furnitureData[customVariable]) || '');
}
}
}
}
if(avatarInfo.isOwner || avatarInfo.isAnyRoomController) pickupMode = PICKUP_MODE_FULL;
else if(avatarInfo.isRoomOwner || (avatarInfo.roomControllerLevel >= RoomControllerLevel.GUILD_ADMIN)) pickupMode = PICKUP_MODE_EJECT;
if(avatarInfo.isStickie) pickupMode = PICKUP_MODE_NONE;
setPickupMode(pickupMode);
setCanMove(canMove);
setCanRotate(canRotate);
setCanUse(canUse);
setFurniKeys(furniKeyss);
setFurniValues(furniValuess);
setCustomKeys(customKeyss);
setCustomValues(customValuess);
setIsCrackable(isCrackable);
setCrackableHits(crackableHits);
setCrackableTarget(crackableTarget);
setGodMode(godMode);
setCanSeeFurniId(canSeeFurniId);
setGroupName(null);
setIsJukeBox(furniIsJukebox);
setIsSongDisk(furniIsSongDisk);
setSongId(furniSongId);
if(avatarInfo.groupId) SendMessageComposer(new GroupInformationComposer(avatarInfo.groupId, false));
}, [ roomSession, avatarInfo ]);
useMessageEvent<GroupInformationEvent>(GroupInformationEvent, event =>
{
const parser = event.getParser();
if(!avatarInfo || avatarInfo.groupId !== parser.id || parser.flag) return;
if(groupName) setGroupName(null);
setGroupName(parser.title);
});
useEffect(() =>
{
const songInfo = GetSoundManager().musicController.getSongInfo(songId);
setSongName(songInfo?.name ?? '');
setSongCreator(songInfo?.creator ?? '');
}, [ songId ]);
const onFurniSettingChange = useCallback((index: number, value: string) =>
{
const clone = Array.from(furniValues);
clone[index] = value;
setFurniValues(clone);
}, [ furniValues ]);
const onCustomVariableChange = useCallback((index: number, value: string) =>
{
const clone = Array.from(customValues);
clone[index] = value;
setCustomValues(clone);
}, [ customValues ]);
const getFurniSettingsAsString = useCallback(() =>
{
if(furniKeys.length === 0 || furniValues.length === 0) return '';
let data = '';
let i = 0;
while(i < furniKeys.length)
{
const key = furniKeys[i];
const value = furniValues[i];
data = (data + (key + '=' + value + '\t'));
i++;
}
return data;
}, [ furniKeys, furniValues ]);
const processButtonAction = useCallback((action: string) =>
{
if(!action || (action === '')) return;
let objectData: string = null;
switch(action)
{
case 'buy_one':
CreateLinkEvent(`catalog/open/offerId/${ avatarInfo.purchaseOfferId }`);
return;
case 'move':
GetRoomEngine().processRoomObjectOperation(avatarInfo.id, avatarInfo.category, RoomObjectOperationType.OBJECT_MOVE);
break;
case 'rotate':
GetRoomEngine().processRoomObjectOperation(avatarInfo.id, avatarInfo.category, RoomObjectOperationType.OBJECT_ROTATE_POSITIVE);
break;
case 'pickup':
if(pickupMode === PICKUP_MODE_FULL)
{
GetRoomEngine().processRoomObjectOperation(avatarInfo.id, avatarInfo.category, RoomObjectOperationType.OBJECT_PICKUP);
}
else
{
GetRoomEngine().processRoomObjectOperation(avatarInfo.id, avatarInfo.category, RoomObjectOperationType.OBJECT_EJECT);
}
break;
case 'use':
GetRoomEngine().useRoomObject(avatarInfo.id, avatarInfo.category);
break;
case 'save_branding_configuration': {
const mapData = new Map<string, string>();
const dataParts = getFurniSettingsAsString().split('\t');
if(dataParts)
{
for(const part of dataParts)
{
const [ key, value ] = part.split('=', 2);
mapData.set(key, value);
}
}
GetRoomEngine().modifyRoomObjectDataWithMap(avatarInfo.id, avatarInfo.category, RoomObjectOperationType.OBJECT_SAVE_STUFF_DATA, mapData);
break;
}
case 'save_custom_variables': {
const map = new Map();
for(let i = 0; i < customKeys.length; i++)
{
const key = customKeys[i];
const value = customValues[i];
if((key && key.length) && (value && value.length)) map.set(key, value);
}
SendMessageComposer(new SetObjectDataMessageComposer(avatarInfo.id, map));
break;
}
}
}, [ avatarInfo, pickupMode, customKeys, customValues, getFurniSettingsAsString ]);
const getGroupBadgeCode = useCallback(() =>
{
const stringDataType = (avatarInfo.stuffData as StringDataType);
if(!stringDataType || !(stringDataType instanceof StringDataType)) return null;
return stringDataType.getValue(2);
}, [ avatarInfo ]);
if(!avatarInfo) return null;
return (
<Column alignItems="end" gap={ 1 }>
<Column className="relative min-w-[190px] max-w-[190px] z-30 pointer-events-auto bg-[rgba(28,_28,_32,_.95)] [box-shadow:inset_0_5px_#22222799,_inset_0_-4px_#12121599] rounded">
<Column className="h-full p-[8px] overflow-auto" gap={ 1 } overflow="visible">
<div className="flex flex-col gap-1">
<Flex alignItems="center" gap={ 1 } justifyContent="between">
<Text small wrap variant="white">{ avatarInfo.name }</Text>
<FaTimes className="cursor-pointer fa-icon" onClick={ onClose } />
</Flex>
<hr className="m-0 bg-[#0003] border-[0] opacity-[.5] h-px" />
</div>
<div className="flex flex-col gap-1">
<Flex gap={ 1 } position="relative">
{ avatarInfo.stuffData.isUnique &&
<div className="absolute end-0">
<LayoutLimitedEditionCompactPlateView uniqueNumber={ avatarInfo.stuffData.uniqueNumber } uniqueSeries={ avatarInfo.stuffData.uniqueSeries } />
</div> }
{ (avatarInfo.stuffData.rarityLevel > -1) &&
<div className="absolute end-0">
<LayoutRarityLevelView level={ avatarInfo.stuffData.rarityLevel } />
</div> }
<Flex center fullWidth>
<LayoutRoomObjectImageView category={ avatarInfo.category } objectId={ avatarInfo.id } roomId={ roomSession.roomId } />
</Flex>
</Flex>
<hr className="m-0 bg-[#0003] border-[0] opacity-[.5] h-px" />
</div>
<div className="flex flex-col gap-1">
<Text fullWidth small textBreak wrap variant="white">{ avatarInfo.description }</Text>
<hr className="m-0 bg-[#0003] border-[0] opacity-[.5] h-px" />
</div>
<div className="flex flex-col gap-1">
<div className="flex items-center gap-1">
<UserProfileIconView userId={ avatarInfo.ownerId } />
<Text small wrap variant="white">
{ LocalizeText('furni.owner', [ 'name' ], [ avatarInfo.ownerName ]) }
</Text>
</div>
{ (avatarInfo.purchaseOfferId > 0) &&
<Flex>
<Text pointer small underline variant="white" onClick={ event => processButtonAction('buy_one') }>
{ LocalizeText('infostand.button.buy') }
</Text>
</Flex> }
</div>
{ (isJukeBox || isSongDisk) &&
<div className="flex flex-col gap-1">
<hr className="m-0 bg-[#0003] border-[0] opacity-[.5] h-px" />
{ (songId === -1) &&
<Text small wrap variant="white">
{ LocalizeText('infostand.jukebox.text.not.playing') }
</Text> }
{ !!songName.length &&
<div className="flex items-center gap-1">
<div className="icon disk-icon" />
<Text small wrap variant="white">
{ songName }
</Text>
</div> }
{ !!songCreator.length &&
<div className="flex items-center gap-1">
<div className="icon disk-creator" />
<Text small wrap variant="white">
{ songCreator }
</Text>
</div> }
</div> }
<div className="flex flex-col gap-1">
{ isCrackable &&
<>
<hr className="m-0 bg-[#0003] border-[0] opacity-[.5] h-px" />
<Text small wrap variant="white">{ LocalizeText('infostand.crackable_furni.hits_remaining', [ 'hits', 'target' ], [ crackableHits.toString(), crackableTarget.toString() ]) }</Text>
</> }
{ avatarInfo.groupId > 0 &&
<>
<hr className="m-0 bg-[#0003] border-[0] opacity-[.5] h-px" />
<Flex pointer alignItems="center" gap={ 2 } onClick={ () => GetGroupInformation(avatarInfo.groupId) }>
<LayoutBadgeImageView badgeCode={ getGroupBadgeCode() } isGroup={ true } />
<Text underline variant="white">{ groupName }</Text>
</Flex>
</> }
{ godMode &&
<>
<hr className="m-0 bg-[#0003] border-[0] opacity-[.5] h-px" />
{ canSeeFurniId && <Text small wrap variant="white">ID: { avatarInfo.id }</Text> }
{ (furniKeys.length > 0) &&
<>
<hr className="m-0 bg-[#0003] border-[0] opacity-[.5] h-px" />
<div className="flex flex-col gap-1">
{ furniKeys.map((key, index) =>
{
return (
<Flex key={ index } alignItems="center" gap={ 1 }>
<Text small wrap align="end" className="col-span-4" variant="white">{ key }</Text>
<NitroInput type="text" value={ furniValues[index] } onChange={ event => onFurniSettingChange(index, event.target.value) } />
</Flex>);
}) }
</div>
</> }
</> }
{ (customKeys.length > 0) &&
<>
<hr className="m-0 bg-[#0003] border-[0] opacity-[.5] h-px" />
<div className="flex flex-col gap-1">
{ customKeys.map((key, index) =>
{
return (
<Flex key={ index } alignItems="center" gap={ 1 }>
<Text small wrap align="end" className="col-span-4" variant="white">{ key }</Text>
<NitroInput type="text" value={ customValues[index] } onChange={ event => onCustomVariableChange(index, event.target.value) } />
</Flex>);
}) }
</div>
</> }
</div>
</Column>
</Column>
<Flex gap={ 1 } justifyContent="end">
{ canMove &&
<Button variant="dark" onClick={ event => processButtonAction('move') }>
{ LocalizeText('infostand.button.move') }
</Button> }
{ canRotate &&
<Button variant="dark" onClick={ event => processButtonAction('rotate') }>
{ LocalizeText('infostand.button.rotate') }
</Button> }
{ (pickupMode !== PICKUP_MODE_NONE) &&
<Button variant="dark" onClick={ event => processButtonAction('pickup') }>
{ LocalizeText((pickupMode === PICKUP_MODE_EJECT) ? 'infostand.button.eject' : 'infostand.button.pickup') }
</Button> }
{ canUse &&
<Button variant="dark" onClick={ event => processButtonAction('use') }>
{ LocalizeText('infostand.button.use') }
</Button> }
{ ((furniKeys.length > 0 && furniValues.length > 0) && (furniKeys.length === furniValues.length)) &&
<Button variant="dark" onClick={ () => processButtonAction('save_branding_configuration') }>
{ LocalizeText('save') }
</Button> }
{ ((customKeys.length > 0 && customValues.length > 0) && (customKeys.length === customValues.length)) &&
<Button variant="dark" onClick={ () => processButtonAction('save_custom_variables') }>
{ LocalizeText('save') }
</Button> }
</Flex>
</Column>
);
};
@@ -0,0 +1,343 @@
import { CreateLinkEvent, PetRespectComposer, PetType } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState, useCallback } from 'react';
import { FaTimes } from 'react-icons/fa';
import { AvatarInfoPet, ConvertSeconds, GetConfigurationValue, LocalizeText, SendMessageComposer } from '../../../../../api';
import { Button, Column, Flex, LayoutCounterTimeView, LayoutPetImageView, LayoutRarityLevelView, Text, UserProfileIconView } from '../../../../../common';
import { useRoom, useSessionInfo } from '../../../../../hooks';
// TypeScript interface for AvatarInfoPet
interface AvatarInfoPet {
id: number;
name: string;
petType: number;
petBreed: number;
petFigure: string;
posture: string;
level: number;
maximumLevel: number;
age: number;
ownerId: number;
ownerName: string;
respect: number;
dead?: boolean;
energy?: number;
maximumEnergy?: number;
happyness?: number;
maximumHappyness?: number;
experience?: number;
levelExperienceGoal?: number;
remainingGrowTime?: number;
remainingTimeToLive?: number;
maximumTimeToLive?: number;
rarityLevel?: number;
isOwner?: boolean;
}
interface InfoStandWidgetPetViewProps {
avatarInfo: AvatarInfoPet;
onClose: () => void;
}
const PetHeader: FC<{ name: string; petType: number; petBreed: number; onClose: () => void }> = ({ name, petType, petBreed, onClose }) => (
<div className="flex flex-col gap-1">
<Flex alignItems="center" gap={1} justifyContent="between">
<Text small wrap variant="white">
{name}
</Text>
<FaTimes
className="cursor-pointer fa-icon"
onClick={onClose}
aria-label={LocalizeText('generic.close')}
title={LocalizeText('generic.close')}
/>
</Flex>
<Text small wrap variant="white">
{LocalizeText(`pet.breed.${petType}.${petBreed}`)}
</Text>
<hr className="m-0" />
</div>
);
const MonsterplantStats: FC<{
avatarInfo: AvatarInfoPet;
remainingGrowTime: number;
remainingTimeToLive: number;
}> = ({ avatarInfo, remainingGrowTime, remainingTimeToLive }) => (
<>
<Column center gap={1}>
<LayoutPetImageView direction={4} figure={avatarInfo.petFigure} posture={avatarInfo.posture} />
<hr className="m-0" />
</Column>
<div className="flex flex-col gap-2">
{!avatarInfo.dead && (
<Column alignItems="center" gap={1}>
<Text center small wrap variant="white">
{LocalizeText('pet.level', ['level', 'maxlevel'], [avatarInfo.level.toString(), avatarInfo.maximumLevel.toString()])}
</Text>
</Column>
)}
<Column alignItems="center" gap={1}>
<Text small truncate variant="white">
{LocalizeText('infostand.pet.text.wellbeing')}
</Text>
<div className="bg-light-dark rounded relative overflow-hidden w-full">
<div className="flex justify-center items-center size-full absolute">
<Text small variant="white">
{avatarInfo.dead || remainingTimeToLive <= 0
? '00:00:00'
: `${ConvertSeconds(remainingTimeToLive).split(':')[1]}:${ConvertSeconds(remainingTimeToLive).split(':')[2]}:${ConvertSeconds(remainingTimeToLive).split(':')[3]}`}
</Text>
</div>
<div
className="bg-success rounded pet-stats"
style={{
width: avatarInfo.dead || remainingTimeToLive <= 0 ? '0' : `${(remainingTimeToLive / avatarInfo.maximumTimeToLive) * 100}%`,
}}
/>
</div>
</Column>
{remainingGrowTime > 0 && (
<Column alignItems="center" gap={1}>
<Text small truncate variant="white">
{LocalizeText('infostand.pet.text.growth')}
</Text>
<LayoutCounterTimeView
className="top-2 end-2"
day={ConvertSeconds(remainingGrowTime).split(':')[0]}
hour={ConvertSeconds(remainingGrowTime).split(':')[1]}
minutes={ConvertSeconds(remainingGrowTime).split(':')[2]}
seconds={ConvertSeconds(remainingGrowTime).split(':')[3]}
/>
</Column>
)}
<Column alignItems="center" gap={1}>
<Text small truncate variant="white">
{LocalizeText('infostand.pet.text.raritylevel', ['level'], [LocalizeText(`infostand.pet.raritylevel.${avatarInfo.rarityLevel}`)])}
</Text>
<LayoutRarityLevelView className="top-2 end-2" level={avatarInfo.rarityLevel} />
</Column>
<hr className="m-0" />
</div>
<div className="flex flex-col gap-1">
<Text small wrap variant="white">
{LocalizeText('pet.age', ['age'], [avatarInfo.age.toString()])}
</Text>
<hr className="m-0" />
</div>
</>
);
// Sub-component: Regular Pet Stats
const RegularPetStats: FC<{ avatarInfo: AvatarInfoPet }> = ({ avatarInfo }) => (
<>
<div className="flex flex-col gap-1">
<div className="flex gap-1">
<Column fullWidth className="body-image pet p-1" overflow="hidden">
<LayoutPetImageView direction={4} figure={avatarInfo.petFigure} posture={avatarInfo.posture} />
</Column>
<Column grow gap={1}>
<Text center small wrap variant="white">
{LocalizeText('pet.level', ['level', 'maxlevel'], [avatarInfo.level.toString(), avatarInfo.maximumLevel.toString()])}
</Text>
<Column alignItems="center" gap={1}>
<Text small truncate variant="white">
{LocalizeText('infostand.pet.text.happiness')}
</Text>
<div className="bg-light-dark rounded relative overflow-hidden w-full">
<div className="flex justify-center items-center size-full absolute">
<Text small variant="white">
{avatarInfo.happyness + '/' + avatarInfo.maximumHappyness}
</Text>
</div>
<div
className="bg-info rounded pet-stats"
style={{ width: (avatarInfo.happyness / avatarInfo.maximumHappyness) * 100 + '%' }}
/>
</div>
</Column>
<Column alignItems="center" gap={1}>
<Text small truncate variant="white">
{LocalizeText('infostand.pet.text.experience')}
</Text>
<div className="bg-light-dark rounded relative overflow-hidden w-full">
<div className="flex justify-center items-center size-full absolute">
<Text small variant="white">
{avatarInfo.experience + '/' + avatarInfo.levelExperienceGoal}
</Text>
</div>
<div
className="bg-purple rounded pet-stats"
style={{ width: (avatarInfo.experience / avatarInfo.levelExperienceGoal) * 100 + '%' }}
/>
</div>
</Column>
<Column alignItems="center" gap={1}>
<Text small truncate variant="white">
{LocalizeText('infostand.pet.text.energy')}
</Text>
<div className="bg-light-dark rounded relative overflow-hidden w-full">
<div className="flex justify-center items-center size-full absolute">
<Text small variant="white">
{avatarInfo.energy + '/' + avatarInfo.maximumEnergy}
</Text>
</div>
<div
className="bg-success rounded pet-stats"
style={{ width: (avatarInfo.energy / avatarInfo.maximumEnergy) * 100 + '%' }}
/>
</div>
</Column>
</Column>
</div>
<hr className="m-0" />
</div>
<div className="flex flex-col gap-1">
<Text small wrap variant="white">
{LocalizeText('infostand.text.petrespect', ['count'], [avatarInfo.respect.toString()])}
</Text>
<Text small wrap variant="white">
{LocalizeText('pet.age', ['age'], [avatarInfo.age.toString()])}
</Text>
<hr className="m-0" />
</div>
</>
);
export const InfoStandWidgetPetView: FC<InfoStandWidgetPetViewProps> = ({ avatarInfo, onClose }) => {
const [remainingGrowTime, setRemainingGrowTime] = useState(0);
const [remainingTimeToLive, setRemainingTimeToLive] = useState(0);
const { roomSession = null } = useRoom();
const { petRespectRemaining = 0, respectPet = null } = useSessionInfo();
useEffect(() => {
setRemainingGrowTime(avatarInfo.remainingGrowTime || 0);
setRemainingTimeToLive(avatarInfo.remainingTimeToLive || 0);
}, [avatarInfo]);
useEffect(() => {
if (avatarInfo.petType !== PetType.MONSTERPLANT || avatarInfo.dead) return;
const interval = setInterval(() => {
setRemainingGrowTime((prev) => (prev <= 0 ? 0 : prev - 1));
setRemainingTimeToLive((prev) => (prev <= 0 ? 0 : prev - 1));
}, 1000);
return () => clearInterval(interval);
}, [avatarInfo]);
const processButtonAction = useCallback(
async (action: string) => {
try {
let hideMenu = true;
if (!action) return;
switch (action) {
case 'respect':
await respectPet(avatarInfo.id);
if (petRespectRemaining - 1 >= 1) hideMenu = false;
break;
case 'buyfood':
CreateLinkEvent('catalog/open/' + GetConfigurationValue('catalog.links')['pets.buy_food']);
break;
case 'train':
roomSession?.requestPetCommands(avatarInfo.id);
break;
case 'treat':
SendMessageComposer(new PetRespectComposer(avatarInfo.id));
break;
case 'compost':
roomSession?.compostPlant(avatarInfo.id);
break;
case 'pick_up':
roomSession?.pickupPet(avatarInfo.id);
break;
}
if (hideMenu) onClose();
} catch (error) {
console.error(`Failed to process action ${action}:`, error);
}
},
[avatarInfo, petRespectRemaining, respectPet, roomSession, onClose]
);
const buttons = [
{
action: 'buyfood',
label: LocalizeText('infostand.button.buyfood'),
condition: avatarInfo.petType !== PetType.MONSTERPLANT,
},
{
action: 'train',
label: LocalizeText('infostand.button.train'),
condition: avatarInfo.isOwner && avatarInfo.petType !== PetType.MONSTERPLANT,
},
{
action: 'treat',
label: LocalizeText('infostand.button.pettreat'),
condition:
!avatarInfo.dead &&
avatarInfo.petType === PetType.MONSTERPLANT &&
avatarInfo.energy / avatarInfo.maximumEnergy < 0.98,
},
{
action: 'compost',
label: LocalizeText('infostand.button.compost'),
condition: roomSession?.isRoomOwner && avatarInfo.petType === PetType.MONSTERPLANT,
},
{
action: 'pick_up',
label: LocalizeText('inventory.pets.pickup'),
condition: avatarInfo.isOwner,
},
{
action: 'respect',
label: LocalizeText('infostand.button.petrespect', ['count'], [petRespectRemaining.toString()]),
condition: petRespectRemaining > 0 && avatarInfo.petType !== PetType.MONSTERPLANT,
},
];
if (!avatarInfo) return <Text variant="white">{LocalizeText('generic.loading')}</Text>;
return (
<Column alignItems="end" gap={1}>
<Column className="nitro-infostand rounded">
<Column className="container-fluid content-area" gap={1} overflow="visible">
<PetHeader
name={avatarInfo.name}
petType={avatarInfo.petType}
petBreed={avatarInfo.petBreed}
onClose={onClose}
/>
{avatarInfo.petType === PetType.MONSTERPLANT ? (
<MonsterplantStats
avatarInfo={avatarInfo}
remainingGrowTime={remainingGrowTime}
remainingTimeToLive={remainingTimeToLive}
/>
) : (
<RegularPetStats avatarInfo={avatarInfo} />
)}
<div className="flex flex-col gap-1">
<div className="flex items-center gap-1">
<UserProfileIconView userId={avatarInfo.ownerId} />
<Text small wrap variant="white">
{LocalizeText('infostand.text.petowner', ['name'], [avatarInfo.ownerName])}
</Text>
</div>
</div>
</Column>
</Column>
<Flex gap={1} justifyContent="end">
{buttons.map(
(button) =>
button.condition && (
<Button key={button.action} variant="dark" onClick={() => processButtonAction(button.action)}>
{button.label}
</Button>
)
)}
</Flex>
</Column>
);
};
@@ -0,0 +1,84 @@
import { BotRemoveComposer } from '@nitrots/nitro-renderer';
import { FC, useMemo } from 'react';
import { FaTimes } from 'react-icons/fa';
import { AvatarInfoRentableBot, BotSkillsEnum, LocalizeText, SendMessageComposer } from '../../../../../api';
import { Button, Column, Flex, LayoutAvatarImageView, LayoutBadgeImageView, Text, UserProfileIconView } from '../../../../../common';
interface InfoStandWidgetRentableBotViewProps
{
avatarInfo: AvatarInfoRentableBot;
onClose: () => void;
}
export const InfoStandWidgetRentableBotView: FC<InfoStandWidgetRentableBotViewProps> = props =>
{
const { avatarInfo = null, onClose = null } = props;
const canPickup = useMemo(() =>
{
if(avatarInfo.botSkills.indexOf(BotSkillsEnum.NO_PICK_UP) >= 0) return false;
if(!avatarInfo.amIOwner && !avatarInfo.amIAnyRoomController) return false;
return true;
}, [ avatarInfo ]);
const pickupBot = () => SendMessageComposer(new BotRemoveComposer(avatarInfo.webID));
if(!avatarInfo) return;
return (
<div className="flex flex-col gap-1">
<div className="flex flex-col nitro-infostand rounded">
<div className="flex flex-col gap-1 overflow-visible container-fluid content-area">
<div className="flex flex-col gap-1">
<Flex alignItems="center" gap={ 1 } justifyContent="between">
<Text small wrap variant="white">{ avatarInfo.name }</Text>
<FaTimes className="cursor-pointer fa-icon" onClick={ onClose } />
</Flex>
<hr className="m-0" />
</div>
<div className="flex flex-col gap-1">
<div className="flex gap-1">
<Column fullWidth className="body-image bot">
<LayoutAvatarImageView direction={ 4 } figure={ avatarInfo.figure } />
</Column>
<Column center grow gap={ 0 }>
{ (avatarInfo.badges.length > 0) && avatarInfo.badges.map(result =>
{
return <LayoutBadgeImageView key={ result } badgeCode={ result } showInfo={ true } />;
}) }
</Column>
</div>
<hr className="m-0" />
</div>
<div className="flex flex-col gap-1">
<Flex alignItems="center" className="bg-light-dark rounded py-1 px-2">
<Text fullWidth small textBreak wrap className="min-h-[18px]" variant="white">{ avatarInfo.motto }</Text>
</Flex>
<hr className="m-0" />
</div>
<div className="flex flex-col gap-1">
<div className="flex items-center gap-1">
<UserProfileIconView userId={ avatarInfo.ownerId } />
<Text small wrap variant="white">
{ LocalizeText('infostand.text.botowner', [ 'name' ], [ avatarInfo.ownerName ]) }
</Text>
</div>
{ (avatarInfo.carryItem > 0) &&
<>
<hr className="m-0" />
<Text small wrap variant="white">
{ LocalizeText('infostand.text.handitem', [ 'item' ], [ LocalizeText('handitem' + avatarInfo.carryItem) ]) }
</Text>
</> }
</div>
</div>
</div>
{ canPickup &&
<div className="flex justify-end">
<Button variant="dark" onClick={ pickupBot }>{ LocalizeText('infostand.button.pickup') }</Button>
</div> }
</div>
);
};
@@ -0,0 +1,31 @@
import { RelationshipStatusEnum, RelationshipStatusInfo } from '@nitrots/nitro-renderer';
import { FC } from 'react';
import { GetUserProfile, LocalizeText } from '../../../../../api';
import { Flex, Text } from '../../../../../common';
interface InfoStandWidgetUserRelationshipsRelationshipItemViewProps
{
type: number;
relationship: RelationshipStatusInfo;
}
export const InfoStandWidgetUserRelationshipsRelationshipItemView: FC<InfoStandWidgetUserRelationshipsRelationshipItemViewProps> = props =>
{
const { type = -1, relationship = null } = props;
if(!relationship) return null;
const relationshipName = RelationshipStatusEnum.RELATIONSHIP_NAMES[type].toLocaleLowerCase();
return (
<div className="flex items-center gap-1">
<i className={ `nitro-friends-spritesheet icon-${ relationshipName }` } />
<Flex alignItems="center" gap={ 0 }>
<Text small variant="white" onClick={ event => GetUserProfile(relationship.randomFriendId) }>
<u>{ relationship.randomFriendName }</u>
{ (relationship.friendCount > 1) && (' ' + LocalizeText(`extendedprofile.relstatus.others.${ relationshipName }`, [ 'count' ], [ (relationship.friendCount - 1).toString() ])) }
</Text>
</Flex>
</div>
);
};
@@ -0,0 +1,23 @@
import { RelationshipStatusEnum, RelationshipStatusInfoMessageParser } from '@nitrots/nitro-renderer';
import { FC } from 'react';
import { InfoStandWidgetUserRelationshipsRelationshipItemView } from './InfoStandWidgetUserRelationshipItemView';
interface InfoStandWidgetUserRelationshipsViewProps
{
relationships: RelationshipStatusInfoMessageParser;
}
export const InfoStandWidgetUserRelationshipsView: FC<InfoStandWidgetUserRelationshipsViewProps> = props =>
{
const { relationships = null } = props;
if(!relationships || !relationships.relationshipStatusMap.length) return null;
return (
<>
<InfoStandWidgetUserRelationshipsRelationshipItemView relationship={ relationships.relationshipStatusMap.getValue(RelationshipStatusEnum.HEART) } type={ RelationshipStatusEnum.HEART } />
<InfoStandWidgetUserRelationshipsRelationshipItemView relationship={ relationships.relationshipStatusMap.getValue(RelationshipStatusEnum.SMILE) } type={ RelationshipStatusEnum.SMILE } />
<InfoStandWidgetUserRelationshipsRelationshipItemView relationship={ relationships.relationshipStatusMap.getValue(RelationshipStatusEnum.BOBBA) } type={ RelationshipStatusEnum.BOBBA } />
</>
);
};
@@ -0,0 +1,31 @@
import { CreateLinkEvent, NavigatorSearchComposer } from '@nitrots/nitro-renderer';
import { FC } from 'react';
import { SendMessageComposer } from '../../../../../api';
import { Flex, Text } from '../../../../../common';
interface InfoStandWidgetUserTagsViewProps
{
tags: string[];
}
const processAction = (tag: string) =>
{
CreateLinkEvent(`navigator/search/${ tag }`);
SendMessageComposer(new NavigatorSearchComposer('hotel_view', `tag:${ tag }`));
};
export const InfoStandWidgetUserTagsView: FC<InfoStandWidgetUserTagsViewProps> = props =>
{
const { tags = null } = props;
if(!tags || !tags.length) return null;
return (
<>
<hr className="m-0" />
<Flex className="flex-tags">
{ tags && (tags.length > 0) && tags.map((tag, index) => <Text key={ index } className="text-tags" variant="white" onClick={ event => processAction(tag) }>{ tag }</Text>) }
</Flex>
</>
);
};
@@ -0,0 +1,262 @@
import { GetSessionDataManager, RelationshipStatusInfoEvent, RelationshipStatusInfoMessageParser, RoomSessionFavoriteGroupUpdateEvent, RoomSessionUserBadgesEvent, RoomSessionUserFigureUpdateEvent, UserRelationshipsComposer } from '@nitrots/nitro-renderer';
import { Dispatch, FC, FocusEvent, KeyboardEvent, SetStateAction, useCallback, useEffect, useState } from 'react';
import { FaPencilAlt, FaTimes } from 'react-icons/fa';
import { AvatarInfoUser, CloneObject, GetConfigurationValue, GetGroupInformation, GetUserProfile, LocalizeText, SendMessageComposer } from '../../../../../api';
import { Base, Column, Flex, LayoutAvatarImageView, LayoutBadgeImageView, Text, UserProfileIconView } from '../../../../../common';
import { useMessageEvent, useNitroEvent, useRoom } from '../../../../../hooks';
import { InfoStandWidgetUserRelationshipsView } from './InfoStandWidgetUserRelationshipsView';
import { InfoStandWidgetUserTagsView } from './InfoStandWidgetUserTagsView';
import { BackgroundsView } from '../../../../backgrounds/BackgroundsView';
interface InfoStandWidgetUserViewProps {
avatarInfo: AvatarInfoUser;
setAvatarInfo: Dispatch<SetStateAction<AvatarInfoUser>>;
onClose: () => void;
}
export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props => {
const { avatarInfo = null, setAvatarInfo = null, onClose = null } = props;
const [motto, setMotto] = useState<string>(null);
const [isEditingMotto, setIsEditingMotto] = useState(false);
const [relationships, setRelationships] = useState<RelationshipStatusInfoMessageParser>(null);
const [backgroundId, setBackgroundId] = useState<number>(null);
const [standId, setStandId] = useState<number>(null);
const [overlayId, setOverlayId] = useState<number>(null);
const [isVisible, setIsVisible] = useState(false);
const { roomSession = null } = useRoom();
const infostandBackgroundClass = `background-${backgroundId ?? 'default'}`;
const infostandStandClass = `stand-${standId ?? 'default'}`;
const infostandOverlayClass = `overlay-${overlayId ?? 'default'}`;
const handleProfileClick = useCallback(() => { GetUserProfile(avatarInfo.webID); }, [avatarInfo.webID]);
const handleEditClick = useCallback((event: React.MouseEvent) => { event.stopPropagation(); setIsVisible(prev => !prev); }, []);
const saveMotto = (motto: string) => {
if (!isEditingMotto || motto.length > GetConfigurationValue<number>('motto.max.length', 38) || !roomSession) return;
roomSession.sendMottoMessage(motto);
setIsEditingMotto(false);
};
const onMottoBlur = (event: FocusEvent<HTMLInputElement>) => saveMotto(event.target.value);
const onMottoKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
event.stopPropagation();
switch (event.key) {
case 'Enter':
saveMotto((event.target as HTMLInputElement).value);
return;
}
};
useNitroEvent<RoomSessionUserBadgesEvent>(RoomSessionUserBadgesEvent.RSUBE_BADGES, event => {
if (!avatarInfo || avatarInfo.webID !== event.userId) return;
const oldBadges = avatarInfo.badges.join('');
if (oldBadges === event.badges.join('')) return;
setAvatarInfo(prevValue => {
const newValue = CloneObject(prevValue);
newValue.badges = event.badges;
return newValue;
});
});
useNitroEvent<RoomSessionUserFigureUpdateEvent>(RoomSessionUserFigureUpdateEvent.USER_FIGURE, event => {
if (!avatarInfo || avatarInfo.roomIndex !== event.roomIndex) return;
setAvatarInfo(prevValue => {
const newValue = CloneObject(prevValue);
newValue.figure = event.figure;
newValue.motto = event.customInfo;
newValue.achievementScore = event.activityPoints;
newValue.backgroundId = event.backgroundId;
newValue.standId = event.standId;
newValue.overlayId = event.overlayId;
return newValue;
});
});
useNitroEvent<RoomSessionFavoriteGroupUpdateEvent>(RoomSessionFavoriteGroupUpdateEvent.FAVOURITE_GROUP_UPDATE, event => {
if (!avatarInfo || avatarInfo.roomIndex !== event.roomIndex) return;
setAvatarInfo(prevValue => {
const newValue = CloneObject(prevValue);
const clearGroup = (event.status === -1) || (event.habboGroupId <= 0);
newValue.groupId = clearGroup ? -1 : event.habboGroupId;
newValue.groupName = clearGroup ? null : event.habboGroupName;
newValue.groupBadgeId = clearGroup ? null : GetSessionDataManager().getGroupBadge(event.habboGroupId);
return newValue;
});
});
useMessageEvent<RelationshipStatusInfoEvent>(RelationshipStatusInfoEvent, event => {
const parser = event.getParser();
if (!avatarInfo || avatarInfo.webID !== parser.userId) return;
setRelationships(parser);
});
useEffect(() => {
setIsEditingMotto(false);
setMotto(avatarInfo.motto);
setBackgroundId(avatarInfo.backgroundId);
setStandId(avatarInfo.standId);
setOverlayId(avatarInfo.overlayId);
SendMessageComposer(new UserRelationshipsComposer(avatarInfo.webID));
return () => {
setIsEditingMotto(false);
setMotto(null);
setRelationships(null);
setBackgroundId(null);
setStandId(null);
setOverlayId(null);
};
}, [avatarInfo]);
if (!avatarInfo) return null;
return (
<>
<Column className="relative min-w-[190px] max-w-[190px] z-30 pointer-events-auto bg-[rgba(28,28,32,0.95)] [box-shadow:inset_0_5px_#22222799,_inset_0_-4px_#12121599] rounded">
<Column className="h-full p-[8px] overflow-auto" gap={1} overflow="visible">
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between">
<div className="flex items-center gap-1">
<UserProfileIconView userId={avatarInfo.webID} />
<Text small wrap variant="white">{avatarInfo.name}</Text>
</div>
<FaTimes className="cursor-pointer fa-icon" onClick={onClose} />
</div>
<hr className="m-0 bg-[#0003] border-[0] opacity-[0.5] h-px" />
</div>
<div className="flex flex-col gap-1">
<div className="flex gap-1">
<Column
fullWidth
className={`flex items-center w-full max-w-[68px] rounded-[0.25rem] profile-background ${infostandBackgroundClass}`}
onClick={handleProfileClick}
>
<Base position="absolute" className={`profile-stand ${infostandStandClass}`} />
<LayoutAvatarImageView direction={2} figure={avatarInfo.figure} />
<Base position="absolute" className={`profile-overlay ${infostandOverlayClass}`} />
{avatarInfo.type === AvatarInfoUser.OWN_USER && (
<Base
className="background-edit-icon background-edit-position"
style={{ pointerEvents: 'auto', cursor: 'pointer' }}
onClick={handleEditClick}
aria-label="Edit profile background"
/>
)}
</Column>
<Column grow alignItems="center" gap={0}>
<div className="flex gap-1">
<div className="flex items-center justify-center relative w-[40px] h-[40px] bg-no-repeat bg-center">
{avatarInfo.badges[0] && <LayoutBadgeImageView badgeCode={avatarInfo.badges[0]} showInfo={true} />}
</div>
<Flex center className="relative w-[40px] h-[40px] bg-no-repeat bg-center" pointer={avatarInfo.groupId > 0} onClick={event => GetGroupInformation(avatarInfo.groupId)}>
{avatarInfo.groupId > 0 &&
<LayoutBadgeImageView badgeCode={avatarInfo.groupBadgeId} customTitle={avatarInfo.groupName} isGroup={true} showInfo={true} />}
</Flex>
</div>
<Flex center gap={1}>
<div className="flex items-center justify-center relative w-[40px] h-[40px] bg-no-repeat bg-center">
{avatarInfo.badges[1] && <LayoutBadgeImageView badgeCode={avatarInfo.badges[1]} showInfo={true} />}
</div>
<div className="flex items-center justify-center relative w-[40px] h-[40px] bg-no-repeat bg-center">
{avatarInfo.badges[2] && <LayoutBadgeImageView badgeCode={avatarInfo.badges[2]} showInfo={true} />}
</div>
</Flex>
<Flex center gap={1}>
<div className="flex items-center justify-center relative w-[40px] h-[40px] bg-no-repeat bg-center">
{avatarInfo.badges[3] && <LayoutBadgeImageView badgeCode={avatarInfo.badges[3]} showInfo={true} />}
</div>
<div className="flex items-center justify-center relative w-[40px] h-[40px] bg-no-repeat bg-center">
{avatarInfo.badges[4] && <LayoutBadgeImageView badgeCode={avatarInfo.badges[4]} showInfo={true} />}
</div>
</Flex>
</Column>
</div>
<hr className="m-0 bg-[#0003] border-[0] opacity-[0.5] h-px" />
</div>
<div className="flex flex-col gap-1">
<Flex alignItems="center" className="bg-light-dark rounded py-1 px-2">
{avatarInfo.type !== AvatarInfoUser.OWN_USER && (
<Flex grow alignItems="center" className="min-h-[18px]">
<Text fullWidth pointer small textBreak wrap variant="white">{motto}</Text>
</Flex>
)}
{avatarInfo.type === AvatarInfoUser.OWN_USER && (
<Flex grow alignItems="center" gap={2}>
<FaPencilAlt className="small fa-icon" />
<Flex grow alignItems="center" className="min-h-[18px]">
{!isEditingMotto && (
<Text fullWidth pointer small textBreak wrap variant="white" onClick={event => setIsEditingMotto(true)}>
{motto} 
</Text>
)}
{isEditingMotto && (
<input
autoFocus={true}
className="w-full h-full text-[12px] p-0 outline-[0] border-[0] text-[#fff] relative bg-transparent resize-none focus:italic border-transparent focus:border-transparent focus:ring-0"
maxLength={GetConfigurationValue<number>('motto.max.length', 38)}
type="text"
value={motto}
onBlur={onMottoBlur}
onChange={event => setMotto(event.target.value)}
onKeyDown={onMottoKeyDown}
/>
)}
</Flex>
</Flex>
)}
</Flex>
<hr className="m-0 bg-[#0003] border-[0] opacity-[0.5] h-px" />
</div>
<div className="flex flex-col gap-1">
<Text small wrap variant="white">
{LocalizeText('infostand.text.achievement_score') + ' ' + avatarInfo.achievementScore}
</Text>
{avatarInfo.carryItem > 0 && (
<>
<hr className="m-0 bg-[#0003] border-[0] opacity-[0.5] h-px" />
<Text small wrap variant="white">
{LocalizeText('infostand.text.handitem', ['item'], [LocalizeText('handitem' + avatarInfo.carryItem)])}
</Text>
</>
)}
</div>
<div className="flex flex-col gap-1">
<InfoStandWidgetUserRelationshipsView relationships={relationships} />
</div>
{GetConfigurationValue('user.tags.enabled') && (
<Column className="mt-1" gap={1}>
<InfoStandWidgetUserTagsView tags={GetSessionDataManager().tags} />
</Column>
)}
</Column>
</Column>
{isVisible && avatarInfo.type === AvatarInfoUser.OWN_USER && (
<div className="backgrounds-view-container">
<BackgroundsView
setIsVisible={setIsVisible}
selectedBackground={backgroundId}
setSelectedBackground={setBackgroundId}
selectedStand={standId}
setSelectedStand={setStandId}
selectedOverlay={overlayId}
setSelectedOverlay={setOverlayId}
/>
</div>
)}
</>
);
};
@@ -0,0 +1,372 @@
import { CreateLinkEvent, GetSessionDataManager, RoomControllerLevel, RoomObjectCategory, RoomObjectVariable, RoomUnitGiveHandItemComposer, SetRelationshipStatusComposer, TradingOpenComposer } from '@nitrots/nitro-renderer';
import { FC, useEffect, useMemo, useState } from 'react';
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa';
import { AvatarInfoUser, DispatchUiEvent, GetOwnRoomObject, GetUserProfile, LocalizeText, MessengerFriend, ReportType, RoomWidgetUpdateChatInputContentEvent, SendMessageComposer } from '../../../../../api';
import { Flex } from '../../../../../common';
import { useFriends, useHelp, useRoom, useSessionInfo } from '../../../../../hooks';
import { ContextMenuHeaderView } from '../../context-menu/ContextMenuHeaderView';
import { ContextMenuListItemView } from '../../context-menu/ContextMenuListItemView';
import { ContextMenuView } from '../../context-menu/ContextMenuView';
interface AvatarInfoWidgetAvatarViewProps
{
avatarInfo: AvatarInfoUser;
onClose: () => void;
}
const MODE_NORMAL = 0;
const MODE_MODERATE = 1;
const MODE_MODERATE_BAN = 2;
const MODE_MODERATE_MUTE = 3;
const MODE_AMBASSADOR = 4;
const MODE_AMBASSADOR_MUTE = 5;
const MODE_RELATIONSHIP = 6;
export const AvatarInfoWidgetAvatarView: FC<AvatarInfoWidgetAvatarViewProps> = props =>
{
const { avatarInfo = null, onClose = null } = props;
const [ mode, setMode ] = useState(MODE_NORMAL);
const { canRequestFriend = null } = useFriends();
const { report = null } = useHelp();
const { roomSession = null } = useRoom();
const { userRespectRemaining = 0, respectUser = null } = useSessionInfo();
const isShowGiveRights = useMemo(() =>
{
return (avatarInfo.amIOwner && (avatarInfo.targetRoomControllerLevel < RoomControllerLevel.GUEST) && !avatarInfo.isGuildRoom);
}, [ avatarInfo ]);
const isShowRemoveRights = useMemo(() =>
{
return (avatarInfo.amIOwner && (avatarInfo.targetRoomControllerLevel === RoomControllerLevel.GUEST) && !avatarInfo.isGuildRoom);
}, [ avatarInfo ]);
const moderateMenuHasContent = useMemo(() =>
{
return (avatarInfo.canBeKicked || avatarInfo.canBeBanned || avatarInfo.canBeMuted || isShowGiveRights || isShowRemoveRights);
}, [ isShowGiveRights, isShowRemoveRights, avatarInfo ]);
const canGiveHandItem = useMemo(() =>
{
let flag = false;
const roomObject = GetOwnRoomObject();
if(roomObject)
{
const carryId = roomObject.model.getValue<number>(RoomObjectVariable.FIGURE_CARRY_OBJECT);
if((carryId > 0) && (carryId < 999999)) flag = true;
}
return flag;
}, []);
const processAction = (name: string) =>
{
let hideMenu = true;
if(name)
{
switch(name)
{
case 'moderate':
hideMenu = false;
setMode(MODE_MODERATE);
break;
case 'ban':
hideMenu = false;
setMode(MODE_MODERATE_BAN);
break;
case 'mute':
hideMenu = false;
setMode(MODE_MODERATE_MUTE);
break;
case 'ambassador':
hideMenu = false;
setMode(MODE_AMBASSADOR);
break;
case 'ambassador_mute':
hideMenu = false;
setMode(MODE_AMBASSADOR_MUTE);
break;
case 'back_moderate':
hideMenu = false;
setMode(MODE_MODERATE);
break;
case 'back_ambassador':
hideMenu = false;
setMode(MODE_AMBASSADOR);
break;
case 'back':
hideMenu = false;
setMode(MODE_NORMAL);
break;
case 'whisper':
DispatchUiEvent(new RoomWidgetUpdateChatInputContentEvent(RoomWidgetUpdateChatInputContentEvent.WHISPER, avatarInfo.name));
break;
case 'friend':
CreateLinkEvent(`friends/request/${ avatarInfo.webID }/${ avatarInfo.name }`);
break;
case 'relationship':
hideMenu = false;
setMode(MODE_RELATIONSHIP);
break;
case 'respect': {
respectUser(avatarInfo.webID);
if((userRespectRemaining - 1) >= 1) hideMenu = false;
break;
}
case 'ignore':
GetSessionDataManager().ignoreUser(avatarInfo.name);
break;
case 'unignore':
GetSessionDataManager().unignoreUser(avatarInfo.name);
break;
case 'kick':
roomSession.sendKickMessage(avatarInfo.webID);
break;
case 'ban_hour':
roomSession.sendBanMessage(avatarInfo.webID, 'RWUAM_BAN_USER_HOUR');
break;
case 'ban_day':
roomSession.sendBanMessage(avatarInfo.webID, 'RWUAM_BAN_USER_DAY');
break;
case 'perm_ban':
roomSession.sendBanMessage(avatarInfo.webID, 'RWUAM_BAN_USER_PERM');
break;
case 'mute_2min':
roomSession.sendMuteMessage(avatarInfo.webID, 2);
break;
case 'mute_5min':
roomSession.sendMuteMessage(avatarInfo.webID, 5);
break;
case 'mute_10min':
roomSession.sendMuteMessage(avatarInfo.webID, 10);
break;
case 'give_rights':
roomSession.sendGiveRightsMessage(avatarInfo.webID);
break;
case 'remove_rights':
roomSession.sendTakeRightsMessage(avatarInfo.webID);
break;
case 'trade':
SendMessageComposer(new TradingOpenComposer(avatarInfo.roomIndex));
break;
case 'report':
report(ReportType.BULLY, { reportedUserId: avatarInfo.webID });
break;
case 'pass_hand_item':
SendMessageComposer(new RoomUnitGiveHandItemComposer(avatarInfo.webID));
break;
case 'ambassador_alert':
roomSession.sendAmbassadorAlertMessage(avatarInfo.webID);
break;
case 'ambassador_kick':
roomSession.sendKickMessage(avatarInfo.webID);
break;
case 'ambassador_mute_2min':
roomSession.sendMuteMessage(avatarInfo.webID, 2);
break;
case 'ambassador_mute_10min':
roomSession.sendMuteMessage(avatarInfo.webID, 10);
break;
case 'ambassador_mute_60min':
roomSession.sendMuteMessage(avatarInfo.webID, 60);
break;
case 'ambassador_mute_18hour':
roomSession.sendMuteMessage(avatarInfo.webID, 1080);
break;
case 'rship_heart':
SendMessageComposer(new SetRelationshipStatusComposer(avatarInfo.webID, MessengerFriend.RELATIONSHIP_HEART));
break;
case 'rship_smile':
SendMessageComposer(new SetRelationshipStatusComposer(avatarInfo.webID, MessengerFriend.RELATIONSHIP_SMILE));
break;
case 'rship_bobba':
SendMessageComposer(new SetRelationshipStatusComposer(avatarInfo.webID, MessengerFriend.RELATIONSHIP_BOBBA));
break;
case 'rship_none':
SendMessageComposer(new SetRelationshipStatusComposer(avatarInfo.webID, MessengerFriend.RELATIONSHIP_NONE));
break;
}
}
if(hideMenu) onClose();
};
useEffect(() =>
{
setMode(MODE_NORMAL);
}, [ avatarInfo ]);
return (
<ContextMenuView category={ RoomObjectCategory.UNIT } collapsable={ true } objectId={ avatarInfo.roomIndex } userType={ avatarInfo.userType } onClose={ onClose }>
<ContextMenuHeaderView className="cursor-pointer" onClick={ event => GetUserProfile(avatarInfo.webID) }>
{ avatarInfo.name }
</ContextMenuHeaderView>
{ (mode === MODE_NORMAL) &&
<>
{ canRequestFriend(avatarInfo.webID) &&
<ContextMenuListItemView onClick={ event => processAction('friend') }>
{ LocalizeText('infostand.button.friend') }
</ContextMenuListItemView> }
<ContextMenuListItemView onClick={ event => processAction('trade') }>
{ LocalizeText('infostand.button.trade') }
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('whisper') }>
{ LocalizeText('infostand.button.whisper') }
</ContextMenuListItemView>
{ (userRespectRemaining > 0) &&
<ContextMenuListItemView onClick={ event => processAction('respect') }>
{ LocalizeText('infostand.button.respect', [ 'count' ], [ userRespectRemaining.toString() ]) }
</ContextMenuListItemView> }
{ !canRequestFriend(avatarInfo.webID) &&
<ContextMenuListItemView onClick={ event => processAction('relationship') }>
{ LocalizeText('infostand.link.relationship') }
<FaChevronRight className="right fa-icon" />
</ContextMenuListItemView> }
{ !avatarInfo.isIgnored &&
<ContextMenuListItemView onClick={ event => processAction('ignore') }>
{ LocalizeText('infostand.button.ignore') }
</ContextMenuListItemView> }
{ avatarInfo.isIgnored &&
<ContextMenuListItemView onClick={ event => processAction('unignore') }>
{ LocalizeText('infostand.button.unignore') }
</ContextMenuListItemView> }
<ContextMenuListItemView onClick={ event => processAction('report') }>
{ LocalizeText('infostand.button.report') }
</ContextMenuListItemView>
{ moderateMenuHasContent &&
<ContextMenuListItemView onClick={ event => processAction('moderate') }>
<FaChevronRight className="right fa-icon" />
{ LocalizeText('infostand.link.moderate') }
</ContextMenuListItemView> }
{ avatarInfo.isAmbassador &&
<ContextMenuListItemView onClick={ event => processAction('ambassador') }>
<FaChevronRight className="right fa-icon" />
{ LocalizeText('infostand.link.ambassador') }
</ContextMenuListItemView> }
{ canGiveHandItem && <ContextMenuListItemView onClick={ event => processAction('pass_hand_item') }>
{ LocalizeText('avatar.widget.pass_hand_item') }
</ContextMenuListItemView> }
</> }
{ (mode === MODE_MODERATE) &&
<>
<ContextMenuListItemView onClick={ event => processAction('kick') }>
{ LocalizeText('infostand.button.kick') }
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('mute') }>
<FaChevronRight className="right fa-icon" />
{ LocalizeText('infostand.button.mute') }
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('ban') }>
<FaChevronRight className="right fa-icon" />
{ LocalizeText('infostand.button.ban') }
</ContextMenuListItemView>
{ isShowGiveRights &&
<ContextMenuListItemView onClick={ event => processAction('give_rights') }>
{ LocalizeText('infostand.button.giverights') }
</ContextMenuListItemView> }
{ isShowRemoveRights &&
<ContextMenuListItemView onClick={ event => processAction('remove_rights') }>
{ LocalizeText('infostand.button.removerights') }
</ContextMenuListItemView> }
<ContextMenuListItemView onClick={ event => processAction('back') }>
<FaChevronLeft className="left fa-icon" />
{ LocalizeText('generic.back') }
</ContextMenuListItemView>
</> }
{ (mode === MODE_MODERATE_BAN) &&
<>
<ContextMenuListItemView onClick={ event => processAction('ban_hour') }>
{ LocalizeText('infostand.button.ban_hour') }
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('ban_day') }>
{ LocalizeText('infostand.button.ban_day') }
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('perm_ban') }>
{ LocalizeText('infostand.button.perm_ban') }
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('back_moderate') }>
<FaChevronLeft className="left fa-icon" />
{ LocalizeText('generic.back') }
</ContextMenuListItemView>
</> }
{ (mode === MODE_MODERATE_MUTE) &&
<>
<ContextMenuListItemView onClick={ event => processAction('mute_2min') }>
{ LocalizeText('infostand.button.mute_2min') }
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('mute_5min') }>
{ LocalizeText('infostand.button.mute_5min') }
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('mute_10min') }>
{ LocalizeText('infostand.button.mute_10min') }
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('back_moderate') }>
<FaChevronLeft className="left fa-icon" />
{ LocalizeText('generic.back') }
</ContextMenuListItemView>
</> }
{ (mode === MODE_AMBASSADOR) &&
<>
<ContextMenuListItemView onClick={ event => processAction('ambassador_alert') }>
{ LocalizeText('infostand.button.alert') }
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('ambassador_kick') }>
{ LocalizeText('infostand.button.kick') }
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('ambassador_mute') }>
{ LocalizeText('infostand.button.mute') }
<FaChevronRight className="right fa-icon" />
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('back') }>
<FaChevronLeft className="left fa-icon" />
{ LocalizeText('generic.back') }
</ContextMenuListItemView>
</> }
{ (mode === MODE_AMBASSADOR_MUTE) &&
<>
<ContextMenuListItemView onClick={ event => processAction('ambassador_mute_2min') }>
{ LocalizeText('infostand.button.mute_2min') }
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('ambassador_mute_10min') }>
{ LocalizeText('infostand.button.mute_10min') }
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('ambassador_mute_60min') }>
{ LocalizeText('infostand.button.mute_60min') }
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('ambassador_mute_18hr') }>
{ LocalizeText('infostand.button.mute_18hour') }
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('back_ambassador') }>
<FaChevronLeft className="left fa-icon" />
{ LocalizeText('generic.back') }
</ContextMenuListItemView>
</> }
{ (mode === MODE_RELATIONSHIP) &&
<>
<Flex className="menu-list-split-3">
<ContextMenuListItemView onClick={ event => processAction('rship_heart') }>
<div className="nitro-friends-spritesheet icon-heart cursor-pointer" />
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('rship_smile') }>
<div className="nitro-friends-spritesheet icon-smile cursor-pointer" />
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('rship_bobba') }>
<div className="nitro-friends-spritesheet icon-bobba cursor-pointer" />
</ContextMenuListItemView>
</Flex>
<ContextMenuListItemView onClick={ event => processAction('rship_none') }>
{ LocalizeText('avatar.widget.clear_relationship') }
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('back') }>
<FaChevronLeft className="left fa-icon" />
{ LocalizeText('generic.back') }
</ContextMenuListItemView>
</> }
</ContextMenuView>
);
};
@@ -0,0 +1,29 @@
import { RoomObjectCategory } from '@nitrots/nitro-renderer';
import { Dispatch, FC, SetStateAction } from 'react';
import { LocalizeText } from '../../../../../api';
import { ContextMenuListItemView } from '../../context-menu/ContextMenuListItemView';
import { ContextMenuListView } from '../../context-menu/ContextMenuListView';
import { ContextMenuView } from '../../context-menu/ContextMenuView';
interface AvatarInfoWidgetDecorateViewProps
{
userId: number;
userName: string;
roomIndex: number;
setIsDecorating: Dispatch<SetStateAction<boolean>>;
}
export const AvatarInfoWidgetDecorateView: FC<AvatarInfoWidgetDecorateViewProps> = props =>
{
const { userId = -1, userName = '', roomIndex = -1, setIsDecorating = null } = props;
return (
<ContextMenuView category={ RoomObjectCategory.UNIT } objectId={ roomIndex } onClose={ null }>
<ContextMenuListView>
<ContextMenuListItemView onClick={ event => setIsDecorating(false) }>
{ LocalizeText('widget.avatar.stop_decorating') }
</ContextMenuListItemView>
</ContextMenuListView>
</ContextMenuView>
);
};
@@ -0,0 +1,66 @@
import { RoomControllerLevel, RoomObjectOperationType } from '@nitrots/nitro-renderer';
import { FC } from 'react';
import { FaArrowsAlt, FaSyncAlt, FaTrashRestore } from 'react-icons/fa';
import { AvatarInfoFurni, ProcessRoomObjectOperation } from '../../../../../api';
import { ContextMenuHeaderView } from '../../context-menu/ContextMenuHeaderView';
import { ContextMenuListItemView } from '../../context-menu/ContextMenuListItemView';
import { ContextMenuView } from '../../context-menu/ContextMenuView';
interface AvatarInfoWidgetFurniViewProps
{
avatarInfo: AvatarInfoFurni;
onClose: () => void;
}
export const AvatarInfoWidgetFurniView: FC<AvatarInfoWidgetFurniViewProps> = props =>
{
const { avatarInfo = null, onClose = null } = props;
const processAction = (name: string) =>
{
let hideMenu = true;
if(name)
{
switch(name)
{
case 'move':
ProcessRoomObjectOperation(avatarInfo.id, avatarInfo.category, RoomObjectOperationType.OBJECT_MOVE);
break;
case 'rotate':
ProcessRoomObjectOperation(avatarInfo.id, avatarInfo.category, RoomObjectOperationType.OBJECT_ROTATE_POSITIVE);
break;
case 'pickup':
ProcessRoomObjectOperation(avatarInfo.id, avatarInfo.category, RoomObjectOperationType.OBJECT_PICKUP);
break;
case 'eject':
ProcessRoomObjectOperation(avatarInfo.id, avatarInfo.category, RoomObjectOperationType.OBJECT_EJECT);
break;
}
}
};
return (
<ContextMenuView category={ avatarInfo.category } collapsable={ true } objectId={ avatarInfo.id } onClose={ onClose }>
<ContextMenuHeaderView>
{ avatarInfo.name }
</ContextMenuHeaderView>
<div className="flex menu-list-split-3">
<ContextMenuListItemView onClick={ event => processAction('move') }>
<FaArrowsAlt className="center fa-icon" />
</ContextMenuListItemView>
<ContextMenuListItemView disabled={ avatarInfo.isWallItem } onClick={ event => processAction('rotate') }>
<FaSyncAlt className="center fa-icon" />
</ContextMenuListItemView>
{ (avatarInfo.isOwner || avatarInfo.isAnyRoomController) &&
<ContextMenuListItemView onClick={ event => processAction('pickup') }>
<FaTrashRestore className="center fa-icon" />
</ContextMenuListItemView> }
{ (!avatarInfo.isOwner && !avatarInfo.isAnyRoomController) && (avatarInfo.isRoomOwner || (avatarInfo.roomControllerLevel >= RoomControllerLevel.GUILD_ADMIN)) &&
<ContextMenuListItemView onClick={ event => processAction('eject') }>
<FaTrashRestore className="center fa-icon" />
</ContextMenuListItemView> }
</div>
</ContextMenuView>
);
};
@@ -0,0 +1,32 @@
import { GetSessionDataManager } from '@nitrots/nitro-renderer';
import { FC, useMemo } from 'react';
import { AvatarInfoName } from '../../../../../api';
import { ContextMenuView } from '../../context-menu/ContextMenuView';
interface AvatarInfoWidgetNameViewProps
{
nameInfo: AvatarInfoName;
onClose: () => void;
}
export const AvatarInfoWidgetNameView: FC<AvatarInfoWidgetNameViewProps> = props =>
{
const { nameInfo = null, onClose = null } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'name-only' ];
if(nameInfo.isFriend) newClassNames.push('is-friend');
return newClassNames;
}, [ nameInfo ]);
return (
<ContextMenuView category={ nameInfo.category } classNames={ getClassNames } fades={ (nameInfo.id !== GetSessionDataManager().userId) } objectId={ nameInfo.roomIndex } userType={ nameInfo.userType } onClose={ onClose }>
<div className="text-shadow">
{ nameInfo.name }
</div>
</ContextMenuView>
);
};
@@ -0,0 +1,292 @@
import { AvatarAction, AvatarExpressionEnum, CreateLinkEvent, RoomControllerLevel, RoomObjectCategory, RoomUnitDropHandItemComposer } from '@nitrots/nitro-renderer';
import { Dispatch, FC, SetStateAction, useState } from 'react';
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa';
import { AvatarInfoUser, DispatchUiEvent, GetCanStandUp, GetCanUseExpression, GetOwnPosture, GetUserProfile, HasHabboClub, HasHabboVip, IsRidingHorse, LocalizeText, PostureTypeEnum, SendMessageComposer } from '../../../../../api';
import { LayoutCurrencyIcon } from '../../../../../common';
import { HelpNameChangeEvent } from '../../../../../events';
import { useRoom } from '../../../../../hooks';
import { ContextMenuHeaderView } from '../../context-menu/ContextMenuHeaderView';
import { ContextMenuListItemView } from '../../context-menu/ContextMenuListItemView';
import { ContextMenuView } from '../../context-menu/ContextMenuView';
interface AvatarInfoWidgetOwnAvatarViewProps
{
avatarInfo: AvatarInfoUser;
isDancing: boolean;
setIsDecorating: Dispatch<SetStateAction<boolean>>;
onClose: () => void;
}
const MODE_NORMAL = 0;
const MODE_CLUB_DANCES = 1;
const MODE_NAME_CHANGE = 2;
const MODE_EXPRESSIONS = 3;
const MODE_SIGNS = 4;
export const AvatarInfoWidgetOwnAvatarView: FC<AvatarInfoWidgetOwnAvatarViewProps> = props =>
{
const { avatarInfo = null, isDancing = false, setIsDecorating = null, onClose = null } = props;
const [ mode, setMode ] = useState((isDancing && HasHabboClub()) ? MODE_CLUB_DANCES : MODE_NORMAL);
const { roomSession = null } = useRoom();
const processAction = (name: string) =>
{
let hideMenu = true;
if(name)
{
if(name.startsWith('sign_'))
{
const sign = parseInt(name.split('_')[1]);
roomSession.sendSignMessage(sign);
}
else
{
switch(name)
{
case 'decorate':
setIsDecorating(true);
break;
case 'change_name':
DispatchUiEvent(new HelpNameChangeEvent(HelpNameChangeEvent.INIT));
break;
case 'change_looks':
CreateLinkEvent('avatar-editor/show');
break;
case 'expressions':
hideMenu = false;
setMode(MODE_EXPRESSIONS);
break;
case 'sit':
roomSession.sendPostureMessage(PostureTypeEnum.POSTURE_SIT);
break;
case 'stand':
roomSession.sendPostureMessage(PostureTypeEnum.POSTURE_STAND);
break;
case 'wave':
roomSession.sendExpressionMessage(AvatarExpressionEnum.WAVE.ordinal);
break;
case 'blow':
roomSession.sendExpressionMessage(AvatarExpressionEnum.BLOW.ordinal);
break;
case 'laugh':
roomSession.sendExpressionMessage(AvatarExpressionEnum.LAUGH.ordinal);
break;
case 'idle':
roomSession.sendExpressionMessage(AvatarExpressionEnum.IDLE.ordinal);
break;
case 'dance_menu':
hideMenu = false;
setMode(MODE_CLUB_DANCES);
break;
case 'dance':
roomSession.sendDanceMessage(1);
break;
case 'dance_stop':
roomSession.sendDanceMessage(0);
break;
case 'dance_1':
case 'dance_2':
case 'dance_3':
case 'dance_4':
roomSession.sendDanceMessage(parseInt(name.charAt((name.length - 1))));
break;
case 'signs':
hideMenu = false;
setMode(MODE_SIGNS);
break;
case 'back':
hideMenu = false;
setMode(MODE_NORMAL);
break;
case 'drop_carry_item':
SendMessageComposer(new RoomUnitDropHandItemComposer());
break;
}
}
}
if(hideMenu) onClose();
};
const isShowDecorate = () => (avatarInfo.amIOwner || avatarInfo.amIAnyRoomController || (avatarInfo.roomControllerLevel > RoomControllerLevel.GUEST));
const isRidingHorse = IsRidingHorse();
return (
<ContextMenuView category={ RoomObjectCategory.UNIT } collapsable={ true } objectId={ avatarInfo.roomIndex } userType={ avatarInfo.userType } onClose={ onClose }>
<ContextMenuHeaderView className="cursor-pointer" onClick={ event => GetUserProfile(avatarInfo.webID) }>
{ avatarInfo.name }
</ContextMenuHeaderView>
{ (mode === MODE_NORMAL) &&
<>
{ avatarInfo.allowNameChange &&
<ContextMenuListItemView onClick={ event => processAction('change_name') }>
{ LocalizeText('widget.avatar.change_name') }
</ContextMenuListItemView> }
{ isShowDecorate() &&
<ContextMenuListItemView onClick={ event => processAction('decorate') }>
{ LocalizeText('widget.avatar.decorate') }
</ContextMenuListItemView> }
<ContextMenuListItemView onClick={ event => processAction('change_looks') }>
{ LocalizeText('widget.memenu.myclothes') }
</ContextMenuListItemView>
{ (HasHabboClub() && !isRidingHorse) &&
<ContextMenuListItemView onClick={ event => processAction('dance_menu') }>
<FaChevronRight className="right fa-icon" />
{ LocalizeText('widget.memenu.dance') }
</ContextMenuListItemView> }
{ (!isDancing && !HasHabboClub() && !isRidingHorse) &&
<ContextMenuListItemView onClick={ event => processAction('dance') }>
{ LocalizeText('widget.memenu.dance') }
</ContextMenuListItemView> }
{ (isDancing && !HasHabboClub() && !isRidingHorse) &&
<ContextMenuListItemView onClick={ event => processAction('dance_stop') }>
{ LocalizeText('widget.memenu.dance.stop') }
</ContextMenuListItemView> }
<ContextMenuListItemView onClick={ event => processAction('expressions') }>
<FaChevronRight className="right fa-icon" />
{ LocalizeText('infostand.link.expressions') }
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('signs') }>
<FaChevronRight className="right fa-icon" />
{ LocalizeText('infostand.show.signs') }
</ContextMenuListItemView>
{ (avatarInfo.carryItem > 0) &&
<ContextMenuListItemView onClick={ event => processAction('drop_carry_item') }>
{ LocalizeText('avatar.widget.drop_hand_item') }
</ContextMenuListItemView> }
</> }
{ (mode === MODE_CLUB_DANCES) &&
<>
{ isDancing &&
<ContextMenuListItemView onClick={ event => processAction('dance_stop') }>
{ LocalizeText('widget.memenu.dance.stop') }
</ContextMenuListItemView> }
<ContextMenuListItemView onClick={ event => processAction('dance_1') }>
{ LocalizeText('widget.memenu.dance1') }
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('dance_2') }>
{ LocalizeText('widget.memenu.dance2') }
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('dance_3') }>
{ LocalizeText('widget.memenu.dance3') }
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('dance_4') }>
{ LocalizeText('widget.memenu.dance4') }
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('back') }>
<FaChevronLeft className="left fa-icon" />
{ LocalizeText('generic.back') }
</ContextMenuListItemView>
</> }
{ (mode === MODE_EXPRESSIONS) &&
<>
{ (GetOwnPosture() === AvatarAction.POSTURE_STAND) &&
<ContextMenuListItemView onClick={ event => processAction('sit') }>
{ LocalizeText('widget.memenu.sit') }
</ContextMenuListItemView> }
{ GetCanStandUp() &&
<ContextMenuListItemView onClick={ event => processAction('stand') }>
{ LocalizeText('widget.memenu.stand') }
</ContextMenuListItemView> }
{ GetCanUseExpression() &&
<ContextMenuListItemView onClick={ event => processAction('wave') }>
{ LocalizeText('widget.memenu.wave') }
</ContextMenuListItemView> }
{ GetCanUseExpression() &&
<ContextMenuListItemView disabled={ !HasHabboVip() } onClick={ event => processAction('laugh') }>
{ !HasHabboVip() && <LayoutCurrencyIcon type="hc" /> }
{ LocalizeText('widget.memenu.laugh') }
</ContextMenuListItemView> }
{ GetCanUseExpression() &&
<ContextMenuListItemView disabled={ !HasHabboVip() } onClick={ event => processAction('blow') }>
{ !HasHabboVip() && <LayoutCurrencyIcon type="hc" /> }
{ LocalizeText('widget.memenu.blow') }
</ContextMenuListItemView> }
<ContextMenuListItemView onClick={ event => processAction('idle') }>
{ LocalizeText('widget.memenu.idle') }
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('back') }>
<FaChevronLeft className="left fa-icon" />
{ LocalizeText('generic.back') }
</ContextMenuListItemView>
</> }
{ (mode === MODE_SIGNS) &&
<>
<div className="flex menu-list-split-3">
<ContextMenuListItemView onClick={ event => processAction('sign_1') }>
1
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('sign_2') }>
2
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('sign_3') }>
3
</ContextMenuListItemView>
</div>
<div className="flex menu-list-split-3">
<ContextMenuListItemView onClick={ event => processAction('sign_4') }>
4
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('sign_5') }>
5
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('sign_6') }>
6
</ContextMenuListItemView>
</div>
<div className="flex menu-list-split-3">
<ContextMenuListItemView onClick={ event => processAction('sign_7') }>
7
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('sign_8') }>
8
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('sign_9') }>
9
</ContextMenuListItemView>
</div>
<div className="flex menu-list-split-3">
<ContextMenuListItemView onClick={ event => processAction('sign_10') }>
10
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('sign_11') }>
<i className="nitro-icon icon-sign-heart" />
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('sign_12') }>
<i className="nitro-icon icon-sign-skull" />
</ContextMenuListItemView>
</div>
<div className="flex menu-list-split-3">
<ContextMenuListItemView onClick={ event => processAction('sign_0') }>
0
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('sign_13') }>
<i className="nitro-icon icon-sign-exclamation" />
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('sign_15') }>
<i className="nitro-icon icon-sign-smile" />
</ContextMenuListItemView>
</div>
<div className="flex menu-list-split-3">
<ContextMenuListItemView onClick={ event => processAction('sign_14') }>
<i className="nitro-icon icon-sign-soccer" />
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('sign_17') }>
<i className="nitro-icon icon-sign-yellow" />
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('sign_16') }>
<i className="nitro-icon icon-sign-red" />
</ContextMenuListItemView>
</div>
<ContextMenuListItemView onClick={ event => processAction('back') }>
<FaChevronLeft className="left fa-icon" />
{ LocalizeText('generic.back') }
</ContextMenuListItemView>
</> }
</ContextMenuView>
);
};
@@ -0,0 +1,220 @@
import { CreateLinkEvent, PetRespectComposer, PetType, RoomObjectCategory, RoomObjectType, RoomObjectVariable, RoomUnitGiveHandItemPetComposer } from '@nitrots/nitro-renderer';
import { FC, useEffect, useMemo, useState } from 'react';
import { AvatarInfoPet, GetConfigurationValue, GetOwnRoomObject, LocalizeText, SendMessageComposer } from '../../../../../api';
import { useRoom, useSessionInfo } from '../../../../../hooks';
import { ContextMenuHeaderView } from '../../context-menu/ContextMenuHeaderView';
import { ContextMenuListItemView } from '../../context-menu/ContextMenuListItemView';
import { ContextMenuView } from '../../context-menu/ContextMenuView';
interface AvatarInfoWidgetOwnPetViewProps
{
avatarInfo: AvatarInfoPet;
onClose: () => void;
}
const MODE_NORMAL: number = 0;
const MODE_SADDLED_UP: number = 1;
const MODE_RIDING: number = 2;
const MODE_MONSTER_PLANT: number = 3;
export const AvatarInfoWidgetOwnPetView: FC<AvatarInfoWidgetOwnPetViewProps> = props =>
{
const { avatarInfo = null, onClose = null } = props;
const [ mode, setMode ] = useState(MODE_NORMAL);
const { roomSession = null } = useRoom();
const { petRespectRemaining = 0, respectPet = null } = useSessionInfo();
const canGiveHandItem = useMemo(() =>
{
let flag = false;
const roomObject = GetOwnRoomObject();
if(roomObject)
{
const carryId = roomObject.model.getValue<number>(RoomObjectVariable.FIGURE_CARRY_OBJECT);
if((carryId > 0) && (carryId < 999999)) flag = true;
}
return flag;
}, []);
const processAction = (name: string) =>
{
let hideMenu = true;
if(name)
{
switch(name)
{
case 'respect':
respectPet(avatarInfo.id);
if((petRespectRemaining - 1) >= 1) hideMenu = false;
break;
case 'treat':
SendMessageComposer(new PetRespectComposer(avatarInfo.id));
break;
case 'pass_handitem':
SendMessageComposer(new RoomUnitGiveHandItemPetComposer(avatarInfo.id));
break;
case 'train':
roomSession.requestPetCommands(avatarInfo.id);
break;
case 'pick_up':
roomSession.pickupPet(avatarInfo.id);
break;
case 'mount':
roomSession.mountPet(avatarInfo.id);
break;
case 'toggle_riding_permission':
roomSession.togglePetRiding(avatarInfo.id);
break;
case 'toggle_breeding_permission':
roomSession.togglePetBreeding(avatarInfo.id);
break;
case 'dismount':
roomSession.dismountPet(avatarInfo.id);
break;
case 'saddle_off':
roomSession.removePetSaddle(avatarInfo.id);
break;
case 'breed':
if(mode === MODE_NORMAL)
{
// _local_7 = RoomWidgetPetCommandMessage._Str_16282;
// _local_8 = ("pet.command." + _local_7);
// _local_9 = _Str_2268.catalog.localization.getLocalization(_local_8);
// _local_4 = new RoomWidgetPetCommandMessage(RoomWidgetPetCommandMessage.RWPCM_PET_COMMAND, this._Str_594.id, ((this._Str_594.name + " ") + _local_9));
}
else if(mode === MODE_MONSTER_PLANT)
{
// messageType = RoomWidgetUserActionMessage.REQUEST_BREED_PET;
}
break;
case 'harvest':
roomSession.harvestPet(avatarInfo.id);
break;
case 'revive':
//
break;
case 'compost':
roomSession.compostPlant(avatarInfo.id);
break;
case 'buy_saddle':
CreateLinkEvent('catalog/open/' + GetConfigurationValue('catalog.links')['pets.buy_saddle']);
break;
}
}
if(hideMenu) onClose();
};
useEffect(() =>
{
setMode(prevValue =>
{
if(avatarInfo.petType === PetType.MONSTERPLANT) return MODE_MONSTER_PLANT;
else if(avatarInfo.saddle && !avatarInfo.rider) return MODE_SADDLED_UP;
else if(avatarInfo.rider) return MODE_RIDING;
return MODE_NORMAL;
});
}, [ avatarInfo ]);
return (
<ContextMenuView category={ RoomObjectCategory.UNIT } collapsable={ true } objectId={ avatarInfo.roomIndex } userType={ RoomObjectType.PET } onClose={ onClose }>
<ContextMenuHeaderView>
{ avatarInfo.name }
</ContextMenuHeaderView>
{ (mode === MODE_NORMAL) &&
<>
{ (petRespectRemaining > 0) &&
<ContextMenuListItemView onClick={ event => processAction('respect') }>
{ LocalizeText('infostand.button.petrespect', [ 'count' ], [ petRespectRemaining.toString() ]) }
</ContextMenuListItemView> }
<ContextMenuListItemView onClick={ event => processAction('train') }>
{ LocalizeText('infostand.button.train') }
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('pick_up') }>
{ LocalizeText('infostand.button.pickup') }
</ContextMenuListItemView>
{ (avatarInfo.petType === PetType.HORSE) &&
<ContextMenuListItemView onClick={ event => processAction('buy_saddle') }>
{ LocalizeText('infostand.button.buy_saddle') }
</ContextMenuListItemView> }
{ ([ PetType.BEAR, PetType.TERRIER, PetType.CAT, PetType.DOG, PetType.PIG ].indexOf(avatarInfo.petType) > -1) &&
<ContextMenuListItemView onClick={ event => processAction('breed') }>
{ LocalizeText('infostand.button.breed') }
</ContextMenuListItemView> }
</> }
{ (mode === MODE_SADDLED_UP) &&
<>
<ContextMenuListItemView onClick={ event => processAction('mount') }>
{ LocalizeText('infostand.button.mount') }
</ContextMenuListItemView>
<ContextMenuListItemView gap={ 1 } onClick={ event => processAction('toggle_riding_permission') }>
<input checked={ !!avatarInfo.publiclyRideable } readOnly={ true } type="checkbox" />
{ LocalizeText('infostand.button.toggle_riding_permission') }
</ContextMenuListItemView>
{ (petRespectRemaining > 0) &&
<ContextMenuListItemView onClick={ event => processAction('respect') }>
{ LocalizeText('infostand.button.petrespect', [ 'count' ], [ petRespectRemaining.toString() ]) }
</ContextMenuListItemView> }
<ContextMenuListItemView onClick={ event => processAction('train') }>
{ LocalizeText('infostand.button.train') }
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('pick_up') }>
{ LocalizeText('infostand.button.pickup') }
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('saddle_off') }>
{ LocalizeText('infostand.button.saddleoff') }
</ContextMenuListItemView>
</> }
{ (mode === MODE_RIDING) &&
<>
<ContextMenuListItemView onClick={ event => processAction('dismount') }>
{ LocalizeText('infostand.button.dismount') }
</ContextMenuListItemView>
{ (petRespectRemaining > 0) &&
<ContextMenuListItemView onClick={ event => processAction('respect') }>
{ LocalizeText('infostand.button.petrespect', [ 'count' ], [ petRespectRemaining.toString() ]) }
</ContextMenuListItemView> }
</> }
{ (mode === MODE_MONSTER_PLANT) &&
<>
<ContextMenuListItemView onClick={ event => processAction('pick_up') }>
{ LocalizeText('infostand.button.pickup') }
</ContextMenuListItemView>
{ avatarInfo.dead &&
<ContextMenuListItemView onClick={ event => processAction('revive') }>
{ LocalizeText('infostand.button.revive') }
</ContextMenuListItemView> }
{ roomSession.isRoomOwner &&
<ContextMenuListItemView onClick={ event => processAction('compost') }>
{ LocalizeText('infostand.button.compost') }
</ContextMenuListItemView> }
{ !avatarInfo.dead && ((avatarInfo.energy / avatarInfo.maximumEnergy) < 0.98) &&
<ContextMenuListItemView onClick={ event => processAction('treat') }>
{ LocalizeText('infostand.button.pettreat') }
</ContextMenuListItemView> }
{ !avatarInfo.dead && (avatarInfo.level === avatarInfo.maximumLevel) && avatarInfo.breedable &&
<>
<ContextMenuListItemView gap={ 1 } onClick={ event => processAction('toggle_breeding_permission') }>
<input checked={ avatarInfo.publiclyBreedable } readOnly={ true } type="checkbox" />
{ LocalizeText('infostand.button.toggle_breeding_permission') }
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('breed') }>
{ LocalizeText('infostand.button.breed') }
</ContextMenuListItemView>
</> }
</> }
{ canGiveHandItem &&
<ContextMenuListItemView onClick={ event => processAction('pass_hand_item') }>
{ LocalizeText('infostand.button.pass_hand_item') }
</ContextMenuListItemView> }
</ContextMenuView>
);
};
@@ -0,0 +1,138 @@
import { GetSessionDataManager, PetRespectComposer, PetType, RoomControllerLevel, RoomObjectCategory, RoomObjectType, RoomObjectVariable, RoomUnitGiveHandItemPetComposer } from '@nitrots/nitro-renderer';
import { FC, useEffect, useMemo, useState } from 'react';
import { AvatarInfoPet, GetOwnRoomObject, LocalizeText, SendMessageComposer } from '../../../../../api';
import { useRoom, useSessionInfo } from '../../../../../hooks';
import { ContextMenuHeaderView } from '../../context-menu/ContextMenuHeaderView';
import { ContextMenuListItemView } from '../../context-menu/ContextMenuListItemView';
import { ContextMenuView } from '../../context-menu/ContextMenuView';
interface AvatarInfoWidgetPetViewProps
{
avatarInfo: AvatarInfoPet;
onClose: () => void;
}
const MODE_NORMAL: number = 0;
const MODE_SADDLED_UP: number = 1;
const MODE_RIDING: number = 2;
const MODE_MONSTER_PLANT: number = 3;
export const AvatarInfoWidgetPetView: FC<AvatarInfoWidgetPetViewProps> = props =>
{
const { avatarInfo = null, onClose = null } = props;
const [ mode, setMode ] = useState(MODE_NORMAL);
const { roomSession = null } = useRoom();
const { petRespectRemaining = 0, respectPet = null } = useSessionInfo();
const canPickUp = useMemo(() =>
{
return (roomSession.isRoomOwner || (roomSession.controllerLevel >= RoomControllerLevel.GUEST) || GetSessionDataManager().isModerator);
}, [ roomSession ]);
const canGiveHandItem = useMemo(() =>
{
let flag = false;
const roomObject = GetOwnRoomObject();
if(roomObject)
{
const carryId = roomObject.model.getValue<number>(RoomObjectVariable.FIGURE_CARRY_OBJECT);
if((carryId > 0) && (carryId < 999999)) flag = true;
}
return flag;
}, []);
const processAction = (name: string) =>
{
let hideMenu = true;
if(name)
{
switch(name)
{
case 'respect':
respectPet(avatarInfo.id);
if((petRespectRemaining - 1) >= 1) hideMenu = false;
break;
case 'treat':
SendMessageComposer(new PetRespectComposer(avatarInfo.id));
break;
case 'pass_handitem':
SendMessageComposer(new RoomUnitGiveHandItemPetComposer(avatarInfo.id));
break;
case 'pick_up':
roomSession.pickupPet(avatarInfo.id);
break;
case 'mount':
roomSession.mountPet(avatarInfo.id);
break;
case 'dismount':
roomSession.dismountPet(avatarInfo.id);
break;
}
}
if(hideMenu) onClose();
};
useEffect(() =>
{
setMode(prevValue =>
{
if(avatarInfo.petType === PetType.MONSTERPLANT) return MODE_MONSTER_PLANT;
else if(avatarInfo.saddle && !avatarInfo.rider) return MODE_SADDLED_UP;
else if(avatarInfo.rider) return MODE_RIDING;
return MODE_NORMAL;
});
}, [ avatarInfo ]);
return (
<ContextMenuView category={ RoomObjectCategory.UNIT } collapsable={ true } objectId={ avatarInfo.roomIndex } userType={ RoomObjectType.PET } onClose={ onClose }>
<ContextMenuHeaderView>
{ avatarInfo.name }
</ContextMenuHeaderView>
{ (mode === MODE_NORMAL) && (petRespectRemaining > 0) &&
<ContextMenuListItemView onClick={ event => processAction('respect') }>
{ LocalizeText('infostand.button.petrespect', [ 'count' ], [ petRespectRemaining.toString() ]) }
</ContextMenuListItemView> }
{ (mode === MODE_SADDLED_UP) &&
<>
{ !!avatarInfo.publiclyRideable &&
<ContextMenuListItemView onClick={ event => processAction('mount') }>
{ LocalizeText('infostand.button.mount') }
</ContextMenuListItemView> }
{ (petRespectRemaining > 0) &&
<ContextMenuListItemView onClick={ event => processAction('respect') }>
{ LocalizeText('infostand.button.petrespect', [ 'count' ], [ petRespectRemaining.toString() ]) }
</ContextMenuListItemView> }
</> }
{ (mode === MODE_RIDING) &&
<>
<ContextMenuListItemView onClick={ event => processAction('dismount') }>
{ LocalizeText('infostand.button.dismount') }
</ContextMenuListItemView>
{ (petRespectRemaining > 0) &&
<ContextMenuListItemView onClick={ event => processAction('respect') }>
{ LocalizeText('infostand.button.petrespect', [ 'count' ], [ petRespectRemaining.toString() ]) }
</ContextMenuListItemView> }
</> }
{ (mode === MODE_MONSTER_PLANT) && !avatarInfo.dead && ((avatarInfo.energy / avatarInfo.maximumEnergy) < 0.98) &&
<ContextMenuListItemView onClick={ event => processAction('treat') }>
{ LocalizeText('infostand.button.pettreat') }
</ContextMenuListItemView> }
{ canPickUp &&
<ContextMenuListItemView onClick={ event => processAction('pick_up') }>
{ LocalizeText('infostand.button.pickup') }
</ContextMenuListItemView> }
{ canGiveHandItem &&
<ContextMenuListItemView onClick={ event => processAction('pass_hand_item') }>
{ LocalizeText('infostand.button.pass_hand_item') }
</ContextMenuListItemView> }
</ContextMenuView>
);
};
@@ -0,0 +1,198 @@
import { BotCommandConfigurationEvent, BotRemoveComposer, BotSkillSaveComposer, CreateLinkEvent, RequestBotCommandConfigurationComposer, RoomObjectCategory, RoomObjectType } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { AvatarInfoRentableBot, BotSkillsEnum, DispatchUiEvent, GetConfigurationValue, LocalizeText, RoomWidgetUpdateRentableBotChatEvent, SendMessageComposer } from '../../../../../api';
import { Button, Column, Text } from '../../../../../common';
import { useMessageEvent } from '../../../../../hooks';
import { NitroInput } from '../../../../../layout';
import { ContextMenuHeaderView } from '../../context-menu/ContextMenuHeaderView';
import { ContextMenuListItemView } from '../../context-menu/ContextMenuListItemView';
import { ContextMenuView } from '../../context-menu/ContextMenuView';
interface AvatarInfoWidgetRentableBotViewProps
{
avatarInfo: AvatarInfoRentableBot;
onClose: () => void;
}
const MODE_NORMAL = 0;
const MODE_CHANGE_NAME = 1;
const MODE_CHANGE_MOTTO = 2;
export const AvatarInfoWidgetRentableBotView: FC<AvatarInfoWidgetRentableBotViewProps> = props =>
{
const { avatarInfo = null, onClose = null } = props;
const [ mode, setMode ] = useState(MODE_NORMAL);
const [ newName, setNewName ] = useState('');
const [ newMotto, setNewMotto ] = useState('');
useMessageEvent<BotCommandConfigurationEvent>(BotCommandConfigurationEvent, event =>
{
const parser = event.getParser();
if(parser.botId !== avatarInfo.webID) return;
switch(parser.commandId)
{
case BotSkillsEnum.CHANGE_BOT_NAME:
setNewName(parser.data);
setMode(MODE_CHANGE_NAME);
return;
case BotSkillsEnum.CHANGE_BOT_MOTTO:
setNewMotto(parser.data);
setMode(MODE_CHANGE_MOTTO);
return;
case BotSkillsEnum.SETUP_CHAT: {
const data = parser.data;
const pieces = data.split(((data.indexOf(';#;') === -1) ? ';' : ';#;'));
if((pieces.length === 3) || (pieces.length === 4))
{
DispatchUiEvent(new RoomWidgetUpdateRentableBotChatEvent(
avatarInfo.roomIndex,
RoomObjectCategory.UNIT,
avatarInfo.webID,
pieces[0],
((pieces[1].toLowerCase() === 'true') || (pieces[1] === '1')),
parseInt(pieces[2]),
((pieces[3]) ? ((pieces[3].toLowerCase() === 'true') || (pieces[3] === '1')) : false)));
onClose();
}
return;
}
}
});
const requestBotCommandConfiguration = (skillType: number) => SendMessageComposer(new RequestBotCommandConfigurationComposer(avatarInfo.webID, skillType));
const processAction = (name: string) =>
{
let hideMenu = true;
if(name)
{
switch(name)
{
case 'donate_to_all':
requestBotCommandConfiguration(BotSkillsEnum.DONATE_TO_ALL);
SendMessageComposer(new BotSkillSaveComposer(avatarInfo.webID, BotSkillsEnum.DONATE_TO_ALL, ''));
break;
case 'donate_to_user':
requestBotCommandConfiguration(BotSkillsEnum.DONATE_TO_USER);
SendMessageComposer(new BotSkillSaveComposer(avatarInfo.webID, BotSkillsEnum.DONATE_TO_USER, ''));
break;
case 'change_bot_name':
requestBotCommandConfiguration(BotSkillsEnum.CHANGE_BOT_NAME);
hideMenu = false;
break;
case 'save_bot_name':
SendMessageComposer(new BotSkillSaveComposer(avatarInfo.webID, BotSkillsEnum.CHANGE_BOT_NAME, newName));
break;
case 'change_bot_motto':
requestBotCommandConfiguration(BotSkillsEnum.CHANGE_BOT_MOTTO);
hideMenu = false;
break;
case 'save_bot_motto':
SendMessageComposer(new BotSkillSaveComposer(avatarInfo.webID, BotSkillsEnum.CHANGE_BOT_MOTTO, newMotto));
break;
case 'dress_up':
SendMessageComposer(new BotSkillSaveComposer(avatarInfo.webID, BotSkillsEnum.DRESS_UP, ''));
break;
case 'random_walk':
SendMessageComposer(new BotSkillSaveComposer(avatarInfo.webID, BotSkillsEnum.RANDOM_WALK, ''));
break;
case 'setup_chat':
requestBotCommandConfiguration(BotSkillsEnum.SETUP_CHAT);
hideMenu = false;
break;
case 'dance':
SendMessageComposer(new BotSkillSaveComposer(avatarInfo.webID, BotSkillsEnum.DANCE, ''));
break;
case 'nux_take_tour':
CreateLinkEvent('help/tour');
SendMessageComposer(new BotSkillSaveComposer(avatarInfo.webID, BotSkillsEnum.NUX_TAKE_TOUR, ''));
break;
case 'pick':
SendMessageComposer(new BotRemoveComposer(avatarInfo.webID));
break;
default:
break;
}
}
if(hideMenu) onClose();
};
useEffect(() =>
{
setMode(MODE_NORMAL);
}, [ avatarInfo ]);
const canControl = (avatarInfo.amIOwner || avatarInfo.amIAnyRoomController);
return (
<ContextMenuView category={ RoomObjectCategory.UNIT } collapsable={ true } objectId={ avatarInfo.roomIndex } userType={ RoomObjectType.RENTABLE_BOT } onClose={ onClose }>
<ContextMenuHeaderView>
{ avatarInfo.name }
</ContextMenuHeaderView>
{ (mode === MODE_NORMAL) && canControl &&
<>
{ (avatarInfo.botSkills.indexOf(BotSkillsEnum.DONATE_TO_ALL) >= 0) &&
<ContextMenuListItemView onClick={ event => processAction('donate_to_all') }>
{ LocalizeText('avatar.widget.donate_to_all') }
</ContextMenuListItemView> }
{ (avatarInfo.botSkills.indexOf(BotSkillsEnum.DONATE_TO_USER) >= 0) &&
<ContextMenuListItemView onClick={ event => processAction('donate_to_user') }>
{ LocalizeText('avatar.widget.donate_to_user') }
</ContextMenuListItemView> }
{ (avatarInfo.botSkills.indexOf(BotSkillsEnum.CHANGE_BOT_NAME) >= 0) &&
<ContextMenuListItemView onClick={ event => processAction('change_bot_name') }>
{ LocalizeText('avatar.widget.change_bot_name') }
</ContextMenuListItemView> }
{ (avatarInfo.botSkills.indexOf(BotSkillsEnum.CHANGE_BOT_MOTTO) >= 0) &&
<ContextMenuListItemView onClick={ event => processAction('change_bot_motto') }>
{ LocalizeText('avatar.widget.change_bot_motto') }
</ContextMenuListItemView> }
{ (avatarInfo.botSkills.indexOf(BotSkillsEnum.DRESS_UP) >= 0) &&
<ContextMenuListItemView onClick={ event => processAction('dress_up') }>
{ LocalizeText('avatar.widget.dress_up') }
</ContextMenuListItemView> }
{ (avatarInfo.botSkills.indexOf(BotSkillsEnum.RANDOM_WALK) >= 0) &&
<ContextMenuListItemView onClick={ event => processAction('random_walk') }>
{ LocalizeText('avatar.widget.random_walk') }
</ContextMenuListItemView> }
{ (avatarInfo.botSkills.indexOf(BotSkillsEnum.SETUP_CHAT) >= 0) &&
<ContextMenuListItemView onClick={ event => processAction('setup_chat') }>
{ LocalizeText('avatar.widget.setup_chat') }
</ContextMenuListItemView> }
{ (avatarInfo.botSkills.indexOf(BotSkillsEnum.DANCE) >= 0) &&
<ContextMenuListItemView onClick={ event => processAction('dance') }>
{ LocalizeText('avatar.widget.dance') }
</ContextMenuListItemView> }
{ (avatarInfo.botSkills.indexOf(BotSkillsEnum.NO_PICK_UP) === -1) &&
<ContextMenuListItemView onClick={ event => processAction('pick') }>
{ LocalizeText('avatar.widget.pick_up') }
</ContextMenuListItemView> }
</> }
{ (mode === MODE_CHANGE_NAME) &&
<Column className="menu-item" gap={ 1 } onClick={ null }>
<Text variant="white">{ LocalizeText('bot.skill.name.configuration.new.name') }</Text>
<NitroInput maxLength={ GetConfigurationValue<number>('bot.name.max.length', 15) } type="text" value={ newName } onChange={ event => setNewName(event.target.value) } />
<div className="flex items-center justify-between gap-1">
<Button fullWidth variant="secondary" onClick={ event => processAction(null) }>{ LocalizeText('cancel') }</Button>
<Button fullWidth variant="success" onClick={ event => processAction('save_bot_name') }>{ LocalizeText('save') }</Button>
</div>
</Column> }
{ (mode === MODE_CHANGE_MOTTO) &&
<Column className="menu-item" gap={ 1 } onClick={ null }>
<Text variant="white">{ LocalizeText('bot.skill.name.configuration.new.motto') }</Text>
<NitroInput maxLength={ GetConfigurationValue<number>('motto.max.length', 38) } type="text" value={ newMotto } onChange={ event => setNewMotto(event.target.value) } />
<div className="flex items-center justify-between gap-1">
<Button fullWidth variant="secondary" onClick={ event => processAction(null) }>{ LocalizeText('cancel') }</Button>
<Button fullWidth variant="success" onClick={ event => processAction('save_bot_motto') }>{ LocalizeText('save') }</Button>
</div>
</Column> }
</ContextMenuView>
);
};
@@ -0,0 +1,85 @@
import { FC, MouseEvent, useEffect, useState } from 'react';
import { ArrowContainer, Popover } from 'react-tiny-popover';
import { Flex, Grid, NitroCardContentView } from '../../../../common';
interface ChatInputStyleSelectorViewProps
{
chatStyleId: number;
chatStyleIds: number[];
selectChatStyleId: (styleId: number) => void;
}
export const ChatInputStyleSelectorView: FC<ChatInputStyleSelectorViewProps> = props =>
{
const { chatStyleId = 0, chatStyleIds = null, selectChatStyleId = null } = props;
const [ target, setTarget ] = useState<(EventTarget & HTMLElement)>(null);
const [ selectorVisible, setSelectorVisible ] = useState(false);
const selectStyle = (styleId: number) =>
{
selectChatStyleId(styleId);
setSelectorVisible(false);
};
const toggleSelector = (event: MouseEvent<HTMLElement>) =>
{
let visible = false;
setSelectorVisible(prevValue =>
{
visible = !prevValue;
return visible;
});
if(visible) setTarget((event.target as (EventTarget & HTMLElement)));
};
useEffect(() =>
{
if(selectorVisible) return;
setTarget(null);
}, [ selectorVisible ]);
return (
<>
<Popover
containerClassName="max-w-[276px] not-italic font-normal leading-normal text-left no-underline [text-shadow:none] normal-case tracking-[normal] [word-break:normal] [word-spacing:normal] whitespace-normal text-[.7875rem] [word-wrap:break-word] bg-[#dfdfdf] bg-clip-padding border-[1px] border-[solid] border-[#283F5D] rounded-[.25rem] [box-shadow:0_2px_#00000073] z-[1070]"
content={ ({ position, childRect, popoverRect }) => (
<ArrowContainer // if you'd like an arrow, you can import the ArrowContainer!
arrowColor={ 'black' }
arrowSize={ 7 }
arrowStyle={ { bottom: 'calc(-.5rem - 1px)' } }
childRect={ childRect }
popoverRect={ popoverRect }
position={ position }
>
<NitroCardContentView className="bg-transparent !max-h-[200px]" overflow="hidden">
<Grid columnCount={ 3 } overflow="auto">
{ chatStyleIds && (chatStyleIds.length > 0) && chatStyleIds.map((styleId) =>
{
return (
<Flex key={ styleId } center pointer className="h-[30px]" onClick={ event => selectStyle(styleId) }>
<div key={ styleId } className="bubble-container relative w-[50px]">
<div className={ `relative max-w-[350px] min-h-[26px] text-[14px] chat-bubble bubble-${ styleId }` }>&nbsp;</div>
</div>
</Flex>
);
}) }
</Grid>
</NitroCardContentView>
</ArrowContainer>
) }
isOpen={ selectorVisible }
positions={ [ 'top' ] }
>
<div className="cursor-pointer nitro-icon chatstyles-icon" onClick={ toggleSelector } />
</Popover>
</>
);
};
@@ -0,0 +1,248 @@
import { GetSessionDataManager, HabboClubLevelEnum, RoomControllerLevel } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { ChatMessageTypeEnum, GetClubMemberLevel, GetConfigurationValue, LocalizeText, RoomWidgetUpdateChatInputContentEvent } from '../../../../api';
import { Text } from '../../../../common';
import { useChatInputWidget, useRoom, useSessionInfo, useUiEvent } from '../../../../hooks';
import { ChatInputStyleSelectorView } from './ChatInputStyleSelectorView';
export const ChatInputView: FC<{}> = props =>
{
const [ chatValue, setChatValue ] = useState<string>('');
const { chatStyleId = 0, updateChatStyleId = null } = useSessionInfo();
const { selectedUsername = '', floodBlocked = false, floodBlockedSeconds = 0, setIsTyping = null, setIsIdle = null, sendChat = null } = useChatInputWidget();
const { roomSession = null } = useRoom();
const inputRef = useRef<HTMLInputElement>();
const chatModeIdWhisper = useMemo(() => LocalizeText('widgets.chatinput.mode.whisper'), []);
const chatModeIdShout = useMemo(() => LocalizeText('widgets.chatinput.mode.shout'), []);
const chatModeIdSpeak = useMemo(() => LocalizeText('widgets.chatinput.mode.speak'), []);
const maxChatLength = useMemo(() => GetConfigurationValue<number>('chat.input.maxlength', 100), []);
const anotherInputHasFocus = useCallback(() =>
{
const activeElement = document.activeElement;
if(!activeElement) return false;
if(inputRef && (inputRef.current === activeElement)) return false;
if(!(activeElement instanceof HTMLInputElement) && !(activeElement instanceof HTMLTextAreaElement)) return false;
return true;
}, [ inputRef ]);
const setInputFocus = useCallback(() =>
{
inputRef.current.focus();
inputRef.current.setSelectionRange((inputRef.current.value.length * 2), (inputRef.current.value.length * 2));
}, [ inputRef ]);
const checkSpecialKeywordForInput = useCallback(() =>
{
setChatValue(prevValue =>
{
if((prevValue !== chatModeIdWhisper) || !selectedUsername.length) return prevValue;
return (`${ prevValue } ${ selectedUsername }`);
});
}, [ selectedUsername, chatModeIdWhisper ]);
const sendChatValue = useCallback((value: string, shiftKey: boolean = false) =>
{
if(!value || (value === '')) return;
let chatType = (shiftKey ? ChatMessageTypeEnum.CHAT_SHOUT : ChatMessageTypeEnum.CHAT_DEFAULT);
let text = value;
const parts = text.split(' ');
let recipientName = '';
let append = '';
switch(parts[0])
{
case chatModeIdWhisper:
chatType = ChatMessageTypeEnum.CHAT_WHISPER;
recipientName = parts[1];
append = (chatModeIdWhisper + ' ' + recipientName + ' ');
parts.shift();
parts.shift();
break;
case chatModeIdShout:
chatType = ChatMessageTypeEnum.CHAT_SHOUT;
parts.shift();
break;
case chatModeIdSpeak:
chatType = ChatMessageTypeEnum.CHAT_DEFAULT;
parts.shift();
break;
}
text = parts.join(' ');
setIsTyping(false);
setIsIdle(false);
if(text.length <= maxChatLength)
{
if(/%CC%/g.test(encodeURIComponent(text)))
{
setChatValue('');
}
else
{
setChatValue('');
sendChat(text, chatType, recipientName, chatStyleId);
}
}
setChatValue(append);
}, [ chatModeIdWhisper, chatModeIdShout, chatModeIdSpeak, maxChatLength, chatStyleId, setIsTyping, setIsIdle, sendChat ]);
const updateChatInput = useCallback((value: string) =>
{
if(!value || !value.length)
{
setIsTyping(false);
}
else
{
setIsTyping(true);
setIsIdle(true);
}
setChatValue(value);
}, [ setIsTyping, setIsIdle ]);
const onKeyDownEvent = useCallback((event: KeyboardEvent) =>
{
if(floodBlocked || !inputRef.current || anotherInputHasFocus()) return;
if(document.activeElement !== inputRef.current) setInputFocus();
const value = (event.target as HTMLInputElement).value;
switch(event.key)
{
case ' ':
case 'Space':
checkSpecialKeywordForInput();
return;
case 'NumpadEnter':
case 'Enter':
sendChatValue(value, event.shiftKey);
return;
case 'Backspace':
if(value)
{
const parts = value.split(' ');
if((parts[0] === chatModeIdWhisper) && (parts.length === 3) && (parts[2] === ''))
{
setChatValue('');
}
}
return;
}
}, [ floodBlocked, inputRef, chatModeIdWhisper, anotherInputHasFocus, setInputFocus, checkSpecialKeywordForInput, sendChatValue ]);
useUiEvent<RoomWidgetUpdateChatInputContentEvent>(RoomWidgetUpdateChatInputContentEvent.CHAT_INPUT_CONTENT, event =>
{
switch(event.chatMode)
{
case RoomWidgetUpdateChatInputContentEvent.WHISPER: {
setChatValue(`${ chatModeIdWhisper } ${ event.userName } `);
return;
}
case RoomWidgetUpdateChatInputContentEvent.SHOUT:
return;
}
});
const chatStyleIds = useMemo(() =>
{
let styleIds: number[] = [];
const styles = GetConfigurationValue<{ styleId: number, minRank: number, isSystemStyle: boolean, isHcOnly: boolean, isAmbassadorOnly: boolean }[]>('chat.styles');
for(const style of styles)
{
if(!style) continue;
if(style.minRank > 0)
{
if(GetSessionDataManager().hasSecurity(style.minRank)) styleIds.push(style.styleId);
continue;
}
if(style.isSystemStyle)
{
if(GetSessionDataManager().hasSecurity(RoomControllerLevel.MODERATOR))
{
styleIds.push(style.styleId);
continue;
}
}
if(GetConfigurationValue<number[]>('chat.styles.disabled').indexOf(style.styleId) >= 0) continue;
if(style.isHcOnly && (GetClubMemberLevel() >= HabboClubLevelEnum.CLUB))
{
styleIds.push(style.styleId);
continue;
}
if(style.isAmbassadorOnly && GetSessionDataManager().isAmbassador)
{
styleIds.push(style.styleId);
continue;
}
if(!style.isHcOnly && !style.isAmbassadorOnly) styleIds.push(style.styleId);
}
return styleIds;
}, []);
useEffect(() =>
{
document.body.addEventListener('keydown', onKeyDownEvent);
return () =>
{
document.body.removeEventListener('keydown', onKeyDownEvent);
};
}, [ onKeyDownEvent ]);
useEffect(() =>
{
if(!inputRef.current) return;
inputRef.current.parentElement.dataset.value = chatValue;
}, [ chatValue ]);
if(!roomSession || roomSession.isSpectator) return null;
return (
createPortal(
<div className="nitro-chat-input-container flex justify-center items-center relative h-10 border-2 border-black bg-gray-200 pr-2.5 w-full overflow-hidden rounded-lg">
<div className="items-center input-sizer">
{ !floodBlocked &&
<input ref={ inputRef } className="[font-size:inherit] placeholder-[#6c757d] bg-transparent border-none focus:border-current focus:shadow-none focus:ring-0 " maxLength={ maxChatLength } placeholder={ LocalizeText('widgets.chatinput.default') } type="text" value={ chatValue } onChange={ event => updateChatInput(event.target.value) } onMouseDown={ event => setInputFocus() } /> }
{ floodBlocked &&
<Text variant="danger">{ LocalizeText('chat.input.alert.flood', [ 'time' ], [ floodBlockedSeconds.toString() ]) } </Text> }
</div>
<ChatInputStyleSelectorView chatStyleId={ chatStyleId } chatStyleIds={ chatStyleIds } selectChatStyleId={ updateChatStyleId } />
</div>, document.getElementById('toolbar-chat-input-container'))
);
};
@@ -0,0 +1,99 @@
import { GetRoomEngine, RoomChatSettings, RoomObjectCategory } from '@nitrots/nitro-renderer';
import { FC, useEffect, useMemo, useRef, useState } from 'react';
import { ChatBubbleMessage } from '../../../../api';
interface ChatWidgetMessageViewProps
{
chat: ChatBubbleMessage;
makeRoom: (chat: ChatBubbleMessage) => void;
bubbleWidth?: number;
}
export const ChatWidgetMessageView: FC<ChatWidgetMessageViewProps> = ({
chat = null,
makeRoom = null,
bubbleWidth = RoomChatSettings.CHAT_BUBBLE_WIDTH_NORMAL
}) =>
{
const [ isVisible, setIsVisible ] = useState(false);
const [ isReady, setIsReady ] = useState(false);
const elementRef = useRef<HTMLDivElement>(null);
const getBubbleWidth = useMemo(() =>
{
switch(bubbleWidth)
{
case RoomChatSettings.CHAT_BUBBLE_WIDTH_NORMAL:
return 'w-350';
case RoomChatSettings.CHAT_BUBBLE_WIDTH_THIN:
return 'w-240';
case RoomChatSettings.CHAT_BUBBLE_WIDTH_WIDE:
return 'w-2000';
default:
return 'w-350';
}
}, [ bubbleWidth ]);
useEffect(() =>
{
setIsVisible(false);
const element = elementRef.current;
if(!element) return;
const { offsetWidth: width, offsetHeight: height } = element;
chat.width = width;
chat.height = height;
chat.elementRef = element;
let { left, top } = chat;
if(!left && !top)
{
left = (chat.location.x - (width / 2));
top = (element.parentElement.offsetHeight - height);
chat.left = left;
chat.top = top;
}
setIsReady(true);
return () =>
{
chat.elementRef = null;
setIsReady(false);
};
}, [ chat ]);
useEffect(() =>
{
if(!isReady || !chat || isVisible) return;
if(makeRoom) makeRoom(chat);
setIsVisible(true);
}, [ chat, isReady, isVisible, makeRoom ]);
return (
<div ref={ elementRef } className={ `bubble-container newbubblehe ${ isVisible ? 'visible' : 'invisible' } w-max absolute select-none pointer-events-auto` }
onClick={ () => GetRoomEngine().selectRoomObject(chat.roomId, chat.senderId, RoomObjectCategory.UNIT) }>
{ chat.styleId === 0 && (
<div className="absolute top-[-1px] left-[1px] w-[30px] h-[calc(100%-0.5px)] rounded-[7px] z-[1]" style={ { backgroundColor: chat.color } } />
) }
<div className={ `chat-bubble bubble-${ chat.styleId } ${ getBubbleWidth } relative z-[1] break-words min-h-[26px] text-[14px] max-w-[350px]` }
style={ { maxWidth: getBubbleWidth } }>
<div className="user-container flex items-center justify-center h-full max-h-[24px] overflow-hidden">
{ chat.imageUrl && chat.imageUrl.length > 0 && (
<div className="user-image absolute top-[-15px] left-[-9.25px] w-[45px] h-[65px] bg-no-repeat bg-center scale-50" style={ { backgroundImage: `url(${ chat.imageUrl })` } } />
) }
</div>
<div className="chat-content py-[5px] px-[6px] ml-[27px] leading-[1] min-h-[25px]">
<b className="username" dangerouslySetInnerHTML={ { __html: `${ chat.username }: ` } } />
<span className="message" dangerouslySetInnerHTML={ { __html: `${ chat.formattedText }` } } />
</div>
<div className="pointer absolute left-[50%] translate-x-[-50%] w-[9px] h-[6px] bottom-[-5px]" />
</div>
</div>
);
};
@@ -0,0 +1,162 @@
import { RoomChatSettings } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useRef } from 'react';
import { ChatBubbleMessage, DoChatsOverlap, GetConfigurationValue } from '../../../../api';
import { useChatWidget } from '../../../../hooks';
import IntervalWebWorker from '../../../../workers/IntervalWebWorker';
import { WorkerBuilder } from '../../../../workers/WorkerBuilder';
import { ChatWidgetMessageView } from './ChatWidgetMessageView';
export const ChatWidgetView: FC<{}> = props =>
{
const { chatMessages = [], setChatMessages = null, chatSettings = null, getScrollSpeed = 6000 } = useChatWidget();
const elementRef = useRef<HTMLDivElement>();
const removeHiddenChats = useCallback(() =>
{
setChatMessages(prevValue =>
{
if(prevValue)
{
const newMessages = prevValue.filter(chat => ((chat.top > (-(chat.height) * 2))));
if(newMessages.length !== prevValue.length) return newMessages;
}
return prevValue;
});
}, [ setChatMessages ]);
const checkOverlappingChats = useCallback((chat: ChatBubbleMessage, moved: number, tempChats: ChatBubbleMessage[]) =>
{
for(let i = (chatMessages.indexOf(chat) - 1); i >= 0; i--)
{
const collides = chatMessages[i];
if(!collides || (chat === collides) || (tempChats.indexOf(collides) >= 0) || (((collides.top + collides.height) - moved) > (chat.top + chat.height))) continue;
if(DoChatsOverlap(chat, collides, -moved, 0))
{
const amount = Math.abs((collides.top + collides.height) - chat.top);
tempChats.push(collides);
collides.top -= amount;
collides.skipMovement = true;
checkOverlappingChats(collides, amount, tempChats);
}
}
}, [ chatMessages ]);
const makeRoom = useCallback((chat: ChatBubbleMessage) =>
{
if(chatSettings.mode === RoomChatSettings.CHAT_MODE_FREE_FLOW)
{
chat.skipMovement = true;
checkOverlappingChats(chat, 0, [ chat ]);
removeHiddenChats();
}
else
{
const lowestPoint = (chat.top + chat.height);
const requiredSpace = chat.height;
const spaceAvailable = (elementRef.current.offsetHeight - lowestPoint);
const amount = (requiredSpace - spaceAvailable);
if(spaceAvailable < requiredSpace)
{
setChatMessages(prevValue =>
{
prevValue.forEach(prevChat =>
{
if(prevChat === chat) return;
prevChat.top -= amount;
});
return prevValue;
});
removeHiddenChats();
}
}
}, [ chatSettings, checkOverlappingChats, removeHiddenChats, setChatMessages ]);
useEffect(() =>
{
const resize = (event: UIEvent = null) =>
{
if(!elementRef || !elementRef.current) return;
const currentHeight = elementRef.current.offsetHeight;
const newHeight = Math.round(document.body.offsetHeight * GetConfigurationValue<number>('chat.viewer.height.percentage'));
elementRef.current.style.height = `${ newHeight }px`;
setChatMessages(prevValue =>
{
if(prevValue)
{
prevValue.forEach(chat => (chat.top -= (currentHeight - newHeight)));
}
return prevValue;
});
};
window.addEventListener('resize', resize);
resize();
return () =>
{
window.removeEventListener('resize', resize);
};
}, [ setChatMessages ]);
useEffect(() =>
{
const moveAllChatsUp = (amount: number) =>
{
setChatMessages(prevValue =>
{
prevValue.forEach(chat =>
{
if(chat.skipMovement)
{
chat.skipMovement = false;
return;
}
chat.top -= amount;
});
return prevValue;
});
removeHiddenChats();
};
const worker = new WorkerBuilder(IntervalWebWorker);
worker.onmessage = () => moveAllChatsUp(15);
worker.postMessage({ action: 'START', content: getScrollSpeed });
return () =>
{
worker.postMessage({ action: 'STOP' });
worker.terminate();
};
}, [ getScrollSpeed, removeHiddenChats, setChatMessages ]);
return (
<div ref={ elementRef } className="absolute flex justify-center items-center w-full top-0 min-h-[1px] z-[var(--chat-zindex)] bg-transparent roundehidden shadow-none pointer-events-none">
{ chatMessages.map(chat => <ChatWidgetMessageView key={ chat.id } bubbleWidth={ chatSettings.weight } chat={ chat } makeRoom={ makeRoom } />) }
</div>
);
};
@@ -0,0 +1,94 @@
import { GetSessionDataManager, FurniturePickupAllComposer } from '@nitrots/nitro-renderer';
import { FC, useEffect, useMemo, useState } from 'react';
import { LocalizeText, RoomObjectItem, SendMessageComposer } from '../../../../api';
import { Button, Flex, InfiniteScroll, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
import { NitroInput, classNames } from '../../../../layout';
const LIMIT_FURNI_PICKALL = 100;
interface ChooserWidgetViewProps {
title: string;
items: RoomObjectItem[];
selectItem: (item: RoomObjectItem) => void;
onClose: () => void;
pickallFurni?: boolean;
}
export const ChooserWidgetView: FC<ChooserWidgetViewProps> = props => {
const { title = null, items = [], selectItem = null, onClose = null, pickallFurni = false } = props;
const [ selectedItem, setSelectedItem ] = useState<RoomObjectItem>(null);
const [ searchValue, setSearchValue ] = useState('');
const [ checkAll, setCheckAll ] = useState(false);
const [ checkedIds, setCheckedIds ] = useState<number[]>([]);
const canSeeId = GetSessionDataManager().isModerator;
const checkedId = (id?: number) => {
if (id) {
if (isChecked(id))
setCheckedIds(checkedIds.filter(x => x !== id));
else if (checkedIds.length < LIMIT_FURNI_PICKALL)
setCheckedIds([ ...checkedIds, id ]);
} else {
setCheckAll(value => !value);
if (!checkAll) {
const itemIds = filteredItems.map(x => x.id).slice(0, LIMIT_FURNI_PICKALL);
setCheckedIds(itemIds);
} else {
setCheckedIds([]);
}
}
}
const isChecked = (id: number) => checkedIds.includes(id);
const onClickPickAll = () => {
SendMessageComposer(new FurniturePickupAllComposer(...checkedIds));
setCheckedIds([]);
setCheckAll(false);
}
const filteredItems = useMemo(() => {
const value = searchValue.toLocaleLowerCase();
const itemsFilter = items.filter(item => item.name?.toLocaleLowerCase().includes(value));
return itemsFilter.sort((a, b) => a.name.localeCompare(b.name));
}, [ items, searchValue ]);
useEffect(() => {
if (!selectedItem) return;
selectItem(selectedItem);
}, [ selectedItem, selectItem ]);
return (
<NitroCardView className="w-[200px] h-[200px]" theme="primary-slim">
<NitroCardHeaderView headerText={ title + (pickallFurni ? ` (${filteredItems.length})` : '') } onCloseClick={ onClose } />
<NitroCardContentView gap={ 2 } overflow="hidden">
<NitroInput placeholder={ LocalizeText('generic.search') } type="text" value={ searchValue } onChange={ event => setSearchValue(event.target.value) } />
{ pickallFurni && (
<Flex gap={ 2 }>
<input className="form-check-input" type="checkbox" checked={ checkAll } onChange={ () => checkedId() } />
<Text>{ LocalizeText('widget.chooser.checkall') }</Text>
</Flex>
)}
<InfiniteScroll rowRender={ row => (
<Flex pointer alignItems="center" className={ classNames('rounded p-1', (selectedItem === row) && 'bg-muted') } onClick={ () => setSelectedItem(row) }>
{ pickallFurni && (
<input
className="flex-shrink-0 mx-1 form-check-input"
type="checkbox"
checked={ isChecked(row.id) }
onChange={ () => checkedId(row.id) }
onClick={ e => e.stopPropagation() }
/>
)}
<Text truncate>{ row.name } { canSeeId && (' - ' + row.id) }</Text>
</Flex>
)} rows={ filteredItems } />
{ pickallFurni && (
<Button variant="secondary" onClick={ onClickPickAll } disabled={ !checkedIds.length }>
{ LocalizeText('widget.chooser.btn.pickall') }
</Button>
)}
</NitroCardContentView>
</NitroCardView>
);
};
@@ -0,0 +1,30 @@
import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer';
import { FC, useEffect } from 'react';
import { LocalizeText } from '../../../../api';
import { useFurniChooserWidget, useRoom } from '../../../../hooks';
import { ChooserWidgetView } from './ChooserWidgetView';
export const FurniChooserWidgetView: FC<{}> = props => {
const { items = null, onClose = null, selectItem = null, populateChooser = null } = useFurniChooserWidget();
const { roomSession = null } = useRoom();
useEffect(() => {
const linkTracker: ILinkEventTracker = {
linkReceived: (url: string) => {
const parts = url.split('/');
populateChooser();
},
eventUrlPrefix: 'furni-chooser/'
};
AddLinkEventTracker(linkTracker);
return () => RemoveLinkEventTracker(linkTracker);
}, [ populateChooser ]);
if (!items) return null;
return (
<ChooserWidgetView className="w-[200px] h-[200px]" items={ items } selectItem={ selectItem } title={ LocalizeText('widget.chooser.furni.title') } onClose={ onClose } pickallFurni={ roomSession?.isRoomOwner } />
);
};
@@ -0,0 +1,31 @@
import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer';
import { FC, useEffect } from 'react';
import { LocalizeText } from '../../../../api';
import { useUserChooserWidget } from '../../../../hooks';
import { ChooserWidgetView } from './ChooserWidgetView';
export const UserChooserWidgetView: FC<{}> = props =>
{
const { items = null, onClose = null, selectItem = null, populateChooser = null } = useUserChooserWidget();
useEffect(() =>
{
const linkTracker: ILinkEventTracker = {
linkReceived: (url: string) =>
{
const parts = url.split('/');
populateChooser();
},
eventUrlPrefix: 'user-chooser/'
};
AddLinkEventTracker(linkTracker);
return () => RemoveLinkEventTracker(linkTracker);
}, [ populateChooser ]);
if(!items) return null;
return <ChooserWidgetView items={ items } selectItem={ selectItem } title={ LocalizeText('widget.chooser.user.title') } onClose={ onClose } />;
};
@@ -0,0 +1,26 @@
import { FC, useMemo } from 'react';
import { FaCaretDown, FaCaretUp } from 'react-icons/fa';
import { Flex, FlexProps } from '../../../../common';
interface CaretViewProps extends FlexProps
{
collapsed?: boolean;
}
export const ContextMenuCaretView: FC<CaretViewProps> = props =>
{
const { justifyContent = 'center', alignItems = 'center', classNames = [], collapsed = true, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'menu-footer' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
return <Flex alignItems={ alignItems } classNames={ getClassNames } justifyContent={ justifyContent } { ...rest }>
{ !collapsed && <FaCaretDown className="fa-icon align-self-center" /> }
{ collapsed && <FaCaretUp className="fa-icon align-self-center" /> }
</Flex>;
};
@@ -0,0 +1,18 @@
import { FC, useMemo } from 'react';
import { Flex, FlexProps } from '../../../../common';
export const ContextMenuHeaderView: FC<FlexProps> = props =>
{
const { justifyContent = 'center', alignItems = 'center', classNames = [], ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'bg-[#3d5f6e] text-[#fff] min-w-[117px] h-[25px] max-h-[25px] text-[16px] mb-[2px]', 'p-1' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
return <Flex alignItems={ alignItems } classNames={ getClassNames } justifyContent={ justifyContent } { ...rest } />;
};
@@ -0,0 +1,32 @@
import { FC, MouseEvent, useMemo } from 'react';
import { Flex, FlexProps } from '../../../../common';
interface ContextMenuListItemViewProps extends FlexProps
{
disabled?: boolean;
}
export const ContextMenuListItemView: FC<ContextMenuListItemViewProps> = props =>
{
const { disabled = false, fullWidth = true, justifyContent = 'center', alignItems = 'center', classNames = [], onClick = null, ...rest } = props;
const handleClick = (event: MouseEvent<HTMLDivElement>) =>
{
if(disabled) return;
if(onClick) onClick(event);
};
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'relative mb-[2px] p-[3px] overflow-hidden', 'h-[24px] max-h-[24px] p-[3px] bg-[repeating-linear-gradient(#131e25,_#131e25_50%,_#0d171d_50%,_#0d171d_100%)] cursor-pointer' ];
if(disabled) newClassNames.push('disabled');
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ disabled, classNames ]);
return <Flex alignItems={ alignItems } classNames={ getClassNames } fullWidth={ fullWidth } justifyContent={ justifyContent } onClick={ handleClick } { ...rest } />;
};
@@ -0,0 +1,18 @@
import { FC, useMemo } from 'react';
import { Column, ColumnProps } from '../../../../common';
export const ContextMenuListView: FC<ColumnProps> = props =>
{
const { classNames = [], ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'menu-list' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
return <Column classNames={ getClassNames } { ...rest } />;
};
@@ -0,0 +1,144 @@
import { GetStage, GetTicker, NitroRectangle, NitroTicker, RoomObjectType } from '@nitrots/nitro-renderer';
import { CSSProperties, FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { FixedSizeStack, GetRoomObjectBounds, GetRoomObjectScreenLocation, GetRoomSession } from '../../../../api';
import { BaseProps } from '../../../../common';
import { ContextMenuCaretView } from './ContextMenuCaretView';
interface ContextMenuViewProps extends BaseProps<HTMLDivElement> {
objectId: number;
category: number;
userType?: number;
fades?: boolean;
onClose: () => void;
collapsable?: boolean;
}
const LOCATION_STACK_SIZE = 25;
const BUBBLE_DROP_SPEED = 3;
const FADE_DELAY = 5000;
const FADE_LENGTH = 75;
const SPACE_AROUND_EDGES = 10;
export const ContextMenuView: FC<ContextMenuViewProps> = ({
objectId = -1,
category = -1,
userType = -1,
fades = false,
onClose,
classNames = [],
style = {},
children = null,
collapsable = false,
...rest
}) => {
const [pos, setPos] = useState<{ x: number; y: number }>({ x: null, y: null });
const [opacity, setOpacity] = useState(1);
const [isFading, setIsFading] = useState(false);
const [isCollapsed, setIsCollapsed] = useState(false);
const elementRef = useRef<HTMLDivElement>(null);
const stackRef = useRef<FixedSizeStack>(new FixedSizeStack(LOCATION_STACK_SIZE));
const maxStackRef = useRef(-1000000);
const updatePosition = useCallback(
(bounds: NitroRectangle, location: { x: number; y: number }) => {
if (!bounds || !location || !elementRef.current) return;
let offset = -elementRef.current.offsetHeight;
if (userType > -1 && [RoomObjectType.USER, RoomObjectType.BOT, RoomObjectType.RENTABLE_BOT].includes(userType)) {
offset += bounds.height > 50 ? 15 : 0;
} else {
offset -= 14;
}
stackRef.current.addValue(location.y - bounds.top);
let maxStack = stackRef.current.getMax();
if (maxStack < maxStackRef.current - BUBBLE_DROP_SPEED) {
maxStack = maxStackRef.current - BUBBLE_DROP_SPEED;
}
maxStackRef.current = maxStack;
const deltaY = location.y - maxStack;
let x = Math.round(location.x - elementRef.current.offsetWidth / 2);
let y = Math.round(deltaY + offset);
const stage = GetStage();
const maxLeft = stage.width - elementRef.current.offsetWidth - SPACE_AROUND_EDGES;
const maxTop = stage.height - elementRef.current.offsetHeight - SPACE_AROUND_EDGES;
x = Math.max(SPACE_AROUND_EDGES, Math.min(x, maxLeft));
y = Math.max(SPACE_AROUND_EDGES, Math.min(y, maxTop));
setPos({ x, y });
},
[userType]
);
const getClassNames = useMemo(() => {
const classes = [
'!p-[2px]',
'bg-[#1c323f]',
'border-[2px]',
'border-[solid]',
'border-[rgba(255,255,255,.5)]',
'rounded-[.25rem]',
'text-[.7875rem]',
'text-white',
'z-40',
'pointer-events-auto',
'absolute',
pos.x !== null ? 'visible' : 'invisible',
];
if (isCollapsed) classes.push('menu-hidden');
return [...classes, ...classNames];
}, [pos.x, isCollapsed, classNames]);
const getStyle = useMemo(
() => ({
left: pos.x ?? 0,
top: pos.y ?? 0,
transition: isFading ? 'opacity 75ms linear' : undefined,
opacity,
...style,
}),
[pos, opacity, isFading, style]
);
useEffect(() => {
if (!elementRef.current) return;
const update = () => {
if (!elementRef.current) return;
const bounds = GetRoomObjectBounds(GetRoomSession().roomId, objectId, category);
const location = GetRoomObjectScreenLocation(GetRoomSession().roomId, objectId, category);
updatePosition(bounds, location);
};
const ticker = GetTicker();
ticker.add(update);
return () => ticker.remove(update);
}, [objectId, category, updatePosition]);
useEffect(() => {
if (!fades) return;
const timeout = setTimeout(() => {
setIsFading(true);
setTimeout(onClose, FADE_LENGTH);
}, FADE_DELAY);
return () => clearTimeout(timeout);
}, [fades, onClose]);
useEffect(() => {
if (!isFading) return;
setOpacity(0);
}, [isFading]);
return (
<div ref={elementRef} className={getClassNames.join(' ')} style={getStyle} {...rest}>
{!(collapsable && isCollapsed) && children}
{collapsable && <ContextMenuCaretView collapsed={isCollapsed} onClick={() => setIsCollapsed((prev) => !prev)} />}
</div>
);
};
@@ -0,0 +1,51 @@
import { FC, useEffect, useState } from 'react';
import { LocalizeText } from '../../../../api';
import { Button, Column, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common';
import { useDoorbellWidget } from '../../../../hooks';
export const DoorbellWidgetView: FC<{}> = props =>
{
const [ isVisible, setIsVisible ] = useState(false);
const { users = [], answer = null } = useDoorbellWidget();
useEffect(() =>
{
setIsVisible(!!users.length);
}, [ users ]);
if(!isVisible) return null;
return (
<NitroCardView className="nitro-widget-doorbell" theme="primary-slim">
<NitroCardHeaderView headerText={ LocalizeText('navigator.doorbell.title') } onCloseClick={ event => setIsVisible(false) } />
<NitroCardContentView gap={ 0 } overflow="hidden">
<Column gap={ 2 }>
<Grid className="text-black font-bold border-bottom px-1 pb-1" gap={ 1 }>
<div className="col-span-6">{ LocalizeText('generic.username') }</div>
<div className="col-span-6" />
</Grid>
</Column>
<Column className="striped-children" gap={ 0 } overflow="auto">
{ users && (users.length > 0) && users.map(userName =>
{
return (
<Grid key={ userName } alignItems="center" className="text-black border-bottom p-1" gap={ 1 }>
<div className="col-span-6">{ userName }</div>
<div className="col-span-6">
<div className="flex items-center gap-1 justify-end">
<Button variant="success" onClick={ () => answer(userName, true) }>
{ LocalizeText('generic.accept') }
</Button>
<Button variant="danger" onClick={ () => answer(userName, false) }>
{ LocalizeText('generic.deny') }
</Button>
</div>
</div>
</Grid>
);
}) }
</Column>
</NitroCardContentView>
</NitroCardView>
);
};
@@ -0,0 +1,28 @@
import { RoomObjectCategory } from '@nitrots/nitro-renderer';
import { FC } from 'react';
import { FaTimes } from 'react-icons/fa';
import { LocalizeText, MessengerRequest } from '../../../../api';
import { Button, Text } from '../../../../common';
import { ObjectLocationView } from '../object-location/ObjectLocationView';
export const FriendRequestDialogView: FC<{ roomIndex: number, request: MessengerRequest, hideFriendRequest: (userId: number) => void, requestResponse: (requestId: number, flag: boolean) => void }> = props =>
{
const { roomIndex = -1, request = null, hideFriendRequest = null, requestResponse = null } = props;
return (
<ObjectLocationView category={ RoomObjectCategory.UNIT } objectId={ roomIndex }>
<div className="nitro-friend-request-dialog nitro-context-menu p-2">
<div className="flex flex-col">
<div className="flex items-center gap-2 justify-between">
<Text fontSize={ 6 } variant="white">{ LocalizeText('widget.friendrequest.from', [ 'username' ], [ request.name ]) }</Text>
<FaTimes className="cursor-pointer fa-icon" onClick={ event => hideFriendRequest(request.requesterUserId) } />
</div>
<div className="flex justify-end gap-1">
<Button variant="danger" onClick={ event => requestResponse(request.requesterUserId, false) }>{ LocalizeText('widget.friendrequest.decline') }</Button>
<Button variant="success" onClick={ event => requestResponse(request.requesterUserId, true) }>{ LocalizeText('widget.friendrequest.accept') }</Button>
</div>
</div>
</div>
</ObjectLocationView>
);
};
@@ -0,0 +1,17 @@
import { FC } from 'react';
import { useFriendRequestWidget, useFriends } from '../../../../hooks';
import { FriendRequestDialogView } from './FriendRequestDialogView';
export const FriendRequestWidgetView: FC<{}> = props =>
{
const { displayedRequests = [], hideFriendRequest = null } = useFriendRequestWidget();
const { requestResponse = null } = useFriends();
if(!displayedRequests.length) return null;
return (
<>
{ displayedRequests.map((request, index) => <FriendRequestDialogView key={ index } hideFriendRequest={ hideFriendRequest } request={ request.request } requestResponse={ requestResponse } roomIndex={ request.roomIndex } />) }
</>
);
};
@@ -0,0 +1,58 @@
import { GetRoomEngine } from '@nitrots/nitro-renderer';
import { FC } from 'react';
import { LocalizeText } from '../../../../api';
import { Button, Column, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
import { useFurnitureAreaHideWidget } from '../../../../hooks';
export const FurnitureAreaHideView: FC<{}> = props =>
{
const { objectId = -1, isOn, setIsOn, wallItems, setWallItems, inverted, setInverted, invisibility, setInvisibility, onClose = null } = useFurnitureAreaHideWidget();
if(objectId === -1) return null;
return (
<NitroCardView theme="primary-slim" className="nitro-room-widget-area-hide" style={ { maxWidth: '400px' }}>
<NitroCardHeaderView headerText={ LocalizeText('widget.areahide.title') } onCloseClick={ onClose } />
<NitroCardContentView overflow="hidden" justifyContent="between">
<Column gap={ 2 }>
<Column gap={ 1 }>
<Text bold>{ LocalizeText('wiredfurni.params.area_selection') }</Text>
<Text bold>{ LocalizeText('wiredfurni.params.area_selection.info') }</Text>
</Column>
<Flex gap={ 1 }>
<Button fullWidth variant="primary" onClick={ event => GetRoomEngine().areaSelectionManager.startSelecting() }>
{ LocalizeText('wiredfurni.params.area_selection.select') }
</Button>
<Button fullWidth variant="primary" onClick={ event => GetRoomEngine().areaSelectionManager.clearHighlight() }>
{ LocalizeText('wiredfurni.params.area_selection.clear') }
</Button>
</Flex>
</Column>
<Column gap={ 1 }>
<Text bold>{ LocalizeText('widget.areahide.options') }</Text>
<Flex gap={ 1 }>
<input className="form-check-input" type="checkbox" id="setWallItems" checked={ wallItems } onChange={ event => setWallItems(event.target.checked ? true : false) } />
<Text>{ LocalizeText('widget.areahide.options.wallitems') }</Text>
</Flex>
<Flex gap={ 1 }>
<input className="form-check-input" type="checkbox" id="setInverted" checked={ inverted } onChange={ event => setInverted(event.target.checked ? true : false) } />
<Column gap={ 1 }>
<Text>{ LocalizeText('widget.areahide.options.invert') }</Text>
<Text>{ LocalizeText('widget.areahide.options.invert.info') }</Text>
</Column>
</Flex>
<Flex gap={ 1 }>
<input className="form-check-input" type="checkbox" id="setInvisibility" checked={ invisibility } onChange={ event => setInvisibility(event.target.checked ? true : false) } />
<Column gap={ 1 }>
<Text>{ LocalizeText('widget.areahide.options.invisibility') }</Text>
<Text>{ LocalizeText('widget.areahide.options.invisibility.info') }</Text>
</Column>
</Flex>
</Column>
<Button fullWidth variant="primary">
{ LocalizeText(isOn ? 'widget.dimmer.button.off' : 'widget.dimmer.button.on') }
</Button>
</NitroCardContentView>
</NitroCardView>
);
};
@@ -0,0 +1,30 @@
import { FC } from 'react';
import { ColorUtils, LocalizeText } from '../../../../api';
import { Button, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common';
import { useFurnitureBackgroundColorWidget } from '../../../../hooks';
export const FurnitureBackgroundColorView: FC<{}> = props =>
{
const { objectId = -1, color = 0, setColor = null, applyToner = null, toggleToner = null, onClose = null } = useFurnitureBackgroundColorWidget();
if(objectId === -1) return null;
return (
<NitroCardView className="nitro-room-widget-toner" theme="primary-slim">
<NitroCardHeaderView headerText={ LocalizeText('widget.backgroundcolor.title') } onCloseClick={ onClose } />
<NitroCardContentView justifyContent="between" overflow="hidden">
<div className="flex flex-col gap-1 overflow-auto">
<input className="min-h-[calc(1.5em+ .5rem+2px)] px-[.5rem] py-[.25rem] rounded-[.2rem]" type="color" value={ ColorUtils.makeColorNumberHex(color) } onChange={ event => setColor(ColorUtils.convertFromHex(event.target.value)) } />
</div>
<div className="flex flex-col gap-1">
<Button fullWidth variant="primary" onClick={ toggleToner }>
{ LocalizeText('widget.backgroundcolor.button.on') }
</Button>
<Button fullWidth variant="primary" onClick={ applyToner }>
{ LocalizeText('widget.backgroundcolor.button.apply') }
</Button>
</div>
</NitroCardContentView>
</NitroCardView>
);
};
@@ -0,0 +1,12 @@
import { FC } from 'react';
import { LayoutTrophyView } from '../../../../common';
import { useFurnitureBadgeDisplayWidget } from '../../../../hooks';
export const FurnitureBadgeDisplayView: FC<{}> = props =>
{
const { objectId = -1, color = '1', badgeName = '', badgeDesc = '', date = '', senderName = '', onClose = null } = useFurnitureBadgeDisplayWidget();
if(objectId === -1) return null;
return <LayoutTrophyView color={ color } customTitle={ badgeName } date={ date } message={ badgeDesc } senderName={ senderName } onCloseClick={ onClose } />;
};
@@ -0,0 +1,115 @@
import { GetRoomEngine, RoomObjectCategory } from '@nitrots/nitro-renderer';
import { FC, ReactElement, useEffect, useMemo, useState } from 'react';
import { IsOwnerOfFurniture, LocalizeText } from '../../../../api';
import { AutoGrid, Button, Column, LayoutGridItem, LayoutLoadingSpinnerView, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common';
import { useFurnitureCraftingWidget, useRoom } from '../../../../hooks';
export const FurnitureCraftingView: FC<{}> = props =>
{
const { objectId = -1, recipes = [], ingredients = [], selectedRecipe = null, requiredIngredients = null, isCrafting = false, craft = null, selectRecipe = null, onClose = null } = useFurnitureCraftingWidget();
const { roomSession = null } = useRoom();
const [ waitingToConfirm, setWaitingToConfirm ] = useState(false);
const isOwner = useMemo(() =>
{
const roomObject = GetRoomEngine().getRoomObject(roomSession.roomId, objectId, RoomObjectCategory.FLOOR);
return IsOwnerOfFurniture(roomObject);
}, [ objectId, roomSession ]);
const canCraft = useMemo(() =>
{
if(!requiredIngredients || !requiredIngredients.length) return false;
for(const ingredient of requiredIngredients)
{
const ingredientData = ingredients.find(data => (data.name === ingredient.itemName));
if(!ingredientData || ingredientData.count < ingredient.count) return false;
}
return true;
}, [ ingredients, requiredIngredients ]);
const tryCraft = () =>
{
if(!waitingToConfirm)
{
setWaitingToConfirm(true);
return;
}
craft();
setWaitingToConfirm(false);
};
useEffect(() =>
{
setWaitingToConfirm(false);
}, [ selectedRecipe ]);
if(objectId === -1) return null;
return (
<NitroCardView className="nitro-widget-crafting" theme="primary-slim">
<NitroCardHeaderView headerText={ LocalizeText('crafting.title') } onCloseClick={ onClose } />
<NitroCardContentView>
<div className="flex !flex-grow gap-2 overflow-hidden">
<div className="flex flex-col w-full gap-2">
<Column fullHeight overflow="hidden">
<div className="bg-muted rounded py-1 text-center">{ LocalizeText('crafting.title.products') }</div>
<AutoGrid columnCount={ 5 }>
{ (recipes.length > 0) && recipes.map((item) => <LayoutGridItem key={ item.name } itemActive={ selectedRecipe && selectedRecipe.name === item.name } itemImage={ item.iconUrl } onClick={ () => selectRecipe(item) } />) }
</AutoGrid>
</Column>
<Column fullHeight overflow="hidden">
<div className="bg-muted rounded py-1 text-center">{ LocalizeText('crafting.title.mixer') }</div>
<AutoGrid columnCount={ 5 }>
{ (ingredients.length > 0) && ingredients.map((item) => <LayoutGridItem key={ item.name } className={ (!item.count ? 'opacity-0-5 ' : '') + 'cursor-default' } itemCount={ item.count } itemCountMinimum={ 0 } itemImage={ item.iconUrl } />) }
</AutoGrid>
</Column>
</div>
<div className="flex flex-col w-full gap-2">
{ !selectedRecipe && <Column center fullHeight className="text-black text-center">{ LocalizeText('crafting.info.start') }</Column> }
{ selectedRecipe && <>
<div className="flex flex-col h-full overflow-hidden">
<div className="bg-muted rounded py-1 text-center">{ LocalizeText('crafting.current_recipe') }</div>
<AutoGrid columnCount={ 5 }>
{ !!requiredIngredients && (requiredIngredients.length > 0) && requiredIngredients.map(ingredient =>
{
const ingredientData = ingredients.find((i) => i.name === ingredient.itemName);
const elements: ReactElement[] = [];
for(let i = 0; i < ingredient.count; i++)
{
elements.push(<LayoutGridItem key={ i } className={ (ingredientData.count - (i) <= 0 ? 'opacity-0-5 ' : '') + 'cursor-default' } itemImage={ ingredientData.iconUrl } />);
}
return elements;
}) }
</AutoGrid>
</div>
<div className="flex flex-col h-full gap-2">
<div className="flex flex-col h-full bg-muted rounded gap-2">
<div className="py-1 text-center">{ LocalizeText('crafting.result') }</div>
<div className="flex items-center justify-center flex-col h-full pb-1 gap-1">
<div className="flex flex-col h-full">
<img src={ selectedRecipe.iconUrl } />
</div>
<div className="text-black">{ selectedRecipe.localizedName }</div>
</div>
</div>
<Button disabled={ !isOwner || !canCraft || isCrafting } variant={ !isOwner || !canCraft ? 'danger' : waitingToConfirm ? 'warning' : isCrafting ? 'primary' : 'success' } onClick={ tryCraft }>
{ !isCrafting && LocalizeText(!isOwner ? 'crafting.btn.notowner' : !canCraft ? 'crafting.status.recipe.incomplete' : waitingToConfirm ? 'generic.confirm' : 'crafting.btn.craft') }
{ isCrafting && <LayoutLoadingSpinnerView /> }
</Button>
</div>
</> }
</div>
</div>
</NitroCardContentView>
</NitroCardView>
);
};
@@ -0,0 +1,87 @@
import { RoomEngineTriggerWidgetEvent } from '@nitrots/nitro-renderer';
import { FC, useEffect, useMemo, useState } from 'react';
import ReactSlider from 'react-slider';
import { ColorUtils, FurnitureDimmerUtilities, GetConfigurationValue, LocalizeText } from '../../../../api';
import { Button, Column, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView, Text } from '../../../../common';
import { useFurnitureDimmerWidget, useNitroEvent } from '../../../../hooks';
import { classNames } from '../../../../layout';
export const FurnitureDimmerView: FC<{}> = props =>
{
const [ isVisible, setIsVisible ] = useState(false);
const { presets = [], dimmerState = 0, selectedPresetId = 0, color = 0xFFFFFF, brightness = 0xFF, effectId = 0, selectedColor = 0, setSelectedColor = null, selectedBrightness = 0, setSelectedBrightness = null, selectedEffectId = 0, setSelectedEffectId = null, selectPresetId = null, applyChanges } = useFurnitureDimmerWidget();
const onClose = () =>
{
FurnitureDimmerUtilities.previewDimmer(color, brightness, (effectId === 2));
setIsVisible(false);
};
useNitroEvent<RoomEngineTriggerWidgetEvent>(RoomEngineTriggerWidgetEvent.REMOVE_DIMMER, event => setIsVisible(false));
useEffect(() =>
{
if(!presets || !presets.length) return;
setIsVisible(true);
}, [ presets ]);
const isFreeColorMode = useMemo(() => GetConfigurationValue<boolean>('widget.dimmer.colorwheel', false), []);
if(!isVisible) return null;
return (
<NitroCardView className="nitro-room-widget-dimmer">
<NitroCardHeaderView headerText={ LocalizeText('widget.dimmer.title') } onCloseClick={ onClose } />
{ (dimmerState === 1) &&
<NitroCardTabsView>
{ presets.map(preset => <NitroCardTabsItemView key={ preset.id } isActive={ (selectedPresetId === preset.id) } onClick={ event => selectPresetId(preset.id) }>{ LocalizeText(`widget.dimmer.tab.${ preset.id }`) }</NitroCardTabsItemView>) }
</NitroCardTabsView> }
<NitroCardContentView>
{ (dimmerState === 0) &&
<Column alignItems="center">
<div className="dimmer-banner" />
<Text center className="p-1 rounded bg-muted">{ LocalizeText('widget.dimmer.info.off') }</Text>
<Button fullWidth variant="success" onClick={ () => FurnitureDimmerUtilities.changeState() }>{ LocalizeText('widget.dimmer.button.on') }</Button>
</Column> }
{ (dimmerState === 1) &&
<>
<div className="flex flex-col gap-1">
<Text fontWeight="bold">{ LocalizeText('widget.backgroundcolor.hue') }</Text>
{ isFreeColorMode &&
<input className="min-h-[calc(1.5em+ .5rem+2px)] px-[.5rem] py-[.25rem] rounded-[.2rem]" type="color" value={ ColorUtils.makeColorNumberHex(selectedColor) } onChange={ event => setSelectedColor(ColorUtils.convertFromHex(event.target.value)) } /> }
{ !isFreeColorMode &&
<Grid columnCount={ 7 } gap={ 1 }>
{ FurnitureDimmerUtilities.AVAILABLE_COLORS.map((color, index) =>
{
return (
<Column key={ index } fullWidth pointer className={ classNames('color-swatch rounded', ((color === selectedColor) && 'active')) } style={ { backgroundColor: FurnitureDimmerUtilities.HTML_COLORS[index] } } onClick={ () => setSelectedColor(color) } />
);
}) }
</Grid> }
</div>
<div className="flex flex-col gap-1">
<Text fontWeight="bold">{ LocalizeText('widget.backgroundcolor.lightness') }</Text>
<ReactSlider
className="nitro-slider"
max={ FurnitureDimmerUtilities.MAX_BRIGHTNESS }
min={ FurnitureDimmerUtilities.MIN_BRIGHTNESS }
renderThumb={ (props, state) => <div { ...props }>{ FurnitureDimmerUtilities.scaleBrightness(state.valueNow) }</div> }
thumbClassName={ 'thumb percent' }
value={ selectedBrightness }
onChange={ value => setSelectedBrightness(value) } />
</div>
<div className="flex items-center gap-1">
<input checked={ (selectedEffectId === 2) } className="form-check-input" type="checkbox" onChange={ event => setSelectedEffectId(event.target.checked ? 2 : 1) } />
<Text>{ LocalizeText('widget.dimmer.type.checkbox') }</Text>
</div>
<div className="flex gap-1">
<Button fullWidth variant="danger" onClick={ () => FurnitureDimmerUtilities.changeState() }>{ LocalizeText('widget.dimmer.button.off') }</Button>
<Button fullWidth variant="success" onClick={ applyChanges }>{ LocalizeText('widget.dimmer.button.apply') }</Button>
</div>
</> }
</NitroCardContentView>
</NitroCardView>
);
};
@@ -0,0 +1,33 @@
import { FC } from 'react';
import { LocalizeText } from '../../../../api';
import { Button, Column, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
import { useFurnitureExchangeWidget } from '../../../../hooks';
export const FurnitureExchangeCreditView: FC<{}> = props =>
{
const { objectId = -1, value = 0, onClose = null, redeem = null } = useFurnitureExchangeWidget();
if(objectId === -1) return null;
return (
<NitroCardView className="nitro-widget-exchange-credit" theme="primary-slim">
<NitroCardHeaderView headerText={ LocalizeText('catalog.redeem.dialog.title') } onCloseClick={ onClose } />
<NitroCardContentView center>
<div className="flex gap-2 overflow-hidden">
<div className="flex flex-col items-center justify-conent-center">
<div className="exchange-image" />
</div>
<div className="flex flex-col justify-between overflow-hidden !flex-grow">
<Column gap={ 1 } overflow="auto">
<Text fontWeight="bold">{ LocalizeText('creditfurni.description', [ 'credits' ], [ value.toString() ]) }</Text>
<Text>{ LocalizeText('creditfurni.prompt') }</Text>
</Column>
<Button variant="success" onClick={ redeem }>
{ LocalizeText('catalog.redeem.dialog.button.exchange') }
</Button>
</div>
</div>
</NitroCardContentView>
</NitroCardView>
);
};
@@ -0,0 +1,34 @@
import { FC } from 'react';
import { GetConfigurationValue, LocalizeText, ReportType } from '../../../../api';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common';
import { useFurnitureExternalImageWidget, useHelp } from '../../../../hooks';
import { CameraWidgetShowPhotoView } from '../../../camera/views/CameraWidgetShowPhotoView';
export const FurnitureExternalImageView: FC<{}> = props => {
const { objectId = -1, currentPhotoIndex = -1, currentPhotos = null, onClose = null } = useFurnitureExternalImageWidget();
const { report = null } = useHelp();
if (objectId === -1 || currentPhotoIndex === -1) return null;
const handleOpenFullPhoto = () => {
const photoUrl = currentPhotos[currentPhotoIndex].w.replace('_small.png', '.png');
if (photoUrl) {
console.log("Opened photo URL:", photoUrl);
window.open(photoUrl, '_blank');
}
};
return (
<NitroCardView className="nitro-external-image-widget no-resize" uniqueKey="photo-viewer" theme="primary-slim">
<NitroCardHeaderView
headerText={ LocalizeText('camera.interface.title') }
isGalleryPhoto={true}
onCloseClick={onClose}
onReportPhoto={() => report(ReportType.PHOTO, { extraData: currentPhotos[currentPhotoIndex].w, roomId: currentPhotos[currentPhotoIndex].s, reportedUserId: GetSessionDataManager().userId, roomObjectId: Number(currentPhotos[currentPhotoIndex].u) })}
/>
<NitroCardContentView>
<CameraWidgetShowPhotoView currentIndex={currentPhotoIndex} currentPhotos={currentPhotos} onClick={handleOpenFullPhoto} />
</NitroCardContentView>
</NitroCardView>
);
};
@@ -0,0 +1,66 @@
import { FC } from 'react';
import { LocalizeText } from '../../../../api';
import { Button, DraggableWindow, LayoutAvatarImageView, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common';
import { useFurnitureFriendFurniWidget } from '../../../../hooks';
export const FurnitureFriendFurniView: FC<{}> = props =>
{
const { objectId = -1, type = 0, stage = 0, usernames = [], figures = [], date = null, onClose = null, respond = null } = useFurnitureFriendFurniWidget();
if(objectId === -1) return null;
if(stage > 0)
{
return (
<NitroCardView className="nitro-engraving-lock" theme="primary-slim">
<NitroCardHeaderView headerText={ LocalizeText('friend.furniture.confirm.lock.caption') } onCloseClick={ onClose } />
<NitroCardContentView>
<h5 className="text-black text-center font-bold mt-2 mb-2">
{ LocalizeText('friend.furniture.confirm.lock.subtitle') }
</h5>
<div className="flex justify-center mb-2">
<div className={ `engraving-lock-stage-${ stage }` }></div>
</div>
{ (stage === 2) &&
<div className="text-small text-black text-center mb-2">{ LocalizeText('friend.furniture.confirm.lock.other.locked') }</div> }
<div className="flex gap-1">
<Button fullWidth onClick={ event => respond(false) }>{ LocalizeText('friend.furniture.confirm.lock.button.cancel') }</Button>
<Button fullWidth variant="success" onClick={ event => respond(true) }>{ LocalizeText('friend.furniture.confirm.lock.button.confirm') }</Button>
</div>
</NitroCardContentView>
</NitroCardView>
);
}
if(usernames.length > 0)
{
return (
<DraggableWindow handleSelector=".nitro-engraving-lock-view">
<div className={ `nitro-engraving-lock-view engraving-lock-${ type }` }>
<div className="engraving-lock-close" onClick={ onClose } />
<div className="flex justify-center">
<div className="engraving-lock-avatar">
<LayoutAvatarImageView direction={ 2 } figure={ figures[0] } />
</div>
<div className="engraving-lock-avatar">
<LayoutAvatarImageView direction={ 4 } figure={ figures[1] } />
</div>
</div>
<div className="flex flex-col mt-1 justify-between">
<div className="flex flex-col items-center gap-1 justify-center">
<div>
{ (type === 0) && LocalizeText('lovelock.engraving.caption') }
{ (type === 3) && LocalizeText('wildwest.engraving.caption') }
</div>
<div>{ date }</div>
</div>
<div className="flex gap-4 justify-center">
<div>{ usernames[0] }</div>
<div>{ usernames[1] }</div>
</div>
</div>
</div>
</DraggableWindow>
);
}
};
@@ -0,0 +1,73 @@
import { CreateLinkEvent } from '@nitrots/nitro-renderer';
import { FC } from 'react';
import { attemptItemPlacement, LocalizeText } from '../../../../api';
import { Button, Column, LayoutGiftTagView, LayoutImage, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
import { useFurniturePresentWidget, useInventoryFurni } from '../../../../hooks';
export const FurnitureGiftOpeningView: FC<{}> = props =>
{
const { objectId = -1, classId = -1, itemType = null, text = null, isOwnerOfFurniture = false, senderName = null, senderFigure = null, placedItemId = -1, placedItemType = null, placedInRoom = false, imageUrl = null, openPresent = null, onClose = null } = useFurniturePresentWidget();
const { groupItems = [] } = useInventoryFurni();
if(objectId === -1) return null;
const place = (itemId: number) =>
{
const groupItem = groupItems.find(group => (group.getItemById(itemId)?.id === itemId));
if(groupItem) attemptItemPlacement(groupItem);
onClose();
};
return (
<NitroCardView className="nitro-gift-opening" theme="primary-slim">
<NitroCardHeaderView headerText={ LocalizeText(senderName ? 'widget.furni.present.window.title_from' : 'widget.furni.present.window.title', [ 'name' ], [ senderName ]) } onCloseClick={ onClose } />
<NitroCardContentView>
{ (placedItemId === -1) &&
<Column overflow="hidden">
<div className="flex justify-center items-center overflow-auto">
<LayoutGiftTagView figure={ senderFigure } message={ text } userName={ senderName } />
</div>
{ isOwnerOfFurniture &&
<div className="flex gap-1">
{ senderName &&
<Button fullWidth onClick={ event => CreateLinkEvent('catalog/open') }>
{ LocalizeText('widget.furni.present.give_gift', [ 'name' ], [ senderName ]) }
</Button> }
<Button fullWidth variant="success" onClick={ openPresent }>
{ LocalizeText('widget.furni.present.open_gift') }
</Button>
</div> }
</Column> }
{ (placedItemId > -1) &&
<div className="flex gap-2 overflow-hidden">
<Column center className="p-2">
<LayoutImage imageUrl={ imageUrl } />
</Column>
<Column grow>
<Column center gap={ 1 }>
<Text small wrap>{ LocalizeText('widget.furni.present.message_opened') }</Text>
<Text bold fontSize={ 5 }>{ text }</Text>
</Column>
<Column grow gap={ 1 }>
<div className="flex gap-1">
{ placedInRoom &&
<Button fullWidth onClick={ null }>
{ LocalizeText('widget.furni.present.put_in_inventory') }
</Button> }
<Button fullWidth variant="success" onClick={ event => place(placedItemId) }>
{ LocalizeText(placedInRoom ? 'widget.furni.present.keep_in_room' : 'widget.furni.present.place_in_room') }
</Button>
</div>
{ (senderName && senderName.length) &&
<Button fullWidth onClick={ event => CreateLinkEvent('catalog/open') }>
{ LocalizeText('widget.furni.present.give_gift', [ 'name' ], [ senderName ]) }
</Button> }
</Column>
</Column>
</div> }
</NitroCardContentView>
</NitroCardView>
);
};
@@ -0,0 +1,60 @@
import { RoomObjectCategory } from '@nitrots/nitro-renderer';
import { FC } from 'react';
import { LocalizeText } from '../../../../api';
import { Column, Text } from '../../../../common';
import { useFurnitureHighScoreWidget } from '../../../../hooks';
import { ContextMenuHeaderView } from '../context-menu/ContextMenuHeaderView';
import { ContextMenuListView } from '../context-menu/ContextMenuListView';
import { ObjectLocationView } from '../object-location/ObjectLocationView';
export const FurnitureHighScoreView: FC<{}> = props =>
{
const { stuffDatas = null, getScoreType = null, getClearType = null } = useFurnitureHighScoreWidget();
if(!stuffDatas || !stuffDatas.size) return null;
return (
<>
{ Array.from(stuffDatas.entries()).map(([ objectId, stuffData ], index) =>
{
return (
<ObjectLocationView key={ index } category={ RoomObjectCategory.FLOOR } objectId={ objectId }>
<Column className="nitro-widget-high-score nitro-context-menu" gap={ 0 }>
<ContextMenuHeaderView>
{ LocalizeText('high.score.display.caption', [ 'scoretype', 'cleartype' ], [ LocalizeText(`high.score.display.scoretype.${ getScoreType(stuffData.scoreType) }`), LocalizeText(`high.score.display.cleartype.${ getClearType(stuffData.clearType) }`) ]) }
</ContextMenuHeaderView>
<ContextMenuListView className="h-full" gap={ 1 } overflow="hidden">
<div className="flex flex-col gap-1">
<div className="flex items-center">
<Text bold center className="col-span-8" variant="white">
{ LocalizeText('high.score.display.users.header') }
</Text>
<Text bold center className="col-span-4" variant="white">
{ LocalizeText('high.score.display.score.header') }
</Text>
</div>
<hr className="m-0" />
</div>
<Column className="overflow-y-scroll" gap={ 1 } overflow="auto">
{ stuffData.entries.map((entry, index) =>
{
return (
<div key={ index } className="flex items-center">
<Text center className="col-span-8" variant="white">
{ entry.users.join(', ') }
</Text>
<Text center className="col-span-4" variant="white">
{ entry.score }
</Text>
</div>
);
}) }
</Column>
</ContextMenuListView>
</Column>
</ObjectLocationView>
);
}) }
</>
);
};
@@ -0,0 +1,9 @@
import { FC } from 'react';
import { useFurnitureInternalLinkWidget } from '../../../../hooks';
export const FurnitureInternalLinkView: FC<{}> = props =>
{
const {} = useFurnitureInternalLinkWidget();
return null;
};
@@ -0,0 +1,145 @@
import { GetAvatarRenderManager, GetSessionDataManager, HabboClubLevelEnum, RoomControllerLevel } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { GetClubMemberLevel, GetRoomSession, LocalizeText, MannequinUtilities } from '../../../../api';
import { Button, Column, LayoutAvatarImageView, LayoutCurrencyIcon, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
import { useFurnitureMannequinWidget } from '../../../../hooks';
import { NitroInput } from '../../../../layout';
const MODE_NONE: number = -1;
const MODE_CONTROLLER: number = 0;
const MODE_UPDATE: number = 1;
const MODE_PEER: number = 2;
const MODE_NO_CLUB: number = 3;
const MODE_WRONG_GENDER: number = 4;
export const FurnitureMannequinView: FC<{}> = props =>
{
const [ renderedFigure, setRenderedFigure ] = useState<string>(null);
const [ mode, setMode ] = useState(MODE_NONE);
const { objectId = -1, figure = null, gender = null, clubLevel = HabboClubLevelEnum.NO_CLUB, name = null, setName = null, saveFigure = null, wearFigure = null, saveName = null, onClose = null } = useFurnitureMannequinWidget();
useEffect(() =>
{
if(objectId === -1) return;
const roomSession = GetRoomSession();
if(roomSession.isRoomOwner || (roomSession.controllerLevel >= RoomControllerLevel.GUEST) || GetSessionDataManager().isModerator)
{
setMode(MODE_CONTROLLER);
return;
}
if(GetSessionDataManager().gender.toLowerCase() !== gender.toLowerCase())
{
setMode(MODE_WRONG_GENDER);
return;
}
if(GetClubMemberLevel() < clubLevel)
{
setMode(MODE_NO_CLUB);
return;
}
setMode(MODE_PEER);
}, [ objectId, gender, clubLevel ]);
useEffect(() =>
{
switch(mode)
{
case MODE_CONTROLLER:
case MODE_WRONG_GENDER: {
const figureContainer = GetAvatarRenderManager().createFigureContainer(figure);
MannequinUtilities.transformAsMannequinFigure(figureContainer);
setRenderedFigure(figureContainer.getFigureString());
break;
}
case MODE_UPDATE: {
const figureContainer = GetAvatarRenderManager().createFigureContainer(GetSessionDataManager().figure);
MannequinUtilities.transformAsMannequinFigure(figureContainer);
setRenderedFigure(figureContainer.getFigureString());
break;
}
case MODE_PEER:
case MODE_NO_CLUB: {
const figureContainer = MannequinUtilities.getMergedMannequinFigureContainer(GetSessionDataManager().figure, figure);
setRenderedFigure(figureContainer.getFigureString());
break;
}
}
}, [ mode, figure, clubLevel ]);
if(objectId === -1) return null;
return (
<NitroCardView className="nitro-mannequin no-resize" theme="primary-slim">
<NitroCardHeaderView headerText={ LocalizeText('mannequin.widget.title') } onCloseClick={ onClose } />
<NitroCardContentView center>
<div className="flex w-full gap-2 overflow-hidden">
<div className="flex flex-col">
<div className="relative mannequin-preview">
<LayoutAvatarImageView direction={ 2 } figure={ renderedFigure } position="absolute" />
{ (clubLevel > 0) &&
<LayoutCurrencyIcon className="absolute end-2 bottom-2" type="hc" /> }
</div>
</div>
<Column grow justifyContent="between" overflow="auto">
{ (mode === MODE_CONTROLLER) &&
<>
<NitroInput type="text" value={ name } onBlur={ saveName } onChange={ event => setName(event.target.value) } />
<div className="flex flex-col gap-1">
<Button variant="success" onClick={ event => setMode(MODE_UPDATE) }>
{ LocalizeText('mannequin.widget.style') }
</Button>
<Button variant="success" onClick={ wearFigure }>
{ LocalizeText('mannequin.widget.wear') }
</Button>
</div>
</> }
{ (mode === MODE_UPDATE) &&
<>
<div className="flex flex-col gap-1">
<Text bold>{ name }</Text>
<Text wrap>{ LocalizeText('mannequin.widget.savetext') }</Text>
</div>
<div className="flex items-center justify-between">
<Text pointer underline onClick={ event => setMode(MODE_CONTROLLER) }>
{ LocalizeText('mannequin.widget.back') }
</Text>
<Button variant="success" onClick={ saveFigure }>
{ LocalizeText('mannequin.widget.save') }
</Button>
</div>
</> }
{ (mode === MODE_PEER) &&
<>
<div className="flex flex-col gap-1">
<Text bold>{ name }</Text>
<Text>{ LocalizeText('mannequin.widget.weartext') }</Text>
</div>
<Button variant="success" onClick={ wearFigure }>
{ LocalizeText('mannequin.widget.wear') }
</Button>
</> }
{ (mode === MODE_NO_CLUB) &&
<div className="flex justify-center items-center !flex-grow">
<Text>{ LocalizeText('mannequin.widget.clubnotification') }</Text>
</div> }
{ (mode === MODE_WRONG_GENDER) &&
<Text>{ LocalizeText('mannequin.widget.wronggender') }</Text> }
</Column>
</div>
</NitroCardContentView>
</NitroCardView>
);
};
@@ -0,0 +1,81 @@
import { CancelMysteryBoxWaitMessageEvent, GetSessionDataManager, GotMysteryBoxPrizeMessageEvent, MysteryBoxWaitingCanceledMessageComposer, ShowMysteryBoxWaitMessageEvent } from '@nitrots/nitro-renderer';
import { FC, useState } from 'react';
import { LocalizeText, SendMessageComposer } from '../../../../api';
import { Button, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
import { LayoutPrizeProductImageView } from '../../../../common/layout/LayoutPrizeProductImageView';
import { useMessageEvent } from '../../../../hooks';
interface FurnitureMysteryBoxOpenDialogViewProps
{
ownerId: number;
}
type PrizeData = {
contentType:string;
classId:number;
}
enum ViewMode {
HIDDEN,
WAITING,
PRIZE
}
export const FurnitureMysteryBoxOpenDialogView: FC<FurnitureMysteryBoxOpenDialogViewProps> = props =>
{
const { ownerId = -1 } = props;
const [ mode, setMode ] = useState<ViewMode>(ViewMode.HIDDEN);
const [ prizeData, setPrizeData ] = useState<PrizeData>(undefined);
const close = () =>
{
if(mode === ViewMode.WAITING) SendMessageComposer(new MysteryBoxWaitingCanceledMessageComposer(ownerId));
setMode(ViewMode.HIDDEN);
setPrizeData(undefined);
};
useMessageEvent<ShowMysteryBoxWaitMessageEvent>(ShowMysteryBoxWaitMessageEvent, event =>
{
setMode(ViewMode.WAITING);
});
useMessageEvent<CancelMysteryBoxWaitMessageEvent>(CancelMysteryBoxWaitMessageEvent, event =>
{
setMode(ViewMode.HIDDEN);
setPrizeData(undefined);
});
useMessageEvent<GotMysteryBoxPrizeMessageEvent>(GotMysteryBoxPrizeMessageEvent, event =>
{
const parser = event.getParser();
setPrizeData({ contentType: parser.contentType, classId: parser.classId });
setMode(ViewMode.PRIZE);
});
const isOwner = GetSessionDataManager().userId === ownerId;
if(mode === ViewMode.HIDDEN) return null;
return (
<NitroCardView className="nitro-mysterybox-dialog" theme="primary-slim">
<NitroCardHeaderView headerText={ mode === ViewMode.WAITING ? LocalizeText(`mysterybox.dialog.${ isOwner ? 'owner' : 'other' }.title`) : LocalizeText('mysterybox.reward.title') } onCloseClick={ close } />
<NitroCardContentView>
{ mode === ViewMode.WAITING && <>
<Text variant="primary"> { LocalizeText(`mysterybox.dialog.${ isOwner ? 'owner' : 'other' }.subtitle`) } </Text>
<Text> { LocalizeText(`mysterybox.dialog.${ isOwner ? 'owner' : 'other' }.description`) } </Text>
<Text> { LocalizeText(`mysterybox.dialog.${ isOwner ? 'owner' : 'other' }.waiting`) }</Text>
<Button className="mt-auto" variant="danger" onClick={ close }> { LocalizeText(`mysterybox.dialog.${ isOwner ? 'owner' : 'other' }.cancel`) } </Button>
</>
}
{ mode === ViewMode.PRIZE && prizeData && <>
<Text variant="black"> { LocalizeText('mysterybox.reward.text') } </Text>
<Flex className="prize-container justify-center mx-auto">
<LayoutPrizeProductImageView classId={ prizeData.classId } productType={ prizeData.contentType }/>
</Flex>
<Button className="mt-auto" variant="success" onClick={ close }> { LocalizeText('mysterybox.reward.close') } </Button>
</>
}
</NitroCardContentView>
</NitroCardView>
);
};
@@ -0,0 +1,50 @@
import { OpenMysteryTrophyMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useState } from 'react';
import { LocalizeText, SendMessageComposer } from '../../../../api';
import { Button, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
interface FurnitureMysteryTrophyOpenDialogViewProps
{
objectId: number;
onClose: () => void;
}
export const FurnitureMysteryTrophyOpenDialogView: FC<FurnitureMysteryTrophyOpenDialogViewProps> = props =>
{
const { objectId = -1, onClose = null } = props;
const [ description, setDescription ] = useState<string>('');
const onConfirm = () =>
{
SendMessageComposer(new OpenMysteryTrophyMessageComposer(objectId, description));
onClose();
};
if(objectId === -1) return null;
return (
<NitroCardView className="nitro-mysterytrophy-dialog no-resize" theme="primary-slim">
<NitroCardHeaderView center headerText={ LocalizeText('mysterytrophy.header.title') } onCloseClick={ onClose } />
<NitroCardContentView>
<div className="flex mysterytrophy-dialog-top p-3">
<div className="mysterytrophy-image flex-shrink-0"></div>
<div className="m-2">
<Text className="mysterytrophy-text-big" variant="white">{ LocalizeText('mysterytrophy.header.description') }</Text>
</div>
</div>
<div className="flex mysterytrophy-dialog-bottom p-2">
<div className="flex flex-col gap-1">
<div className="flex items-center bg-white rounded py-1 px-2 input-mysterytrophy-dialog">
<textarea className="min-h-[calc(1.5em+ .5rem+2px)] px-[.5rem] py-[.25rem] rounded-[.2rem] form-control-sm input-mysterytrophy" value={ description } onChange={ event => setDescription(event.target.value) } />
<div className="mysterytrophy-pencil-image flex-shrink-0 small fa-icon"></div>
</div>
<div className="flex items-center mt-2 gap-5 justify-center">
<Text pointer className="text-decoration" onClick={ () => onClose() }>{ LocalizeText('cancel') }</Text>
<Button variant="success" onClick={ () => onConfirm() }>{ LocalizeText('generic.ok') }</Button>
</div>
</div>
</div>
</NitroCardContentView>
</NitroCardView>
);
};
@@ -0,0 +1,9 @@
import { FC } from 'react';
import { useFurnitureRoomLinkWidget } from '../../../../hooks';
export const FurnitureRoomLinkView: FC<{}> = props =>
{
const {} = useFurnitureRoomLinkWidget();
return null;
};
@@ -0,0 +1,46 @@
import { FC } from 'react';
import { ColorUtils } from '../../../../api';
import { DraggableWindow, DraggableWindowPosition } from '../../../../common';
import { useFurnitureSpamWallPostItWidget } from '../../../../hooks';
const STICKIE_COLORS = [ '9CCEFF', 'FF9CFF', '9CFF9C', 'FFFF33' ];
const STICKIE_COLOR_NAMES = [ 'blue', 'pink', 'green', 'yellow' ];
const getStickieColorName = (color: string) =>
{
let index = STICKIE_COLORS.indexOf(color);
if(index === -1) index = 0;
return STICKIE_COLOR_NAMES[index];
};
export const FurnitureSpamWallPostItView: FC<{}> = props =>
{
const { objectId = -1, color = '0', setColor = null, text = '', setText = null, canModify = false, onClose = null } = useFurnitureSpamWallPostItWidget();
if(objectId === -1) return null;
return (
<DraggableWindow handleSelector=".drag-handler" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
<div className={ 'nitro-stickie nitro-stickie-image stickie-' + getStickieColorName(color) }>
<div className="flex items-center stickie-header drag-handler">
<div className="flex items-center !flex-grow h-full">
{ canModify &&
<>
<div className="nitro-stickie-image stickie-trash header-trash" onClick={ onClose }></div>
{ STICKIE_COLORS.map(color =>
{
return <div key={ color } className="stickie-color ms-1" style={ { backgroundColor: ColorUtils.makeColorHex(color) } } onClick={ event => setColor(color) } />;
}) }
</> }
</div>
<div className="flex items-center nitro-stickie-image stickie-close header-close" onClick={ onClose }></div>
</div>
<div className="stickie-context">
<textarea autoFocus className="context-text" tabIndex={ 0 } value={ text } onChange={ event => setText(event.target.value) }></textarea>
</div>
</div>
</DraggableWindow>
);
};
@@ -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 (
<NitroCardView className="nitro-widget-custom-stack-height" theme="primary-slim">
<NitroCardHeaderView headerText={ LocalizeText('widget.custom.stack.height.title') } onCloseClick={ onClose } />
<NitroCardContentView justifyContent="between">
<Text>{ LocalizeText('widget.custom.stack.height.text') }</Text>
<div className="flex gap-2">
<ReactSlider
className="nitro-slider"
max={ maxHeight }
min={ 0 }
renderThumb={ (props, state) => <div { ...props }>{ state.valueNow }</div> }
step={ 0.01 }
value={ height }
onChange={ event => updateHeight(event) } />
<input className="show-number-arrows" max={ maxHeight } min={ 0 } style={ { width: 50 } } type="number" value={ tempHeight } onChange={ event => updateTempHeight(event.target.value) } />
</div>
<div className="flex flex-col gap-1">
<Button onClick={ event => SendMessageComposer(new FurnitureStackHeightComposer(objectId, -100)) }>
{ LocalizeText('furniture.above.stack') }
</Button>
<Button onClick={ event => SendMessageComposer(new FurnitureStackHeightComposer(objectId, 0)) }>
{ LocalizeText('furniture.floor.level') }
</Button>
</div>
</NitroCardContentView>
</NitroCardView>
);
};
@@ -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 (
<DraggableWindow handleSelector=".drag-handler" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
<div className={ 'nitro-stickie nitro-stickie-image stickie-' + (type == 'post_it' ? getStickieColorName(color) : getStickieTypeName(type)) }>
<div className="flex items-center stickie-header drag-handler">
<div className="flex items-center !flex-grow h-full">
{ canModify &&
<>
<div className="nitro-stickie-image stickie-trash header-trash" onClick={ trash }></div>
{ type == 'post_it' &&
<>
{ STICKIE_COLORS.map(color =>
{
return <div key={ color } className="stickie-color ms-1" style={ { backgroundColor: ColorUtils.makeColorHex(color) } } onClick={ event => updateColor(color) } />;
}) }
</> }
</> }
</div>
<div className="flex items-center nitro-stickie-image stickie-close header-close" onClick={ onClose }></div>
</div>
<div className="stickie-context">
{ (!isEditing || !canModify) ? <div className="context-text" onClick={ event => (canModify && setIsEditing(true)) }>{ text }</div> : <textarea autoFocus className="context-text" defaultValue={ text } tabIndex={ 0 } onBlur={ event => updateText(event.target.value) }></textarea> }
</div>
</div>
</DraggableWindow>
);
};
@@ -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 <LayoutTrophyView color={ color } date={ date } message={ message } senderName={ senderName } onCloseClick={ onClose } />;
};
@@ -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 (
<>
<FurnitureBackgroundColorView />
<FurnitureBadgeDisplayView />
<FurnitureCraftingView />
<FurnitureDimmerView />
<FurnitureExchangeCreditView />
<FurnitureExternalImageView />
<FurnitureFriendFurniView />
<FurnitureGiftOpeningView />
<FurnitureHighScoreView />
<FurnitureInternalLinkView />
<FurnitureMannequinView />
<FurniturePlaylistEditorWidgetView />
<FurnitureRoomLinkView />
<FurnitureSpamWallPostItView />
<FurnitureStackHeightView />
<FurnitureStickieView />
<FurnitureTrophyView />
<FurnitureContextMenuView />
<FurnitureYoutubeDisplayView />
</>
);
};
@@ -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<any>(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 (
<NitroCardView className="youtube-tv-widget">
<NitroCardHeaderView headerText={ LocalizeText('catalog.page.youtube_tvs') } onCloseClick={ onClose } />
<NitroCardContentView>
<div className="row size-full">
<div className="youtube-video-container col-span-9 overflow-hidden">
{ (videoId && videoId.length > 0) &&
<YouTube containerClassName={ 'youtubeContainer' } opts={ youtubeOptions } videoId={ videoId } onReady={ event => setPlayer(event.target) } onStateChange={ onStateChange } />
}
{ (!videoId || videoId.length === 0) &&
<div className="empty-video size-full justify-center items-center flex">{ LocalizeText('widget.furni.video_viewer.no_videos') }</div>
}
</div>
<div className="playlist-container col-span-3 flex flex-col">
<span className="playlist-controls justify-center flex">
<i className="icon icon-youtube-prev cursor-pointer" onClick={ previous } />
<i className="icon icon-youtube-next cursor-pointer" onClick={ next } />
</span>
<div className="mb-1">{ LocalizeText('widget.furni.video_viewer.playlists') }</div>
<AutoGrid className="mb-1" columnCount={ 1 } columnMinHeight={ 100 } columnMinWidth={ 80 } overflow="auto">
{ playlists && playlists.map((entry, index) =>
{
return (
<LayoutGridItem key={ index } itemActive={ (entry.video === selectedVideo) } onClick={ event => selectVideo(entry.video) }>
<b>{ entry.title }</b>
</LayoutGridItem>
);
}) }
</AutoGrid>
</div>
</div>
</NitroCardContentView>
</NitroCardView>
);
};
@@ -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<EffectBoxConfirmViewProps> = props =>
{
const { objectId = -1, onClose = null } = props;
const { roomSession = null } = useRoom();
const useProduct = () =>
{
roomSession.useMultistateItem(objectId);
onClose();
};
return (
<NitroCardView className="nitro-use-product-confirmation">
<NitroCardHeaderView headerText={ LocalizeText('effectbox.header.title') } onCloseClick={ onClose } />
<NitroCardContentView center>
<div className="flex gap-2">
<Column justifyContent="between">
<Text>{ LocalizeText('effectbox.header.description') }</Text>
<div className="flex items-center justify-between">
<Button variant="danger" onClick={ onClose }>{ LocalizeText('generic.cancel') }</Button>
<Button variant="success" onClick={ useProduct }>{ LocalizeText('generic.ok') }</Button>
</div>
</Column>
</div>
</NitroCardContentView>
</NitroCardView>
);
};
@@ -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>(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) &&
<MonsterPlantSeedConfirmView objectId={ confirmingObjectId } onClose={ closeConfirm } /> }
{ (confirmMode === PURCHASABLE_CLOTHING_CONFIRMATION) &&
<PurchasableClothingConfirmView objectId={ confirmingObjectId } onClose={ closeConfirm } /> }
{ (confirmMode === EFFECTBOX_OPEN) &&
<EffectBoxConfirmView objectId={ confirmingObjectId } onClose={ closeConfirm } /> }
{ (confirmMode === MYSTERYTROPHY_OPEN_DIALOG) &&
<FurnitureMysteryTrophyOpenDialogView objectId={ confirmingObjectId } onClose={ closeConfirm } /> }
<FurnitureMysteryBoxOpenDialogView ownerId={ objectOwnerId } />
{ (objectId >= 0) && mode &&
<ContextMenuView category={ RoomObjectCategory.FLOOR } fades={ true } objectId={ objectId } onClose={ onClose }>
{ (mode === ContextMenuEnum.FRIEND_FURNITURE) &&
<>
<ContextMenuHeaderView>
{ LocalizeText('friendfurni.context.title') }
</ContextMenuHeaderView>
<ContextMenuListItemView onClick={ event => processAction('use_friend_furni') }>
{ LocalizeText('friendfurni.context.use') }
</ContextMenuListItemView>
</> }
{ (mode === ContextMenuEnum.MONSTERPLANT_SEED) &&
<>
<ContextMenuHeaderView>
{ LocalizeText('furni.mnstr_seed.name') }
</ContextMenuHeaderView>
<ContextMenuListItemView onClick={ event => processAction('use_monsterplant_seed') }>
{ LocalizeText('widget.monsterplant_seed.button.use') }
</ContextMenuListItemView>
</> }
{ (mode === ContextMenuEnum.RANDOM_TELEPORT) &&
<>
<ContextMenuHeaderView>
{ LocalizeText('furni.random_teleport.name') }
</ContextMenuHeaderView>
<ContextMenuListItemView onClick={ event => processAction('use_random_teleport') }>
{ LocalizeText('widget.random_teleport.button.use') }
</ContextMenuListItemView>
</> }
{ (mode === ContextMenuEnum.PURCHASABLE_CLOTHING) &&
<>
<ContextMenuHeaderView>
{ LocalizeText('furni.generic_usable.name') }
</ContextMenuHeaderView>
<ContextMenuListItemView onClick={ event => processAction('use_purchaseable_clothing') }>
{ LocalizeText('widget.generic_usable.button.use') }
</ContextMenuListItemView>
</> }
{ (mode === ContextMenuEnum.MYSTERY_BOX) &&
<>
<ContextMenuHeaderView>
{ LocalizeText('mysterybox.context.title') }
</ContextMenuHeaderView>
<ContextMenuListItemView onClick={ event => processAction('use_mystery_box') }>
{ LocalizeText('mysterybox.context.' + ((isOwner) ? 'owner' : 'other') + '.use') }
</ContextMenuListItemView>
</> }
{ (mode === ContextMenuEnum.MYSTERY_TROPHY) &&
<>
<ContextMenuHeaderView>
{ LocalizeText('mysterytrophy.header.title') }
</ContextMenuHeaderView>
<ContextMenuListItemView onClick={ event => processAction('use_mystery_trophy') }>
{ LocalizeText('friendfurni.context.use') }
</ContextMenuListItemView>
</> }
{ (mode === GROUP_FURNITURE) && groupData &&
<>
<ContextMenuHeaderView className="cursor-pointer text-truncate" onClick={ () => GetGroupInformation(groupData.guildId) }>
{ groupData.guildName }
</ContextMenuHeaderView>
{ !isGroupMember &&
<ContextMenuListItemView onClick={ event => processAction('join_group') }>
{ LocalizeText('widget.furniture.button.join.group') }
</ContextMenuListItemView> }
<ContextMenuListItemView onClick={ event => processAction('go_to_group_homeroom') }>
{ LocalizeText('widget.furniture.button.go.to.group.home.room') }
</ContextMenuListItemView>
{ groupData.guildHasReadableForum &&
<ContextMenuListItemView onClick={ event => processAction('open_forum') }>
{ LocalizeText('widget.furniture.button.open_group_forum') }
</ContextMenuListItemView> }
</> }
</ContextMenuView> }
</>
);
};
@@ -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<MonsterPlantSeedConfirmViewProps> = props =>
{
const { objectId = -1, onClose = null } = props;
const [ furniData, setFurniData ] = useState<IFurnitureData>(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 (
<NitroCardView className="nitro-use-product-confirmation">
<NitroCardHeaderView headerText={ LocalizeText('useproduct.widget.title.plant_seed', [ 'name' ], [ furniData.name ]) } onCloseClick={ onClose } />
<NitroCardContentView center>
<div className="flex gap-2 overflow-hidden">
<div className="flex flex-col">
<div className="product-preview">
<div className="monsterplant-image" />
</div>
</div>
<div className="flex flex-col justify-between overflow-auto">
<Column gap={ 2 }>
<Text>{ LocalizeText('useproduct.widget.text.plant_seed', [ 'productName' ], [ furniData.name ]) }</Text>
<Text>{ LocalizeText('useproduct.widget.info.plant_seed') }</Text>
</Column>
<div className="flex items-center justify-between">
<Button variant="danger" onClick={ onClose }>{ LocalizeText('useproduct.widget.cancel') }</Button>
<Button variant="success" onClick={ useProduct }>{ LocalizeText('widget.monsterplant_seed.button.use') }</Button>
</div>
</div>
</div>
</NitroCardContentView>
</NitroCardView>
);
};
@@ -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<PurchasableClothingConfirmViewProps> = props =>
{
const { objectId = -1, onClose = null } = props;
const [ mode, setMode ] = useState(MODE_DEFAULT);
const [ gender, setGender ] = useState<string>(AvatarFigurePartType.MALE);
const [ newFigure, setNewFigure ] = useState<string>(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 (
<NitroCardView className="nitro-use-product-confirmation">
<NitroCardHeaderView headerText={ LocalizeText('useproduct.widget.title.bind_clothing') } onCloseClick={ onClose } />
<NitroCardContentView center>
<div className="flex overflow-hidden gap-2">
<div className="flex flex-col">
<div className="mannequin-preview">
<LayoutAvatarImageView direction={ 2 } figure={ newFigure } />
</div>
</div>
<div className="flex flex-col justify-between overflow-auto">
<Column gap={ 2 }>
<Text>{ LocalizeText('useproduct.widget.text.bind_clothing') }</Text>
<Text>{ LocalizeText('useproduct.widget.info.bind_clothing') }</Text>
</Column>
<div className="flex items-center justify-between">
<Button variant="danger" onClick={ onClose }>{ LocalizeText('useproduct.widget.cancel') }</Button>
<Button variant="success" onClick={ useProduct }>{ LocalizeText('useproduct.widget.bind_clothing') }</Button>
</div>
</div>
</div>
</NitroCardContentView>
</NitroCardView>
);
};
@@ -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<number, number>;
addToPlaylist: (diskId: number, slotNumber: number) => void;
}
export const DiskInventoryView: FC<DiskInventoryViewProps> = props =>
{
const { diskInventory = null, addToPlaylist = null } = props;
const [ selectedItem, setSelectedItem ] = useState<number>(-1);
const [ previewSongId, setPreviewSongId ] = useState<number>(-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 (<>
<div className="flex justify-center py-3 rounded bg-success container-fluid">
<img className="my-music" src={ GetConfigurationValue('image.library.url') + 'playlist/title_mymusic.gif' } />
<h2 className="ms-4">{ LocalizeText('playlist.editor.my.music') }</h2>
</div>
<div className="h-full py-2 mt-4 overflow-y-scroll">
<AutoGrid columnCount={ 3 } columnMinWidth={ 95 } gap={ 1 }>
{ diskInventory && diskInventory.getKeys().map((key, index) =>
{
const diskId = diskInventory.getKey(index);
const songId = diskInventory.getWithIndex(index);
const songInfo = GetSoundManager().musicController?.getSongInfo(songId);
return (
<LayoutGridItem key={ index } classNames={ [ 'text-black' ] } itemActive={ (selectedItem === index) } onClick={ () => setSelectedItem(prev => prev === index ? -1 : index) }>
<div className="flex-shrink-0 disk-image mb-n2" style={ { backgroundColor: GetDiskColor(songInfo?.songData) } }>
</div>
<Text fullWidth truncate className="text-center">{ songInfo?.name }</Text>
{ (selectedItem === index) &&
<Flex alignItems="center" className="bottom-0 p-1 mb-1 rounded bg-secondary" gap={ 2 } justifyContent="center" position="absolute">
<Button variant="light" onClick={ event => previewSong(event, songId) }>
<div className={ (previewSongId === songId) ? 'pause-btn' : 'preview-song' } />
</Button>
<Button variant="light" onClick={ event => addSong(event, diskId) }>
<div className="move-disk" />
</Button>
</Flex>
}
</LayoutGridItem>);
}) }
</AutoGrid>
</div>
<div className="p-1 text-black playlist-bottom">
<h5>{ LocalizeText('playlist.editor.text.get.more.music') }</h5>
<div>{ LocalizeText('playlist.editor.text.you.have.no.songdisks.available') }</div>
<div>{ LocalizeText('playlist.editor.text.you.can.buy.some.from.the.catalogue') }</div>
<button className="btn btn-primary btn-sm" onClick={ () => openCatalogPage() }>{ LocalizeText('playlist.editor.button.open.catalogue') }</button>
</div>
<img className="get-more" src={ `${ GetConfigurationValue('image.library.url') }playlist/background_get_more_music.gif` } />
</>);
};
@@ -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 (
<NitroCardView className="nitro-playlist-editor-widget" theme="primary-slim">
<NitroCardHeaderView headerText={ LocalizeText('playlist.editor.title') } onCloseClick={ onClose } />
<NitroCardContentView>
<div className="flex flex-row gap-1 h-full">
<div className="w-50 relative overflow-hidden h-full rounded flex flex-col">
<DiskInventoryView addToPlaylist={ addToPlaylist } diskInventory={ diskInventory } />
</div>
<div className="w-50 relative overflow-hidden h-full rounded flex flex-col">
<SongPlaylistView currentPlayingIndex={ currentPlayingIndex } furniId={ objectId } playlist={ playlist } removeFromPlaylist={ removeFromPlaylist } togglePlayPause={ togglePlayPause } />
</div>
</div>
</NitroCardContentView>
</NitroCardView>
);
};
@@ -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<SongPlaylistViewProps> = props =>
{
const { furniId = -1, playlist = null, currentPlayingIndex = -1, removeFromPlaylist = null, togglePlayPause = null } = props;
const [ selectedItem, setSelectedItem ] = useState<number>(-1);
const action = (index: number) =>
{
if(selectedItem === index) removeFromPlaylist(index);
};
const playPause = (furniId: number, selectedItem: number) =>
{
togglePlayPause(furniId, selectedItem !== -1 ? selectedItem : 0);
};
return (<>
<div className="bg-primary py-3 container-fluid justify-center flex rounded">
<img className="playlist-img" src={ GetConfigurationValue('image.library.url') + 'playlist/title_playlist.gif' } />
<h2 className="ms-4">{ LocalizeText('playlist.editor.playlist') }</h2>
</div>
<div className="h-full overflow-y-scroll py-2">
<div className="flex flex-col gap-2">
{ playlist && playlist.map((songInfo, index) =>
{
return <div key={ index } className={ 'flex gap-1 items-center text-black cursor-pointer ' + (selectedItem === index ? 'border border-muted border-2 rounded' : 'border-2') } onClick={ () => setSelectedItem(prev => prev === index ? -1 : index) }>
<div className={ 'disk-2 ' + (selectedItem === index ? 'selected-song' : '') } style={ { backgroundColor: (selectedItem === index ? '' : GetDiskColor(songInfo.songData)) } } onClick={ () => action(index) } />
{ songInfo.name }
</div>;
}) }
</div>
</div>
{ (!playlist || playlist.length === 0) &&
<><div className="playlist-bottom text-black p-1 ms-5">
<h5>{ LocalizeText('playlist.editor.add.songs.to.your.playlist') }</h5>
<div>{ LocalizeText('playlist.editor.text.click.song.to.choose.click.again.to.move') }</div>
</div>
<img className="add-songs" src={ GetConfigurationValue('image.library.url') + 'playlist/background_add_songs.gif' } /></>
}
{ (playlist && playlist.length > 0) &&
<>
{ (currentPlayingIndex === -1) &&
<Button size="lg" variant="success" onClick={ () => playPause(furniId, selectedItem) }>
{ LocalizeText('playlist.editor.button.play.now') }
</Button>
}
{ (currentPlayingIndex !== -1) &&
<div className="flex gap-1">
<Button variant="danger" onClick={ () => playPause(furniId, selectedItem) }>
<div className="pause-song" />
</Button>
<div className="flex flex-col">
<Text bold display="block">{ LocalizeText('playlist.editor.text.now.playing.in.your.room') }</Text>
<Text>
{ playlist[currentPlayingIndex]?.name + ' - ' + playlist[currentPlayingIndex]?.creator }
</Text>
</div>
</div>
}
</>
}
</>);
};
@@ -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<boolean>(true);
const [ keyColor, setKeyColor ] = useState<string>('');
const [ boxColor, setBoxColor ] = useState<string>('');
useNitroEvent<MysteryBoxKeysUpdateEvent>(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 (
<div className="px-[5px] py-[6px] [box-shadow:inset_0_5px_#22222799,_inset_0_-4px_#12121599] text-sm bg-[#1c1c20f2] rounded mysterybox-extension">
<div className="flex flex-col">
<Flex pointer alignItems="center" justifyContent="between" onClick={ event => setIsOpen(value => !value) }>
<Text variant="white">{ LocalizeText('mysterybox.tracker.title') }</Text>
{ isOpen && <FaChevronUp className="fa-icon" /> }
{ !isOpen && <FaChevronDown className="fa-icon" /> }
</Flex>
{ isOpen &&
<>
<Text variant="white">{ LocalizeText('mysterybox.tracker.description') }</Text>
<div className="flex items-center gap-2 justify-center">
<LayoutGridItem className="mysterybox-container">
<div className="box-image flex-shrink-0" style={ { backgroundColor: getRgbColor(boxColor) } }>
<div className="chain-overlay-image" />
</div>
</LayoutGridItem>
<LayoutGridItem className="mysterybox-container">
<div className="key-image flex-shrink-0" style={ { backgroundColor: getRgbColor(keyColor) } }>
<div className="key-overlay-image" />
</div>
</LayoutGridItem>
</div>
</> }
</div>
</div>
);
};
@@ -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<HTMLDivElement>
{
objectId: number;
category: number;
noFollow?: boolean;
}
export const ObjectLocationView: FC<ObjectLocationViewProps> = 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<HTMLDivElement>();
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 <div ref={ elementRef } className="object-location absolute" style={ { left: pos.x, top: pos.y, visibility: ((pos.x + (elementRef.current ? elementRef.current.offsetWidth : 0)) > -1) ? 'visible' : 'hidden' } } { ...rest } />;
};
@@ -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 &&
<NitroCardView className="nitro-pet-package no-resize" theme="primary-slim">
<NitroCardHeaderView center headerText={ objectType === 'gnome_box' ? LocalizeText('widgets.gnomepackage.name.title') : LocalizeText('furni.petpackage.open') } onCloseClick={ () => onClose() } />
<NitroCardContentView>
<div className="flex pet-package-container-top p-3">
<div className={ `package-image-${ objectType } flex-shrink-0` }></div>
<div className="m-2">
<Text className="package-text-big" variant="white">{ objectType === 'gnome_box' ? LocalizeText('widgets.gnomepackage.name.title') : LocalizeText('furni.petpackage') }</Text>
</div>
</div>
<div className="flex pet-package-container-bottom p-2">
<div className="flex flex-col gap-1">
<div className="flex items-center bg-white rounded py-1 px-2 input-pet-package-container">
<input className="min-h-[calc(1.5em+ .5rem+2px)] px-[.5rem] py-[.25rem] rounded-[.2rem] form-control-sm input-pet-package" maxLength={ GetConfigurationValue('pet.package.name.max.length') } placeholder={ objectType === 'gnome_box' ? LocalizeText('widgets.gnomepackage.name.select') : LocalizeText('widgets.petpackage.name.title') } type="text" value={ petName } onChange={ event => onChangePetName(event.target.value) } />
<div className="package-pencil-image flex-shrink-0 small fa-icon"></div>
</div>
{ (errorResult.length > 0) &&
<div className="invalid-feedback d-block m-0">{ errorResult }</div> }
<div className="flex items-center gap-5 justify-center mt-2">
<Text pointer className="text-decoration" onClick={ () => onClose() }>{ LocalizeText('cancel') }</Text>
<Button disabled={ petName.length < 3 } variant={ petName.length < 3 ? 'danger' : 'success' } onClick={ () => onConfirm() }>{ objectType === 'gnome_box' ? LocalizeText('widgets.gnomepackage.name.pick') : LocalizeText('furni.petpackage.confirm') }</Button>
</div>
</div>
</div>
</NitroCardContentView>
</NitroCardView>
}
</>
);
};
@@ -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<string>('bobba');
const [ selectedWord, setSelectedWord ] = useState<string>('');
const [ isSelectingWord, setIsSelectingWord ] = useState<boolean>(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 (
<NitroCardView className="nitro-guide-tool no-resize" theme="primary-slim">
<NitroCardHeaderView headerText={ LocalizeText('navigator.roomsettings.roomfilter') } onCloseClick={ () => onClose() } />
<NitroCardContentView className="text-black">
<Grid className="flex items-center gap-2 justify-end">
<NitroInput maxLength={ 255 } type="text" value={ word } onChange={ event => onTyping(event.target.value) } />
<Button onClick={ () => processAction(true) }>{ LocalizeText('navigator.roomsettings.roomfilter.addword') }</Button>
</Grid>
<Column className="min-h-[calc(1.5em+ .5rem+2px)] px-[.5rem] py-[.25rem] rounded-[.2rem] form-control-sm" gap={ 0 } overflow="auto" style={ { height: '100px' } }>
{ wordsFilter && (wordsFilter.length > 0) && wordsFilter.map((word, index) =>
{
return (
<Flex key={ index } pointer alignItems="center" className={ classNames('rounded p-1', (selectedWord === word) && 'bg-muted') } onClick={ event => onSelectedWord(word) }>
<Text truncate>{ word }</Text>
</Flex>
);
}) }
</Column>
<Grid className="flex items-center gap-2 justify-end">
<Button disabled={ wordsFilter.length === 0 || !isSelectingWord } variant="danger" onClick={ () => processAction(false) }>{ LocalizeText('navigator.roomsettings.roomfilter.removeword') }</Button>
</Grid>
</NitroCardContentView>
</NitroCardView>
);
};
@@ -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<boolean>(false);
const [ isOpen, setIsOpen ] = useState<boolean>(true);
const { promoteInformation, setPromoteInformation } = useRoomPromote();
useMessageEvent<DesktopViewEvent>(DesktopViewEvent, event =>
{
setPromoteInformation(null);
});
if(!promoteInformation) return null;
return (
<>
{ promoteInformation.data.adId !== -1 &&
<div className="px-[5px] py-[6px] [box-shadow:inset_0_5px_#22222799,_inset_0_-4px_#12121599] text-sm bg-[#1c1c20f2] rounded">
<div className="flex flex-col">
<Flex pointer alignItems="center" justifyContent="between" onClick={ event => setIsOpen(value => !value) }>
<Text overflow="hidden" variant="white">{ promoteInformation.data.eventName }</Text>
{ isOpen && <FaChevronUp className="fa-icon" /> }
{ !isOpen && <FaChevronDown className="fa-icon" /> }
</Flex>
{ (isOpen && GetSessionDataManager().userId !== promoteInformation.data.ownerAvatarId) &&
<RoomPromoteOtherEventWidgetView
eventDescription={ promoteInformation.data.eventDescription }
/>
}
{ (isOpen && GetSessionDataManager().userId === promoteInformation.data.ownerAvatarId) &&
<RoomPromoteMyOwnEventWidgetView
eventDescription={ promoteInformation.data.eventDescription }
setIsEditingPromote={ () => setIsEditingPromote(true) }
/>
}
{ isEditingPromote &&
<RoomPromoteEditWidgetView
eventDescription={ promoteInformation.data.eventDescription }
eventId={ promoteInformation.data.adId }
eventName={ promoteInformation.data.eventName }
setIsEditingPromote={ () => setIsEditingPromote(false) }
/>
}
</div>
</div>
}
</>
);
};
@@ -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<RoomPromoteEditWidgetViewProps> = props =>
{
const { eventId = -1, eventName = '', eventDescription = '', setIsEditingPromote = null } = props;
const [ newEventName, setNewEventName ] = useState<string>(eventName);
const [ newEventDescription, setNewEventDescription ] = useState<string>(eventDescription);
const updatePromote = () =>
{
SendMessageComposer(new EditEventMessageComposer(eventId, newEventName, newEventDescription));
setIsEditingPromote(false);
};
return (
<NitroCardView className="nitro-guide-tool" theme="primary-slim">
<NitroCardHeaderView headerText={ LocalizeText('navigator.eventsettings.editcaption') } onCloseClick={ () => setIsEditingPromote(false) } />
<NitroCardContentView className="text-black">
<div className="flex flex-col">
<Text bold>{ LocalizeText('navigator.eventsettings.name') }</Text>
<NitroInput maxLength={ 64 } placeholder={ LocalizeText('navigator.eventsettings.name') } type="text" value={ newEventName } onChange={ event => setNewEventName(event.target.value) } />
</div>
<div className="flex flex-col">
<Text bold>{ LocalizeText('navigator.eventsettings.desc') }</Text>
<textarea className="min-h-[calc(1.5em+ .5rem+2px)] px-[.5rem] py-[.25rem] rounded-[.2rem] form-control-sm" maxLength={ 64 } placeholder={ LocalizeText('navigator.eventsettings.desc') } value={ newEventDescription } onChange={ event => setNewEventDescription(event.target.value) }></textarea>
</div>
<div className="flex flex-col">
<Button fullWidth disabled={ !newEventName || !newEventDescription } variant={ (!newEventName || !newEventDescription) ? 'danger' : 'success' } onClick={ event => updatePromote() }>{ LocalizeText('navigator.eventsettings.edit') }</Button>
</div>
</NitroCardContentView>
</NitroCardView>
);
};
@@ -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<RoomPromoteMyOwnEventWidgetViewProps> = props =>
{
const { eventDescription = '', setIsEditingPromote = null } = props;
const { setIsExtended } = useRoomPromote();
const extendPromote = () =>
{
setIsExtended(true);
CreateLinkEvent('catalog/open/room_event');
};
return (
<>
<Flex alignItems="center" gap={ 2 } style={ { overflowWrap: 'anywhere' } }>
<Text variant="white">{ eventDescription }</Text>
</Flex>
<br /><br />
<Grid className="flex items-center justify-end gap-2">
<Button className="btn btn-primary w-full btn-sm" onClick={ event => setIsEditingPromote(true) }>{ LocalizeText('navigator.roominfo.editevent') }</Button>
<Button className="btn btn-success w-full btn-sm" onClick={ event => extendPromote() }>{ LocalizeText('roomad.extend.event') }</Button>
</Grid>
</>
);
};
@@ -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<RoomPromoteOtherEventWidgetViewProps> = props =>
{
const { eventDescription = '' } = props;
return (
<>
<Flex alignItems="center" gap={ 2 } style={ { overflowWrap: 'anywhere' } }>
<Text variant="white">{ eventDescription }</Text>
</Flex>
<br /><br />
<Column alignItems="center" gap={ 1 }>
<div className="bg-light-dark rounded relative overflow-hidden w-full">
<div className="flex justify-center items-center size-full absolute">
<Text center variant="white">{ LocalizeText('navigator.eventinprogress') }</Text>
</div>
<Text>&nbsp;</Text>
</div>
</Column>
</>
);
};
@@ -0,0 +1,3 @@
export * from './RoomPromoteEditWidgetView';
export * from './RoomPromoteMyOwnEventWidgetView';
export * from './RoomPromoteOtherEventWidgetView';
@@ -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 <LayoutMiniCameraView roomId={ roomSession.roomId } textureReceiver={ receiveTexture } onClose={ () => setIsVisible(false) } />;
};
@@ -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<boolean>(false);
const [roomName, setRoomName] = useState<string>(null);
const [roomOwner, setRoomOwner] = useState<string>(null);
const [roomTags, setRoomTags] = useState<string[]>(null);
const [isOpen, setIsOpen] = useState<boolean>(false);
const [isOpenHistory, setIsOpenHistory] = useState<boolean>(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>(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 (
<div className="flex space-x-2 nitro-room-tools-container">
<div className="flex flex-col items-center justify-center p-2 nitro-room-tools">
<div className="cursor-pointer nitro-icon icon-cog" title={LocalizeText('room.settings.button.text')} onClick={() => handleToolClick('settings')} />
<div className={classNames('cursor-pointer', 'nitro-icon', (!isZoomedIn && 'icon-zoom-less'), (isZoomedIn && 'icon-zoom-more'))} title={LocalizeText('room.zoom.button.text')} onClick={() => handleToolClick('zoom')} />
<div className="cursor-pointer nitro-icon icon-chat-history" title={LocalizeText('room.chathistory.button.text')} onClick={() => handleToolClick('chat_history')} />
<div className={classNames('cursor-pointer', 'nitro-icon', (areBubblesMuted ? 'icon-chat-disablebubble' : 'icon-chat-enablebubble'))} title={areBubblesMuted ? LocalizeText('room.unmute.button.text') : LocalizeText('room.mute.button.text')} onClick={() => handleToolClick('hiddenbubbles')} />
{navigatorData.canRate && (
<div className="cursor-pointer nitro-icon icon-like-room" title={LocalizeText('room.like.button.text')} onClick={() => handleToolClick('like_room')} />
)}
<div className="cursor-pointer nitro-icon icon-room-link" title={LocalizeText('navigator.embed.caption')} onClick={() => handleToolClick('toggle_room_link')} />
<div className="cursor-pointer nitro-icon icon-room-history-enabled" title={LocalizeText('room.history.button.tooltip')} onClick={() => handleToolClick('room_history')} />
</div>
<div className="flex flex-col justify-center">
<AnimatePresence>
{isOpen && (
<motion.div initial={{ x: -100 }} animate={{ x: 0 }} exit={{ x: -100 }} transition={{ duration: 0.3 }}>
<div className="flex flex-col items-center justify-center">
<div className="flex flex-col px-3 py-2 rounded nitro-room-tools-info">
<div className="flex flex-col gap-1">
<Text wrap fontSize={4} variant="white">{roomName}</Text>
<Text fontSize={5} variant="gray">{roomOwner}</Text>
</div>
{roomTags && roomTags.length > 0 && (
<div className="flex gap-2">
{roomTags.map((tag, index) => (
<Text key={index} pointer small className="p-1 rounded bg-primary" variant="white" onClick={() => handleToolClick('navigator_search_tag', tag)}>
#{tag}
</Text>
))}
</div>
)}
</div>
</div>
</motion.div>
)}
{isOpenHistory && (
<motion.div initial={{ x: -100 }} animate={{ x: 0 }} exit={{ x: -100 }} transition={{ duration: 0.3 }} className="nitro-room-tools-history">
<div className="flex flex-col px-3 py-2 rounded nitro-room-history">
{roomHistory.map(history => (
<Text key={history.roomId} bold={history.roomId === navigatorData.currentRoomId} variant={history.roomId === navigatorData.currentRoomId ? 'white' : 'muted'} pointer onClick={() => TryVisitRoom(history.roomId)}>
{history.roomName}
</Text>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
);
};
@@ -0,0 +1,24 @@
import { RoomObjectCategory } from '@nitrots/nitro-renderer';
import { FC } from 'react';
import { BaseProps } from '../../../../common';
import { useRoom } from '../../../../hooks';
import { ObjectLocationView } from '../object-location/ObjectLocationView';
interface UserLocationViewProps extends BaseProps<HTMLDivElement>
{
userId: number;
}
export const UserLocationView: FC<UserLocationViewProps> = props =>
{
const { userId = -1, ...rest } = props;
const { roomSession = null } = useRoom();
if((userId === -1) || !roomSession) return null;
const userData = roomSession.userDataManager.getUserData(userId);
if(!userData) return null;
return <ObjectLocationView category={ RoomObjectCategory.UNIT } objectId={ userData.roomIndex } { ...rest } />;
};
@@ -0,0 +1,44 @@
import { FC } from 'react';
import { VALUE_KEY_DISLIKE, VALUE_KEY_LIKE } from '../../../../api';
import { Column, Flex, Text } from '../../../../common';
interface WordQuizQuestionViewProps
{
question: string;
canVote: boolean;
vote(value: string): void;
noVotes: number;
yesVotes: number;
}
export const WordQuizQuestionView: FC<WordQuizQuestionViewProps> = props =>
{
const { question = null, canVote = null, vote = null, noVotes = null, yesVotes = null } = props;
return (
<Column className="wordquiz-question p-2" gap={ 2 }>
{ !canVote &&
<div className="flex w-full items-center gap-2">
<div className="flex items-center justify-center cursor-pointer bg-danger rounded p-2">
<Text variant="white">{ noVotes }</Text>
</div>
<Text center textBreak variant="white">{ question }</Text>
<div className="flex items-center justify-center cursor-pointer bg-success rounded p-2">
<Text variant="white">{ yesVotes }</Text>
</div>
</div> }
{ canVote &&
<div className="flex flex-col">
<Text center textBreak variant="white">{ question }</Text>
<div className="flex w-full gap-1 justify-center">
<Flex center pointer className="bg-danger rounded p-1" onClick={ event => vote(VALUE_KEY_DISLIKE) }>
<div className="word-quiz-dislike" />
</Flex>
<Flex center pointer className="bg-success rounded p-1" onClick={ event => vote(VALUE_KEY_LIKE) }>
<div className="word-quiz-like" />
</Flex>
</div>
</div> }
</Column>
);
};
@@ -0,0 +1,24 @@
import { RoomObjectCategory } from '@nitrots/nitro-renderer';
import { FC } from 'react';
import { VALUE_KEY_DISLIKE } from '../../../../api';
import { BaseProps } from '../../../../common';
import { ObjectLocationView } from '../object-location/ObjectLocationView';
interface WordQuizVoteViewProps extends BaseProps<HTMLDivElement>
{
userIndex: number;
vote: string;
}
export const WordQuizVoteView: FC<WordQuizVoteViewProps> = props =>
{
const { userIndex = null, vote = null, ...rest } = props;
return (
<ObjectLocationView category={ RoomObjectCategory.UNIT } objectId={ userIndex } { ...rest }>
<div className={ `flex justify-center items-center cursor-pointer bg-${ (vote === VALUE_KEY_DISLIKE) ? 'danger' : 'success' } rounded p-1` }>
<div className={ `word-quiz-${ (vote === VALUE_KEY_DISLIKE) ? 'dislike' : 'like' }-sm` } />
</div>
</ObjectLocationView>
);
};
@@ -0,0 +1,19 @@
import { FC } from 'react';
import { VALUE_KEY_DISLIKE, VALUE_KEY_LIKE } from '../../../../api';
import { useWordQuizWidget } from '../../../../hooks';
import { WordQuizQuestionView } from './WordQuizQuestionView';
import { WordQuizVoteView } from './WordQuizVoteView';
export const WordQuizWidgetView: FC<{}> = props =>
{
const { question = null, answerSent = false, answerCounts = null, userAnswers = null, vote = null } = useWordQuizWidget();
return (
<>
{ question &&
<WordQuizQuestionView canVote={ !answerSent } noVotes={ answerCounts.get(VALUE_KEY_DISLIKE) || 0 } question={ question.content } vote={ vote } yesVotes={ answerCounts.get(VALUE_KEY_LIKE) || 0 } /> }
{ userAnswers &&
Array.from(userAnswers.entries()).map(([ key, value ], index) => <WordQuizVoteView key={ index } userIndex={ key } vote={ value.value } />) }
</>
);
};