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.
This commit is contained in:
simoleo89
2026-05-19 21:40:11 +02:00
parent 73b9f9319d
commit 888a6a3255
@@ -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<AvatarInfoWidgetAvatarViewProps> = 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>(FlatControllerAddedEvent, event =>
{
const parser = event.getParser();
if(!parser || (parser.data.userId !== avatarInfo.webID)) return;
setControllerLevel(RoomControllerLevel.GUEST);
});
useMessageEvent<FlatControllerRemovedEvent>(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<AvatarInfoWidgetAvatarViewProps> = 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<AvatarInfoWidgetAvatarViewProps> = p
useEffect(() =>
{
setMode(MODE_NORMAL);
setControllerLevel(avatarInfo.targetRoomControllerLevel);
}, [ avatarInfo ]);
return (