mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 23:16:21 +00:00
Refine mobile avatar widgets and login flow
This commit is contained in:
@@ -195,11 +195,15 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated, isEntering = fa
|
||||
const [ localeError, setLocaleError ] = useState('');
|
||||
const [ loginViewConfig, setLoginViewConfig ] = useState<Record<string, unknown>>(() => GetConfigurationValue<Record<string, unknown>>('loginview', {}));
|
||||
const submitTimeRef = useRef(0);
|
||||
const preloadedLoginImagesRef = useRef<Set<string>>(new Set());
|
||||
|
||||
const configuredLoginImages: Record<string, string> = (loginViewConfig?.['images'] as Record<string, string>) ?? {};
|
||||
const loginImages: Record<string, string> = { ...getDefaultLoginImages(), ...configuredLoginImages };
|
||||
|
||||
const configuredLoginWidgets: Record<string, unknown> = (loginViewConfig?.['widgets'] as Record<string, unknown>) ?? {};
|
||||
const configuredLoginImages = useMemo<Record<string, string>>(() =>
|
||||
(loginViewConfig?.['images'] as Record<string, string>) ?? {}, [ loginViewConfig ]);
|
||||
const loginImages = useMemo<Record<string, string>>(() =>
|
||||
({ ...getDefaultLoginImages(), ...configuredLoginImages }), [ configuredLoginImages ]);
|
||||
|
||||
const configuredLoginWidgets = useMemo<Record<string, unknown>>(() =>
|
||||
(loginViewConfig?.['widgets'] as Record<string, unknown>) ?? {}, [ loginViewConfig ]);
|
||||
const loginWidgetSlots = useMemo(() =>
|
||||
{
|
||||
return Object.entries(configuredLoginWidgets)
|
||||
@@ -311,18 +315,32 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated, isEntering = fa
|
||||
if(!loginImageUrls.length) return;
|
||||
|
||||
let cancelled = false;
|
||||
let remaining = 0;
|
||||
|
||||
loginImageUrls.forEach(url =>
|
||||
{
|
||||
const image = new Image();
|
||||
|
||||
image.onload = image.onerror = () =>
|
||||
loginImageUrls
|
||||
.filter(url =>
|
||||
{
|
||||
if(!cancelled) setLoginImagesVersion(version => version + 1);
|
||||
};
|
||||
if(preloadedLoginImagesRef.current.has(url)) return false;
|
||||
|
||||
image.src = url;
|
||||
});
|
||||
preloadedLoginImagesRef.current.add(url);
|
||||
|
||||
return true;
|
||||
})
|
||||
.forEach(url =>
|
||||
{
|
||||
remaining++;
|
||||
|
||||
const image = new Image();
|
||||
|
||||
image.onload = image.onerror = () =>
|
||||
{
|
||||
remaining--;
|
||||
|
||||
if(!cancelled && remaining <= 0) setLoginImagesVersion(version => version + 1);
|
||||
};
|
||||
|
||||
image.src = url;
|
||||
});
|
||||
|
||||
return () =>
|
||||
{
|
||||
|
||||
@@ -27,7 +27,9 @@ export const NewsWindow: FC<NewsWindowProps> = ({ newsUrl }) =>
|
||||
{
|
||||
if(!newsUrl) { setFailed(true); return; }
|
||||
let cancelled = false;
|
||||
fetch(newsUrl, { credentials: 'omit' })
|
||||
const controller = new AbortController();
|
||||
|
||||
fetch(newsUrl, { credentials: 'omit', signal: controller.signal })
|
||||
.then(async r =>
|
||||
{
|
||||
if(!r.ok) throw new Error('status ' + r.status);
|
||||
@@ -42,7 +44,11 @@ export const NewsWindow: FC<NewsWindowProps> = ({ newsUrl }) =>
|
||||
setItems(list);
|
||||
})
|
||||
.catch(() => { if(!cancelled) setFailed(true); });
|
||||
return () => { cancelled = true; };
|
||||
return () =>
|
||||
{
|
||||
cancelled = true;
|
||||
controller.abort();
|
||||
};
|
||||
}, [ newsUrl ]);
|
||||
|
||||
useEffect(() =>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GetRenderer, RoomSession } from '@nitrots/nitro-renderer';
|
||||
import { GetEventDispatcher, GetRenderer, RoomObjectMouseEvent, RoomObjectTileMouseEvent, RoomSession } from '@nitrots/nitro-renderer';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { FC, useEffect, useRef } from 'react';
|
||||
import { DispatchMouseEvent, DispatchTouchEvent } from '../../api';
|
||||
@@ -30,6 +30,68 @@ export const RoomView: FC<{}> = (props) =>
|
||||
canvas.ontouchend = (event) => DispatchTouchEvent(event);
|
||||
canvas.ontouchcancel = (event) => DispatchTouchEvent(event);
|
||||
|
||||
let touchStartX = 0;
|
||||
let touchStartY = 0;
|
||||
let touchMoved = false;
|
||||
let lastTileTap: { x: number; y: number; time: number } = null;
|
||||
|
||||
const isMobileTouch = () => window.matchMedia('(pointer: coarse), (hover: none)').matches;
|
||||
|
||||
const onTouchStart = (event: TouchEvent) =>
|
||||
{
|
||||
const touch = event.touches[0];
|
||||
|
||||
if(!touch || !isMobileTouch()) return;
|
||||
|
||||
touchStartX = touch.clientX;
|
||||
touchStartY = touch.clientY;
|
||||
touchMoved = false;
|
||||
};
|
||||
|
||||
const onTouchMove = (event: TouchEvent) =>
|
||||
{
|
||||
const touch = event.touches[0];
|
||||
|
||||
if(!touch || !isMobileTouch()) return;
|
||||
|
||||
if(Math.abs(touch.clientX - touchStartX) > 8 || Math.abs(touch.clientY - touchStartY) > 8) touchMoved = true;
|
||||
};
|
||||
|
||||
const onTouchEnd = (event: TouchEvent) =>
|
||||
{
|
||||
const touch = event.changedTouches[0];
|
||||
|
||||
if(!touch || touchMoved || !isMobileTouch()) return;
|
||||
|
||||
lastTileTap = { x: touch.clientX, y: touch.clientY, time: Date.now() };
|
||||
};
|
||||
|
||||
const showTouchFeedback = () =>
|
||||
{
|
||||
if(!lastTileTap || ((Date.now() - lastTileTap.time) > 250)) return;
|
||||
|
||||
const feedback = document.createElement('div');
|
||||
|
||||
feedback.className = 'nitro-room-touch-feedback';
|
||||
feedback.style.left = `${ lastTileTap.x }px`;
|
||||
feedback.style.top = `${ lastTileTap.y }px`;
|
||||
|
||||
document.body.appendChild(feedback);
|
||||
window.setTimeout(() => feedback.remove(), 420);
|
||||
|
||||
lastTileTap = null;
|
||||
};
|
||||
|
||||
const onTileClick = (event: RoomObjectMouseEvent) =>
|
||||
{
|
||||
if(event instanceof RoomObjectTileMouseEvent) window.setTimeout(showTouchFeedback, 0);
|
||||
};
|
||||
|
||||
canvas.addEventListener('touchstart', onTouchStart, { passive: true });
|
||||
canvas.addEventListener('touchmove', onTouchMove, { passive: true });
|
||||
canvas.addEventListener('touchend', onTouchEnd, { passive: true });
|
||||
GetEventDispatcher().addEventListener(RoomObjectMouseEvent.CLICK, onTileClick);
|
||||
|
||||
const element = elementRef.current;
|
||||
|
||||
if(!element) return;
|
||||
@@ -37,6 +99,14 @@ export const RoomView: FC<{}> = (props) =>
|
||||
canvas.classList.add('bg-black');
|
||||
|
||||
element.appendChild(canvas);
|
||||
|
||||
return () =>
|
||||
{
|
||||
canvas.removeEventListener('touchstart', onTouchStart);
|
||||
canvas.removeEventListener('touchmove', onTouchMove);
|
||||
canvas.removeEventListener('touchend', onTouchEnd);
|
||||
GetEventDispatcher().removeEventListener(RoomObjectMouseEvent.CLICK, onTileClick);
|
||||
};
|
||||
}, [roomSession]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AddLinkEventTracker, GetRoomEngine, GetSessionDataManager, ILinkEventTracker, RemoveLinkEventTracker, RoomEngineEvent, RoomEnterEffect, RoomSessionDanceEvent } from '@nitrots/nitro-renderer';
|
||||
import { AddLinkEventTracker, GetRoomEngine, GetSessionDataManager, ILinkEventTracker, RemoveLinkEventTracker, RoomEngineEvent, RoomEngineObjectEvent, RoomEnterEffect, RoomSessionDanceEvent } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { AvatarInfoFurni, AvatarInfoPet, AvatarInfoRentableBot, AvatarInfoUser, GetConfigurationValue, RoomWidgetUpdateRentableBotChatEvent } from '../../../../api';
|
||||
import { Column } from '../../../../common';
|
||||
import { Column, LayoutFurniIconImageView } from '../../../../common';
|
||||
import { useAvatarInfoWidget, useNitroEvent, useRoom, useUiEvent } from '../../../../hooks';
|
||||
import { AvatarInfoPetTrainingPanelView } from './AvatarInfoPetTrainingPanelView';
|
||||
import { AvatarInfoRentableBotChatView } from './AvatarInfoRentableBotChatView';
|
||||
@@ -27,6 +27,9 @@ export const AvatarInfoWidgetView: FC<{}> = props =>
|
||||
const BLOCK_ROTATE_WINDOW_MS = 500;
|
||||
const [ isGameMode, setGameMode ] = useState(false);
|
||||
const [ isDancing, setIsDancing ] = useState(false);
|
||||
const [ isTouchLayout, setIsTouchLayout ] = useState(false);
|
||||
const [ mobileFurniDetailsOpen, setMobileFurniDetailsOpen ] = useState(false);
|
||||
const [ mobileUserDetailsOpen, setMobileUserDetailsOpen ] = useState(false);
|
||||
const [ rentableBotChatEvent, setRentableBotChatEvent ] = useState<RoomWidgetUpdateRentableBotChatEvent>(null);
|
||||
const { avatarInfo = null, setAvatarInfo = null, activeNameBubble = null, setActiveNameBubble = null, nameBubbles = [], removeNameBubble = null, productBubbles = [], confirmingProduct = null, updateConfirmingProduct = null, removeProductBubble = null, isDecorating = false, setIsDecorating = null } = useAvatarInfoWidget();
|
||||
const { roomSession = null } = useRoom();
|
||||
@@ -56,6 +59,17 @@ export const AvatarInfoWidgetView: FC<{}> = props =>
|
||||
if(!isGameMode) setGameMode(true);
|
||||
});
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const query = window.matchMedia('(pointer: coarse), (hover: none)');
|
||||
const updateTouchLayout = () => setIsTouchLayout(query.matches);
|
||||
|
||||
updateTouchLayout();
|
||||
query.addEventListener('change', updateTouchLayout);
|
||||
|
||||
return () => query.removeEventListener('change', updateTouchLayout);
|
||||
}, []);
|
||||
|
||||
useNitroEvent<RoomSessionDanceEvent>(RoomSessionDanceEvent.RSDE_DANCE, event =>
|
||||
{
|
||||
if(event.roomIndex !== roomSession.ownRoomIndex) return;
|
||||
@@ -65,6 +79,13 @@ export const AvatarInfoWidgetView: FC<{}> = props =>
|
||||
|
||||
useUiEvent<RoomWidgetUpdateRentableBotChatEvent>(RoomWidgetUpdateRentableBotChatEvent.UPDATE_CHAT, event => setRentableBotChatEvent(event));
|
||||
|
||||
useNitroEvent<RoomEngineObjectEvent>(RoomEngineObjectEvent.REQUEST_MANIPULATION, event =>
|
||||
{
|
||||
if(event.category !== avatarInfo?.category || event.objectId !== avatarInfo?.id) return;
|
||||
|
||||
setMobileFurniDetailsOpen(false);
|
||||
});
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const linkTracker: ILinkEventTracker = {
|
||||
@@ -100,6 +121,19 @@ export const AvatarInfoWidgetView: FC<{}> = props =>
|
||||
return () => RemoveLinkEventTracker(linkTracker);
|
||||
}, [ roomSession, setActiveNameBubble, setAvatarInfo ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(avatarInfo?.type !== AvatarInfoFurni.FURNI)
|
||||
{
|
||||
setMobileFurniDetailsOpen(false);
|
||||
}
|
||||
|
||||
if(avatarInfo?.type !== AvatarInfoUser.OWN_USER && avatarInfo?.type !== AvatarInfoUser.PEER)
|
||||
{
|
||||
setMobileUserDetailsOpen(false);
|
||||
}
|
||||
}, [ avatarInfo ]);
|
||||
|
||||
const getMenuView = () =>
|
||||
{
|
||||
if(!roomSession || isGameMode) return null;
|
||||
@@ -120,6 +154,9 @@ export const AvatarInfoWidgetView: FC<{}> = props =>
|
||||
case AvatarInfoUser.OWN_USER:
|
||||
case AvatarInfoUser.PEER: {
|
||||
const info = (avatarInfo as AvatarInfoUser);
|
||||
|
||||
if(isTouchLayout && !mobileUserDetailsOpen) return null;
|
||||
|
||||
if(GetConfigurationValue('user.tags.enabled')) GetSessionDataManager().getUserTags(info.roomIndex);
|
||||
|
||||
if(info.isSpectatorMode) return null;
|
||||
@@ -156,9 +193,41 @@ export const AvatarInfoWidgetView: FC<{}> = props =>
|
||||
switch(avatarInfo.type)
|
||||
{
|
||||
case AvatarInfoFurni.FURNI:
|
||||
if(isTouchLayout && !isDecorating)
|
||||
{
|
||||
const info = (avatarInfo as AvatarInfoFurni);
|
||||
|
||||
if(!mobileFurniDetailsOpen)
|
||||
{
|
||||
return (
|
||||
<button className="nitro-mobile-furni-infostand-trigger" type="button" onClick={ () => setMobileFurniDetailsOpen(true) }>
|
||||
<LayoutFurniIconImageView productType={ info.productType } productClassId={ info.spriteId } />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return <InfoStandWidgetFurniView avatarInfo={ (avatarInfo as AvatarInfoFurni) } onClose={ () => setAvatarInfo(null) } />;
|
||||
case AvatarInfoUser.OWN_USER:
|
||||
case AvatarInfoUser.PEER:
|
||||
if(isTouchLayout)
|
||||
{
|
||||
const info = (avatarInfo as AvatarInfoUser);
|
||||
const figure = encodeURIComponent(info.figure || '');
|
||||
const avatarHeadUrl = `https://www.habbo.com/habbo-imaging/avatarimage?figure=${ figure }&direction=2&head_direction=2&gesture=sml&size=m&headonly=1`;
|
||||
|
||||
if(!mobileUserDetailsOpen)
|
||||
{
|
||||
return (
|
||||
<button className="nitro-mobile-furni-infostand-trigger nitro-mobile-user-infostand-trigger" type="button" onClick={ () => setMobileUserDetailsOpen(true) }>
|
||||
<div className="nitro-mobile-user-infostand-avatar">
|
||||
<img className="nitro-mobile-user-infostand-avatar-image" src={ avatarHeadUrl } alt={ info.name } draggable={ false } />
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return <InfoStandWidgetUserView avatarInfo={ (avatarInfo as AvatarInfoUser) } setAvatarInfo={ setAvatarInfo } onClose={ () => setAvatarInfo(null) } />;
|
||||
case AvatarInfoUser.BOT:
|
||||
return <InfoStandWidgetBotView avatarInfo={ (avatarInfo as AvatarInfoUser) } onClose={ () => setAvatarInfo(null) } />;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CrackableDataType, CreateLinkEvent, FurnitureFloorUpdateEvent, GetRoomEngine, GetSessionDataManager, GetSoundManager, GroupInformationComposer, GroupInformationEvent, NowPlayingEvent, RoomControllerLevel, RoomObjectCategory, RoomObjectOperationType, RoomObjectVariable, RoomWidgetEnumItemExtradataParameter, RoomWidgetFurniInfoUsagePolicyEnum, SetObjectDataMessageComposer, SongInfoReceivedEvent, StringDataType, UpdateFurniturePositionComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { FaCrosshairs, FaRulerVertical, FaTimes } from 'react-icons/fa';
|
||||
import { FaCrosshairs, FaTimes } from 'react-icons/fa';
|
||||
import { GrFormNextLink, GrRotateLeft, GrRotateRight } from 'react-icons/gr';
|
||||
import { AvatarInfoFurni, GetGroupInformation, LocalizeText, SendMessageComposer } from '../../../../../api';
|
||||
import { Button, Column, Flex, LayoutBadgeImageView, LayoutLimitedEditionCompactPlateView, LayoutRarityLevelView, LayoutRoomObjectImageView, Text, UserProfileIconView } from '../../../../../common';
|
||||
@@ -487,17 +487,23 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
|
||||
<div className="absolute inset-e-0">
|
||||
<LayoutRarityLevelView level={ avatarInfo.stuffData.rarityLevel } />
|
||||
</div> }
|
||||
<Flex center fullWidth>
|
||||
<LayoutRoomObjectImageView category={ avatarInfo.category } objectId={ avatarInfo.id } roomId={ roomSession.roomId } />
|
||||
<Flex center fullWidth className="min-h-[74px] max-h-[86px] overflow-hidden">
|
||||
<LayoutRoomObjectImageView
|
||||
category={ avatarInfo.category }
|
||||
objectId={ avatarInfo.id }
|
||||
roomId={ roomSession.roomId }
|
||||
style={ {
|
||||
maxWidth: 120,
|
||||
maxHeight: 82,
|
||||
backgroundSize: 'contain',
|
||||
backgroundPosition: 'center',
|
||||
backgroundRepeat: 'no-repeat'
|
||||
} } />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<hr className="m-0 bg-[#0003] border-0 opacity-[.5] h-px" />
|
||||
</div>
|
||||
}
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text fullWidth small textBreak wrap variant="white">{ avatarInfo.description }</Text>
|
||||
<hr className="m-0 bg-[#0003] border-0 opacity-[.5] h-px" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-1">
|
||||
{ showOwnerProfileIcon && <UserProfileIconView userId={ avatarInfo.ownerId } /> }
|
||||
@@ -551,13 +557,9 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
|
||||
{ (itemLocation.x > -1) &&
|
||||
<>
|
||||
<hr className="m-0 bg-[#0003] border-0 opacity-[.5] h-px" />
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex items-center gap-1 min-w-0">
|
||||
<FaCrosshairs className="fa-icon shrink-0" />
|
||||
<Text small wrap variant="white">X: { itemLocation.x } · Y: { itemLocation.y }</Text>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<FaRulerVertical className="fa-icon shrink-0" />
|
||||
<Text small wrap variant="white">{ LocalizeText('stack.magic.tile.height.label') }: { itemLocation.z < 0.01 ? 0 : itemLocation.z }</Text>
|
||||
<Text small textBreak variant="white">X: { itemLocation.x } · Y: { itemLocation.y } · H: { itemLocation.z < 0.01 ? 0 : itemLocation.z }</Text>
|
||||
</div>
|
||||
</> }
|
||||
{ godMode &&
|
||||
|
||||
@@ -208,7 +208,7 @@ export const AvatarInfoWidgetAvatarView: FC<AvatarInfoWidgetAvatarViewProps> = p
|
||||
}, [ avatarInfo ]);
|
||||
|
||||
return (
|
||||
<ContextMenuView category={ RoomObjectCategory.UNIT } collapsable={ true } objectId={ avatarInfo.roomIndex } userType={ avatarInfo.userType } onClose={ onClose }>
|
||||
<ContextMenuView category={ RoomObjectCategory.UNIT } classNames={ [ 'nitro-avatar-action-menu' ] } collapsable={ true } objectId={ avatarInfo.roomIndex } userType={ avatarInfo.userType } onClose={ onClose }>
|
||||
<ContextMenuHeaderView className="cursor-pointer" onClick={ event => GetUserProfile(avatarInfo.webID) } dangerouslySetInnerHTML={ { __html: `${ avatarInfo.name }` } }></ContextMenuHeaderView>
|
||||
{ (mode === MODE_NORMAL) &&
|
||||
<>
|
||||
|
||||
@@ -58,6 +58,9 @@ export const AvatarInfoWidgetOwnAvatarView: FC<AvatarInfoWidgetOwnAvatarViewProp
|
||||
case 'avatar_effect':
|
||||
CreateLinkEvent('avatar-effects/show');
|
||||
break;
|
||||
case 'customize_nick':
|
||||
CreateLinkEvent('customize/show');
|
||||
break;
|
||||
case 'expressions':
|
||||
hideMenu = false;
|
||||
setMode(MODE_EXPRESSIONS);
|
||||
@@ -122,7 +125,7 @@ export const AvatarInfoWidgetOwnAvatarView: FC<AvatarInfoWidgetOwnAvatarViewProp
|
||||
const isRidingHorse = IsRidingHorse();
|
||||
|
||||
return (
|
||||
<ContextMenuView category={ RoomObjectCategory.UNIT } collapsable={ true } objectId={ avatarInfo.roomIndex } userType={ avatarInfo.userType } onClose={ onClose }>
|
||||
<ContextMenuView category={ RoomObjectCategory.UNIT } classNames={ [ 'nitro-avatar-action-menu' ] } collapsable={ true } objectId={ avatarInfo.roomIndex } userType={ avatarInfo.userType } onClose={ onClose }>
|
||||
|
||||
<ContextMenuHeaderView className="cursor-pointer" onClick={ event => GetUserProfile(avatarInfo.webID) }>
|
||||
{ avatarInfo.name }
|
||||
@@ -143,6 +146,9 @@ export const AvatarInfoWidgetOwnAvatarView: FC<AvatarInfoWidgetOwnAvatarViewProp
|
||||
<ContextMenuListItemView onClick={ event => processAction('avatar_effect') }>
|
||||
{ LocalizeText('product.type.effect') }
|
||||
</ContextMenuListItemView>
|
||||
<ContextMenuListItemView onClick={ event => processAction('customize_nick') }>
|
||||
Nick Custom
|
||||
</ContextMenuListItemView>
|
||||
{ (HasHabboClub() && !isRidingHorse) &&
|
||||
<ContextMenuListItemView onClick={ event => processAction('dance_menu') }>
|
||||
<FaChevronRight className="right fa-icon" />
|
||||
|
||||
@@ -12,7 +12,7 @@ export const ContextMenuCaretView: FC<CaretViewProps> = props =>
|
||||
|
||||
const getClassNames = useMemo(() =>
|
||||
{
|
||||
const newClassNames: string[] = [ 'menu-footer' ];
|
||||
const newClassNames: string[] = [ 'menu-footer nitro-context-menu-footer' ];
|
||||
|
||||
if(classNames.length) newClassNames.push(...classNames);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ export const ContextMenuHeaderView: FC<FlexProps> = props =>
|
||||
|
||||
const getClassNames = useMemo(() =>
|
||||
{
|
||||
const newClassNames: string[] = [ 'bg-[#3d5f6e] text-[#fff] min-w-[117px] h-[25px] max-h-[25px] text-[16px] mb-[2px]', 'p-1' ];
|
||||
const newClassNames: string[] = [ 'nitro-context-menu-header', 'bg-[#3d5f6e] text-[#fff] min-w-[117px] h-[25px] max-h-[25px] text-[16px] mb-[2px]', 'p-1' ];
|
||||
|
||||
if(classNames.length) newClassNames.push(...classNames);
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ export const ContextMenuListItemView: FC<ContextMenuListItemViewProps> = props =
|
||||
|
||||
const getClassNames = useMemo(() =>
|
||||
{
|
||||
const newClassNames: string[] = [ 'relative mb-[2px] p-[3px] overflow-hidden', 'h-[24px] max-h-[24px] p-[3px] bg-[repeating-linear-gradient(#131e25,#131e25_50%,#0d171d_50%,#0d171d_100%)] cursor-pointer' ];
|
||||
const newClassNames: string[] = [ 'nitro-context-menu-item', 'relative mb-[2px] p-[3px] overflow-hidden', 'h-[24px] max-h-[24px] p-[3px] bg-[repeating-linear-gradient(#131e25,#131e25_50%,#0d171d_50%,#0d171d_100%)] cursor-pointer' ];
|
||||
|
||||
if(disabled) newClassNames.push('disabled');
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ export const ContextMenuView: FC<ContextMenuViewProps> = ({
|
||||
|
||||
const getClassNames = useMemo(() => {
|
||||
const classes = [
|
||||
'nitro-context-menu',
|
||||
'p-[2px]!',
|
||||
'bg-[#1c323f]',
|
||||
'border-2',
|
||||
|
||||
@@ -25,6 +25,7 @@ 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 [ useGuideTool, setUseGuideTool ] = useState(false);
|
||||
const [ youtubeEnabled, setYoutubeEnabled ] = useState(false);
|
||||
const { userFigure = null } = useSessionInfo();
|
||||
@@ -36,6 +37,14 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
||||
const isMod = GetSessionDataManager().isModerator;
|
||||
const hasDesktopUnifiedShell = (isInRoom && isToolbarOpen);
|
||||
const showDesktopShell = (isToolbarOpen || !isInRoom);
|
||||
const desktopToolbarFrameClasses = isTouchLayout ? '' : '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]';
|
||||
const desktopToolbarOpenClasses = isTouchLayout ? '' : 'md:rounded-none md:border-0 md:bg-transparent md:shadow-none';
|
||||
const desktopToggleClasses = isTouchLayout ? '' : 'md:mb-0';
|
||||
const desktopToggleIconClasses = isTouchLayout ? '' : (isToolbarOpen ? 'md:-rotate-90' : 'md:rotate-90');
|
||||
const desktopChatInputClasses = isTouchLayout ? '' : 'md:px-0';
|
||||
const mobileOnlyClasses = isTouchLayout ? '' : 'md:hidden';
|
||||
const desktopBlockClasses = isTouchLayout ? 'hidden' : 'hidden md:block';
|
||||
const desktopFlexClasses = isTouchLayout ? 'hidden' : 'hidden md:flex';
|
||||
|
||||
useMessageEvent<YouTubeRoomSettingsEvent>(YouTubeRoomSettingsEvent, event =>
|
||||
{
|
||||
@@ -53,6 +62,17 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
||||
}
|
||||
}, [ isInRoom ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const query = window.matchMedia('(pointer: coarse), (hover: none)');
|
||||
const updateTouchLayout = () => setIsTouchLayout(query.matches);
|
||||
|
||||
updateTouchLayout();
|
||||
query.addEventListener('change', updateTouchLayout);
|
||||
|
||||
return () => query.removeEventListener('change', updateTouchLayout);
|
||||
}, []);
|
||||
|
||||
const openYouTubePlayer = () => window.dispatchEvent(new CustomEvent('youtube:toggle'));
|
||||
|
||||
useMessageEvent<PerkAllowancesMessageEvent>(PerkAllowancesMessageEvent, event =>
|
||||
@@ -103,13 +123,13 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
||||
{ youtubeEnabled && <YouTubePlayerView /> }
|
||||
|
||||
{ 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' }` }>
|
||||
<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 ${ desktopToolbarFrameClasses } ${ isToolbarOpen ? (hasDesktopUnifiedShell ? `${ desktopToolbarOpenClasses } 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 ${ desktopToolbarOpenClasses }` }` }>
|
||||
<motion.div
|
||||
className="tb-toggle pointer-events-auto mr-2 mb-[4px] flex-shrink-0 md:mb-0"
|
||||
className={ `tb-toggle pointer-events-auto mr-2 mb-[4px] flex-shrink-0 ${ desktopToggleClasses }` }
|
||||
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' }` }
|
||||
className={ `h-3.5 w-3.5 text-white/70 transition-transform duration-300 ${ isToolbarOpen ? 'rotate-180' : 'rotate-0' } ${ desktopToggleIconClasses }` }
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
@@ -119,9 +139,9 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
||||
<Flex
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
className="pointer-events-auto h-full w-full min-w-0 flex-1 px-[6px] md:px-0"
|
||||
className={ `pointer-events-auto h-full w-full min-w-0 flex-1 px-[6px] ${ desktopChatInputClasses }` }
|
||||
id="toolbar-chat-input-container" />
|
||||
<div className="pointer-events-auto relative mr-[6px] shrink-0 md:hidden">
|
||||
<div className={ `pointer-events-auto relative mr-[6px] shrink-0 ${ mobileOnlyClasses }` }>
|
||||
<ToolbarItemView icon="friendall" onClick={ () => CreateLinkEvent('friends/toggle') } className="tb-icon" />
|
||||
{ (requests.length > 0) &&
|
||||
<LayoutItemCountView count={ requests.length } className="absolute -right-1 top-0" /> }
|
||||
@@ -138,7 +158,7 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
||||
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" /> }
|
||||
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(10,10,12,0.58)] shadow-[0_-6px_18px_rgba(0,0,0,0.18)] ${ desktopBlockClasses }` } /> }
|
||||
|
||||
<motion.div
|
||||
key="left-nav"
|
||||
@@ -146,7 +166,7 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
||||
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">
|
||||
className={ `fixed bottom-0 left-0 z-40 h-[52px] max-w-[calc(50vw-242px)] items-center overflow-visible pl-3 pointer-events-auto ${ desktopFlexClasses }` }>
|
||||
<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
|
||||
@@ -218,7 +238,7 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
||||
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' }` }>
|
||||
className={ `fixed bottom-0 z-40 h-[52px] max-w-[calc(50vw-242px)] items-center overflow-visible pr-3 pointer-events-auto ${ desktopFlexClasses } ${ 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" />
|
||||
@@ -229,8 +249,8 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
||||
<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" />
|
||||
<div className={ `mx-1 h-5 w-[1px] bg-white/20 ${ desktopBlockClasses }` } />
|
||||
<div className={ `h-full shrink-0 ${ desktopBlockClasses }` } id="toolbar-friend-bar-container-desktop" />
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
@@ -240,7 +260,7 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
||||
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' }` }>
|
||||
className={ `fixed left-1/2 z-40 flex w-[95vw] -translate-x-1/2 items-center overflow-visible pointer-events-auto ${ mobileOnlyClasses } ${ 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
|
||||
|
||||
@@ -166,7 +166,7 @@ export const useAvailableUserSources = (trigger: Triggerable, userSources: Wired
|
||||
|
||||
if(!trigger) return;
|
||||
|
||||
const intervalId = window.setInterval(refreshStackSources, 100);
|
||||
const intervalId = window.setInterval(refreshStackSources, 1000);
|
||||
|
||||
return () => window.clearInterval(intervalId);
|
||||
}, [ refreshStackSources, trigger ]);
|
||||
|
||||
Reference in New Issue
Block a user