mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
feat(hooks): generalise security-level family + audit catch + reactivity test
Build on the useIsModerator landing (532cb28c) along three axes: 1. Family. Extract `useHasSecurityLevel(min)` as the primitive, backed by a fresh `useUserSecurityLevel()` raw-level reader. The six SecurityLevel constants (1..9) deserve named wrappers so the "show this only to X-and-up" pattern doesn't get re-derived ad-hoc each time: shipped `useIsModerator` / `useIsPlayerSupport` / `useIsCommunity` / `useIsAdmin` as one-line shims. Also added `useIsAmbassador()` as a sibling — not derived from security level, reads the boolean field on the snapshot directly. 2. Audit. The532cb28cmigration covered 6 React-render reads but missed 5 more discovered by a follow-up grep: - FurniEditorView (top-level `const isMod`) - InfoStandWidgetFurniView (inline JSX, mod-only build-tools button) - NavigatorRoomInfoView (3 reads in hasPermission(): isModerator and securityLevel >= COMMUNITY for the staff-pick gate. The userId read stays imperative — userId doesn't flip at runtime in practice, no reactivity gain.) - AvatarInfoWidgetPetView (inside useMemo with [roomSession] deps; migrated and isModerator added to the deps so a runtime promote/demote re-derives canPickUp without remount) - FurnitureMannequinView (inside useEffect; same treatment — added isModerator to the deps so the mode re-resolves on flip) The remaining ~17 reads (CanManipulateFurniture, AvatarInfoUtilities.populate*, useChatInputActions, useFurnitureDimmerWidget / useFurniturePlaylistEditorWidget / useFurnitureStickieWidget canModify checks, useCatalog admin filter, useNavigator door-mode guard) are click-time / event-time imperative — they read at the moment a user action fires, so a reactive value would be cached at hook execution and stale by the time the action runs. Leaving them on the synchronous manager read is correct. 3. Test. Added four cases pinning the contract: - useUserSecurityLevel returns the raw level. - useHasSecurityLevel does `>=` against the threshold. - Named wrappers map to the right constants (MODERATOR=5, COMMUNITY=7, ADMINISTRATOR=8). - **Reactive flip** — mutate the snapshot, dispatch the SESSION_DATA_UPDATED event on the mock dispatcher, assert the hook re-derives. Locks in the whole point of the snapshot pattern (a static read would pass cases 1-3 but fail case 4). Mock changes: - Added SecurityLevel class (mirrors the renderer enum 0..9) so the family wrappers resolve to actual numbers in jsdom — without it `useIsModerator()` would call `useHasSecurityLevel(undefined)` and the test would silently pass false-positives. Verification: yarn typecheck clean, yarn lint:hooks clean, yarn test 213/213 (209 baseline + 4 new family/reactivity cases).
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { GetSessionDataManager } from '../../api';
|
||||
import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common';
|
||||
import { useIsModerator } from '../../hooks';
|
||||
import { useFurniEditor } from '../../hooks/furni-editor';
|
||||
import { FurniEditorEditView } from './views/FurniEditorEditView';
|
||||
import { FurniEditorSearchView } from './views/FurniEditorSearchView';
|
||||
@@ -21,7 +21,7 @@ export const FurniEditorView: FC<{}> = () =>
|
||||
searchItems, loadDetail, loadBySpriteId, updateItem, deleteItem, loadInteractions
|
||||
} = useFurniEditor();
|
||||
|
||||
const isMod = GetSessionDataManager()?.isModerator;
|
||||
const isMod = useIsModerator();
|
||||
|
||||
// Auto-switch to edit tab when an item is selected
|
||||
useEffect(() =>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { CreateLinkEvent, GetCustomRoomFilterMessageComposer, GetGuestRoomMessageComposer, GetSessionDataManager, NavigatorSearchComposer, RemoveOwnRoomRightsRoomMessageComposer, RoomControllerLevel, RoomMuteComposer, RoomSettingsComposer, SecurityLevel, ToggleStaffPickMessageComposer, UpdateHomeRoomMessageComposer } from '@nitrots/nitro-renderer';
|
||||
import { CreateLinkEvent, GetCustomRoomFilterMessageComposer, GetGuestRoomMessageComposer, GetSessionDataManager, NavigatorSearchComposer, RemoveOwnRoomRightsRoomMessageComposer, RoomControllerLevel, RoomMuteComposer, RoomSettingsComposer, ToggleStaffPickMessageComposer, UpdateHomeRoomMessageComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useMemo, useState } from 'react';
|
||||
import { FaLink, FaSignOutAlt } from 'react-icons/fa';
|
||||
import { DispatchUiEvent, GetGroupInformation, LocalizeText, ReportType, SendMessageComposer, ToggleFavoriteRoom } from '../../../api';
|
||||
import { Button, Column, Flex, LayoutBadgeImageView, LayoutRoomThumbnailView, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text, UserProfileIconView } from '../../../common';
|
||||
import { RoomWidgetThumbnailEvent } from '../../../events';
|
||||
import { useHelp, useNavigator, useRoom } from '../../../hooks';
|
||||
import { useHelp, useIsCommunity, useIsModerator, useNavigator, useRoom } from '../../../hooks';
|
||||
import { classNames } from '../../../layout';
|
||||
|
||||
export interface NavigatorRoomInfoViewProps {
|
||||
@@ -19,6 +19,8 @@ export const NavigatorRoomInfoView: FC<NavigatorRoomInfoViewProps> = props =>
|
||||
const { report = null } = useHelp();
|
||||
const { navigatorData = null, favouriteRoomIds = [] } = useNavigator();
|
||||
const { roomSession = null } = useRoom();
|
||||
const isModerator = useIsModerator();
|
||||
const isCommunity = useIsCommunity();
|
||||
|
||||
const enteredRoomId = navigatorData?.enteredGuestRoom?.roomId ?? 0;
|
||||
|
||||
@@ -51,9 +53,9 @@ export const NavigatorRoomInfoView: FC<NavigatorRoomInfoViewProps> = props =>
|
||||
switch(permission)
|
||||
{
|
||||
case 'settings':
|
||||
return (GetSessionDataManager().userId === navigatorData.enteredGuestRoom.ownerId || GetSessionDataManager().isModerator);
|
||||
return (GetSessionDataManager().userId === navigatorData.enteredGuestRoom.ownerId || isModerator);
|
||||
case 'staff_pick':
|
||||
return GetSessionDataManager().securityLevel >= SecurityLevel.COMMUNITY;
|
||||
return isCommunity;
|
||||
case 'floor':
|
||||
return roomSession?.controllerLevel >= RoomControllerLevel.GUEST;
|
||||
case 'guest':
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
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 { CrackableDataType, CreateLinkEvent, FurnitureFloorUpdateEvent, GetRoomEngine, 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, 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';
|
||||
import { useMessageEvent, useNitroEvent, useRoom, useWiredTools } from '../../../../../hooks';
|
||||
import { useIsModerator, useMessageEvent, useNitroEvent, useRoom, useWiredTools } from '../../../../../hooks';
|
||||
import { NitroInput } from '../../../../../layout';
|
||||
|
||||
interface InfoStandWidgetFurniViewProps
|
||||
@@ -22,6 +22,7 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
|
||||
const { avatarInfo = null, onClose = null } = props;
|
||||
const { roomSession = null } = useRoom();
|
||||
const { openInspectionForFurni, showInspectButton } = useWiredTools();
|
||||
const isModerator = useIsModerator();
|
||||
|
||||
const [ pickupMode, setPickupMode ] = useState(0);
|
||||
const [ canMove, setCanMove ] = useState(false);
|
||||
@@ -590,7 +591,7 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
|
||||
onClick={ () => setDropdownOpen(!dropdownOpen) }>
|
||||
{ dropdownOpen ? `${LocalizeText('widget.furni.present.close')} Buildtools` : `${LocalizeText('navigator.roomsettings.doormode.open')} Buildtools` }
|
||||
</button>
|
||||
{ GetSessionDataManager().isModerator &&
|
||||
{ isModerator &&
|
||||
<button
|
||||
className="w-full text-white text-xs bg-[#1e7295] hover:bg-[#1a617f] border border-[#ffffff33] rounded px-2 py-1 cursor-pointer transition-colors"
|
||||
onClick={ () =>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { GetSessionDataManager, PetRespectComposer, PetType, RoomControllerLevel, RoomObjectCategory, RoomObjectType, RoomObjectVariable, RoomUnitGiveHandItemPetComposer } from '@nitrots/nitro-renderer';
|
||||
import { PetRespectComposer, PetType, RoomControllerLevel, RoomObjectCategory, RoomObjectType, RoomObjectVariable, RoomUnitGiveHandItemPetComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useMemo, useState } from 'react';
|
||||
import { AvatarInfoPet, GetOwnRoomObject, LocalizeText, SendMessageComposer } from '../../../../../api';
|
||||
import { useRoom, useSessionInfo } from '../../../../../hooks';
|
||||
import { useIsModerator, useRoom, useSessionInfo } from '../../../../../hooks';
|
||||
import { ContextMenuHeaderView } from '../../context-menu/ContextMenuHeaderView';
|
||||
import { ContextMenuListItemView } from '../../context-menu/ContextMenuListItemView';
|
||||
import { ContextMenuView } from '../../context-menu/ContextMenuView';
|
||||
@@ -23,11 +23,12 @@ export const AvatarInfoWidgetPetView: FC<AvatarInfoWidgetPetViewProps> = props =
|
||||
const [ mode, setMode ] = useState(MODE_NORMAL);
|
||||
const { roomSession = null, isHandItemBlocked = false } = useRoom();
|
||||
const { petRespectRemaining = 0, respectPet = null } = useSessionInfo();
|
||||
const isModerator = useIsModerator();
|
||||
|
||||
const canPickUp = useMemo(() =>
|
||||
{
|
||||
return (roomSession.isRoomOwner || (roomSession.controllerLevel >= RoomControllerLevel.GUEST) || GetSessionDataManager().isModerator);
|
||||
}, [ roomSession ]);
|
||||
return (roomSession.isRoomOwner || (roomSession.controllerLevel >= RoomControllerLevel.GUEST) || isModerator);
|
||||
}, [ roomSession, isModerator ]);
|
||||
|
||||
const canGiveHandItem = useMemo(() =>
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@ import { GetAvatarRenderManager, GetSessionDataManager, HabboClubLevelEnum, Room
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { GetClubMemberLevel, GetRoomSession, LocalizeText, MannequinUtilities } from '../../../../api';
|
||||
import { Button, Column, LayoutAvatarImageView, LayoutCurrencyIcon, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
|
||||
import { useFurnitureMannequinWidget } from '../../../../hooks';
|
||||
import { useFurnitureMannequinWidget, useIsModerator } from '../../../../hooks';
|
||||
import { NitroInput } from '../../../../layout';
|
||||
|
||||
const MODE_NONE: number = -1;
|
||||
@@ -17,6 +17,7 @@ export const FurnitureMannequinView: FC<{}> = props =>
|
||||
const [ renderedFigure, setRenderedFigure ] = useState<string>(null);
|
||||
const [ mode, setMode ] = useState(MODE_NONE);
|
||||
const { objectId = -1, figure = null, gender = null, clubLevel = HabboClubLevelEnum.NO_CLUB, name = null, setName = null, saveFigure = null, wearFigure = null, saveName = null, onClose = null } = useFurnitureMannequinWidget();
|
||||
const isModerator = useIsModerator();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
@@ -24,7 +25,7 @@ export const FurnitureMannequinView: FC<{}> = props =>
|
||||
|
||||
const roomSession = GetRoomSession();
|
||||
|
||||
if(roomSession.isRoomOwner || (roomSession.controllerLevel >= RoomControllerLevel.GUEST) || GetSessionDataManager().isModerator)
|
||||
if(roomSession.isRoomOwner || (roomSession.controllerLevel >= RoomControllerLevel.GUEST) || isModerator)
|
||||
{
|
||||
setMode(MODE_CONTROLLER);
|
||||
|
||||
@@ -46,7 +47,7 @@ export const FurnitureMannequinView: FC<{}> = props =>
|
||||
}
|
||||
|
||||
setMode(MODE_PEER);
|
||||
}, [ objectId, gender, clubLevel ]);
|
||||
}, [ objectId, gender, clubLevel, isModerator ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { cleanup, render, renderHook } from '@testing-library/react';
|
||||
import { act, cleanup, render, renderHook } from '@testing-library/react';
|
||||
import { Component, ReactNode, useSyncExternalStore } from 'react';
|
||||
import { useBetween } from 'use-between';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { GetEventDispatcher, GetSessionDataManager } from '../../nitro-renderer.mock';
|
||||
import { useHasSecurityLevel, useIsAdmin, useIsCommunity, useIsModerator, useUserSecurityLevel } from './useSessionSnapshots';
|
||||
|
||||
// Regression guard for the rolled-back snapshot-consumer migration.
|
||||
//
|
||||
@@ -106,3 +108,113 @@ describe('use-between + useSyncExternalStore incompatibility', () =>
|
||||
expect(result.current.count).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// useHasSecurityLevel + named wrappers — reactive flip on snapshot invalidation
|
||||
// ============================================================================
|
||||
//
|
||||
// The family hangs off useUserDataSnapshot() which is a useSyncExternalStore
|
||||
// wrapper. The renderer's real SessionDataManager pushes a frozen snapshot
|
||||
// out of getUserDataSnapshot() and dispatches a SESSION_DATA_UPDATED event
|
||||
// whenever a mutator invalidates the cache. These tests fake both sides:
|
||||
// a mock dispatcher with a real .subscribe(), and a mock SessionDataManager
|
||||
// whose snapshot can be mutated between dispatches.
|
||||
|
||||
const makeFakeDispatcher = () =>
|
||||
{
|
||||
const listeners = new Map<string, Set<() => void>>();
|
||||
|
||||
return {
|
||||
subscribe(type: string, cb: () => void): () => void
|
||||
{
|
||||
let bucket = listeners.get(type);
|
||||
if(!bucket)
|
||||
{
|
||||
bucket = new Set();
|
||||
listeners.set(type, bucket);
|
||||
}
|
||||
bucket.add(cb);
|
||||
return () => bucket!.delete(cb);
|
||||
},
|
||||
dispatch(type: string): void
|
||||
{
|
||||
listeners.get(type)?.forEach(cb => cb());
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
describe('useHasSecurityLevel + named wrappers', () =>
|
||||
{
|
||||
let snapshot: { securityLevel: number };
|
||||
let fakeDispatcher: ReturnType<typeof makeFakeDispatcher>;
|
||||
|
||||
beforeEach(() =>
|
||||
{
|
||||
snapshot = { securityLevel: 0 };
|
||||
fakeDispatcher = makeFakeDispatcher();
|
||||
|
||||
vi.mocked(GetSessionDataManager).mockReturnValue({
|
||||
// useSessionSnapshots reads getUserDataSnapshot() and guards on
|
||||
// `typeof manager.getUserDataSnapshot !== 'function'`, so we
|
||||
// expose it as a real function returning the mutable test snapshot.
|
||||
getUserDataSnapshot: () => snapshot
|
||||
} as any);
|
||||
|
||||
vi.mocked(GetEventDispatcher).mockReturnValue(fakeDispatcher as any);
|
||||
});
|
||||
|
||||
afterEach(() =>
|
||||
{
|
||||
cleanup();
|
||||
vi.mocked(GetSessionDataManager).mockReset();
|
||||
vi.mocked(GetEventDispatcher).mockReset();
|
||||
});
|
||||
|
||||
it('useUserSecurityLevel reads the raw level', () =>
|
||||
{
|
||||
snapshot = { securityLevel: 7 };
|
||||
const { result } = renderHook(() => useUserSecurityLevel());
|
||||
expect(result.current).toBe(7);
|
||||
});
|
||||
|
||||
it('useHasSecurityLevel compares >= the threshold', () =>
|
||||
{
|
||||
snapshot = { securityLevel: 5 };
|
||||
const { result } = renderHook(() => useHasSecurityLevel(5));
|
||||
expect(result.current).toBe(true);
|
||||
|
||||
const { result: lowResult } = renderHook(() => useHasSecurityLevel(8));
|
||||
expect(lowResult.current).toBe(false);
|
||||
});
|
||||
|
||||
it('named wrappers map to the right thresholds (MODERATOR=5, COMMUNITY=7, ADMINISTRATOR=8)', () =>
|
||||
{
|
||||
snapshot = { securityLevel: 7 }; // COMMUNITY
|
||||
|
||||
expect(renderHook(() => useIsModerator()).result.current).toBe(true); // 7 >= 5
|
||||
expect(renderHook(() => useIsCommunity()).result.current).toBe(true); // 7 >= 7
|
||||
expect(renderHook(() => useIsAdmin()).result.current).toBe(false); // 7 < 8
|
||||
});
|
||||
|
||||
it('re-renders when SESSION_DATA_UPDATED fires after the snapshot mutates', () =>
|
||||
{
|
||||
snapshot = { securityLevel: 0 };
|
||||
const { result } = renderHook(() => useIsModerator());
|
||||
expect(result.current).toBe(false);
|
||||
|
||||
// Mutate the snapshot reference (renderer invariant: every
|
||||
// invalidation produces a NEW frozen object) and dispatch the
|
||||
// event. The hook's getSnapshot closure reads `snapshot`, so a
|
||||
// fresh object reference flips React's bailout.
|
||||
act(() =>
|
||||
{
|
||||
snapshot = { securityLevel: 5 };
|
||||
// The mock's NitroEventType proxy resolves any property to
|
||||
// `mock:NitroEventType:<PROP>`, so that's the wire string
|
||||
// useSessionSnapshots subscribes against.
|
||||
fakeDispatcher.dispatch('mock:NitroEventType:SESSION_DATA_UPDATED');
|
||||
});
|
||||
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -124,18 +124,38 @@ export const useIsUserIgnored = (name: string): boolean =>
|
||||
};
|
||||
|
||||
/**
|
||||
* Reactive equivalent of `GetSessionDataManager().isModerator`. Derives
|
||||
* from `useUserDataSnapshot().securityLevel` so any future
|
||||
* promote/demote that flips the SESSION_DATA_UPDATED event re-renders
|
||||
* the consumer without an F5. Mirrors the renderer-side getter at
|
||||
* `SessionDataManager.ts:684` (`securityLevel >= SecurityLevel.MODERATOR`).
|
||||
* Reactive raw security level from the user snapshot. Use this when
|
||||
* you need the numeric level (e.g. to compare against a threshold not
|
||||
* covered by the named wrappers below); for the common case of "is
|
||||
* the user at least <X>?", prefer the matching `useIsXxx` predicate.
|
||||
*/
|
||||
export const useIsModerator = (): boolean =>
|
||||
{
|
||||
const userData = useUserDataSnapshot();
|
||||
export const useUserSecurityLevel = (): number => useUserDataSnapshot().securityLevel;
|
||||
|
||||
return userData.securityLevel >= SecurityLevel.MODERATOR;
|
||||
};
|
||||
/**
|
||||
* Reactive predicate: does the current user's security level satisfy
|
||||
* `>= minLevel`? Mirrors the renderer-side comparison used by
|
||||
* `SessionDataManager.isModerator` (and its peers) and propagates the
|
||||
* SESSION_DATA_UPDATED invalidation, so a runtime promote/demote
|
||||
* re-renders the consumer.
|
||||
*
|
||||
* The named wrappers below (`useIsModerator`, `useIsAdmin`, …) are
|
||||
* one-line shims over this primitive — use them in widget bodies for
|
||||
* readability; reach for `useHasSecurityLevel(level)` directly only
|
||||
* when the threshold is dynamic or not covered by a named wrapper.
|
||||
*/
|
||||
export const useHasSecurityLevel = (minLevel: number): boolean =>
|
||||
useUserSecurityLevel() >= minLevel;
|
||||
|
||||
export const useIsModerator = (): boolean => useHasSecurityLevel(SecurityLevel.MODERATOR);
|
||||
export const useIsPlayerSupport = (): boolean => useHasSecurityLevel(SecurityLevel.PLAYER_SUPPORT);
|
||||
export const useIsCommunity = (): boolean => useHasSecurityLevel(SecurityLevel.COMMUNITY);
|
||||
export const useIsAdmin = (): boolean => useHasSecurityLevel(SecurityLevel.ADMINISTRATOR);
|
||||
|
||||
/**
|
||||
* Reactive ambassador flag. Not derived from security level — it's a
|
||||
* separate boolean on the snapshot.
|
||||
*/
|
||||
export const useIsAmbassador = (): boolean => useUserDataSnapshot().isAmbassador;
|
||||
|
||||
export const useGroupBadgesSnapshot = (): ReadonlyMap<number, string> =>
|
||||
useExternalSnapshot(
|
||||
|
||||
@@ -124,6 +124,23 @@ export class RoomControllerLevel
|
||||
static readonly MODERATOR = 5;
|
||||
}
|
||||
|
||||
// Mirrors `packages/api/src/nitro/session/enum/SecurityLevel.ts`. Tests
|
||||
// that gate behaviour on `securityLevel >= SecurityLevel.MODERATOR` (via
|
||||
// useIsModerator / useHasSecurityLevel) depend on these constants.
|
||||
export class SecurityLevel
|
||||
{
|
||||
static readonly NONE = 0;
|
||||
static readonly CELEBRITY = 1;
|
||||
static readonly PARTNER = 2;
|
||||
static readonly BUS_PARTNER = 3;
|
||||
static readonly EMPLOYEE = 4;
|
||||
static readonly MODERATOR = 5;
|
||||
static readonly PLAYER_SUPPORT = 6;
|
||||
static readonly COMMUNITY = 7;
|
||||
static readonly ADMINISTRATOR = 8;
|
||||
static readonly SUPER_USER = 9;
|
||||
}
|
||||
|
||||
export class RoomObjectCategory
|
||||
{
|
||||
static readonly MINIMUM = 0;
|
||||
|
||||
Reference in New Issue
Block a user