Refine mobile avatar widgets and login flow

This commit is contained in:
Lorenzune
2026-05-07 21:19:15 +02:00
parent 851d82f93f
commit 57b83c1097
24 changed files with 654 additions and 166 deletions
@@ -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" />