mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 07:26:19 +00:00
🆙 Init V3
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
+31
@@ -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>
|
||||
);
|
||||
};
|
||||
+23
@@ -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 }` }> </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>
|
||||
);
|
||||
};
|
||||
+104
@@ -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` } />
|
||||
</>);
|
||||
};
|
||||
+29
@@ -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> </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 } />) }
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user