🆙 Remove wrong fixes fix the right 🆙

This commit is contained in:
Remco
2026-01-27 12:23:12 +01:00
parent b032566b53
commit 5c88770fa6
348 changed files with 4 additions and 31746 deletions
-11
View File
@@ -1,11 +0,0 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# You can see what browsers were selected by your queries by running:
# npx browserslist
last 1 Chrome version
last 1 Firefox version
last 1 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
-16
View File
@@ -1,16 +0,0 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false
-5
View File
@@ -1,5 +0,0 @@
{
"css.validate": false,
"scss.validate": false,
"stylelint.enable": true
}
-69
View File
@@ -1,69 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1"
/>
<link
rel="apple-touch-icon"
sizes="180x180"
href="/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/favicon-16x16.png"
/>
<link
rel="manifest"
crossorigin="use-credentials"
href="/site.webmanifest"
/>
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#000000" />
<meta name="apple-mobile-web-app-title" content="Nitro" />
<meta name="application-name" content="Nitro" />
<meta name="msapplication-TileColor" content="#000000" />
<meta name="theme-color" content="#000000" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta
name="apple-mobile-web-app-status-bar-style"
content="black-translucent"
/>
<base href="./" />
<title>Nitro</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root" class="w-full h-full"></div>
<script>
window.NitroConfig = {
"config.urls": ["/renderer-config.json", "/ui-config.json"],
"sso.ticket":
new URLSearchParams(window.location.search).get("sso") ||
null,
"forward.type": new URLSearchParams(window.location.search).get(
"room",
)
? 2
: -1,
"forward.id":
new URLSearchParams(window.location.search).get("room") ||
0,
"friend.id":
new URLSearchParams(window.location.search).get("friend") ||
0,
};
</script>
<script type="module" src="./src/index.tsx"></script>
</body>
</html>
@@ -20,7 +20,7 @@ export const LayoutBadgeImageView: FC<LayoutBadgeImageViewProps> = props =>
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'relative w-[40px] h-[40px] bg-no-repeat bg-center z-50' ];
const newClassNames: string[] = [ 'relative w-[40px] h-[40px] bg-no-repeat bg-center' ];
if(isGroup) newClassNames.push('group-badge');
@@ -31,7 +31,6 @@ export const LayoutBadgeImageView: FC<LayoutBadgeImageViewProps> = props =>
return newClassNames;
}, [ classNames, isGroup, isGrayscale ]);
const getStyle = useMemo(() =>
{
let newStyle: CSSProperties = {};
@@ -41,8 +40,6 @@ export const LayoutBadgeImageView: FC<LayoutBadgeImageViewProps> = props =>
newStyle.backgroundImage = `url(${ (isGroup) ? imageElement.src : GetConfigurationValue<string>('badge.asset.url').replace('%badgename%', badgeCode.toString()) })`;
newStyle.width = imageElement.width;
newStyle.height = imageElement.height;
newStyle.zIndex = 50;
newStyle.position = 'relative';
if(scale !== 1)
{
@@ -1,4 +1,4 @@
import { FC, ReactNode } from 'react';
import { FC } from 'react';
import { LocalizeText } from '../../api';
import { Base } from '../Base';
import { Column } from '../Column';
@@ -14,22 +14,20 @@ interface LayoutTrophyViewProps
senderName: string;
customTitle?: string;
onCloseClick: () => void;
children?: React.ReactNode;
}
export const LayoutTrophyView: FC<LayoutTrophyViewProps> = props =>
{
const { color = '', message = '', date = '', senderName = '', customTitle = null, onCloseClick = null, children = null } = props;
const { color = '', message = '', date = '', senderName = '', customTitle = null, onCloseClick = null } = props;
return (
<DraggableWindow handleSelector=".drag-handler">
<Column alignItems="center" className={ `nitro-layout-trophy trophy-${ color }` } gap={ 0 }>
<Column alignItems="center" className={ `nitro-layout-trophy trophy-${ color } z-50` } gap={ 0 }>
<Flex center fullWidth className="trophy-header drag-handler" position="relative">
<Base pointer className="trophy-close" position="absolute" onClick={ onCloseClick } />
<Text bold>{ LocalizeText('widget.furni.trophy.title') }</Text>
</Flex>
<Column className="trophy-content py-1" gap={ 1 }>
{ children }
{ customTitle &&
<Text bold>{ customTitle }</Text> }
{ message }
@@ -1,176 +0,0 @@
import { CallForHelpTopicData, DefaultSanctionMessageComposer, ModAlertMessageComposer, ModBanMessageComposer, ModKickMessageComposer, ModMessageMessageComposer, ModMuteMessageComposer, ModTradingLockMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useMemo, useState } from 'react';
import { ISelectedUser, LocalizeText, ModActionDefinition, NotificationAlertType, SendMessageComposer } from '../../../../api';
import { Button, DraggableWindowPosition, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
import { useModTools, useNotification } from '../../../../hooks';
interface ModToolsUserModActionViewProps
{
user: ISelectedUser;
onCloseClick: () => void;
}
const MOD_ACTION_DEFINITIONS = [
new ModActionDefinition(1, 'Alert', ModActionDefinition.ALERT, 1, 0),
new ModActionDefinition(2, 'Mute 1h', ModActionDefinition.MUTE, 2, 0),
new ModActionDefinition(3, 'Ban 18h', ModActionDefinition.BAN, 3, 0),
new ModActionDefinition(4, 'Ban 7 days', ModActionDefinition.BAN, 4, 0),
new ModActionDefinition(5, 'Ban 30 days (step 1)', ModActionDefinition.BAN, 5, 0),
new ModActionDefinition(7, 'Ban 30 days (step 2)', ModActionDefinition.BAN, 7, 0),
new ModActionDefinition(6, 'Ban 100 years', ModActionDefinition.BAN, 6, 0),
new ModActionDefinition(106, 'Ban avatar-only 100 years', ModActionDefinition.BAN, 6, 0),
new ModActionDefinition(101, 'Kick', ModActionDefinition.KICK, 0, 0),
new ModActionDefinition(102, 'Lock trade 1 week', ModActionDefinition.TRADE_LOCK, 0, 168),
new ModActionDefinition(104, 'Lock trade permanent', ModActionDefinition.TRADE_LOCK, 0, 876000),
new ModActionDefinition(105, 'Message', ModActionDefinition.MESSAGE, 0, 0),
];
export const ModToolsUserModActionView: FC<ModToolsUserModActionViewProps> = props =>
{
const { user = null, onCloseClick = null } = props;
const [ selectedTopic, setSelectedTopic ] = useState(-1);
const [ selectedAction, setSelectedAction ] = useState(-1);
const [ message, setMessage ] = useState<string>('');
const { cfhCategories = null, settings = null } = useModTools();
const { simpleAlert = null } = useNotification();
const topics = useMemo(() =>
{
const values: CallForHelpTopicData[] = [];
if(cfhCategories && cfhCategories.length)
{
for(const category of cfhCategories)
{
for(const topic of category.topics) values.push(topic);
}
}
return values;
}, [ cfhCategories ]);
const sendAlert = (message: string) => simpleAlert(message, NotificationAlertType.DEFAULT, null, null, 'Error');
const sendDefaultSanction = () =>
{
let errorMessage: string = null;
const category = topics[selectedTopic];
if(selectedTopic === -1) errorMessage = 'You must select a CFH topic';
if(errorMessage) return sendAlert(errorMessage);
const messageOrDefault = (message.trim().length === 0) ? LocalizeText(`help.cfh.topic.${ category.id }`) : message;
SendMessageComposer(new DefaultSanctionMessageComposer(user.userId, selectedTopic, messageOrDefault));
onCloseClick();
};
const sendSanction = () =>
{
let errorMessage: string = null;
const category = topics[selectedTopic];
const sanction = MOD_ACTION_DEFINITIONS[selectedAction];
if((selectedTopic === -1) || (selectedAction === -1)) errorMessage = 'You must select a CFH topic and Sanction';
else if(!settings || !settings.cfhPermission) errorMessage = 'You do not have permission to do this';
else if(!category) errorMessage = 'You must select a CFH topic';
else if(!sanction) errorMessage = 'You must select a sanction';
if(errorMessage)
{
sendAlert(errorMessage);
return;
}
const messageOrDefault = (message.trim().length === 0) ? LocalizeText(`help.cfh.topic.${ category.id }`) : message;
switch(sanction.actionType)
{
case ModActionDefinition.ALERT: {
if(!settings.alertPermission)
{
sendAlert('You have insufficient permissions');
return;
}
SendMessageComposer(new ModAlertMessageComposer(user.userId, messageOrDefault, category.id));
break;
}
case ModActionDefinition.MUTE:
SendMessageComposer(new ModMuteMessageComposer(user.userId, messageOrDefault, category.id));
break;
case ModActionDefinition.BAN: {
if(!settings.banPermission)
{
sendAlert('You have insufficient permissions');
return;
}
SendMessageComposer(new ModBanMessageComposer(user.userId, messageOrDefault, category.id, selectedAction, (sanction.actionId === 106)));
break;
}
case ModActionDefinition.KICK: {
if(!settings.kickPermission)
{
sendAlert('You have insufficient permissions');
return;
}
SendMessageComposer(new ModKickMessageComposer(user.userId, messageOrDefault, category.id));
break;
}
case ModActionDefinition.TRADE_LOCK: {
const numSeconds = (sanction.actionLengthHours * 60);
SendMessageComposer(new ModTradingLockMessageComposer(user.userId, messageOrDefault, numSeconds, category.id));
break;
}
case ModActionDefinition.MESSAGE: {
if(message.trim().length === 0)
{
sendAlert('Please write a message to user');
return;
}
SendMessageComposer(new ModMessageMessageComposer(user.userId, message, category.id));
break;
}
}
onCloseClick();
};
if(!user) return null;
return (
<NitroCardView className="nitro-mod-tools-user-action" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
<NitroCardHeaderView headerText={ 'Mod Action: ' + (user ? user.username : '') } onCloseClick={ () => onCloseClick() } />
<NitroCardContentView className="text-black">
<select className="form-select form-select-sm" value={ selectedTopic } onChange={ event => setSelectedTopic(parseInt(event.target.value)) }>
<option disabled value={ -1 }>CFH Topic</option>
{ topics.map((topic, index) => <option key={ index } value={ index }>{ LocalizeText('help.cfh.topic.' + topic.id) }</option>) }
</select>
<select className="form-select form-select-sm" value={ selectedAction } onChange={ event => setSelectedAction(parseInt(event.target.value)) }>
<option disabled value={ -1 }>Sanction Type</option>
{ MOD_ACTION_DEFINITIONS.map((action, index) => <option key={ index } value={ index }>{ action.name }</option>) }
</select>
<div className="flex flex-col gap-1">
<Text small>Optional message type, overrides default</Text>
<textarea className="min-h-[calc(1.5em+ .5rem+2px)] px-[.5rem] py-[.25rem] rounded-[.2rem]" value={ message } onChange={ event => setMessage(event.target.value) } />
</div>
<Flex gap={ 1 } justifyContent="between">
<Button variant="primary" onClick={ sendDefaultSanction }>Default Sanction</Button>
<Button variant="success" onClick={ sendSanction }>Sanction</Button>
</Flex>
</NitroCardContentView>
</NitroCardView>
);
};
@@ -1,60 +0,0 @@
import { GetRoomVisitsMessageComposer, RoomVisitsData, RoomVisitsEvent } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { SendMessageComposer, TryVisitRoom } from '../../../../api';
import { Column, DraggableWindowPosition, Grid, InfiniteScroll, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
import { useMessageEvent } from '../../../../hooks';
interface ModToolsUserRoomVisitsViewProps
{
userId: number;
onCloseClick: () => void;
}
export const ModToolsUserRoomVisitsView: FC<ModToolsUserRoomVisitsViewProps> = props =>
{
const { userId = null, onCloseClick = null } = props;
const [ roomVisitData, setRoomVisitData ] = useState<RoomVisitsData>(null);
useMessageEvent<RoomVisitsEvent>(RoomVisitsEvent, event =>
{
const parser = event.getParser();
if(parser.data.userId !== userId) return;
setRoomVisitData(parser.data);
});
useEffect(() =>
{
SendMessageComposer(new GetRoomVisitsMessageComposer(userId));
}, [ userId ]);
if(!userId) return null;
return (
<NitroCardView className="nitro-mod-tools-user-visits" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
<NitroCardHeaderView headerText={ 'User Visits' } onCloseClick={ onCloseClick } />
<NitroCardContentView className="text-black" gap={ 1 }>
<Column fullHeight gap={ 0 } overflow="hidden">
<Column gap={ 2 }>
<Grid className="text-black font-bold border-bottom pb-1" gap={ 1 }>
<div className="col-span-2">Time</div>
<div className="col-span-7">Room name</div>
<div className="col-span-3">Visit</div>
</Grid>
</Column>
<InfiniteScroll rowRender={ row =>
{
return (
<Grid alignItems="center" className="text-black py-1 border-bottom" fullHeight={ false } gap={ 1 }>
<Text className="col-span-2">{ row.enterHour.toString().padStart(2, '0') }: { row.enterMinute.toString().padStart(2, '0') }</Text>
<Text className="col-span-7">{ row.roomName }</Text>
<Text bold pointer underline className="col-span-3" variant="primary" onClick={ event => TryVisitRoom(row.roomId) }>Visit Room</Text>
</Grid>
);
} } rows={ roomVisitData?.rooms ?? [] } />
</Column>
</NitroCardContentView>
</NitroCardView>
);
};
@@ -1,45 +0,0 @@
import { ModMessageMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useState } from 'react';
import { ISelectedUser, SendMessageComposer } from '../../../../api';
import { Button, DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
import { useNotification } from '../../../../hooks';
interface ModToolsUserSendMessageViewProps
{
user: ISelectedUser;
onCloseClick: () => void;
}
export const ModToolsUserSendMessageView: FC<ModToolsUserSendMessageViewProps> = props =>
{
const { user = null, onCloseClick = null } = props;
const [ message, setMessage ] = useState('');
const { simpleAlert = null } = useNotification();
if(!user) return null;
const sendMessage = () =>
{
if(message.trim().length === 0)
{
simpleAlert('Please write a message to user.', null, null, null, 'Error', null);
return;
}
SendMessageComposer(new ModMessageMessageComposer(user.userId, message, -999));
onCloseClick();
};
return (
<NitroCardView className="nitro-mod-tools-user-message" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
<NitroCardHeaderView headerText={ 'Send Message' } onCloseClick={ () => onCloseClick() } />
<NitroCardContentView className="text-black">
<Text>Message To: { user.username }</Text>
<textarea className="min-h-[calc(1.5em+ .5rem+2px)] px-[.5rem] py-[.25rem] rounded-[.2rem]" value={ message } onChange={ event => setMessage(event.target.value) }></textarea>
<Button fullWidth onClick={ sendMessage }>Send message</Button>
</NitroCardContentView>
</NitroCardView>
);
};
@@ -1,156 +0,0 @@
import { CreateLinkEvent, GetModeratorUserInfoMessageComposer, ModeratorUserInfoData, ModeratorUserInfoEvent } from '@nitrots/nitro-renderer';
import { FC, useEffect, useMemo, useState } from 'react';
import { FriendlyTime, LocalizeText, SendMessageComposer } from '../../../../api';
import { Button, Column, DraggableWindowPosition, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common';
import { useMessageEvent } from '../../../../hooks';
import { ModToolsUserModActionView } from './ModToolsUserModActionView';
import { ModToolsUserRoomVisitsView } from './ModToolsUserRoomVisitsView';
import { ModToolsUserSendMessageView } from './ModToolsUserSendMessageView';
interface ModToolsUserViewProps
{
userId: number;
onCloseClick: () => void;
}
export const ModToolsUserView: FC<ModToolsUserViewProps> = props =>
{
const { onCloseClick = null, userId = null } = props;
const [ userInfo, setUserInfo ] = useState<ModeratorUserInfoData>(null);
const [ sendMessageVisible, setSendMessageVisible ] = useState(false);
const [ modActionVisible, setModActionVisible ] = useState(false);
const [ roomVisitsVisible, setRoomVisitsVisible ] = useState(false);
const userProperties = useMemo(() =>
{
if(!userInfo) return null;
return [
{
localeKey: 'modtools.userinfo.userName',
value: userInfo.userName,
showOnline: true
},
{
localeKey: 'modtools.userinfo.cfhCount',
value: userInfo.cfhCount.toString()
},
{
localeKey: 'modtools.userinfo.abusiveCfhCount',
value: userInfo.abusiveCfhCount.toString()
},
{
localeKey: 'modtools.userinfo.cautionCount',
value: userInfo.cautionCount.toString()
},
{
localeKey: 'modtools.userinfo.banCount',
value: userInfo.banCount.toString()
},
{
localeKey: 'modtools.userinfo.lastSanctionTime',
value: userInfo.lastSanctionTime
},
{
localeKey: 'modtools.userinfo.tradingLockCount',
value: userInfo.tradingLockCount.toString()
},
{
localeKey: 'modtools.userinfo.tradingExpiryDate',
value: userInfo.tradingExpiryDate
},
{
localeKey: 'modtools.userinfo.minutesSinceLastLogin',
value: FriendlyTime.format(userInfo.minutesSinceLastLogin * 60, '.ago', 2)
},
{
localeKey: 'modtools.userinfo.lastPurchaseDate',
value: userInfo.lastPurchaseDate
},
{
localeKey: 'modtools.userinfo.primaryEmailAddress',
value: userInfo.primaryEmailAddress
},
{
localeKey: 'modtools.userinfo.identityRelatedBanCount',
value: userInfo.identityRelatedBanCount.toString()
},
{
localeKey: 'modtools.userinfo.registrationAgeInMinutes',
value: FriendlyTime.format(userInfo.registrationAgeInMinutes * 60, '.ago', 2)
},
{
localeKey: 'modtools.userinfo.userClassification',
value: userInfo.userClassification
}
];
}, [ userInfo ]);
useMessageEvent<ModeratorUserInfoEvent>(ModeratorUserInfoEvent, event =>
{
const parser = event.getParser();
if(!parser || parser.data.userId !== userId) return;
setUserInfo(parser.data);
});
useEffect(() =>
{
SendMessageComposer(new GetModeratorUserInfoMessageComposer(userId));
}, [ userId ]);
if(!userInfo) return null;
return (
<>
<NitroCardView className="nitro-mod-tools-user" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
<NitroCardHeaderView headerText={ LocalizeText('modtools.userinfo.title', [ 'username' ], [ userInfo.userName ]) } onCloseClick={ () => onCloseClick() } />
<NitroCardContentView className="text-black">
<Grid overflow="hidden">
<Column overflow="auto" size={ 8 }>
<table className="table table-striped table-sm table-text-small text-black m-0">
<tbody>
{ userProperties.map( (property, index) =>
{
return (
<tr key={ index }>
<th scope="row">{ LocalizeText(property.localeKey) }</th>
<td>
{ property.value }
{ property.showOnline &&
<i className={ `icon icon-pf-${ userInfo.online ? 'online' : 'offline' } ms-2` } /> }
</td>
</tr>
);
}) }
</tbody>
</table>
</Column>
<Column gap={ 1 } size={ 4 }>
<Button onClick={ event => CreateLinkEvent(`mod-tools/open-user-chatlog/${ userId }`) }>
Room Chat
</Button>
<Button onClick={ event => setSendMessageVisible(!sendMessageVisible) }>
Send Message
</Button>
<Button onClick={ event => setRoomVisitsVisible(!roomVisitsVisible) }>
Room Visits
</Button>
<Button onClick={ event => setModActionVisible(!modActionVisible) }>
Mod Action
</Button>
</Column>
</Grid>
</NitroCardContentView>
</NitroCardView>
{ sendMessageVisible &&
<ModToolsUserSendMessageView user={ { userId: userId, username: userInfo.userName } } onCloseClick={ () => setSendMessageVisible(false) } /> }
{ modActionVisible &&
<ModToolsUserModActionView user={ { userId: userId, username: userInfo.userName } } onCloseClick={ () => setModActionVisible(false) } /> }
{ roomVisitsVisible &&
<ModToolsUserRoomVisitsView userId={ userId } onCloseClick={ () => setRoomVisitsVisible(false) } /> }
</>
);
};
@@ -1,240 +0,0 @@
import { NitroCard } from '@layout/NitroCard';
import { AddLinkEventTracker, ConvertGlobalRoomIdMessageComposer, HabboWebTools, ILinkEventTracker, LegacyExternalInterface, NavigatorInitComposer, NavigatorSearchComposer, RemoveLinkEventTracker, RoomSessionEvent } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useRef, useState } from 'react';
import { FaPlus } from 'react-icons/fa';
import { LocalizeText, SendMessageComposer, TryVisitRoom } from '../../api';
import { useNavigator, useNitroEvent } from '../../hooks';
import { NavigatorDoorStateView } from './views/NavigatorDoorStateView';
import { NavigatorRoomCreatorView } from './views/NavigatorRoomCreatorView';
import { NavigatorRoomInfoView } from './views/NavigatorRoomInfoView';
import { NavigatorRoomLinkView } from './views/NavigatorRoomLinkView';
import { NavigatorRoomSettingsView } from './views/room-settings/NavigatorRoomSettingsView';
import { NavigatorSearchResultView } from './views/search/NavigatorSearchResultView';
import { NavigatorSearchView } from './views/search/NavigatorSearchView';
export const NavigatorView: FC<{}> = props =>
{
const [ isVisible, setIsVisible ] = useState(false);
const [ isReady, setIsReady ] = useState(false);
const [ isCreatorOpen, setCreatorOpen ] = useState(false);
const [ isRoomInfoOpen, setRoomInfoOpen ] = useState(false);
const [ isRoomLinkOpen, setRoomLinkOpen ] = useState(false);
const [ isLoading, setIsLoading ] = useState(false);
const [ needsInit, setNeedsInit ] = useState(true);
const [ needsSearch, setNeedsSearch ] = useState(false);
const { searchResult = null, topLevelContext = null, topLevelContexts = null, navigatorData = null } = useNavigator();
const pendingSearch = useRef<{ value: string, code: string }>(null);
const elementRef = useRef<HTMLDivElement>();
useNitroEvent<RoomSessionEvent>(RoomSessionEvent.CREATED, event =>
{
setIsVisible(false);
setCreatorOpen(false);
});
const sendSearch = useCallback((searchValue: string, contextCode: string) =>
{
setCreatorOpen(false);
SendMessageComposer(new NavigatorSearchComposer(contextCode, searchValue));
setIsLoading(true);
}, []);
const reloadCurrentSearch = useCallback(() =>
{
if(!isReady)
{
setNeedsSearch(true);
return;
}
if(pendingSearch.current)
{
sendSearch(pendingSearch.current.value, pendingSearch.current.code);
pendingSearch.current = null;
return;
}
if(searchResult)
{
sendSearch(searchResult.data, searchResult.code);
return;
}
if(!topLevelContext) return;
sendSearch('', topLevelContext.code);
}, [ isReady, searchResult, topLevelContext, sendSearch ]);
useEffect(() =>
{
const linkTracker: ILinkEventTracker = {
linkReceived: (url: string) =>
{
const parts = url.split('/');
if(parts.length < 2) return;
switch(parts[1])
{
case 'show': {
setIsVisible(true);
setNeedsSearch(true);
return;
}
case 'hide':
setIsVisible(false);
return;
case 'toggle': {
if(isVisible)
{
setIsVisible(false);
return;
}
setIsVisible(true);
setNeedsSearch(true);
return;
}
case 'toggle-room-info':
setRoomInfoOpen(value => !value);
return;
case 'toggle-room-link':
setRoomLinkOpen(value => !value);
return;
case 'goto':
if(parts.length <= 2) return;
switch(parts[2])
{
case 'home':
if(navigatorData.homeRoomId <= 0) return;
TryVisitRoom(navigatorData.homeRoomId);
break;
default: {
const roomId = parseInt(parts[2]);
TryVisitRoom(roomId);
}
}
return;
case 'create':
setIsVisible(true);
setCreatorOpen(true);
return;
case 'search':
if(parts.length > 2)
{
const topLevelContextCode = parts[2];
let searchValue = '';
if(parts.length > 3) searchValue = parts[3];
pendingSearch.current = { value: searchValue, code: topLevelContextCode };
setIsVisible(true);
setNeedsSearch(true);
}
return;
}
},
eventUrlPrefix: 'navigator/'
};
AddLinkEventTracker(linkTracker);
return () => RemoveLinkEventTracker(linkTracker);
}, [ isVisible, navigatorData ]);
useEffect(() =>
{
if(!searchResult) return;
setIsLoading(false);
if(elementRef && elementRef.current) elementRef.current.scrollTop = 0;
}, [ searchResult ]);
useEffect(() =>
{
if(!isVisible || !isReady || !needsSearch) return;
reloadCurrentSearch();
setNeedsSearch(false);
}, [ isVisible, isReady, needsSearch, reloadCurrentSearch ]);
useEffect(() =>
{
if(isReady || !topLevelContext) return;
setIsReady(true);
}, [ isReady, topLevelContext ]);
useEffect(() =>
{
if(!isVisible || !needsInit) return;
SendMessageComposer(new NavigatorInitComposer());
setNeedsInit(false);
}, [ isVisible, needsInit ]);
useEffect(() =>
{
LegacyExternalInterface.addCallback(HabboWebTools.OPENROOM, (k: string, _arg_2: boolean = false, _arg_3: string = null) => SendMessageComposer(new ConvertGlobalRoomIdMessageComposer(k)));
}, []);
return (
<>
{ isVisible &&
<NitroCard
className="w-navigator-w h-navigator-h min-w-navigator-w min-h-navigator-h"
uniqueKey="navigator">
<NitroCard.Header
headerText={ LocalizeText(isCreatorOpen ? 'navigator.createroom.title' : 'navigator.title') }
onCloseClick={ event => setIsVisible(false) } />
<NitroCard.Tabs>
{ topLevelContexts && (topLevelContexts.length > 0) && topLevelContexts.map((context, index) =>
{
return (
<NitroCard.TabItem
key={ index }
isActive={ ((topLevelContext === context) && !isCreatorOpen) }
onClick={ event => sendSearch('', context.code) }>
{ LocalizeText(('navigator.toplevelview.' + context.code)) }
</NitroCard.TabItem>
);
}) }
<NitroCard.TabItem
isActive={ isCreatorOpen }
onClick={ event => setCreatorOpen(true) }>
<FaPlus className="fa-icon" />
</NitroCard.TabItem>
</NitroCard.Tabs>
<NitroCard.Content isLoading={ isLoading }>
{ !isCreatorOpen &&
<>
<NavigatorSearchView sendSearch={ sendSearch } />
<div ref={ elementRef } className="flex flex-col overflow-auto gap-2">
{ (searchResult && searchResult.results.map((result, index) => <NavigatorSearchResultView key={ index } searchResult={ result } />)) }
</div>
</> }
{ isCreatorOpen && <NavigatorRoomCreatorView /> }
</NitroCard.Content>
</NitroCard> }
<NavigatorDoorStateView />
{ isRoomInfoOpen && <NavigatorRoomInfoView onCloseClick={ () => setRoomInfoOpen(false) } /> }
{ isRoomLinkOpen && <NavigatorRoomLinkView onCloseClick={ () => setRoomLinkOpen(false) } /> }
<NavigatorRoomSettingsView />
</>
);
};
@@ -1,111 +0,0 @@
import { FC, useEffect, useState } from 'react';
import { CreateRoomSession, DoorStateType, GoToDesktop, LocalizeText } from '../../../api';
import { Button, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../common';
import { useNavigator } from '../../../hooks';
import { NitroInput } from '../../../layout';
const VISIBLE_STATES = [ DoorStateType.START_DOORBELL, DoorStateType.STATE_WAITING, DoorStateType.STATE_NO_ANSWER, DoorStateType.START_PASSWORD, DoorStateType.STATE_WRONG_PASSWORD ];
const DOORBELL_STATES = [ DoorStateType.START_DOORBELL, DoorStateType.STATE_WAITING, DoorStateType.STATE_NO_ANSWER ];
const PASSWORD_STATES = [ DoorStateType.START_PASSWORD, DoorStateType.STATE_WRONG_PASSWORD ];
export const NavigatorDoorStateView: FC<{}> = props =>
{
const [ password, setPassword ] = useState('');
const { doorData = null, setDoorData = null } = useNavigator();
const onClose = () =>
{
if(doorData && (doorData.state === DoorStateType.STATE_WAITING)) GoToDesktop();
setDoorData(null);
};
const ring = () =>
{
if(!doorData || !doorData.roomInfo) return;
CreateRoomSession(doorData.roomInfo.roomId);
setDoorData(prevValue =>
{
const newValue = { ...prevValue };
newValue.state = DoorStateType.STATE_PENDING_SERVER;
return newValue;
});
};
const tryEntering = () =>
{
if(!doorData || !doorData.roomInfo) return;
CreateRoomSession(doorData.roomInfo.roomId, password);
setDoorData(prevValue =>
{
const newValue = { ...prevValue };
newValue.state = DoorStateType.STATE_PENDING_SERVER;
return newValue;
});
};
useEffect(() =>
{
if(!doorData || (doorData.state !== DoorStateType.STATE_NO_ANSWER)) return;
GoToDesktop();
}, [ doorData ]);
if(!doorData || (doorData.state === DoorStateType.NONE) || (VISIBLE_STATES.indexOf(doorData.state) === -1)) return null;
const isDoorbell = (DOORBELL_STATES.indexOf(doorData.state) >= 0);
return (
<NitroCardView className="nitro-navigator-doorbell" theme="primary-slim">
<NitroCardHeaderView headerText={ LocalizeText(isDoorbell ? 'navigator.doorbell.title' : 'navigator.password.title') } onCloseClick={ onClose } />
<NitroCardContentView>
<div className="flex flex-col gap-1">
<Text bold>{ doorData && doorData.roomInfo && doorData.roomInfo.roomName }</Text>
{ (doorData.state === DoorStateType.START_DOORBELL) &&
<Text>{ LocalizeText('navigator.doorbell.info') }</Text> }
{ (doorData.state === DoorStateType.STATE_WAITING) &&
<Text>{ LocalizeText('navigator.doorbell.waiting') }</Text> }
{ (doorData.state === DoorStateType.STATE_NO_ANSWER) &&
<Text>{ LocalizeText('navigator.doorbell.no.answer') }</Text> }
{ (doorData.state === DoorStateType.START_PASSWORD) &&
<Text>{ LocalizeText('navigator.password.info') }</Text> }
{ (doorData.state === DoorStateType.STATE_WRONG_PASSWORD) &&
<Text>{ LocalizeText('navigator.password.retryinfo') }</Text> }
</div>
{ isDoorbell &&
<div className="flex flex-col gap-1">
{ (doorData.state === DoorStateType.START_DOORBELL) &&
<Button variant="success" onClick={ ring }>
{ LocalizeText('navigator.doorbell.button.ring') }
</Button> }
<Button variant="danger" onClick={ onClose }>
{ LocalizeText('generic.cancel') }
</Button>
</div> }
{ !isDoorbell &&
<>
<div className="flex flex-col gap-1">
<Text>{ LocalizeText('navigator.password.enter') }</Text>
<NitroInput type="password" onChange={ event => setPassword(event.target.value) } />
</div>
<div className="flex flex-col gap-1">
<Button variant="success" onClick={ tryEntering }>
{ LocalizeText('navigator.password.button.try') }
</Button>
<Button variant="danger" onClick={ onClose }>
{ LocalizeText('generic.cancel') }
</Button>
</div>
</> }
</NitroCardContentView>
</NitroCardView>
);
};
@@ -1,123 +0,0 @@
import { CreateFlatMessageComposer, HabboClubLevelEnum } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { GetClubMemberLevel, GetConfigurationValue, IRoomModel, LocalizeText, SendMessageComposer } from '../../../api';
import { Button, Flex, Grid, LayoutCurrencyIcon, LayoutGridItem, Text } from '../../../common';
import { useNavigator } from '../../../hooks';
import { NitroInput } from '../../../layout';
export const NavigatorRoomCreatorView: FC<{}> = props =>
{
const [ maxVisitorsList, setMaxVisitorsList ] = useState<number[]>(null);
const [ name, setName ] = useState<string>(null);
const [ description, setDescription ] = useState<string>(null);
const [ category, setCategory ] = useState<number>(null);
const [ visitorsCount, setVisitorsCount ] = useState<number>(null);
const [ tradesSetting, setTradesSetting ] = useState<number>(0);
const [ roomModels, setRoomModels ] = useState<IRoomModel[]>([]);
const [ selectedModelName, setSelectedModelName ] = useState<string>('');
const { categories = null } = useNavigator();
const hcDisabled = GetConfigurationValue<boolean>('hc.disabled', false);
const getRoomModelImage = (name: string) => GetConfigurationValue<string>('images.url') + `/navigator/models/model_${ name }.png`;
const selectModel = (model: IRoomModel, index: number) =>
{
if(!model || (model.clubLevel > GetClubMemberLevel())) return;
setSelectedModelName(roomModels[index].name);
};
const createRoom = () =>
{
SendMessageComposer(new CreateFlatMessageComposer(name, description, 'model_' + selectedModelName, Number(category), Number(visitorsCount), tradesSetting));
};
useEffect(() =>
{
if(!maxVisitorsList)
{
const list = [];
for(let i = 10; i <= 100; i = i + 10) list.push(i);
setMaxVisitorsList(list);
setVisitorsCount(list[0]);
}
}, [ maxVisitorsList ]);
useEffect(() =>
{
if(categories && categories.length) setCategory(categories[0].id);
}, [ categories ]);
useEffect(() =>
{
const models = GetConfigurationValue<IRoomModel[]>('navigator.room.models');
if(models && models.length)
{
setRoomModels(models);
setSelectedModelName(models[0].name);
}
}, []);
return (
<div className="flex flex-col overflow-auto">
<Grid overflow="hidden">
<div className="flex flex-col gap-1 overflow-auto col-span-6">
<div className="flex flex-col gap-1">
<Text>{ LocalizeText('navigator.createroom.roomnameinfo') }</Text>
<NitroInput maxLength={ 60 } placeholder={ LocalizeText('navigator.createroom.roomnameinfo') } type="text" onChange={ event => setName(event.target.value) } />
</div>
<div className="flex flex-col !flex-grow gap-1">
<Text>{ LocalizeText('navigator.createroom.roomdescinfo') }</Text>
<textarea className="!flex-grow form-control form-control-sm w-full" maxLength={ 255 } placeholder={ LocalizeText('navigator.createroom.roomdescinfo') } onChange={ event => setDescription(event.target.value) } />
</div>
<div className="flex flex-col gap-1">
<Text>{ LocalizeText('navigator.category') }</Text>
<select className="form-select form-select-sm" onChange={ event => setCategory(Number(event.target.value)) }>
{ categories && (categories.length > 0) && categories.map(category =>
{
return <option key={ category.id } value={ category.id }>{ LocalizeText(category.name) }</option>;
}) }
</select>
</div>
<div className="flex flex-col gap-1">
<Text>{ LocalizeText('navigator.maxvisitors') }</Text>
<select className="form-select form-select-sm" onChange={ event => setVisitorsCount(Number(event.target.value)) }>
{ maxVisitorsList && maxVisitorsList.map(value =>
{
return <option key={ value } value={ value }>{ value }</option>;
}) }
</select>
</div>
<div className="flex flex-col gap-1">
<Text>{ LocalizeText('navigator.tradesettings') }</Text>
<select className="form-select form-select-sm" onChange={ event => setTradesSetting(Number(event.target.value)) }>
<option value="0">{ LocalizeText('navigator.roomsettings.trade_not_allowed') }</option>
<option value="1">{ LocalizeText('navigator.roomsettings.trade_not_with_Controller') }</option>
<option value="2">{ LocalizeText('navigator.roomsettings.trade_allowed') }</option>
</select>
</div>
</div>
<div className="flex flex-col gap-1 overflow-auto col-span-6">
{
roomModels.map((model, index) =>
{
return (<LayoutGridItem key={ model.name } fullHeight className="p-1" disabled={ (GetClubMemberLevel() < model.clubLevel) } gap={ 0 } itemActive={ (selectedModelName === model.name) } overflow="unset" onClick={ () => selectModel(model, index) }>
<Flex center fullHeight overflow="hidden">
<img alt="" src={ getRoomModelImage(model.name) } />
</Flex>
<Text bold>{ model.tileSize } { LocalizeText('navigator.createroom.tilesize') }</Text>
{ !hcDisabled && model.clubLevel > HabboClubLevelEnum.NO_CLUB && <LayoutCurrencyIcon className="top-1 end-1" position="absolute" type="hc" /> }
</LayoutGridItem>);
})
}
</div>
</Grid>
<Button fullWidth disabled={ (!name || (name.length < 3)) } variant={ (!name || (name.length < 3)) ? 'danger' : 'success' } onClick={ createRoom }>{ LocalizeText('navigator.createroom.create') }</Button>
</div>
);
};
@@ -1,181 +0,0 @@
import { CreateLinkEvent, GetCustomRoomFilterMessageComposer, GetSessionDataManager, NavigatorSearchComposer, RemoveOwnRoomRightsRoomMessageComposer, RoomControllerLevel, RoomMuteComposer, RoomSettingsComposer, SecurityLevel, ToggleStaffPickMessageComposer, UpdateHomeRoomMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { FaLink, FaSignOutAlt } from 'react-icons/fa';
import { DispatchUiEvent, GetGroupInformation, LocalizeText, ReportType, SendMessageComposer } from '../../../api';
import { Button, Column, Flex, LayoutBadgeImageView, LayoutRoomThumbnailView, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text, UserProfileIconView } from '../../../common';
import { RoomWidgetThumbnailEvent } from '../../../events';
import { useHelp, useNavigator, useRoom } from '../../../hooks';
import { classNames } from '../../../layout';
export interface NavigatorRoomInfoViewProps {
onCloseClick: () => void;
}
export const NavigatorRoomInfoView: FC<NavigatorRoomInfoViewProps> = props => {
const { onCloseClick = null } = props;
const [ isRoomPicked, setIsRoomPicked ] = useState(false);
const [ isRoomMuted, setIsRoomMuted ] = useState(false);
const { report = null } = useHelp();
const { navigatorData = null } = useNavigator();
const { roomSession = null } = useRoom();
const hasPermission = (permission: string) => {
switch(permission) {
case 'settings':
return (GetSessionDataManager().userId === navigatorData.enteredGuestRoom.ownerId || GetSessionDataManager().isModerator);
case 'staff_pick':
return GetSessionDataManager().securityLevel >= SecurityLevel.COMMUNITY;
case 'floor':
return roomSession?.controllerLevel >= RoomControllerLevel.GUEST;
case 'guest':
return roomSession?.controllerLevel === RoomControllerLevel.GUEST;
default: return false;
}
};
const processAction = (action: string, value?: string) => {
if(!navigatorData || !navigatorData.enteredGuestRoom) return;
switch(action) {
case 'set_home_room':
let newRoomId = -1;
if(navigatorData.homeRoomId !== navigatorData.enteredGuestRoom.roomId) {
newRoomId = navigatorData.enteredGuestRoom.roomId;
}
if(newRoomId > 0) SendMessageComposer(new UpdateHomeRoomMessageComposer(newRoomId));
return;
case 'navigator_search_tag':
CreateLinkEvent(`navigator/search/${ value }`);
SendMessageComposer(new NavigatorSearchComposer('hotel_view', `tag:${ value }`));
return;
case 'open_room_thumbnail_camera':
DispatchUiEvent(new RoomWidgetThumbnailEvent(RoomWidgetThumbnailEvent.TOGGLE_THUMBNAIL));
return;
case 'open_group_info':
GetGroupInformation(navigatorData.enteredGuestRoom.habboGroupId);
return;
case 'toggle_room_link':
CreateLinkEvent('navigator/toggle-room-link');
return;
case 'open_room_settings':
SendMessageComposer(new RoomSettingsComposer(navigatorData.enteredGuestRoom.roomId));
return;
case 'toggle_pick':
setIsRoomPicked(value => !value);
SendMessageComposer(new ToggleStaffPickMessageComposer(navigatorData.enteredGuestRoom.roomId));
return;
case 'toggle_mute':
setIsRoomMuted(value => !value);
SendMessageComposer(new RoomMuteComposer());
return;
case 'room_filter':
SendMessageComposer(new GetCustomRoomFilterMessageComposer(navigatorData.enteredGuestRoom.roomId));
return;
case 'open_floorplan_editor':
CreateLinkEvent('floor-editor/toggle');
return;
case 'report_room':
report(ReportType.ROOM, { roomId: navigatorData.enteredGuestRoom.roomId, roomName: navigatorData.enteredGuestRoom.roomName });
return;
case 'remove_rights':
SendMessageComposer(new RemoveOwnRoomRightsRoomMessageComposer(navigatorData.enteredGuestRoom.roomId));
return;
case 'close':
onCloseClick();
return;
}
};
useEffect(() => {
if(!navigatorData) return;
setIsRoomPicked(navigatorData.currentRoomIsStaffPick);
if(navigatorData.enteredGuestRoom) setIsRoomMuted(navigatorData.enteredGuestRoom.allInRoomMuted);
}, [ navigatorData ]);
if(!navigatorData.enteredGuestRoom) return null;
return (
<NitroCardView className="nitro-room-info" theme="primary-slim">
<NitroCardHeaderView headerText={ LocalizeText('navigator.roomsettings.roominfo') } onCloseClick={ () => processAction('close') } />
<NitroCardContentView className="text-black">
{ navigatorData.enteredGuestRoom &&
<>
<Flex gap={ 2 } overflow="hidden">
<LayoutRoomThumbnailView customUrl={ navigatorData.enteredGuestRoom.officialRoomPicRef } roomId={ navigatorData.enteredGuestRoom.roomId }>
{ hasPermission('settings') && <i className="top-0 m-1 cursor-pointer nitro-icon icon-camera-small absolute b-0 r-0" onClick={ () => processAction('open_room_thumbnail_camera') } /> }
</LayoutRoomThumbnailView>
<Column grow gap={ 1 } overflow="hidden">
<div className="flex gap-1">
<Column grow gap={ 1 }>
<div className="flex gap-1">
<Text>{ navigatorData.enteredGuestRoom.roomName }</Text>
</div>
{ navigatorData.enteredGuestRoom.showOwner &&
<div className="flex items-center gap-1">
<Text>{ LocalizeText('navigator.roomownercaption') }</Text>
<div className="flex items-center gap-1">
<UserProfileIconView userId={ navigatorData.enteredGuestRoom.ownerId } />
<Text>{ navigatorData.enteredGuestRoom.ownerName }</Text>
</div>
</div> }
<div className="flex items-center gap-1">
<Text>{ LocalizeText('navigator.roomrating') }</Text>
<Text>{ navigatorData.currentRoomRating }</Text>
</div>
{ (navigatorData.enteredGuestRoom.tags.length > 0) &&
<div className="flex items-center gap-1">
{ navigatorData.enteredGuestRoom.tags.map(tag =>
<Text key={ tag } pointer className="p-1 rounded bg-muted" onClick={ event => processAction('navigator_search_tag', tag) }>#{ tag }</Text>
) }
</div> }
</Column>
<Column alignItems="center" gap={ 1 }>
<i className={ classNames('flex-shrink-0 nitro-icon icon-house-small cursor-pointer', ((navigatorData.homeRoomId !== navigatorData.enteredGuestRoom.roomId) && 'gray')) } onClick={ () => processAction('set_home_room') } />
{ hasPermission('settings') &&
<i className="cursor-pointer nitro-icon icon-cog" title={ LocalizeText('navigator.room.popup.info.room.settings') } onClick={ event => processAction('open_room_settings') } /> }
<FaLink className="cursor-pointer fa-icon" title={ LocalizeText('navigator.embed.caption') } onClick={ event => processAction('toggle_room_link') } />
{ hasPermission('guest') &&
<FaSignOutAlt className="cursor-pointer fa-icon" title={ LocalizeText('navigator.roominfo.removerights.tooltip') } onClick={ event => processAction('remove_rights') } /> }
</Column>
</div>
<Text overflow="auto" style={ { maxHeight: 50 } }>{ navigatorData.enteredGuestRoom.description }</Text>
{ (navigatorData.enteredGuestRoom.habboGroupId > 0) &&
<Flex pointer alignItems="center" gap={ 1 } onClick={ () => processAction('open_group_info') }>
<LayoutBadgeImageView badgeCode={ navigatorData.enteredGuestRoom.groupBadgeCode } className="flex-none" isGroup={ true } />
<Text underline>
{ LocalizeText('navigator.guildbase', [ 'groupName' ], [ navigatorData.enteredGuestRoom.groupName ]) }
</Text>
</Flex> }
</Column>
</Flex>
<div className="flex flex-col gap-1">
{ hasPermission('staff_pick') &&
<Button onClick={ () => processAction('toggle_pick') }>
{ LocalizeText(isRoomPicked ? 'navigator.staffpicks.unpick' : 'navigator.staffpicks.pick') }
</Button> }
<Button variant="danger" onClick={ () => processAction('report_room') }>
{ LocalizeText('help.emergency.main.report.room') }
</Button>
{ hasPermission('settings') &&
<>
<Button onClick={ () => processAction('toggle_mute') }>
{ LocalizeText(isRoomMuted ? 'navigator.muteall_on' : 'navigator.muteall_off') }
</Button>
<Button onClick={ () => processAction('room_filter') }>
{ LocalizeText('navigator.roomsettings.roomfilter') }
</Button>
<Button onClick={ () => processAction('open_floorplan_editor') }>
{ LocalizeText('open.floor.plan.editor') }
</Button>
</> }
{ hasPermission('floor') && !hasPermission('settings') &&
<Button onClick={ () => processAction('open_floorplan_editor') }>
{ LocalizeText('open.floor.plan.editor') }
</Button> }
</div>
</> }
</NitroCardContentView>
</NitroCardView>
);
};
@@ -1,33 +0,0 @@
import { FC } from 'react';
import { GetConfigurationValue, LocalizeText } from '../../../api';
import { LayoutRoomThumbnailView, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../common';
import { useNavigator } from '../../../hooks';
export class NavigatorRoomLinkViewProps
{
onCloseClick: () => void;
}
export const NavigatorRoomLinkView: FC<NavigatorRoomLinkViewProps> = props =>
{
const { onCloseClick = null } = props;
const { navigatorData = null } = useNavigator();
if(!navigatorData.enteredGuestRoom) return null;
return (
<NitroCardView className="nitro-room-link" theme="primary-slim">
<NitroCardHeaderView headerText={ LocalizeText('navigator.embed.title') } onCloseClick={ onCloseClick } />
<NitroCardContentView className="text-black flex items-center">
<div className="flex gap-2">
<LayoutRoomThumbnailView customUrl={ navigatorData.enteredGuestRoom.officialRoomPicRef } roomId={ navigatorData.enteredGuestRoom.roomId } />
<div className="flex flex-col">
<Text bold fontSize={ 5 }>{ LocalizeText('navigator.embed.headline') }</Text>
<Text>{ LocalizeText('navigator.embed.info') }</Text>
<input readOnly className="min-h-[calc(1.5em+ .5rem+2px)] px-[.5rem] py-[.25rem] rounded-[.2rem] form-control-sm" type="text" value={ LocalizeText('navigator.embed.src', [ 'roomId' ], [ navigatorData.enteredGuestRoom.roomId.toString() ]).replace('${url.prefix}', GetConfigurationValue<string>('url.prefix', '')) } />
</div>
</div>
</NitroCardContentView>
</NitroCardView>
);
};
@@ -1,88 +0,0 @@
import { RoomDataParser } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { IRoomData, LocalizeText } from '../../../../api';
import { Text } from '../../../../common';
interface NavigatorRoomSettingsTabViewProps
{
roomData: IRoomData;
handleChange: (field: string, value: string | number | boolean) => void;
}
export const NavigatorRoomSettingsAccessTabView: FC<NavigatorRoomSettingsTabViewProps> = props =>
{
const { roomData = null, handleChange = null } = props;
const [ password, setPassword ] = useState<string>('');
const [ confirmPassword, setConfirmPassword ] = useState('');
const [ isTryingPassword, setIsTryingPassword ] = useState(false);
const saveRoomPassword = () =>
{
if(!isTryingPassword || ((password.length <= 0) || (confirmPassword.length <= 0) || (password !== confirmPassword))) return;
handleChange('password', password);
};
useEffect(() =>
{
setPassword('');
setConfirmPassword('');
setIsTryingPassword(false);
}, [ roomData ]);
return (
<>
<div className="flex flex-col gap-1">
<Text small bold>{ LocalizeText('navigator.roomsettings.roomaccess.caption') }</Text>
<Text small>{ LocalizeText('navigator.roomsettings.roomaccess.info') }</Text>
</div>
<div className="overflow-auto">
<div className="flex flex-col gap-1">
<Text small bold>{ LocalizeText('navigator.roomsettings.doormode') }</Text>
<div className="flex items-center gap-1">
<input checked={ (roomData.lockState === RoomDataParser.OPEN_STATE) && !isTryingPassword } className="form-check-input" name="lockState" type="radio" onChange={ event => handleChange('lock_state', RoomDataParser.OPEN_STATE) } />
<Text small>{ LocalizeText('navigator.roomsettings.doormode.open') }</Text>
</div>
<div className="flex items-center gap-1">
<input checked={ (roomData.lockState === RoomDataParser.DOORBELL_STATE) && !isTryingPassword } className="form-check-input" name="lockState" type="radio" onChange={ event => handleChange('lock_state', RoomDataParser.DOORBELL_STATE) } />
<Text small>{ LocalizeText('navigator.roomsettings.doormode.doorbell') }</Text>
</div>
<div className="flex items-center gap-1">
<input checked={ (roomData.lockState === RoomDataParser.INVISIBLE_STATE) && !isTryingPassword } className="form-check-input" name="lockState" type="radio" onChange={ event => handleChange('lock_state', RoomDataParser.INVISIBLE_STATE) } />
<Text small>{ LocalizeText('navigator.roomsettings.doormode.invisible') }</Text>
</div>
<div className="flex w-full gap-1">
<input checked={ (roomData.lockState === RoomDataParser.PASSWORD_STATE) || isTryingPassword } className="form-check-input" name="lockState" type="radio" onChange={ event => setIsTryingPassword(event.target.checked) } />
{ !isTryingPassword && (roomData.lockState !== RoomDataParser.PASSWORD_STATE) &&
<Text small>{ LocalizeText('navigator.roomsettings.doormode.password') }</Text> }
{ (isTryingPassword || (roomData.lockState === RoomDataParser.PASSWORD_STATE)) &&
<div className="flex flex-col gap-1">
<Text small>{ LocalizeText('navigator.roomsettings.doormode.password') }</Text>
<input className="min-h-[calc(1.5em+ .5rem+2px)] px-[.5rem] py-[.25rem] rounded-[.2rem] form-control-sm col-span-4" placeholder={ LocalizeText('navigator.roomsettings.password') } type="password" value={ password } onChange={ event => setPassword(event.target.value) } onFocus={ event => setIsTryingPassword(true) } />
{ isTryingPassword && (password.length <= 0) &&
<Text small bold variant="danger">
{ LocalizeText('navigator.roomsettings.passwordismandatory') }
</Text> }
<input className="min-h-[calc(1.5em+ .5rem+2px)] px-[.5rem] py-[.25rem] rounded-[.2rem] form-control-sm col-span-4" placeholder={ LocalizeText('navigator.roomsettings.passwordconfirm') } type="password" value={ confirmPassword } onBlur={ saveRoomPassword } onChange={ event => setConfirmPassword(event.target.value) } />
{ isTryingPassword && ((password.length > 0) && (password !== confirmPassword)) &&
<Text small bold variant="danger">
{ LocalizeText('navigator.roomsettings.invalidconfirm') }
</Text> }
</div> }
</div>
</div>
<div className="flex flex-col gap-1">
<Text small bold>{ LocalizeText('navigator.roomsettings.pets') }</Text>
<div className="flex items-center gap-1">
<input checked={ roomData.allowPets } className="form-check-input" type="checkbox" onChange={ event => handleChange('allow_pets', event.target.checked) } />
<Text small>{ LocalizeText('navigator.roomsettings.allowpets') }</Text>
</div>
<div className="flex items-center gap-1">
<input checked={ roomData.allowPetsEat } className="form-check-input" type="checkbox" onChange={ event => handleChange('allow_pets_eat', event.target.checked) } />
<Text small>{ LocalizeText('navigator.roomsettings.allowfoodconsume') }</Text>
</div>
</div>
</div>
</>
);
};
@@ -1,172 +0,0 @@
import { CreateLinkEvent, RoomDeleteComposer, RoomSettingsSaveErrorEvent, RoomSettingsSaveErrorParser } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { FaTimes } from 'react-icons/fa';
import { GetMaxVisitorsList, IRoomData, LocalizeText, SendMessageComposer } from '../../../../api';
import { Column, Text } from '../../../../common';
import { useMessageEvent, useNavigator, useNotification } from '../../../../hooks';
import { NitroInput } from '../../../../layout';
const ROOM_NAME_MIN_LENGTH = 3;
const ROOM_NAME_MAX_LENGTH = 60;
const DESC_MAX_LENGTH = 255;
const TAGS_MAX_LENGTH = 15;
interface NavigatorRoomSettingsTabViewProps
{
roomData: IRoomData;
handleChange: (field: string, value: string | number | boolean | string[]) => void;
onClose: () => void;
}
export const NavigatorRoomSettingsBasicTabView: FC<NavigatorRoomSettingsTabViewProps> = props =>
{
const { roomData = null, handleChange = null, onClose = null } = props;
const [ roomName, setRoomName ] = useState<string>('');
const [ roomDescription, setRoomDescription ] = useState<string>('');
const [ roomTag1, setRoomTag1 ] = useState<string>('');
const [ roomTag2, setRoomTag2 ] = useState<string>('');
const [ tagIndex, setTagIndex ] = useState(0);
const [ typeError, setTypeError ] = useState<string>('');
const { showConfirm = null } = useNotification();
const { categories = null } = useNavigator();
useMessageEvent<RoomSettingsSaveErrorEvent>(RoomSettingsSaveErrorEvent, event =>
{
const parser = event.getParser();
if(!parser) return;
switch(parser.code)
{
case RoomSettingsSaveErrorParser.ERROR_INVALID_TAG:
setTypeError('navigator.roomsettings.unacceptablewords');
case RoomSettingsSaveErrorParser.ERROR_NON_USER_CHOOSABLE_TAG:
setTypeError('navigator.roomsettings.nonuserchoosabletag');
break;
default:
setTypeError('');
break;
}
});
const deleteRoom = () =>
{
showConfirm(LocalizeText('navigator.roomsettings.deleteroom.confirm.message', [ 'room_name' ], [ roomData.roomName ]), () =>
{
SendMessageComposer(new RoomDeleteComposer(roomData.roomId));
if(onClose) onClose();
CreateLinkEvent('navigator/search/myworld_view');
},
null, null, null, LocalizeText('navigator.roomsettings.deleteroom.confirm.title'));
};
const saveRoomName = () =>
{
if((roomName === roomData.roomName) || (roomName.length < ROOM_NAME_MIN_LENGTH) || (roomName.length > ROOM_NAME_MAX_LENGTH)) return;
handleChange('name', roomName);
};
const saveRoomDescription = () =>
{
if((roomDescription === roomData.roomDescription) || (roomDescription.length > DESC_MAX_LENGTH)) return;
handleChange('description', roomDescription);
};
const saveTags = (index: number) =>
{
if(index === 0 && (roomTag1 === roomData.tags[0]) || (roomTag1.length > TAGS_MAX_LENGTH)) return;
if(index === 1 && (roomTag2 === roomData.tags[1]) || (roomTag2.length > TAGS_MAX_LENGTH)) return;
if(roomTag1 === '' && roomTag2 !== '') setRoomTag2('');
setTypeError('');
setTagIndex(index);
handleChange('tags', (roomTag1 === '' && roomTag2 !== '') ? [ roomTag2 ] : [ roomTag1, roomTag2 ]);
};
useEffect(() =>
{
setRoomName(roomData.roomName);
setRoomDescription(roomData.roomDescription);
setRoomTag1((roomData.tags.length > 0 && roomData.tags[0]) ? roomData.tags[0] : '');
setRoomTag2((roomData.tags.length > 0 && roomData.tags[1]) ? roomData.tags[1] : '');
}, [ roomData ]);
return (
<>
<div className="flex items-center gap-1">
<Text small className="col-span-3">{ LocalizeText('navigator.roomname') }</Text>
<Column fullWidth gap={ 0 }>
<NitroInput className="form-control-sm" maxLength={ ROOM_NAME_MAX_LENGTH } value={ roomName } onBlur={ saveRoomName } onChange={ event => setRoomName(event.target.value) } />
{ (roomName.length < ROOM_NAME_MIN_LENGTH) &&
<Text bold small variant="danger">
{ LocalizeText('navigator.roomsettings.roomnameismandatory') }
</Text> }
</Column>
</div>
<div className="flex items-center gap-1">
<Text small className="col-span-3">{ LocalizeText('navigator.roomsettings.desc') }</Text>
<textarea className="min-h-[calc(1.5em+ .5rem+2px)] px-[.5rem] py-[.25rem] rounded-[.2rem] form-control-sm" maxLength={ DESC_MAX_LENGTH } value={ roomDescription } onBlur={ saveRoomDescription } onChange={ event => setRoomDescription(event.target.value) } />
</div>
<div className="flex items-center gap-1">
<Text small className="col-span-3">{ LocalizeText('navigator.category') }</Text>
<select className="form-select form-select-sm" value={ roomData.categoryId } onChange={ event => handleChange('category', event.target.value) }>
{ categories && categories.map(category => <option key={ category.id } value={ category.id }>{ LocalizeText(category.name) }</option>) }
</select>
</div>
<div className="flex items-center gap-1">
<Text small className="col-span-3">{ LocalizeText('navigator.maxvisitors') }</Text>
<select className="form-select form-select-sm" value={ roomData.userCount } onChange={ event => handleChange('max_visitors', event.target.value) }>
{ GetMaxVisitorsList && GetMaxVisitorsList.map(value => <option key={ value } value={ value }>{ value }</option>) }
</select>
</div>
<div className="flex items-center gap-0">
<Text small className="col-span-3">{ LocalizeText('navigator.tradesettings') }</Text>
<select className="form-select form-select-sm" value={ roomData.tradeState } onChange={ event => handleChange('trade_state', event.target.value) }>
<option value="0">{ LocalizeText('navigator.roomsettings.trade_not_allowed') }</option>
<option value="1">{ LocalizeText('navigator.roomsettings.trade_not_with_Controller') }</option>
<option value="2">{ LocalizeText('navigator.roomsettings.trade_allowed') }</option>
</select>
</div>
<div className="flex items-center gap-1">
<Text small className="col-span-3">{ LocalizeText('navigator.tags') }</Text>
<Column fullWidth gap={ 0 }>
<NitroInput className="form-control-sm" value={ roomTag1 } onBlur={ () => saveTags(0) } onChange={ event => setRoomTag1(event.target.value) } />
{ (roomTag1.length > TAGS_MAX_LENGTH) &&
<Text bold small variant="danger">
{ LocalizeText('navigator.roomsettings.toomanycharacters') }
</Text> }
{ (tagIndex === 0 && typeError != '') &&
<Text bold small variant="danger">
{ LocalizeText(typeError) }
</Text> }
</Column>
<Column fullWidth gap={ 0 }>
<NitroInput className="form-control-sm" value={ roomTag2 } onBlur={ () => saveTags(1) } onChange={ event => setRoomTag2(event.target.value) } />
{ (roomTag2.length > TAGS_MAX_LENGTH) &&
<Text bold small variant="danger">
{ LocalizeText('navigator.roomsettings.toomanycharacters') }
</Text> }
{ (tagIndex === 1 && typeError != '') &&
<Text bold small variant="danger">
{ LocalizeText(typeError) }
</Text> }
</Column>
</div>
<div className="flex items-center gap-1">
<div className="col-span-1" />
<input checked={ roomData.allowWalkthrough } className="form-check-input" type="checkbox" onChange={ event => handleChange('allow_walkthrough', event.target.checked) } />
<Text small>{ LocalizeText('navigator.roomsettings.allow_walk_through') }</Text>
</div>
<Text small bold pointer underline className="flex items-center justify-center gap-1" variant="danger" onClick={ deleteRoom }>
<FaTimes className="fa-icon" />
{ LocalizeText('navigator.roomsettings.delete') }
</Text>
</>
);
};
@@ -1,118 +0,0 @@
import { BannedUserData, BannedUsersFromRoomEvent, RoomBannedUsersComposer, RoomModerationSettings, RoomUnbanUserComposer } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { IRoomData, LocalizeText, SendMessageComposer } from '../../../../api';
import { Button, Column, Flex, Grid, Text, UserProfileIconView } from '../../../../common';
import { useMessageEvent } from '../../../../hooks';
interface NavigatorRoomSettingsTabViewProps
{
roomData: IRoomData;
handleChange: (field: string, value: string | number | boolean) => void;
}
export const NavigatorRoomSettingsModTabView: FC<NavigatorRoomSettingsTabViewProps> = props =>
{
const { roomData = null, handleChange = null } = props;
const [ selectedUserId, setSelectedUserId ] = useState<number>(-1);
const [ bannedUsers, setBannedUsers ] = useState<BannedUserData[]>([]);
const unBanUser = (userId: number) =>
{
setBannedUsers(prevValue =>
{
const newValue = [ ...prevValue ];
const index = newValue.findIndex(value => (value.userId === userId));
if(index >= 0) newValue.splice(index, 1);
return newValue;
});
SendMessageComposer(new RoomUnbanUserComposer(userId, roomData.roomId));
setSelectedUserId(-1);
};
useMessageEvent<BannedUsersFromRoomEvent>(BannedUsersFromRoomEvent, event =>
{
const parser = event.getParser();
if(!roomData || (roomData.roomId !== parser.roomId)) return;
setBannedUsers(parser.bannedUsers);
});
useEffect(() =>
{
SendMessageComposer(new RoomBannedUsersComposer(roomData.roomId));
}, [ roomData.roomId ]);
return (
<Grid overflow="auto">
<Column size={ 6 }>
<Text small bold>{ LocalizeText('navigator.roomsettings.moderation.banned.users') } ({ bannedUsers.length })</Text>
<Flex className="bg-white rounded list-container p-2" overflow="hidden">
<Column fullWidth gap={ 1 } overflow="auto">
{ bannedUsers && (bannedUsers.length > 0) && bannedUsers.map((user, index) =>
{
return (
<Flex key={ index } shrink alignItems="center" gap={ 1 } overflow="hidden">
<UserProfileIconView userName={ user.userName } />
<Text small grow pointer onClick={ event => setSelectedUserId(user.userId) }> { user.userName }</Text>
</Flex>
);
}) }
</Column>
</Flex>
<Button disabled={ (selectedUserId <= 0) } onClick={ event => unBanUser(selectedUserId) }>
{ LocalizeText('navigator.roomsettings.moderation.unban') } { selectedUserId > 0 && bannedUsers.find(user => (user.userId === selectedUserId))?.userName }
</Button>
</Column>
<Column size={ 6 }>
<div className="flex flex-col gap-1">
<Text small bold>{ LocalizeText('navigator.roomsettings.moderation.mute.header') }</Text>
<div className="flex items-center gap-1">
<select className="form-select form-select-sm" value={ roomData.moderationSettings.allowMute } onChange={ event => handleChange('moderation_mute', event.target.value) }>
<option value={ RoomModerationSettings.MODERATION_LEVEL_NONE }>
{ LocalizeText('navigator.roomsettings.moderation.none') }
</option>
<option value={ RoomModerationSettings.MODERATION_LEVEL_USER_WITH_RIGHTS }>
{ LocalizeText('navigator.roomsettings.moderation.rights') }
</option>
</select>
</div>
</div>
<div className="flex flex-col gap-1">
<Text small bold>{ LocalizeText('navigator.roomsettings.moderation.kick.header') }</Text>
<div className="flex items-center gap-1">
<select className="form-select form-select-sm" value={ roomData.moderationSettings.allowKick } onChange={ event => handleChange('moderation_kick', event.target.value) }>
<option value={ RoomModerationSettings.MODERATION_LEVEL_NONE }>
{ LocalizeText('navigator.roomsettings.moderation.none') }
</option>
<option value={ RoomModerationSettings.MODERATION_LEVEL_USER_WITH_RIGHTS }>
{ LocalizeText('navigator.roomsettings.moderation.rights') }
</option>
<option value={ RoomModerationSettings.MODERATION_LEVEL_ALL }>
{ LocalizeText('navigator.roomsettings.moderation.all') }
</option>
</select>
</div>
</div>
<div className="flex flex-col gap-1">
<Text small bold>{ LocalizeText('navigator.roomsettings.moderation.ban.header') }</Text>
<div className="flex items-center gap-1">
<select className="form-select form-select-sm" value={ roomData.moderationSettings.allowBan } onChange={ event => handleChange('moderation_ban', event.target.value) }>
<option value={ RoomModerationSettings.MODERATION_LEVEL_NONE }>
{ LocalizeText('navigator.roomsettings.moderation.none') }
</option>
<option value={ RoomModerationSettings.MODERATION_LEVEL_USER_WITH_RIGHTS }>
{ LocalizeText('navigator.roomsettings.moderation.rights') }
</option>
</select>
</div>
</div>
</Column>
</Grid>
);
};
@@ -1,91 +0,0 @@
import { FlatControllerAddedEvent, FlatControllerRemovedEvent, FlatControllersEvent, RemoveAllRightsMessageComposer, RoomTakeRightsComposer, RoomUsersWithRightsComposer } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { IRoomData, LocalizeText, SendMessageComposer } from '../../../../api';
import { Button, Column, Flex, Grid, Text, UserProfileIconView } from '../../../../common';
import { useMessageEvent } from '../../../../hooks';
interface NavigatorRoomSettingsTabViewProps
{
roomData: IRoomData;
handleChange: (field: string, value: string | number | boolean) => void;
}
export const NavigatorRoomSettingsRightsTabView: FC<NavigatorRoomSettingsTabViewProps> = props =>
{
const { roomData = null } = props;
const [ usersWithRights, setUsersWithRights ] = useState<Map<number, string>>(new Map());
useMessageEvent<FlatControllersEvent>(FlatControllersEvent, event =>
{
const parser = event.getParser();
if(!roomData || (roomData.roomId !== parser.roomId)) return;
setUsersWithRights(parser.users);
});
useMessageEvent<FlatControllerAddedEvent>(FlatControllerAddedEvent, event =>
{
const parser = event.getParser();
if(!roomData || (roomData.roomId !== parser.roomId)) return;
setUsersWithRights(prevValue =>
{
const newValue = new Map(prevValue);
newValue.set(parser.data.userId, parser.data.userName);
return newValue;
});
});
useMessageEvent<FlatControllerRemovedEvent>(FlatControllerRemovedEvent, event =>
{
const parser = event.getParser();
if(!roomData || (roomData.roomId !== parser.roomId)) return;
setUsersWithRights(prevValue =>
{
const newValue = new Map(prevValue);
newValue.delete(parser.userId);
return newValue;
});
});
useEffect(() =>
{
SendMessageComposer(new RoomUsersWithRightsComposer(roomData.roomId));
}, [ roomData.roomId ]);
return (
<Grid>
<Column size={ 6 }>
<Text small bold>
{ LocalizeText('navigator.flatctrls.userswithrights', [ 'displayed', 'total' ], [ usersWithRights.size.toString(), usersWithRights.size.toString() ]) }
</Text>
<Flex className="bg-white rounded list-container p-2" overflow="hidden">
<Column fullWidth gap={ 1 } overflow="auto">
{ Array.from(usersWithRights.entries()).map(([ id, name ], index) =>
{
return (
<Flex key={ index } shrink alignItems="center" gap={ 1 } overflow="hidden">
<UserProfileIconView userName={ name } />
<Text small grow pointer onClick={ event => SendMessageComposer(new RoomTakeRightsComposer(id)) }> { name }</Text>
</Flex>
);
}) }
</Column>
</Flex>
</Column>
<Column justifyContent="end" size={ 6 }>
<Button disabled={ !usersWithRights.size } variant="danger" onClick={ event => SendMessageComposer(new RemoveAllRightsMessageComposer(roomData.roomId)) } >
{ LocalizeText('navigator.flatctrls.clear') }
</Button>
</Column>
</Grid>
);
};
@@ -1,206 +0,0 @@
import { RoomBannedUsersComposer, RoomDataParser, RoomSettingsDataEvent, SaveRoomSettingsComposer } from '@nitrots/nitro-renderer';
import { FC, useState } from 'react';
import { IRoomData, LocalizeText, SendMessageComposer } from '../../../../api';
import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../../../common';
import { useMessageEvent } from '../../../../hooks';
import { NavigatorRoomSettingsAccessTabView } from './NavigatorRoomSettingsAccessTabView';
import { NavigatorRoomSettingsBasicTabView } from './NavigatorRoomSettingsBasicTabView';
import { NavigatorRoomSettingsModTabView } from './NavigatorRoomSettingsModTabView';
import { NavigatorRoomSettingsRightsTabView } from './NavigatorRoomSettingsRightsTabView';
import { NavigatorRoomSettingsVipChatTabView } from './NavigatorRoomSettingsVipChatTabView';
const TABS: string[] = [
'navigator.roomsettings.tab.1',
'navigator.roomsettings.tab.2',
'navigator.roomsettings.tab.3',
'navigator.roomsettings.tab.4',
'navigator.roomsettings.tab.5'
];
export const NavigatorRoomSettingsView: FC<{}> = props =>
{
const [ roomData, setRoomData ] = useState<IRoomData>(null);
const [ currentTab, setCurrentTab ] = useState(TABS[0]);
useMessageEvent<RoomSettingsDataEvent>(RoomSettingsDataEvent, event =>
{
const parser = event.getParser();
if(!parser) return;
const data = parser.data;
setRoomData({
roomId: data.roomId,
roomName: data.name,
roomDescription: data.description,
categoryId: data.categoryId,
userCount: data.maximumVisitorsLimit,
tags: data.tags,
tradeState: data.tradeMode,
allowWalkthrough: data.allowWalkThrough,
lockState: data.doorMode,
password: null,
allowPets: data.allowPets,
allowPetsEat: data.allowFoodConsume,
hideWalls: data.hideWalls,
wallThickness: data.wallThickness,
floorThickness: data.floorThickness,
chatSettings: {
mode: data.chatSettings.mode,
weight: data.chatSettings.weight,
speed: data.chatSettings.speed,
distance: data.chatSettings.distance,
protection: data.chatSettings.protection
},
moderationSettings: {
allowMute: data.roomModerationSettings.allowMute,
allowKick: data.roomModerationSettings.allowKick,
allowBan: data.roomModerationSettings.allowBan
}
});
SendMessageComposer(new RoomBannedUsersComposer(data.roomId));
});
const onClose = () =>
{
setRoomData(null);
setCurrentTab(TABS[0]);
};
const handleChange = (field: string, value: string | number | boolean | string[]) =>
{
setRoomData(prevValue =>
{
const newValue = { ...prevValue };
switch(field)
{
case 'name':
newValue.roomName = String(value);
break;
case 'description':
newValue.roomDescription = String(value);
break;
case 'category':
newValue.categoryId = Number(value);
break;
case 'max_visitors':
newValue.userCount = Number(value);
break;
case 'trade_state':
newValue.tradeState = Number(value);
break;
case 'tags':
newValue.tags = value as Array<string>;
break;
case 'allow_walkthrough':
newValue.allowWalkthrough = Boolean(value);
break;
case 'allow_pets':
newValue.allowPets = Boolean(value);
break;
case 'allow_pets_eat':
newValue.allowPetsEat = Boolean(value);
break;
case 'hide_walls':
newValue.hideWalls = Boolean(value);
break;
case 'wall_thickness':
newValue.wallThickness = Number(value);
break;
case 'floor_thickness':
newValue.floorThickness = Number(value);
break;
case 'lock_state':
newValue.lockState = Number(value);
break;
case 'password':
newValue.lockState = RoomDataParser.PASSWORD_STATE;
newValue.password = String(value);
break;
case 'moderation_mute':
newValue.moderationSettings.allowMute = Number(value);
break;
case 'moderation_kick':
newValue.moderationSettings.allowKick = Number(value);
break;
case 'moderation_ban':
newValue.moderationSettings.allowBan = Number(value);
break;
case 'bubble_mode':
newValue.chatSettings.mode = Number(value);
break;
case 'chat_weight':
newValue.chatSettings.weight = Number(value);
break;
case 'bubble_speed':
newValue.chatSettings.speed = Number(value);
break;
case 'flood_protection':
newValue.chatSettings.protection = Number(value);
break;
case 'chat_distance':
newValue.chatSettings.distance = Number(value);
break;
}
SendMessageComposer(
new SaveRoomSettingsComposer(
newValue.roomId,
newValue.roomName,
newValue.roomDescription,
newValue.lockState,
newValue.password,
newValue.userCount,
newValue.categoryId,
newValue.tags.length,
newValue.tags,
newValue.tradeState,
newValue.allowPets,
newValue.allowPetsEat,
newValue.allowWalkthrough,
newValue.hideWalls,
newValue.wallThickness,
newValue.floorThickness,
newValue.moderationSettings.allowMute,
newValue.moderationSettings.allowKick,
newValue.moderationSettings.allowBan,
newValue.chatSettings.mode,
newValue.chatSettings.weight,
newValue.chatSettings.speed,
newValue.chatSettings.distance,
newValue.chatSettings.protection
));
return newValue;
});
};
if(!roomData) return null;
return (
<NitroCardView className="nitro-room-settings" uniqueKey="nitro-room-settings">
<NitroCardHeaderView headerText={ LocalizeText('navigator.roomsettings') } onCloseClick={ onClose } />
<NitroCardTabsView>
{ TABS.map(tab =>
{
return <NitroCardTabsItemView key={ tab } isActive={ (currentTab === tab) } onClick={ event => setCurrentTab(tab) }>{ LocalizeText(tab) }</NitroCardTabsItemView>;
}) }
</NitroCardTabsView>
<NitroCardContentView>
{ (currentTab === TABS[0]) &&
<NavigatorRoomSettingsBasicTabView handleChange={ handleChange } roomData={ roomData } onClose={ onClose } /> }
{ (currentTab === TABS[1]) &&
<NavigatorRoomSettingsAccessTabView handleChange={ handleChange } roomData={ roomData } /> }
{ (currentTab === TABS[2]) &&
<NavigatorRoomSettingsRightsTabView handleChange={ handleChange } roomData={ roomData } /> }
{ (currentTab === TABS[3]) &&
<NavigatorRoomSettingsVipChatTabView handleChange={ handleChange } roomData={ roomData } /> }
{ (currentTab === TABS[4]) &&
<NavigatorRoomSettingsModTabView handleChange={ handleChange } roomData={ roomData } /> }
</NitroCardContentView>
</NitroCardView>
);
};
@@ -1,77 +0,0 @@
import { RoomChatSettings } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { IRoomData, LocalizeText } from '../../../../api';
import { Column, Grid, Text } from '../../../../common';
import { NitroInput } from '../../../../layout';
interface NavigatorRoomSettingsTabViewProps
{
roomData: IRoomData;
handleChange: (field: string, value: string | number | boolean) => void;
}
export const NavigatorRoomSettingsVipChatTabView: FC<NavigatorRoomSettingsTabViewProps> = props =>
{
const { roomData = null, handleChange = null } = props;
const [ chatDistance, setChatDistance ] = useState<number>(0);
useEffect(() =>
{
setChatDistance(roomData.chatSettings.distance);
}, [ roomData.chatSettings ]);
return (
<>
<div className="flex flex-col gap-1">
<Text small bold>{ LocalizeText('navigator.roomsettings.vip.caption') }</Text>
<Text small>{ LocalizeText('navigator.roomsettings.vip.info') }</Text>
</div>
<Grid overflow="auto">
<Column gap={ 1 } size={ 6 }>
<Text small bold>{ LocalizeText('navigator.roomsettings.chat_settings') }</Text>
<Text small>{ LocalizeText('navigator.roomsettings.chat_settings.info') }</Text>
<select className="form-select form-select-sm" value={ roomData.chatSettings.mode } onChange={ event => handleChange('bubble_mode', event.target.value) }>
<option value={ RoomChatSettings.CHAT_MODE_FREE_FLOW }>{ LocalizeText('navigator.roomsettings.chat.mode.free.flow') }</option>
<option value={ RoomChatSettings.CHAT_MODE_LINE_BY_LINE }>{ LocalizeText('navigator.roomsettings.chat.mode.line.by.line') }</option>
</select>
<select className="form-select form-select-sm" value={ roomData.chatSettings.weight } onChange={ event => handleChange('chat_weight', event.target.value) }>
<option value={ RoomChatSettings.CHAT_BUBBLE_WIDTH_NORMAL }>{ LocalizeText('navigator.roomsettings.chat.bubbles.width.normal') }</option>
<option value={ RoomChatSettings.CHAT_BUBBLE_WIDTH_THIN }>{ LocalizeText('navigator.roomsettings.chat.bubbles.width.thin') }</option>
<option value={ RoomChatSettings.CHAT_BUBBLE_WIDTH_WIDE }>{ LocalizeText('navigator.roomsettings.chat.bubbles.width.wide') }</option>
</select>
<select className="form-select form-select-sm" value={ roomData.chatSettings.speed } onChange={ event => handleChange('bubble_speed', event.target.value) }>
<option value={ RoomChatSettings.CHAT_SCROLL_SPEED_FAST }>{ LocalizeText('navigator.roomsettings.chat.speed.fast') }</option>
<option value={ RoomChatSettings.CHAT_SCROLL_SPEED_NORMAL }>{ LocalizeText('navigator.roomsettings.chat.speed.normal') }</option>
<option value={ RoomChatSettings.CHAT_SCROLL_SPEED_SLOW }>{ LocalizeText('navigator.roomsettings.chat.speed.slow') }</option>
</select>
<select className="form-select form-select-sm" value={ roomData.chatSettings.protection } onChange={ event => handleChange('flood_protection', event.target.value) }>
<option value={ RoomChatSettings.FLOOD_FILTER_LOOSE }>{ LocalizeText('navigator.roomsettings.chat.flood.loose') }</option>
<option value={ RoomChatSettings.FLOOD_FILTER_NORMAL }>{ LocalizeText('navigator.roomsettings.chat.flood.normal') }</option>
<option value={ RoomChatSettings.FLOOD_FILTER_STRICT }>{ LocalizeText('navigator.roomsettings.chat.flood.strict') }</option>
</select>
<Text small>{ LocalizeText('navigator.roomsettings.chat_settings.hearing.distance') }</Text>
<NitroInput className="form-control-sm" min="0" type="number" value={ chatDistance } onBlur={ event => handleChange('chat_distance', chatDistance) } onChange={ event => setChatDistance(event.target.valueAsNumber) } />
</Column>
<Column gap={ 1 } size={ 6 }>
<Text small bold>{ LocalizeText('navigator.roomsettings.vip_settings') }</Text>
<div className="flex items-center gap-1">
<input checked={ roomData.hideWalls } className="form-check-input" type="checkbox" onChange={ event => handleChange('hide_walls', event.target.checked) } />
<Text small>{ LocalizeText('navigator.roomsettings.hide_walls') }</Text>
</div>
<select className="form-select form-select-sm" value={ roomData.wallThickness } onChange={ event => handleChange('wall_thickness', event.target.value) }>
<option value="0">{ LocalizeText('navigator.roomsettings.wall_thickness.normal') }</option>
<option value="1">{ LocalizeText('navigator.roomsettings.wall_thickness.thick') }</option>
<option value="-1">{ LocalizeText('navigator.roomsettings.wall_thickness.thin') }</option>
<option value="-2">{ LocalizeText('navigator.roomsettings.wall_thickness.thinnest') }</option>
</select>
<select className="form-select form-select-sm" value={ roomData.floorThickness } onChange={ event => handleChange('floor_thickness', event.target.value) }>
<option value="0">{ LocalizeText('navigator.roomsettings.floor_thickness.normal') }</option>
<option value="1">{ LocalizeText('navigator.roomsettings.floor_thickness.thick') }</option>
<option value="-1">{ LocalizeText('navigator.roomsettings.floor_thickness.thin') }</option>
<option value="-2">{ LocalizeText('navigator.roomsettings.floor_thickness.thinnest') }</option>
</select>
</Column>
</Grid>
</>
);
};
@@ -1,106 +0,0 @@
import { RoomDataParser } from '@nitrots/nitro-renderer';
import { FC, useRef, useState } from 'react';
import { FaUser } from 'react-icons/fa';
import { ArrowContainer, Popover } from 'react-tiny-popover';
import { LocalizeText } from '../../../../api';
import { Flex, LayoutBadgeImageView, LayoutRoomThumbnailView, NitroCardContentView, Text, UserProfileIconView } from '../../../../common';
export const NavigatorSearchResultItemInfoView: FC<{
roomData: RoomDataParser;
}> = props =>
{
const { roomData = null } = props;
const [ isVisible, setIsVisible ] = useState(false);
const elementRef = useRef<HTMLDivElement>();
const getUserCounterColor = () =>
{
const num: number = (100 * (roomData.userCount / roomData.maxUserCount));
let bg = 'bg-primary';
if(num >= 92)
{
bg = 'bg-danger';
}
else if(num >= 50)
{
bg = 'bg-warning';
}
else if(num > 0)
{
bg = 'bg-success';
}
return bg;
};
function dispatch(arg0: string): void
{
throw new Error('Function not implemented.');
}
return (
<>
<Popover
ref={ elementRef } // if you'd like a ref to your popover's child, you can grab one here
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={ { left: 'calc(-.5rem - 0px)' } }
childRect={ childRect }
popoverRect={ popoverRect }
position={ position }
>
<NitroCardContentView className="bg-transparent room-info image-rendering-pixelated" overflow="hidden">
<Flex gap={ 2 } overflow="hidden">
<LayoutRoomThumbnailView className="flex flex-col items-center mb-1 justify-content-end" customUrl={ roomData.officialRoomPicRef } roomId={ roomData.roomId }>
{ roomData.habboGroupId > 0 && (
<LayoutBadgeImageView badgeCode={ roomData.groupBadgeCode } className={ 'absolute top-0 start-0 m-1 ' } isGroup={ true } />) }
{ roomData.doorMode !== RoomDataParser.OPEN_STATE && (
<i className={ 'absolute end-0 mb-1 me-1 icon icon-navigator-room-' + (roomData.doorMode === RoomDataParser.DOORBELL_STATE ? 'locked' : roomData.doorMode === RoomDataParser.PASSWORD_STATE ? 'password' : roomData.doorMode === RoomDataParser.INVISIBLE_STATE ? 'invisible' : '') } />) }
</LayoutRoomThumbnailView>
<div className="flex flex-col gap-1">
<Text bold truncate className="flex-grow-1" style={ { maxHeight: 13 } }>
{ roomData.roomName }
</Text>
<div className="flex gap-2">
<Text italics variant="muted">
{ LocalizeText('navigator.roomownercaption') }
</Text>
<div className="flex items-center gap-1">
<UserProfileIconView userId={ roomData.ownerId } />
<Text italics>{ roomData.ownerName }</Text>
</div>
</div>
<Text className="flex-grow-1">
{ roomData.description }
</Text>
<Flex className={ 'badge p-1 absolute m-1 bottom-0 end-0 m-2 ' + getUserCounterColor() } gap={ 1 }>
<FaUser className="fa-icon" />
{ roomData.userCount }
</Flex>
</div>
</Flex>
</NitroCardContentView>
</ArrowContainer>
) }
isOpen={ isVisible }
padding={ 10 }
positions={ [ 'right' ] }
>
<div ref={ elementRef } className="cursor-pointer nitro-icon icon-navigator-info" onMouseLeave={ event => setIsVisible(false) } onMouseOver={ event => setIsVisible(true) } />
</Popover>
</>
);
};
@@ -1,121 +0,0 @@
import { GetSessionDataManager, RoomDataParser } from '@nitrots/nitro-renderer';
import { FC, MouseEvent } from 'react';
import { FaUser } from 'react-icons/fa';
import { CreateRoomSession, DoorStateType, TryVisitRoom } from '../../../../api';
import { Column, Flex, LayoutBadgeImageView, LayoutGridItemProps, LayoutRoomThumbnailView, Text } from '../../../../common';
import { useNavigator } from '../../../../hooks';
import { NavigatorSearchResultItemInfoView } from './NavigatorSearchResultItemInfoView';
export interface NavigatorSearchResultItemViewProps extends LayoutGridItemProps
{
roomData: RoomDataParser
thumbnail?: boolean
}
export const NavigatorSearchResultItemView: FC<NavigatorSearchResultItemViewProps> = props =>
{
const { roomData = null, children = null, thumbnail = false, ...rest } = props;
const { setDoorData = null } = useNavigator();
const getUserCounterColor = () =>
{
const num: number = (100 * (roomData.userCount / roomData.maxUserCount));
let bg = 'bg-primary';
if(num >= 92)
{
bg = 'bg-danger';
}
else if(num >= 50)
{
bg = 'bg-warning';
}
else if(num > 0)
{
bg = 'bg-success';
}
return bg;
};
const visitRoom = (event: MouseEvent) =>
{
if(roomData.ownerId !== GetSessionDataManager().userId)
{
if(roomData.habboGroupId !== 0)
{
TryVisitRoom(roomData.roomId);
return;
}
switch(roomData.doorMode)
{
case RoomDataParser.DOORBELL_STATE:
setDoorData(prevValue =>
{
const newValue = { ...prevValue };
newValue.roomInfo = roomData;
newValue.state = DoorStateType.START_DOORBELL;
return newValue;
});
return;
case RoomDataParser.PASSWORD_STATE:
setDoorData(prevValue =>
{
const newValue = { ...prevValue };
newValue.roomInfo = roomData;
newValue.state = DoorStateType.START_PASSWORD;
return newValue;
});
return;
}
}
CreateRoomSession(roomData.roomId);
};
if(thumbnail) return (
<Column pointer alignItems="center" className="navigator-item p-1 bg-light rounded-3 small mb-1 flex-col border border-muted" gap={ 0 } overflow="hidden" onClick={ visitRoom } { ...rest }>
<LayoutRoomThumbnailView className="flex flex-col items-center justify-end mb-1" customUrl={ roomData.officialRoomPicRef } roomId={ roomData.roomId }>
{ roomData.habboGroupId > 0 && <LayoutBadgeImageView badgeCode={ roomData.groupBadgeCode } className={ 'absolute top-0 start-0 m-1' } isGroup={ true } /> }
<Flex center className={ 'inline-block px-[.65em] py-[.35em] text-[.75em] font-bold leading-none text-[#fff] text-center whitespace-nowrap align-baseline rounded-[.25rem] p-1 absolute m-1 ' + getUserCounterColor() } gap={ 1 }>
<FaUser className="fa-icon" />
{ roomData.userCount }
</Flex>
{ (roomData.doorMode !== RoomDataParser.OPEN_STATE) &&
<i className={ ('absolute end-0 mb-1 me-1 icon icon-navigator-room-' + ((roomData.doorMode === RoomDataParser.DOORBELL_STATE) ? 'locked' : (roomData.doorMode === RoomDataParser.PASSWORD_STATE) ? 'password' : (roomData.doorMode === RoomDataParser.INVISIBLE_STATE) ? 'invisible' : '')) } /> }
</LayoutRoomThumbnailView>
<Flex className="w-full">
<Text truncate className="!flex-grow">{ roomData.roomName }</Text>
<Flex reverse alignItems="center" gap={ 1 }>
<NavigatorSearchResultItemInfoView roomData={ roomData } />
</Flex>
{ children }
</Flex>
</Column>
);
return (
<Flex pointer alignItems="center" className="navigator-item px-2 py-1 small" gap={ 2 } overflow="hidden" onClick={ visitRoom } { ...rest }>
<Flex center className={ 'inline-block px-[.65em] py-[.35em] text-[.75em] font-bold leading-none text-[#fff] text-center whitespace-nowrap align-baseline rounded-[.25rem] p-1 ' + getUserCounterColor() } gap={ 1 }>
<FaUser className="fa-icon" />
{ roomData.userCount }
</Flex>
<Text grow truncate>{ roomData.roomName }</Text>
<Flex reverse alignItems="center" gap={ 1 }>
<NavigatorSearchResultItemInfoView roomData={ roomData } />
{ roomData.habboGroupId > 0 && <i className="nitro-icon icon-navigator-room-group" /> }
{ (roomData.doorMode !== RoomDataParser.OPEN_STATE) &&
<i className={ ('nitro-icon icon-navigator-room-' + ((roomData.doorMode === RoomDataParser.DOORBELL_STATE) ? 'locked' : (roomData.doorMode === RoomDataParser.PASSWORD_STATE) ? 'password' : (roomData.doorMode === RoomDataParser.INVISIBLE_STATE) ? 'invisible' : '')) } /> }
</Flex>
{ children }
</Flex>
);
};
@@ -1,118 +0,0 @@
import { NavigatorSearchComposer, NavigatorSearchResultList } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { FaBars, FaMinus, FaPlus, FaTh, FaWindowMaximize, FaWindowRestore } from 'react-icons/fa';
import { LocalizeText, NavigatorSearchResultViewDisplayMode, SendMessageComposer } from '../../../../api';
import { AutoGrid, AutoGridProps, Column, Flex, Grid, Text } from '../../../../common';
import { useNavigator } from '../../../../hooks';
import { NavigatorSearchResultItemView } from './NavigatorSearchResultItemView';
export interface NavigatorSearchResultViewProps extends AutoGridProps
{
searchResult: NavigatorSearchResultList;
}
export const NavigatorSearchResultView: FC<NavigatorSearchResultViewProps> = props =>
{
const { searchResult = null, ...rest } = props;
const [ isExtended, setIsExtended ] = useState(true);
const [ displayMode, setDisplayMode ] = useState<number>(0);
const { topLevelContext = null } = useNavigator();
const getResultTitle = () =>
{
let name = searchResult.code;
if(!name || !name.length || LocalizeText('navigator.searchcode.title.' + name) == ('navigator.searchcode.title.' + name)) return searchResult.data;
if(name.startsWith('${')) return name.slice(2, (name.length - 1));
return ('navigator.searchcode.title.' + name);
};
const toggleDisplayMode = () =>
{
setDisplayMode(prevValue =>
{
if(prevValue === NavigatorSearchResultViewDisplayMode.LIST) return NavigatorSearchResultViewDisplayMode.THUMBNAILS;
return NavigatorSearchResultViewDisplayMode.LIST;
});
};
const showMore = () =>
{
if(searchResult.action == 1) SendMessageComposer(new NavigatorSearchComposer(searchResult.code, ''));
else if(searchResult.action == 2 && topLevelContext) SendMessageComposer(new NavigatorSearchComposer(topLevelContext.code, ''));
};
useEffect(() =>
{
if(!searchResult) return;
setIsExtended(!searchResult.closed);
setDisplayMode(searchResult.mode);
}, [ searchResult ]);
const gridHasTwoColumns = (displayMode >= NavigatorSearchResultViewDisplayMode.THUMBNAILS);
return (
<Column className="bg-white rounded border border-muted" gap={ 0 }>
<Flex fullWidth alignItems="center" className="px-2 py-1" justifyContent="between">
<Flex grow pointer alignItems="center" gap={ 1 } onClick={ event => setIsExtended(prevValue => !prevValue) }>
{ isExtended && <FaMinus className="text-secondary fa-icon" /> }
{ !isExtended && <FaPlus className="text-secondary fa-icon" /> }
<Text>{ LocalizeText(getResultTitle()) }</Text>
</Flex>
<div className="flex gap-2">
{ (displayMode === NavigatorSearchResultViewDisplayMode.LIST) && <FaTh className="text-secondary fa-icon" onClick={ toggleDisplayMode } /> }
{ (displayMode >= NavigatorSearchResultViewDisplayMode.THUMBNAILS) && <FaBars className="text-secondary fa-icon" onClick={ toggleDisplayMode } /> }
{ (searchResult.action > 0) && (searchResult.action === 1) && <FaWindowMaximize className="text-secondary fa-icon" onClick={ showMore } /> }
{ (searchResult.action > 0) && (searchResult.action !== 1) && <FaWindowRestore className="text-secondary fa-icon" onClick={ showMore } /> }
</div>
</Flex> { isExtended &&
<>
{
gridHasTwoColumns ? <AutoGrid columnCount={ 3 } { ...rest } className="mx-2" columnMinHeight={ 130 } columnMinWidth={ 110 }>
{ searchResult.rooms.length > 0 && searchResult.rooms.map((room, index) => <NavigatorSearchResultItemView key={ index } roomData={ room } thumbnail={ true } />) }
</AutoGrid> : <Grid className="navigator-grid" columnCount={ 1 } gap={ 0 }>
{ searchResult.rooms.length > 0 && searchResult.rooms.map((room, index) => <NavigatorSearchResultItemView key={ index } roomData={ room } />) }
</Grid>
}
</>
}
</Column>
// <div className="nitro-navigator-search-result bg-white rounded mb-2 overflow-hidden">
// <div className="flex flex-col">
// <div className="flex items-center px-2 py-1 text-black">
// <i className={ 'text-secondary fas ' + (isExtended ? 'fa-minus' : 'fa-plus') } onClick={ toggleExtended }></i>
// <div className="ms-2 !flex-grow">{ LocalizeText(getResultTitle()) }</div>
// <i className={ 'text-secondary fas ' + classNames({ 'fa-bars': (displayMode === NavigatorSearchResultViewDisplayMode.LIST), 'fa-th': displayMode >= NavigatorSearchResultViewDisplayMode.THUMBNAILS })}></i>
// </div>
// { isExtended &&
// <div className={ 'nitro-navigator-result-list row row-cols-' + classNames({ '1': (displayMode === NavigatorSearchResultViewDisplayMode.LIST), '2': (displayMode >= NavigatorSearchResultViewDisplayMode.THUMBNAILS) }) }>
// { searchResult.rooms.length > 0 && searchResult.rooms.map((room, index) =>
// {
// return <NavigatorSearchResultItemView key={ index } roomData={ room } />
// }) }
// </div> }
// </div>
// </div>
// <div className="nitro-navigator-result-list p-2">
// <div className="flex mb-2 small cursor-pointer" onClick={ toggleList }>
// <i className={ "fas " + classNames({ 'fa-plus': !isExtended, 'fa-minus': isExtended })}></i>
// <div className="align-self-center w-full ml-2">{ LocalizeText(getListCode()) }</div>
// <i className={ "fas " + classNames({ 'fa-bars': displayMode === NavigatorResultListViewDisplayMode.LIST, 'fa-th': displayMode >= NavigatorResultListViewDisplayMode.THUMBNAILS })} onClick={ toggleDisplayMode }></i>
// </div>
// <div className={ 'row mr-n2 row-cols-' + classNames({ '1': displayMode === NavigatorResultListViewDisplayMode.LIST, '2': displayMode >= NavigatorResultListViewDisplayMode.THUMBNAILS }) }>
// { isExtended && resultList && resultList.rooms.map((room, index) =>
// {
// return <NavigatorResultView key={ index } result={ room } />
// })
// }
// </div>
// </div>
);
};
@@ -1,81 +0,0 @@
import { FC, KeyboardEvent, useEffect, useState } from 'react';
import { FaSearch } from 'react-icons/fa';
import { INavigatorSearchFilter, LocalizeText, SearchFilterOptions } from '../../../../api';
import { Button } from '../../../../common';
import { useNavigator } from '../../../../hooks';
export const NavigatorSearchView: FC<{
sendSearch: (searchValue: string, contextCode: string) => void;
}> = props =>
{
const { sendSearch = null } = props;
const [ searchFilterIndex, setSearchFilterIndex ] = useState(0);
const [ searchValue, setSearchValue ] = useState('');
const { topLevelContext = null, searchResult = null } = useNavigator();
const processSearch = () =>
{
if(!topLevelContext) return;
let searchFilter = SearchFilterOptions[searchFilterIndex];
if(!searchFilter) searchFilter = SearchFilterOptions[0];
const searchQuery = ((searchFilter.query ? (searchFilter.query + ':') : '') + searchValue);
sendSearch((searchQuery || ''), topLevelContext.code);
};
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) =>
{
if(event.key !== 'Enter') return;
processSearch();
};
useEffect(() =>
{
if(!searchResult) return;
const split = searchResult.data.split(':');
let filter: INavigatorSearchFilter = null;
let value: string = '';
if(split.length >= 2)
{
const [ query, ...rest ] = split;
filter = SearchFilterOptions.find(option => (option.query === query));
value = rest.join(':');
}
else
{
value = searchResult.data;
}
if(!filter) filter = SearchFilterOptions[0];
setSearchFilterIndex(SearchFilterOptions.findIndex(option => (option === filter)));
setSearchValue(value);
}, [ searchResult ]);
return (
<div className="flex w-full gap-1">
<div className="flex shrink-0">
<select className="form-select" value={ searchFilterIndex } onChange={ event => setSearchFilterIndex(parseInt(event.target.value)) }>
{ SearchFilterOptions.map((filter, index) =>
{
return <option key={ index } value={ index }>{ LocalizeText('navigator.filter.' + filter.name) }</option>;
}) }
</select>
</div>
<div className="flex w-full gap-1">
<input className="w-full form-control" placeholder={ LocalizeText('navigator.filter.input.placeholder') } type="text" value={ searchValue } onChange={ event => setSearchValue(event.target.value) } onKeyDown={ event => handleKeyDown(event) } />
<Button variant="primary" onClick={ processSearch }>
<FaSearch className="fa-icon" />
</Button>
</div>
</div>
);
};
@@ -1,59 +0,0 @@
import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer';
import { FC, useEffect, useMemo, useRef, useState } from 'react';
import { useChatHistory } from '../../hooks';
export const NitrobubbleHiddenView: FC<{}> = props =>
{
const [ isVisible, setIsVisible ] = useState(false);
const [ searchText, setSearchText ] = useState<string>('');
const { chatHistory = [] } = useChatHistory();
const elementRef = useRef<HTMLDivElement>(null);
const filteredChatHistory = useMemo(() =>
{
if (searchText.length === 0) return chatHistory;
let text = searchText.toLowerCase();
return chatHistory.filter(entry => ((entry.message && entry.message.toLowerCase().includes(text))) || (entry.name && entry.name.toLowerCase().includes(text)));
}, [ chatHistory, searchText ]);
useEffect(() =>
{
if(elementRef && elementRef.current && isVisible) elementRef.current.scrollTop = elementRef.current.scrollHeight;
}, [ isVisible ]);
useEffect(() =>
{
const linkTracker: ILinkEventTracker = {
linkReceived: (url: string) =>
{
const parts = url.split('/');
if(parts.length < 2) return;
switch(parts[1])
{
case 'show':
setIsVisible(true);
return;
case 'hide':
setIsVisible(false);
return;
case 'toggle':
setIsVisible(prevValue => !prevValue);
return;
}
},
eventUrlPrefix: 'nitrobubblehidden/'
};
AddLinkEventTracker(linkTracker);
return () => RemoveLinkEventTracker(linkTracker);
}, []);
if(!isVisible) return null;
var stylecssnew = "<style>.newbubblehe { visibility: hidden !important; }</style>";
return ( <div dangerouslySetInnerHTML={ { __html: stylecssnew }} />);
}
@@ -1,105 +0,0 @@
import { AddLinkEventTracker, ILinkEventTracker, NitroLogger, RemoveLinkEventTracker } from '@nitrots/nitro-renderer';
import { FC, useEffect, useRef, useState } from 'react';
import { GetConfigurationValue, OpenUrl } from '../../api';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../common';
const NEW_LINE_REGEX = /\n\r|\n|\r/mg;
export const NitropediaView: FC<{}> = props =>
{
const [ content, setContent ] = useState<string>(null);
const [ header, setHeader ] = useState<string>('');
const [ dimensions, setDimensions ] = useState<{ width: number, height: number }>(null);
const elementRef = useRef<HTMLDivElement>(null);
useEffect(() =>
{
const openPage = async (link: string) =>
{
try
{
const response = await fetch(link);
if(!response) return;
const text = await response.text();
const splitData = text.split(NEW_LINE_REGEX);
const line = splitData.shift().split('|');
setHeader(line[0]);
setDimensions(prevValue =>
{
if(line[1] && (line[1].split(';').length === 2))
{
return {
width: parseInt(line[1].split(';')[0]),
height: parseInt(line[1].split(';')[1])
};
}
return null;
});
setContent(splitData.join(''));
}
catch (error)
{
NitroLogger.error(`Failed to fetch ${ link }`);
}
};
const linkTracker: ILinkEventTracker = {
linkReceived: (url: string) =>
{
const value = url.split('/');
if(value.length < 2) return;
value.shift();
openPage(GetConfigurationValue<string>('habbopages.url') + value.join('/'));
},
eventUrlPrefix: 'habbopages/'
};
AddLinkEventTracker(linkTracker);
return () => RemoveLinkEventTracker(linkTracker);
}, []);
useEffect(() =>
{
const handle = (event: MouseEvent) =>
{
if(!(event.target instanceof HTMLAnchorElement)) return;
event.preventDefault();
const link = event.target.href;
if(!link || !link.length) return;
OpenUrl(link);
};
document.addEventListener('click', handle);
return () =>
{
document.removeEventListener('click', handle);
};
}, []);
if(!content) return null;
return (
<NitroCardView className="nitropedia" style={ dimensions ? { width: dimensions.width, height: dimensions.height } : {} } theme="primary-slim">
<NitroCardHeaderView headerText={ header } onCloseClick={ () => setContent(null) }/>
<NitroCardContentView>
<div ref={ elementRef } className="text-black size-full" dangerouslySetInnerHTML={ { __html: content } } />
</NitroCardContentView>
</NitroCardView>
);
};
@@ -1,76 +0,0 @@
import { FC, ReactNode, useMemo } from 'react';
import { NotificationBubbleType } from '../../api';
import { useNotification } from '../../hooks';
import { GetAlertLayout } from './views/alert-layouts/GetAlertLayout';
import { GetBubbleLayout } from './views/bubble-layouts/GetBubbleLayout';
import { GetConfirmLayout } from './views/confirm-layouts/GetConfirmLayout';
export const NotificationCenterView: FC<{}> = props =>
{
const { alerts = [], bubbleAlerts = [], confirms = [], closeAlert = null, closeBubbleAlert = null, closeConfirm = null } = useNotification();
const getAlerts = useMemo(() =>
{
if(!alerts || !alerts.length) return null;
const elements: ReactNode[] = [];
for(const alert of alerts)
{
const element = GetAlertLayout(alert, () => closeAlert(alert));
elements.push(element);
}
return elements;
}, [ alerts, closeAlert ]);
const getBubbleAlerts = useMemo(() =>
{
if(!bubbleAlerts || !bubbleAlerts.length) return null;
const elements: ReactNode[] = [];
for(const alert of bubbleAlerts)
{
const element = GetBubbleLayout(alert, () => closeBubbleAlert(alert));
if(alert.notificationType === NotificationBubbleType.CLUBGIFT)
{
elements.unshift(element);
continue;
}
elements.push(element);
}
return elements;
}, [ bubbleAlerts, closeBubbleAlert ]);
const getConfirms = useMemo(() =>
{
if(!confirms || !confirms.length) return null;
const elements: ReactNode[] = [];
for(const confirm of confirms)
{
const element = GetConfirmLayout(confirm, () => closeConfirm(confirm));
elements.push(element);
}
return elements;
}, [ confirms, closeConfirm ]);
return (
<>
<div className="flex flex-col gap-1">
{ getBubbleAlerts }
</div>
{ getConfirms }
{ getAlerts }
</>
);
};
@@ -1,22 +0,0 @@
import { NotificationAlertItem, NotificationAlertType } from '../../../../api';
import { NitroSystemAlertView } from './NitroSystemAlertView';
import { NotificationDefaultAlertView } from './NotificationDefaultAlertView';
import { NotificationSeachAlertView } from './NotificationSearchAlertView';
export const GetAlertLayout = (item: NotificationAlertItem, onClose: () => void) =>
{
if(!item) return null;
const key = item.id;
const props = { item, onClose };
switch(item.alertType)
{
case NotificationAlertType.NITRO:
return <NitroSystemAlertView key={key} {...props} />;
case NotificationAlertType.SEARCH:
return <NotificationSeachAlertView key={key} {...props} />;
default:
return <NotificationDefaultAlertView key={key} {...props} />;
}
};
@@ -1,42 +0,0 @@
import { FC } from 'react';
import { GetRendererVersion, GetUIVersion, NotificationAlertItem } from '../../../../api';
import { Button, Column, Grid, LayoutNotificationAlertView, LayoutNotificationAlertViewProps, Text } from '../../../../common';
interface NotificationDefaultAlertViewProps extends LayoutNotificationAlertViewProps
{
item: NotificationAlertItem;
}
export const NitroSystemAlertView: FC<NotificationDefaultAlertViewProps> = props =>
{
const { title = 'Nitro', onClose = null, ...rest } = props;
return (
<LayoutNotificationAlertView title={ title } onClose={ onClose } { ...rest }>
<Grid>
<Column size={ 12 }>
<Column alignItems="center" gap={ 0 }>
<Text bold fontSize={ 4 }>Nitro React</Text>
<Text>v{ GetUIVersion() }</Text>
</Column>
<Column alignItems="center">
<Text><b>Renderer:</b> v{ GetRendererVersion() }</Text>
<Column fullWidth gap={ 1 }>
<Button fullWidth variant="success" onClick={ event => window.open('https://discord.nitrodev.co') }>Discord</Button>
</Column>
</Column>
<div className="alertView_nitro-coolui-logo"></div>
<Column size={ 12 }>
<Column alignItems="center" gap={ 0 }>
<Text center bold fontSize={ 5 }>Cool UI</Text>
<Text>- DuckieTM (Design)</Text>
<Text center bold small>v3.0.0</Text>
<Button fullWidth onClick={ event => window.open('https://github.com/duckietm/Nitro-Cool-UI') }>Cool UI Git</Button>
</Column>
</Column>
</Column>
</Grid>
</LayoutNotificationAlertView>
);
};
@@ -1,56 +0,0 @@
import { FC, useState } from 'react';
import { LocalizeText, NotificationAlertItem, NotificationAlertType, OpenUrl } from '../../../../api';
import { Button, Column, Flex, LayoutNotificationAlertView, LayoutNotificationAlertViewProps } from '../../../../common';
interface NotificationDefaultAlertViewProps extends LayoutNotificationAlertViewProps
{
item: NotificationAlertItem;
}
export const NotificationDefaultAlertView: FC<NotificationDefaultAlertViewProps> = props =>
{
const { item = null, title = ((props.item && props.item.title) || ''), onClose = null, ...rest } = props;
const [ imageFailed, setImageFailed ] = useState<boolean>(false);
const visitUrl = () =>
{
OpenUrl(item.clickUrl);
onClose();
};
const hasFrank = (item.alertType === NotificationAlertType.DEFAULT);
return (
<LayoutNotificationAlertView title={ title } onClose={ onClose } { ...rest } type={ hasFrank ? NotificationAlertType.DEFAULT : item.alertType }>
<Flex fullHeight gap={ hasFrank || (item.imageUrl && !imageFailed) ? 2 : 0 } overflow="auto">
{ hasFrank && !item.imageUrl && <div className="notification-frank flex-shrink-0" /> }
{ item.imageUrl && !imageFailed && <img alt={ item.title } className="align-self-baseline" src={ item.imageUrl } onError={ () =>
{
setImageFailed(true);
} } /> }
<div className={ [ 'notification-text overflow-y-auto flex flex-col w-full', (item.clickUrl && !hasFrank) ? 'justify-center' : '' ].join(' ') }>
{ (item.messages.length > 0) && item.messages.map((message, index) =>
{
const htmlText = message.replace(/\r\n|\r|\n/g, '<br />');
return <div key={ index } dangerouslySetInnerHTML={ { __html: htmlText } } />;
}) }
{ item.clickUrl && (item.clickUrl.length > 0) && (item.imageUrl && !imageFailed) && <>
<hr className="my-2 w-full" />
<Button className="align-self-center px-3" onClick={ visitUrl }>{ LocalizeText(item.clickUrlText) }</Button>
</> }
</div>
</Flex>
{ (!item.imageUrl || (item.imageUrl && imageFailed)) && <>
<Column center alignItems="center" gap={ 0 }>
<hr className="my-2 w-full" />
{ !item.clickUrl &&
<Button onClick={ onClose }>{ LocalizeText('generic.close') }</Button> }
{ item.clickUrl && (item.clickUrl.length > 0) && <Button onClick={ visitUrl }>{ LocalizeText(item.clickUrlText) }</Button> }
</Column>
</> }
</LayoutNotificationAlertView>
);
};
@@ -1,62 +0,0 @@
import { FC, useEffect, useState } from 'react';
import { LocalizeText, NotificationAlertItem, OpenUrl } from '../../../../api';
import { AutoGrid, Button, Column, Flex, LayoutNotificationAlertView, LayoutNotificationAlertViewProps } from '../../../../common';
import { NitroInput } from '../../../../layout';
interface NotificationDefaultAlertViewProps extends LayoutNotificationAlertViewProps
{
item: NotificationAlertItem;
}
export const NotificationSeachAlertView: FC<NotificationDefaultAlertViewProps> = props =>
{
const { item = null, title = ((props.item && props.item.title) || ''), onClose = null, ...rest } = props;
const [ searchValue, setSearchValue ] = useState('');
const [ results, setResults ] = useState<string[]>([]);
const visitUrl = () =>
{
OpenUrl(item.clickUrl);
onClose();
};
const updateSearchValue = (value: string) =>
{
let res = JSON.parse(item.messages[0]);
setResults(res.filter((val: string) => val.includes(value)));
setSearchValue(value);
};
useEffect(() =>
{
setResults(JSON.parse(item.messages[0]));
}, [ item ]);
const isAction = (item.clickUrl && item.clickUrl.startsWith('event:'));
return (
<LayoutNotificationAlertView title={ title } onClose={ onClose } { ...rest }>
<Flex fullWidth alignItems="center" position="relative">
<NitroInput placeholder={ LocalizeText('generic.search') } type="text" value={ searchValue } onChange={ event => updateSearchValue(event.target.value) } />
</Flex>
<Column fullHeight className="py-1" overflow="hidden">
<AutoGrid columnCount={ 1 } gap={ 1 }>
{ results && results.map((n, index) =>
{
return <span key={ index }>{ n }</span>;
}) }
</AutoGrid>
</Column>
<hr className="my-2" />
<Column center alignItems="center" gap={ 1 }>
{ !isAction && !item.clickUrl &&
<Button onClick={ onClose }>{ LocalizeText('generic.close') }</Button> }
{ item.clickUrl && (item.clickUrl.length > 0) &&
<Button onClick={ visitUrl }>{ LocalizeText(item.clickUrlText) }</Button> }
</Column>
</LayoutNotificationAlertView>
);
};
@@ -1,18 +0,0 @@
import { NotificationBubbleItem, NotificationBubbleType } from '../../../../api';
import { NotificationClubGiftBubbleView } from './NotificationClubGiftBubbleView';
import { NotificationDefaultBubbleView } from './NotificationDefaultBubbleView';
export const GetBubbleLayout = (item: NotificationBubbleItem, onClose: () => void) =>
{
if(!item) return null;
const props = { item, onClose };
switch(item.notificationType)
{
case NotificationBubbleType.CLUBGIFT:
return <NotificationClubGiftBubbleView key={ item.id } { ...props } />;
default:
return <NotificationDefaultBubbleView key={ item.id } { ...props } />;
}
};
@@ -1,26 +0,0 @@
import { FC } from 'react';
import { LocalizeText, NotificationBubbleItem, OpenUrl } from '../../../../api';
import { LayoutCurrencyIcon, LayoutNotificationBubbleView, LayoutNotificationBubbleViewProps } from '../../../../common';
export interface NotificationClubGiftBubbleViewProps extends LayoutNotificationBubbleViewProps
{
item: NotificationBubbleItem;
}
export const NotificationClubGiftBubbleView: FC<NotificationClubGiftBubbleViewProps> = props =>
{
const { item = null, onClose = null, ...rest } = props;
return (
<LayoutNotificationBubbleView className="flex-col nitro-notification-bubble" fadesOut={ false } onClose={ onClose } { ...rest }>
<div className="flex items-center gap-2 mb-2">
<LayoutCurrencyIcon className="flex-shrink-0" type="hc" />
<span className="ms-1">{ LocalizeText('notifications.text.club_gift') }</span>
</div>
<div className="flex items-center justify-end gap-2">
<button className="btn btn-success w-full btn-sm" type="button" onClick={ () => OpenUrl(item.linkUrl) }>{ LocalizeText('notifications.button.show_gift_list') }</button>
<span className="underline cursor-pointer text-nowrap" onClick={ onClose }>{ LocalizeText('notifications.button.later') }</span>
</div>
</LayoutNotificationBubbleView>
);
};
@@ -1,25 +0,0 @@
import { FC } from 'react';
import { NotificationBubbleItem, OpenUrl } from '../../../../api';
import { Flex, LayoutNotificationBubbleView, LayoutNotificationBubbleViewProps, Text } from '../../../../common';
export interface NotificationDefaultBubbleViewProps extends LayoutNotificationBubbleViewProps
{
item: NotificationBubbleItem;
}
export const NotificationDefaultBubbleView: FC<NotificationDefaultBubbleViewProps> = props =>
{
const { item = null, onClose = null, ...rest } = props;
const htmlText = item.message.replace(/\r\n|\r|\n/g, '<br />');
return (
<LayoutNotificationBubbleView alignItems="center" gap={ 2 } onClick={ event => (item.linkUrl && item.linkUrl.length && OpenUrl(item.linkUrl)) } onClose={ onClose } { ...rest }>
<Flex center className="w-[50px] h-[50px]">
{ (item.iconUrl && item.iconUrl.length) &&
<img alt="" className="no-select" src={ item.iconUrl } /> }
</Flex>
<Text wrap dangerouslySetInnerHTML={ { __html: htmlText } } variant="white" />
</LayoutNotificationBubbleView>
);
};
@@ -1,15 +0,0 @@
import { NotificationConfirmItem } from '../../../../api';
import { NotificationDefaultConfirmView } from './NotificationDefaultConfirmView';
export const GetConfirmLayout = (item: NotificationConfirmItem, onClose: () => void) =>
{
if(!item) return null;
const props = { key: item.id, item, onClose };
switch(item.confirmType)
{
default:
return <NotificationDefaultConfirmView { ...props } />;
}
};
@@ -1,40 +0,0 @@
import { FC } from 'react';
import { NotificationAlertType, NotificationConfirmItem } from '../../../../api';
import { Button, Flex, LayoutNotificationAlertView, LayoutNotificationAlertViewProps, Text } from '../../../../common';
export interface NotificationDefaultConfirmViewProps extends LayoutNotificationAlertViewProps
{
item: NotificationConfirmItem;
}
export const NotificationDefaultConfirmView: FC<NotificationDefaultConfirmViewProps> = props =>
{
const { item = null, onClose = null, ...rest } = props;
const { message = null, onConfirm = null, onCancel = null, confirmText = null, cancelText = null, title = null } = item;
const confirm = () =>
{
if(onConfirm) onConfirm();
onClose();
};
const cancel = () =>
{
if(onCancel) onCancel();
onClose();
};
return (
<LayoutNotificationAlertView title={ title } onClose={ onClose } { ...rest } type={ NotificationAlertType.ALERT }>
<Flex center grow>
<Text>{ message }</Text>
</Flex>
<div className="flex gap-1">
<Button fullWidth variant="danger" onClick={ cancel }>{ cancelText }</Button>
<Button fullWidth onClick={ confirm }>{ confirmText }</Button>
</div>
</LayoutNotificationAlertView>
);
};
@@ -1,95 +0,0 @@
import { CreateLinkEvent, HabboClubLevelEnum } from '@nitrots/nitro-renderer';
import { FC, useMemo } from 'react';
import { FriendlyTime, GetConfigurationValue, LocalizeText } from '../../api';
import { Column, Flex, Grid, LayoutCurrencyIcon, Text } from '../../common';
import { usePurse } from '../../hooks';
import { CurrencyView } from './views/CurrencyView';
import { SeasonalView } from './views/SeasonalView';
export const PurseView: FC<{}> = props => {
const { purse = null, hcDisabled = false } = usePurse();
const displayedCurrencies = useMemo(() => GetConfigurationValue<number[]>('system.currency.types', []), []);
const currencyDisplayNumberShort = useMemo(() => GetConfigurationValue<boolean>('currency.display.number.short', false), []);
const getClubText = (() => {
if (!purse) return null;
const totalDays = ((purse.clubPeriods * 31) + purse.clubDays);
const minutesUntilExpiration = purse.minutesUntilExpiration;
if (purse.clubLevel === HabboClubLevelEnum.NO_CLUB) return LocalizeText('purse.clubdays.zero.amount.text');
else if ((minutesUntilExpiration > -1) && (minutesUntilExpiration < (60 * 24))) return FriendlyTime.shortFormat(minutesUntilExpiration * 60);
else return FriendlyTime.shortFormat(totalDays * 86400);
})();
const getCurrencyElements = (offset: number, limit: number = -1, seasonal: boolean = false) => {
if (!purse || !purse.activityPoints || !purse.activityPoints.size) return null;
const types = Array.from(purse.activityPoints.keys()).filter(type => (displayedCurrencies.indexOf(type) >= 0));
types.sort((a, b) => {
if (a === 0) return -1;
if (b === 0) return 1;
if (a === 5) return -1;
if (b === 5) return 1;
return a - b;
});
let count = 0;
while (count < offset) {
types.shift();
count++;
}
count = 0;
const elements: JSX.Element[] = [];
for (const type of types) {
if ((limit > -1) && (count === limit)) break;
if (seasonal) {
elements.push(<SeasonalView key={type} type={type} amount={purse.activityPoints.get(type)} />);
} else {
elements.push(<CurrencyView key={type} type={type} amount={purse.activityPoints.get(type)} short={currencyDisplayNumberShort} />);
}
count++;
}
return elements;
}
if (!purse) return null;
return (
<Column alignItems="end" className="nitro-purse-container" gap={0}>
<Flex className="nitro-purse rounded-bottom p-1">
<Grid fullWidth gap={1}>
<Column justifyContent="center" size={hcDisabled ? 10 : 6} gap={0}>
<CurrencyView type={-1} amount={purse.credits} short={currencyDisplayNumberShort} />
{getCurrencyElements(0, 2)}
</Column>
{!hcDisabled &&
<Column center pointer size={4} gap={1} className="nitro-purse-subscription rounded borderhccontent" onClick={event => CreateLinkEvent('habboUI/open/hccenter')}>
<LayoutCurrencyIcon type="hc" />
<Text variant="white">{getClubText}</Text>
</Column>}
<Column justifyContent="center" size={1} gap={0}>
<Flex center pointer fullHeight className="nitro-purse-button p-1 rounded coffecurrencybutton" onClick={event => CreateLinkEvent('help/show')}>
<i className="nitro-icon icon-help"/>
</Flex>
<Flex center pointer fullHeight className="nitro-purse-button p-1 rounded coffecurrencybutton" onClick={event => CreateLinkEvent('user-settings/toggle')}>
<i className="nitro-icon icon-cog"/>
</Flex>
</Column>
<Column justifyContent="center" size={11} gap={0}>
{getCurrencyElements(2, -1, true)}
</Column>
</Grid>
</Flex>
</Column>
);
}
@@ -1,39 +0,0 @@
import { FC, useMemo } from 'react';
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
import { LocalizeFormattedNumber, LocalizeShortNumber } from '../../../api';
import { Flex, LayoutCurrencyIcon, Text } from '../../../common';
interface CurrencyViewProps
{
type: number;
amount: number;
short: boolean;
}
export const CurrencyView: FC<CurrencyViewProps> = props =>
{
const { type = -1, amount = -1, short = false } = props;
const element = useMemo(() =>
{
return (
<Flex justifyContent="end" pointer gap={ 1 } className={`nitro-purse-button rounded allcurrencypurse nitro-purse-button currency-${type}`}>
<Text truncate textEnd variant="white" grow>{ short ? LocalizeShortNumber(amount) : LocalizeFormattedNumber(amount) }</Text>
<LayoutCurrencyIcon type={ type } />
</Flex>);
}, [ amount, short, type ]);
if(!short) return element;
return (
<OverlayTrigger
placement="left"
overlay={
<Tooltip id={ `tooltip-${ type }` }>
{ LocalizeFormattedNumber(amount) }
</Tooltip>
}>
{ element }
</OverlayTrigger>
);
}
@@ -1,38 +0,0 @@
import { FC } from 'react';
import { GetConfigurationValue, LocalizeFormattedNumber, LocalizeText } from '../../../api';
import { Flex, LayoutCurrencyIcon, Text } from '../../../common';
interface SeasonalViewProps {
type: number;
amount: number;
}
export const SeasonalView: FC<SeasonalViewProps> = props => {
const { type = -1, amount = -1 } = props;
const seasonalColor = GetConfigurationValue<string>('currency.seasonal.color', 'blue');
return (
<Flex
fullWidth
justifyContent="between"
className={`nitro-purse-seasonal-currency nitro-notification ${seasonalColor}`}
>
<Flex fullWidth>
<Text truncate fullWidth variant="white" className="seasonal-text-padding seasonal-text">
{LocalizeText(`purse.seasonal.currency.${type}`)}
</Text>
<Text
truncate
variant="white"
className="seasonal-amount text-end"
title={amount > 99999 ? LocalizeFormattedNumber(amount) : ''}
>
{amount > 99999 ? '99 999' : LocalizeFormattedNumber(amount)}
</Text>
<Flex className="nitro-seasonal-box seasonal-image-padding">
<LayoutCurrencyIcon type={type} />
</Flex>
</Flex>
</Flex>
);
};
@@ -1,24 +0,0 @@
import { FC } from 'react';
import { Column } from '../../common';
import { OfferView } from '../catalog/views/targeted-offer/OfferView';
import { GroupRoomInformationView } from '../groups/views/GroupRoomInformationView';
import { NotificationCenterView } from '../notification-center/NotificationCenterView';
import { PurseView } from '../purse/PurseView';
import { MysteryBoxExtensionView } from '../room/widgets/mysterybox/MysteryBoxExtensionView';
import { RoomPromotesWidgetView } from '../room/widgets/room-promotes/RoomPromotesWidgetView';
export const RightSideView: FC<{}> = props =>
{
return (
<div className="absolute top-[0] right-[10px] min-w-[200px] max-w-[200px] h-[calc(100%-55px)] pointer-events-none">
<Column gap={ 1 } position="relative">
<PurseView />
<GroupRoomInformationView />
<MysteryBoxExtensionView />
<OfferView />
<RoomPromotesWidgetView />
<NotificationCenterView />
</Column>
</div>
);
};
@@ -1,59 +0,0 @@
import { GetRenderer, RoomSession } from '@nitrots/nitro-renderer';
import { AnimatePresence, motion } from 'framer-motion';
import { FC, useEffect, useRef } from 'react';
import { DispatchMouseEvent, DispatchTouchEvent } from '../../api';
import { useRoom } from '../../hooks';
import { classNames } from '../../layout';
import { RoomSpectatorView } from './spectator/RoomSpectatorView';
import { RoomWidgetsView } from './widgets/RoomWidgetsView';
export const RoomView: FC<{}> = (props) =>
{
const { roomSession = null } = useRoom();
const elementRef = useRef<HTMLDivElement>();
useEffect(() =>
{
if(!roomSession) return;
const canvas = GetRenderer().canvas;
if(!canvas) return;
canvas.onclick = (event) => DispatchMouseEvent(event);
canvas.onmousemove = (event) => DispatchMouseEvent(event);
canvas.onmousedown = (event) => DispatchMouseEvent(event);
canvas.onmouseup = (event) => DispatchMouseEvent(event);
canvas.ontouchstart = (event) => DispatchTouchEvent(event);
canvas.ontouchmove = (event) => DispatchTouchEvent(event);
canvas.ontouchend = (event) => DispatchTouchEvent(event);
canvas.ontouchcancel = (event) => DispatchTouchEvent(event);
const element = elementRef.current;
if(!element) return;
canvas.classList.add('bg-black');
element.appendChild(canvas);
}, [roomSession]);
return (
<AnimatePresence>
{
<motion.div
initial={ { opacity: 0 }}
animate={ { opacity: 1 }}
exit={ { opacity: 0 }}>
<div ref={ elementRef } className="w-100 h-100">
{ roomSession instanceof RoomSession &&
<>
<RoomWidgetsView />
{ roomSession.isSpectator && <RoomSpectatorView /> }
</> }
</div>
</motion.div> }
</AnimatePresence>
);
};
@@ -1,8 +0,0 @@
import { FC } from 'react';
export const RoomSpectatorView: FC<{}> = props =>
{
return (
<div className="room-spectator"></div>
);
};
@@ -1,174 +0,0 @@
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 />
</>
);
};
@@ -1,59 +0,0 @@
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>
);
};
@@ -1,69 +0,0 @@
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>
);
};
@@ -1,281 +0,0 @@
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>
);
};
@@ -1,135 +0,0 @@
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>
);
};
@@ -1,139 +0,0 @@
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 />
</>
);
};
@@ -1,55 +0,0 @@
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>
);
};
@@ -1,475 +0,0 @@
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>
);
};
@@ -1,343 +0,0 @@
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>
);
};
@@ -1,84 +0,0 @@
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>
);
};
@@ -1,31 +0,0 @@
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>
);
};
@@ -1,23 +0,0 @@
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 } />
</>
);
};
@@ -1,31 +0,0 @@
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>
</>
);
};
@@ -1,262 +0,0 @@
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 z-50">
{avatarInfo.badges[0] && <LayoutBadgeImageView badgeCode={avatarInfo.badges[0]} showInfo={true} />}
</div>
<Flex center className="relative w-[40px] h-[40px] bg-no-repeat bg-center z-50" 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 z-50">
{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 z-50">
{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 z-50">
{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 z-50">
{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>
)}
</>
);
};
@@ -1,372 +0,0 @@
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>
);
};
@@ -1,29 +0,0 @@
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>
);
};
@@ -1,66 +0,0 @@
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>
);
};
@@ -1,32 +0,0 @@
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>
);
};
@@ -1,292 +0,0 @@
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>
);
};
@@ -1,220 +0,0 @@
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>
);
};
@@ -1,138 +0,0 @@
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>
);
};
@@ -1,198 +0,0 @@
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>
);
};
@@ -1,85 +0,0 @@
import { FC, MouseEvent, useEffect, useState } from 'react';
import { ArrowContainer, Popover } from 'react-tiny-popover';
import { Flex, Grid, NitroCardContentView } from '../../../../common';
interface ChatInputStyleSelectorViewProps
{
chatStyleId: number;
chatStyleIds: number[];
selectChatStyleId: (styleId: number) => void;
}
export const ChatInputStyleSelectorView: FC<ChatInputStyleSelectorViewProps> = props =>
{
const { chatStyleId = 0, chatStyleIds = null, selectChatStyleId = null } = props;
const [ target, setTarget ] = useState<(EventTarget & HTMLElement)>(null);
const [ selectorVisible, setSelectorVisible ] = useState(false);
const selectStyle = (styleId: number) =>
{
selectChatStyleId(styleId);
setSelectorVisible(false);
};
const toggleSelector = (event: MouseEvent<HTMLElement>) =>
{
let visible = false;
setSelectorVisible(prevValue =>
{
visible = !prevValue;
return visible;
});
if(visible) setTarget((event.target as (EventTarget & HTMLElement)));
};
useEffect(() =>
{
if(selectorVisible) return;
setTarget(null);
}, [ selectorVisible ]);
return (
<>
<Popover
containerClassName="max-w-[276px] not-italic font-normal leading-normal text-left no-underline [text-shadow:none] normal-case tracking-[normal] [word-break:normal] [word-spacing:normal] whitespace-normal text-[.7875rem] [word-wrap:break-word] bg-[#dfdfdf] bg-clip-padding border-[1px] border-[solid] border-[#283F5D] rounded-[.25rem] [box-shadow:0_2px_#00000073] z-[1070]"
content={ ({ position, childRect, popoverRect }) => (
<ArrowContainer // if you'd like an arrow, you can import the ArrowContainer!
arrowColor={ 'black' }
arrowSize={ 7 }
arrowStyle={ { bottom: 'calc(-.5rem - 1px)' } }
childRect={ childRect }
popoverRect={ popoverRect }
position={ position }
>
<NitroCardContentView className="bg-transparent !max-h-[200px]" overflow="hidden">
<Grid columnCount={ 3 } overflow="auto">
{ chatStyleIds && (chatStyleIds.length > 0) && chatStyleIds.map((styleId) =>
{
return (
<Flex key={ styleId } center pointer className="h-[30px]" onClick={ event => selectStyle(styleId) }>
<div key={ styleId } className="bubble-container relative w-[50px]">
<div className={ `relative max-w-[350px] min-h-[26px] text-[14px] chat-bubble bubble-${ styleId }` }>&nbsp;</div>
</div>
</Flex>
);
}) }
</Grid>
</NitroCardContentView>
</ArrowContainer>
) }
isOpen={ selectorVisible }
positions={ [ 'top' ] }
>
<div className="cursor-pointer nitro-icon chatstyles-icon" onClick={ toggleSelector } />
</Popover>
</>
);
};
@@ -1,248 +0,0 @@
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'))
);
};
@@ -1,99 +0,0 @@
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>
);
};
@@ -1,162 +0,0 @@
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>
);
};
@@ -1,94 +0,0 @@
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>
);
};
@@ -1,30 +0,0 @@
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 } />
);
};
@@ -1,31 +0,0 @@
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 } />;
};
@@ -1,26 +0,0 @@
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>;
};
@@ -1,18 +0,0 @@
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 } />;
};
@@ -1,32 +0,0 @@
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 } />;
};
@@ -1,18 +0,0 @@
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 } />;
};
@@ -1,144 +0,0 @@
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>
);
};
@@ -1,51 +0,0 @@
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>
);
};
@@ -1,28 +0,0 @@
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>
);
};
@@ -1,17 +0,0 @@
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 } />) }
</>
);
};
@@ -1,58 +0,0 @@
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>
);
};
@@ -1,30 +0,0 @@
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>
);
};
@@ -1,18 +0,0 @@
import { FC } from 'react';
import { LayoutBadgeImageView, 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 }>
<div className="flex justify-center mb-2">
<LayoutBadgeImageView badgeCode={ badgeName } showInfo={true} scale={2} />
</div>
</LayoutTrophyView>
);
};
@@ -1,115 +0,0 @@
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>
);
};
@@ -1,87 +0,0 @@
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>
);
};
@@ -1,33 +0,0 @@
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>
);
};
@@ -1,34 +0,0 @@
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>
);
};
@@ -1,66 +0,0 @@
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>
);
}
};
@@ -1,73 +0,0 @@
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>
);
};
@@ -1,60 +0,0 @@
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>
);
}) }
</>
);
};
@@ -1,9 +0,0 @@
import { FC } from 'react';
import { useFurnitureInternalLinkWidget } from '../../../../hooks';
export const FurnitureInternalLinkView: FC<{}> = props =>
{
const {} = useFurnitureInternalLinkWidget();
return null;
};
@@ -1,145 +0,0 @@
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>
);
};
@@ -1,81 +0,0 @@
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>
);
};
@@ -1,50 +0,0 @@
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>
);
};
@@ -1,9 +0,0 @@
import { FC } from 'react';
import { useFurnitureRoomLinkWidget } from '../../../../hooks';
export const FurnitureRoomLinkView: FC<{}> = props =>
{
const {} = useFurnitureRoomLinkWidget();
return null;
};
@@ -1,46 +0,0 @@
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>
);
};
@@ -1,58 +0,0 @@
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>
);
};
@@ -1,66 +0,0 @@
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>
);
};
@@ -1,12 +0,0 @@
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 } />;
};
@@ -1,47 +0,0 @@
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 />
</>
);
};

Some files were not shown because too many files have changed in this diff Show More