From 888a6a325507b97954cf4713a696901cdbdc7468 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 19 May 2026 21:40:11 +0200 Subject: [PATCH] feat(avatar-info): make Give/Remove Rights instantly reactive Replaced the cached `avatarInfo.targetRoomControllerLevel` derivation with a local `controllerLevel` state that: - starts from the popup-open snapshot - listens to FlatControllerAddedEvent / FlatControllerRemovedEvent filtered by avatarInfo.webID - is optimistically bumped on `give_rights` / `remove_rights` clicks so the moderate submenu flips immediately without waiting for the server roundtrip Same shape as the recent useIsUserIgnored migration: the popup now auto-flips the button without forcing the user to close+reopen it. --- .../menu/AvatarInfoWidgetAvatarView.tsx | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/src/components/room/widgets/avatar-info/menu/AvatarInfoWidgetAvatarView.tsx b/src/components/room/widgets/avatar-info/menu/AvatarInfoWidgetAvatarView.tsx index 60345eb..d5e1c19 100644 --- a/src/components/room/widgets/avatar-info/menu/AvatarInfoWidgetAvatarView.tsx +++ b/src/components/room/widgets/avatar-info/menu/AvatarInfoWidgetAvatarView.tsx @@ -1,9 +1,9 @@ -import { CreateLinkEvent, GetSessionDataManager, RoomControllerLevel, RoomObjectCategory, RoomObjectVariable, RoomUnitGiveHandItemComposer, SetRelationshipStatusComposer, TradingOpenComposer } from '@nitrots/nitro-renderer'; +import { CreateLinkEvent, FlatControllerAddedEvent, FlatControllerRemovedEvent, GetSessionDataManager, RoomControllerLevel, RoomObjectCategory, RoomObjectVariable, RoomUnitGiveHandItemComposer, SetRelationshipStatusComposer, TradingOpenComposer } from '@nitrots/nitro-renderer'; import { FC, useEffect, useMemo, useState } from 'react'; import { FaChevronLeft, FaChevronRight } from 'react-icons/fa'; import { AvatarInfoUser, DispatchUiEvent, GetOwnRoomObject, GetUserProfile, LocalizeText, MessengerFriend, ReportType, RoomWidgetUpdateChatInputContentEvent, SendMessageComposer } from '../../../../../api'; import { Flex } from '../../../../../common'; -import { useFriends, useHelp, useIsUserIgnored, useRoom, useSessionInfo, useWiredTools } from '../../../../../hooks'; +import { useFriends, useHelp, useIsUserIgnored, useMessageEvent, useRoom, useSessionInfo, useWiredTools } from '../../../../../hooks'; import { ContextMenuHeaderView } from '../../context-menu/ContextMenuHeaderView'; import { ContextMenuListItemView } from '../../context-menu/ContextMenuListItemView'; import { ContextMenuView } from '../../context-menu/ContextMenuView'; @@ -36,16 +36,39 @@ export const AvatarInfoWidgetAvatarView: FC = p // scope here) so useSyncExternalStore installs against the real // React dispatcher. const isIgnored = useIsUserIgnored(avatarInfo.name); + // Reactive controller level: starts from the cached value at popup + // open time, then updates from FlatControllerAdded/Removed events + // and from optimistic clicks so the Give/Remove Rights buttons flip + // instantly without waiting for a server roundtrip. + const [ controllerLevel, setControllerLevel ] = useState(avatarInfo.targetRoomControllerLevel); + + useMessageEvent(FlatControllerAddedEvent, event => + { + const parser = event.getParser(); + + if(!parser || (parser.data.userId !== avatarInfo.webID)) return; + + setControllerLevel(RoomControllerLevel.GUEST); + }); + + useMessageEvent(FlatControllerRemovedEvent, event => + { + const parser = event.getParser(); + + if(!parser || (parser.userId !== avatarInfo.webID)) return; + + setControllerLevel(RoomControllerLevel.NONE); + }); const isShowGiveRights = useMemo(() => { - return (avatarInfo.amIOwner && (avatarInfo.targetRoomControllerLevel < RoomControllerLevel.GUEST) && !avatarInfo.isGuildRoom); - }, [ avatarInfo ]); + return (avatarInfo.amIOwner && (controllerLevel < RoomControllerLevel.GUEST) && !avatarInfo.isGuildRoom); + }, [ avatarInfo, controllerLevel ]); const isShowRemoveRights = useMemo(() => { - return (avatarInfo.amIOwner && (avatarInfo.targetRoomControllerLevel === RoomControllerLevel.GUEST) && !avatarInfo.isGuildRoom); - }, [ avatarInfo ]); + return (avatarInfo.amIOwner && (controllerLevel === RoomControllerLevel.GUEST) && !avatarInfo.isGuildRoom); + }, [ avatarInfo, controllerLevel ]); const moderateMenuHasContent = useMemo(() => { @@ -155,9 +178,15 @@ export const AvatarInfoWidgetAvatarView: FC = p break; case 'give_rights': roomSession.sendGiveRightsMessage(avatarInfo.webID); + setControllerLevel(RoomControllerLevel.GUEST); + hideMenu = false; + setMode(MODE_MODERATE); break; case 'remove_rights': roomSession.sendTakeRightsMessage(avatarInfo.webID); + setControllerLevel(RoomControllerLevel.NONE); + hideMenu = false; + setMode(MODE_MODERATE); break; case 'trade': SendMessageComposer(new TradingOpenComposer(avatarInfo.roomIndex)); @@ -210,6 +239,7 @@ export const AvatarInfoWidgetAvatarView: FC = p useEffect(() => { setMode(MODE_NORMAL); + setControllerLevel(avatarInfo.targetRoomControllerLevel); }, [ avatarInfo ]); return (