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:
+40
-2
@@ -10,6 +10,23 @@ import { useMessageEvent, useNitroEvent } from './hooks';
|
||||
|
||||
NitroVersion.UI_VERSION = GetUIVersion();
|
||||
|
||||
const getViewportDimensions = () =>
|
||||
{
|
||||
const viewport = window.visualViewport;
|
||||
const width = Math.max(1, Math.floor(viewport?.width ?? window.innerWidth));
|
||||
const height = Math.max(1, Math.floor(viewport?.height ?? window.innerHeight));
|
||||
|
||||
return { width, height };
|
||||
};
|
||||
|
||||
const syncViewportCssVars = () =>
|
||||
{
|
||||
const { width, height } = getViewportDimensions();
|
||||
|
||||
document.documentElement.style.setProperty('--nitro-app-width', `${ width }px`);
|
||||
document.documentElement.style.setProperty('--nitro-app-height', `${ height }px`);
|
||||
};
|
||||
|
||||
const preloadUrl = async (url: string): Promise<void> =>
|
||||
{
|
||||
if(!url) return;
|
||||
@@ -268,6 +285,25 @@ export const App: FC<{}> = props =>
|
||||
return warmupPromiseRef.current;
|
||||
}, [ startRenderer ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
syncViewportCssVars();
|
||||
|
||||
const handleViewportResize = () => syncViewportCssVars();
|
||||
const viewport = window.visualViewport;
|
||||
|
||||
window.addEventListener('resize', handleViewportResize);
|
||||
viewport?.addEventListener('resize', handleViewportResize);
|
||||
viewport?.addEventListener('scroll', handleViewportResize);
|
||||
|
||||
return () =>
|
||||
{
|
||||
window.removeEventListener('resize', handleViewportResize);
|
||||
viewport?.removeEventListener('resize', handleViewportResize);
|
||||
viewport?.removeEventListener('scroll', handleViewportResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const prepare = async (width: number, height: number) =>
|
||||
@@ -370,7 +406,9 @@ export const App: FC<{}> = props =>
|
||||
}
|
||||
};
|
||||
|
||||
prepare(window.innerWidth, window.innerHeight);
|
||||
const { width, height } = getViewportDimensions();
|
||||
|
||||
prepare(width, height);
|
||||
|
||||
return () =>
|
||||
{
|
||||
@@ -380,7 +418,7 @@ export const App: FC<{}> = props =>
|
||||
}, [ prepareTrigger, startWarmup, startRenderer, tryRememberLogin, applySsoTicket, rotateRememberLogin ]);
|
||||
|
||||
return (
|
||||
<Base fit overflow="hidden" className={ !(window.devicePixelRatio % 1) && 'image-rendering-pixelated' }>
|
||||
<Base fit overflow="hidden" className={ `nitro-app-root ${ !(window.devicePixelRatio % 1) ? 'image-rendering-pixelated' : '' }` }>
|
||||
{ !isReady && !showLogin &&
|
||||
<LoadingView isError={ errorMessage.length > 0 } message={ errorMessage } homeUrl={ homeUrl } /> }
|
||||
{ !isReady && showLogin && <LoginView onAuthenticated={ handleAuthenticated } isEntering={ isEnteringHotel } /> }
|
||||
|
||||
@@ -36,6 +36,8 @@ export class AvatarInfoFurni implements IAvatarInfo
|
||||
public allowLay: boolean = false;
|
||||
public allowWalk: boolean = false;
|
||||
public teleportTargetId: number = 0;
|
||||
public spriteId: number = -1;
|
||||
public productType: string = 's';
|
||||
|
||||
constructor(public readonly type: string)
|
||||
{}
|
||||
|
||||
@@ -118,6 +118,8 @@ export class AvatarInfoUtilities
|
||||
{
|
||||
furniInfo.name = furnitureData.name;
|
||||
furniInfo.description = furnitureData.description;
|
||||
furniInfo.spriteId = furnitureData.id;
|
||||
furniInfo.productType = ((category === RoomObjectCategory.WALL) ? 'i' : 's');
|
||||
furniInfo.purchaseOfferId = furnitureData.purchaseOfferId;
|
||||
furniInfo.purchaseCouldBeUsedForBuyout = furnitureData.purchaseCouldBeUsedForBuyout;
|
||||
furniInfo.rentOfferId = furnitureData.rentOfferId;
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
import { getClientMode, installSecureFetch, secureUrl } from './secure-assets';
|
||||
|
||||
const ensureMobileViewport = () =>
|
||||
{
|
||||
let viewport = document.querySelector<HTMLMetaElement>('meta[name="viewport"]');
|
||||
|
||||
if(!viewport)
|
||||
{
|
||||
viewport = document.createElement('meta');
|
||||
viewport.name = 'viewport';
|
||||
document.head.appendChild(viewport);
|
||||
}
|
||||
|
||||
viewport.content = 'width=device-width, initial-scale=1, viewport-fit=cover';
|
||||
};
|
||||
|
||||
ensureMobileViewport();
|
||||
installSecureFetch();
|
||||
|
||||
const setBootDebug = (message: string) =>
|
||||
|
||||
@@ -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 ]);
|
||||
|
||||
@@ -45,18 +45,70 @@ body {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: #000;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
overscroll-behavior: none;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #6d7b84 #c8d0d4;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: var(--nitro-app-width, 100vw);
|
||||
height: var(--nitro-app-height, 100vh);
|
||||
min-height: var(--nitro-app-height, 100vh);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nitro-app-root {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
width: var(--nitro-app-width, 100vw) !important;
|
||||
height: var(--nitro-app-height, 100vh) !important;
|
||||
min-height: var(--nitro-app-height, 100vh) !important;
|
||||
overflow: hidden !important;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
@supports (height: 100dvh) {
|
||||
html,
|
||||
body,
|
||||
#root,
|
||||
.nitro-app-root {
|
||||
height: var(--nitro-app-height, 100dvh) !important;
|
||||
min-height: var(--nitro-app-height, 100dvh) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.image-rendering-pixelated {
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.nitro-room-touch-feedback {
|
||||
position: fixed;
|
||||
z-index: 35;
|
||||
width: 34px;
|
||||
height: 18px;
|
||||
margin-left: -17px;
|
||||
margin-top: -9px;
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
border: 2px solid rgba(255, 255, 255, 0.9);
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
transform: scale(0.35);
|
||||
opacity: 0.9;
|
||||
animation: nitroRoomTouchFeedback 0.42s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes nitroRoomTouchFeedback {
|
||||
to {
|
||||
transform: scale(1.15);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
*,
|
||||
*:focus,
|
||||
*:hover {
|
||||
|
||||
@@ -249,3 +249,127 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nitro-mobile-furni-infostand-trigger {
|
||||
pointer-events: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
padding: 2px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.55);
|
||||
border-radius: 8px;
|
||||
background: rgba(27, 40, 52, 0.92);
|
||||
box-shadow:
|
||||
inset 1px 1px 0 rgba(255, 255, 255, 0.25),
|
||||
0 4px 10px rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
|
||||
.nitro-mobile-furni-infostand-trigger .object-preview {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.nitro-mobile-user-infostand-avatar {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 54px;
|
||||
max-height: 62px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transform: translate(-50%, -50%);
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
.nitro-mobile-user-infostand-avatar-image {
|
||||
display: block;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
max-width: 54px;
|
||||
max-height: 62px;
|
||||
object-fit: contain;
|
||||
image-rendering: pixelated;
|
||||
margin: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.nitro-mobile-user-infostand-trigger {
|
||||
position: relative;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nitro-avatar-action-menu {
|
||||
min-width: 132px;
|
||||
padding: 4px !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08) !important;
|
||||
border-radius: 12px;
|
||||
background: rgba(10, 10, 12, 0.58) !important;
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.2);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nitro-avatar-action-menu .nitro-context-menu-header {
|
||||
min-width: 0;
|
||||
height: 28px;
|
||||
max-height: 28px;
|
||||
margin-bottom: 4px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 9px;
|
||||
background: rgba(10, 10, 12, 0.58);
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.nitro-avatar-action-menu .nitro-context-menu-item {
|
||||
min-height: 26px;
|
||||
max-height: none;
|
||||
margin-bottom: 2px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
text-decoration: none;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.nitro-avatar-action-menu .nitro-context-menu-item:hover {
|
||||
background: rgba(18, 18, 22, 0.72);
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.nitro-avatar-action-menu .nitro-context-menu-item.disabled {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.nitro-avatar-action-menu .nitro-context-menu-item .right.fa-icon,
|
||||
.nitro-avatar-action-menu .nitro-context-menu-item .left.fa-icon {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.nitro-avatar-action-menu .nitro-context-menu-footer {
|
||||
height: 20px;
|
||||
margin-top: 2px;
|
||||
border-radius: 8px;
|
||||
background: rgba(10, 10, 12, 0.58);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,16 @@ import { useBetween } from 'use-between';
|
||||
import { CanManipulateFurniture, DispatchUiEvent, GetRoomSession, IsFurnitureSelectionDisabled, ProcessRoomObjectOperation, RoomWidgetUpdateBackgroundColorPreviewEvent, RoomWidgetUpdateRoomObjectEvent, SetActiveRoomId, StartRoomSession } from '../../api';
|
||||
import { useMessageEvent, useNitroEvent, useUiEvent } from '../events';
|
||||
|
||||
const getViewportSize = () =>
|
||||
{
|
||||
const viewport = window.visualViewport;
|
||||
|
||||
return {
|
||||
width: Math.max(1, Math.floor(viewport?.width ?? window.innerWidth)),
|
||||
height: Math.max(1, Math.floor(viewport?.height ?? window.innerHeight))
|
||||
};
|
||||
};
|
||||
|
||||
const useRoomState = () =>
|
||||
{
|
||||
const [roomSession, setRoomSession] = useState<IRoomSession>(null);
|
||||
@@ -215,8 +225,7 @@ const useRoomState = () =>
|
||||
const roomEngine = GetRoomEngine();
|
||||
const roomId = roomSession.roomId;
|
||||
const canvasId = 1;
|
||||
const width = Math.floor(window.innerWidth);
|
||||
const height = Math.floor(window.innerHeight);
|
||||
const { width, height } = getViewportSize();
|
||||
const renderer = GetRenderer();
|
||||
|
||||
if (renderer) renderer.resize(width, height);
|
||||
@@ -266,10 +275,9 @@ const useRoomState = () =>
|
||||
|
||||
SetActiveRoomId(roomSession.roomId);
|
||||
|
||||
const resize = (event: UIEvent) =>
|
||||
const resize = () =>
|
||||
{
|
||||
const newWidth = Math.floor(window.innerWidth);
|
||||
const newHeight = Math.floor(window.innerHeight);
|
||||
const { width: newWidth, height: newHeight } = getViewportSize();
|
||||
|
||||
const offsetX = canvas.screenOffsetX - (newWidth - canvas.width) / 2;
|
||||
const offsetY = canvas.screenOffsetY - (newHeight - canvas.height) / 2;
|
||||
@@ -284,7 +292,11 @@ const useRoomState = () =>
|
||||
canvas.screenOffsetY = ~~offsetY;
|
||||
};
|
||||
|
||||
const viewport = window.visualViewport;
|
||||
|
||||
window.addEventListener('resize', resize);
|
||||
viewport?.addEventListener('resize', resize);
|
||||
viewport?.addEventListener('scroll', resize);
|
||||
|
||||
return () =>
|
||||
{
|
||||
@@ -293,6 +305,8 @@ const useRoomState = () =>
|
||||
setOriginalRoomBackgroundColor(0);
|
||||
|
||||
window.removeEventListener('resize', resize);
|
||||
viewport?.removeEventListener('resize', resize);
|
||||
viewport?.removeEventListener('scroll', resize);
|
||||
};
|
||||
}, [roomSession]);
|
||||
|
||||
|
||||
+18
-1
@@ -80,6 +80,7 @@ const textDecoder = new TextDecoder();
|
||||
let secureSessionPromise: Promise<SecureSession> = null;
|
||||
let installed = false;
|
||||
const secureResponseCache = new Map<string, Promise<Response>>();
|
||||
const SECURE_RESPONSE_CACHE_LIMIT = 128;
|
||||
let secureSessionCreatedAt = 0;
|
||||
const SECURE_SESSION_TTL_MS = 5 * 60 * 1000;
|
||||
const REKEY_ENDPOINTS = new Set([
|
||||
@@ -336,6 +337,22 @@ const cloneCachedResponse = async (responsePromise: Promise<Response>): Promise<
|
||||
return response.clone();
|
||||
};
|
||||
|
||||
const cacheSecureResponse = (cacheKey: string, responsePromise: Promise<Response>): void =>
|
||||
{
|
||||
secureResponseCache.set(cacheKey, responsePromise);
|
||||
|
||||
responsePromise.catch(() => secureResponseCache.delete(cacheKey));
|
||||
|
||||
while(secureResponseCache.size > SECURE_RESPONSE_CACHE_LIMIT)
|
||||
{
|
||||
const oldestKey = secureResponseCache.keys().next().value;
|
||||
|
||||
if(!oldestKey) break;
|
||||
|
||||
secureResponseCache.delete(oldestKey);
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeSecureCacheKey = (requestUrl: string): string =>
|
||||
{
|
||||
try
|
||||
@@ -488,7 +505,7 @@ export const installSecureFetch = (): void =>
|
||||
return response;
|
||||
})();
|
||||
|
||||
if(cacheKey) secureResponseCache.set(cacheKey, responsePromise);
|
||||
if(cacheKey) cacheSecureResponse(cacheKey, responsePromise);
|
||||
|
||||
return cloneCachedResponse(responsePromise);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user