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