+
, document.getElementById('toolbar-chat-input-container'))
);
};
diff --git a/src/components/toolbar/ToolbarView.tsx b/src/components/toolbar/ToolbarView.tsx
index e8ad69a..ec10799 100644
--- a/src/components/toolbar/ToolbarView.tsx
+++ b/src/components/toolbar/ToolbarView.tsx
@@ -1,7 +1,7 @@
-import { CreateLinkEvent, Dispose, DropBounce, EaseOut, JumpBy, Motions, NitroToolbarAnimateIconEvent, PerkAllowancesMessageEvent, PerkEnum, Queue, Wait, YouTubeRoomSettingsEvent } from '@nitrots/nitro-renderer';
+import { CreateLinkEvent, Dispose, DropBounce, EaseOut, FindNewFriendsMessageComposer, JumpBy, Motions, NitroToolbarAnimateIconEvent, PerkAllowancesMessageEvent, PerkEnum, Queue, Wait, YouTubeRoomSettingsEvent } from '@nitrots/nitro-renderer';
import { AnimatePresence, motion, Variants } from 'framer-motion';
-import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import { GetConfigurationValue, isHousekeepingEnabled, MessengerIconState, OpenMessengerChat, setYoutubeRoomEnabled, VisitDesktop } from '../../api';
+import { FC, useEffect, useMemo, useState } from 'react';
+import { GetConfigurationValue, isHousekeepingEnabled, MessengerIconState, OpenMessengerChat, SendMessageComposer, setYoutubeRoomEnabled, VisitDesktop } from '../../api';
import { Flex, LayoutAvatarImageView, LayoutItemCountView } from '../../common';
import { useAchievements, useFriends, useHasPermission, useInventoryUnseenTracker, useMentionsSnapshot, useMessageEvent, useMessenger, useModTools, useNitroEvent, useSessionInfo, useSoundboard, useWiredTools } from '../../hooks';
import { ToolbarItemView } from './ToolbarItemView';
@@ -26,14 +26,14 @@ const shellVariants: Variants = {
const SHELL_TRANSITION = { type: 'spring' as const, stiffness: 260, damping: 26 };
const NAV_TRANSITION = { type: 'spring' as const, stiffness: 300, damping: 28 };
const ME_POPOVER_TRANSITION = { type: 'spring' as const, stiffness: 420, damping: 28 };
-const TOGGLE_LOCK_MS = 220;
export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
{
const { isInRoom } = props;
const [ isMeExpanded, setMeExpanded ] = useState(false);
- const [ isToolbarOpen, setIsToolbarOpen ] = useState(false);
const [ isTouchLayout, setIsTouchLayout ] = useState(false);
+ const [ leftCollapsed, setLeftCollapsed ] = useState(false);
+ const [ rightCollapsed, setRightCollapsed ] = useState(false);
const [ staffStackBottom, setStaffStackBottom ] = useState
(null);
const [ useGuideTool, setUseGuideTool ] = useState(false);
const [ youtubeEnabled, setYoutubeEnabled ] = useState(false);
@@ -54,26 +54,9 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
() => isMod ? tickets.filter(ticket => ticket && (ticket.state === 1)).length : 0,
[ isMod, tickets ]
);
- const isVisible = (isToolbarOpen || !isInRoom);
- const visibilityVariant = isVisible ? 'visible' : 'hidden';
- const toggleLockRef = useRef(false);
- const toggleTimeoutRef = useRef | null>(null);
+ const visibilityVariant = 'visible';
- useEffect(() => () =>
- {
- if(toggleTimeoutRef.current) clearTimeout(toggleTimeoutRef.current);
- }, []);
-
- const handleToggleClick = useCallback(() =>
- {
- if(toggleLockRef.current) return;
- toggleLockRef.current = true;
- setIsToolbarOpen(value => !value);
- if(toggleTimeoutRef.current) clearTimeout(toggleTimeoutRef.current);
- toggleTimeoutRef.current = setTimeout(() => { toggleLockRef.current = false; }, TOGGLE_LOCK_MS);
- }, []);
-
- const compactFramePosition = (isToolbarOpen && isInRoom) ? 'bottom-[90px] min-[1700px]:bottom-0' : 'bottom-0';
+ const compactFramePosition = 'bottom-[90px] min-[1700px]:bottom-[7px]';
const mobileOnlyClasses = isTouchLayout ? '' : 'min-[1700px]:hidden';
const desktopBlockClasses = isTouchLayout ? 'hidden' : 'hidden min-[1700px]:block';
const desktopFlexClasses = isTouchLayout ? 'hidden' : 'hidden min-[1700px]:flex';
@@ -196,20 +179,6 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
{ isInRoom &&
}
+ className={ `pointer-events-none fixed bottom-0 left-0 right-0 z-[39] h-[52px] rounded-t-[12px] border border-b-0 border-white/8 bg-[rgba(62,64,72,0.55)] shadow-[0_-6px_18px_rgba(0,0,0,0.18)] ${ desktopBlockClasses }` } />
+
+ { !leftCollapsed && (<>
{ isInRoom
? VisitDesktop() } className="tb-icon" />
@@ -245,6 +224,7 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
CreateLinkEvent('games/toggle') } className="tb-icon" />
}
+ >) }
CreateLinkEvent('catalog/toggle/normal') } className="tb-icon" />
@@ -282,6 +262,7 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
{ (getFullCount > 0) &&
}
+ { !leftCollapsed && (<>
CreateLinkEvent('rare-values/toggle') } className="tb-icon" />
@@ -292,10 +273,12 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
}
+ >) }
{ isInRoom &&
CreateLinkEvent('camera/toggle') } className="tb-icon" />
}
+ { !leftCollapsed && (<>
{ (isInRoom && youtubeEnabled) &&
@@ -304,6 +287,7 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
CreateLinkEvent('soundboard/toggle') } className="tb-icon" />
}
+ >) }
{ isMod &&
CreateLinkEvent('mod-tools/toggle') } className="tb-icon" />
@@ -321,7 +305,7 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
= props =>
{ (requests.length > 0) &&
}
+ { rightCollapsed &&
+
+ SendMessageComposer(new FindNewFriendsMessageComposer()) } className="tb-icon" />
+ }
+ { !rightCollapsed && (<>
{ mentionsEnabled &&
CreateLinkEvent('mentions/toggle') } className="tb-icon" />
@@ -346,14 +335,24 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
}
+ >) }
+
+ className={ `fixed left-1/2 bottom-0 z-40 flex w-[95vw] -translate-x-1/2 items-center overflow-visible ${ mobileOnlyClasses } ${ isInRoom ? 'rounded-[12px] border border-white/8 bg-[rgba(62,64,72,0.55)] px-[6px] py-[4px] mb-[3px] shadow-[0_-6px_18px_rgba(0,0,0,0.18)]' : '' }` }>
@@ -443,12 +442,12 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
room. Always present (Builders Club), plus camera in-room
and the staff-only tools when permitted. */ }
+ className={ `fixed left-1 z-40 flex flex-col items-center gap-2 rounded-[12px] border border-white/8 bg-[rgba(62,64,72,0.55)] px-[4px] py-[6px] shadow-[0_6px_18px_rgba(0,0,0,0.18)] ${ staffStackBottom == null ? 'top-1/2 -translate-y-1/2' : '' } ${ mobileOnlyClasses }` }>
CreateLinkEvent('catalog/toggle/builder') } className="tb-icon" />
@@ -499,9 +498,7 @@ const TOOLBAR_STYLES = `
Negative inset margins on the clip path keep vertical breathing
room for the popover even on engines that fall back to 'hidden'. */
.tb-nav-clip {
- overflow-x: clip;
- overflow-y: visible;
- overflow-clip-margin: 0 0 200px 0;
+ overflow: visible;
}
.tb-icon {
@@ -518,26 +515,24 @@ const TOOLBAR_STYLES = `
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);
+ .tb-collapse {
+ width: 15px;
+ height: 34px;
display: flex;
align-items: center;
justify-content: center;
+ flex-shrink: 0;
+ border-radius: 6px;
+ background: rgba(62, 64, 72, 0.55);
+ border: 1px solid rgba(255, 255, 255, 0.10);
+ color: rgba(255, 255, 255, 0.70);
cursor: pointer;
- box-shadow: 0 3px 12px rgba(0, 0, 0, 0.5);
- transition: background 0.15s, border-color 0.15s;
+ transition: color 0.15s, background 0.15s;
}
- .tb-toggle:hover {
- background: rgba(30, 26, 20, 0.88);
- border-color: rgba(255, 255, 255, 0.13);
+ .tb-collapse:hover {
+ color: #fff;
+ background: rgba(80, 82, 90, 0.65);
}
.tb-bar-scroll {
diff --git a/src/components/user-settings/UserSettingsView.tsx b/src/components/user-settings/UserSettingsView.tsx
index 4ed5dbd..7d4acf5 100644
--- a/src/components/user-settings/UserSettingsView.tsx
+++ b/src/components/user-settings/UserSettingsView.tsx
@@ -2,13 +2,24 @@ import { AddLinkEventTracker, CreateLinkEvent, ILinkEventTracker, NitroSettingsE
import { FC, useEffect, useState } from 'react';
import { FaUserCog, FaVolumeDown, FaVolumeMute, FaVolumeUp } from 'react-icons/fa';
import { DispatchMainEvent, DispatchUiEvent, LocalizeText, SendMessageComposer } from '../../api';
-import { NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common';
+import { Button, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common';
import { useCatalogPlaceMultipleItems, useCatalogSkipPurchaseConfirmation, useChatWindow, useMessageEvent } from '../../hooks';
import { classNames } from '../../layout';
+const localizeWithFallback = (key: string, fallback: string) =>
+{
+ const text = LocalizeText(key);
+ return (text && text !== key) ? text : fallback;
+};
+
+// null = full window (legacy). 'audio' | 'chat' | 'other' = focused section
+// opened from the purse gear dropdown.
+type SettingsSection = null | 'audio' | 'chat' | 'other';
+
export const UserSettingsView: FC<{}> = props =>
{
const [ isVisible, setIsVisible ] = useState(false);
+ const [ section, setSection ] = useState(null);
const [ userSettings, setUserSettings ] = useState(null);
const [ catalogPlaceMultipleObjects, setCatalogPlaceMultipleObjects ] = useCatalogPlaceMultipleItems();
const [ catalogSkipPurchaseConfirmation, setCatalogSkipPurchaseConfirmation ] = useCatalogSkipPurchaseConfirmation();
@@ -100,12 +111,14 @@ export const UserSettingsView: FC<{}> = props =>
switch(parts[1])
{
case 'show':
+ setSection((parts[2] as SettingsSection) || null);
setIsVisible(true);
return;
case 'hide':
setIsVisible(false);
return;
case 'toggle':
+ setSection((parts[2] as SettingsSection) || null);
setIsVisible(prevValue => !prevValue);
return;
}
@@ -127,81 +140,104 @@ export const UserSettingsView: FC<{}> = props =>
if(!isVisible || !userSettings) return null;
+ const showChat = (section === null || section === 'chat');
+ const showOther = (section === null || section === 'other');
+ const showAudio = (section === null || section === 'audio');
+ const showAccountLink = (section === null);
+
+ const headerText = (section === 'audio')
+ ? localizeWithFallback('widget.memenu.settings.volume', 'Audio settings')
+ : (section === 'chat')
+ ? localizeWithFallback('room.chat.settings.title', 'Chat settings')
+ : (section === 'other')
+ ? localizeWithFallback('memenu.settings.other', 'Other settings')
+ : LocalizeText('widget.memenu.settings.title');
+
return (
- processAction('close_view') } />
+ processAction('close_view') } />
-
-
-
{ LocalizeText('widget.memenu.settings.volume') }
+ { showChat &&
+
+ setChatWindowEnabled(event.target.checked) } />
+ { LocalizeText('memenu.settings.other.enable.chat.window') }
+
+
}
+ { showOther &&
-
-
-