chore: checkpoint current work

This commit is contained in:
Lorenzune
2026-04-03 05:22:26 +02:00
parent 83540ff329
commit 36c0221a54
477 changed files with 3799 additions and 1071 deletions
+2 -2
View File
@@ -256,7 +256,7 @@ const CatalogModernViewInner: FC<{}> = () =>
{ /* Content area */ }
<div className="flex flex-1 overflow-hidden">
{ showFavorites
? <div className="flex-1 overflow-auto bg-card-content-area">
? <div className="flex-1 overflow-auto nitro-card-content-shell">
<CatalogFavoritesView onClose={ () => setShowFavorites(false) } />
</div>
: <>
@@ -264,7 +264,7 @@ const CatalogModernViewInner: FC<{}> = () =>
<div className="w-[170px] min-w-[170px] border-r-2 border-card-grid-item-border bg-card-grid-item overflow-y-auto py-1">
<CatalogNavigationView node={ activeNodes[0] } />
</div> }
<div className="flex-1 overflow-auto p-2 bg-card-content-area">
<div className="flex-1 overflow-auto p-2 nitro-card-content-shell">
{ adminMode && <CatalogAdminPageEditView /> }
{ GetCatalogLayout(currentPage, () => setNavigationHidden(true)) }
</div>
@@ -110,9 +110,9 @@ export const CatalogAdminOfferEditView: FC<{}> = () =>
<div className="fixed inset-0 flex items-center justify-center" style={ { zIndex: 1000 } } onClick={ () => setEditingOffer(null) }>
<div className="absolute inset-0 bg-black/30 backdrop-blur-[1px]" />
<div className="relative bg-light rounded-lg w-[420px] border-2 border-card-border overflow-hidden shadow-lg" onClick={ e => e.stopPropagation() }>
<div className="nitro-card-shell relative w-[420px] overflow-hidden shadow-lg" onClick={ e => e.stopPropagation() }>
{ /* Header */ }
<div className="flex items-center justify-between px-3 py-2 bg-card-header">
<div className="nitro-card-header-shell flex items-center justify-between px-3 py-2">
<span className="text-sm font-bold text-white">
{ isNew ? LocalizeText('catalog.admin.offer.new') : `${ LocalizeText('catalog.admin.offer.edit') } #${ editingOffer.offerId }` }
</span>
+36 -5
View File
@@ -1,19 +1,50 @@
import { FC } from 'react';
import { FC, useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import { useFriends } from '../../hooks';
import { FriendBarView } from './views/friends-bar/FriendsBarView';
import { FriendsListView } from './views/friends-list/FriendsListView';
import { FriendsMessengerView } from './views/messenger/FriendsMessengerView';
export const FriendsView: FC<{}> = props =>
{
const { settings = null, onlineFriends = [] } = useFriends();
const FRIEND_BAR_TARGET_IDS = [ 'toolbar-friend-bar-container-desktop' ];
export const FriendsView: FC<{}> = props => {
const { settings = null, onlineFriends = [], requests = [] } = useFriends();
const [ portalTarget, setPortalTarget ] = useState<HTMLElement | null>(null);
useEffect(() =>
{
if(typeof document === 'undefined') return;
const resolveTarget = () =>
{
for(const id of FRIEND_BAR_TARGET_IDS)
{
const element = document.getElementById(id);
if(element)
{
setPortalTarget(previous => ((previous === element) ? previous : element));
return;
}
}
setPortalTarget(null);
};
resolveTarget();
const observer = new MutationObserver(resolveTarget);
observer.observe(document.body, { childList: true, subtree: true });
return () => observer.disconnect();
}, []);
if(!settings) return null;
return (
<>
{ createPortal(<FriendBarView onlineFriends={ onlineFriends } />, document.getElementById('toolbar-friend-bar-container')) }
{ portalTarget && createPortal(<FriendBarView onlineFriends={ onlineFriends } requestsCount={ requests.length } />, portalTarget) }
<FriendsListView />
<FriendsMessengerView />
</>
@@ -1,66 +1,108 @@
import { FindNewFriendsMessageComposer, MouseEventType } from '@nitrots/nitro-renderer';
import { FC, useEffect, useRef, useState } from 'react';
import { GetUserProfile, LocalizeText, MessengerFriend, OpenMessengerChat, SendMessageComposer } from '../../../../api';
import { Button, LayoutAvatarImageView, LayoutBadgeImageView } from '../../../../common';
import { LayoutAvatarImageView, LayoutBadgeImageView } from '../../../../common';
import { useFriends } from '../../../../hooks';
import { motion, AnimatePresence } from 'framer-motion';
export const FriendBarItemView: FC<{ friend: MessengerFriend }> = props =>
{
export const FriendBarItemView: FC<{ friend: MessengerFriend }> = props => {
const { friend = null } = props;
const [ isVisible, setVisible ] = useState(false);
const [isVisible, setVisible] = useState(false);
const { followFriend = null } = useFriends();
const elementRef = useRef<HTMLDivElement>();
const elementRef = useRef<HTMLDivElement>(null);
useEffect(() =>
{
const onClick = (event: MouseEvent) =>
{
useEffect(() => {
const onClick = (event: MouseEvent) => {
const element = elementRef.current;
if(!element) return;
if((event.target !== element) && !element.contains((event.target as Node)))
{
if (!element) return;
if ((event.target !== element) && !element.contains((event.target as Node))) {
setVisible(false);
}
};
document.addEventListener(MouseEventType.MOUSE_CLICK, onClick);
return () => document.removeEventListener(MouseEventType.MOUSE_CLICK, onClick);
}, []);
if(!friend)
{
if (!friend) {
return (
<div ref={ elementRef } className={ 'friend-bar-item btn btn-secondary w-[130px] mx-[3px] my-0 z-0 relative pl-[37px] text-left friend-bar-search ' + (isVisible ? 'friend-bar-search-item-active' : '') } onClick={ () => setVisible(prev => !prev) }>
<div className="friend-bar-item-head absolute -top-[3px] left-[5px] w-[31px] h-[34px] bg-[url('@/assets/images/toolbar/friend-search.png')]" />
<div className="truncate text-white text-[13px]">{ LocalizeText('friend.bar.find.title') }</div>
{ isVisible &&
<div className="search-content mt-3">
<div className="bg-white text-black px-1 py-1 text-xs">{ LocalizeText('friend.bar.find.text') }</div>
<Button className="mt-2 mb-4" variant="secondary" onClick={ () => SendMessageComposer(new FindNewFriendsMessageComposer()) }>{ LocalizeText('friend.bar.find.button') }</Button>
</div> }
<div ref={elementRef} className="relative">
<motion.button
type="button"
whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}
className="relative flex h-[34px] w-[132px] items-center rounded-[7px] border border-[#9fc56f] bg-[#5f7d2f] pl-[34px] pr-[10px] text-left text-white shadow-[inset_0_1px_0_rgba(255,255,255,0.18),0_2px_0_rgba(0,0,0,0.25)]"
onClick={() => setVisible(prev => !prev)}
>
<div className="absolute left-[6px] top-1/2 h-[24px] w-[24px] -translate-y-1/2 bg-[url('@/assets/images/toolbar/friend-search.png')] bg-contain bg-center bg-no-repeat pointer-events-none" />
<div className="truncate text-[0.8rem] font-bold">{LocalizeText('friend.bar.find.title')}</div>
</motion.button>
<AnimatePresence>
{isVisible && (
<motion.div
initial={{ opacity: 0, y: 10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 10, scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 25 }}
className="absolute bottom-[calc(100%+12px)] left-1/2 -translate-x-1/2 tbme-panel whitespace-nowrap z-[80] flex flex-col items-center gap-2 pointer-events-auto min-w-[170px]"
>
<div className="text-white text-[13px] font-bold drop-shadow-[1px_1px_0_#000]">{LocalizeText('friend.bar.find.title')}</div>
<div className="text-white/80 text-xs px-2">{LocalizeText('friend.bar.find.text')}</div>
<button
className="px-3 py-1 bg-black/40 hover:bg-black/60 border border-white/10 rounded-lg text-white text-[11px] font-bold transition-colors cursor-pointer mt-1"
onClick={event => { event.stopPropagation(); SendMessageComposer(new FindNewFriendsMessageComposer()); setVisible(false); }}
>
{LocalizeText('friend.bar.find.button')}
</button>
</motion.div>
)}
</AnimatePresence>
</div>
);
}
return (
<div ref={ elementRef } className={ `friend-bar-item btn btn-success ${ isVisible ? 'w-[165px]' : 'w-[130px]' } mx-[3px] my-0 z-0 relative text-left${ isVisible ? ' mb-[84px]' : '' }` } style={{ paddingLeft: friend.id > 0 ? '70px' : '46px', paddingRight: isVisible ? '4px' : undefined }} onClick={ () => setVisible(prev => !prev) }>
<div className={ `friend-bar-item-head absolute ${ friend.id > 0 ? '-top-[31px] -left-[25px]' : '-top-[5px] -left-[3.5px]' }` }>
{ (friend.id > 0) &&
<LayoutAvatarImageView direction={ isVisible ? 2 : 3 } figure={ friend.figure } headOnly={ !isVisible } /> }
{ (friend.id <= 0) &&
<LayoutBadgeImageView badgeCode="ADM" isGroup={ false } /> }
<div ref={elementRef} className="relative">
<div className="absolute left-[-4px] bottom-[-2px] z-10 h-[66px] w-[34px] overflow-hidden pointer-events-none">
{(friend.id > 0) ? (
<LayoutAvatarImageView
direction={2}
figure={friend.figure}
headOnly={false}
className="block pointer-events-none drop-shadow-[1px_1px_0_rgba(0,0,0,0.6)]"
style={ { marginLeft: '-28px', marginTop: '-10px' } }
/>
) : (
<LayoutBadgeImageView badgeCode="ADM" isGroup={false} className="scale-75 block pointer-events-none drop-shadow-[1px_1px_0_rgba(0,0,0,0.6)]" />
)}
</div>
<div className="truncate text-white text-[13px]">{ friend.name }</div>
{ isVisible &&
<div className="flex justify-between gap-2 mt-1">
<div className="cursor-pointer nitro-friends-spritesheet icon-friendbar-chat" onClick={ event => { event.stopPropagation(); OpenMessengerChat(friend.id); } } />
{ friend.followingAllowed &&
<div className="cursor-pointer nitro-friends-spritesheet icon-friendbar-visit" onClick={ event => { event.stopPropagation(); followFriend(friend); } } /> }
<div className="cursor-pointer nitro-friends-spritesheet icon-profile" onClick={ event => { event.stopPropagation(); GetUserProfile(friend.id); } } />
</div> }
<motion.button
type="button"
whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}
className="relative flex h-[34px] w-[132px] items-center rounded-[7px] border border-[#9fc56f] bg-[#6f8f39] pl-[44px] pr-[10px] text-left text-white shadow-[inset_0_1px_0_rgba(255,255,255,0.18),0_2px_0_rgba(0,0,0,0.25)] overflow-visible"
onClick={() => setVisible(prev => !prev)}
>
<div className="truncate text-[0.82rem] font-bold">{friend.name}</div>
</motion.button>
<AnimatePresence>
{isVisible && (
<motion.div
initial={{ opacity: 0, y: 10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 10, scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 25 }}
className="absolute bottom-[calc(100%+12px)] left-1/2 -translate-x-1/2 tbme-panel flex flex-col items-center gap-2 z-[80] pointer-events-auto min-w-[110px]"
>
<div className="text-white font-bold text-[13px] drop-shadow-[1px_1px_0_#000] truncate max-w-[120px] px-1">{friend.name}</div>
<div className="flex justify-center gap-3 px-2">
<div className="cursor-pointer tbme-icon nitro-friends-spritesheet icon-friendbar-chat hover:-translate-y-1 transition-transform" onClick={event => { event.stopPropagation(); OpenMessengerChat(friend.id); setVisible(false); }} />
{friend.followingAllowed &&
<div className="cursor-pointer tbme-icon nitro-friends-spritesheet icon-friendbar-visit hover:-translate-y-1 transition-transform" onClick={event => { event.stopPropagation(); followFriend(friend); setVisible(false); }} />}
<div className="cursor-pointer tbme-icon nitro-friends-spritesheet icon-profile hover:-translate-y-1 transition-transform" onClick={event => { event.stopPropagation(); GetUserProfile(friend.id); setVisible(false); }} />
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
};
@@ -1,26 +1,133 @@
import { FC, useRef, useState } from 'react';
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa';
import { MessengerFriend } from '../../../../api';
import { Button } from '../../../../common';
import { LocalizeText, MessengerFriend } from '../../../../api';
import { FriendBarItemView } from './FriendBarItemView';
import { motion, AnimatePresence } from 'framer-motion';
const MAX_DISPLAY_COUNT = 3;
export const FriendBarView: FC<{ onlineFriends: MessengerFriend[] }> = props =>
{
const { onlineFriends = null } = props;
// Mirrored from Toolbar to keep physics identical
const containerVariants = {
hidden: {},
visible: { transition: { staggerChildren: 0.05 } },
exit: { transition: { staggerChildren: 0.03, staggerDirection: -1 as const } },
};
const itemVariants = {
hidden: { opacity: 0, y: 10, scale: 0.8 },
visible: { opacity: 1, y: 0, scale: 1, transition: { type: 'spring', stiffness: 400, damping: 22 } },
exit: { opacity: 0, y: 6, scale: 0.85, transition: { duration: 0.1 } },
};
export const FriendBarView: FC<{ onlineFriends: MessengerFriend[]; requestsCount?: number }> = props => {
const { onlineFriends = [], requestsCount = 0 } = props;
const [ indexOffset, setIndexOffset ] = useState(0);
const elementRef = useRef<HTMLDivElement>();
const elementRef = useRef<HTMLDivElement>(null);
const hasScrollableFriends = (onlineFriends.length > MAX_DISPLAY_COUNT);
const visibleFriends = onlineFriends.slice(indexOffset, (indexOffset + MAX_DISPLAY_COUNT));
return (
<div ref={ elementRef } className="flex items-center ">
<Button className="z-2 cursor-pointer" disabled={ (indexOffset <= 0) } variant="black" onClick={ event => setIndexOffset(indexOffset - 1) }>
<FaChevronLeft className="fa-icon" />
</Button>
{ Array.from(Array(MAX_DISPLAY_COUNT), (e, i) => <FriendBarItemView key={ i } friend={ (onlineFriends[indexOffset + i] || null) } />) }
<Button className="z-2 cursor-pointer" disabled={ !((onlineFriends.length > MAX_DISPLAY_COUNT) && ((indexOffset + MAX_DISPLAY_COUNT) <= (onlineFriends.length - 1))) } variant="black" onClick={ event => setIndexOffset(indexOffset + 1) }>
<FaChevronRight className="fa-icon" />
</Button>
</div>
<motion.div
ref={elementRef}
className="flex h-[40px] items-center gap-[6px] px-[2px] py-[3px]"
variants={containerVariants}
initial="hidden"
animate="visible"
exit="exit"
>
{ (requestsCount > 0) &&
<motion.div variants={ itemVariants }>
<div className="flex h-[34px] items-center rounded-[7px] border border-[#9fc56f] bg-[#5f7d2f] px-[10px] text-[0.74rem] font-bold whitespace-nowrap text-white shadow-[inset_0_1px_0_rgba(255,255,255,0.18),0_2px_0_rgba(0,0,0,0.25)]">
{ requestsCount } richieste
</div>
</motion.div> }
<motion.div variants={itemVariants}>
<div
className={ `flex h-[34px] w-[20px] items-center justify-center text-white/80 transition-all ${ (!hasScrollableFriends || (indexOffset <= 0)) ? 'opacity-30 cursor-not-allowed' : 'cursor-pointer hover:text-white active:scale-95' }` }
onClick={ () => { if(indexOffset > 0) setIndexOffset(indexOffset - 1); } }
>
<FaChevronLeft className="text-white/70 text-sm drop-shadow-[1px_1px_0_#000]" />
</div>
</motion.div>
<AnimatePresence mode="popLayout">
{ visibleFriends.map(friend => (
<motion.div
key={ friend.id }
variants={ itemVariants }
layout
initial="hidden"
animate="visible"
exit="exit"
>
<FriendBarItemView friend={ friend } />
</motion.div>
)) }
<motion.div
key="friend-search"
variants={ itemVariants }
layout
initial="hidden"
animate="visible"
exit="exit"
>
<FriendBarItemView friend={ null } />
</motion.div>
{ (!onlineFriends.length && (requestsCount <= 0)) &&
<motion.div
key="friend-empty"
variants={ itemVariants }
layout
initial="hidden"
animate="visible"
exit="exit"
>
<div className="flex h-[34px] items-center rounded-[7px] border border-[#9fc56f] bg-[#5f7d2f] px-[10px] text-[0.74rem] font-medium whitespace-nowrap text-white shadow-[inset_0_1px_0_rgba(255,255,255,0.18),0_2px_0_rgba(0,0,0,0.25)]">
Nessun amico online
</div>
</motion.div> }
</AnimatePresence>
<motion.div variants={itemVariants}>
<div
className={ `flex h-[34px] w-[20px] items-center justify-center text-white/80 transition-all ${ (!hasScrollableFriends || !((onlineFriends.length > MAX_DISPLAY_COUNT) && ((indexOffset + MAX_DISPLAY_COUNT) <= (onlineFriends.length - 1)))) ? 'opacity-30 cursor-not-allowed' : 'cursor-pointer hover:text-white active:scale-95' }` }
onClick={ () => { if((onlineFriends.length > MAX_DISPLAY_COUNT) && ((indexOffset + MAX_DISPLAY_COUNT) <= (onlineFriends.length - 1))) setIndexOffset(indexOffset + 1); } }
>
<FaChevronRight className="text-white/70 text-sm drop-shadow-[1px_1px_0_#000]" />
</div>
</motion.div>
<style>{FRIENDBAR_STYLES}</style>
</motion.div>
);
};
const FRIENDBAR_STYLES = `
.tbme-panel {
background: rgba(18, 16, 14, 0.88);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 14px;
padding: 10px 12px;
box-shadow:
0 10px 36px rgba(0, 0, 0, 0.65),
0 1px 0 rgba(255, 255, 255, 0.05) inset;
}
.tbme-icon {
opacity: 0.72;
cursor: pointer;
transition: opacity 0.15s ease, transform 0.15s ease;
}
.tbme-icon:hover {
opacity: 1;
transform: translateY(-2px);
}
.tbme-icon:active {
opacity: 0.85;
transform: translateY(0);
}
`;
@@ -1,7 +1,8 @@
import { DesktopViewEvent, GetGuestRoomResultEvent, GetSessionDataManager, GroupInformationComposer, GroupInformationEvent, GroupInformationParser, GroupRemoveMemberComposer, HabboGroupDeactivatedMessageEvent, RoomEntryInfoMessageEvent } from '@nitrots/nitro-renderer';
import { FC, useEffect, useRef, useState } from 'react';
import { FaChevronDown, FaChevronUp } from 'react-icons/fa';
import { FaChevronDown } from 'react-icons/fa';
import { GetGroupInformation, GetGroupManager, GroupMembershipType, GroupType, LocalizeText, SendMessageComposer, TryJoinGroup } from '../../../api';
import groupIcon from '../../../assets/images/rightside/group.png';
import { Button, Flex, LayoutBadgeImageView, Text } from '../../../common';
import { useMessageEvent, useNotification } from '../../../hooks';
@@ -12,6 +13,7 @@ export const GroupRoomInformationView: FC<{}> = props =>
const requestRetryTimeoutRef = useRef<ReturnType<typeof setTimeout>>(null);
const [ groupInformation, setGroupInformation ] = useState<GroupInformationParser>(null);
const [ isOpen, setIsOpen ] = useState<boolean>(true);
const [ isCompact, setIsCompact ] = useState(false);
const { showConfirm = null } = useNotification();
const clearRequestRetryTimeout = () =>
@@ -113,6 +115,19 @@ export const GroupRoomInformationView: FC<{}> = props =>
useEffect(() => () => clearRequestRetryTimeout(), []);
useEffect(() =>
{
if(isOpen)
{
setIsCompact(false);
return;
}
const timeout = window.setTimeout(() => setIsCompact(true), 220);
return () => window.clearTimeout(timeout);
}, [ isOpen ]);
const leaveGroup = () =>
{
showConfirm(LocalizeText('group.leaveconfirm.desc'), () =>
@@ -157,27 +172,43 @@ export const GroupRoomInformationView: FC<{}> = props =>
if(!groupInformation) return null;
return (
<div className="pointer-events-auto px-[5px] py-[6px] [box-shadow:inset_0_5px_#22222799,inset_0_-4px_#12121599] bg-[#1c1c20f2] rounded text-sm">
<div className="flex flex-col gap-2">
<Flex pointer alignItems="center" justifyContent="between" onClick={ event => setIsOpen(value => !value) }>
<Text variant="white">{ LocalizeText('group.homeroominfo.title') }</Text>
{ isOpen && <FaChevronUp className="fa-icon" /> }
{ !isOpen && <FaChevronDown className="fa-icon" /> }
<div className={ `pointer-events-auto mt-[6px] overflow-hidden rounded-[10px] border border-white/6 bg-[rgba(10,10,12,0.58)] text-sm shadow-[0_8px_18px_rgba(0,0,0,0.12)] transition-[width,max-width] duration-200 ease-out ${ isCompact ? 'ml-auto w-[52px] max-w-[52px]' : 'w-full max-w-[188px]' }` }>
<div className="flex flex-col">
<Flex pointer alignItems="center" justifyContent={ isCompact ? 'end' : 'between' } className={ `border-b border-white/6 bg-[linear-gradient(180deg,rgba(255,255,255,0.08),rgba(255,255,255,0.03))] px-[7px] py-[5px] max-[420px]:px-[6px] max-[420px]:py-[4px] ${ isCompact ? 'gap-[5px] px-[6px] py-[5px]' : '' }` } onClick={ event => setIsOpen(value => !value) }>
<Flex alignItems="center" gap={ 1 } className={ isCompact ? 'mr-[0]' : '' }>
<div className="flex h-[18px] w-[18px] items-center justify-center">
<img src={ groupIcon } alt="" className="h-[14px] w-auto object-contain" />
</div>
{ !isCompact && <Text variant="white" className="text-[0.78rem] font-bold leading-none text-white/90 max-[420px]:text-[0.74rem]">{ LocalizeText('group.homeroominfo.title') }</Text> }
</Flex>
<div className={ `flex h-[20px] w-[20px] items-center justify-center rounded-[6px] border border-white/8 bg-white/6 text-white/80 transition-transform duration-300 ease-out ${ isOpen ? 'rotate-180' : 'rotate-0' }` }>
<FaChevronDown className="fa-icon text-[10px]" />
</div>
</Flex>
{ isOpen &&
<div className={ `overflow-hidden transition-all duration-[580ms] ease-[cubic-bezier(0.16,1,0.3,1)] ${ isOpen ? 'max-h-[280px] opacity-100 translate-y-0 scale-y-100' : 'max-h-0 opacity-0 -translate-y-[8px] scale-y-95' } origin-top` }>
<>
<Flex pointer alignItems="center" gap={ 2 } onClick={ event => GetGroupInformation(groupInformation.id) }>
<div className="group-badge">
<LayoutBadgeImageView badgeCode={ groupInformation.badge } isGroup={ true } />
<Flex pointer alignItems="center" gap={ 2 } className={ `px-[10px] py-[10px] max-[420px]:px-[8px] max-[420px]:py-[8px] transition-all duration-[480ms] ease-[cubic-bezier(0.16,1,0.3,1)] ${ isOpen ? 'translate-y-0 opacity-100 delay-[80ms]' : '-translate-y-[6px] opacity-0 delay-0' }` } onClick={ event => GetGroupInformation(groupInformation.id) }>
<div className="group-badge flex h-[42px] w-[42px] shrink-0 items-center justify-center overflow-hidden max-[420px]:h-[38px] max-[420px]:w-[38px]">
<LayoutBadgeImageView
badgeCode={ groupInformation.badge }
isGroup={ true }
classNames={ [ 'w-full!', 'h-full!', 'bg-contain!' ] }
style={ { width: '100%', height: '100%', backgroundSize: 'contain' } }
/>
</div>
<div className="min-w-0 flex-1">
<Text truncate variant="white" className="text-[0.82rem] font-bold leading-tight text-white/92 max-[420px]:text-[0.76rem]">{ groupInformation.title }</Text>
</div>
<Text variant="white">{ groupInformation.title }</Text>
</Flex>
{ (groupInformation.type !== GroupType.PRIVATE || isRealOwner) &&
<Button fullWidth disabled={ (groupInformation.membershipType === GroupMembershipType.REQUEST_PENDING) } variant="success" onClick={ handleButtonClick }>
{ LocalizeText(getButtonText()) }
</Button>
<div className={ `px-[9px] pb-[9px] max-[420px]:px-[8px] max-[420px]:pb-[8px] transition-all duration-[480ms] ease-[cubic-bezier(0.16,1,0.3,1)] ${ isOpen ? 'translate-y-0 opacity-100 delay-[160ms]' : '-translate-y-[6px] opacity-0 delay-0' }` }>
<Button fullWidth disabled={ (groupInformation.membershipType === GroupMembershipType.REQUEST_PENDING) } className="h-[30px] rounded-[6px] border border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.88),rgba(214,214,214,0.76))] !text-black text-[0.82rem] font-bold shadow-[inset_0_1px_0_rgba(255,255,255,0.75),0_1px_0_rgba(0,0,0,0.18)] hover:brightness-100 max-[420px]:h-[28px] max-[420px]:text-[0.78rem]" onClick={ handleButtonClick }>
{ LocalizeText(getButtonText()) }
</Button>
</div>
}
</> }
</>
</div>
</div>
</div>
);
@@ -17,7 +17,7 @@ export const GuideToolAcceptView: FC<GuideToolAcceptViewProps> = props =>
return (
<div className="flex flex-col">
<div className="flex flex-col gap-0 bg-muted p-2 rounded">
<div className="nitro-card-panel flex flex-col gap-0 p-2">
<Text bold>{ LocalizeText('guide.help.request.guide.accept.request.title') }</Text>
<Text variant="muted">{ LocalizeText('guide.help.request.type.1') }</Text>
<Text textBreak wrap>{ helpRequestDescription }</Text>
@@ -35,7 +35,7 @@ export const GuideToolMenuView: FC<GuideToolMenuViewProps> = props =>
return (
<div className="flex flex-col">
<Flex alignItems="center" className="bg-muted p-2 rounded" gap={ 2 }>
<Flex alignItems="center" className="nitro-card-panel p-2" gap={ 2 }>
<div className={ 'duty-switch' + (isOnDuty ? '' : ' off') } onClick={ event => processAction('toggle_duty') } />
<Column gap={ 0 }>
<Text bold>{ LocalizeText('guide.help.guide.tool.yourstatus') }</Text>
@@ -57,7 +57,7 @@ export const GuideToolMenuView: FC<GuideToolMenuViewProps> = props =>
<Text>{ LocalizeText('guide.help.guide.tool.tickettypeselection.bullyreports') }</Text>
</div>
</div>
<hr className="bg-dark m-0" />
<hr className="nitro-card-divider m-0" />
<div className="flex justify-enter items-center gap-2">
<div className="info-icon" />
<div className="flex flex-col gap-1">
@@ -66,7 +66,7 @@ export const GuideToolMenuView: FC<GuideToolMenuViewProps> = props =>
<div dangerouslySetInnerHTML={ { __html: LocalizeText('guide.help.guide.tool.guardiansonduty', [ 'amount' ], [ guardiansOnDuty.toString() ]) } } />
</div>
</div>
<hr className="bg-dark m-0" />
<hr className="nitro-card-divider m-0" />
<Flex gap={ 2 } justifyContent="between">
<Button disabled onClick={ event => processAction('forum_link') }>{ LocalizeText('guide.help.guide.tool.forum.link') }</Button>
<Button disabled>{ LocalizeText('guide.help.guide.tool.skill.link') }</Button>
@@ -73,7 +73,7 @@ export const GuideToolOngoingView: FC<GuideToolOngoingViewProps> = props =>
return (
<Column fullHeight>
<Flex alignItems="center" className="p-2 rounded bg-muted" gap={ 1 } justifyContent="between">
<Flex alignItems="center" className="nitro-card-panel p-2" gap={ 1 } justifyContent="between">
{ isGuide &&
<div className="relative inline-flex align-middle">
<Button onClick={ visit }>{ LocalizeText('guide.help.request.guide.ongoing.visit.button') }</Button>
@@ -86,7 +86,7 @@ export const GuideToolOngoingView: FC<GuideToolOngoingViewProps> = props =>
</Column> }
<Button disabled variant="danger">{ LocalizeText('guide.help.common.report.link') }</Button>
</Flex>
<Column className="p-2 rounded bg-muted chat-messages" gap={ 1 } overflow="hidden">
<Column className="nitro-card-panel p-2 chat-messages" gap={ 1 } overflow="hidden">
<Column overflow="auto">
{ messageGroups.map((group, index) =>
{
@@ -96,7 +96,7 @@ export const GuideToolOngoingView: FC<GuideToolOngoingViewProps> = props =>
{ (!isOwnChat(group.userId)) &&
<LayoutAvatarImageView direction={ 2 } figure={ userFigure } /> }
</div>
<div className={ 'bg-light text-black border-radius mb-2 rounded py-1 px-2 messages-group-' + (isOwnChat(group.userId) ? 'right' : 'left') }>
<div className={ 'nitro-card-row text-black border-radius mb-2 py-1 px-2 messages-group-' + (isOwnChat(group.userId) ? 'right' : 'left') }>
<Text bold>
{ (isOwnChat(group.userId)) && GetSessionDataManager().userName }
{ (!isOwnChat(group.userId)) && userName }
@@ -16,7 +16,7 @@ export const GuideToolUserFeedbackView: FC<GuideToolUserFeedbackViewProps> = pro
return (
<div className="flex flex-col">
<Flex className="bg-muted p-2 rounded" gap={ 1 } justifyContent="between">
<Flex className="nitro-card-panel p-2" gap={ 1 } justifyContent="between">
<Column gap={ 0 }>
<Text bold>{ userName }</Text>
<Text>{ LocalizeText('guide.help.request.user.feedback.guide.desc') }</Text>
@@ -29,7 +29,7 @@ export const GuideToolUserFeedbackView: FC<GuideToolUserFeedbackViewProps> = pro
</div>
{ userName && (userName.length > 0) &&
<>
<hr className="bg-dark m-0 mt-auto" />
<hr className="nitro-card-divider m-0 mt-auto" />
<div className="flex flex-col">
<Text bold center>{ LocalizeText('guide.help.request.user.feedback.question') }</Text>
<div className="flex gap-1">
@@ -17,7 +17,7 @@ export const GuideToolUserPendingView: FC<GuideToolUserPendingViewProps> = props
return (
<div className="flex flex-col">
<Column className="bg-muted rounded p-2" gap={ 0 }>
<Column className="nitro-card-panel p-2" gap={ 0 }>
<Text bold>{ LocalizeText('guide.help.request.guide.accept.request.title') }</Text>
<Text variant="muted">{ LocalizeText('guide.help.request.type.1') }</Text>
<Text textBreak wrap>{ helpRequestDescription }</Text>
@@ -30,7 +30,7 @@ export const NameChangeConfirmationView: FC<NameChangeLayoutViewProps> = props =
return (
<div className="flex flex-col gap-4 h-full">
<div className="bg-muted rounded p-2 text-center">{ LocalizeText('tutorial.name_change.info.confirm') }</div>
<div className="nitro-card-panel p-2 text-center">{ LocalizeText('tutorial.name_change.info.confirm') }</div>
<div className="flex flex-col items-center gap-1 h-full">
<div>{ LocalizeText('tutorial.name_change.confirm') }</div>
<div className="font-bold ">{ username }</div>
@@ -9,7 +9,7 @@ export const NameChangeInitView: FC<NameChangeLayoutViewProps> = props =>
return (
<div className="flex flex-col gap-4 h-full">
<div className="bg-muted rounded p-2 text-center">{ LocalizeText('tutorial.name_change.info.main') }</div>
<div className="nitro-card-panel p-2 text-center">{ LocalizeText('tutorial.name_change.info.main') }</div>
<div className="font-bold flex items-center justify-center size-full">{ LocalizeText('tutorial.name_change.current', [ 'name' ], [ GetSessionDataManager().userName ]) }</div>
<div className="flex gap-2">
<button className="btn btn-success w-full" onClick={ () => onAction('start') }>{ LocalizeText('tutorial.name_change.change') }</button>
@@ -80,14 +80,14 @@ export const NameChangeInputView: FC<NameChangeLayoutViewProps> = props =>
<button className="btn btn-primary" disabled={ newUsername === '' || isChecking } onClick={ check }>{ LocalizeText('tutorial.name_change.check') }</button>
</div>
{ !errorCode && !canProceed &&
<div className="p-2 text-center rounded bg-muted">{ LocalizeText('help.tutorial.name.info') }</div> }
<div className="nitro-card-panel p-2 text-center">{ LocalizeText('help.tutorial.name.info') }</div> }
{ errorCode &&
<div className="p-2 text-center text-white rounded bg-danger">{ LocalizeText(`help.tutorial.name.${ errorCode }`, [ 'name' ], [ newUsername ]) }</div> }
{ canProceed &&
<div className="p-2 text-center text-white rounded bg-success">{ LocalizeText('help.tutorial.name.available', [ 'name' ], [ newUsername ]) }</div> }
{ suggestions &&
<div className="flex flex-col gap-2">
{ suggestions.map((suggestion, index) => <div key={ index } className="p-1 rounded cursor-pointer col bg-muted" onClick={ () => handleUsernameChange(suggestion) }>{ suggestion }</div>) }
{ suggestions.map((suggestion, index) => <div key={ index } className="nitro-card-row p-1 cursor-pointer col" onClick={ () => handleUsernameChange(suggestion) }>{ suggestion }</div>) }
</div> }
<div className="flex gap-2">
<button className="w-full btn btn-success" disabled={ !canProceed } onClick={ () => onAction('confirmation', newUsername) }>{ LocalizeText('tutorial.name_change.pick') }</button>
+5 -1
View File
@@ -244,8 +244,12 @@ export const NavigatorView: FC<{}> = props =>
<NavigatorSearchView sendSearch={ sendSearch } />
<div ref={ elementRef } className="flex flex-col flex-1 min-h-0 overflow-auto gap-2">
{ (searchResult && searchResult.results.map((result, index) => <NavigatorSearchResultView key={ index } searchResult={ result } />)) }
{ (searchResult && (!searchResult.results || (searchResult.results.length === 0))) &&
<div className="nitro-card-panel px-3 py-2 text-sm text-muted">
{ LocalizeText(searchResult.code === 'myworld_view' ? 'navigator.no.user.rooms.to.show' : 'navigator.no.results') }
</div> }
</div>
<Flex className="pt-2 border-t border-muted gap-2">
<Flex className="nitro-card-divider pt-2 border-t gap-2">
<Flex
pointer
alignItems="center"
@@ -163,7 +163,7 @@ export const NavigatorRoomInfoView: FC<NavigatorRoomInfoViewProps> = props =>
{ (navigatorData.enteredGuestRoom.tags.length > 0) &&
<div className="flex flex-wrap items-center gap-1 mt-1">
{ navigatorData.enteredGuestRoom.tags.map(tag => (
<Text key={ tag } pointer className="px-1 rounded bg-muted cursor-pointer text-xs" onClick={ () => processAction('navigator_search_tag', tag) }>
<Text key={ tag } pointer className="nitro-card-row px-1 cursor-pointer text-xs" onClick={ () => processAction('navigator_search_tag', tag) }>
#{ tag }
</Text>
)) }
@@ -52,7 +52,7 @@ export const NavigatorRoomSettingsModTabView: FC<NavigatorRoomSettingsTabViewPro
<Grid overflow="auto">
<Column size={ 6 }>
<Text bold>{ LocalizeText('navigator.roomsettings.moderation.banned.users') } ({ bannedUsers.length })</Text>
<Flex overflow="hidden" className="bg-white rounded list-container p-2">
<Flex overflow="hidden" className="nitro-card-panel list-container p-2">
<Column fullWidth overflow="auto" gap={ 1 }>
{ bannedUsers && (bannedUsers.length > 0) && bannedUsers.map((user, index) =>
{
@@ -116,7 +116,7 @@ export const NavigatorRoomSettingsRightsTabView: FC<NavigatorRoomSettingsTabView
) }
</Text>
<Flex overflow="hidden" className="p-2 bg-white rounded list-container">
<Flex overflow="hidden" className="nitro-card-panel p-2 list-container">
<Column fullWidth overflow="auto" gap={ 1 }>
{ Array.from(filteredUsersWithRights.entries()).map(([ id, name ], index) =>
{
@@ -155,7 +155,7 @@ export const NavigatorRoomSettingsRightsTabView: FC<NavigatorRoomSettingsTabView
) }
</Text>
<Flex overflow="hidden" className="p-2 bg-white rounded list-container">
<Flex overflow="hidden" className="nitro-card-panel p-2 list-container">
<Column fullWidth overflow="auto" gap={ 1 }>
{ friendsWithoutRights.map((friend, index) =>
{
@@ -89,7 +89,7 @@ export const NavigatorSearchResultItemInfoView: FC<NavigatorSearchResultItemInfo
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 border-[solid] border-[#283F5D] rounded-[.25rem] [box-shadow:0_2px_#00000073] z-[1070]"
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-[#f2f2eb] border border-[#000] rounded-[8px] shadow-none z-[1070]"
content={ ({ position, childRect, popoverRect }) => (
<ArrowContainer
arrowColor="black"
@@ -99,7 +99,7 @@ export const NavigatorSearchResultItemInfoView: FC<NavigatorSearchResultItemInfo
popoverRect={ popoverRect }
position={ position }
>
<NitroCardContentView className="bg-transparent room-info image-rendering-pixelated" overflow="hidden" onClick={ e => e.stopPropagation() }>
<NitroCardContentView className="bg-transparent room-info image-rendering-pixelated !p-0" overflow="hidden" onClick={ e => e.stopPropagation() }>
<Flex gap={ 1 } overflow="hidden" className="p-2">
<LayoutRoomThumbnailView className="flex flex-col items-center justify-end mb-1" customUrl={ roomData.officialRoomPicRef } roomId={ roomData.roomId }>
{ roomData.habboGroupId > 0 && (
@@ -129,7 +129,7 @@ export const NavigatorSearchResultItemView: FC<NavigatorSearchResultItemViewProp
pointer
overflow="hidden"
alignItems="center"
className="navigator-item p-1 bg-light rounded-3 small mb-1 flex-col border border-muted"
className="navigator-item nitro-card-row p-1 small mb-1 flex-col"
gap={ 0 }
onClick={ visitRoom }
onMouseEnter={ handleMouseEnter }
@@ -52,14 +52,14 @@ export const NavigatorSearchResultView: FC<NavigatorSearchResultViewProps> = pro
{
if(!searchResult) return;
setIsExtended(!searchResult.closed);
setIsExtended((searchResult.code === 'myworld_view') ? true : !searchResult.closed);
setDisplayMode(searchResult.mode);
}, [ searchResult ]);
const gridHasTwoColumns = (displayMode >= NavigatorSearchResultViewDisplayMode.THUMBNAILS);
return (
<Column className="bg-white rounded border border-muted" gap={ 0 }>
<Column className="nitro-card-panel" 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" /> }
@@ -105,6 +105,10 @@ export const NavigatorSearchResultView: FC<NavigatorSearchResultViewProps> = pro
/>
)) }
</Grid> }
{ (searchResult.rooms.length === 0) &&
<Text className="px-3 py-2 text-sm" variant="muted">
{ LocalizeText(searchResult.code === 'myworld_view' ? 'navigator.no.user.rooms.to.show' : 'navigator.no.results') }
</Text> }
</> }
</Column>
);
+22 -5
View File
@@ -56,6 +56,7 @@ export interface INitroPluginApi
// Internal plugin storage
const _plugins: INitroPlugin[] = [];
const _listeners: Array<() => void> = [];
const _windowCleanup = new Map<string, () => void>();
function notifyListeners()
{
@@ -182,7 +183,7 @@ const pluginApi: INitroPluginApi = {
let isDragging = false;
let offsetX = 0, offsetY = 0;
header.addEventListener('mousedown', (e: MouseEvent) =>
const onMouseDown = (e: MouseEvent) =>
{
isDragging = true;
const rect = overlay.getBoundingClientRect();
@@ -191,16 +192,20 @@ const pluginApi: INitroPluginApi = {
overlay.style.transform = 'none';
overlay.style.left = rect.left + 'px';
overlay.style.top = rect.top + 'px';
});
};
document.addEventListener('mousemove', (e: MouseEvent) =>
const onMouseMove = (e: MouseEvent) =>
{
if (!isDragging) return;
overlay.style.left = (e.clientX - offsetX) + 'px';
overlay.style.top = (e.clientY - offsetY) + 'px';
});
};
document.addEventListener('mouseup', () => { isDragging = false; });
const onMouseUp = () => { isDragging = false; };
header.addEventListener('mousedown', onMouseDown);
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
// Content area
const content = document.createElement('div');
@@ -211,11 +216,23 @@ const pluginApi: INitroPluginApi = {
overlay.appendChild(card);
document.body.appendChild(overlay);
_windowCleanup.set(id, () =>
{
header.removeEventListener('mousedown', onMouseDown);
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
});
return content;
},
destroyWindow(id: string)
{
const cleanup = _windowCleanup.get(id);
cleanup?.();
_windowCleanup.delete(id);
const existing = document.getElementById(`nitro-plugin-window-${id}`);
if (existing) existing.remove();
}
+68 -52
View File
@@ -1,17 +1,21 @@
import { CreateLinkEvent, HabboClubLevelEnum } from '@nitrots/nitro-renderer';
import { FC, useMemo } from 'react';
import { FC, useEffect, useMemo, useState } from 'react';
import { FaChevronDown, FaQuestionCircle } from 'react-icons/fa';
import { FriendlyTime, GetConfigurationValue, LocalizeText } from '../../api';
import { Column, Flex, Grid, LayoutCurrencyIcon, Text } from '../../common';
import { Column, Flex, LayoutCurrencyIcon, Text } from '../../common';
import { usePurse } from '../../hooks';
import purseIcon from '../../assets/images/rightside/purse.gif';
import { CurrencyView } from './views/CurrencyView';
import { SeasonalView } from './views/SeasonalView';
export const PurseView: FC<{}> = props => {
const { purse = null, hcDisabled = false } = usePurse();
const [ isOpen, setIsOpen ] = useState(true);
const [ isCompact, setIsCompact ] = useState(false);
const displayedCurrencies = useMemo(() => GetConfigurationValue<number[]>('system.currency.types', []), []);
const currencyDisplayNumberShort = useMemo(() => GetConfigurationValue<boolean>('currency.display.number.short', false), []);
const getClubText = (() => {
if (!purse) return null;
@@ -23,11 +27,10 @@ export const PurseView: FC<{}> = props => {
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 currencyTypes = useMemo(() => {
if (!purse || !purse.activityPoints || !purse.activityPoints.size) return [];
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;
@@ -36,60 +39,73 @@ export const PurseView: FC<{}> = props => {
return a - b;
});
let count = 0;
return types;
}, [ displayedCurrencies, purse ]);
while (count < offset) {
types.shift();
count++;
const primaryCurrencies = currencyTypes.slice(0, 2);
const seasonalCurrencies = currencyTypes.slice(2);
useEffect(() =>
{
if(isOpen)
{
setIsCompact(false);
return;
}
count = 0;
const elements: JSX.Element[] = [];
const timeout = window.setTimeout(() => setIsCompact(true), 220);
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;
}
return () => window.clearTimeout(timeout);
}, [ isOpen ]);
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"/>
<Column alignItems="end" className="nitro-purse-container" gap={ 0 }>
<div className={ `nitro-purse-shell ${ isCompact ? 'is-closed' : '' }` }>
<div className={ `nitro-purse ${ isCompact ? 'is-closed' : '' }` }>
<div className={ `nitro-purse__header ${ isCompact ? 'is-closed' : '' }` } onClick={ () => setIsOpen(value => !value) }>
<Flex alignItems="center" gap={ 1 } className={ isCompact ? 'nitro-purse__header-main is-closed' : 'nitro-purse__header-main' }>
<div className="nitro-purse__header-icon">
<img src={ purseIcon } alt="" className="nitro-purse__header-image" />
</div>
</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>
<div className={ `nitro-purse__header-toggle ${ isOpen ? 'is-open' : '' }` }>
<FaChevronDown className="fa-icon text-[10px]" />
</div>
</div>
<div className={ `nitro-purse__content ${ isOpen ? 'is-open' : 'is-closed' }` }>
<div className={ `nitro-purse__summary nitro-purse__summary--compact ${ hcDisabled ? 'is-no-hc' : '' }` }>
<div className="nitro-purse__primary">
<CurrencyView type={ -1 } amount={ purse.credits } short={ currencyDisplayNumberShort } />
{ primaryCurrencies.map(type => <CurrencyView key={ type } type={ type } amount={ purse.activityPoints.get(type) || 0 } short={ currencyDisplayNumberShort } />) }
</div>
{ !hcDisabled &&
<div className="nitro-purse-subscription" onClick={ event => { event.stopPropagation(); CreateLinkEvent('habboUI/open/hccenter'); } }>
<div className="nitro-purse-subscription__icon">
<LayoutCurrencyIcon type="hc" />
</div>
<div className="nitro-purse-subscription__copy">
<Text variant="white" className="nitro-purse-subscription__label">HC</Text>
<Text variant="white" className="nitro-purse-subscription__value">{ getClubText }</Text>
</div>
</div> }
<div className="nitro-purse__actions">
<button type="button" className="nitro-purse__action-button nitro-purse__action-button--help" onClick={ event => { event.stopPropagation(); CreateLinkEvent('help/show'); } } title={ LocalizeText('help.button.name') }>
<FaQuestionCircle />
</button>
<button type="button" className="nitro-purse__action-button nitro-purse__action-button--settings" onClick={ event => { event.stopPropagation(); CreateLinkEvent('user-settings/toggle'); } } title={ LocalizeText('widget.memenu.settings.title') }>
<i className="nitro-icon icon-cog" />
</button>
</div>
</div>
{ seasonalCurrencies.length > 0 &&
<div className="nitro-purse__seasonal">
{ seasonalCurrencies.map(type => <SeasonalView key={ type } type={ type } amount={ purse.activityPoints.get(type) || 0 } />) }
</div> }
</div>
</div>
</div>
</Column>
);
}
};
+10 -9
View File
@@ -1,6 +1,6 @@
import { FC } from 'react';
import { GetConfigurationValue, LocalizeFormattedNumber, LocalizeText } from '../../../api';
import { Flex, LayoutCurrencyIcon, Text } from '../../../common';
import { Flex, Text } from '../../../common';
interface SeasonalViewProps {
type: number;
@@ -10,6 +10,8 @@ interface SeasonalViewProps {
export const SeasonalView: FC<SeasonalViewProps> = props => {
const { type = -1, amount = -1 } = props;
const seasonalColor = GetConfigurationValue<string>('currency.seasonal.color', 'blue');
const formattedAmount = LocalizeFormattedNumber(amount);
const iconUrl = GetConfigurationValue<string>('currency.asset.icon.url', '').replace('%type%', type.toString());
return (
<Flex
@@ -17,22 +19,21 @@ export const SeasonalView: FC<SeasonalViewProps> = props => {
justifyContent="between"
className={`nitro-purse-seasonal-currency nitro-notification ${seasonalColor}`}
>
<Flex fullWidth>
<Flex fullWidth className="seasonal-row">
<Flex className="nitro-seasonal-box seasonal-image-padding">
<img src={ iconUrl } alt="" className="seasonal-image" />
</Flex>
<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) : ''}
title={formattedAmount}
>
{amount > 99999 ? '99 999' : LocalizeFormattedNumber(amount)}
{formattedAmount}
</Text>
<Flex className="nitro-seasonal-box seasonal-image-padding">
<LayoutCurrencyIcon type={type} />
</Flex>
</Flex>
</Flex>
);
};
};
+2 -2
View File
@@ -10,8 +10,8 @@ import { RoomPromotesWidgetView } from '../room/widgets/room-promotes/RoomPromot
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">
<div className="absolute top-0 right-[8px] z-10 w-[min(188px,calc(100vw-16px))] sm:right-[10px] sm:w-[min(188px,calc(100vw-20px))] h-[calc(100%-55px)] pointer-events-none">
<Column gap={ 1 } position="relative" className="w-full">
<PurseView />
<GroupRoomInformationView />
<MysteryBoxExtensionView />
@@ -14,7 +14,7 @@ export const ChatInputView: FC<{}> = props =>
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 inputRef = useRef<HTMLInputElement>(null);
const { isVisible: commandSelectorVisible, filteredCommands, selectedIndex, setSelectedIndex, moveUp, moveDown, selectCurrent, close: closeCommandSelector } = useChatCommandSelector(chatValue);
const chatModeIdWhisper = useMemo(() => LocalizeText('widgets.chatinput.mode.whisper'), []);
@@ -279,7 +279,7 @@ export const ChatInputView: FC<{}> = props =>
return (
createPortal(
<div className="nitro-chat-input-container flex justify-between items-center relative h-10 border-2 border-black bg-gray-200 pr-2.5 w-full overflow-visible rounded-lg">
<div className="nitro-chat-input-container relative flex h-[38px] w-full items-center justify-between overflow-visible rounded-[12px] border-2 border-black bg-white pr-[8px]">
{ commandSelectorVisible &&
<ChatInputCommandSelectorView
commands={ filteredCommands }
@@ -289,7 +289,7 @@ export const ChatInputView: FC<{}> = props =>
/> }
<div className="flex-1 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() } /> }
<input ref={ inputRef } className="w-full border-none bg-transparent px-[10px] text-[0.86rem] text-black placeholder:text-[#6c757d] 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>
@@ -6,6 +6,7 @@ import { GetConfigurationValue, LocalizeText, SendMessageComposer, SetLocalStora
import { Text } from '../../../../common';
import { useMessageEvent, useNavigator, useRoom } from '../../../../hooks';
import { getRegisteredPlugins, INitroPlugin, subscribePlugins } from '../../../plugins/NitroPluginApi';
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa';
export const RoomToolsWidgetView: FC<{}> = props => {
const [areBubblesMuted, setAreBubblesMuted] = useState(false);
@@ -15,6 +16,7 @@ export const RoomToolsWidgetView: FC<{}> = props => {
const [roomTags, setRoomTags] = useState<string[]>(null);
const [isOpen, setIsOpen] = useState<boolean>(false);
const [isOpenHistory, setIsOpenHistory] = useState<boolean>(false);
const [isCollapsed, setIsCollapsed] = useState<boolean>(false);
const [roomHistory, setRoomHistory] = useState<{ roomId: number, roomName: string }[]>([]);
const [plugins, setPlugins] = useState<INitroPlugin[]>([]);
const { navigatorData = null } = useNavigator();
@@ -79,10 +81,11 @@ export const RoomToolsWidgetView: FC<{}> = props => {
const onChangeRoomHistory = (roomId: number, roomName: string) => {
let newStorage = JSON.parse(window.localStorage.getItem('nitro.room.history') || '[]');
if (newStorage.some((room: { roomId: number }) => room.roomId === roomId)) return;
if (newStorage.length >= 10) newStorage.shift();
newStorage = [...newStorage, { roomId, roomName }];
newStorage = newStorage.filter((room: { roomId: number }) => room.roomId !== roomId);
newStorage = [ ...newStorage, { roomId, roomName } ];
if (newStorage.length > 10) newStorage = newStorage.slice(-10);
setRoomHistory(newStorage);
SetLocalStorage('nitro.room.history', newStorage);
@@ -99,48 +102,55 @@ export const RoomToolsWidgetView: FC<{}> = props => {
});
useEffect(() => {
setIsOpen(true);
const timeout = setTimeout(() => setIsOpen(false), 5000);
return () => clearTimeout(timeout);
if(roomName || roomOwner || (roomTags && roomTags.length)) setIsOpen(true);
}, [roomName, roomOwner, roomTags]);
useEffect(() => {
if(!isCollapsed && (roomName || roomOwner || (roomTags && roomTags.length))) setIsOpen(true);
}, [ isCollapsed, roomName, roomOwner, roomTags ]);
useEffect(() => {
setRoomHistory(JSON.parse(window.localStorage.getItem('nitro.room.history') || '[]'));
}, []);
useEffect(() => {
const handleTabClose = () => {
window.localStorage.removeItem('nitro.room.history');
};
window.addEventListener('beforeunload', handleTabClose);
return () => window.removeEventListener('beforeunload', handleTabClose);
}, []);
return (
<div className="flex space-x-2 nitro-room-tools-container">
<div className="flex flex-col items-center justify-center p-2 nitro-room-tools">
<div className="cursor-pointer nitro-icon icon-cog" title={LocalizeText('room.settings.button.text')} onClick={() => handleToolClick('settings')} />
<div className={classNames('cursor-pointer', 'nitro-icon', (!isZoomedIn && 'icon-zoom-less'), (isZoomedIn && 'icon-zoom-more'))} title={LocalizeText('room.zoom.button.text')} onClick={() => handleToolClick('zoom')} />
<div className="cursor-pointer nitro-icon icon-chat-history" title={LocalizeText('room.chathistory.button.text')} onClick={() => handleToolClick('chat_history')} />
<div className={classNames('cursor-pointer', 'nitro-icon', (areBubblesMuted ? 'icon-chat-disablebubble' : 'icon-chat-enablebubble'))} title={areBubblesMuted ? LocalizeText('room.unmute.button.text') : LocalizeText('room.mute.button.text')} onClick={() => handleToolClick('hiddenbubbles')} />
<div className="nitro-room-tools-container">
<div className="nitro-room-tools-rail">
<button type="button" className={ `nitro-room-tools-toggle ${ isCollapsed ? 'is-collapsed' : 'is-open' }` } onClick={ () => setIsCollapsed(value => !value) } title={ isCollapsed ? 'Apri strumenti stanza' : 'Chiudi strumenti stanza' }>
{ isCollapsed ? <FaChevronRight className="text-[11px]" /> : <FaChevronLeft className="text-[11px]" /> }
</button>
<AnimatePresence initial={ false }>
{ !isCollapsed &&
<motion.div
initial={ { opacity: 0, x: -10, scale: 0.96 } }
animate={ { opacity: 1, x: 0, scale: 1 } }
exit={ { opacity: 0, x: -10, scale: 0.96 } }
transition={ { duration: 0.26, ease: [ 0.16, 1, 0.3, 1 ] } }
className="flex flex-col items-center justify-center p-2 nitro-room-tools">
<div className="cursor-pointer nitro-icon icon-cog" title={LocalizeText('room.settings.button.text')} onClick={() => handleToolClick('settings')} />
<div className={classNames('cursor-pointer', 'nitro-icon', (!isZoomedIn && 'icon-zoom-less'), (isZoomedIn && 'icon-zoom-more'))} title={LocalizeText('room.zoom.button.text')} onClick={() => handleToolClick('zoom')} />
<div className="cursor-pointer nitro-icon icon-chat-history" title={LocalizeText('room.chathistory.button.text')} onClick={() => handleToolClick('chat_history')} />
<div className={classNames('cursor-pointer', 'nitro-icon', (areBubblesMuted ? 'icon-chat-disablebubble' : 'icon-chat-enablebubble'))} title={areBubblesMuted ? LocalizeText('room.unmute.button.text') : LocalizeText('room.mute.button.text')} onClick={() => handleToolClick('hiddenbubbles')} />
{navigatorData.canRate && (
<div className="cursor-pointer nitro-icon icon-like-room" title={LocalizeText('room.like.button.text')} onClick={() => handleToolClick('like_room')} />
)}
<div className="cursor-pointer nitro-icon icon-room-link" title={LocalizeText('navigator.embed.caption')} onClick={() => handleToolClick('toggle_room_link')} />
<div className="cursor-pointer nitro-icon icon-room-history-enabled" title={LocalizeText('room.history.button.tooltip')} onClick={() => handleToolClick('room_history')} />
{plugins.map(plugin => (
<div
key={plugin.name}
className={`cursor-pointer nitro-icon ${plugin.icon || 'icon-cog'}`}
title={plugin.label}
onClick={() => plugin.onOpen()}
/>
))}
{navigatorData.canRate && (
<div className="cursor-pointer nitro-icon icon-like-room" title={LocalizeText('room.like.button.text')} onClick={() => handleToolClick('like_room')} />
)}
<div className="cursor-pointer nitro-icon icon-room-link" title={LocalizeText('navigator.embed.caption')} onClick={() => handleToolClick('toggle_room_link')} />
<div className="cursor-pointer nitro-icon icon-room-history-enabled" title={LocalizeText('room.history.button.tooltip')} onClick={() => handleToolClick('room_history')} />
{plugins.map(plugin => (
<div
key={plugin.name}
className={`cursor-pointer nitro-icon ${plugin.icon || 'icon-cog'}`}
title={plugin.label}
onClick={() => plugin.onOpen()}
/>
))}
</motion.div> }
</AnimatePresence>
</div>
<div className="flex flex-col justify-center">
<div className="flex flex-col justify-center nitro-room-tools-side-container">
<AnimatePresence>
{isOpen && (
{(!isCollapsed && (isOpen || !!roomName || !!roomOwner || !!(roomTags && roomTags.length))) && (
<motion.div initial={{ x: -100 }} animate={{ x: 0 }} exit={{ x: -100 }} transition={{ duration: 0.3 }}>
<div className="flex flex-col items-center justify-center">
<div className="flex flex-col px-3 py-2 rounded nitro-room-tools-info">
@@ -161,7 +171,7 @@ export const RoomToolsWidgetView: FC<{}> = props => {
</div>
</motion.div>
)}
{isOpenHistory && (
{(!isCollapsed && isOpenHistory) && (
<motion.div initial={{ x: -100 }} animate={{ x: 0 }} exit={{ x: -100 }} transition={{ duration: 0.3 }} className="nitro-room-tools-history">
<div className="flex flex-col px-3 py-2 rounded nitro-room-history">
{roomHistory.map(history => (
+1 -1
View File
@@ -11,7 +11,7 @@ export const ToolbarItemView = forwardRef<HTMLDivElement, PropsWithChildren<{
<div
ref={ ref }
className={ classNames(
'cursor-pointer relative',
'relative h-[32px] w-[32px] shrink-0 cursor-pointer bg-center bg-no-repeat transition-transform duration-200 ease-out hover:-translate-y-[1px] active:translate-y-0',
`nitro-icon icon-${ icon }`,
className
) }
+20 -26
View File
@@ -1,8 +1,9 @@
import { CreateLinkEvent, GetRoomEngine, GetSessionDataManager, MouseEventType, RoomObjectCategory } from '@nitrots/nitro-renderer';
import { CreateLinkEvent, GetRoomEngine, GetSessionDataManager, RoomObjectCategory } from '@nitrots/nitro-renderer';
import { Dispatch, FC, PropsWithChildren, SetStateAction, useEffect, useRef } from 'react';
import { DispatchUiEvent, GetConfigurationValue, GetRoomSession, GetUserProfile, LocalizeText } from '../../api';
import { Flex, LayoutItemCountView } from '../../common';
import { LayoutItemCountView } from '../../common';
import { GuideToolEvent } from '../../events';
import { ToolbarItemView } from './ToolbarItemView';
export const ToolbarMeView: FC<PropsWithChildren<{
useGuideTool: boolean;
@@ -10,8 +11,8 @@ export const ToolbarMeView: FC<PropsWithChildren<{
setMeExpanded: Dispatch<SetStateAction<boolean>>;
}>> = props =>
{
const { useGuideTool = false, unseenAchievementCount = 0, setMeExpanded = null, children = null, ...rest } = props;
const elementRef = useRef<HTMLDivElement>();
const { useGuideTool = false, unseenAchievementCount = 0, children = null } = props;
const elementRef = useRef<HTMLDivElement>(null);
useEffect(() =>
{
@@ -22,29 +23,22 @@ export const ToolbarMeView: FC<PropsWithChildren<{
GetRoomEngine().selectRoomObject(roomSession.roomId, roomSession.ownRoomIndex, RoomObjectCategory.UNIT);
}, []);
useEffect(() =>
{
const onClick = (event: MouseEvent) => setMeExpanded(false);
document.addEventListener('click', onClick);
return () => document.removeEventListener(MouseEventType.MOUSE_CLICK, onClick);
}, [ setMeExpanded ]);
return (
<Flex alignItems="center" className="absolute bottom-[60px] left-[33px] bg-[rgba(20,20,20,.95)] border border-[solid] border-[#101010] [box-shadow:inset_2px_2px_rgba(255,255,255,.1),inset_-2px_-2px_rgba(255,255,255,.1)] rounded-[$border-radius] p-2" gap={ 2 } innerRef={ elementRef }>
{ (GetConfigurationValue('guides.enabled') && useGuideTool) &&
<div className="navigation-item relative nitro-icon icon-me-helper-tool cursor-pointer" onClick={ event => DispatchUiEvent(new GuideToolEvent(GuideToolEvent.TOGGLE_GUIDE_TOOL)) } /> }
<div className="navigation-item relative nitro-icon icon-me-achievements cursor-pointer" onClick={ event => CreateLinkEvent('achievements/toggle') }>
{ (unseenAchievementCount > 0) &&
<LayoutItemCountView count={ unseenAchievementCount } /> }
<div className="w-fit max-w-[min(calc(100vw-16px),520px)] rounded-[12px] border border-white/8 bg-[rgba(10,10,12,0.58)] px-[10px] py-[7px] shadow-[0_10px_24px_rgba(0,0,0,0.2)]" ref={ elementRef }>
<div className="flex items-center gap-[8px] overflow-x-auto overflow-y-visible whitespace-nowrap">
{ (GetConfigurationValue('guides.enabled') && useGuideTool) &&
<ToolbarItemView icon="me-helper-tool" onClick={ event => DispatchUiEvent(new GuideToolEvent(GuideToolEvent.TOGGLE_GUIDE_TOOL)) } title={ LocalizeText('guide.help.button.label') } /> }
<ToolbarItemView icon="me-achievements" onClick={ event => CreateLinkEvent('achievements/toggle') } title={ LocalizeText('toolbar.icon.label.achievements') }>
{ (unseenAchievementCount > 0) &&
<LayoutItemCountView count={ unseenAchievementCount } /> }
</ToolbarItemView>
<ToolbarItemView icon="me-profile" onClick={ event => GetUserProfile(GetSessionDataManager().userId) } title={ LocalizeText('toolbar.icon.label.memenu') } />
<ToolbarItemView icon="me-rooms" onClick={ event => CreateLinkEvent('navigator/search/myworld_view') } title={ LocalizeText('navigator.myworlds') } />
<ToolbarItemView icon="me-clothing" onClick={ event => CreateLinkEvent('avatar-editor/toggle') } title={ LocalizeText('widget.memenu.settings.avatar') } />
<ToolbarItemView icon="me-settings" onClick={ event => CreateLinkEvent('user-settings/toggle') } title={ LocalizeText('widget.memenu.settings.title') } />
<ToolbarItemView icon="me-forums" onClick={ event => CreateLinkEvent('groupforum/toggle') } title={ LocalizeText('toolbar.icon.label.forums') } />
{ children }
</div>
<div className="navigation-item relative nitro-icon icon-me-profile cursor-pointer" onClick={ event => GetUserProfile(GetSessionDataManager().userId) } />
<div className="navigation-item relative nitro-icon icon-me-rooms cursor-pointer" onClick={ event => CreateLinkEvent('navigator/search/myworld_view') } />
<div className="navigation-item relative nitro-icon icon-me-clothing cursor-pointer" onClick={ event => CreateLinkEvent('avatar-editor/toggle') } />
<div className="navigation-item relative nitro-icon icon-me-settings cursor-pointer" onClick={ event => CreateLinkEvent('user-settings/toggle') } />
<div className="navigation-item relative nitro-icon icon-me-forums cursor-pointer" onClick={ event => CreateLinkEvent('groupforum/toggle') } title={ LocalizeText('toolbar.icon.label.forums') } />
{ children }
</Flex>
</div>
);
};
+275 -51
View File
@@ -7,10 +7,23 @@ import { useAchievements, useFriends, useInventoryUnseenTracker, useMessageEvent
import { ToolbarItemView } from './ToolbarItemView';
import { ToolbarMeView } from './ToolbarMeView';
const containerVariants = {
hidden: {},
visible: { transition: { staggerChildren: 0.05 } },
exit: { transition: { staggerChildren: 0.03, staggerDirection: -1 as const } }
};
const itemVariants = {
hidden: { opacity: 0, y: 10, scale: 0.8 },
visible: { opacity: 1, y: 0, scale: 1, transition: { type: 'spring', stiffness: 400, damping: 22 } },
exit: { opacity: 0, y: 6, scale: 0.85, transition: { duration: 0.1 } }
};
export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
{
const { isInRoom } = props;
const [ isMeExpanded, setMeExpanded ] = useState(false);
const [ isToolbarOpen, setIsToolbarOpen ] = useState(false);
const [ useGuideTool, setUseGuideTool ] = useState(false);
const { userFigure = null } = useSessionInfo();
const { getFullCount = 0 } = useInventoryUnseenTracker();
@@ -19,6 +32,8 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
const { iconState = MessengerIconState.HIDDEN } = useMessenger();
const { openMonitor, showToolbarButton } = useWiredTools();
const isMod = GetSessionDataManager().isModerator;
const hasDesktopUnifiedShell = (isInRoom && isToolbarOpen);
const showDesktopShell = (isToolbarOpen || !isInRoom);
useMessageEvent<PerkAllowancesMessageEvent>(PerkAllowancesMessageEvent, event =>
{
@@ -42,13 +57,11 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
const targetBounds = target.getBoundingClientRect();
const imageBounds = image.getBoundingClientRect();
const left = (imageBounds.x - targetBounds.x);
const top = (imageBounds.y - targetBounds.y);
const squared = Math.sqrt(((left * left) + (top * top)));
const wait = (500 - Math.abs(((((1 / squared) * 100) * 500) * 0.5)));
const height = 20;
const motionName = (`ToolbarBouncing[${ iconName }]`);
if(!Motions.getMotionByTag(motionName))
@@ -66,55 +79,266 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
return (
<>
<AnimatePresence> { isMeExpanded && ( <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.3 }}>
<ToolbarMeView setMeExpanded={ setMeExpanded } unseenAchievementCount={ getTotalUnseen } useGuideTool={ useGuideTool } />
</motion.div> )}
</AnimatePresence>
<Flex alignItems="center" className="absolute bottom-0 left-0 w-full h-[55px] bg-[rgba(28,28,32,.95)] [box-shadow:inset_0_5px_#22222799,inset_0_-4px_#12121599] py-1 px-3" gap={ 2 }>
<Flex alignItems="center" gap={ 2 } className="flex-shrink-0">
<Flex center pointer className={ 'relative w-[50px] h-[45px] overflow-hidden ' + (isMeExpanded ? 'active ' : '') } onClick={ event =>
{
setMeExpanded(!isMeExpanded);
event.stopPropagation();
} }>
<LayoutAvatarImageView className="-ml-[5px] mt-[25px]" direction={ 2 } figure={ userFigure } position="absolute" />
{ (getTotalUnseen > 0) &&
<LayoutItemCountView count={ getTotalUnseen } /> }
</Flex>
{ isInRoom &&
<ToolbarItemView icon="habbo" onClick={ event => VisitDesktop() } /> }
{ !isInRoom &&
<ToolbarItemView icon="house" onClick={ event => CreateLinkEvent('navigator/goto/home') } /> }
<ToolbarItemView icon="rooms" onClick={ event => CreateLinkEvent('navigator/toggle') } />
{ GetConfigurationValue('game.center.enabled') &&
<ToolbarItemView icon="game" onClick={ event => CreateLinkEvent('games/toggle') } /> }
<ToolbarItemView icon="catalog" onClick={ event => CreateLinkEvent('catalog/toggle') } />
<ToolbarItemView icon="inventory" onClick={ event => CreateLinkEvent('inventory/toggle') }>
{ (getFullCount > 0) &&
<LayoutItemCountView count={ getFullCount } /> }
</ToolbarItemView>
{ (isInRoom && showToolbarButton) &&
<ToolbarItemView icon="wired-tools" onClick={ openMonitor } /> }
{ isInRoom &&
<ToolbarItemView icon="camera" onClick={ event => CreateLinkEvent('camera/toggle') } /> }
{ isMod &&
<ToolbarItemView icon="modtools" onClick={ event => CreateLinkEvent('mod-tools/toggle') } /> }
{ isMod &&
<ToolbarItemView icon="furnieditor" onClick={ event => CreateLinkEvent('furni-editor/toggle') } /> }
</Flex>
<Flex alignItems="center" justifyContent="center" className="flex-1 min-w-0 max-w-[600px] mx-auto" id="toolbar-chat-input-container" />
<Flex alignItems="center" gap={ 2 } className="flex-shrink-0">
<Flex gap={ 2 }>
<ToolbarItemView icon="friendall" onClick={ event => CreateLinkEvent('friends/toggle') }>
{ (requests.length > 0) &&
<LayoutItemCountView count={ requests.length } /> }
</ToolbarItemView>
{ ((iconState === MessengerIconState.SHOW) || (iconState === MessengerIconState.UNREAD)) &&
<ToolbarItemView className={ (iconState === MessengerIconState.UNREAD) && 'is-unseen' } icon="message" onClick={ event => OpenMessengerChat() } /> }
</Flex>
<div className="hidden lg:block" id="toolbar-friend-bar-container" />
</Flex>
</Flex>
<style>{ TOOLBAR_STYLES }</style>
{ isInRoom &&
<div className={ `fixed bottom-0 left-0 right-0 z-40 flex h-[52px] items-end px-0 pt-[2px] pb-0 pointer-events-none md:left-1/2 md:right-auto md:h-[52px] md:w-[420px] md:-translate-x-1/2 md:items-center md:px-[6px] md:py-[4px] lg:w-[460px] ${ isToolbarOpen ? (hasDesktopUnifiedShell ? 'md:rounded-none md:border-0 md:bg-transparent md:shadow-none rounded-t-[12px] border border-b-0 border-white/8 bg-[rgba(10,10,12,0.58)] shadow-[0_-6px_18px_rgba(0,0,0,0.18)]' : 'rounded-t-[12px] border border-b-0 border-white/8 bg-[rgba(10,10,12,0.58)] shadow-[0_-6px_18px_rgba(0,0,0,0.18)]') : 'border-0 bg-transparent shadow-none md:border-0 md:bg-transparent md:shadow-none' }` }>
<motion.div
className="tb-toggle pointer-events-auto mr-2 mb-[4px] flex-shrink-0 md:mb-0"
onClick={ () => setIsToolbarOpen(value => !value) }
whileTap={ { scale: 0.9 } }>
<svg
className={ `h-3.5 w-3.5 text-white/70 transition-transform duration-300 ${ isToolbarOpen ? 'rotate-180 md:-rotate-90' : 'rotate-0 md:rotate-90' }` }
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={ 2.5 } d="M5 15l7-7 7 7" />
</svg>
</motion.div>
<Flex
alignItems="center"
justifyContent="center"
className="pointer-events-auto h-full w-full min-w-0 flex-1 px-[6px] md:px-0"
id="toolbar-chat-input-container" />
<div className="pointer-events-auto relative mr-[6px] shrink-0 md:hidden">
<ToolbarItemView icon="friendall" onClick={ () => CreateLinkEvent('friends/toggle') } className="tb-icon" />
{ (requests.length > 0) &&
<LayoutItemCountView count={ requests.length } className="absolute -right-1 top-0" /> }
</div>
</div> }
<AnimatePresence>
{ (isToolbarOpen || !isInRoom) &&
<>
{ showDesktopShell &&
<motion.div
key="desktop-unified-shell"
initial={ { opacity: 0, y: 8 } }
animate={ { opacity: 1, y: 0 } }
exit={ { opacity: 0, y: 8 } }
transition={ { type: 'spring', stiffness: 260, damping: 26 } }
className="pointer-events-none fixed bottom-0 left-0 right-0 z-[39] hidden h-[52px] rounded-t-[12px] border border-b-0 border-white/8 bg-[rgba(10,10,12,0.58)] shadow-[0_-6px_18px_rgba(0,0,0,0.18)] md:block" /> }
<motion.div
key="left-nav"
initial={ { opacity: 0, x: isInRoom ? -10 : 0, y: isInRoom ? 0 : 8 } }
animate={ { opacity: 1, x: 0, y: 0 } }
exit={ { opacity: 0, x: isInRoom ? -10 : 0, y: isInRoom ? 0 : 8 } }
transition={ { type: 'spring', stiffness: 300, damping: 28 } }
className="fixed bottom-0 left-0 z-40 hidden h-[52px] max-w-[calc(50vw-242px)] items-center overflow-visible pl-3 pointer-events-auto md:flex">
<motion.div variants={ containerVariants } initial="hidden" animate="visible" exit="exit" className={ `tb-open-shell flex h-[52px] max-w-full items-center gap-2 overflow-visible px-[8px] pt-[10px] pb-[2px] ${ showDesktopShell ? 'bg-transparent' : 'rounded-t-[10px] border border-b-0 border-white/8 bg-[rgba(10,10,12,0.58)] shadow-[0_-6px_18px_rgba(0,0,0,0.18)]' }` }>
<motion.div variants={ itemVariants }>
{ isInRoom
? <ToolbarItemView icon="habbo" onClick={ () => VisitDesktop() } className="tb-icon" />
: <ToolbarItemView icon="house" onClick={ () => CreateLinkEvent('navigator/goto/home') } className="tb-icon" /> }
</motion.div>
<motion.div variants={ itemVariants }>
<ToolbarItemView icon="rooms" onClick={ () => CreateLinkEvent('navigator/toggle') } className="tb-icon" />
</motion.div>
{ GetConfigurationValue('game.center.enabled') &&
<motion.div variants={ itemVariants }>
<ToolbarItemView icon="game" onClick={ () => CreateLinkEvent('games/toggle') } className="tb-icon" />
</motion.div> }
<motion.div variants={ itemVariants }>
<ToolbarItemView icon="catalog" onClick={ () => CreateLinkEvent('catalog/toggle') } className="tb-icon" />
</motion.div>
<motion.div variants={ itemVariants } className="relative">
<ToolbarItemView icon="inventory" onClick={ () => CreateLinkEvent('inventory/toggle') } className="tb-icon" />
{ (getFullCount > 0) &&
<LayoutItemCountView count={ getFullCount } className="absolute -right-1 top-0" /> }
</motion.div>
<motion.div variants={ itemVariants } className="relative">
<AnimatePresence>
{ isMeExpanded &&
<motion.div
initial={ { opacity: 0, y: 6, scale: 0.97 } }
animate={ { opacity: 1, y: 0, scale: 1 } }
exit={ { opacity: 0, y: 6, scale: 0.97 } }
transition={ { type: 'spring', stiffness: 420, damping: 28 } }
className="pointer-events-auto absolute bottom-[calc(100%+8px)] left-1/2 z-50 -translate-x-1/2">
<ToolbarMeView setMeExpanded={ setMeExpanded } unseenAchievementCount={ getTotalUnseen } useGuideTool={ useGuideTool } />
</motion.div> }
</AnimatePresence>
<motion.div whileHover={ { scale: 1.08 } } whileTap={ { scale: 0.95 } } className="cursor-pointer" onClick={ event => { setMeExpanded(value => !value); event.stopPropagation(); } }>
<LayoutAvatarImageView headOnly={ true } direction={ 2 } figure={ userFigure } className="tb-icon !h-[44px] !w-[32px] !bg-center !bg-no-repeat" style={ { marginTop: '4px' } } />
</motion.div>
{ (getTotalUnseen > 0) &&
<LayoutItemCountView count={ getTotalUnseen } className="pointer-events-none absolute -right-1 -top-1 z-10" /> }
</motion.div>
{ (isInRoom && showToolbarButton) &&
<motion.div variants={ itemVariants }>
<ToolbarItemView icon="wired-tools" onClick={ openMonitor } className="tb-icon" />
</motion.div> }
{ isInRoom &&
<motion.div variants={ itemVariants }>
<ToolbarItemView icon="camera" onClick={ () => CreateLinkEvent('camera/toggle') } className="tb-icon" />
</motion.div> }
{ isMod &&
<motion.div variants={ itemVariants }>
<ToolbarItemView icon="modtools" onClick={ () => CreateLinkEvent('mod-tools/toggle') } className="tb-icon" />
</motion.div> }
{ isMod &&
<motion.div variants={ itemVariants }>
<ToolbarItemView icon="furnieditor" onClick={ () => CreateLinkEvent('furni-editor/toggle') } className="tb-icon" />
</motion.div> }
</motion.div>
</motion.div>
<motion.div
key="right-nav"
initial={ { opacity: 0, x: 10 } }
animate={ { opacity: 1, x: 0 } }
exit={ { opacity: 0, x: 10 } }
transition={ { type: 'spring', stiffness: 300, damping: 28 } }
className={ `fixed bottom-0 z-40 hidden h-[52px] max-w-[calc(50vw-242px)] items-center overflow-visible pr-3 pointer-events-auto md:flex ${ isInRoom ? 'right-0' : 'right-3' }` }>
<motion.div variants={ containerVariants } initial="hidden" animate="visible" exit="exit" className="tb-open-shell flex h-[52px] max-w-full items-center gap-3 overflow-visible bg-transparent px-[8px] pt-[10px] pb-[2px]">
<motion.div variants={ itemVariants } className="relative">
<ToolbarItemView icon="friendall" onClick={ () => CreateLinkEvent('friends/toggle') } className="tb-icon" />
{ (requests.length > 0) &&
<LayoutItemCountView count={ requests.length } className="absolute -right-2 -top-1" /> }
</motion.div>
{ ((iconState === MessengerIconState.SHOW) || (iconState === MessengerIconState.UNREAD)) &&
<motion.div variants={ itemVariants }>
<ToolbarItemView className={ `tb-icon ${ iconState === MessengerIconState.UNREAD ? 'is-unseen animate-pulse' : '' }` } icon="message" onClick={ () => OpenMessengerChat() } />
</motion.div> }
<div className="mx-1 hidden h-5 w-[1px] bg-white/20 md:block" />
<div className="hidden h-full shrink-0 md:block" id="toolbar-friend-bar-container-desktop" />
</motion.div>
</motion.div>
<motion.div
key="mobile-nav"
initial={ { opacity: 0, y: 8 } }
animate={ { opacity: 1, y: 0 } }
exit={ { opacity: 0, y: 8 } }
transition={ { type: 'spring', stiffness: 300, damping: 28 } }
className={ `fixed left-1/2 z-40 flex w-[95vw] -translate-x-1/2 items-center overflow-visible pointer-events-auto md:hidden ${ isInRoom ? 'bottom-[52px] rounded-t-[12px] border border-b-0 border-white/8 bg-[rgba(10,10,12,0.58)] px-[6px] py-[4px] shadow-[0_-6px_18px_rgba(0,0,0,0.18)]' : 'bottom-0' }` }>
<motion.div variants={ containerVariants } initial="hidden" animate="visible" exit="exit" className="tb-bar-scroll flex h-full min-w-0 flex-1 items-center gap-2 overflow-x-auto overflow-y-visible px-1">
<motion.div variants={ itemVariants }>
{ isInRoom
? <ToolbarItemView icon="habbo" onClick={ () => VisitDesktop() } className="tb-icon" />
: <ToolbarItemView icon="house" onClick={ () => CreateLinkEvent('navigator/goto/home') } className="tb-icon" /> }
</motion.div>
<motion.div variants={ itemVariants }>
<ToolbarItemView icon="rooms" onClick={ () => CreateLinkEvent('navigator/toggle') } className="tb-icon" />
</motion.div>
{ GetConfigurationValue('game.center.enabled') &&
<motion.div variants={ itemVariants }>
<ToolbarItemView icon="game" onClick={ () => CreateLinkEvent('games/toggle') } className="tb-icon" />
</motion.div> }
<motion.div variants={ itemVariants }>
<ToolbarItemView icon="catalog" onClick={ () => CreateLinkEvent('catalog/toggle') } className="tb-icon" />
</motion.div>
<motion.div variants={ itemVariants } className="relative">
<ToolbarItemView icon="inventory" onClick={ () => CreateLinkEvent('inventory/toggle') } className="tb-icon" />
{ (getFullCount > 0) &&
<LayoutItemCountView count={ getFullCount } className="absolute -right-1 top-0" /> }
</motion.div>
</motion.div>
<motion.div variants={ itemVariants } className="relative mx-[2px] shrink-0">
<AnimatePresence>
{ isMeExpanded &&
<motion.div
initial={ { opacity: 0, y: 6, scale: 0.97 } }
animate={ { opacity: 1, y: 0, scale: 1 } }
exit={ { opacity: 0, y: 6, scale: 0.97 } }
transition={ { type: 'spring', stiffness: 420, damping: 28 } }
className="pointer-events-auto absolute bottom-[calc(100%+10px)] left-1/2 z-[70] -translate-x-1/2">
<ToolbarMeView setMeExpanded={ setMeExpanded } unseenAchievementCount={ getTotalUnseen } useGuideTool={ useGuideTool } />
</motion.div> }
</AnimatePresence>
<motion.div whileHover={ { scale: 1.08 } } whileTap={ { scale: 0.95 } } className="cursor-pointer" onClick={ event => { setMeExpanded(value => !value); event.stopPropagation(); } }>
<LayoutAvatarImageView headOnly={ true } direction={ 2 } figure={ userFigure } className="tb-icon !h-[44px] !w-[32px] !bg-center !bg-no-repeat" style={ { marginTop: '4px' } } />
</motion.div>
{ (getTotalUnseen > 0) &&
<LayoutItemCountView count={ getTotalUnseen } className="pointer-events-none absolute -right-1 -top-1 z-10" /> }
</motion.div>
<motion.div variants={ containerVariants } initial="hidden" animate="visible" exit="exit" className="tb-bar-scroll flex h-full items-center gap-2 overflow-x-auto overflow-y-visible px-1">
{ (isInRoom && showToolbarButton) &&
<motion.div variants={ itemVariants }>
<ToolbarItemView icon="wired-tools" onClick={ openMonitor } className="tb-icon" />
</motion.div> }
{ isInRoom &&
<motion.div variants={ itemVariants }>
<ToolbarItemView icon="camera" onClick={ () => CreateLinkEvent('camera/toggle') } className="tb-icon" />
</motion.div> }
{ isMod &&
<motion.div variants={ itemVariants }>
<ToolbarItemView icon="modtools" onClick={ () => CreateLinkEvent('mod-tools/toggle') } className="tb-icon" />
</motion.div> }
{ isMod &&
<motion.div variants={ itemVariants }>
<ToolbarItemView icon="furnieditor" onClick={ () => CreateLinkEvent('furni-editor/toggle') } className="tb-icon" />
</motion.div> }
{ !isInRoom &&
<motion.div variants={ itemVariants } className="relative">
<ToolbarItemView icon="friendall" onClick={ () => CreateLinkEvent('friends/toggle') } className="tb-icon" />
{ (requests.length > 0) &&
<LayoutItemCountView count={ requests.length } className="absolute -right-2 -top-1" /> }
</motion.div> }
</motion.div>
</motion.div>
</> }
</AnimatePresence>
</>
);
};
const TOOLBAR_STYLES = `
.tb-icon {
opacity: 1;
transition: transform 0.15s ease;
cursor: pointer;
}
.tb-icon:hover {
transform: translateY(-2px);
}
.tb-icon:active {
transform: translateY(0);
}
.tb-toggle {
width: 32px;
height: 32px;
flex-shrink: 0;
border-radius: 9px;
background: rgba(18, 16, 14, 0.80);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.08);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.5);
transition: background 0.15s, border-color 0.15s;
}
.tb-toggle:hover {
background: rgba(30, 26, 20, 0.88);
border-color: rgba(255, 255, 255, 0.13);
}
.tb-bar-scroll {
overflow-x: auto;
overflow-y: visible;
scrollbar-width: none;
-ms-overflow-style: none;
flex-wrap: nowrap;
}
.tb-bar-scroll::-webkit-scrollbar {
display: none;
}
.tb-open-shell {
scrollbar-width: none;
-ms-overflow-style: none;
}
.tb-open-shell::-webkit-scrollbar {
display: none;
}
`;
@@ -14,14 +14,14 @@ export const BadgeInfoView: FC<BadgeInfoViewProps> = props =>
return (
<Flex center
className="w-[45px] h-[45px] rounded bg-white/50 relative cursor-pointer"
className="nitro-card-panel w-[45px] h-[45px] relative cursor-pointer"
onMouseEnter={ () => setIsHovered(true) }
onMouseLeave={ () => setIsHovered(false) }
>
<LayoutBadgeImageView badgeCode={ badgeCode } />
{ isHovered && (
<div className="absolute top-full left-1/2 -translate-x-1/2 mt-1 z-50 bg-white text-black rounded shadow-lg py-1 px-2 text-xs w-[180px] pointer-events-none">
<div className="absolute -top-1 left-1/2 -translate-x-1/2 w-2 h-2 bg-white rotate-45" />
<div className="absolute top-full left-1/2 z-50 mt-1 w-[180px] -translate-x-1/2 border border-[#c4cabf] bg-[#f2f2eb] px-2 py-1 text-xs text-black shadow-none pointer-events-none rounded-[6px]">
<div className="absolute -top-1 left-1/2 h-2 w-2 -translate-x-1/2 rotate-45 border-l border-t border-[#c4cabf] bg-[#f2f2eb]" />
<div className="font-bold mb-0.5">{ LocalizeBadgeName(badgeCode) }</div>
<div className="text-gray-600">{ LocalizeBadgeDescription(badgeCode) }</div>
</div>
@@ -28,7 +28,7 @@ export const RelationshipsContainerView: FC<RelationshipsContainerViewProps> = p
<i className={ `nitro-friends-spritesheet icon-${ relationshipName }` } />
</Flex>
<div className="flex flex-col grow gap-0">
<div className="flex items-center justify-between bg-white rounded px-2 py-1 h-[25px]">
<div className="nitro-card-row flex items-center justify-between px-2 py-1 h-[25px]">
<p className="text-sm underline pointer" onClick={ event => (relationshipInfo && (relationshipInfo.randomFriendId >= 1) && GetUserProfile(relationshipInfo.randomFriendId)) }>
{ (!relationshipInfo || (relationshipInfo.friendCount === 0)) &&
LocalizeText('extendedprofile.add.friends') }
@@ -159,7 +159,7 @@ export const UserProfileView: FC<{}> = props =>
</NitroCard.Tabs>
<div className="flex-1 overflow-auto p-2">
{ activeTab === 'badge' && (
<div className="flex flex-wrap content-start gap-2 p-2 rounded bg-muted h-full">
<div className="nitro-card-panel flex flex-wrap content-start gap-2 p-2 h-full">
{ userBadges && (userBadges.length > 0)
? userBadges.map((badge, index) => (
<BadgeInfoView key={ badge + index } badgeCode={ badge } />
@@ -196,7 +196,7 @@ export const UserProfileView: FC<{}> = props =>
</Flex>
) }
{ userRooms && userRooms.length > 0 && userRooms.map(room => (
<Flex key={ room.roomId } alignItems="center" gap={ 2 } className="px-2 py-1.5 rounded bg-white/50 cursor-pointer hover:bg-white/80" onClick={ () => CreateRoomSession(room.roomId) }>
<Flex key={ room.roomId } alignItems="center" gap={ 2 } className="nitro-card-row px-2 py-1.5 cursor-pointer" onClick={ () => CreateRoomSession(room.roomId) }>
<div className="flex flex-col min-w-0 grow">
<Text bold small truncate>{ room.roomName }</Text>
{ room.description && <Text small truncate variant="muted">{ room.description }</Text> }
@@ -1,4 +1,4 @@
import { AddLinkEventTracker, AvatarExpressionEnum, FigureUpdateEvent, FurnitureFloorUpdateEvent, FurnitureMultiStateComposer, FurnitureWallMultiStateComposer, FurnitureWallUpdateComposer, FurnitureWallUpdateEvent, GetLocalizationManager, GetRoomEngine, GetSessionDataManager, ILinkEventTracker, PetMoveComposer, RemoveLinkEventTracker, RoomControllerLevel, RoomObjectCategory, RoomObjectType, RoomObjectVariable, RoomUnitDanceEvent, RoomUnitEffectEvent, RoomUnitExpressionEvent, RoomUnitHandItemEvent, RoomUnitInfoEvent, RoomUnitLookComposer, RoomUnitStatusEvent, RoomUnitWalkComposer, UpdateFurniturePositionComposer, Vector3d } from '@nitrots/nitro-renderer';
import { AddLinkEventTracker, AvatarExpressionEnum, FigureUpdateEvent, FurnitureFloorUpdateEvent, FurnitureMultiStateComposer, FurnitureWallMultiStateComposer, FurnitureWallUpdateComposer, FurnitureWallUpdateEvent, GetLocalizationManager, GetRoomEngine, GetSessionDataManager, ILinkEventTracker, RemoveLinkEventTracker, RoomControllerLevel, RoomObjectCategory, RoomObjectType, RoomObjectVariable, RoomUnitDanceEvent, RoomUnitEffectEvent, RoomUnitExpressionEvent, RoomUnitHandItemEvent, RoomUnitInfoEvent, RoomUnitStatusEvent, UpdateFurniturePositionComposer, Vector3d, WiredUserInspectMoveComposer } from '@nitrots/nitro-renderer';
import { WiredMonitorDataEvent, WiredMonitorRequestComposer } from '@nitrots/nitro-renderer';
import { FC, KeyboardEvent, useCallback, useEffect, useMemo, useState } from 'react';
import furniInspectionIcon from '../../assets/images/wiredtools/furni.png';
@@ -433,16 +433,6 @@ const VARIABLE_DEFINITIONS: Record<VariablesElementType, VariableDefinition[]> =
createVariableDefinition('@chat_style', 'Context', 'Conditional')
]
};
const USER_DIRECTION_VECTORS: Array<{ x: number; y: number; }> = [
{ x: 0, y: -1 },
{ x: 1, y: -1 },
{ x: 1, y: 0 },
{ x: 1, y: 1 },
{ x: 0, y: 1 },
{ x: -1, y: 1 },
{ x: -1, y: 0 },
{ x: -1, y: -1 }
];
const WIRED_FREEZE_EFFECT_IDS: Set<number> = new Set([ 218, 12, 11, 53, 163 ]);
const TEAM_COLOR_NAMES: Record<number, string> = {
1: 'red',
@@ -641,6 +631,7 @@ export const WiredCreatorToolsView: FC<{}> = () =>
const [ isManagedGiveOpen, setIsManagedGiveOpen ] = useState(false);
const [ managedGiveVariableItemId, setManagedGiveVariableItemId ] = useState(0);
const [ managedGiveValue, setManagedGiveValue ] = useState('0');
const shouldPauseVariableSnapshotRefresh = (!!editingVariable || !!editingManagedHolderVariableId || isInspectionGiveOpen || isManagedGiveOpen);
const [ selectedVariableKeys, setSelectedVariableKeys ] = useState<Record<VariablesElementType, string>>({
furni: VARIABLE_DEFINITIONS.furni[0].key,
user: VARIABLE_DEFINITIONS.user[0].key,
@@ -1296,14 +1287,14 @@ export const WiredCreatorToolsView: FC<{}> = () =>
useEffect(() =>
{
if(!isVisible || !roomSession?.roomId || !roomSettings.canInspect) return;
if(!isVisible || !roomSession?.roomId || !roomSettings.canInspect || shouldPauseVariableSnapshotRefresh) return;
requestUserVariables();
const interval = window.setInterval(requestUserVariables, WIRED_VARIABLES_POLL_MS);
return () => window.clearInterval(interval);
}, [ isVisible, roomSession?.roomId, roomSettings.canInspect, requestUserVariables ]);
}, [ isVisible, roomSession?.roomId, roomSettings.canInspect, requestUserVariables, shouldPauseVariableSnapshotRefresh ]);
useEffect(() =>
{
@@ -2789,9 +2780,8 @@ export const WiredCreatorToolsView: FC<{}> = () =>
break;
}
requestUserVariables();
setEditingManagedHolderVariableId(0);
}, [ selectedManagedVariableEntry, selectedManagedHolderVariableEntry, roomSettings.canModify, editingManagedHolderValue, variablesType, updateUserVariableValue, updateFurniVariableValue, updateRoomVariableValue, requestUserVariables ]);
}, [ selectedManagedVariableEntry, selectedManagedHolderVariableEntry, roomSettings.canModify, editingManagedHolderValue, variablesType, updateUserVariableValue, updateFurniVariableValue, updateRoomVariableValue ]);
const giveManagedHolderVariable = useCallback(() =>
{
if(!selectedManagedVariableEntry || !selectedManagedGiveDefinition || !roomSettings.canModify) return;
@@ -2803,11 +2793,10 @@ export const WiredCreatorToolsView: FC<{}> = () =>
if(variablesType === 'user') assignUserVariable(selectedManagedVariableEntry.entityId, selectedManagedGiveDefinition.itemId, nextValue);
else assignFurniVariable(selectedManagedVariableEntry.entityId, selectedManagedGiveDefinition.itemId, nextValue);
requestUserVariables();
setSelectedManagedHolderVariableId(selectedManagedGiveDefinition.itemId);
setManagedGiveValue('0');
setIsManagedGiveOpen(false);
}, [ selectedManagedVariableEntry, selectedManagedGiveDefinition, roomSettings.canModify, variablesType, managedGiveValue, assignUserVariable, assignFurniVariable, requestUserVariables ]);
}, [ selectedManagedVariableEntry, selectedManagedGiveDefinition, roomSettings.canModify, variablesType, managedGiveValue, assignUserVariable, assignFurniVariable ]);
const removeManagedHolderVariable = useCallback(() =>
{
if(!selectedManagedVariableEntry || !selectedManagedHolderVariableEntry || !roomSettings.canModify || selectedManagedHolderVariableEntry.isReadOnly) return;
@@ -2816,9 +2805,8 @@ export const WiredCreatorToolsView: FC<{}> = () =>
if(variablesType === 'user') removeUserVariable(selectedManagedVariableEntry.entityId, selectedManagedHolderVariableEntry.variableItemId);
else removeFurniVariable(selectedManagedVariableEntry.entityId, selectedManagedHolderVariableEntry.variableItemId);
requestUserVariables();
setEditingManagedHolderVariableId(0);
}, [ selectedManagedVariableEntry, selectedManagedHolderVariableEntry, roomSettings.canModify, variablesType, removeUserVariable, removeFurniVariable, requestUserVariables ]);
}, [ selectedManagedVariableEntry, selectedManagedHolderVariableEntry, roomSettings.canModify, variablesType, removeUserVariable, removeFurniVariable ]);
const clearMonitorLogs = () =>
{
@@ -3012,28 +3000,7 @@ export const WiredCreatorToolsView: FC<{}> = () =>
return;
}
if(selectedUser.kind === 'pet')
{
SendMessageComposer(new PetMoveComposer(selectedUser.userId, nextX, nextY, nextDirection));
}
else if((selectedUser.kind === 'user') && (selectedUser.roomIndex === roomSession.ownRoomIndex))
{
if(editingVariable === '@direction')
{
const directionVector = USER_DIRECTION_VECTORS[nextDirection] ?? USER_DIRECTION_VECTORS[0];
SendMessageComposer(new RoomUnitLookComposer((currentLiveState.positionX + directionVector.x), (currentLiveState.positionY + directionVector.y)));
}
else
{
SendMessageComposer(new RoomUnitWalkComposer(nextX, nextY));
}
}
else
{
cancelVariableEdit();
return;
}
SendMessageComposer(new WiredUserInspectMoveComposer(selectedUser.roomIndex, nextX, nextY, nextDirection));
setSelectedUserLiveState({
...currentLiveState,
@@ -3352,10 +3319,9 @@ export const WiredCreatorToolsView: FC<{}> = () =>
setSelectedInspectionVariableKeys(prev => ({ ...prev, furni: selectedInspectionGiveDefinition.name }));
}
requestUserVariables();
setInspectionGiveValue('0');
setIsInspectionGiveOpen(false);
}, [ canManageInspectionVariableAssignments, selectedInspectionGiveDefinition, inspectionGiveValue, inspectionType, selectedUser, assignUserVariable, selectedFurni, assignFurniVariable, requestUserVariables ]);
}, [ canManageInspectionVariableAssignments, selectedInspectionGiveDefinition, inspectionGiveValue, inspectionType, selectedUser, assignUserVariable, selectedFurni, assignFurniVariable ]);
const removeInspectionVariable = useCallback(() =>
{
if(!canManageInspectionVariableAssignments || !selectedInspectionCustomDefinition || selectedInspectionCustomDefinition.isReadOnly) return;
@@ -3373,25 +3339,44 @@ export const WiredCreatorToolsView: FC<{}> = () =>
setSelectedInspectionVariableKeys(prev => ({ ...prev, furni: '' }));
}
requestUserVariables();
setIsInspectionGiveOpen(false);
}, [ canManageInspectionVariableAssignments, selectedInspectionCustomDefinition, inspectionType, selectedUser, removeUserVariable, selectedFurni, removeFurniVariable, requestUserVariables ]);
}, [ canManageInspectionVariableAssignments, selectedInspectionCustomDefinition, inspectionType, selectedUser, removeUserVariable, selectedFurni, removeFurniVariable ]);
const onVariableInputKeyDown = (event: KeyboardEvent<HTMLInputElement>) =>
{
event.stopPropagation();
if(event.nativeEvent.stopImmediatePropagation) event.nativeEvent.stopImmediatePropagation();
switch(event.key)
{
case 'Enter':
case 'NumpadEnter':
event.preventDefault();
commitVariableEdit();
window.requestAnimationFrame(() => event.currentTarget.blur());
return;
case 'Escape':
event.preventDefault();
cancelVariableEdit();
window.requestAnimationFrame(() => event.currentTarget.blur());
return;
}
};
const onManagedHolderValueInputKeyDown = (event: KeyboardEvent<HTMLInputElement>) =>
{
event.stopPropagation();
switch(event.key)
{
case 'Enter':
case 'NumpadEnter':
event.preventDefault();
commitManagedHolderValueEdit();
window.requestAnimationFrame(() => event.currentTarget.blur());
return;
case 'Escape':
event.preventDefault();
setEditingManagedHolderVariableId(0);
window.requestAnimationFrame(() => event.currentTarget.blur());
return;
}
};
@@ -3731,15 +3716,9 @@ export const WiredCreatorToolsView: FC<{}> = () =>
type="text"
value={ editingValue }
onClick={ event => event.stopPropagation() }
onBlur={ commitVariableEdit }
onBlur={ cancelVariableEdit }
onChange={ event => setEditingValue(event.target.value) }
onKeyDownCapture={ event =>
{
event.stopPropagation();
if(event.nativeEvent.stopImmediatePropagation) event.nativeEvent.stopImmediatePropagation();
} }
onKeyDown={ onVariableInputKeyDown } /> }
onKeyDownCapture={ onVariableInputKeyDown } /> }
{ (editingVariable !== variable.key) && !variable.editable && <span className={ variable.valueClassName }>{ variable.value }</span> }
{ (editingVariable !== variable.key) && variable.editable &&
<button
@@ -4224,19 +4203,10 @@ export const WiredCreatorToolsView: FC<{}> = () =>
className="w-[72px] rounded border border-[#b8b2a4] bg-white px-2 py-[2px] text-[12px]"
type="number"
value={ editingManagedHolderValue }
onBlur={ () => commitManagedHolderValueEdit() }
onBlur={ () => setEditingManagedHolderVariableId(0) }
onChange={ event => setEditingManagedHolderValue(event.target.value) }
onClick={ event => event.stopPropagation() }
onKeyDown={ (event: KeyboardEvent<HTMLInputElement>) =>
{
if(event.key === 'Enter') commitManagedHolderValueEdit();
if(event.key === 'Escape')
{
setEditingManagedHolderVariableId(0);
setEditingManagedHolderValue('');
}
} } /> }
onKeyDownCapture={ onManagedHolderValueInputKeyDown } /> }
</td>
</tr>
);