diff --git a/src/hooks/session/index.ts b/src/hooks/session/index.ts index e61c7f3..6cd3e40 100644 --- a/src/hooks/session/index.ts +++ b/src/hooks/session/index.ts @@ -1 +1,2 @@ export * from './useSessionInfo'; +export * from './useSessionSnapshots'; diff --git a/src/hooks/session/useSessionSnapshots.ts b/src/hooks/session/useSessionSnapshots.ts new file mode 100644 index 0000000..ebc1075 --- /dev/null +++ b/src/hooks/session/useSessionSnapshots.ts @@ -0,0 +1,113 @@ +import { GetEventDispatcher, GetRoomSessionManager, GetSessionDataManager, GetSoundManager, IRoomSessionSnapshot, IRoomUserData, ISoundVolumesSnapshot, IUserDataSnapshot, NitroEventType } from '@nitrots/nitro-renderer'; +import { useMemo } from 'react'; +import { useExternalSnapshot } from '../events/useExternalSnapshot'; + +/** + * React-side consumers for the referentially-stable snapshot getters + * the renderer exposes (Nitro_Render_V3 v2.1.0+ pattern). + * + * Every hook here is a thin `useSyncExternalStore` wrapper: it subscribes + * to the corresponding `NitroEventType.*_UPDATED` invalidation event and + * reads the matching `getXxxSnapshot()`. Because the renderer guarantees + * snapshot reference invariance until invalidation, React's bailout logic + * skips re-renders when the snapshot is unchanged — so widgets that read + * the same slice across many components share a single subscription and + * only re-paint when the underlying state actually changes. + * + * Prefer these over reaching into the manager directly with + * `GetSessionDataManager().userId` etc., which never trigger a re-render + * when the value changes. + */ + +const subscribeTo = (eventType: string) => (onChange: () => void) => + GetEventDispatcher().subscribe(eventType, onChange); + +export const useUserDataSnapshot = (): Readonly => + useExternalSnapshot( + subscribeTo(NitroEventType.SESSION_DATA_UPDATED), + () => GetSessionDataManager().getUserDataSnapshot() + ); + +export const useActiveRoomSessionSnapshot = (): Readonly | null => + useExternalSnapshot( + subscribeTo(NitroEventType.ROOM_SESSION_UPDATED), + () => GetRoomSessionManager().getActiveRoomSessionSnapshot() + ); + +export const useIgnoredUsersSnapshot = (): ReadonlyArray => + useExternalSnapshot( + subscribeTo(NitroEventType.IGNORED_USERS_UPDATED), + () => GetSessionDataManager().ignoredUsersManager.getIgnoredUsersSnapshot() + ); + +/** + * Reactive predicate built on top of `useIgnoredUsersSnapshot`. + * Re-renders only when the array reference flips (i.e. someone is added + * or removed) — not on unrelated session updates. + */ +export const useIsUserIgnored = (name: string): boolean => +{ + const list = useIgnoredUsersSnapshot(); + + return useMemo(() => list.includes(name), [ list, name ]); +}; + +export const useGroupBadgesSnapshot = (): ReadonlyMap => + useExternalSnapshot( + subscribeTo(NitroEventType.GROUP_BADGES_UPDATED), + () => GetSessionDataManager().groupInformationManager.getGroupBadgesSnapshot() + ); + +/** + * Returns the badge id for a given group, reactive. Empty string when + * the badge isn't known (matches the legacy `getGroupBadge` fallback). + */ +export const useGroupBadge = (groupId: number): string => +{ + const badges = useGroupBadgesSnapshot(); + + return useMemo(() => badges.get(groupId) ?? '', [ badges, groupId ]); +}; + +export const useVolumesSnapshot = (): Readonly => + useExternalSnapshot( + subscribeTo(NitroEventType.SOUND_VOLUMES_UPDATED), + () => GetSoundManager().getVolumesSnapshot() + ); + +/** + * Returns the active room's user list, reactive. Returns an empty + * frozen array when no room session is active (matches the renderer's + * "no active session" shape). + * + * The room session itself is read via the active-room snapshot — + * `ROOM_USER_LIST_UPDATED` fires on user join/leave/update inside the + * active session, but the underlying `userDataManager` reference + * follows whichever session is current, so we re-resolve it on every + * snapshot read. The empty-array fallback is also frozen so consumers + * relying on referential stability don't accidentally trigger renders + * by getting a fresh `[]` each call when no session is active. + */ +const EMPTY_USER_LIST = Object.freeze([]) as ReadonlyArray; + +export const useRoomUserListSnapshot = (): ReadonlyArray => + useExternalSnapshot( + // Subscribe to BOTH events: ROOM_USER_LIST_UPDATED fires for + // join/leave/update inside the active session, but + // ROOM_SESSION_UPDATED fires when the active session itself + // changes (room change) — and the underlying `userDataManager` + // reference flips with it, so we need to re-read. + (onChange) => + { + const dispatcher = GetEventDispatcher(); + const offList = dispatcher.subscribe(NitroEventType.ROOM_USER_LIST_UPDATED, onChange); + const offSession = dispatcher.subscribe(NitroEventType.ROOM_SESSION_UPDATED, onChange); + + return () => + { + offList(); + offSession(); + }; + }, + () => GetRoomSessionManager().getActiveRoomSessionSnapshot()?.session?.userDataManager?.getRoomUserListSnapshot() ?? EMPTY_USER_LIST + );