diff --git a/src/assets/images/help/help-duck.png b/src/assets/images/help/help-duck.png new file mode 100644 index 0000000..855a48e Binary files /dev/null and b/src/assets/images/help/help-duck.png differ diff --git a/src/components/friends/views/friends-bar/FriendsBarView.tsx b/src/components/friends/views/friends-bar/FriendsBarView.tsx index 9234a07..aa66c09 100644 --- a/src/components/friends/views/friends-bar/FriendsBarView.tsx +++ b/src/components/friends/views/friends-bar/FriendsBarView.tsx @@ -79,11 +79,17 @@ export const FriendBarView: FC<{ onlineFriends: MessengerFriend[]; requestsCount // below uses it, so a stale `indexOffset` (after the list shrinks or the fit // grows) renders correctly and self-corrects on the next arrow click — no // write-back effect needed. - const maxOffset = Math.max(0, (onlineFriends.length - maxVisible)); + // Defensive: never let a null/undefined slip into the friend map. The + // legacy bar padded empty slots with `null` and rendered each as a + // FriendBarItemView (which falls back to the "find friends" chip), so an + // empty list produced THREE "Trova Amici" buttons. Filtering here makes the + // search chip below the ONLY source of that affordance — exactly one, always. + const validFriends = onlineFriends.filter(Boolean); + const maxOffset = Math.max(0, (validFriends.length - maxVisible)); const safeOffset = Math.min(indexOffset, maxOffset); const canScrollLeft = (safeOffset > 0); const canScrollRight = (safeOffset < maxOffset); - const visibleFriends = onlineFriends.slice(safeOffset, (safeOffset + maxVisible)); + const visibleFriends = validFriends.slice(safeOffset, (safeOffset + maxVisible)); return ( - { (!onlineFriends.length && (requestsCount <= 0)) && + { (!validFriends.length && (requestsCount <= 0)) && = props => return ( <> { isVisible && - + - - -
- - - - - + { activeReport + ? + +
+ + + + + + : } } diff --git a/src/components/help/views/HelpIndexView.tsx b/src/components/help/views/HelpIndexView.tsx index 339993a..eea952b 100644 --- a/src/components/help/views/HelpIndexView.tsx +++ b/src/components/help/views/HelpIndexView.tsx @@ -1,9 +1,11 @@ import { GetCfhStatusMessageComposer } from '@nitrots/nitro-renderer'; import { FC } from 'react'; -import { DispatchUiEvent, GetConfigurationValue, LocalizeText, ReportState, ReportType, SendMessageComposer } from '../../../api'; -import { Button, Text } from '../../../common'; +import { FaArrowCircleRight } from 'react-icons/fa'; +import { CreateLinkEvent, DispatchUiEvent, GetConfigurationValue, LocalizeText, ReportState, ReportType, SendMessageComposer } from '../../../api'; +import { Text } from '../../../common'; import { GuideToolEvent } from '../../../events'; import { useHelp } from '../../../hooks'; +import helpDuck from '../../../assets/images/help/help-duck.png'; export const HelpIndexView: FC<{}> = props => { @@ -21,17 +23,30 @@ export const HelpIndexView: FC<{}> = props => }; return ( - <> -
- { LocalizeText('help.main.frame.title') } - { LocalizeText('help.main.self.description') } +
+ { LocalizeText('help.main.frame.title') } + { LocalizeText('help.main.frame.description') } +
+
-
- - - +
+ +
- - +
+ + + +
+
); }; diff --git a/src/components/help/views/SanctionStatusView.tsx b/src/components/help/views/SanctionStatusView.tsx index 88beb43..a0106a5 100644 --- a/src/components/help/views/SanctionStatusView.tsx +++ b/src/components/help/views/SanctionStatusView.tsx @@ -1,6 +1,7 @@ import { FC } from 'react'; -import { LocalizeText } from '../../../api'; -import { Button, Column, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../common'; +import { FaArrowCircleRight } from 'react-icons/fa'; +import { CreateLinkEvent, LocalizeText } from '../../../api'; +import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../common'; import { useHelp } from '../../../hooks'; export const SanctionSatusView: FC<{}> = props => @@ -37,38 +38,45 @@ export const SanctionSatusView: FC<{}> = props => if(!sanctionInfo) return null; return ( - + setSanctionInfo(null) } /> - - -
- - +
+
{ (sanctionInfo.sanctionReason === 'cfh.reason.EMPTY') - ?
{ LocalizeText('help.sanction.current.none') }
+ ?
{ LocalizeText('help.sanction.current.none') }
: <> { ((sanctionInfo.probationHoursLeft > 0) || (sanctionInfo.isSanctionActive)) && -
{ LocalizeText('help.sanction.probation.reminder') }
+
{ LocalizeText('help.sanction.probation.reminder') }
} -
+
{ LocalizeText('help.sanction.last.sanction') } { sanctionLocalization('current', sanctionInfo.sanctionName, sanctionInfo.sanctionLengthHours) }
-
{ LocalizeText('generic.start.time') } { sanctionInfo.sanctionCreationTime }
-
{ LocalizeText('generic.reason') } { sanctionInfo.sanctionReason }
-
{ LocalizeText('help.sanction.probation.days.left') } { Math.trunc((sanctionInfo.probationHoursLeft / 24)) + 1 }
+
{ LocalizeText('generic.start.time') } { sanctionInfo.sanctionCreationTime }
+
{ LocalizeText('generic.reason') } { sanctionInfo.sanctionReason }
+
{ LocalizeText('help.sanction.probation.days.left') } { Math.trunc((sanctionInfo.probationHoursLeft / 24)) + 1 }
} { ((sanctionInfo.hasCustomMute) && (!(sanctionInfo.isSanctionActive))) && -
{ LocalizeText('help.sanction.custom.mute') }
+
{ LocalizeText('help.sanction.custom.mute') }
} { (sanctionInfo.tradeLockExpiryTime && sanctionInfo.tradeLockExpiryTime.length > 0) && -
{ LocalizeText('trade.locked.until') } { sanctionInfo.tradeLockExpiryTime }
+
{ LocalizeText('trade.locked.until') } { sanctionInfo.tradeLockExpiryTime }
} -
{ sanctionLocalization('next', sanctionInfo.nextSanctionName, sanctionInfo.nextSanctionLengthHours) }
- - - + { (sanctionInfo.sanctionReason !== 'cfh.reason.EMPTY') && +
{ sanctionLocalization('next', sanctionInfo.nextSanctionName, sanctionInfo.nextSanctionLengthHours) }
+ } +
+
+ + +
+
); diff --git a/src/components/purse/PurseClassicView.tsx b/src/components/purse/PurseClassicView.tsx deleted file mode 100644 index d8f6408..0000000 --- a/src/components/purse/PurseClassicView.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import { CreateLinkEvent, HabboClubLevelEnum } from '@nitrots/nitro-renderer'; -import { FC, useCallback, useEffect, useMemo, useState } from 'react'; -import { FaChevronDown, FaLanguage, FaQuestionCircle, FaSignOutAlt } from 'react-icons/fa'; -import { ClearRememberLogin, FriendlyTime, GetConfigurationValue, GetRememberLogin, LocalizeText } from '../../api'; -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 PurseClassicView: FC<{}> = props => -{ - const { purse = null, hcDisabled = false } = usePurse(); - const [ isOpen, setIsOpen ] = useState(true); - const [ isCompact, setIsCompact ] = useState(false); - - const displayedCurrencies = useMemo(() => GetConfigurationValue('system.currency.types', []), []); - const currencyDisplayNumberShort = useMemo(() => GetConfigurationValue('currency.display.number.short', false), []); - - const getClubText = (() => - { - if (!purse) return null; - - const totalDays = ((purse.clubPeriods * 31) + purse.clubDays); - const minutesUntilExpiration = purse.minutesUntilExpiration; - - if (purse.clubLevel === HabboClubLevelEnum.NO_CLUB) return LocalizeText('purse.clubdays.zero.amount.text'); - else if ((minutesUntilExpiration > -1) && (minutesUntilExpiration < (60 * 24))) return FriendlyTime.shortFormat(minutesUntilExpiration * 60); - else return FriendlyTime.shortFormat(totalDays * 86400); - })(); - - const 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; - if (a === 5) return -1; - if (b === 5) return 1; - return a - b; - }); - - return types; - }, [ displayedCurrencies, purse ]); - - const primaryCurrencies = currencyTypes.slice(0, 2); - const seasonalCurrencies = currencyTypes.slice(2); - - useEffect(() => - { - if(isOpen) - { - setIsCompact(false); - return; - } - - const timeout = window.setTimeout(() => setIsCompact(true), 220); - - return () => window.clearTimeout(timeout); - }, [ isOpen ]); - - const handleLogout = useCallback(async (event: React.MouseEvent) => - { - event.stopPropagation(); - - const logoutUrl = GetConfigurationValue('login.logout.endpoint', '/api/auth/logout'); - const ssoTicket = (window.NitroConfig?.['sso.ticket'] as string) ?? ''; - const rememberToken = GetRememberLogin()?.token || ''; - - try - { - await fetch(logoutUrl, { - method: 'POST', - credentials: 'include', - keepalive: true, - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'X-Requested-With': 'NitroPurseLogout' - }, - body: JSON.stringify({ ssoTicket, rememberToken }) - }); - } - catch - { /* best-effort — proceed with local logout regardless */ } - - ClearRememberLogin(); - if(window.NitroConfig) window.NitroConfig['sso.ticket'] = ''; - window.location.reload(); - }, []); - - if (!purse) return null; - - return ( - -
-
-
-
setIsOpen(value => !value) }> - -
- -
-
-
- -
-
-
-
-
- - { primaryCurrencies.map(type => ) } -
- { !hcDisabled && -
- { - event.stopPropagation(); CreateLinkEvent('catalog/open/' + GetConfigurationValue>('catalog.links')?.['hc.buy_hc']); - } }> -
- -
-
- HC - { getClubText } -
-
} -
- - - - -
-
-
-
-
-
- { seasonalCurrencies.length > 0 && -
- { seasonalCurrencies.map(type => ) } -
} -
- ); -}; diff --git a/src/components/purse/PurseModernView.tsx b/src/components/purse/PurseModernView.tsx deleted file mode 100644 index e3579fd..0000000 --- a/src/components/purse/PurseModernView.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { CreateLinkEvent } from '@nitrots/nitro-renderer'; -import { FC, useCallback, useMemo } from 'react'; -import { FaChartBar, FaCog, FaSignOutAlt } from 'react-icons/fa'; -import { ClearRememberLogin, GetConfigurationValue, GetRememberLogin, LocalizeText } from '../../api'; -import { Column, LayoutCurrencyIcon } from '../../common'; -import { usePurse } from '../../hooks'; -import { CurrencyView } from './views/CurrencyView'; -import { SeasonalView } from './views/SeasonalView'; - -const localizeWithFallback = (key: string, fallback: string) => -{ - const text = LocalizeText(key); - return (text && text !== key) ? text : fallback; -}; - -export const PurseModernView: FC<{}> = props => -{ - const { purse = null, hcDisabled = false } = usePurse(); - - const displayedCurrencies = useMemo(() => GetConfigurationValue('system.currency.types', []), []); - const currencyDisplayNumberShort = useMemo(() => GetConfigurationValue('currency.display.number.short', false), []); - - 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; - if (a === 5) return -1; - if (b === 5) return 1; - return a - b; - }); - - return types; - }, [ displayedCurrencies, purse ]); - - const hasDiamonds = currencyTypes.indexOf(5) >= 0; - const hasDuckets = currencyTypes.indexOf(0) >= 0; - const otherCurrencies = currencyTypes.filter(type => (type !== 0 && type !== 5)); - - const joinLabel = useMemo(() => localizeWithFallback('purse.join', 'Join'), []); - const earningsLabel = useMemo(() => localizeWithFallback('earnings.title', 'Earnings'), []); - const helpLabel = useMemo(() => localizeWithFallback('help.button.name', 'Help'), []); - - const openClub = useCallback((event: React.MouseEvent) => - { - event.stopPropagation(); - - const page = GetConfigurationValue('hc.buy_hc', 'habbo_club'); - CreateLinkEvent('catalog/open/' + page); - }, []); - - const openEarnings = useCallback((event: React.MouseEvent) => - { - event.stopPropagation(); - CreateLinkEvent('habboUI/open/vault'); - }, []); - - const handleLogout = useCallback(async (event: React.MouseEvent) => - { - event.stopPropagation(); - - const logoutUrl = GetConfigurationValue('login.logout.endpoint', '/api/auth/logout'); - const ssoTicket = (window.NitroConfig?.['sso.ticket'] as string) ?? ''; - const rememberToken = GetRememberLogin()?.token || ''; - - try - { - await fetch(logoutUrl, { - method: 'POST', - credentials: 'include', - keepalive: true, - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'X-Requested-With': 'NitroPurseLogout' - }, - body: JSON.stringify({ ssoTicket, rememberToken }) - }); - } - catch - { /* best-effort — proceed with local logout regardless */ } - - ClearRememberLogin(); - if(window.NitroConfig) window.NitroConfig['sso.ticket'] = ''; - window.location.reload(); - }, []); - - if (!purse) return null; - - return ( - -
-
-
- { hasDiamonds && } - - { hasDuckets && } -
-
- { !hcDisabled && - } - -
-
- - - -
-
-
- { otherCurrencies.length > 0 && -
- { otherCurrencies.map(type => ) } -
} -
- ); -}; diff --git a/src/components/purse/PurseView.tsx b/src/components/purse/PurseView.tsx index b039ebd..60786fe 100644 --- a/src/components/purse/PurseView.tsx +++ b/src/components/purse/PurseView.tsx @@ -1,7 +1,153 @@ -import { FC } from 'react'; -import { PurseModernView } from './PurseModernView'; +import { CreateLinkEvent } from '@nitrots/nitro-renderer'; +import { FC, useCallback, useMemo, useState } from 'react'; +import { FaChartBar, FaCog, FaSignOutAlt } from 'react-icons/fa'; +import { ClearRememberLogin, GetConfigurationValue, GetRememberLogin, LocalizeText } from '../../api'; +import { Column, LayoutCurrencyIcon } from '../../common'; +import { usePurse } from '../../hooks'; +import { CurrencyView } from './views/CurrencyView'; +import { SeasonalView } from './views/SeasonalView'; + +const localizeWithFallback = (key: string, fallback: string) => +{ + const text = LocalizeText(key); + return (text && text !== key) ? text : fallback; +}; export const PurseView: FC<{}> = props => { - return ; + const { purse = null, hcDisabled = false } = usePurse(); + const [ settingsMenuOpen, setSettingsMenuOpen ] = useState(false); + + const openSettingsSection = useCallback((section: string) => + { + CreateLinkEvent('user-settings/show/' + section); + setSettingsMenuOpen(false); + }, []); + + const displayedCurrencies = useMemo(() => GetConfigurationValue('system.currency.types', []), []); + const currencyDisplayNumberShort = useMemo(() => GetConfigurationValue('currency.display.number.short', false), []); + + 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; + if (a === 5) return -1; + if (b === 5) return 1; + return a - b; + }); + + return types; + }, [ displayedCurrencies, purse ]); + + const hasDiamonds = currencyTypes.indexOf(5) >= 0; + const hasDuckets = currencyTypes.indexOf(0) >= 0; + const otherCurrencies = currencyTypes.filter(type => (type !== 0 && type !== 5)); + + const joinLabel = useMemo(() => localizeWithFallback('purse.join', 'Join'), []); + const earningsLabel = useMemo(() => localizeWithFallback('earnings.title', 'Earnings'), []); + const helpLabel = useMemo(() => localizeWithFallback('help.button.name', 'Help'), []); + + const openClub = useCallback((event: React.MouseEvent) => + { + event.stopPropagation(); + CreateLinkEvent('habboUI/open/hccenter'); + }, []); + + const openEarnings = useCallback((event: React.MouseEvent) => + { + event.stopPropagation(); + CreateLinkEvent('habboUI/open/vault'); + }, []); + + const handleLogout = useCallback(async (event: React.MouseEvent) => + { + event.stopPropagation(); + + const logoutUrl = GetConfigurationValue('login.logout.endpoint', '/api/auth/logout'); + const ssoTicket = (window.NitroConfig?.['sso.ticket'] as string) ?? ''; + const rememberToken = GetRememberLogin()?.token || ''; + + try + { + await fetch(logoutUrl, { + method: 'POST', + credentials: 'include', + keepalive: true, + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-Requested-With': 'NitroPurseLogout' + }, + body: JSON.stringify({ ssoTicket, rememberToken }) + }); + } + catch + { /* best-effort — proceed with local logout regardless */ } + + ClearRememberLogin(); + if(window.NitroConfig) window.NitroConfig['sso.ticket'] = ''; + window.location.reload(); + }, []); + + if (!purse) return null; + + return ( + +
+
+
+ { hasDiamonds && } + + { hasDuckets && } +
+
+ { !hcDisabled && + } + +
+
+ + + +
+
+
+ { settingsMenuOpen && +
+ + + + + + +
} + { otherCurrencies.length > 0 && +
+ { otherCurrencies.map(type => ) } +
} +
+ ); }; diff --git a/src/components/room/widgets/chat-input/ChatInputStyleSelectorView.tsx b/src/components/room/widgets/chat-input/ChatInputStyleSelectorView.tsx index 15f2863..3de98ea 100644 --- a/src/components/room/widgets/chat-input/ChatInputStyleSelectorView.tsx +++ b/src/components/room/widgets/chat-input/ChatInputStyleSelectorView.tsx @@ -23,8 +23,11 @@ export const ChatInputStyleSelectorView: FC = p return ( -
-
+
+ + + +
diff --git a/src/components/room/widgets/chat-input/ChatInputView.tsx b/src/components/room/widgets/chat-input/ChatInputView.tsx index 1edcfe5..475060e 100644 --- a/src/components/room/widgets/chat-input/ChatInputView.tsx +++ b/src/components/room/widgets/chat-input/ChatInputView.tsx @@ -313,7 +313,7 @@ export const ChatInputView: FC<{}> = props => return ( createPortal( -
+
{ commandSelectorVisible && = props => onSelect={ mention.apply } onHover={ mention.setSelectedIndex } /> } -
- { !floodBlocked && - updateChatInput(event.target.value) } onMouseDown={ event => setInputFocus() } /> } - { floodBlocked && - { LocalizeText('chat.input.alert.flood', [ 'time' ], [ floodBlockedSeconds.toString() ]) } } -
- + { !floodBlocked && +
+ updateChatInput(event.target.value) } onMouseDown={ event => setInputFocus() } /> +
} + { floodBlocked && +
+ { LocalizeText('chat.input.alert.flood', [ 'time' ], [ floodBlockedSeconds.toString() ]) } +
} +
, 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 &&
- - - - - = props =>
} + 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') } /> -
-
- processAction('oldchat', event.target.checked) } /> - { LocalizeText('memenu.settings.chat.prefer.old.chat') } -
-
- processAction('room_invites', event.target.checked) } /> - { LocalizeText('memenu.settings.other.ignore.room.invites') } -
-
- processAction('camera_follow', event.target.checked) } /> - { LocalizeText('memenu.settings.other.disable.room.camera.follow') } -
-
- setCatalogPlaceMultipleObjects(event.target.checked) } /> - { LocalizeText('memenu.settings.other.place.multiple.objects') } -
-
- setCatalogSkipPurchaseConfirmation(event.target.checked) } /> - { LocalizeText('memenu.settings.other.skip.purchase.confirmation') } -
-
- setChatWindowEnabled(event.target.checked) } /> - { LocalizeText('memenu.settings.other.enable.chat.window') } -
-
-
- { LocalizeText('widget.memenu.settings.volume') } + { showChat &&
- { LocalizeText('widget.memenu.settings.volume.ui') }
- { (userSettings.volumeSystem === 0) && = 50) && 'text-muted', 'fa-icon') } /> } - { (userSettings.volumeSystem > 0) && = 50) && 'text-muted', 'fa-icon') } /> } - processAction('system_volume', event.target.value) } onMouseUp={ () => saveRangeSlider('volume') } /> - + processAction('oldchat', event.target.checked) } /> + { LocalizeText('memenu.settings.chat.prefer.old.chat') }
-
+
+ setChatWindowEnabled(event.target.checked) } /> + { LocalizeText('memenu.settings.other.enable.chat.window') } +
+
} + { showOther &&
- { LocalizeText('widget.memenu.settings.volume.furni') }
- { (userSettings.volumeFurni === 0) && = 50) && 'text-muted', 'fa-icon') } /> } - { (userSettings.volumeFurni > 0) && = 50) && 'text-muted', 'fa-icon') } /> } - processAction('furni_volume', event.target.value) } onMouseUp={ () => saveRangeSlider('volume') } /> - + processAction('room_invites', event.target.checked) } /> + { LocalizeText('memenu.settings.other.ignore.room.invites') }
-
-
- { LocalizeText('widget.memenu.settings.volume.trax') }
- { (userSettings.volumeTrax === 0) && = 50) && 'text-muted', 'fa-icon') } /> } - { (userSettings.volumeTrax > 0) && = 50) && 'text-muted', 'fa-icon') } /> } - processAction('trax_volume', event.target.value) } onMouseUp={ () => saveRangeSlider('volume') } /> - + processAction('camera_follow', event.target.checked) } /> + { LocalizeText('memenu.settings.other.disable.room.camera.follow') }
-
-
-
- -
+
} + { showAudio && +
+ { LocalizeText('widget.memenu.settings.volume') } +
+ { LocalizeText('widget.memenu.settings.volume.ui') } +
+ { (userSettings.volumeSystem === 0) && = 50) && 'text-muted', 'fa-icon') } /> } + { (userSettings.volumeSystem > 0) && = 50) && 'text-muted', 'fa-icon') } /> } + processAction('system_volume', event.target.value) } onMouseUp={ () => saveRangeSlider('volume') } /> + +
+
+
+ { LocalizeText('widget.memenu.settings.volume.furni') } +
+ { (userSettings.volumeFurni === 0) && = 50) && 'text-muted', 'fa-icon') } /> } + { (userSettings.volumeFurni > 0) && = 50) && 'text-muted', 'fa-icon') } /> } + processAction('furni_volume', event.target.value) } onMouseUp={ () => saveRangeSlider('volume') } /> + +
+
+
+ { LocalizeText('widget.memenu.settings.volume.trax') } +
+ { (userSettings.volumeTrax === 0) && = 50) && 'text-muted', 'fa-icon') } /> } + { (userSettings.volumeTrax > 0) && = 50) && 'text-muted', 'fa-icon') } /> } + processAction('trax_volume', event.target.value) } onMouseUp={ () => saveRangeSlider('volume') } /> + +
+
+
} + { showAccountLink && +
+ +
} + { (section !== null) && +
+ +
} ); diff --git a/src/css/help/HelpView.css b/src/css/help/HelpView.css new file mode 100644 index 0000000..e05897e --- /dev/null +++ b/src/css/help/HelpView.css @@ -0,0 +1,83 @@ +/* Aiuto (Help) + Sanctions windows — custom hotel restyle. + Global (not an inline