+
setVoucher(event.target.value) } />
diff --git a/src/views/catalog/views/select-group/CatalogSelectGroupView.tsx b/src/views/catalog/views/select-group/CatalogSelectGroupView.tsx
new file mode 100644
index 00000000..8b8c58c6
--- /dev/null
+++ b/src/views/catalog/views/select-group/CatalogSelectGroupView.tsx
@@ -0,0 +1,40 @@
+import { FC } from 'react';
+import { LocalizeText } from '../../../../api';
+import { NitroLayoutButton, NitroLayoutFlex } from '../../../../layout';
+import { NitroLayoutBase } from '../../../../layout/base';
+import { useCatalogContext } from '../../context/CatalogContext';
+import { CatalogSelectGroupViewProps } from './CatalogSelectGroupView.types';
+
+export const CatalogSelectGroupView: FC
= props =>
+{
+ const { selectedGroupIndex = -1, setSelectedGroupIndex = null } = props;
+ const { catalogState = null } = useCatalogContext();
+ const { groups = null } = catalogState;
+
+ if(!groups || !groups.length)
+ {
+ return (
+
+ { LocalizeText('catalog.guild_selector.members_only') }
+
+ { LocalizeText('catalog.guild_selector.find_groups') }
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/views/catalog/views/select-group/CatalogSelectGroupView.types.ts b/src/views/catalog/views/select-group/CatalogSelectGroupView.types.ts
new file mode 100644
index 00000000..328c92e3
--- /dev/null
+++ b/src/views/catalog/views/select-group/CatalogSelectGroupView.types.ts
@@ -0,0 +1,7 @@
+import { Dispatch, SetStateAction } from 'react';
+
+export interface CatalogSelectGroupViewProps
+{
+ selectedGroupIndex: number;
+ setSelectedGroupIndex: Dispatch>;
+}
diff --git a/src/views/chat-history/ChatHistoryMessageHandler.tsx b/src/views/chat-history/ChatHistoryMessageHandler.tsx
new file mode 100644
index 00000000..a482a36a
--- /dev/null
+++ b/src/views/chat-history/ChatHistoryMessageHandler.tsx
@@ -0,0 +1,107 @@
+import { GetGuestRoomResultEvent, RoomSessionChatEvent, RoomSessionEvent } from '@nitrots/nitro-renderer';
+import { FC, useCallback, useState } from 'react';
+import { GetRoomSession } from '../../api';
+import { CreateMessageHook, useRoomSessionManagerEvent } from '../../hooks';
+import { useChatHistoryContext } from './context/ChatHistoryContext';
+import { ChatEntryType, CHAT_HISTORY_MAX, IChatEntry, IRoomHistoryEntry, ROOM_HISTORY_MAX } from './context/ChatHistoryContext.types';
+import { currentDate } from './utils/Utilities';
+
+export const ChatHistoryMessageHandler: FC<{}> = props =>
+{
+ const { chatHistoryState = null, roomHistoryState = null } = useChatHistoryContext();
+
+ const [needsRoomInsert, setNeedsRoomInsert ] = useState(false);
+
+ const addChatEntry = useCallback((entry: IChatEntry) =>
+ {
+ entry.id = chatHistoryState.chats.length;
+
+ chatHistoryState.chats.push(entry);
+
+ //check for overflow
+ if(chatHistoryState.chats.length > CHAT_HISTORY_MAX)
+ {
+ chatHistoryState.chats.shift();
+ }
+ chatHistoryState.notify();
+
+ //dispatchUiEvent(new ChatHistoryEvent(ChatHistoryEvent.CHAT_HISTORY_CHANGED));
+
+ }, [chatHistoryState]);
+
+ const addRoomHistoryEntry = useCallback((entry: IRoomHistoryEntry) =>
+ {
+ roomHistoryState.roomHistory.push(entry);
+
+ // check for overflow
+ if(roomHistoryState.roomHistory.length > ROOM_HISTORY_MAX)
+ {
+ roomHistoryState.roomHistory.shift();
+ }
+
+ roomHistoryState.notify();
+ }, [roomHistoryState]);
+
+ const onChatEvent = useCallback((event: RoomSessionChatEvent) =>
+ {
+ const roomSession = GetRoomSession();
+
+ if(!roomSession) return;
+
+ const userData = roomSession.userDataManager.getUserDataByIndex(event.objectId);
+
+ if(!userData) return;
+
+ const timeString = currentDate();
+
+ const entry: IChatEntry = { id: -1, entityId: userData.webID, name: userData.name, look: userData.figure, entityType: userData.type, message: event.message, timestamp: timeString, type: ChatEntryType.TYPE_CHAT, roomId: roomSession.roomId };
+
+ addChatEntry(entry);
+ }, [addChatEntry]);
+
+ useRoomSessionManagerEvent(RoomSessionChatEvent.CHAT_EVENT, onChatEvent);
+
+ const onRoomSessionEvent = useCallback((event: RoomSessionEvent) =>
+ {
+ switch(event.type)
+ {
+ case RoomSessionEvent.STARTED:
+ setNeedsRoomInsert(true);
+ break;
+ case RoomSessionEvent.ENDED:
+ //dispatchUiEvent(new ChatHistoryEvent(ChatHistoryEvent.HIDE_CHAT_HISTORY));
+ break;
+ }
+ }, []);
+
+ useRoomSessionManagerEvent(RoomSessionEvent.ENDED, onRoomSessionEvent);
+ useRoomSessionManagerEvent(RoomSessionEvent.STARTED, onRoomSessionEvent);
+
+ const onGetGuestRoomResultEvent = useCallback((event: GetGuestRoomResultEvent) =>
+ {
+ const parser = event.getParser();
+
+ if(!parser) return;
+
+ const session = GetRoomSession();
+
+ if(!session || (session.roomId !== parser.data.roomId)) return;
+
+ if(needsRoomInsert)
+ {
+ const chatEntry: IChatEntry = { id: -1, entityId: parser.data.roomId, name: parser.data.roomName, timestamp: currentDate(), type: ChatEntryType.TYPE_ROOM_INFO, roomId: parser.data.roomId };
+
+ addChatEntry(chatEntry);
+
+ const roomEntry: IRoomHistoryEntry = { id: parser.data.roomId, name: parser.data.roomName };
+
+ addRoomHistoryEntry(roomEntry);
+
+ setNeedsRoomInsert(false);
+ }
+ }, [addChatEntry, addRoomHistoryEntry, needsRoomInsert]);
+
+ CreateMessageHook(GetGuestRoomResultEvent, onGetGuestRoomResultEvent);
+
+ return null;
+}
diff --git a/src/views/chat-history/ChatHistoryView.scss b/src/views/chat-history/ChatHistoryView.scss
new file mode 100644
index 00000000..d103ad8c
--- /dev/null
+++ b/src/views/chat-history/ChatHistoryView.scss
@@ -0,0 +1,27 @@
+.nitro-chat-history
+{
+ width: 300px;
+
+ .chat-history-content
+ {
+
+ .chat-history-container
+ {
+ min-height: 200px;
+
+ .chat-history-list
+ {
+ .chathistory-entry
+ {
+ .light {
+ background-color: #121f27;
+ }
+
+ .dark {
+ background-color: #0d171d;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/views/chat-history/ChatHistoryView.tsx b/src/views/chat-history/ChatHistoryView.tsx
new file mode 100644
index 00000000..f80ddbbd
--- /dev/null
+++ b/src/views/chat-history/ChatHistoryView.tsx
@@ -0,0 +1,166 @@
+import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { AutoSizer, CellMeasurer, CellMeasurerCache, List, ListRowProps, ListRowRenderer, Size } from 'react-virtualized';
+import { RenderedRows } from 'react-virtualized/dist/es/List';
+import { ChatHistoryEvent } from '../../events/chat-history/ChatHistoryEvent';
+import { useUiEvent } from '../../hooks';
+import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../layout';
+import { ChatHistoryMessageHandler } from './ChatHistoryMessageHandler';
+import { ChatHistoryState } from './common/ChatHistoryState';
+import { SetChatHistory } from './common/GetChatHistory';
+import { RoomHistoryState } from './common/RoomHistoryState';
+import { ChatHistoryContextProvider } from './context/ChatHistoryContext';
+import { ChatEntryType } from './context/ChatHistoryContext.types';
+
+export const ChatHistoryView: FC<{}> = props =>
+{
+ const [ isVisible, setIsVisible ] = useState(false);
+ const [ needsScroll, setNeedsScroll ] = useState(false);
+ const [ chatHistoryUpdateId, setChatHistoryUpdateId ] = useState(-1);
+ const [ roomHistoryUpdateId, setRoomHistoryUpdateId ] = useState(-1);
+ const [ chatHistoryState, setChatHistoryState ] = useState(new ChatHistoryState());
+ const [ roomHistoryState, setRoomHistoryState ] = useState(new RoomHistoryState());
+ const elementRef = useRef(null);
+
+ useEffect(() =>
+ {
+ const chatState = new ChatHistoryState();
+ const roomState = new RoomHistoryState();
+
+ SetChatHistory(chatState);
+
+ chatState.notifier = () => setChatHistoryUpdateId(prevValue => (prevValue + 1));
+ roomState.notifier = () => setRoomHistoryUpdateId(prevValue => (prevValue + 1));
+
+ setChatHistoryState(chatState);
+ setRoomHistoryState(roomState);
+
+ return () => {chatState.notifier = null; roomState.notifier = null;};
+ }, []);
+
+ const onChatHistoryEvent = useCallback((event: ChatHistoryEvent) =>
+ {
+ switch(event.type)
+ {
+ case ChatHistoryEvent.SHOW_CHAT_HISTORY:
+ setIsVisible(true);
+ break;
+ case ChatHistoryEvent.HIDE_CHAT_HISTORY:
+ setIsVisible(false);
+ break;
+ case ChatHistoryEvent.TOGGLE_CHAT_HISTORY:
+ setIsVisible(!isVisible);
+ break;
+ }
+ }, [isVisible]);
+
+ useUiEvent(ChatHistoryEvent.HIDE_CHAT_HISTORY, onChatHistoryEvent);
+ useUiEvent(ChatHistoryEvent.SHOW_CHAT_HISTORY, onChatHistoryEvent);
+ useUiEvent(ChatHistoryEvent.TOGGLE_CHAT_HISTORY, onChatHistoryEvent);
+
+ const cache = useMemo(() =>
+ {
+ return new CellMeasurerCache({
+ defaultHeight: 25,
+ fixedWidth: true,
+ //keyMapper: (index) => chatHistoryState.chats[index].id
+ });
+ }, []);
+
+ const RowRenderer: ListRowRenderer = (props: ListRowProps) =>
+ {
+ const item = chatHistoryState.chats[props.index];
+
+ const isDark = (props.index % 2 === 0);
+
+ return (
+
+
+ {(item.type === ChatEntryType.TYPE_CHAT) &&
+
+
{item.timestamp}
+
+
{item.message}
+
+ }
+ {(item.type === ChatEntryType.TYPE_ROOM_INFO) &&
+
+
{item.timestamp}
+
{item.name}
+
+ }
+
+
+
+ );
+ };
+
+ const onResize = useCallback((info: Size) =>
+ {
+ cache.clearAll();
+ }, [cache]);
+
+ const onRowsRendered = useCallback((info: RenderedRows) =>
+ {
+ if(elementRef && elementRef.current && isVisible && needsScroll)
+ {
+ console.log('stop ' + info.stopIndex);
+ //if(chatHistoryState.chats.length > 0) elementRef.current.measureAllRows();
+ elementRef.current.scrollToRow(chatHistoryState.chats.length);
+ console.log('scroll')
+ setNeedsScroll(false);
+ }
+ }, [chatHistoryState.chats.length, isVisible, needsScroll]);
+
+ useEffect(() =>
+ {
+
+ if(elementRef && elementRef.current && isVisible)
+ {
+ //if(chatHistoryState.chats.length > 0) elementRef.current.measureAllRows();
+ elementRef.current.scrollToRow(chatHistoryState.chats.length);
+ }
+ //console.log(chatHistoryState.chats.length);
+
+ setNeedsScroll(true);
+ }, [chatHistoryState.chats, isVisible, chatHistoryUpdateId]);
+
+ return (
+
+
+ {isVisible &&
+
+ setIsVisible(false) } theme={'dark'}/>
+
+
+
+ {({ height, width }) =>
+ {
+ return (
+
+ )
+ }
+ }
+
+
+
+
+ }
+
+ );
+}
diff --git a/src/views/chat-history/common/ChatHistoryState.ts b/src/views/chat-history/common/ChatHistoryState.ts
new file mode 100644
index 00000000..895c00a1
--- /dev/null
+++ b/src/views/chat-history/common/ChatHistoryState.ts
@@ -0,0 +1,32 @@
+import { IChatEntry, IChatHistoryState } from '../context/ChatHistoryContext.types';
+
+export class ChatHistoryState implements IChatHistoryState
+{
+ private _chats: IChatEntry[];
+ private _notifier: () => void;
+
+ constructor()
+ {
+ this._chats = [];
+ }
+
+ public get chats(): IChatEntry[]
+ {
+ return this._chats;
+ }
+
+ public get notifier(): () => void
+ {
+ return this._notifier;
+ }
+
+ public set notifier(notifier: () => void)
+ {
+ this._notifier = notifier;
+ }
+
+ notify(): void
+ {
+ if(this._notifier) this._notifier();
+ }
+}
diff --git a/src/views/chat-history/common/GetChatHistory.ts b/src/views/chat-history/common/GetChatHistory.ts
new file mode 100644
index 00000000..d39ef584
--- /dev/null
+++ b/src/views/chat-history/common/GetChatHistory.ts
@@ -0,0 +1,7 @@
+import { IChatHistoryState } from '../context/ChatHistoryContext.types';
+
+let GLOBAL_CHATS: IChatHistoryState = null;
+
+export const SetChatHistory = (chatHistory: IChatHistoryState) => (GLOBAL_CHATS = chatHistory);
+
+export const GetChatHistory = () => GLOBAL_CHATS;
diff --git a/src/views/chat-history/common/RoomHistoryState.ts b/src/views/chat-history/common/RoomHistoryState.ts
new file mode 100644
index 00000000..d3002b81
--- /dev/null
+++ b/src/views/chat-history/common/RoomHistoryState.ts
@@ -0,0 +1,32 @@
+import { IRoomHistoryEntry, IRoomHistoryState } from '../context/ChatHistoryContext.types';
+
+export class RoomHistoryState implements IRoomHistoryState
+{
+ private _roomHistory: IRoomHistoryEntry[];
+ private _notifier: () => void;
+
+ constructor()
+ {
+ this._roomHistory = [];
+ }
+
+ public get roomHistory(): IRoomHistoryEntry[]
+ {
+ return this._roomHistory;
+ }
+
+ public get notifier(): () => void
+ {
+ return this._notifier;
+ }
+
+ public set notifier(notifier: () => void)
+ {
+ this._notifier = notifier;
+ }
+
+ notify(): void
+ {
+ if(this._notifier) this._notifier();
+ }
+}
diff --git a/src/views/chat-history/context/ChatHistoryContext.tsx b/src/views/chat-history/context/ChatHistoryContext.tsx
new file mode 100644
index 00000000..981d6007
--- /dev/null
+++ b/src/views/chat-history/context/ChatHistoryContext.tsx
@@ -0,0 +1,14 @@
+import { createContext, FC, useContext } from 'react';
+import { ChatHistoryContextProps, IChatHistoryContext } from './ChatHistoryContext.types';
+
+const ChatHistoryContext = createContext({
+ chatHistoryState: null,
+ roomHistoryState: null
+});
+
+export const ChatHistoryContextProvider: FC = props =>
+{
+ return { props.children }
+}
+
+export const useChatHistoryContext = () => useContext(ChatHistoryContext);
diff --git a/src/views/chat-history/context/ChatHistoryContext.types.ts b/src/views/chat-history/context/ChatHistoryContext.types.ts
new file mode 100644
index 00000000..51cd19cf
--- /dev/null
+++ b/src/views/chat-history/context/ChatHistoryContext.types.ts
@@ -0,0 +1,50 @@
+import { ProviderProps } from 'react';
+
+export interface IChatHistoryContext
+{
+ chatHistoryState: IChatHistoryState;
+ roomHistoryState: IRoomHistoryState;
+}
+
+export interface IChatHistoryState {
+ chats: IChatEntry[];
+ notifier: () => void
+ notify(): void;
+}
+
+export interface IRoomHistoryState {
+ roomHistory: IRoomHistoryEntry[];
+ notifier: () => void
+ notify(): void;
+}
+
+export interface IChatEntry {
+ id: number;
+ entityId: number;
+ name: string;
+ look?: string;
+ message?: string;
+ entityType?: number;
+ roomId: number;
+ timestamp: string;
+ type: number;
+}
+
+export interface IRoomHistoryEntry {
+ id: number;
+ name: string;
+}
+
+export class ChatEntryType
+{
+ public static TYPE_CHAT = 1;
+ public static TYPE_ROOM_INFO = 2;
+}
+
+export const CHAT_HISTORY_MAX = 1000;
+export const ROOM_HISTORY_MAX = 10;
+
+export interface ChatHistoryContextProps extends ProviderProps
+{
+
+}
diff --git a/src/views/chat-history/utils/Utilities.ts b/src/views/chat-history/utils/Utilities.ts
new file mode 100644
index 00000000..29b00cfa
--- /dev/null
+++ b/src/views/chat-history/utils/Utilities.ts
@@ -0,0 +1,5 @@
+export const currentDate = () =>
+{
+ const currentTime = new Date();
+ return `${currentTime.getHours().toString().padStart(2, '0')}:${currentTime.getMinutes().toString().padStart(2, '0')}`;
+}
diff --git a/src/views/floorplan-editor/FloorplanEditorView.scss b/src/views/floorplan-editor/FloorplanEditorView.scss
new file mode 100644
index 00000000..b11b5107
--- /dev/null
+++ b/src/views/floorplan-editor/FloorplanEditorView.scss
@@ -0,0 +1,21 @@
+.nitro-floorplan-editor {
+ width: 760px;
+ height: 575px;
+
+ .editor-area {
+ width: 100%;
+ height: 300px;
+ min-height: 300px;
+ overflow-x: scroll;
+ }
+
+ .color {
+ height: 50px;
+ width: 10px;
+ }
+}
+
+.floorplan-import-export {
+ width: 500px;
+ height: 475px;
+}
diff --git a/src/views/floorplan-editor/FloorplanEditorView.tsx b/src/views/floorplan-editor/FloorplanEditorView.tsx
new file mode 100644
index 00000000..48a7d34b
--- /dev/null
+++ b/src/views/floorplan-editor/FloorplanEditorView.tsx
@@ -0,0 +1,151 @@
+import { FloorHeightMapEvent, NitroPoint, RoomEngineEvent, RoomVisualizationSettingsEvent, UpdateFloorPropertiesMessageComposer } from '@nitrots/nitro-renderer';
+import { FC, useCallback, useState } from 'react';
+import { LocalizeText } from '../../api';
+import { FloorplanEditorEvent } from '../../events/floorplan-editor/FloorplanEditorEvent';
+import { CreateMessageHook, SendMessageHook, UseMountEffect, useRoomEngineEvent, useUiEvent } from '../../hooks';
+import { NitroCardContentView, NitroCardHeaderView, NitroCardView, NitroLayoutFlex, NitroLayoutGrid, NitroLayoutGridColumn } from '../../layout';
+import { FloorplanEditor } from './common/FloorplanEditor';
+import { convertNumbersForSaving, convertSettingToNumber } from './common/Utils';
+import { FloorplanEditorContextProvider } from './context/FloorplanEditorContext';
+import { IFloorplanSettings, initialFloorplanSettings, IVisualizationSettings } from './context/FloorplanEditorContext.types';
+import { FloorplanCanvasView } from './views/FloorplanCanvasView';
+import { FloorplanImportExportView } from './views/FloorplanImportExportView';
+import { FloorplanOptionsView } from './views/FloorplanOptionsView';
+
+export const FloorplanEditorView: FC<{}> = props =>
+{
+ const [isVisible, setIsVisible] = useState(false);
+ const [ importExportVisible, setImportExportVisible ] = useState(false);
+ const [originalFloorplanSettings, setOriginalFloorplanSettings] = useState(initialFloorplanSettings);
+ const [visualizationSettings, setVisualizationSettings] = useState(
+ {
+ entryPointDir: 2,
+ wallHeight: -1,
+ thicknessWall: 1,
+ thicknessFloor: 1
+ });
+
+ const onFloorplanEditorEvent = useCallback((event: FloorplanEditorEvent) =>
+ {
+ switch(event.type)
+ {
+ case FloorplanEditorEvent.HIDE_FLOORPLAN_EDITOR:
+ setIsVisible(false);
+ break;
+ case FloorplanEditorEvent.SHOW_FLOORPLAN_EDITOR:
+ setIsVisible(true);
+ break;
+ case FloorplanEditorEvent.TOGGLE_FLOORPLAN_EDITOR:
+ setIsVisible(!isVisible);
+ break;
+ }
+ }, [isVisible]);
+
+ useUiEvent(FloorplanEditorEvent.HIDE_FLOORPLAN_EDITOR, onFloorplanEditorEvent);
+ useUiEvent(FloorplanEditorEvent.SHOW_FLOORPLAN_EDITOR, onFloorplanEditorEvent);
+ useUiEvent(FloorplanEditorEvent.TOGGLE_FLOORPLAN_EDITOR, onFloorplanEditorEvent);
+
+ UseMountEffect(() =>
+ {
+ FloorplanEditor.instance.initialize();
+ });
+
+ const onRoomEngineEvent = useCallback((event: RoomEngineEvent) =>
+ {
+ setIsVisible(false);
+ }, []);
+
+ useRoomEngineEvent(RoomEngineEvent.DISPOSED, onRoomEngineEvent);
+
+ const onFloorHeightMapEvent = useCallback((event: FloorHeightMapEvent) =>
+ {
+ const parser = event.getParser();
+
+ if(!parser) return;
+
+ const settings = Object.assign({}, originalFloorplanSettings);
+ settings.tilemap = parser.model;
+ settings.wallHeight = parser.wallHeight + 1;
+ setOriginalFloorplanSettings(settings);
+
+ const vSettings = Object.assign({}, visualizationSettings);
+ vSettings.wallHeight = parser.wallHeight + 1;
+ setVisualizationSettings(vSettings);
+
+ }, [originalFloorplanSettings, visualizationSettings]);
+
+ CreateMessageHook(FloorHeightMapEvent, onFloorHeightMapEvent);
+
+ const onRoomVisualizationSettingsEvent = useCallback((event: RoomVisualizationSettingsEvent) =>
+ {
+ const parser = event.getParser();
+
+ if(!parser) return;
+
+ const settings = Object.assign({}, originalFloorplanSettings);
+ settings.thicknessFloor = convertSettingToNumber(parser.thicknessFloor);
+ settings.thicknessWall = convertSettingToNumber(parser.thicknessWall);
+
+ setOriginalFloorplanSettings(settings);
+
+ const vSettings = Object.assign({}, visualizationSettings);
+ vSettings.thicknessFloor = convertSettingToNumber(parser.thicknessFloor);
+ vSettings.thicknessWall = convertSettingToNumber(parser.thicknessWall);
+ setVisualizationSettings(vSettings);
+ }, [originalFloorplanSettings, visualizationSettings]);
+
+ CreateMessageHook(RoomVisualizationSettingsEvent, onRoomVisualizationSettingsEvent);
+
+ const saveFloorChanges = useCallback(() =>
+ {
+ SendMessageHook(new UpdateFloorPropertiesMessageComposer(
+ FloorplanEditor.instance.getCurrentTilemapString(),
+ FloorplanEditor.instance.doorLocation.x,
+ FloorplanEditor.instance.doorLocation.y,
+ visualizationSettings.entryPointDir,
+ convertNumbersForSaving(visualizationSettings.thicknessWall),
+ convertNumbersForSaving(visualizationSettings.thicknessFloor),
+ visualizationSettings.wallHeight - 1
+ ));
+ }, [visualizationSettings.entryPointDir, visualizationSettings.thicknessFloor, visualizationSettings.thicknessWall, visualizationSettings.wallHeight]);
+
+ const revertChanges = useCallback(() =>
+ {
+ setVisualizationSettings({ wallHeight: originalFloorplanSettings.wallHeight, thicknessWall: originalFloorplanSettings.thicknessWall, thicknessFloor: originalFloorplanSettings.thicknessFloor, entryPointDir: originalFloorplanSettings.entryPointDir });
+
+ FloorplanEditor.instance.doorLocation = new NitroPoint(originalFloorplanSettings.entryPoint[0], originalFloorplanSettings.entryPoint[1]);
+ FloorplanEditor.instance.setTilemap(originalFloorplanSettings.tilemap, originalFloorplanSettings.reservedTiles);
+ FloorplanEditor.instance.renderTiles();
+ }, [originalFloorplanSettings.entryPoint, originalFloorplanSettings.entryPointDir, originalFloorplanSettings.reservedTiles, originalFloorplanSettings.thicknessFloor, originalFloorplanSettings.thicknessWall, originalFloorplanSettings.tilemap, originalFloorplanSettings.wallHeight])
+
+ return (
+ <>
+
+ {isVisible &&
+
+ setIsVisible(false)} />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ {importExportVisible && setImportExportVisible(false)}/>}
+
+ >
+ );
+}
diff --git a/src/views/floorplan-editor/common/ActionSettings.ts b/src/views/floorplan-editor/common/ActionSettings.ts
new file mode 100644
index 00000000..8ac38251
--- /dev/null
+++ b/src/views/floorplan-editor/common/ActionSettings.ts
@@ -0,0 +1,39 @@
+import { FloorAction, HEIGHT_SCHEME } from './Constants';
+
+export class ActionSettings
+{
+ private _currentAction: number;
+ private _currentHeight: string;
+
+ constructor()
+ {
+ this._currentAction = FloorAction.SET;
+ this._currentHeight = HEIGHT_SCHEME[1];
+ }
+
+ public get currentAction(): number
+ {
+ return this._currentAction;
+ }
+
+ public set currentAction(value: number)
+ {
+ this._currentAction = value;
+ }
+
+ public get currentHeight(): string
+ {
+ return this._currentHeight;
+ }
+
+ public set currentHeight(value: string)
+ {
+ this._currentHeight = value;
+ }
+
+ public clear(): void
+ {
+ this._currentAction = FloorAction.SET;
+ this._currentHeight = HEIGHT_SCHEME[1];
+ }
+}
diff --git a/src/views/floorplan-editor/common/Constants.ts b/src/views/floorplan-editor/common/Constants.ts
new file mode 100644
index 00000000..8f52f282
--- /dev/null
+++ b/src/views/floorplan-editor/common/Constants.ts
@@ -0,0 +1,44 @@
+export const TILE_SIZE = 32;
+export const MAX_NUM_TILE_PER_AXIS = 64;
+
+export const HEIGHT_SCHEME: string = 'x0123456789abcdefghijklmnopq';
+
+export class FloorAction
+{
+ public static readonly DOOR = 0;
+ public static readonly UP = 1;
+ public static readonly DOWN = 2;
+ public static readonly SET = 3;
+ public static readonly UNSET = 4;
+}
+
+export const COLORMAP: object = {
+ 'x': '101010',
+ '0': '0065ff',
+ '1': '0091ff',
+ '2': '00bcff',
+ '3': '00e8ff',
+ '4': '00ffea',
+ '5': '00ffbf',
+ '6': '00ff93',
+ '7': '00ff68',
+ '8': '00ff3d',
+ '9': '19ff00',
+ 'a': '44ff00',
+ 'b': '70ff00',
+ 'c': '9bff00',
+ 'd': 'f2ff00',
+ 'e': 'ffe000',
+ 'f': 'ffb500',
+ 'g': 'ff8900',
+ 'h': 'ff5e00',
+ 'i': 'ff3200',
+ 'j': 'ff0700',
+ 'k': 'ff0023',
+ 'l': 'ff007a',
+ 'm': 'ff00a5',
+ 'n': 'ff00d1',
+ 'o': 'ff00fc',
+ 'p': 'd600ff',
+ 'q': 'aa00ff'
+};
diff --git a/src/views/floorplan-editor/common/FloorplanEditor.ts b/src/views/floorplan-editor/common/FloorplanEditor.ts
new file mode 100644
index 00000000..99cdd12c
--- /dev/null
+++ b/src/views/floorplan-editor/common/FloorplanEditor.ts
@@ -0,0 +1,417 @@
+import { NitroPoint, NitroTilemap, PixiApplicationProxy, PixiInteractionEventProxy, POINT_STRUCT_SIZE } from '@nitrots/nitro-renderer';
+import { GetConfiguration } from '../../../api';
+import { ActionSettings } from './ActionSettings';
+import { FloorAction, HEIGHT_SCHEME, MAX_NUM_TILE_PER_AXIS, TILE_SIZE } from './Constants';
+import { Tile } from './Tile';
+import { getScreenPositionForTile, getTileFromScreenPosition } from './Utils';
+
+export class FloorplanEditor extends PixiApplicationProxy
+{
+ private static _instance: FloorplanEditor = new FloorplanEditor();
+
+ private static readonly TILE_BLOCKED = 'r_blocked';
+ private static readonly TILE_DOOR = 'r_door';
+
+ private _tilemap: Tile[][];
+ private _width: number;
+ private _height: number;
+ private _isHolding: boolean;
+ private _doorLocation: NitroPoint;
+ private _lastUsedTile: NitroPoint;
+ private _tilemapRenderer: NitroTilemap;
+ private _actionSettings: ActionSettings;
+ private _isInitialized: boolean;
+
+ private constructor()
+ {
+ const width = TILE_SIZE * MAX_NUM_TILE_PER_AXIS + 20;
+ const height = (TILE_SIZE * MAX_NUM_TILE_PER_AXIS) / 2 + 100;
+
+ super({
+ width: width,
+ height: height,
+ backgroundColor: 0x2b2b2b,
+ antialias: true,
+ autoDensity: true,
+ resolution: 1,
+ sharedLoader: true,
+ sharedTicker: true
+ });
+
+ this._tilemap = [];
+ this._doorLocation = new NitroPoint(0, 0);
+ this._width = 0;
+ this._height = 0;
+ this._isHolding = false;
+ this._lastUsedTile = new NitroPoint(-1,-1);
+ this._actionSettings = new ActionSettings();
+ }
+
+ public initialize(): void
+ {
+ if(!this._isInitialized)
+ {
+ this.loader.add('tiles', GetConfiguration('floorplan.tile.url'));
+
+ this.loader.load((_, resources) =>
+ {
+ this._tilemapRenderer = new NitroTilemap(resources['tiles'].spritesheet.baseTexture);
+ this.registerEventListeners();
+ this.stage.addChild(this._tilemapRenderer);
+ });
+ this._isInitialized = true;
+ }
+ }
+
+ private registerEventListeners(): void
+ {
+ //this._tilemapRenderer.interactive = true;
+
+ const tempPoint = new NitroPoint();
+ // @ts-ignore
+ this._tilemapRenderer.containsPoint = (position) =>
+ {
+ this._tilemapRenderer.worldTransform.applyInverse(position, tempPoint);
+ return this.tileHitDettection(tempPoint, false);
+ };
+
+ this._tilemapRenderer.on('pointerup', () =>
+ {
+ this._isHolding = false;
+ });
+
+ this._tilemapRenderer.on('pointerout', () =>
+ {
+ this._isHolding = false;
+ });
+
+ this._tilemapRenderer.on('pointerdown', (event: PixiInteractionEventProxy) =>
+ {
+ if(!(event.data.originalEvent instanceof PointerEvent)) return;
+
+ const pointerEvent = event.data.originalEvent;
+ if(pointerEvent.button === 2) return;
+
+
+ const location = event.data.global;
+ this.tileHitDettection(location, true);
+ });
+
+ this._tilemapRenderer.on('click', (event: PixiInteractionEventProxy) =>
+ {
+ if(!(event.data.originalEvent instanceof PointerEvent)) return;
+
+ const pointerEvent = event.data.originalEvent;
+ if(pointerEvent.button === 2) return;
+
+ const location = event.data.global;
+ this.tileHitDettection(location, true, true);
+ });
+ }
+
+ private tileHitDettection(tempPoint: NitroPoint, setHolding: boolean, isClick: boolean = false): boolean
+ {
+ // @ts-ignore
+ const buffer = this._tilemapRenderer.pointsBuf;
+ const bufSize = POINT_STRUCT_SIZE;
+
+ const len = buffer.length;
+
+ if(setHolding)
+ {
+ this._isHolding = true;
+ }
+
+ for(let j = 0; j < len; j += bufSize)
+ {
+ const bufIndex = j + bufSize;
+ const data = buffer.slice(j, bufIndex);
+
+ const width = data[4];
+ const height = data[5];
+
+
+ const mousePositionX = Math.floor(tempPoint.x);
+ const mousePositionY = Math.floor(tempPoint.y);
+
+ const tileStartX = data[2];
+ const tileStartY = data[3];
+
+
+ const centreX = tileStartX + (width / 2);
+ const centreY = tileStartY + (height / 2);
+
+ const dx = Math.abs(mousePositionX - centreX - 2);
+ const dy = Math.abs(mousePositionY - centreY - 2);
+
+ const solution = (dx / (width * 0.5) + dy / (height * 0.5) <= 1);//todo: improve this
+ if(solution)
+ {
+ if(this._isHolding)
+ {
+ const [realX, realY] = getTileFromScreenPosition(tileStartX, tileStartY);
+
+ if(isClick)
+ {
+ this.onClick(realX, realY);
+ }
+
+ else if(this._lastUsedTile.x !== realX || this._lastUsedTile.y !== realY)
+ {
+ this._lastUsedTile.x = realX;
+ this._lastUsedTile.y = realY;
+ this.onClick(realX, realY);
+ }
+
+ }
+ return true;
+ }
+
+ }
+ return false;
+ }
+
+
+ private onClick(x: number, y: number): void
+ {
+ const tile = this._tilemap[y][x];
+ const heightIndex = HEIGHT_SCHEME.indexOf(tile.height);
+
+ let futureHeightIndex = 0;
+
+ switch(this._actionSettings.currentAction)
+ {
+ case FloorAction.DOOR:
+
+ if(tile.height !== 'x')
+ {
+ this._doorLocation.x = x;
+ this._doorLocation.y = y;
+ this.renderTiles();
+ }
+ return;
+ case FloorAction.UP:
+ futureHeightIndex = heightIndex + 1;
+ break;
+ case FloorAction.DOWN:
+ futureHeightIndex = heightIndex - 1;
+ break;
+ case FloorAction.SET:
+ futureHeightIndex = HEIGHT_SCHEME.indexOf(this._actionSettings.currentHeight);
+ break;
+ case FloorAction.UNSET:
+ futureHeightIndex = 0;
+ break;
+ }
+
+ if(futureHeightIndex === -1) return;
+
+ if(heightIndex === futureHeightIndex) return;
+
+ if(futureHeightIndex > 0)
+ {
+ if((x + 1) > this._width) this._width = x + 1;
+
+ if( (y + 1) > this._height) this._height = y + 1;
+ }
+
+ const newHeight = HEIGHT_SCHEME[futureHeightIndex];
+
+ if(!newHeight) return;
+
+ if(tile.isBlocked) return;
+
+ this._tilemap[y][x].height = newHeight;
+
+ this.renderTiles();
+ }
+
+ public renderTiles(): void
+ {
+ this.tilemapRenderer.clear();
+
+ for(let y = 0; y < this._tilemap.length; y++)
+ {
+ for(let x = 0; x < this.tilemap[y].length; x++)
+ {
+ const tile = this.tilemap[y][x];
+ let assetName = tile.height;
+
+ if(this._doorLocation.x === x && this._doorLocation.y === y)
+ assetName = FloorplanEditor.TILE_DOOR;
+
+ if(tile.isBlocked) assetName = FloorplanEditor.TILE_BLOCKED;
+
+ //if((tile.height === 'x') || tile.height === 'X') continue;
+ const [positionX, positionY ] = getScreenPositionForTile(x, y);
+ this._tilemapRenderer.tile(`${assetName}.png`, positionX, positionY);
+ }
+ }
+ }
+
+ public setTilemap(map: string, blockedTiles: boolean[][]): void
+ {
+ this._tilemap = [];
+ const roomMapStringSplit = map.split('\r');
+
+ let width = 0;
+ let height = roomMapStringSplit.length;
+
+ // find the map width, height
+ for(let y = 0; y < height; y++)
+ {
+ const originalRow = roomMapStringSplit[y];
+
+ if(originalRow.length === 0)
+ {
+ roomMapStringSplit.splice(y, 1);
+ height = roomMapStringSplit.length;
+ y--;
+ continue;
+ }
+
+ if(originalRow.length > width)
+ {
+ width = originalRow.length;
+ }
+ }
+ // fill map with room heightmap tiles
+ for(let y = 0; y < height; y++)
+ {
+ this._tilemap[y] = [];
+ const rowString = roomMapStringSplit[y];
+
+ for(let x = 0; x < width; x++)
+ {
+ const blocked = (blockedTiles[y] && blockedTiles[y][x]) || false;
+
+ const char = rowString[x];
+ if(((!(char === 'x')) && (!(char === 'X')) && char))
+ {
+ this._tilemap[y][x] = new Tile(char, blocked);
+ }
+ else
+ {
+ this._tilemap[y][x] = new Tile('x', blocked);
+ }
+ }
+
+ for(let x = width; x < MAX_NUM_TILE_PER_AXIS; x++)
+ {
+ this.tilemap[y][x] = new Tile('x', false);
+ }
+ }
+
+ // fill remaining map with empty tiles
+ for(let y = height; y < MAX_NUM_TILE_PER_AXIS; y++)
+ {
+ if(!this.tilemap[y]) this.tilemap[y] = [];
+ for(let x = 0; x < MAX_NUM_TILE_PER_AXIS; x++)
+ {
+ this.tilemap[y][x] = new Tile('x', false);
+ }
+ }
+
+ this._width = width;
+ this._height = height;
+ }
+
+ public getCurrentTilemapString(): string
+ {
+ const highestTile = this._tilemap[this._height - 1][this._width - 1];
+
+ if(highestTile.height === 'x')
+ {
+ this._width = -1;
+ this._height = -1;
+
+ for(let y = MAX_NUM_TILE_PER_AXIS - 1; y >= 0; y--)
+ {
+ if(!this._tilemap[y]) continue;
+
+ for(let x = MAX_NUM_TILE_PER_AXIS - 1; x >= 0; x--)
+ {
+ if(!this._tilemap[y][x]) continue;
+
+ const tile = this._tilemap[y][x];
+
+ if(tile.height !== 'x')
+ {
+ if( (x + 1) > this._width)
+ this._width = x + 1;
+
+ if( (y + 1) > this._height)
+ this._height = y + 1;
+ }
+ }
+ }
+ }
+
+
+ const rows = [];
+
+ for(let y = 0; y < this._height; y++)
+ {
+ const row = [];
+
+ for(let x = 0; x < this._width; x++)
+ {
+ const tile = this._tilemap[y][x];
+
+ row[x] = tile.height;
+ }
+
+ rows[y] = row.join('');
+ }
+
+ return rows.join('\r');
+ }
+
+ public clear(): void
+ {
+ this._tilemapRenderer.interactive = false;
+ this._tilemap = [];
+ this._doorLocation.set(-1, -1);
+ this._width = 0;
+ this._height = 0;
+ this._isHolding = false;
+ this._lastUsedTile.set(-1, -1);
+ this._actionSettings.clear();
+ this._tilemapRenderer.clear();
+ }
+
+ public get tilemapRenderer(): NitroTilemap
+ {
+ return this._tilemapRenderer;
+ }
+
+ public get tilemap(): Tile[][]
+ {
+ return this._tilemap;
+ }
+
+ public get doorLocation(): NitroPoint
+ {
+ return this._doorLocation;
+ }
+
+ public set doorLocation(value: NitroPoint)
+ {
+ this._doorLocation = value;
+ }
+
+ public get actionSettings(): ActionSettings
+ {
+ return this._actionSettings;
+ }
+
+ public static get instance(): FloorplanEditor
+ {
+ if(!FloorplanEditor._instance)
+ {
+ FloorplanEditor._instance = new FloorplanEditor();
+ }
+
+ return FloorplanEditor._instance;
+ }
+}
diff --git a/src/views/floorplan-editor/common/Tile.ts b/src/views/floorplan-editor/common/Tile.ts
new file mode 100644
index 00000000..fd9c0596
--- /dev/null
+++ b/src/views/floorplan-editor/common/Tile.ts
@@ -0,0 +1,31 @@
+export class Tile
+{
+ private _height: string;
+ private _isBlocked: boolean;
+
+ constructor(height: string, isBlocked: boolean)
+ {
+ this._height = height;
+ this._isBlocked = isBlocked;
+ }
+
+ public get height(): string
+ {
+ return this._height;
+ }
+
+ public set height(height: string)
+ {
+ this._height = height;
+ }
+
+ public get isBlocked(): boolean
+ {
+ return this._isBlocked;
+ }
+
+ public set isBlocked(val: boolean)
+ {
+ this._isBlocked = val;
+ }
+}
diff --git a/src/views/floorplan-editor/common/Utils.ts b/src/views/floorplan-editor/common/Utils.ts
new file mode 100644
index 00000000..6ffa52de
--- /dev/null
+++ b/src/views/floorplan-editor/common/Utils.ts
@@ -0,0 +1,53 @@
+import { TILE_SIZE } from './Constants';
+
+export const getScreenPositionForTile = (x: number, y: number): [number , number] =>
+{
+ let positionX = (x * TILE_SIZE / 2) - (y * TILE_SIZE / 2);
+ const positionY = (x * TILE_SIZE / 4) + (y * TILE_SIZE / 4);
+
+ positionX = positionX + 1024; // center the map in the canvas
+
+ return [positionX, positionY];
+}
+
+export const getTileFromScreenPosition = (x: number, y: number): [number, number] =>
+{
+ const translatedX = x - 1024; // after centering translation
+
+ const realX = ((translatedX /(TILE_SIZE / 2)) + (y / (TILE_SIZE / 4))) / 2;
+ const realY = ((y /(TILE_SIZE / 4)) - (translatedX / (TILE_SIZE / 2))) / 2;
+
+ return [realX, realY];
+}
+
+export const convertNumbersForSaving = (value: number): number =>
+{
+ value = parseInt(value.toString());
+ switch(value)
+ {
+ case 0:
+ return -2;
+ case 1:
+ return -1;
+ case 3:
+ return 1;
+ default:
+ return 0;
+
+ }
+}
+
+export const convertSettingToNumber = (value: number): number =>
+{
+ switch(value)
+ {
+ case 0.25:
+ return 0;
+ case 0.5:
+ return 1;
+ case 2:
+ return 3;
+ default:
+ return 2;
+ }
+}
diff --git a/src/views/floorplan-editor/context/FloorplanEditorContext.tsx b/src/views/floorplan-editor/context/FloorplanEditorContext.tsx
new file mode 100644
index 00000000..8a8ce970
--- /dev/null
+++ b/src/views/floorplan-editor/context/FloorplanEditorContext.tsx
@@ -0,0 +1,16 @@
+import { createContext, FC, useContext } from 'react';
+import { FloorplanEditorContextProps, IFloorplanEditorContext } from './FloorplanEditorContext.types';
+
+const FloorplanEditorContext = createContext({
+ originalFloorplanSettings: null,
+ setOriginalFloorplanSettings: null,
+ visualizationSettings: null,
+ setVisualizationSettings: null
+});
+
+export const FloorplanEditorContextProvider: FC = props =>
+{
+ return { props.children }
+}
+
+export const useFloorplanEditorContext = () => useContext(FloorplanEditorContext);
diff --git a/src/views/floorplan-editor/context/FloorplanEditorContext.types.ts b/src/views/floorplan-editor/context/FloorplanEditorContext.types.ts
new file mode 100644
index 00000000..581ff6af
--- /dev/null
+++ b/src/views/floorplan-editor/context/FloorplanEditorContext.types.ts
@@ -0,0 +1,37 @@
+import { ProviderProps } from 'react';
+
+export interface IFloorplanEditorContext
+{
+ originalFloorplanSettings: IFloorplanSettings;
+ setOriginalFloorplanSettings: React.Dispatch>;
+ visualizationSettings: IVisualizationSettings;
+ setVisualizationSettings: React.Dispatch>;
+}
+
+export interface IFloorplanSettings extends IVisualizationSettings {
+ tilemap: string;
+ reservedTiles: boolean[][];
+ entryPoint: [number, number];
+}
+
+export interface IVisualizationSettings {
+ entryPointDir: number;
+ wallHeight: number;
+ thicknessWall: number;
+ thicknessFloor: number;
+}
+
+export const initialFloorplanSettings: IFloorplanSettings = {
+ tilemap: '',
+ reservedTiles: [],
+ entryPoint: [0, 0],
+ entryPointDir: 2,
+ wallHeight: -1,
+ thicknessWall: 1,
+ thicknessFloor: 1
+}
+
+export interface FloorplanEditorContextProps extends ProviderProps
+{
+
+}
diff --git a/src/views/floorplan-editor/views/FloorplanCanvasView.tsx b/src/views/floorplan-editor/views/FloorplanCanvasView.tsx
new file mode 100644
index 00000000..f68a9248
--- /dev/null
+++ b/src/views/floorplan-editor/views/FloorplanCanvasView.tsx
@@ -0,0 +1,80 @@
+import { GetOccupiedTilesMessageComposer, GetRoomEntryTileMessageComposer, NitroPoint, RoomEntryTileMessageEvent, RoomOccupiedTilesMessageEvent } from '@nitrots/nitro-renderer';
+import { FC, useCallback, useEffect, useRef, useState } from 'react';
+import { CreateMessageHook, SendMessageHook, UseMountEffect } from '../../../hooks';
+import { FloorplanEditor } from '../common/FloorplanEditor';
+import { useFloorplanEditorContext } from '../context/FloorplanEditorContext';
+
+export const FloorplanCanvasView: FC<{}> = props =>
+{
+ const { originalFloorplanSettings = null, setOriginalFloorplanSettings = null, visualizationSettings = null, setVisualizationSettings = null } = useFloorplanEditorContext();
+ const [ occupiedTilesReceived , setOccupiedTilesReceived ] = useState(false);
+ const [ entryTileReceived, setEntryTileReceived ] = useState(false);
+ const elementRef = useRef(null);
+
+ useEffect(() =>
+ {
+ return ( () =>
+ {
+ FloorplanEditor.instance.clear();
+ setVisualizationSettings( prev => {return { wallHeight: originalFloorplanSettings.wallHeight, thicknessWall: originalFloorplanSettings.thicknessWall, thicknessFloor: originalFloorplanSettings.thicknessFloor, entryPointDir: prev.entryPointDir } });
+ });
+ }, [originalFloorplanSettings.thicknessFloor, originalFloorplanSettings.thicknessWall, originalFloorplanSettings.wallHeight, setVisualizationSettings]);
+
+ UseMountEffect(() =>
+ {
+ SendMessageHook(new GetRoomEntryTileMessageComposer());
+ SendMessageHook(new GetOccupiedTilesMessageComposer());
+ FloorplanEditor.instance.tilemapRenderer.interactive = true;
+ elementRef.current.appendChild(FloorplanEditor.instance.renderer.view);
+ });
+
+ const onRoomOccupiedTilesMessageEvent = useCallback((event: RoomOccupiedTilesMessageEvent) =>
+ {
+ const parser = event.getParser();
+
+ if(!parser) return;
+
+ const settings = Object.assign({}, originalFloorplanSettings);
+ settings.reservedTiles = parser.blockedTilesMap;
+ setOriginalFloorplanSettings(settings);
+
+ FloorplanEditor.instance.setTilemap(originalFloorplanSettings.tilemap, parser.blockedTilesMap);
+
+ setOccupiedTilesReceived(true);
+
+ elementRef.current.scrollTo(FloorplanEditor.instance.view.width / 3, 0);
+ }, [originalFloorplanSettings, setOriginalFloorplanSettings]);
+
+ CreateMessageHook(RoomOccupiedTilesMessageEvent, onRoomOccupiedTilesMessageEvent);
+
+ const onRoomEntryTileMessageEvent = useCallback((event: RoomEntryTileMessageEvent) =>
+ {
+ const parser = event.getParser();
+
+ if(!parser) return;
+
+ const settings = Object.assign({}, originalFloorplanSettings);
+ settings.entryPoint = [parser.x, parser.y];
+ settings.entryPointDir = parser.direction;
+ setOriginalFloorplanSettings(settings);
+
+ const vSettings = Object.assign({}, visualizationSettings);
+ vSettings.entryPointDir = parser.direction;
+ setVisualizationSettings(vSettings);
+
+ FloorplanEditor.instance.doorLocation = new NitroPoint(parser.x, parser.y);
+ setEntryTileReceived(true);
+ }, [originalFloorplanSettings, setOriginalFloorplanSettings, setVisualizationSettings, visualizationSettings]);
+
+ CreateMessageHook(RoomEntryTileMessageEvent, onRoomEntryTileMessageEvent);
+
+ useEffect(() =>
+ {
+ if(entryTileReceived && occupiedTilesReceived)
+ FloorplanEditor.instance.renderTiles();
+ }, [entryTileReceived, occupiedTilesReceived])
+
+ return (
+
+ );
+}
diff --git a/src/views/floorplan-editor/views/FloorplanImportExportView.tsx b/src/views/floorplan-editor/views/FloorplanImportExportView.tsx
new file mode 100644
index 00000000..274d5ec6
--- /dev/null
+++ b/src/views/floorplan-editor/views/FloorplanImportExportView.tsx
@@ -0,0 +1,68 @@
+import { UpdateFloorPropertiesMessageComposer } from '@nitrots/nitro-renderer';
+import { FC, useCallback, useState } from 'react';
+import { LocalizeText } from '../../../api';
+import { SendMessageHook, UseMountEffect } from '../../../hooks';
+import { NitroCardContentView, NitroCardHeaderView, NitroCardView, NitroLayoutFlex, NitroLayoutGridColumn } from '../../../layout';
+import { convertNumbersForSaving } from '../common/Utils';
+import { useFloorplanEditorContext } from '../context/FloorplanEditorContext';
+
+export const FloorplanImportExportView: FC = props =>
+{
+ const { originalFloorplanSettings = null, setOriginalFloorplanSettings = null } = useFloorplanEditorContext();
+
+ const { onCloseClick = null } = props;
+ const [ map, setMap ] = useState('');
+
+ const convertMapToString = useCallback((map: string) =>
+ {
+ return map.replace(/\r\n|\r|\n/g, '\n').toLowerCase();
+ }, []);
+
+ const revertChanges= useCallback(() =>
+ {
+ setMap(convertMapToString(originalFloorplanSettings.tilemap));
+ }, [convertMapToString, originalFloorplanSettings.tilemap]);
+
+ const saveFloorChanges = useCallback(() =>
+ {
+ SendMessageHook(new UpdateFloorPropertiesMessageComposer(
+ map.split('\n').join('\r'),
+ originalFloorplanSettings.entryPoint[0],
+ originalFloorplanSettings.entryPoint[1],
+ originalFloorplanSettings.entryPointDir,
+ convertNumbersForSaving(originalFloorplanSettings.thicknessWall),
+ convertNumbersForSaving(originalFloorplanSettings.thicknessFloor),
+ originalFloorplanSettings.wallHeight - 1
+ ));
+ }, [map, originalFloorplanSettings.entryPoint, originalFloorplanSettings.entryPointDir, originalFloorplanSettings.thicknessFloor, originalFloorplanSettings.thicknessWall, originalFloorplanSettings.wallHeight]);
+
+ UseMountEffect(() =>
+ {
+ revertChanges();
+ });
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export interface FloorplanImportExportViewProps
+{
+ onCloseClick(): void;
+}
diff --git a/src/views/floorplan-editor/views/FloorplanOptionsView.tsx b/src/views/floorplan-editor/views/FloorplanOptionsView.tsx
new file mode 100644
index 00000000..0c6fc18a
--- /dev/null
+++ b/src/views/floorplan-editor/views/FloorplanOptionsView.tsx
@@ -0,0 +1,188 @@
+import { FC, useCallback, useState } from 'react';
+import ReactSlider from 'react-slider';
+import { LocalizeText } from '../../../api';
+import { NitroCardGridItemView, NitroCardGridView, NitroLayoutFlex, NitroLayoutFlexColumn, NitroLayoutGrid, NitroLayoutGridColumn } from '../../../layout';
+import { NitroLayoutBase } from '../../../layout/base';
+import { COLORMAP, FloorAction } from '../common/Constants';
+import { FloorplanEditor } from '../common/FloorplanEditor';
+import { useFloorplanEditorContext } from '../context/FloorplanEditorContext';
+
+const MIN_WALL_HEIGHT: number = 0;
+const MAX_WALL_HEIGHT: number = 16;
+
+const MIN_FLOOR_HEIGHT: number = 0;
+const MAX_FLOOR_HEIGHT: number = 26;
+
+export const FloorplanOptionsView: FC<{}> = props =>
+{
+ const { visualizationSettings = null, setVisualizationSettings = null } = useFloorplanEditorContext();
+ const [ floorAction, setFloorAction ] = useState(FloorAction.SET);
+ const [ floorHeight, setFloorHeight ] = useState(0);
+
+ const selectAction = useCallback((action: number) =>
+ {
+ setFloorAction(action);
+ FloorplanEditor.instance.actionSettings.currentAction = action;
+ }, []);
+
+ const changeDoorDirection = useCallback(() =>
+ {
+ setVisualizationSettings(prevValue =>
+ {
+ const newValue = Object.assign({}, prevValue);
+
+ if(newValue.entryPointDir < 7)
+ {
+ ++newValue.entryPointDir;
+ }
+ else
+ {
+ newValue.entryPointDir = 0;
+ }
+
+ return newValue;
+ });
+ }, [ setVisualizationSettings ]);
+
+ const onFloorHeightChange = useCallback((value: number) =>
+ {
+ if(isNaN(value) || (value <= 0)) value = 0;
+
+ if(value > 26) value = 26;
+
+ setFloorHeight(value);
+
+ FloorplanEditor.instance.actionSettings.currentHeight = value.toString(36);
+ }, []);
+
+ const onFloorThicknessChange = useCallback((value: number) =>
+ {
+ setVisualizationSettings(prevValue =>
+ {
+ const newValue = Object.assign({}, prevValue);
+ newValue.thicknessFloor = value;
+
+ return newValue;
+ });
+ }, [setVisualizationSettings]);
+
+ const onWallThicknessChange = useCallback((value: number) =>
+ {
+ setVisualizationSettings(prevValue =>
+ {
+ const newValue = Object.assign({}, prevValue);
+ newValue.thicknessWall = value;
+
+ return newValue;
+ });
+ }, [setVisualizationSettings]);
+
+ const onWallHeightChange = useCallback((value: number) =>
+ {
+ if(isNaN(value) || (value <= 0)) value = MIN_WALL_HEIGHT;
+
+ if(value > MAX_WALL_HEIGHT) value = MAX_WALL_HEIGHT;
+
+ setVisualizationSettings(prevValue =>
+ {
+ const newValue = Object.assign({}, prevValue);
+
+ newValue.wallHeight = value;
+
+ return newValue;
+ });
+ }, [ setVisualizationSettings ]);
+
+ function increaseWallHeight(): void
+ {
+ let height = (visualizationSettings.wallHeight + 1);
+
+ if(height > MAX_WALL_HEIGHT) height = MAX_WALL_HEIGHT;
+
+ onWallHeightChange(height);
+ }
+
+ function decreaseWallHeight(): void
+ {
+ let height = (visualizationSettings.wallHeight - 1);
+
+ if(height <= 0) height = MIN_WALL_HEIGHT;
+
+ onWallHeightChange(height);
+ }
+
+ return (
+
+
+
+ { LocalizeText('floor.plan.editor.draw.mode') }
+
+ selectAction(FloorAction.SET) }>
+
+
+ selectAction(FloorAction.UNSET) }>
+
+
+ selectAction(FloorAction.UP) }>
+
+
+ selectAction(FloorAction.DOWN) }>
+
+
+ selectAction(FloorAction.DOOR) }>
+
+
+
+
+
+
+
+ { LocalizeText('floor.plan.editor.enter.direction') }
+
+
+
+
+
+ { LocalizeText('floor.editor.wall.height') }
+
+
+ onWallHeightChange(event.target.valueAsNumber)} />
+
+
+
+
+
+
+ { LocalizeText('floor.plan.editor.tile.height') }: { floorHeight }
+ onFloorHeightChange(event) }
+ renderThumb={ ({ style, ...rest }, state) => { state.valueNow }
} />
+
+
+
+
+ { LocalizeText('floor.plan.editor.room.options') }
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/views/groups/views/room-information/GroupRoomInformationView.tsx b/src/views/groups/views/room-information/GroupRoomInformationView.tsx
index 9b8f29b7..edbf18b4 100644
--- a/src/views/groups/views/room-information/GroupRoomInformationView.tsx
+++ b/src/views/groups/views/room-information/GroupRoomInformationView.tsx
@@ -1,4 +1,4 @@
-import { DesktopViewEvent, GroupInformationComposer, GroupInformationEvent, GroupInformationParser, GroupJoinComposer, GroupRemoveMemberComposer, RoomInfoEvent } from '@nitrots/nitro-renderer';
+import { DesktopViewEvent, GetGuestRoomResultEvent, GroupInformationComposer, GroupInformationEvent, GroupInformationParser, GroupJoinComposer, GroupRemoveMemberComposer } from '@nitrots/nitro-renderer';
import { FC, useCallback, useState } from 'react';
import { GetGroupInformation, GetSessionDataManager, LocalizeText } from '../../../../api';
import { GetGroupManager } from '../../../../api/groups/GetGroupManager';
@@ -13,7 +13,7 @@ export const GroupRoomInformationView: FC<{}> = props =>
const [ groupInformation, setGroupInformation ] = useState(null);
const [ isExpended, setIsExpended ] = useState(true);
- const onRoomInfoEvent = useCallback((event: RoomInfoEvent) =>
+ const onGetGuestRoomResultEvent = useCallback((event: GetGuestRoomResultEvent) =>
{
const parser = event.getParser();
@@ -26,7 +26,7 @@ export const GroupRoomInformationView: FC<{}> = props =>
}
}, []);
- CreateMessageHook(RoomInfoEvent, onRoomInfoEvent);
+ CreateMessageHook(GetGuestRoomResultEvent, onGetGuestRoomResultEvent);
const onGroupInformationEvent = useCallback((event: GroupInformationEvent) =>
{
diff --git a/src/views/help/HelpMessageHandler.tsx b/src/views/help/HelpMessageHandler.tsx
index f71634b7..959abdb9 100644
--- a/src/views/help/HelpMessageHandler.tsx
+++ b/src/views/help/HelpMessageHandler.tsx
@@ -1,12 +1,10 @@
-import { CallForHelpResultMessageEvent, FurnitureListItemParser, PetData } from '@nitrots/nitro-renderer';
+import { CallForHelpResultMessageEvent } from '@nitrots/nitro-renderer';
import { FC, useCallback } from 'react';
import { LocalizeText } from '../../api';
import { CreateMessageHook } from '../../hooks/messages/message-event';
import { NotificationAlertType } from '../notification-center/common/NotificationAlertType';
import { NotificationUtilities } from '../notification-center/common/NotificationUtilities';
import { GetCloseReasonKey } from './common/GetCloseReasonKey';
-let furniMsgFragments: Map[] = null;
-let petMsgFragments: Map[] = null;
export const HelpMessageHandler: FC<{}> = props =>
{
diff --git a/src/views/help/HelpView.scss b/src/views/help/HelpView.scss
new file mode 100644
index 00000000..f7f513f8
--- /dev/null
+++ b/src/views/help/HelpView.scss
@@ -0,0 +1,10 @@
+.nitro-help {
+ height: 430px;
+ width: 300px;
+
+ .index-image {
+ background: url('../../assets/images/help/help_index.png');
+ width: 126px;
+ height: 105px;
+ }
+}
diff --git a/src/views/help/HelpView.tsx b/src/views/help/HelpView.tsx
index 4917c713..630d41d1 100644
--- a/src/views/help/HelpView.tsx
+++ b/src/views/help/HelpView.tsx
@@ -1,11 +1,79 @@
-import { FC } from 'react';
+import { FC, useCallback, useState } from 'react';
+import { LocalizeText } from '../../api';
+import { HelpEvent } from '../../events/help/HelpEvent';
+import { HelpReportUserEvent } from '../../events/help/HelpReportUserEvent';
+import { useUiEvent } from '../../hooks';
+import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../layout';
+import { HelpContextProvider } from './context/HelpContext';
+import { IHelpReportState, initialReportState } from './context/HelpContext.types';
import { HelpMessageHandler } from './HelpMessageHandler';
+import { DescribeReportView } from './views/DescribeReportView';
+import { HelpIndexView } from './views/HelpIndexView';
+import { SelectReportedChatsView } from './views/SelectReportedChatsView';
+import { SelectReportedUserView } from './views/SelectReportedUserView';
+import { SelectTopicView } from './views/SelectTopicView';
export const HelpView: FC<{}> = props =>
{
+ const [isVisible, setIsVisible] = useState(false);
+ const [ helpReportState, setHelpReportState ] = useState(initialReportState);
+
+ const onHelpEvent = useCallback((event: HelpEvent) =>
+ {
+ setHelpReportState(initialReportState);
+
+ switch(event.type)
+ {
+ case HelpEvent.SHOW_HELP_CENTER:
+ setIsVisible(true);
+ break;
+ case HelpEvent.HIDE_HELP_CENTER:
+ setIsVisible(false);
+ break;
+ case HelpEvent.TOGGLE_HELP_CENTER:
+ setIsVisible(!isVisible);
+ break;
+ case HelpReportUserEvent.REPORT_USER:
+ const reportEvent = event as HelpReportUserEvent;
+ const reportState = Object.assign({}, helpReportState );
+ reportState.reportedUserId = reportEvent.reportedUserId;
+ reportState.currentStep = 2;
+ setHelpReportState(reportState);
+ setIsVisible(true);
+ break;
+ }
+ }, [helpReportState, isVisible]);
+
+ useUiEvent(HelpEvent.TOGGLE_HELP_CENTER, onHelpEvent);
+ useUiEvent(HelpEvent.SHOW_HELP_CENTER, onHelpEvent);
+ useUiEvent(HelpEvent.HIDE_HELP_CENTER, onHelpEvent);
+ useUiEvent(HelpReportUserEvent.REPORT_USER, onHelpEvent);
+
+ const CurrentStepView = useCallback(() =>
+ {
+ switch(helpReportState.currentStep)
+ {
+ case 0: return
+ case 1: return
+ case 2: return
+ case 3: return
+ case 4: return
+ }
+
+ return null;
+ }, [helpReportState.currentStep]);
+
return (
- <>
+
- >
+ {isVisible &&
+
+ setIsVisible(false)} />
+
+
+
+
+ }
+
);
}
diff --git a/src/views/help/common/IUser.ts b/src/views/help/common/IUser.ts
new file mode 100644
index 00000000..bd3852ab
--- /dev/null
+++ b/src/views/help/common/IUser.ts
@@ -0,0 +1,5 @@
+export interface IUser
+{
+ id: number;
+ username: string;
+}
diff --git a/src/views/help/context/HelpContext.tsx b/src/views/help/context/HelpContext.tsx
new file mode 100644
index 00000000..2cad4a52
--- /dev/null
+++ b/src/views/help/context/HelpContext.tsx
@@ -0,0 +1,14 @@
+import { createContext, FC, useContext } from 'react';
+import { HelpContextProps, IHelpContext } from './HelpContext.types';
+
+const HelpContext = createContext({
+ helpReportState: null,
+ setHelpReportState: null
+});
+
+export const HelpContextProvider: FC = props =>
+{
+ return { props.children }
+}
+
+export const useHelpContext = () => useContext(HelpContext);
diff --git a/src/views/help/context/HelpContext.types.ts b/src/views/help/context/HelpContext.types.ts
new file mode 100644
index 00000000..9368308b
--- /dev/null
+++ b/src/views/help/context/HelpContext.types.ts
@@ -0,0 +1,33 @@
+import { ProviderProps } from 'react';
+import { IChatEntry } from '../../chat-history/context/ChatHistoryContext.types';
+
+export interface IHelpContext
+{
+ helpReportState: IHelpReportState;
+ setHelpReportState: React.Dispatch>;
+}
+
+export interface IHelpReportState {
+ reportedUserId: number;
+ reportedChats: IChatEntry[];
+ cfhCategory: number;
+ cfhTopic: number;
+ roomId: number;
+ message: string;
+ currentStep: number;
+}
+
+export const initialReportState: IHelpReportState = {
+ reportedUserId: -1,
+ reportedChats: [],
+ cfhCategory: -1,
+ cfhTopic: -1,
+ roomId: -1,
+ message: '',
+ currentStep: 0
+}
+
+export interface HelpContextProps extends ProviderProps
+{
+
+}
diff --git a/src/views/help/views/DescribeReportView.tsx b/src/views/help/views/DescribeReportView.tsx
new file mode 100644
index 00000000..f936d225
--- /dev/null
+++ b/src/views/help/views/DescribeReportView.tsx
@@ -0,0 +1,50 @@
+import { CallForHelpMessageComposer } from '@nitrots/nitro-renderer';
+import { FC, useCallback, useState } from 'react';
+import { LocalizeText } from '../../../api';
+import { HelpEvent } from '../../../events/help/HelpEvent';
+import { dispatchUiEvent, SendMessageHook } from '../../../hooks';
+import { useHelpContext } from '../context/HelpContext';
+
+export const DescribeReportView: FC<{}> = props =>
+{
+ const { helpReportState = null, setHelpReportState = null } = useHelpContext();
+ const [message, setMessage] = useState('');
+
+ const submitReport = useCallback(() =>
+ {
+ if(message.length < 15) return;
+
+ const reportState = Object.assign({}, helpReportState);
+
+ reportState.message = message;
+
+ setHelpReportState(reportState);
+
+ const roomId = reportState.reportedChats[0].roomId;
+ const chats: (string | number )[] = [];
+ reportState.reportedChats.forEach(entry =>
+ {
+ chats.push(entry.entityId);
+ chats.push(entry.message);
+ });
+
+ SendMessageHook(new CallForHelpMessageComposer(message, reportState.cfhTopic, reportState.reportedUserId, roomId, chats));
+
+ dispatchUiEvent(new HelpEvent(HelpEvent.HIDE_HELP_CENTER));
+ }, [helpReportState, message, setHelpReportState]);
+
+ return (
+ <>
+
+
{LocalizeText('help.emergency.chat_report.subtitle')}
+
{LocalizeText('help.cfh.input.text')}
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/views/help/views/HelpIndexView.tsx b/src/views/help/views/HelpIndexView.tsx
new file mode 100644
index 00000000..35d85270
--- /dev/null
+++ b/src/views/help/views/HelpIndexView.tsx
@@ -0,0 +1,36 @@
+import { FC, useCallback } from 'react';
+import { LocalizeText } from '../../../api';
+import { useHelpContext } from '../context/HelpContext';
+
+export const HelpIndexView: FC<{}> = props =>
+{
+ const { helpReportState = null, setHelpReportState = null } = useHelpContext();
+
+ const onReportClick = useCallback(() =>
+ {
+ const reportState = Object.assign({}, helpReportState );
+ reportState.currentStep = 1;
+ setHelpReportState(reportState);
+ },[helpReportState, setHelpReportState]);
+
+ return (
+ <>
+
+
{LocalizeText('help.main.frame.title')}
+
+
{LocalizeText('help.main.self.description')}
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/src/views/help/views/SelectReportedChatsView.tsx b/src/views/help/views/SelectReportedChatsView.tsx
new file mode 100644
index 00000000..ce1080b1
--- /dev/null
+++ b/src/views/help/views/SelectReportedChatsView.tsx
@@ -0,0 +1,88 @@
+import { RoomObjectType } from '@nitrots/nitro-renderer';
+import { FC, useCallback, useMemo, useState } from 'react';
+import { LocalizeText } from '../../../api';
+import { NitroCardGridItemView, NitroCardGridView } from '../../../layout';
+import { GetChatHistory } from '../../chat-history/common/GetChatHistory';
+import { ChatEntryType, IChatEntry } from '../../chat-history/context/ChatHistoryContext.types';
+import { useHelpContext } from '../context/HelpContext';
+
+export const SelectReportedChatsView: FC<{}> = props =>
+{
+ const { helpReportState = null, setHelpReportState = null } = useHelpContext();
+ const [ selectedChats, setSelectedChats ] = useState
);
}
diff --git a/src/views/mod-tools/ModToolsMessageHandler.tsx b/src/views/mod-tools/ModToolsMessageHandler.tsx
new file mode 100644
index 00000000..b810aa13
--- /dev/null
+++ b/src/views/mod-tools/ModToolsMessageHandler.tsx
@@ -0,0 +1,267 @@
+import { CfhSanctionMessageEvent, CfhTopicsInitEvent, IssueDeletedMessageEvent, IssueInfoMessageEvent, IssuePickFailedMessageEvent, ModeratorActionResultMessageEvent, ModeratorInitMessageEvent, ModeratorToolPreferencesEvent, RoomEngineEvent } from '@nitrots/nitro-renderer';
+import { FC, useCallback } from 'react';
+import { NotificationAlertEvent } from '../../events';
+import { ModToolsEvent } from '../../events/mod-tools/ModToolsEvent';
+import { ModToolsOpenRoomChatlogEvent } from '../../events/mod-tools/ModToolsOpenRoomChatlogEvent';
+import { ModToolsOpenRoomInfoEvent } from '../../events/mod-tools/ModToolsOpenRoomInfoEvent';
+import { ModToolsOpenUserChatlogEvent } from '../../events/mod-tools/ModToolsOpenUserChatlogEvent';
+import { ModToolsOpenUserInfoEvent } from '../../events/mod-tools/ModToolsOpenUserInfoEvent';
+import { CreateMessageHook, dispatchUiEvent, useRoomEngineEvent, useUiEvent } from '../../hooks';
+import { SetCfhCategories } from './common/GetCFHCategories';
+import { useModToolsContext } from './context/ModToolsContext';
+import { ModToolsActions } from './reducers/ModToolsReducer';
+
+export const ModToolsMessageHandler: FC<{}> = props =>
+{
+ const { modToolsState = null, dispatchModToolsState = null } = useModToolsContext();
+ const { openRooms = null, openRoomChatlogs = null, openUserChatlogs = null, openUserInfo = null, tickets= null } = modToolsState;
+
+ const onModeratorInitMessageEvent = useCallback((event: ModeratorInitMessageEvent) =>
+ {
+ const parser = event.getParser();
+
+ if(!parser) return;
+
+ const data = parser.data;
+
+ dispatchModToolsState({
+ type: ModToolsActions.SET_INIT_DATA,
+ payload: {
+ settings: data
+ }
+ });
+
+ dispatchModToolsState({
+ type: ModToolsActions.SET_TICKETS,
+ payload: {
+ tickets: data.issues
+ }
+ });
+
+ console.log(parser);
+ }, [dispatchModToolsState]);
+
+ const onIssueInfoMessageEvent = useCallback((event: IssueInfoMessageEvent) =>
+ {
+ const parser = event.getParser();
+
+ if(!parser) return;
+
+ const newTickets = tickets ? Array.from(tickets) : [];
+ const existingIndex = newTickets.findIndex( entry => entry.issueId === parser.issueData.issueId)
+
+ if(existingIndex > -1)
+ {
+ newTickets[existingIndex] = parser.issueData;
+ }
+ else
+ {
+ newTickets.push(parser.issueData);
+ }
+
+ dispatchModToolsState({
+ type: ModToolsActions.SET_TICKETS,
+ payload: {
+ tickets: newTickets
+ }
+ });
+
+ //todo: play ticket sound
+ //GetNitroInstance().events.dispatchEvent(new NitroSoundEvent(NitroSoundEvent.PLAY_SOUND, sound)
+ console.log(parser);
+ }, [dispatchModToolsState, tickets]);
+
+ const onModeratorToolPreferencesEvent = useCallback((event: ModeratorToolPreferencesEvent) =>
+ {
+ const parser = event.getParser();
+
+ if(!parser) return;
+
+ console.log(parser);
+ }, []);
+
+ const onIssuePickFailedMessageEvent = useCallback((event: IssuePickFailedMessageEvent) =>
+ {
+ const parser = event.getParser();
+
+ if(!parser) return;
+
+ // todo: let user know it failed
+ dispatchUiEvent(new NotificationAlertEvent(['Failed to pick issue'], null, null, null, 'Error', null));
+ }, []);
+
+ const onIssueDeletedMessageEvent = useCallback((event: IssueDeletedMessageEvent) =>
+ {
+ const parser = event.getParser();
+
+ if(!parser) return;
+
+ const newTickets = tickets ? Array.from(tickets) : [];
+ const existingIndex = newTickets.findIndex( entry => entry.issueId === parser.issueId);
+
+ if(existingIndex === -1) return;
+
+ newTickets.splice(existingIndex, 1);
+
+ dispatchModToolsState({
+ type: ModToolsActions.SET_TICKETS,
+ payload: {
+ tickets: newTickets
+ }
+ });
+ }, [dispatchModToolsState, tickets]);
+
+ const onModeratorActionResultMessageEvent = useCallback((event: ModeratorActionResultMessageEvent) =>
+ {
+ const parser = event.getParser();
+
+ if(!parser) return;
+
+ if(parser.success)
+ {
+ dispatchUiEvent(new NotificationAlertEvent(['Moderation action was successfull'], null, null, null, 'Success', null));
+ }
+ else
+ {
+ dispatchUiEvent(new NotificationAlertEvent(['There was a problem applying that moderation action'], null, null, null, 'Error', null));
+ }
+ }, []);
+
+ const onCfhTopicsInitEvent = useCallback((event: CfhTopicsInitEvent) =>
+ {
+ const parser = event.getParser();
+
+ if(!parser) return;
+
+ const categories = parser.callForHelpCategories;
+
+ dispatchModToolsState({
+ type: ModToolsActions.SET_CFH_CATEGORIES,
+ payload: {
+ cfhCategories: categories
+ }
+ });
+
+ SetCfhCategories(categories);
+
+ console.log(parser);
+ }, [dispatchModToolsState]);
+
+ const onCfhSanctionMessageEvent = useCallback((event: CfhSanctionMessageEvent) =>
+ {
+ const parser = event.getParser();
+
+ if(!parser) return;
+
+ console.log(parser);
+ }, []);
+
+ CreateMessageHook(ModeratorInitMessageEvent, onModeratorInitMessageEvent);
+ CreateMessageHook(IssueInfoMessageEvent, onIssueInfoMessageEvent);
+ CreateMessageHook(ModeratorToolPreferencesEvent, onModeratorToolPreferencesEvent);
+ CreateMessageHook(IssuePickFailedMessageEvent, onIssuePickFailedMessageEvent);
+ CreateMessageHook(IssueDeletedMessageEvent, onIssueDeletedMessageEvent);
+ CreateMessageHook(ModeratorActionResultMessageEvent, onModeratorActionResultMessageEvent);
+ CreateMessageHook(CfhTopicsInitEvent, onCfhTopicsInitEvent);
+ CreateMessageHook(CfhSanctionMessageEvent, onCfhSanctionMessageEvent);
+
+ const onRoomEngineEvent = useCallback((event: RoomEngineEvent) =>
+ {
+ switch(event.type)
+ {
+ case RoomEngineEvent.INITIALIZED:
+ dispatchModToolsState({
+ type: ModToolsActions.SET_CURRENT_ROOM_ID,
+ payload: {
+ currentRoomId: event.roomId
+ }
+ });
+ return;
+ case RoomEngineEvent.DISPOSED:
+ dispatchModToolsState({
+ type: ModToolsActions.SET_CURRENT_ROOM_ID,
+ payload: {
+ currentRoomId: null
+ }
+ });
+ return;
+ }
+ }, [ dispatchModToolsState ]);
+
+ useRoomEngineEvent(RoomEngineEvent.INITIALIZED, onRoomEngineEvent);
+ useRoomEngineEvent(RoomEngineEvent.DISPOSED, onRoomEngineEvent);
+
+ const onModToolsEvent = useCallback((event: ModToolsEvent) =>
+ {
+ switch(event.type)
+ {
+ case ModToolsEvent.OPEN_ROOM_INFO: {
+ const castedEvent = (event as ModToolsOpenRoomInfoEvent);
+
+ if(openRooms && openRooms.includes(castedEvent.roomId)) return;
+
+ const rooms = openRooms || [];
+
+ dispatchModToolsState({
+ type: ModToolsActions.SET_OPEN_ROOMS,
+ payload: {
+ openRooms: [...rooms, castedEvent.roomId]
+ }
+ });
+ return;
+ }
+ case ModToolsEvent.OPEN_ROOM_CHATLOG: {
+ const castedEvent = (event as ModToolsOpenRoomChatlogEvent);
+
+ if(openRoomChatlogs && openRoomChatlogs.includes(castedEvent.roomId)) return;
+
+ const chatlogs = openRoomChatlogs || [];
+
+ dispatchModToolsState({
+ type: ModToolsActions.SET_OPEN_ROOM_CHATLOGS,
+ payload: {
+ openRoomChatlogs: [...chatlogs, castedEvent.roomId]
+ }
+ });
+ return;
+ }
+ case ModToolsEvent.OPEN_USER_INFO: {
+ const castedEvent = (event as ModToolsOpenUserInfoEvent);
+
+ if(openUserInfo && openUserInfo.includes(castedEvent.userId)) return;
+
+ const userInfo = openUserInfo || [];
+
+ dispatchModToolsState({
+ type: ModToolsActions.SET_OPEN_USERINFO,
+ payload: {
+ openUserInfo: [...userInfo, castedEvent.userId]
+ }
+ });
+ return;
+ }
+ case ModToolsEvent.OPEN_USER_CHATLOG: {
+ const castedEvent = (event as ModToolsOpenUserChatlogEvent);
+
+ if(openUserChatlogs && openUserChatlogs.includes(castedEvent.userId)) return;
+
+ const userChatlog = openUserChatlogs || [];
+
+ dispatchModToolsState({
+ type: ModToolsActions.SET_OPEN_USER_CHATLOGS,
+ payload: {
+ openUserChatlogs: [...userChatlog, castedEvent.userId]
+ }
+ });
+ return;
+ }
+ }
+ }, [openRooms, dispatchModToolsState, openRoomChatlogs, openUserInfo, openUserChatlogs]);
+
+ useUiEvent(ModToolsEvent.OPEN_ROOM_INFO, onModToolsEvent);
+ useUiEvent(ModToolsEvent.OPEN_ROOM_CHATLOG, onModToolsEvent);
+ useUiEvent(ModToolsEvent.OPEN_USER_INFO, onModToolsEvent);
+ useUiEvent(ModToolsEvent.OPEN_USER_CHATLOG, onModToolsEvent);
+
+ return null;
+}
diff --git a/src/views/mod-tools/ModToolsView.scss b/src/views/mod-tools/ModToolsView.scss
index 9f5554e9..e8fb9e18 100644
--- a/src/views/mod-tools/ModToolsView.scss
+++ b/src/views/mod-tools/ModToolsView.scss
@@ -2,5 +2,8 @@
width: 200px;
}
-@import './views/chatlog/ModToolsChatlogView';
-@import './views/room/ModToolsRoomView';
+@import './views/room/room-tools/ModToolsRoomView';
+@import './views/chatlog/ChatlogView';
+@import './views/user/user-info/ModToolsUserView';
+@import './views/user/user-room-visits/ModToolsUserRoomVisitsView';
+@import './views/tickets/ModToolsTicketView';
diff --git a/src/views/mod-tools/ModToolsView.tsx b/src/views/mod-tools/ModToolsView.tsx
index b6591d92..902ec5b9 100644
--- a/src/views/mod-tools/ModToolsView.tsx
+++ b/src/views/mod-tools/ModToolsView.tsx
@@ -1,26 +1,30 @@
-import { RoomEngineEvent } from '@nitrots/nitro-renderer';
-import { FC, useCallback, useEffect, useReducer, useState } from 'react';
+import { RoomEngineObjectEvent, RoomObjectCategory } from '@nitrots/nitro-renderer';
+import { FC, useCallback, useReducer, useState } from 'react';
+import { GetRoomSession } from '../../api';
import { ModToolsEvent } from '../../events/mod-tools/ModToolsEvent';
+import { ModToolsOpenRoomChatlogEvent } from '../../events/mod-tools/ModToolsOpenRoomChatlogEvent';
import { ModToolsOpenRoomInfoEvent } from '../../events/mod-tools/ModToolsOpenRoomInfoEvent';
-import { ModToolsSelectUserEvent } from '../../events/mod-tools/ModToolsSelectUserEvent';
+import { ModToolsOpenUserInfoEvent } from '../../events/mod-tools/ModToolsOpenUserInfoEvent';
import { useRoomEngineEvent } from '../../hooks/events';
import { dispatchUiEvent, useUiEvent } from '../../hooks/events/ui/ui-event';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../layout';
import { ModToolsContextProvider } from './context/ModToolsContext';
+import { ModToolsMessageHandler } from './ModToolsMessageHandler';
import { ModToolsViewProps } from './ModToolsView.types';
import { initialModTools, ModToolsActions, ModToolsReducer } from './reducers/ModToolsReducer';
-import { ModToolsRoomView } from './views/room/ModToolsRoomView';
+import { ISelectedUser } from './utils/ISelectedUser';
+import { ModToolsChatlogView } from './views/room/room-chatlog/ModToolsChatlogView';
+import { ModToolsRoomView } from './views/room/room-tools/ModToolsRoomView';
import { ModToolsTicketsView } from './views/tickets/ModToolsTicketsView';
-import { ModToolsUserView } from './views/user/ModToolsUserView';
+import { ModToolsUserChatlogView } from './views/user/user-chatlog/ModToolsUserChatlogView';
+import { ModToolsUserView } from './views/user/user-info/ModToolsUserView';
export const ModToolsView: FC
= props =>
{
const [ isVisible, setIsVisible ] = useState(false);
const [ modToolsState, dispatchModToolsState ] = useReducer(ModToolsReducer, initialModTools);
- const { currentRoomId = null, selectedUser = null, openRooms = null, openChatlogs = null } = modToolsState;
-
- const [ isRoomVisible, setIsRoomVisible ] = useState(false);
- const [ isUserVisible, setIsUserVisible ] = useState(false);
+ const { currentRoomId = null, openRooms = null, openRoomChatlogs = null, openUserChatlogs = null, openUserInfo = null } = modToolsState;
+ const [ selectedUser, setSelectedUser] = useState(null);
const [ isTicketsVisible, setIsTicketsVisible ] = useState(false);
const onModToolsEvent = useCallback((event: ModToolsEvent) =>
@@ -36,67 +40,29 @@ export const ModToolsView: FC = props =>
case ModToolsEvent.TOGGLE_MOD_TOOLS:
setIsVisible(value => !value);
return;
- case ModToolsEvent.SELECT_USER: {
- const castedEvent = (event as ModToolsSelectUserEvent);
-
- dispatchModToolsState({
- type: ModToolsActions.SET_SELECTED_USER,
- payload: {
- selectedUser: {
- webID: castedEvent.webID,
- name: castedEvent.name
- }
- }
- });
- return;
- }
- case ModToolsEvent.OPEN_ROOM_INFO: {
- const castedEvent = (event as ModToolsOpenRoomInfoEvent);
-
- if(openRooms && openRooms.includes(castedEvent.roomId)) return;
-
- dispatchModToolsState({
- type: ModToolsActions.SET_OPEN_ROOMS,
- payload: {
- openRooms: [...openRooms, castedEvent.roomId]
- }
- });
- return;
- }
}
- }, [ dispatchModToolsState, setIsVisible, openRooms ]);
+ }, []);
useUiEvent(ModToolsEvent.SHOW_MOD_TOOLS, onModToolsEvent);
useUiEvent(ModToolsEvent.HIDE_MOD_TOOLS, onModToolsEvent);
useUiEvent(ModToolsEvent.TOGGLE_MOD_TOOLS, onModToolsEvent);
- useUiEvent(ModToolsEvent.SELECT_USER, onModToolsEvent);
- useUiEvent(ModToolsEvent.OPEN_ROOM_INFO, onModToolsEvent);
-
- const onRoomEngineEvent = useCallback((event: RoomEngineEvent) =>
+
+ const onRoomEngineObjectEvent = useCallback((event: RoomEngineObjectEvent) =>
{
- switch(event.type)
- {
- case RoomEngineEvent.INITIALIZED:
- dispatchModToolsState({
- type: ModToolsActions.SET_CURRENT_ROOM_ID,
- payload: {
- currentRoomId: event.roomId
- }
- });
- return;
- case RoomEngineEvent.DISPOSED:
- dispatchModToolsState({
- type: ModToolsActions.SET_CURRENT_ROOM_ID,
- payload: {
- currentRoomId: null
- }
- });
- return;
- }
- }, [ dispatchModToolsState ]);
+ if(event.category !== RoomObjectCategory.UNIT) return;
- useRoomEngineEvent(RoomEngineEvent.INITIALIZED, onRoomEngineEvent);
- useRoomEngineEvent(RoomEngineEvent.DISPOSED, onRoomEngineEvent);
+ const roomSession = GetRoomSession();
+
+ if(!roomSession) return;
+
+ const userData = roomSession.userDataManager.getUserDataByIndex(event.objectId);
+
+ if(!userData) return;
+
+ setSelectedUser({ userId: userData.webID, username: userData.name });
+ }, []);
+
+ useRoomEngineEvent(RoomEngineObjectEvent.SELECTED, onRoomEngineObjectEvent);
const handleClick = useCallback((action: string, value?: string) =>
{
@@ -111,9 +77,7 @@ export const ModToolsView: FC = props =>
return;
}
- const itemIndex = openRooms.indexOf(currentRoomId);
-
- if(itemIndex > -1)
+ if(openRooms.indexOf(currentRoomId) > -1)
{
handleClick('close_room', currentRoomId.toString());
}
@@ -137,46 +101,124 @@ export const ModToolsView: FC = props =>
});
return;
}
- case 'close_chatlog': {
- const itemIndex = openChatlogs.indexOf(Number(value));
+ case 'toggle_room_chatlog': {
+ if(!openRoomChatlogs)
+ {
+ dispatchUiEvent(new ModToolsOpenRoomChatlogEvent(currentRoomId));
+ return;
+ }
- const clone = Array.from(openChatlogs);
+ if(openRoomChatlogs.indexOf(currentRoomId) > -1)
+ {
+ handleClick('close_room_chatlog', currentRoomId.toString());
+ }
+ else
+ {
+ dispatchUiEvent(new ModToolsOpenRoomChatlogEvent(currentRoomId));
+ }
+ return;
+ }
+ case 'close_room_chatlog': {
+ const itemIndex = openRoomChatlogs.indexOf(Number(value));
+
+ const clone = Array.from(openRoomChatlogs);
clone.splice(itemIndex, 1);
dispatchModToolsState({
- type: ModToolsActions.SET_OPEN_CHATLOGS,
+ type: ModToolsActions.SET_OPEN_ROOM_CHATLOGS,
payload: {
- openChatlogs: clone
+ openRoomChatlogs: clone
+ }
+ });
+ return;
+ }
+ case 'toggle_user_info': {
+
+ if(!selectedUser) return;
+
+ const userId = selectedUser.userId;
+
+ if(!openUserInfo)
+ {
+ dispatchUiEvent(new ModToolsOpenUserInfoEvent(userId));
+ return;
+ }
+
+ if(openUserInfo.indexOf(userId) > -1)
+ {
+ handleClick('close_user_info', userId.toString());
+ }
+ else
+ {
+ dispatchUiEvent(new ModToolsOpenUserInfoEvent(userId));
+ }
+ return;
+ }
+ case 'close_user_info': {
+ const itemIndex = openUserInfo.indexOf(Number(value));
+
+ const clone = Array.from(openUserInfo);
+ clone.splice(itemIndex, 1);
+
+ dispatchModToolsState({
+ type: ModToolsActions.SET_OPEN_USERINFO,
+ payload: {
+ openUserInfo: clone
+ }
+ });
+ return;
+ }
+ case 'close_user_chatlog': {
+ const itemIndex = openUserChatlogs.indexOf(Number(value));
+
+ const clone = Array.from(openUserChatlogs);
+ clone.splice(itemIndex, 1);
+
+ dispatchModToolsState({
+ type: ModToolsActions.SET_OPEN_USER_CHATLOGS,
+ payload: {
+ openUserChatlogs: clone
}
});
return;
}
}
- }, [ dispatchModToolsState, openRooms, openChatlogs, currentRoomId ]);
-
- useEffect(() =>
- {
- if(!isVisible) return;
- }, [ isVisible ]);
+ }, [openRooms, currentRoomId, openRoomChatlogs, selectedUser, openUserInfo, openUserChatlogs]);
return (
+
{ isVisible &&
setIsVisible(false) } />
-
-
+
+
}
{ openRooms && openRooms.map(roomId =>
{
return handleClick('close_room', roomId.toString()) } />;
- }) }
+ })
+ }
+ { openRoomChatlogs && openRoomChatlogs.map(roomId =>
+ {
+ return handleClick('close_room_chatlog', roomId.toString()) } />;
+ })
+ }
+ { openUserInfo && openUserInfo.map(userId =>
+ {
+ return handleClick('close_user_info', userId.toString())}/>
+ })
+ }
+ { openUserChatlogs && openUserChatlogs.map(userId =>
+ {
+ return handleClick('close_user_chatlog', userId.toString())}/>
+ })
+ }
- { isUserVisible && }
{ isTicketsVisible && setIsTicketsVisible(false) } /> }
);
diff --git a/src/views/mod-tools/common/GetCFHCategories.ts b/src/views/mod-tools/common/GetCFHCategories.ts
new file mode 100644
index 00000000..a9e8a57f
--- /dev/null
+++ b/src/views/mod-tools/common/GetCFHCategories.ts
@@ -0,0 +1,7 @@
+import { CallForHelpCategoryData } from '@nitrots/nitro-renderer';
+
+let cfhCategories: CallForHelpCategoryData[] = [];
+
+export const SetCfhCategories = (categories: CallForHelpCategoryData[]) => (cfhCategories = categories);
+
+export const GetCfhCategories = () => cfhCategories;
diff --git a/src/views/mod-tools/common/IssueCategoryNames.ts b/src/views/mod-tools/common/IssueCategoryNames.ts
new file mode 100644
index 00000000..bb1ca2c5
--- /dev/null
+++ b/src/views/mod-tools/common/IssueCategoryNames.ts
@@ -0,0 +1,35 @@
+export const getSourceName = (categoryId: number): string =>
+{
+ switch(categoryId)
+ {
+ case 1:
+ case 2:
+ return 'Normal';
+ case 3:
+ return 'Automatic';
+ case 4:
+ return 'Automatic IM';
+ case 5:
+ return 'Guide System';
+ case 6:
+ return 'IM';
+ case 7:
+ return 'Room';
+ case 8:
+ return 'Panic';
+ case 9:
+ return 'Guardian';
+ case 10:
+ return 'Automatic Helper';
+ case 11:
+ return 'Discussion';
+ case 12:
+ return 'Selfie';
+ case 14:
+ return 'Photo';
+ case 15:
+ return 'Ambassador';
+ default:
+ return 'Unknown';
+ }
+}
diff --git a/src/views/mod-tools/reducers/ModToolsReducer.tsx b/src/views/mod-tools/reducers/ModToolsReducer.tsx
index 955d70b9..b6fcf343 100644
--- a/src/views/mod-tools/reducers/ModToolsReducer.tsx
+++ b/src/views/mod-tools/reducers/ModToolsReducer.tsx
@@ -1,48 +1,65 @@
+import { CallForHelpCategoryData, IssueMessageData, ModeratorInitData } from '@nitrots/nitro-renderer';
import { Reducer } from 'react';
export interface IModToolsState
{
- selectedUser: {webID: number, name: string};
+ settings: ModeratorInitData;
currentRoomId: number;
openRooms: number[];
- openChatlogs: number[];
+ openRoomChatlogs: number[];
+ openUserInfo: number[];
+ openUserChatlogs: number[];
+ tickets: IssueMessageData[]
+ cfhCategories: CallForHelpCategoryData[];
}
export interface IModToolsAction
{
type: string;
payload: {
- selectedUser?: {webID: number, name: string};
+ settings?: ModeratorInitData;
currentRoomId?: number;
openRooms?: number[];
- openChatlogs?: number[];
+ openRoomChatlogs?: number[];
+ openUserInfo?: number[];
+ openUserChatlogs?: number[];
+ tickets?: IssueMessageData[];
+ cfhCategories?: CallForHelpCategoryData[];
}
}
export class ModToolsActions
{
- public static SET_SELECTED_USER: string = 'MTA_SET_SELECTED_USER';
+ public static SET_INIT_DATA: string = 'MTA_SET_INIT_DATA';
public static SET_CURRENT_ROOM_ID: string = 'MTA_SET_CURRENT_ROOM_ID';
public static SET_OPEN_ROOMS: string = 'MTA_SET_OPEN_ROOMS';
- public static SET_OPEN_CHATLOGS: string = 'MTA_SET_OPEN_CHATLOGS';
+ public static SET_OPEN_USERINFO: string = 'MTA_SET_OPEN_USERINFO';
+ public static SET_OPEN_ROOM_CHATLOGS: string = 'MTA_SET_OPEN_CHATLOGS';
+ public static SET_OPEN_USER_CHATLOGS: string = 'MTA_SET_OPEN_USER_CHATLOGS';
+ public static SET_TICKETS: string = 'MTA_SET_TICKETS';
+ public static SET_CFH_CATEGORIES: string = 'MTA_SET_CFH_CATEGORIES';
public static RESET_STATE: string = 'MTA_RESET_STATE';
}
export const initialModTools: IModToolsState = {
- selectedUser: null,
+ settings: null,
currentRoomId: null,
openRooms: null,
- openChatlogs: null
+ openRoomChatlogs: null,
+ openUserChatlogs: null,
+ openUserInfo: null,
+ tickets: null,
+ cfhCategories: null
};
export const ModToolsReducer: Reducer = (state, action) =>
{
switch(action.type)
{
- case ModToolsActions.SET_SELECTED_USER: {
- const selectedUser = (action.payload.selectedUser || state.selectedUser || null);
+ case ModToolsActions.SET_INIT_DATA: {
+ const settings = (action.payload.settings || state.settings || null);
- return { ...state, selectedUser };
+ return { ...state, settings };
}
case ModToolsActions.SET_CURRENT_ROOM_ID: {
const currentRoomId = (action.payload.currentRoomId || state.currentRoomId || null);
@@ -54,6 +71,31 @@ export const ModToolsReducer: Reducer = (state,
return { ...state, openRooms };
}
+ case ModToolsActions.SET_OPEN_USERINFO: {
+ const openUserInfo = (action.payload.openUserInfo || state.openUserInfo || null);
+
+ return { ...state, openUserInfo };
+ }
+ case ModToolsActions.SET_OPEN_ROOM_CHATLOGS: {
+ const openRoomChatlogs = (action.payload.openRoomChatlogs || state.openRoomChatlogs || null);
+
+ return { ...state, openRoomChatlogs };
+ }
+ case ModToolsActions.SET_OPEN_USER_CHATLOGS: {
+ const openUserChatlogs = (action.payload.openUserChatlogs || state.openUserChatlogs || null);
+
+ return { ...state, openUserChatlogs };
+ }
+ case ModToolsActions.SET_TICKETS: {
+ const tickets = (action.payload.tickets || state.tickets || null);
+
+ return { ...state, tickets };
+ }
+ case ModToolsActions.SET_CFH_CATEGORIES: {
+ const cfhCategories = (action.payload.cfhCategories || state.cfhCategories || null);
+
+ return { ...state, cfhCategories };
+ }
case ModToolsActions.RESET_STATE: {
return { ...initialModTools };
}
diff --git a/src/views/mod-tools/utils/ISelectedUser.ts b/src/views/mod-tools/utils/ISelectedUser.ts
new file mode 100644
index 00000000..c5a657a9
--- /dev/null
+++ b/src/views/mod-tools/utils/ISelectedUser.ts
@@ -0,0 +1,4 @@
+export interface ISelectedUser {
+ userId: number;
+ username: string;
+}
diff --git a/src/views/mod-tools/utils/IUserInfo.ts b/src/views/mod-tools/utils/IUserInfo.ts
new file mode 100644
index 00000000..8d49aa74
--- /dev/null
+++ b/src/views/mod-tools/utils/IUserInfo.ts
@@ -0,0 +1,6 @@
+export interface IUserInfo
+{
+ nameKey: string;
+ nameKeyFallback: string;
+ value: string;
+}
diff --git a/src/views/mod-tools/utils/ModActionDefinition.ts b/src/views/mod-tools/utils/ModActionDefinition.ts
new file mode 100644
index 00000000..b8318afa
--- /dev/null
+++ b/src/views/mod-tools/utils/ModActionDefinition.ts
@@ -0,0 +1,49 @@
+export class ModActionDefinition
+{
+ public static ALERT:number = 1;
+ public static MUTE:number = 2;
+ public static BAN:number = 3;
+ public static KICK:number = 4;
+ public static TRADE_LOCK:number = 5;
+ public static MESSAGE:number = 6;
+
+ private readonly _actionId:number;
+ private readonly _name:string;
+ private readonly _actionType:number;
+ private readonly _sanctionTypeId:number;
+ private readonly _actionLengthHours:number;
+
+ constructor(actionId:number, actionName:string, actionType:number, sanctionTypeId:number, actionLengthHours:number)
+ {
+ this._actionId = actionId;
+ this._name = actionName;
+ this._actionType = actionType;
+ this._sanctionTypeId = sanctionTypeId;
+ this._actionLengthHours = actionLengthHours;
+ }
+
+ public get actionId():number
+ {
+ return this._actionId;
+ }
+
+ public get name():string
+ {
+ return this._name;
+ }
+
+ public get actionType():number
+ {
+ return this._actionType;
+ }
+
+ public get sanctionTypeId():number
+ {
+ return this._sanctionTypeId;
+ }
+
+ public get actionLengthHours():number
+ {
+ return this._actionLengthHours;
+ }
+}
diff --git a/src/views/mod-tools/views/chatlog/ChatlogView.scss b/src/views/mod-tools/views/chatlog/ChatlogView.scss
new file mode 100644
index 00000000..5d5f7d04
--- /dev/null
+++ b/src/views/mod-tools/views/chatlog/ChatlogView.scss
@@ -0,0 +1,40 @@
+.chatlog-messages {
+ color: $black;
+ min-width: 400px;
+
+ $username-col-width: 100px;
+
+ .username-label {
+ width: $username-col-width;
+ }
+
+ .chatlog {
+ min-height: 200px;
+
+ .chatlog-container {
+ color: $black;
+
+ div.chatlog-entry {
+ border-bottom: 1px solid rgba(0, 0, 0, 0.2);
+ .username {
+ color: #1E7295;
+ text-decoration: underline;
+ width: $username-col-width;
+ }
+
+ &.highlighted {
+ border: 1px solid $red;
+ }
+
+ .message {
+ word-break: break-all;
+ }
+ }
+
+ .room-info {
+ border-bottom: 1px solid rgba(0, 0, 0, 0.2);
+ background: rgba(0, 0, 0, .05);
+ }
+ }
+ }
+}
diff --git a/src/views/mod-tools/views/chatlog/ChatlogView.tsx b/src/views/mod-tools/views/chatlog/ChatlogView.tsx
new file mode 100644
index 00000000..c20839cd
--- /dev/null
+++ b/src/views/mod-tools/views/chatlog/ChatlogView.tsx
@@ -0,0 +1,150 @@
+import { ChatlineData, ChatRecordData, UserProfileComposer } from '@nitrots/nitro-renderer';
+import { FC, useCallback } from 'react';
+import { AutoSizer, CellMeasurer, CellMeasurerCache, List, ListRowProps, ListRowRenderer } from 'react-virtualized';
+import { TryVisitRoom } from '../../../../api';
+import { ModToolsOpenRoomInfoEvent } from '../../../../events/mod-tools/ModToolsOpenRoomInfoEvent';
+import { dispatchUiEvent, SendMessageHook } from '../../../../hooks';
+import { ChatlogViewProps } from './ChatlogView.types';
+
+export const ChatlogView: FC = props =>
+{
+ const { records = null } = props;
+
+ const simpleRowRenderer: ListRowRenderer = (props: ListRowProps) =>
+ {
+ const item = records[0].chatlog[props.index];
+
+ return (
+
+
+
{item.timestamp}
+
SendMessageHook(new UserProfileComposer(item.userId))}>{item.userName}
+
{item.message}
+
+
+ );
+ };
+
+ const advancedRowRenderer: ListRowRenderer = (props: ListRowProps) =>
+ {
+ let chatlogEntry: ChatlineData;
+ let currentRecord: ChatRecordData;
+ let isRoomInfo = false;
+
+ let totalIndex = 0;
+ for(let i = 0; i < records.length; i++)
+ {
+ currentRecord = records[i];
+
+ totalIndex++; // row for room info
+ totalIndex = totalIndex + currentRecord.chatlog.length;
+
+ if(props.index > (totalIndex - 1))
+ {
+ continue; // it is not in current one
+ }
+
+ if( (props.index + 1) === (totalIndex - currentRecord.chatlog.length))
+ {
+ isRoomInfo = true;
+ break;
+ }
+ const index = props.index - (totalIndex - currentRecord.chatlog.length);
+ chatlogEntry = currentRecord.chatlog[index];
+ break;
+ }
+
+ return (
+
+ {isRoomInfo && }
+ {!isRoomInfo &&
+
+
{chatlogEntry.timestamp}
+
SendMessageHook(new UserProfileComposer(chatlogEntry.userId))}>{chatlogEntry.userName}
+
{chatlogEntry.message}
+
+ }
+
+
+ );
+ }
+
+ const getNumRowsForAdvanced = useCallback(() =>
+ {
+ let count = 0;
+
+ for(let i = 0; i < records.length; i++)
+ {
+ count++; // add room info row
+ count = count + records[i].chatlog.length;
+ }
+
+ return count;
+ }, [records]);
+
+ const cache = new CellMeasurerCache({
+ defaultHeight: 25,
+ fixedWidth: true
+ });
+
+ const RoomInfo = useCallback(({ roomId, roomName, uniqueKey, style }) =>
+ {
+ return (
+
+
Room: {roomName}
+
+
+
+ );
+ }, []);
+
+ return (
+ <>
+ {
+ (records && records.length) &&
+ <>
+ {(records.length === 1) && }
+
+
+
Time
+
User
+
Message
+
+
+
+ {({ height, width }) =>
+ {
+ cache.clearAll();
+
+ return (
+ 1 ? getNumRowsForAdvanced() : records[0].chatlog.length}
+ rowHeight={cache.rowHeight}
+ className={'chatlog-container'}
+ rowRenderer={records.length > 1 ? advancedRowRenderer : simpleRowRenderer}
+ deferredMeasurementCache={cache} />
+ )
+ }
+ }
+
+
+
+ >
+ }
+ >
+ );
+}
diff --git a/src/views/mod-tools/views/chatlog/ChatlogView.types.ts b/src/views/mod-tools/views/chatlog/ChatlogView.types.ts
new file mode 100644
index 00000000..f307109f
--- /dev/null
+++ b/src/views/mod-tools/views/chatlog/ChatlogView.types.ts
@@ -0,0 +1,6 @@
+import { ChatRecordData } from '@nitrots/nitro-renderer';
+
+export interface ChatlogViewProps
+{
+ records: ChatRecordData[];
+}
diff --git a/src/views/mod-tools/views/chatlog/ModToolsChatlogView.scss b/src/views/mod-tools/views/chatlog/ModToolsChatlogView.scss
deleted file mode 100644
index 7a2ddc8a..00000000
--- a/src/views/mod-tools/views/chatlog/ModToolsChatlogView.scss
+++ /dev/null
@@ -1,26 +0,0 @@
-.nitro-mod-tools-chatlog {
- width: 480px;
-
- .chatlog-messages {
- height: 300px;
- max-height: 300px;
-
- .table {
- color: $black;
-
- > :not(caption) > * > * {
- box-shadow: none;
- border-bottom: 1px solid rgba(0, 0, 0, .2);
- }
-
- &.table-striped > tbody > tr:nth-of-type(odd) {
- color: $black;
- background: rgba(0, 0, 0, .05);
- }
-
- td {
- padding: 0px 5px;
- }
- }
- }
-}
diff --git a/src/views/mod-tools/views/chatlog/ModToolsChatlogView.tsx b/src/views/mod-tools/views/chatlog/ModToolsChatlogView.tsx
deleted file mode 100644
index c9dc276e..00000000
--- a/src/views/mod-tools/views/chatlog/ModToolsChatlogView.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-import { ModtoolRequestRoomChatlogComposer, ModtoolRoomChatlogEvent, ModtoolRoomChatlogLine } from '@nitrots/nitro-renderer';
-import { FC, useCallback, useEffect, useState } from 'react';
-import { TryVisitRoom } from '../../../../api';
-import { CreateMessageHook, SendMessageHook } from '../../../../hooks/messages';
-import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
-import { ModToolsChatlogViewProps } from './ModToolsChatlogView.types';
-
-export const ModToolsChatlogView: FC = props =>
-{
- const { roomId = null, onCloseClick = null } = props;
-
- const [ roomName, setRoomName ] = useState(null);
- const [ messages, setMessages ] = useState(null);
- const [ loadedRoomId, setLoadedRoomId ] = useState(null);
-
- const [ messagesRequested, setMessagesRequested ] = useState(false);
-
- useEffect(() =>
- {
- if(messagesRequested) return;
-
- SendMessageHook(new ModtoolRequestRoomChatlogComposer(roomId));
- setMessagesRequested(true);
- }, [ roomId, messagesRequested, setMessagesRequested ]);
-
- const onModtoolRoomChatlogEvent = useCallback((event: ModtoolRoomChatlogEvent) =>
- {
- const parser = event.getParser();
-
- setRoomName(parser.name);
- setMessages(parser.chatlogs);
- setLoadedRoomId(parser.id);
- }, [ setRoomName, setMessages ]);
-
- CreateMessageHook(ModtoolRoomChatlogEvent, onModtoolRoomChatlogEvent);
-
- const handleClick = useCallback((action: string, value?: string) =>
- {
- if(!action) return;
-
- switch(action)
- {
- case 'close':
- onCloseClick();
- return;
- case 'visit_room':
- TryVisitRoom(loadedRoomId);
- return;
- }
- }, [ onCloseClick, loadedRoomId ]);
-
- return (
-
- handleClick('close') } />
-
-
-
-
-
-
- { messages &&
-
-
- Time |
- User |
- Message |
-
-
-
- { messages.map((message, index) =>
- {
- return
- { message.timestamp } |
- { message.userName } |
- { message.message } |
-
;
- }) }
-
-
}
-
-
-
- );
-}
diff --git a/src/views/mod-tools/views/room/ModToolsRoomView.scss b/src/views/mod-tools/views/room/ModToolsRoomView.scss
deleted file mode 100644
index 55ed938a..00000000
--- a/src/views/mod-tools/views/room/ModToolsRoomView.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-.nitro-mod-tools-room {
- width: 240px;
-}
diff --git a/src/views/mod-tools/views/room/room-chatlog/ModToolsChatlogView.tsx b/src/views/mod-tools/views/room/room-chatlog/ModToolsChatlogView.tsx
new file mode 100644
index 00000000..96d11aba
--- /dev/null
+++ b/src/views/mod-tools/views/room/room-chatlog/ModToolsChatlogView.tsx
@@ -0,0 +1,40 @@
+import { ChatRecordData, GetRoomChatlogMessageComposer, RoomChatlogEvent } from '@nitrots/nitro-renderer';
+import { FC, useCallback, useEffect, useState } from 'react';
+import { CreateMessageHook, SendMessageHook } from '../../../../../hooks/messages';
+import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../../layout';
+import { ChatlogView } from '../../chatlog/ChatlogView';
+import { ModToolsChatlogViewProps } from './ModToolsChatlogView.types';
+
+export const ModToolsChatlogView: FC = props =>
+{
+ const { roomId = null, onCloseClick = null } = props;
+
+ const [roomChatlog, setRoomChatlog] = useState(null);
+
+ useEffect(() =>
+ {
+ SendMessageHook(new GetRoomChatlogMessageComposer(roomId));
+ }, [roomId]);
+
+ const onModtoolRoomChatlogEvent = useCallback((event: RoomChatlogEvent) =>
+ {
+ const parser = event.getParser();
+
+ if(!parser || parser.data.roomId !== roomId) return;
+
+ setRoomChatlog(parser.data);
+ }, [roomId, setRoomChatlog]);
+
+ CreateMessageHook(RoomChatlogEvent, onModtoolRoomChatlogEvent);
+
+ return (
+
+ onCloseClick()} />
+
+ {roomChatlog &&
+
+ }
+
+
+ );
+}
diff --git a/src/views/mod-tools/views/chatlog/ModToolsChatlogView.types.ts b/src/views/mod-tools/views/room/room-chatlog/ModToolsChatlogView.types.ts
similarity index 100%
rename from src/views/mod-tools/views/chatlog/ModToolsChatlogView.types.ts
rename to src/views/mod-tools/views/room/room-chatlog/ModToolsChatlogView.types.ts
diff --git a/src/views/mod-tools/views/room/room-tools/ModToolsRoomView.scss b/src/views/mod-tools/views/room/room-tools/ModToolsRoomView.scss
new file mode 100644
index 00000000..4faa2b33
--- /dev/null
+++ b/src/views/mod-tools/views/room/room-tools/ModToolsRoomView.scss
@@ -0,0 +1,8 @@
+.nitro-mod-tools-room {
+ width: 240px;
+
+ .username {
+ color: #1E7295;
+ text-decoration: underline;
+ }
+}
diff --git a/src/views/mod-tools/views/room/ModToolsRoomView.tsx b/src/views/mod-tools/views/room/room-tools/ModToolsRoomView.tsx
similarity index 53%
rename from src/views/mod-tools/views/room/ModToolsRoomView.tsx
rename to src/views/mod-tools/views/room/room-tools/ModToolsRoomView.tsx
index c236a9d2..d169c1fb 100644
--- a/src/views/mod-tools/views/room/ModToolsRoomView.tsx
+++ b/src/views/mod-tools/views/room/room-tools/ModToolsRoomView.tsx
@@ -1,7 +1,10 @@
-import { ModtoolRequestRoomInfoComposer, ModtoolRoomInfoEvent } from '@nitrots/nitro-renderer';
+import { GetModeratorRoomInfoMessageComposer, ModerateRoomMessageComposer, ModeratorActionMessageComposer, ModeratorRoomInfoEvent } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react';
-import { CreateMessageHook, SendMessageHook } from '../../../../hooks/messages';
-import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
+import { TryVisitRoom } from '../../../../../api';
+import { ModToolsOpenRoomChatlogEvent } from '../../../../../events/mod-tools/ModToolsOpenRoomChatlogEvent';
+import { dispatchUiEvent } from '../../../../../hooks';
+import { CreateMessageHook, SendMessageHook } from '../../../../../hooks/messages';
+import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../../layout';
import { ModToolsRoomViewProps } from './ModToolsRoomView.types';
export const ModToolsRoomView: FC = props =>
@@ -17,27 +20,35 @@ export const ModToolsRoomView: FC = props =>
const [ ownerInRoom, setOwnerInRoom ] = useState(false);
const [ usersInRoom, setUsersInRoom ] = useState(0);
+ //form data
+ const [kickUsers, setKickUsers] = useState(false);
+ const [lockRoom, setLockRoom] = useState(false);
+ const [changeRoomName, setChangeRoomName] = useState(false);
+ const [message, setMessage] = useState('');
+
useEffect(() =>
{
if(infoRequested) return;
- SendMessageHook(new ModtoolRequestRoomInfoComposer(roomId));
+ SendMessageHook(new GetModeratorRoomInfoMessageComposer(roomId));
setInfoRequested(true);
}, [ roomId, infoRequested, setInfoRequested ]);
- const onModtoolRoomInfoEvent = useCallback((event: ModtoolRoomInfoEvent) =>
+ const onModtoolRoomInfoEvent = useCallback((event: ModeratorRoomInfoEvent) =>
{
const parser = event.getParser();
- setLoadedRoomId(parser.id);
- setName(parser.name);
- setOwnerId(parser.ownerId);
- setOwnerName(parser.ownerName);
- setOwnerInRoom(parser.ownerInRoom);
- setUsersInRoom(parser.playerAmount);
- }, [ setLoadedRoomId, setName, setOwnerId, setOwnerName, setOwnerInRoom, setUsersInRoom ]);
+ if(!parser || parser.data.flatId !== roomId) return;
- CreateMessageHook(ModtoolRoomInfoEvent, onModtoolRoomInfoEvent);
+ setLoadedRoomId(parser.data.flatId);
+ setName(parser.data.room.name);
+ setOwnerId(parser.data.ownerId);
+ setOwnerName(parser.data.ownerName);
+ setOwnerInRoom(parser.data.ownerInRoom);
+ setUsersInRoom(parser.data.userCount);
+ }, [ setLoadedRoomId, setName, setOwnerId, setOwnerName, setOwnerInRoom, setUsersInRoom, roomId ]);
+
+ CreateMessageHook(ModeratorRoomInfoEvent, onModtoolRoomInfoEvent);
const handleClick = useCallback((action: string, value?: string) =>
{
@@ -45,27 +56,33 @@ export const ModToolsRoomView: FC = props =>
switch(action)
{
- case 'close':
- onCloseClick();
+ case 'alert_only':
+ if(message.trim().length === 0) return;
+ SendMessageHook(new ModeratorActionMessageComposer(ModeratorActionMessageComposer.ACTION_ALERT, message, ''));
+ return;
+ case 'send_message':
+ if(message.trim().length === 0) return;
+ SendMessageHook(new ModeratorActionMessageComposer(ModeratorActionMessageComposer.ACTION_MESSAGE, message, ''));
+ SendMessageHook(new ModerateRoomMessageComposer(roomId, lockRoom ? 1 : 0, changeRoomName ? 1 : 0, kickUsers ? 1 : 0))
return;
}
- }, [ onCloseClick ]);
+ }, [changeRoomName, kickUsers, lockRoom, message, roomId]);
return (
- handleClick('close') } />
+ onCloseClick() } />
-
+
Users in room: { usersInRoom }
-
+
@@ -75,28 +92,28 @@ export const ModToolsRoomView: FC = props =>
-
+
-
-
+
+
diff --git a/src/views/mod-tools/views/room/ModToolsRoomView.types.ts b/src/views/mod-tools/views/room/room-tools/ModToolsRoomView.types.ts
similarity index 100%
rename from src/views/mod-tools/views/room/ModToolsRoomView.types.ts
rename to src/views/mod-tools/views/room/room-tools/ModToolsRoomView.types.ts
diff --git a/src/views/mod-tools/views/tickets/ModToolsTicketView.scss b/src/views/mod-tools/views/tickets/ModToolsTicketView.scss
new file mode 100644
index 00000000..f2459483
--- /dev/null
+++ b/src/views/mod-tools/views/tickets/ModToolsTicketView.scss
@@ -0,0 +1,11 @@
+.nitro-mod-tools-tickets
+{
+ width: 400px;
+ height: 200px;
+}
+
+.nitro-mod-tools-handle-issue
+{
+ width: 400px;
+ height: 300px;
+}
diff --git a/src/views/mod-tools/views/tickets/ModToolsTicketsView.tsx b/src/views/mod-tools/views/tickets/ModToolsTicketsView.tsx
index 5d2ebf47..e2ccea90 100644
--- a/src/views/mod-tools/views/tickets/ModToolsTicketsView.tsx
+++ b/src/views/mod-tools/views/tickets/ModToolsTicketsView.tsx
@@ -1,6 +1,13 @@
-import { FC, useState } from 'react';
+import { IssueMessageData } from '@nitrots/nitro-renderer';
+import { FC, useCallback, useMemo, useState } from 'react';
+import { GetSessionDataManager } from '../../../../api';
import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../../../layout';
+import { useModToolsContext } from '../../context/ModToolsContext';
+import { IssueInfoView } from './issue-info/IssueInfoView';
import { ModToolsTicketsViewProps } from './ModToolsTicketsView.types';
+import { ModToolsMyIssuesTabView } from './my-issues/ModToolsMyIssuesTabView';
+import { ModToolsOpenIssuesTabView } from './open-issues/ModToolsOpenIssuesTabView';
+import { ModToolsPickedIssuesTabView } from './picked-issues/ModToolsPickedIssuesTabView';
const TABS: string[] = [
'Open Issues',
@@ -11,10 +18,70 @@ const TABS: string[] = [
export const ModToolsTicketsView: FC
= props =>
{
const { onCloseClick = null } = props;
-
+ const { modToolsState = null } = useModToolsContext();
+ const { tickets= null } = modToolsState;
const [ currentTab, setCurrentTab ] = useState(0);
+ const [ issueInfoWindows, setIssueInfoWindows ] = useState([]);
+
+ const openIssues = useMemo(() =>
+ {
+ if(!tickets) return [];
+
+ return tickets.filter(issue => issue.state === IssueMessageData.STATE_OPEN);
+ }, [tickets]);
+
+ const myIssues = useMemo(() =>
+ {
+ if(!tickets) return [];
+
+ return tickets.filter(issue => (issue.state === IssueMessageData.STATE_PICKED) && (issue.pickerUserId === GetSessionDataManager().userId));
+ }, [tickets]);
+
+ const pickedIssues = useMemo(() =>
+ {
+ if(!tickets) return [];
+
+ return tickets.filter(issue => issue.state === IssueMessageData.STATE_PICKED);
+ }, [tickets]);
+
+ const onIssueInfoClosed = useCallback((issueId: number) =>
+ {
+ const indexOfValue = issueInfoWindows.indexOf(issueId);
+
+ if(indexOfValue === -1) return;
+
+ const newValues = Array.from(issueInfoWindows);
+ newValues.splice(indexOfValue, 1);
+ setIssueInfoWindows(newValues);
+ }, [issueInfoWindows]);
+
+ const onIssueHandleClicked = useCallback((issueId: number) =>
+ {
+ if(issueInfoWindows.indexOf(issueId) === -1)
+ {
+ const newValues = Array.from(issueInfoWindows);
+ newValues.push(issueId);
+ setIssueInfoWindows(newValues);
+ }
+ else
+ {
+ onIssueInfoClosed(issueId);
+ }
+ }, [issueInfoWindows, onIssueInfoClosed]);
+
+ const CurrentTabComponent = useCallback(() =>
+ {
+ switch(currentTab)
+ {
+ case 0: return ;
+ case 1: return ;
+ case 2: return ;
+ default: return null;
+ }
+ }, [currentTab, myIssues, onIssueHandleClicked, openIssues, pickedIssues]);
return (
+ <>
@@ -26,8 +93,14 @@ export const ModToolsTicketsView: FC = props =>
);
}) }
-
+
+
+
+ {
+ issueInfoWindows && issueInfoWindows.map(issueId => )
+ }
+ >
);
}
diff --git a/src/views/mod-tools/views/tickets/issue-info/CfhChatlogView.tsx b/src/views/mod-tools/views/tickets/issue-info/CfhChatlogView.tsx
new file mode 100644
index 00000000..fdb4c28e
--- /dev/null
+++ b/src/views/mod-tools/views/tickets/issue-info/CfhChatlogView.tsx
@@ -0,0 +1,37 @@
+import { CfhChatlogData, CfhChatlogEvent, GetCfhChatlogMessageComposer } from '@nitrots/nitro-renderer';
+import { FC, useCallback, useEffect, useState } from 'react';
+import { CreateMessageHook, SendMessageHook } from '../../../../../hooks';
+import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../../layout';
+import { ChatlogView } from '../../chatlog/ChatlogView';
+import { CfhChatlogViewProps } from './CfhChatlogView.types';
+
+export const CfhChatlogView: FC = props =>
+{
+ const { onCloseClick = null, issueId = null } = props;
+ const [ chatlogData, setChatlogData ] = useState(null);
+
+ useEffect(() =>
+ {
+ SendMessageHook(new GetCfhChatlogMessageComposer(issueId));
+ }, [issueId]);
+
+ const onCfhChatlogEvent = useCallback((event: CfhChatlogEvent) =>
+ {
+ const parser = event.getParser();
+
+ if(!parser || parser.data.issueId !== issueId) return;
+
+ setChatlogData(parser.data);
+ }, [issueId]);
+
+ CreateMessageHook(CfhChatlogEvent, onCfhChatlogEvent);
+
+ return (
+
+
+
+ { chatlogData && }
+
+
+ );
+}
diff --git a/src/views/mod-tools/views/tickets/issue-info/CfhChatlogView.types.ts b/src/views/mod-tools/views/tickets/issue-info/CfhChatlogView.types.ts
new file mode 100644
index 00000000..c40c18ad
--- /dev/null
+++ b/src/views/mod-tools/views/tickets/issue-info/CfhChatlogView.types.ts
@@ -0,0 +1,5 @@
+export interface CfhChatlogViewProps
+{
+ issueId: number;
+ onCloseClick(): void;
+}
diff --git a/src/views/mod-tools/views/tickets/issue-info/IssueInfoView.tsx b/src/views/mod-tools/views/tickets/issue-info/IssueInfoView.tsx
new file mode 100644
index 00000000..61a9784a
--- /dev/null
+++ b/src/views/mod-tools/views/tickets/issue-info/IssueInfoView.tsx
@@ -0,0 +1,72 @@
+import { CloseIssuesMessageComposer, ReleaseIssuesMessageComposer } from '@nitrots/nitro-renderer';
+import { FC, useCallback, useMemo, useState } from 'react';
+import { LocalizeText } from '../../../../../api';
+import { ModToolsOpenUserInfoEvent } from '../../../../../events/mod-tools/ModToolsOpenUserInfoEvent';
+import { dispatchUiEvent, SendMessageHook } from '../../../../../hooks';
+import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../../layout';
+import { getSourceName } from '../../../common/IssueCategoryNames';
+import { useModToolsContext } from '../../../context/ModToolsContext';
+import { CfhChatlogView } from './CfhChatlogView';
+import { IssueInfoViewProps } from './IssueInfoView.types';
+
+export const IssueInfoView: FC = props =>
+{
+ const { issueId = null, onIssueInfoClosed = null } = props;
+ const { modToolsState = null } = useModToolsContext();
+ const { tickets= null } = modToolsState;
+ const [ cfhChatlogOpen, setcfhChatlogOpen ] = useState(false);
+
+ const ticket = useMemo(() =>
+ {
+ return tickets.find( issue => issue.issueId === issueId);
+ }, [issueId, tickets]);
+
+ const onReleaseIssue = useCallback((issueId: number) =>
+ {
+ SendMessageHook(new ReleaseIssuesMessageComposer([issueId]));
+ onIssueInfoClosed(issueId);
+ }, [onIssueInfoClosed]);
+
+ const openUserInfo = useCallback((userId: number) =>
+ {
+ dispatchUiEvent(new ModToolsOpenUserInfoEvent(userId));
+ }, []);
+
+ const closeIssue = useCallback((resolutionType: number) =>
+ {
+ SendMessageHook(new CloseIssuesMessageComposer([issueId], resolutionType));
+ onIssueInfoClosed(issueId)
+ }, [issueId, onIssueInfoClosed]);
+
+ return (
+ <>
+
+ onIssueInfoClosed(issueId)} />
+
+
+
+
Issue Information
+
Source: {getSourceName(ticket.categoryId)}
+
Category: {LocalizeText('help.cfh.topic.' + ticket.reportedCategoryId)}
+
Description: {ticket.message}
+
Caller:
+
Reported User:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ { cfhChatlogOpen && setcfhChatlogOpen(false) }/>}
+ >
+ );
+}
diff --git a/src/views/mod-tools/views/tickets/issue-info/IssueInfoView.types.ts b/src/views/mod-tools/views/tickets/issue-info/IssueInfoView.types.ts
new file mode 100644
index 00000000..1b28d803
--- /dev/null
+++ b/src/views/mod-tools/views/tickets/issue-info/IssueInfoView.types.ts
@@ -0,0 +1,5 @@
+export interface IssueInfoViewProps
+{
+ issueId: number;
+ onIssueInfoClosed(issueId: number): void;
+}
diff --git a/src/views/mod-tools/views/tickets/my-issues/ModToolsMyIssuesTabView.tsx b/src/views/mod-tools/views/tickets/my-issues/ModToolsMyIssuesTabView.tsx
new file mode 100644
index 00000000..1a3fcde3
--- /dev/null
+++ b/src/views/mod-tools/views/tickets/my-issues/ModToolsMyIssuesTabView.tsx
@@ -0,0 +1,45 @@
+import { ReleaseIssuesMessageComposer } from '@nitrots/nitro-renderer';
+import { FC, useCallback } from 'react';
+import { SendMessageHook } from '../../../../../hooks';
+import { ModToolsMyIssuesTabViewProps } from './ModToolsMyIssuesTabView.types';
+
+export const ModToolsMyIssuesTabView: FC = props =>
+{
+ const { myIssues = null, onIssueHandleClick = null } = props;
+
+
+ const onReleaseIssue = useCallback((issueId: number) =>
+ {
+ SendMessageHook(new ReleaseIssuesMessageComposer([issueId]));
+ }, []);
+
+ return (
+ <>
+
+
+
+ Type |
+ Room/Player |
+ Opened |
+ |
+ |
+
+
+
+ {myIssues.map(issue =>
+ {
+ return (
+
+ {issue.categoryId} |
+ {issue.reportedUserName} |
+ {new Date(Date.now() - issue.issueAgeInMilliseconds).toLocaleTimeString()} |
+ |
+ |
+
)
+ })
+ }
+
+
+ >
+ );
+}
diff --git a/src/views/mod-tools/views/tickets/my-issues/ModToolsMyIssuesTabView.types.ts b/src/views/mod-tools/views/tickets/my-issues/ModToolsMyIssuesTabView.types.ts
new file mode 100644
index 00000000..6d03fbf8
--- /dev/null
+++ b/src/views/mod-tools/views/tickets/my-issues/ModToolsMyIssuesTabView.types.ts
@@ -0,0 +1,7 @@
+import { IssueMessageData } from '@nitrots/nitro-renderer';
+
+export interface ModToolsMyIssuesTabViewProps
+{
+ myIssues: IssueMessageData[];
+ onIssueHandleClick(issueId: number): void;
+}
diff --git a/src/views/mod-tools/views/tickets/open-issues/ModToolsOpenIssuesTabView.tsx b/src/views/mod-tools/views/tickets/open-issues/ModToolsOpenIssuesTabView.tsx
new file mode 100644
index 00000000..4f8bb6d6
--- /dev/null
+++ b/src/views/mod-tools/views/tickets/open-issues/ModToolsOpenIssuesTabView.tsx
@@ -0,0 +1,42 @@
+import { PickIssuesMessageComposer } from '@nitrots/nitro-renderer';
+import { FC, useCallback } from 'react';
+import { SendMessageHook } from '../../../../../hooks';
+import { ModToolsOpenIssuesTabViewProps } from './ModToolsOpenIssuesTabView.types';
+
+export const ModToolsOpenIssuesTabView: FC = props =>
+{
+ const { openIssues = null } = props;
+
+ const onPickIssue = useCallback((issueId: number) =>
+ {
+ SendMessageHook(new PickIssuesMessageComposer([issueId], false, 0, 'pick issue button'));
+ }, []);
+
+ return (
+ <>
+
+
+
+ Type |
+ Room/Player |
+ Opened |
+ |
+
+
+
+ {openIssues.map(issue =>
+ {
+ return (
+
+ {issue.categoryId} |
+ {issue.reportedUserName} |
+ {new Date(Date.now() - issue.issueAgeInMilliseconds).toLocaleTimeString()} |
+ |
+
)
+ })
+ }
+
+
+ >
+ );
+}
diff --git a/src/views/mod-tools/views/tickets/open-issues/ModToolsOpenIssuesTabView.types.ts b/src/views/mod-tools/views/tickets/open-issues/ModToolsOpenIssuesTabView.types.ts
new file mode 100644
index 00000000..45a80d7d
--- /dev/null
+++ b/src/views/mod-tools/views/tickets/open-issues/ModToolsOpenIssuesTabView.types.ts
@@ -0,0 +1,6 @@
+import { IssueMessageData } from '@nitrots/nitro-renderer';
+
+export interface ModToolsOpenIssuesTabViewProps
+{
+ openIssues: IssueMessageData[];
+}
diff --git a/src/views/mod-tools/views/tickets/picked-issues/ModToolsPickedIssuesTabView.tsx b/src/views/mod-tools/views/tickets/picked-issues/ModToolsPickedIssuesTabView.tsx
new file mode 100644
index 00000000..988a0fd9
--- /dev/null
+++ b/src/views/mod-tools/views/tickets/picked-issues/ModToolsPickedIssuesTabView.tsx
@@ -0,0 +1,35 @@
+import { FC } from 'react';
+import { ModToolsPickedIssuesTabViewProps } from './ModToolsPickedIssuesTabView.types';
+
+export const ModToolsPickedIssuesTabView: FC = props =>
+{
+ const { pickedIssues = null } = props;
+
+ return (
+ <>
+
+
+
+ Type |
+ Room/Player |
+ Opened |
+ Picker |
+
+
+
+ {pickedIssues.map(issue =>
+ {
+ return (
+
+ {issue.categoryId} |
+ {issue.reportedUserName} |
+ {new Date(Date.now() - issue.issueAgeInMilliseconds).toLocaleTimeString()} |
+ {issue.pickerUserName} |
+
)
+ })
+ }
+
+
+ >
+ );
+}
diff --git a/src/views/mod-tools/views/tickets/picked-issues/ModToolsPickedIssuesTabView.types.ts b/src/views/mod-tools/views/tickets/picked-issues/ModToolsPickedIssuesTabView.types.ts
new file mode 100644
index 00000000..c69b1772
--- /dev/null
+++ b/src/views/mod-tools/views/tickets/picked-issues/ModToolsPickedIssuesTabView.types.ts
@@ -0,0 +1,6 @@
+import { IssueMessageData } from '@nitrots/nitro-renderer';
+
+export interface ModToolsPickedIssuesTabViewProps
+{
+ pickedIssues: IssueMessageData[];
+}
diff --git a/src/views/mod-tools/views/user/ModToolsUserView.tsx b/src/views/mod-tools/views/user/ModToolsUserView.tsx
deleted file mode 100644
index d3a2f076..00000000
--- a/src/views/mod-tools/views/user/ModToolsUserView.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { FC } from 'react';
-import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
-import { ModToolsUserViewProps } from './ModToolsUserView.types';
-
-export const ModToolsUserView: FC = props =>
-{
- return (
-
- {} } />
-
-
-
-
- );
-}
diff --git a/src/views/mod-tools/views/user/ModToolsUserView.types.ts b/src/views/mod-tools/views/user/ModToolsUserView.types.ts
deleted file mode 100644
index 55b9477c..00000000
--- a/src/views/mod-tools/views/user/ModToolsUserView.types.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export interface ModToolsUserViewProps
-{}
diff --git a/src/views/mod-tools/views/user/user-chatlog/ModToolsUserChatlogView.tsx b/src/views/mod-tools/views/user/user-chatlog/ModToolsUserChatlogView.tsx
new file mode 100644
index 00000000..705cc690
--- /dev/null
+++ b/src/views/mod-tools/views/user/user-chatlog/ModToolsUserChatlogView.tsx
@@ -0,0 +1,41 @@
+import { ChatRecordData, GetUserChatlogMessageComposer, UserChatlogEvent } from '@nitrots/nitro-renderer';
+import { FC, useCallback, useEffect, useState } from 'react';
+import { CreateMessageHook, SendMessageHook } from '../../../../../hooks';
+import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../../layout';
+import { ChatlogView } from '../../chatlog/ChatlogView';
+import { ModToolsUserChatlogViewProps } from './ModToolsUserChatlogView.types';
+
+export const ModToolsUserChatlogView: FC = props =>
+{
+ const { userId = null, onCloseClick = null } = props;
+ const [userChatlog, setUserChatlog] = useState(null);
+ const [username, setUsername] = useState(null);
+
+ useEffect(() =>
+ {
+ SendMessageHook(new GetUserChatlogMessageComposer(userId));
+ }, [userId]);
+
+ const onModtoolUserChatlogEvent = useCallback((event: UserChatlogEvent) =>
+ {
+ const parser = event.getParser();
+
+ if(!parser || parser.data.userId !== userId) return;
+
+ setUsername(parser.data.username);
+ setUserChatlog(parser.data.roomChatlogs);
+ }, [setUsername, setUserChatlog, userId]);
+
+ CreateMessageHook(UserChatlogEvent, onModtoolUserChatlogEvent);
+
+ return (
+
+ onCloseClick()} />
+
+ {userChatlog &&
+
+ }
+
+
+ );
+}
diff --git a/src/views/mod-tools/views/user/user-chatlog/ModToolsUserChatlogView.types.ts b/src/views/mod-tools/views/user/user-chatlog/ModToolsUserChatlogView.types.ts
new file mode 100644
index 00000000..c8eea569
--- /dev/null
+++ b/src/views/mod-tools/views/user/user-chatlog/ModToolsUserChatlogView.types.ts
@@ -0,0 +1,5 @@
+export interface ModToolsUserChatlogViewProps
+{
+ userId: number;
+ onCloseClick: () => void;
+}
diff --git a/src/views/mod-tools/views/user/user-info/ModToolsUserView.scss b/src/views/mod-tools/views/user/user-info/ModToolsUserView.scss
new file mode 100644
index 00000000..ddc24350
--- /dev/null
+++ b/src/views/mod-tools/views/user/user-info/ModToolsUserView.scss
@@ -0,0 +1,23 @@
+.nitro-mod-tools-user {
+ width: 350px;
+ height: 370px;
+
+ .username {
+ color: #1E7295;
+ text-decoration: underline;
+ }
+
+ .table {
+ color: $black;
+
+ > :not(caption) > * > * {
+ box-shadow: none;
+ border-bottom: 1px solid rgba(0, 0, 0, .2);
+ }
+
+ &.table-striped > tbody > tr:nth-of-type(odd) {
+ color: $black;
+ background: rgba(0, 0, 0, .05);
+ }
+ }
+}
diff --git a/src/views/mod-tools/views/user/user-info/ModToolsUserView.tsx b/src/views/mod-tools/views/user/user-info/ModToolsUserView.tsx
new file mode 100644
index 00000000..24f8c876
--- /dev/null
+++ b/src/views/mod-tools/views/user/user-info/ModToolsUserView.tsx
@@ -0,0 +1,153 @@
+import { FriendlyTime, GetModeratorUserInfoMessageComposer, ModeratorUserInfoData, ModeratorUserInfoEvent } from '@nitrots/nitro-renderer';
+import { FC, useCallback, useEffect, useMemo, useState } from 'react';
+import { LocalizeText } from '../../../../../api';
+import { ModToolsOpenUserChatlogEvent } from '../../../../../events/mod-tools/ModToolsOpenUserChatlogEvent';
+import { CreateMessageHook, dispatchUiEvent, SendMessageHook } from '../../../../../hooks';
+import { NitroCardContentView, NitroCardHeaderView, NitroCardView, NitroLayoutButton, NitroLayoutGrid, NitroLayoutGridColumn } from '../../../../../layout';
+import { ModToolsUserModActionView } from '../user-mod-action/ModToolsUserModActionView';
+import { ModToolsUserRoomVisitsView } from '../user-room-visits/ModToolsUserRoomVisitsView';
+import { ModToolsSendUserMessageView } from '../user-sendmessage/ModToolsSendUserMessageView';
+import { ModToolsUserViewProps } from './ModToolsUserView.types';
+
+export const ModToolsUserView: FC = props =>
+{
+ const { onCloseClick = null, userId = null } = props;
+ const [ userInfo, setUserInfo ] = useState(null);
+ const [ sendMessageVisible, setSendMessageVisible ] = useState(false);
+ const [ modActionVisible, setModActionVisible ] = useState(false);
+ const [ roomVisitsVisible, setRoomVisitsVisible ] = useState(false);
+
+ useEffect(() =>
+ {
+ SendMessageHook(new GetModeratorUserInfoMessageComposer(userId));
+ }, [ userId ]);
+
+ const onModtoolUserInfoEvent = useCallback((event: ModeratorUserInfoEvent) =>
+ {
+ const parser = event.getParser();
+
+ if(!parser || parser.data.userId !== userId) return;
+
+ setUserInfo(parser.data);
+ }, [setUserInfo, userId]);
+
+ CreateMessageHook(ModeratorUserInfoEvent, onModtoolUserInfoEvent);
+
+ const userProperties = useMemo(() =>
+ {
+ if(!userInfo) return null;
+
+ return [
+ {
+ localeKey: 'modtools.userinfo.userName',
+ value: userInfo.userName,
+ showOnline: true
+ },
+ {
+ localeKey: 'modtools.userinfo.cfhCount',
+ value: userInfo.cfhCount.toString()
+ },
+ {
+ localeKey: 'modtools.userinfo.abusiveCfhCount',
+ value: userInfo.abusiveCfhCount.toString()
+ },
+ {
+ localeKey: 'modtools.userinfo.cautionCount',
+ value: userInfo.cautionCount.toString()
+ },
+ {
+ localeKey: 'modtools.userinfo.banCount',
+ value: userInfo.banCount.toString()
+ },
+ {
+ localeKey: 'modtools.userinfo.lastSanctionTime',
+ value: userInfo.lastSanctionTime
+ },
+ {
+ localeKey: 'modtools.userinfo.tradingLockCount',
+ value: userInfo.tradingLockCount.toString()
+ },
+ {
+ localeKey: 'modtools.userinfo.tradingExpiryDate',
+ value: userInfo.tradingExpiryDate
+ },
+ {
+ localeKey: 'modtools.userinfo.minutesSinceLastLogin',
+ value: FriendlyTime.format(userInfo.minutesSinceLastLogin * 60, '.ago', 2)
+ },
+ {
+ localeKey: 'modtools.userinfo.lastPurchaseDate',
+ value: userInfo.lastPurchaseDate
+ },
+ {
+ localeKey: 'modtools.userinfo.primaryEmailAddress',
+ value: userInfo.primaryEmailAddress
+ },
+ {
+ localeKey: 'modtools.userinfo.identityRelatedBanCount',
+ value: userInfo.identityRelatedBanCount.toString()
+ },
+ {
+ localeKey: 'modtools.userinfo.registrationAgeInMinutes',
+ value: FriendlyTime.format(userInfo.registrationAgeInMinutes * 60, '.ago', 2)
+ },
+ {
+ localeKey: 'modtools.userinfo.userClassification',
+ value: userInfo.userClassification
+ }
+ ];
+ }, [ userInfo ]);
+
+ if(!userInfo) return null;
+
+ return (
+ <>
+
+ onCloseClick() } />
+
+
+
+
+
+ { userProperties.map( (property, index) =>
+ {
+
+ return (
+
+ { LocalizeText(property.localeKey) } |
+
+ { property.value }
+ { property.showOnline && }
+ |
+
+ );
+ }) }
+
+
+
+
+ dispatchUiEvent(new ModToolsOpenUserChatlogEvent(userId)) }>
+ Room Chat
+
+ setSendMessageVisible(!sendMessageVisible) }>
+ Send Message
+
+ setRoomVisitsVisible(!roomVisitsVisible) }>
+ Room Visits
+
+ setModActionVisible(!modActionVisible) }>
+ Mod Action
+
+
+
+
+
+ { sendMessageVisible &&
+ setSendMessageVisible(false) } /> }
+ { modActionVisible &&
+ setModActionVisible(false) } /> }
+ { roomVisitsVisible &&
+ setRoomVisitsVisible(false) } /> }
+ >
+ );
+}
diff --git a/src/views/mod-tools/views/user/user-info/ModToolsUserView.types.ts b/src/views/mod-tools/views/user/user-info/ModToolsUserView.types.ts
new file mode 100644
index 00000000..534d647d
--- /dev/null
+++ b/src/views/mod-tools/views/user/user-info/ModToolsUserView.types.ts
@@ -0,0 +1,5 @@
+export interface ModToolsUserViewProps
+{
+ userId: number;
+ onCloseClick: () => void;
+}
diff --git a/src/views/mod-tools/views/user/user-mod-action/ModToolsUserModActionView.tsx b/src/views/mod-tools/views/user/user-mod-action/ModToolsUserModActionView.tsx
new file mode 100644
index 00000000..09454bdc
--- /dev/null
+++ b/src/views/mod-tools/views/user/user-mod-action/ModToolsUserModActionView.tsx
@@ -0,0 +1,201 @@
+import { CallForHelpTopicData, DefaultSanctionMessageComposer, ModAlertMessageComposer, ModBanMessageComposer, ModKickMessageComposer, ModMessageMessageComposer, ModMuteMessageComposer, ModTradingLockMessageComposer } from '@nitrots/nitro-renderer';
+import { FC, useCallback, useMemo, useState } from 'react';
+import { LocalizeText } from '../../../../../api';
+import { NotificationAlertEvent } from '../../../../../events';
+import { dispatchUiEvent, SendMessageHook } from '../../../../../hooks';
+import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../../layout';
+import { useModToolsContext } from '../../../context/ModToolsContext';
+import { ModActionDefinition } from '../../../utils/ModActionDefinition';
+import { ModToolsUserModActionViewProps } from './ModToolsUserModActionView.types';
+
+const actions = [
+ new ModActionDefinition(1, 'Alert', ModActionDefinition.ALERT, 1, 0),
+ new ModActionDefinition(2, 'Mute 1h', ModActionDefinition.MUTE, 2, 0),
+ new ModActionDefinition(4, 'Ban 7 days', ModActionDefinition.BAN, 4, 0),
+ new ModActionDefinition(3, 'Ban 18h', ModActionDefinition.BAN, 3, 0),
+ new ModActionDefinition(5, 'Ban 30 days (step 1)', ModActionDefinition.BAN, 5, 0),
+ new ModActionDefinition(7, 'Ban 30 days (step 2)', ModActionDefinition.BAN, 7, 0),
+ new ModActionDefinition(6, 'Ban 100 years', ModActionDefinition.BAN, 6, 0),
+ new ModActionDefinition(106, 'Ban avatar-only 100 years', ModActionDefinition.BAN, 6, 0),
+ new ModActionDefinition(101, 'Kick', ModActionDefinition.KICK, 0, 0),
+ new ModActionDefinition(102, 'Lock trade 1 week', ModActionDefinition.TRADE_LOCK, 0, 168),
+ new ModActionDefinition(104, 'Lock trade permanent', ModActionDefinition.TRADE_LOCK, 0, 876000),
+ new ModActionDefinition(105, 'Message', ModActionDefinition.MESSAGE, 0, 0),
+];
+
+export const ModToolsUserModActionView: FC = props =>
+{
+ const { user = null, onCloseClick = null } = props;
+ const { modToolsState = null, dispatchModToolsState = null } = useModToolsContext();
+ const { cfhCategories = null, settings = null } = modToolsState;
+ const [ selectedTopic, setSelectedTopic ] = useState(-1);
+ const [ selectedAction, setSelectedAction ] = useState(-1);
+ const [ message, setMessage ] = useState('');
+
+ const topics = useMemo(() =>
+ {
+ const values: CallForHelpTopicData[] = [];
+
+ if(!cfhCategories) return values;
+
+ for(let category of cfhCategories)
+ {
+ for(let topic of category.topics)
+ {
+ values.push(topic)
+ }
+ }
+
+ return values;
+ }, [cfhCategories]);
+
+ const sendDefaultSanction = useCallback(() =>
+ {
+ SendMessageHook(new DefaultSanctionMessageComposer(user.userId, selectedTopic, message));
+ onCloseClick();
+ }, [message, onCloseClick, selectedTopic, user.userId]);
+
+ const sendSanction = useCallback(() =>
+ {
+ if( (selectedTopic === -1) || (selectedAction === -1) )
+ {
+ dispatchUiEvent(new NotificationAlertEvent(['You must select a CFH topic and Sanction'], null, null, null, 'Error', null));
+ return;
+ }
+
+ if(!settings || !settings.cfhPermission)
+ {
+ dispatchUiEvent(new NotificationAlertEvent(['You do not have permission to do this'], null, null, null, 'Error', null));
+ return;
+ }
+
+ const category = topics[selectedTopic];
+ const sanction = actions[selectedAction];
+
+ if(!category)
+ {
+ dispatchUiEvent(new NotificationAlertEvent(['You must select a CFH topic'], null, null, null, 'Error', null));
+ return;
+ }
+
+ if(!sanction)
+ {
+ dispatchUiEvent(new NotificationAlertEvent(['You must select a sanction'], null, null, null, 'Error', null));
+ return;
+ }
+
+ const messageOrDefault = message.trim().length === 0 ? LocalizeText('help.cfh.topic.' + category.id) : message;
+
+ switch(sanction.actionType)
+ {
+ case ModActionDefinition.ALERT:
+
+ if(!settings.alertPermission)
+ {
+ dispatchUiEvent(new NotificationAlertEvent(['You have insufficient permissions.'], null, null, null, 'Error', null));
+ return;
+ }
+
+ if(message.trim().length === 0)
+ {
+ dispatchUiEvent(new NotificationAlertEvent(['Please write a message to user.'], null, null, null, 'Error', null));
+ return;
+ }
+
+ SendMessageHook(new ModAlertMessageComposer(user.userId, message, category.id));
+
+ break;
+ case ModActionDefinition.MUTE:
+ SendMessageHook(new ModMuteMessageComposer(user.userId, messageOrDefault, category.id));
+
+ break;
+ case ModActionDefinition.BAN:
+
+ if(!settings.banPermission)
+ {
+ dispatchUiEvent(new NotificationAlertEvent(['You have insufficient permissions.'], null, null, null, 'Error', null));
+ return;
+ }
+
+ SendMessageHook(new ModBanMessageComposer(user.userId, messageOrDefault, category.id, selectedAction, (sanction.actionId === 106)));
+
+ break;
+
+ case ModActionDefinition.KICK:
+
+ if(!settings.kickPermission)
+ {
+ dispatchUiEvent(new NotificationAlertEvent(['You have insufficient permissions.'], null, null, null, 'Error', null));
+ return;
+ }
+
+ SendMessageHook(new ModKickMessageComposer(user.userId, messageOrDefault, category.id));
+
+ break;
+
+ case ModActionDefinition.TRADE_LOCK:
+ {
+ const numSeconds = sanction.actionLengthHours * 60;
+ SendMessageHook(new ModTradingLockMessageComposer(user.userId, messageOrDefault, numSeconds, category.id));
+ }
+ break;
+
+ case ModActionDefinition.MESSAGE:
+
+ if(message.trim().length === 0)
+ {
+ dispatchUiEvent(new NotificationAlertEvent(['Please write a message to user.'], null, null, null, 'Error', null));
+ return;
+ }
+
+ SendMessageHook(new ModMessageMessageComposer(user.userId, message, category.id));
+
+ break;
+ }
+
+ onCloseClick();
+ }, [message, onCloseClick, selectedAction, selectedTopic, settings, topics, user.userId]);
+
+ return (
+
+ onCloseClick() } />
+
+ { user &&
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ }
+
+
+ );
+}
diff --git a/src/views/mod-tools/views/user/user-mod-action/ModToolsUserModActionView.types.ts b/src/views/mod-tools/views/user/user-mod-action/ModToolsUserModActionView.types.ts
new file mode 100644
index 00000000..2025d10d
--- /dev/null
+++ b/src/views/mod-tools/views/user/user-mod-action/ModToolsUserModActionView.types.ts
@@ -0,0 +1,7 @@
+import { ISelectedUser } from '../../../utils/ISelectedUser';
+
+export interface ModToolsUserModActionViewProps
+{
+ user: ISelectedUser;
+ onCloseClick: () => void;
+}
diff --git a/src/views/mod-tools/views/user/user-room-visits/ModToolsUserRoomVisitsView.scss b/src/views/mod-tools/views/user/user-room-visits/ModToolsUserRoomVisitsView.scss
new file mode 100644
index 00000000..46efbaa5
--- /dev/null
+++ b/src/views/mod-tools/views/user/user-room-visits/ModToolsUserRoomVisitsView.scss
@@ -0,0 +1,14 @@
+.nitro-mod-tools-user-visits {
+ min-width: 300px;
+
+ .user-visits {
+ min-height: 200px;
+
+ .roomvisits-container {
+ div.room-visit {
+ border-bottom: 1px solid rgba(0, 0, 0, 0.2);
+ }
+ }
+
+ }
+}
diff --git a/src/views/mod-tools/views/user/user-room-visits/ModToolsUserRoomVisitsView.tsx b/src/views/mod-tools/views/user/user-room-visits/ModToolsUserRoomVisitsView.tsx
new file mode 100644
index 00000000..4f09828f
--- /dev/null
+++ b/src/views/mod-tools/views/user/user-room-visits/ModToolsUserRoomVisitsView.tsx
@@ -0,0 +1,69 @@
+import { GetRoomVisitsMessageComposer, RoomVisitsData, RoomVisitsEvent } from '@nitrots/nitro-renderer';
+import { FC, useCallback, useEffect, useState } from 'react';
+import { AutoSizer, List, ListRowProps, ListRowRenderer } from 'react-virtualized';
+import { TryVisitRoom } from '../../../../../api';
+import { CreateMessageHook, SendMessageHook } from '../../../../../hooks';
+import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../../layout';
+import { ModToolsUserRoomVisitsViewProps } from './ModToolsUserRoomVisitsView.types';
+
+export const ModToolsUserRoomVisitsView: FC = props =>
+{
+ const { userId = null, onCloseClick = null } = props;
+
+ const [roomVisitData, setRoomVisitData] = useState(null);
+
+ useEffect(() =>
+ {
+ SendMessageHook(new GetRoomVisitsMessageComposer(userId));
+ }, [userId]);
+
+ const onModtoolReceivedRoomsUserEvent = useCallback((event: RoomVisitsEvent) =>
+ {
+ const parser = event.getParser();
+
+ if(!parser || parser.data.userId !== userId) return;
+
+ setRoomVisitData(parser.data);
+ }, [userId]);
+
+ CreateMessageHook(RoomVisitsEvent, onModtoolReceivedRoomsUserEvent);
+
+ const RowRenderer: ListRowRenderer = (props: ListRowProps) =>
+ {
+ const item = roomVisitData.rooms[props.index];
+
+ return (
+
+
{item.enterHour.toString().padStart(2, '0')}:{item.enterMinute.toString().padStart(2, '0')}
+
Room: {item.roomName}
+
+
);
+ }
+
+ return (
+
+ onCloseClick() } />
+
+ {roomVisitData &&
+
+
+ {({ height, width }) =>
+ {
+ return (
+
+ )
+ }}
+
+
+ }
+
+
+ );
+}
diff --git a/src/views/mod-tools/views/user/user-room-visits/ModToolsUserRoomVisitsView.types.ts b/src/views/mod-tools/views/user/user-room-visits/ModToolsUserRoomVisitsView.types.ts
new file mode 100644
index 00000000..a5188a3c
--- /dev/null
+++ b/src/views/mod-tools/views/user/user-room-visits/ModToolsUserRoomVisitsView.types.ts
@@ -0,0 +1,6 @@
+
+export interface ModToolsUserRoomVisitsViewProps
+{
+ userId: number;
+ onCloseClick: () => void;
+}
diff --git a/src/views/mod-tools/views/user/user-sendmessage/ModToolsSendUserMessage.types.ts b/src/views/mod-tools/views/user/user-sendmessage/ModToolsSendUserMessage.types.ts
new file mode 100644
index 00000000..617945f0
--- /dev/null
+++ b/src/views/mod-tools/views/user/user-sendmessage/ModToolsSendUserMessage.types.ts
@@ -0,0 +1,7 @@
+import { ISelectedUser } from '../../../utils/ISelectedUser';
+
+export interface ModToolsSendUserMessageViewProps
+{
+ user: ISelectedUser;
+ onCloseClick: () => void;
+}
diff --git a/src/views/mod-tools/views/user/user-sendmessage/ModToolsSendUserMessageView.tsx b/src/views/mod-tools/views/user/user-sendmessage/ModToolsSendUserMessageView.tsx
new file mode 100644
index 00000000..a332b426
--- /dev/null
+++ b/src/views/mod-tools/views/user/user-sendmessage/ModToolsSendUserMessageView.tsx
@@ -0,0 +1,44 @@
+import { ModMessageMessageComposer } from '@nitrots/nitro-renderer';
+import { FC, useCallback, useState } from 'react';
+import { NotificationAlertEvent } from '../../../../../events';
+import { dispatchUiEvent, SendMessageHook } from '../../../../../hooks';
+import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../../layout';
+import { ModToolsSendUserMessageViewProps } from './ModToolsSendUserMessage.types';
+
+export const ModToolsSendUserMessageView: FC = props =>
+{
+ const { user = null, onCloseClick = null } = props;
+ const [message, setMessage] = useState('');
+
+
+ const sendMessage = useCallback(() =>
+ {
+ if(message.trim().length === 0)
+ {
+ dispatchUiEvent(new NotificationAlertEvent(['Please write a message to user.'], null, null, null, 'Error', null));
+ return;
+ }
+
+ SendMessageHook(new ModMessageMessageComposer(user.userId, message, -999));
+
+ onCloseClick();
+ }, [message, onCloseClick, user.userId]);
+
+ return (
+
+ onCloseClick() } />
+
+ {user && <>
+ Message To: {user.username}
+
+
+
+
+
+
+
+ >}
+
+
+ );
+}
diff --git a/src/views/navigator/NavigatorMessageHandler.tsx b/src/views/navigator/NavigatorMessageHandler.tsx
index f2e4e019..3ca2dea8 100644
--- a/src/views/navigator/NavigatorMessageHandler.tsx
+++ b/src/views/navigator/NavigatorMessageHandler.tsx
@@ -1,4 +1,4 @@
-import { GenericErrorEvent, NavigatorCategoriesComposer, NavigatorCategoriesEvent, NavigatorHomeRoomEvent, NavigatorMetadataEvent, NavigatorSearchEvent, NavigatorSettingsComposer, RoomCreatedEvent, RoomDataParser, RoomDoorbellAcceptedEvent, RoomDoorbellEvent, RoomDoorbellRejectedEvent, RoomForwardEvent, RoomInfoComposer, RoomInfoEvent, RoomInfoOwnerEvent, RoomSettingsUpdatedEvent, UserInfoEvent } from '@nitrots/nitro-renderer';
+import { GenericErrorEvent, GetGuestRoomResultEvent, NavigatorCategoriesComposer, NavigatorCategoriesEvent, NavigatorHomeRoomEvent, NavigatorMetadataEvent, NavigatorSearchEvent, NavigatorSettingsComposer, RoomCreatedEvent, RoomDataParser, RoomDoorbellAcceptedEvent, RoomDoorbellEvent, RoomDoorbellRejectedEvent, RoomEntryInfoMessageEvent, RoomForwardEvent, RoomInfoComposer, RoomSettingsUpdatedEvent, UserInfoEvent } from '@nitrots/nitro-renderer';
import { FC, useCallback } from 'react';
import { CreateRoomSession, GetSessionDataManager } from '../../api';
import { UpdateDoorStateEvent } from '../../events';
@@ -25,7 +25,7 @@ export const NavigatorMessageHandler: FC = props =
SendMessageHook(new RoomInfoComposer(parser.roomId, false, true));
}, []);
- const onRoomInfoOwnerEvent = useCallback((event: RoomInfoOwnerEvent) =>
+ const onRoomEntryInfoMessageEvent = useCallback((event: RoomEntryInfoMessageEvent) =>
{
const parser = event.getParser();
@@ -43,7 +43,7 @@ export const NavigatorMessageHandler: FC = props =
SendMessageHook(new RoomInfoComposer(parser.roomId, true, false));
}, [ navigatorState, dispatchNavigatorState ]);
- const onRoomInfoEvent = useCallback((event: RoomInfoEvent) =>
+ const onGetGuestRoomResultEvent = useCallback((event: GetGuestRoomResultEvent) =>
{
const parser = event.getParser();
@@ -196,8 +196,8 @@ export const NavigatorMessageHandler: FC = props =
CreateMessageHook(UserInfoEvent, onUserInfoEvent);
CreateMessageHook(RoomForwardEvent, onRoomForwardEvent);
- CreateMessageHook(RoomInfoOwnerEvent, onRoomInfoOwnerEvent);
- CreateMessageHook(RoomInfoEvent, onRoomInfoEvent);
+ CreateMessageHook(RoomEntryInfoMessageEvent, onRoomEntryInfoMessageEvent);
+ CreateMessageHook(GetGuestRoomResultEvent, onGetGuestRoomResultEvent);
CreateMessageHook(RoomDoorbellEvent, onRoomDoorbellEvent);
CreateMessageHook(RoomDoorbellAcceptedEvent, onRoomDoorbellAcceptedEvent);
CreateMessageHook(RoomDoorbellRejectedEvent, onRoomDoorbellRejectedEvent);
diff --git a/src/views/navigator/NavigatorView.tsx b/src/views/navigator/NavigatorView.tsx
index 892fa2ae..d26d9147 100644
--- a/src/views/navigator/NavigatorView.tsx
+++ b/src/views/navigator/NavigatorView.tsx
@@ -27,7 +27,7 @@ export const NavigatorView: FC = props =>
const [ isRoomLinkOpen, setRoomLinkOpen ] = useState(false);
const [ pendingDoorState, setPendingDoorState ] = useState<{ roomData: RoomDataParser, state: string }>(null);
const [ navigatorState, dispatchNavigatorState ] = useReducer(NavigatorReducer, initialNavigator);
- const { needsNavigatorUpdate = false, topLevelContext = null, topLevelContexts = null } = navigatorState;
+ const { needsNavigatorUpdate = false, topLevelContext = null, topLevelContexts = null, homeRoomId } = navigatorState;
const onNavigatorEvent = useCallback((event: NavigatorEvent) =>
{
@@ -119,6 +119,13 @@ export const NavigatorView: FC = props =>
SendMessageHook(new NavigatorSearchComposer(contextCode, searchValue));
}, []);
+ const goToHomeRoom = useCallback(() =>
+ {
+ if(homeRoomId <= 0) return;
+
+ TryVisitRoom(homeRoomId);
+ }, [ homeRoomId ]);
+
const linkReceived = useCallback((url: string) =>
{
const parts = url.split('/');
@@ -133,7 +140,7 @@ export const NavigatorView: FC = props =>
switch(parts[2])
{
case 'home':
- //goToHomeRoom();
+ goToHomeRoom();
break;
default: {
const roomId = parseInt(parts[2]);
@@ -147,8 +154,8 @@ export const NavigatorView: FC = props =>
setIsVisible(true);
setCreatorOpen(true);
return;
- }
- }, []);
+ }
+ }, [ goToHomeRoom ]);
const closePendingDoorState = useCallback((state: string) =>
{
diff --git a/src/views/navigator/views/room-info/NavigatorRoomInfoView.tsx b/src/views/navigator/views/room-info/NavigatorRoomInfoView.tsx
index f46de29c..f0a9ab42 100644
--- a/src/views/navigator/views/room-info/NavigatorRoomInfoView.tsx
+++ b/src/views/navigator/views/room-info/NavigatorRoomInfoView.tsx
@@ -3,6 +3,7 @@ import classNames from 'classnames';
import { FC, useCallback, useEffect, useState } from 'react';
import { GetConfiguration, GetGroupInformation, GetSessionDataManager, LocalizeText } from '../../../../api';
import { NavigatorEvent } from '../../../../events';
+import { FloorplanEditorEvent } from '../../../../events/floorplan-editor/FloorplanEditorEvent';
import { RoomWidgetThumbnailEvent } from '../../../../events/room-widgets/thumbnail';
import { dispatchUiEvent } from '../../../../hooks/events';
import { SendMessageHook } from '../../../../hooks/messages';
@@ -98,6 +99,9 @@ export const NavigatorRoomInfoView: FC = props =>
setIsRoomMuted(value => !value);
SendMessageHook(new RoomMuteComposer());
return;
+ case 'open_floorplan_editor':
+ dispatchUiEvent(new FloorplanEditorEvent(FloorplanEditorEvent.TOGGLE_FLOORPLAN_EDITOR));
+ return;
case 'close':
onCloseClick();
return;
@@ -155,7 +159,7 @@ export const NavigatorRoomInfoView: FC = props =>
{ hasPermission('settings') && <>
-
+
> }
{ hasPermission('staff_pick') && }
diff --git a/src/views/notification-center/NotificationCenterMessageHandler.tsx b/src/views/notification-center/NotificationCenterMessageHandler.tsx
index 9afc098d..76c68e65 100644
--- a/src/views/notification-center/NotificationCenterMessageHandler.tsx
+++ b/src/views/notification-center/NotificationCenterMessageHandler.tsx
@@ -60,7 +60,7 @@ export const NotificationCenterMessageHandler: FC
+
{ LocalizeText('notifications.text.club_gift') }
diff --git a/src/views/notification-center/views/bubble-layouts/default/NotificationDefaultBubbleView.tsx b/src/views/notification-center/views/bubble-layouts/default/NotificationDefaultBubbleView.tsx
index 3e70b32c..4b748636 100644
--- a/src/views/notification-center/views/bubble-layouts/default/NotificationDefaultBubbleView.tsx
+++ b/src/views/notification-center/views/bubble-layouts/default/NotificationDefaultBubbleView.tsx
@@ -7,7 +7,7 @@ export const NotificationDefaultBubbleView: FC
+
{ (item.iconUrl && item.iconUrl.length) && }
{ item.message }
diff --git a/src/views/purse/PurseView.tsx b/src/views/purse/PurseView.tsx
index 7a2f8028..bfab90ea 100644
--- a/src/views/purse/PurseView.tsx
+++ b/src/views/purse/PurseView.tsx
@@ -1,6 +1,7 @@
import { FriendlyTime, HabboClubLevelEnum, UserCurrencyComposer, UserSubscriptionComposer } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { GetConfiguration, LocalizeText } from '../../api';
+import { HelpEvent } from '../../events/help/HelpEvent';
import { UserSettingsUIEvent } from '../../events/user-settings/UserSettingsUIEvent';
import { dispatchUiEvent } from '../../hooks';
import { SendMessageHook } from '../../hooks/messages/message-event';
@@ -24,6 +25,11 @@ export const PurseView: FC<{}> = props =>
dispatchUiEvent(new UserSettingsUIEvent(UserSettingsUIEvent.TOGGLE_USER_SETTINGS));
}, []);
+ const handleHelpCenterClick = useCallback(() =>
+ {
+ dispatchUiEvent(new HelpEvent(HelpEvent.TOGGLE_HELP_CENTER));
+ }, []);
+
const displayedCurrencies = useMemo(() =>
{
return GetConfiguration('system.currency.types', []);
@@ -140,7 +146,7 @@ export const PurseView: FC<{}> = props =>
diff --git a/src/views/room/RoomView.tsx b/src/views/room/RoomView.tsx
index 9ab44e69..b885b48c 100644
--- a/src/views/room/RoomView.tsx
+++ b/src/views/room/RoomView.tsx
@@ -1,6 +1,6 @@
import { EventDispatcher, NitroRectangle, RoomGeometry, RoomVariableEnum, Vector3d } from '@nitrots/nitro-renderer';
import { FC, useEffect, useRef, useState } from 'react';
-import { DispatchMouseEvent, DispatchTouchEvent, DoorbellWidgetHandler, FurniChooserWidgetHandler, FurnitureContextMenuWidgetHandler, FurnitureCreditWidgetHandler, FurnitureCustomStackHeightWidgetHandler, FurnitureDimmerWidgetHandler, FurnitureExternalImageWidgetHandler, FurniturePresentWidgetHandler, GetNitroInstance, GetRoomEngine, InitializeRoomInstanceRenderingCanvas, IRoomWidgetHandlerManager, RoomWidgetAvatarInfoHandler, RoomWidgetChatHandler, RoomWidgetChatInputHandler, RoomWidgetHandlerManager, RoomWidgetInfostandHandler, RoomWidgetRoomToolsHandler, RoomWidgetUpdateRoomViewEvent, UserChooserWidgetHandler } from '../../api';
+import { DispatchMouseEvent, DispatchTouchEvent, DoorbellWidgetHandler, FurniChooserWidgetHandler, FurnitureContextMenuWidgetHandler, FurnitureCreditWidgetHandler, FurnitureCustomStackHeightWidgetHandler, FurnitureDimmerWidgetHandler, FurnitureExternalImageWidgetHandler, FurnitureMannequinWidgetHandler, FurniturePresentWidgetHandler, GetNitroInstance, GetRoomEngine, InitializeRoomInstanceRenderingCanvas, IRoomWidgetHandlerManager, RoomWidgetAvatarInfoHandler, RoomWidgetChatHandler, RoomWidgetChatInputHandler, RoomWidgetHandlerManager, RoomWidgetInfostandHandler, RoomWidgetRoomToolsHandler, RoomWidgetUpdateRoomViewEvent, UserChooserWidgetHandler } from '../../api';
import { FurnitureYoutubeDisplayWidgetHandler } from '../../api/nitro/room/widgets/handlers/FurnitureYoutubeDisplayWidgetHandler';
import { RoomContextProvider } from './context/RoomContext';
import { RoomColorView } from './RoomColorView';
@@ -46,13 +46,14 @@ export const RoomView: FC = props =>
widgetHandlerManager.registerHandler(new FurniturePresentWidgetHandler());
widgetHandlerManager.registerHandler(new FurnitureDimmerWidgetHandler());
widgetHandlerManager.registerHandler(new FurnitureYoutubeDisplayWidgetHandler());
+ widgetHandlerManager.registerHandler(new FurnitureMannequinWidgetHandler());
setWidgetHandler(widgetHandlerManager);
GetNitroInstance().renderer.resize(window.innerWidth, window.innerHeight);
const canvasId = 1;
-
+
const displayObject = GetRoomEngine().getRoomInstanceDisplay(roomSession.roomId, canvasId, GetNitroInstance().width, GetNitroInstance().height, RoomGeometry.SCALE_ZOOMED_IN);
if(!displayObject) return;
diff --git a/src/views/room/widgets/RoomWidgetsView.tsx b/src/views/room/widgets/RoomWidgetsView.tsx
index c2c8922d..3fd67cdc 100644
--- a/src/views/room/widgets/RoomWidgetsView.tsx
+++ b/src/views/room/widgets/RoomWidgetsView.tsx
@@ -1,7 +1,9 @@
import { RoomEngineEvent, RoomEngineObjectEvent, RoomEngineRoomAdEvent, RoomEngineTriggerWidgetEvent, RoomEngineUseProductEvent, RoomId, RoomObjectCategory, RoomObjectOperationType, RoomObjectVariable, RoomSessionChatEvent, RoomSessionDanceEvent, RoomSessionDimmerPresetsEvent, RoomSessionDoorbellEvent, RoomSessionErrorMessageEvent, RoomSessionEvent, RoomSessionFriendRequestEvent, RoomSessionPetInfoUpdateEvent, RoomSessionPresentEvent, RoomSessionUserBadgesEvent, RoomZoomEvent } from '@nitrots/nitro-renderer';
import { FC, useCallback } from 'react';
-import { CanManipulateFurniture, GetRoomEngine, GetSessionDataManager, IsFurnitureSelectionDisabled, LocalizeText, ProcessRoomObjectOperation, RoomWidgetFurniToWidgetMessage, RoomWidgetRoomEngineUpdateEvent, RoomWidgetRoomObjectUpdateEvent } from '../../../api';
+import { CanManipulateFurniture, GetRoomEngine, GetSessionDataManager, IsFurnitureSelectionDisabled, LocalizeText, ProcessRoomObjectOperation, RoomWidgetFurniToWidgetMessage, RoomWidgetUpdateRoomEngineEvent, RoomWidgetUpdateRoomObjectEvent } from '../../../api';
import { useRoomEngineEvent, useRoomSessionManagerEvent } from '../../../hooks/events';
+import { NotificationAlertType } from '../../notification-center/common/NotificationAlertType';
+import { NotificationUtilities } from '../../notification-center/common/NotificationUtilities';
import { useRoomContext } from '../context/RoomContext';
import { AvatarInfoWidgetView } from './avatar-info/AvatarInfoWidgetView';
import { ChatInputView } from './chat-input/ChatInputView';
@@ -25,10 +27,10 @@ export const RoomWidgetsView: FC = props =>
switch(event.type)
{
case RoomEngineEvent.NORMAL_MODE:
- eventDispatcher.dispatchEvent(new RoomWidgetRoomEngineUpdateEvent(RoomWidgetRoomEngineUpdateEvent.NORMAL_MODE, event.roomId));
+ eventDispatcher.dispatchEvent(new RoomWidgetUpdateRoomEngineEvent(RoomWidgetUpdateRoomEngineEvent.NORMAL_MODE, event.roomId));
return;
case RoomEngineEvent.GAME_MODE:
- eventDispatcher.dispatchEvent(new RoomWidgetRoomEngineUpdateEvent(RoomWidgetRoomEngineUpdateEvent.GAME_MODE, event.roomId));
+ eventDispatcher.dispatchEvent(new RoomWidgetUpdateRoomEngineEvent(RoomWidgetUpdateRoomEngineEvent.GAME_MODE, event.roomId));
return;
case RoomZoomEvent.ROOM_ZOOM: {
const zoomEvent = (event as RoomZoomEvent);
@@ -65,15 +67,15 @@ export const RoomWidgetsView: FC = props =>
const objectId = event.objectId;
const category = event.category;
- let updateEvent: RoomWidgetRoomObjectUpdateEvent = null;
+ let updateEvent: RoomWidgetUpdateRoomObjectEvent = null;
switch(event.type)
{
case RoomEngineObjectEvent.SELECTED:
- if(!IsFurnitureSelectionDisabled(event)) updateEvent = new RoomWidgetRoomObjectUpdateEvent(RoomWidgetRoomObjectUpdateEvent.OBJECT_SELECTED, objectId, category, event.roomId);
+ if(!IsFurnitureSelectionDisabled(event)) updateEvent = new RoomWidgetUpdateRoomObjectEvent(RoomWidgetUpdateRoomObjectEvent.OBJECT_SELECTED, objectId, category, event.roomId);
break;
case RoomEngineObjectEvent.DESELECTED:
- updateEvent = new RoomWidgetRoomObjectUpdateEvent(RoomWidgetRoomObjectUpdateEvent.OBJECT_DESELECTED, objectId, category, event.roomId);
+ updateEvent = new RoomWidgetUpdateRoomObjectEvent(RoomWidgetUpdateRoomObjectEvent.OBJECT_DESELECTED, objectId, category, event.roomId);
break;
case RoomEngineObjectEvent.ADDED: {
let addedEventType: string = null;
@@ -82,14 +84,14 @@ export const RoomWidgetsView: FC = props =>
{
case RoomObjectCategory.FLOOR:
case RoomObjectCategory.WALL:
- addedEventType = RoomWidgetRoomObjectUpdateEvent.FURNI_ADDED;
+ addedEventType = RoomWidgetUpdateRoomObjectEvent.FURNI_ADDED;
break;
case RoomObjectCategory.UNIT:
- addedEventType = RoomWidgetRoomObjectUpdateEvent.USER_ADDED;
+ addedEventType = RoomWidgetUpdateRoomObjectEvent.USER_ADDED;
break;
}
- if(addedEventType) updateEvent = new RoomWidgetRoomObjectUpdateEvent(addedEventType, objectId, category, event.roomId);
+ if(addedEventType) updateEvent = new RoomWidgetUpdateRoomObjectEvent(addedEventType, objectId, category, event.roomId);
break;
}
case RoomEngineObjectEvent.REMOVED: {
@@ -99,14 +101,14 @@ export const RoomWidgetsView: FC = props =>
{
case RoomObjectCategory.FLOOR:
case RoomObjectCategory.WALL:
- removedEventType = RoomWidgetRoomObjectUpdateEvent.FURNI_REMOVED;
+ removedEventType = RoomWidgetUpdateRoomObjectEvent.FURNI_REMOVED;
break;
case RoomObjectCategory.UNIT:
- removedEventType = RoomWidgetRoomObjectUpdateEvent.USER_REMOVED;
+ removedEventType = RoomWidgetUpdateRoomObjectEvent.USER_REMOVED;
break;
}
- if(removedEventType) updateEvent = new RoomWidgetRoomObjectUpdateEvent(removedEventType, objectId, category, event.roomId);
+ if(removedEventType) updateEvent = new RoomWidgetUpdateRoomObjectEvent(removedEventType, objectId, category, event.roomId);
break;
}
case RoomEngineObjectEvent.REQUEST_MOVE:
@@ -116,13 +118,13 @@ export const RoomWidgetsView: FC = props =>
if(CanManipulateFurniture(roomSession, objectId, category)) ProcessRoomObjectOperation(objectId, category, RoomObjectOperationType.OBJECT_ROTATE_POSITIVE);
break;
case RoomEngineObjectEvent.REQUEST_MANIPULATION:
- if(CanManipulateFurniture(roomSession, objectId, category)) updateEvent = new RoomWidgetRoomObjectUpdateEvent(RoomWidgetRoomObjectUpdateEvent.OBJECT_REQUEST_MANIPULATION, objectId, category, event.roomId);
+ if(CanManipulateFurniture(roomSession, objectId, category)) updateEvent = new RoomWidgetUpdateRoomObjectEvent(RoomWidgetUpdateRoomObjectEvent.OBJECT_REQUEST_MANIPULATION, objectId, category, event.roomId);
break;
case RoomEngineObjectEvent.MOUSE_ENTER:
- updateEvent = new RoomWidgetRoomObjectUpdateEvent(RoomWidgetRoomObjectUpdateEvent.OBJECT_ROLL_OVER, objectId, category, event.roomId);
+ updateEvent = new RoomWidgetUpdateRoomObjectEvent(RoomWidgetUpdateRoomObjectEvent.OBJECT_ROLL_OVER, objectId, category, event.roomId);
break;
case RoomEngineObjectEvent.MOUSE_LEAVE:
- updateEvent = new RoomWidgetRoomObjectUpdateEvent(RoomWidgetRoomObjectUpdateEvent.OBJECT_ROLL_OUT, objectId, category, event.roomId);
+ updateEvent = new RoomWidgetUpdateRoomObjectEvent(RoomWidgetUpdateRoomObjectEvent.OBJECT_ROLL_OUT, objectId, category, event.roomId);
break;
case RoomEngineTriggerWidgetEvent.REQUEST_CREDITFURNI:
widgetHandler.processWidgetMessage(new RoomWidgetFurniToWidgetMessage(RoomWidgetFurniToWidgetMessage.REQUEST_CREDITFURNI, objectId, category, event.roomId));
@@ -200,7 +202,7 @@ export const RoomWidgetsView: FC = props =>
{
let dispatchEvent = true;
- if(updateEvent instanceof RoomWidgetRoomObjectUpdateEvent) dispatchEvent = (!RoomId.isRoomPreviewerId(updateEvent.roomId));
+ if(updateEvent instanceof RoomWidgetUpdateRoomObjectEvent) dispatchEvent = (!RoomId.isRoomPreviewerId(updateEvent.roomId));
if(dispatchEvent) widgetHandler.eventDispatcher.dispatchEvent(updateEvent);
}
@@ -316,6 +318,8 @@ export const RoomWidgetsView: FC = props =>
default:
return;
}
+
+ NotificationUtilities.simpleAlert(errorMessage, NotificationAlertType.DEFAULT, null, null, errorTitle);
}, []);
useRoomSessionManagerEvent(RoomSessionErrorMessageEvent.RSEME_KICKED, onRoomSessionErrorMessageEvent);
diff --git a/src/views/room/widgets/avatar-info/AvatarInfoWidgetView.tsx b/src/views/room/widgets/avatar-info/AvatarInfoWidgetView.tsx
index a12d17bc..7014b2f7 100644
--- a/src/views/room/widgets/avatar-info/AvatarInfoWidgetView.tsx
+++ b/src/views/room/widgets/avatar-info/AvatarInfoWidgetView.tsx
@@ -1,6 +1,6 @@
import { RoomEnterEffect, RoomObjectCategory } from '@nitrots/nitro-renderer';
import { FC, useCallback, useMemo, useState } from 'react';
-import { GetRoomSession, GetSessionDataManager, RoomWidgetObjectNameEvent, RoomWidgetRoomEngineUpdateEvent, RoomWidgetRoomObjectMessage, RoomWidgetRoomObjectUpdateEvent, RoomWidgetUpdateDanceStatusEvent, RoomWidgetUpdateDecorateModeEvent, RoomWidgetUpdateInfostandEvent, RoomWidgetUpdateInfostandFurniEvent, RoomWidgetUpdateInfostandPetEvent, RoomWidgetUpdateInfostandRentableBotEvent, RoomWidgetUpdateInfostandUserEvent, RoomWidgetUpdateRentableBotChatEvent, RoomWidgetUseProductBubbleEvent, UseProductItem } from '../../../../api';
+import { GetRoomSession, GetSessionDataManager, RoomWidgetObjectNameEvent, RoomWidgetRoomObjectMessage, RoomWidgetUpdateDanceStatusEvent, RoomWidgetUpdateDecorateModeEvent, RoomWidgetUpdateInfostandEvent, RoomWidgetUpdateInfostandFurniEvent, RoomWidgetUpdateInfostandPetEvent, RoomWidgetUpdateInfostandRentableBotEvent, RoomWidgetUpdateInfostandUserEvent, RoomWidgetUpdateRentableBotChatEvent, RoomWidgetUpdateRoomEngineEvent, RoomWidgetUpdateRoomObjectEvent, RoomWidgetUseProductBubbleEvent, UseProductItem } from '../../../../api';
import { CreateEventDispatcherHook } from '../../../../hooks/events/event-dispatcher.base';
import { useRoomContext } from '../../context/RoomContext';
import { AvatarInfoWidgetAvatarView } from './views/avatar/AvatarInfoWidgetAvatarView';
@@ -73,25 +73,25 @@ export const AvatarInfoWidgetView: FC<{}> = props =>
setProductBubbles([]);
}, []);
- const onRoomWidgetRoomEngineUpdateEvent = useCallback((event: RoomWidgetRoomEngineUpdateEvent) =>
+ const onRoomWidgetRoomEngineUpdateEvent = useCallback((event: RoomWidgetUpdateRoomEngineEvent) =>
{
switch(event.type)
{
- case RoomWidgetRoomEngineUpdateEvent.NORMAL_MODE: {
+ case RoomWidgetUpdateRoomEngineEvent.NORMAL_MODE: {
if(isGameMode) setGameMode(false);
return;
}
- case RoomWidgetRoomEngineUpdateEvent.GAME_MODE: {
+ case RoomWidgetUpdateRoomEngineEvent.GAME_MODE: {
if(!isGameMode) setGameMode(true);
return;
}
}
}, [ isGameMode ]);
- CreateEventDispatcherHook(RoomWidgetRoomEngineUpdateEvent.NORMAL_MODE, eventDispatcher, onRoomWidgetRoomEngineUpdateEvent);
- CreateEventDispatcherHook(RoomWidgetRoomEngineUpdateEvent.GAME_MODE, eventDispatcher, onRoomWidgetRoomEngineUpdateEvent);
+ CreateEventDispatcherHook(RoomWidgetUpdateRoomEngineEvent.NORMAL_MODE, eventDispatcher, onRoomWidgetRoomEngineUpdateEvent);
+ CreateEventDispatcherHook(RoomWidgetUpdateRoomEngineEvent.GAME_MODE, eventDispatcher, onRoomWidgetRoomEngineUpdateEvent);
- const onRoomObjectRemoved = useCallback((event: RoomWidgetRoomObjectUpdateEvent) =>
+ const onRoomObjectRemoved = useCallback((event: RoomWidgetUpdateRoomObjectEvent) =>
{
if(name)
{
@@ -152,15 +152,15 @@ export const AvatarInfoWidgetView: FC<{}> = props =>
}
}, [ name, infoStandEvent, nameBubbles, productBubbles, removeNameBubble, clearInfoStandEvent ]);
- CreateEventDispatcherHook(RoomWidgetRoomObjectUpdateEvent.USER_REMOVED, eventDispatcher, onRoomObjectRemoved);
- CreateEventDispatcherHook(RoomWidgetRoomObjectUpdateEvent.FURNI_REMOVED, eventDispatcher, onRoomObjectRemoved);
+ CreateEventDispatcherHook(RoomWidgetUpdateRoomObjectEvent.USER_REMOVED, eventDispatcher, onRoomObjectRemoved);
+ CreateEventDispatcherHook(RoomWidgetUpdateRoomObjectEvent.FURNI_REMOVED, eventDispatcher, onRoomObjectRemoved);
- const onObjectRolled = useCallback((event: RoomWidgetRoomObjectUpdateEvent) =>
+ const onObjectRolled = useCallback((event: RoomWidgetUpdateRoomObjectEvent) =>
{
switch(event.type)
{
- case RoomWidgetRoomObjectUpdateEvent.OBJECT_ROLL_OVER: {
- const roomObjectEvent = (event as RoomWidgetRoomObjectUpdateEvent);
+ case RoomWidgetUpdateRoomObjectEvent.OBJECT_ROLL_OVER: {
+ const roomObjectEvent = (event as RoomWidgetUpdateRoomObjectEvent);
if(infoStandEvent) return;
@@ -168,8 +168,8 @@ export const AvatarInfoWidgetView: FC<{}> = props =>
return;
}
- case RoomWidgetRoomObjectUpdateEvent.OBJECT_ROLL_OUT: {
- const roomObjectEvent = (event as RoomWidgetRoomObjectUpdateEvent);
+ case RoomWidgetUpdateRoomObjectEvent.OBJECT_ROLL_OUT: {
+ const roomObjectEvent = (event as RoomWidgetUpdateRoomObjectEvent);
if(!name || (name.roomIndex !== roomObjectEvent.id)) return;
@@ -180,16 +180,16 @@ export const AvatarInfoWidgetView: FC<{}> = props =>
}
}, [ infoStandEvent, name, widgetHandler ]);
- CreateEventDispatcherHook(RoomWidgetRoomObjectUpdateEvent.OBJECT_ROLL_OVER, eventDispatcher, onObjectRolled);
- CreateEventDispatcherHook(RoomWidgetRoomObjectUpdateEvent.OBJECT_ROLL_OUT, eventDispatcher, onObjectRolled);
+ CreateEventDispatcherHook(RoomWidgetUpdateRoomObjectEvent.OBJECT_ROLL_OVER, eventDispatcher, onObjectRolled);
+ CreateEventDispatcherHook(RoomWidgetUpdateRoomObjectEvent.OBJECT_ROLL_OUT, eventDispatcher, onObjectRolled);
- const onObjectDeselected = useCallback((event: RoomWidgetRoomObjectUpdateEvent) =>
+ const onObjectDeselected = useCallback((event: RoomWidgetUpdateRoomObjectEvent) =>
{
if(infoStandEvent) clearInfoStandEvent();
if(productBubbles.length) setProductBubbles([]);
}, [ infoStandEvent, productBubbles, clearInfoStandEvent ]);
- CreateEventDispatcherHook(RoomWidgetRoomObjectUpdateEvent.OBJECT_DESELECTED, eventDispatcher, onObjectDeselected);
+ CreateEventDispatcherHook(RoomWidgetUpdateRoomObjectEvent.OBJECT_DESELECTED, eventDispatcher, onObjectDeselected);
const onRoomWidgetObjectNameEvent = useCallback((event: RoomWidgetObjectNameEvent) =>
{
diff --git a/src/views/room/widgets/avatar-info/views/own-avatar/AvatarInfoWidgetOwnAvatarView.tsx b/src/views/room/widgets/avatar-info/views/own-avatar/AvatarInfoWidgetOwnAvatarView.tsx
index 1fa8c8e0..e20da225 100644
--- a/src/views/room/widgets/avatar-info/views/own-avatar/AvatarInfoWidgetOwnAvatarView.tsx
+++ b/src/views/room/widgets/avatar-info/views/own-avatar/AvatarInfoWidgetOwnAvatarView.tsx
@@ -1,5 +1,5 @@
-import { AvatarAction, AvatarExpressionEnum, RoomObjectCategory, UserProfileComposer } from '@nitrots/nitro-renderer';
-import { FC, useCallback, useState } from 'react';
+import { AvatarAction, AvatarExpressionEnum, RoomControllerLevel, RoomObjectCategory, UserProfileComposer } from '@nitrots/nitro-renderer';
+import { FC, useCallback, useMemo, useState } from 'react';
import { GetCanStandUp, GetCanUseExpression, GetOwnPosture, HasHabboClub, HasHabboVip, IsRidingHorse, LocalizeText, RoomWidgetAvatarExpressionMessage, RoomWidgetChangePostureMessage, RoomWidgetDanceMessage, RoomWidgetMessage, RoomWidgetUpdateDecorateModeEvent, RoomWidgetUserActionMessage } from '../../../../../../api';
import { AvatarEditorEvent } from '../../../../../../events';
import { dispatchUiEvent, SendMessageHook } from '../../../../../../hooks';
@@ -107,6 +107,11 @@ export const AvatarInfoWidgetOwnAvatarView: FC
+ {
+ return (userData.amIOwner || userData.amIAnyRoomController || (userData.roomControllerLevel > RoomControllerLevel.GUEST));
+ }, [ userData ]);
const isRidingHorse = IsRidingHorse();
@@ -121,9 +126,10 @@ export const AvatarInfoWidgetOwnAvatarView: FC processAction('change_name') }>
{ LocalizeText('widget.avatar.change_name') }
}
- processAction('decorate') }>
- { LocalizeText('widget.avatar.decorate') }
-
+ { isShowDecorate &&
+ processAction('decorate') }>
+ { LocalizeText('widget.avatar.decorate') }
+ }
processAction('change_looks') }>
{ LocalizeText('widget.memenu.myclothes') }
diff --git a/src/views/room/widgets/chat-input/ChatInputView.scss b/src/views/room/widgets/chat-input/ChatInputView.scss
index 60995c42..650a2e73 100644
--- a/src/views/room/widgets/chat-input/ChatInputView.scss
+++ b/src/views/room/widgets/chat-input/ChatInputView.scss
@@ -1,13 +1,3 @@
-@media only screen and (max-width: 600px) {
- .nitro-chat-input {
- position: fixed;
- left: 50%;
- transform: translateX(-50%);
- bottom: 65px !important;
- z-index: $chatinput-zindex;
- }
-}
-
.nitro-chat-input-container {
display: flex;
justify-content: center;
@@ -17,9 +7,17 @@
border-radius: 8px;
border: 2px solid rgb(0, 0, 0);
background: #EDEDED;
- padding-right: 30px;
+ padding-right: 10px;
width: 100%;
+ @include media-breakpoint-down(sm) {
+ display: flex;
+ position: absolute;
+ bottom: 70px;
+ left: calc(100% / 3);
+ width: 200px;
+ }
+
&:before {
content: "";
position: absolute;
@@ -60,6 +58,44 @@
white-space: pre-wrap;
}
}
+
+ .bubble-container {
+ visibility: visible;
+ width: 75%;
+ }
}
-@import './style-selector/ChatInputStyleSelectorView';
+.nitro-chat-style-selector-button {
+ position: absolute;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100%;
+ right: 7px;
+}
+
+.nitro-chat-style-selector-container {
+ width: $chat-input-style-selector-widget-width;
+ max-height: $chat-input-style-selector-widget-height;
+
+ .content-area {
+ max-height: $chat-input-style-selector-widget-height !important;
+ }
+
+ .grid-item {
+ font-size: $font-size-sm;
+ height: 30px !important;
+ border-color: unset !important;
+ border: 0 !important;
+ padding: 1px 3px;
+
+ &:not(.active) {
+ background-color: unset !important;
+ }
+
+ .bubble-container {
+ visibility: visible;
+ width: 75%;
+ }
+ }
+}
diff --git a/src/views/room/widgets/chat-input/ChatInputView.tsx b/src/views/room/widgets/chat-input/ChatInputView.tsx
index 534dd93c..ca2b54d7 100644
--- a/src/views/room/widgets/chat-input/ChatInputView.tsx
+++ b/src/views/room/widgets/chat-input/ChatInputView.tsx
@@ -1,6 +1,7 @@
+import { HabboClubLevelEnum, RoomControllerLevel } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
-import { GetConfiguration, GetSessionDataManager, LocalizeText, RoomWidgetChatMessage, RoomWidgetChatTypingMessage, RoomWidgetRoomObjectUpdateEvent, RoomWidgetUpdateChatInputContentEvent, RoomWidgetUpdateInfostandUserEvent } from '../../../../api';
+import { GetConfiguration, GetSessionDataManager, LocalizeText, RoomWidgetChatMessage, RoomWidgetChatTypingMessage, RoomWidgetUpdateChatInputContentEvent, RoomWidgetUpdateInfostandUserEvent, RoomWidgetUpdateRoomObjectEvent } from '../../../../api';
import { CreateEventDispatcherHook } from '../../../../hooks/events';
import { useRoomContext } from '../../context/RoomContext';
import { ChatInputStyleSelectorView } from './style-selector/ChatInputStyleSelectorView';
@@ -117,6 +118,7 @@ export const ChatInputView: FC<{}> = props =>
{
if(needsChatStyleUpdate)
{
+ console.log('send')
GetSessionDataManager().sendChatStyleUpdate(chatStyleId);
setNeedsChatStyleUpdate(false);
@@ -175,18 +177,12 @@ export const ChatInputView: FC<{}> = props =>
}, [ inputRef, chatModeIdWhisper, anotherInputHasFocus, setInputFocus, checkSpecialKeywordForInput, sendChatValue ]);
- const onStyleSelected = useCallback((styleId: number) =>
- {
- setChatStyleId(styleId);
- setNeedsChatStyleUpdate(true);
- }, []);
-
- const onRoomWidgetRoomObjectUpdateEvent = useCallback((event: RoomWidgetRoomObjectUpdateEvent) =>
+ const onRoomWidgetRoomObjectUpdateEvent = useCallback((event: RoomWidgetUpdateRoomObjectEvent) =>
{
setSelectedUsername('');
}, []);
- CreateEventDispatcherHook(RoomWidgetRoomObjectUpdateEvent.OBJECT_DESELECTED, eventDispatcher, onRoomWidgetRoomObjectUpdateEvent);
+ CreateEventDispatcherHook(RoomWidgetUpdateRoomObjectEvent.OBJECT_DESELECTED, eventDispatcher, onRoomWidgetRoomObjectUpdateEvent);
const onRoomWidgetUpdateInfostandUserEvent = useCallback((event: RoomWidgetUpdateInfostandUserEvent) =>
{
@@ -210,6 +206,61 @@ export const ChatInputView: FC<{}> = props =>
CreateEventDispatcherHook(RoomWidgetUpdateChatInputContentEvent.CHAT_INPUT_CONTENT, eventDispatcher, onRoomWidgetChatInputContentUpdateEvent);
+ const selectChatStyleId = useCallback((styleId: number) =>
+ {
+ setChatStyleId(styleId);
+ setNeedsChatStyleUpdate(true);
+ }, []);
+
+ const chatStyleIds = useMemo(() =>
+ {
+ let styleIds: number[] = [];
+
+ const styles = GetConfiguration<{ styleId: number, minRank: number, isSystemStyle: boolean, isHcOnly: boolean, isAmbassadorOnly: boolean }[]>('chat.styles');
+
+ for(const style of styles)
+ {
+ if(!style) continue;
+
+ if(style.minRank > 0)
+ {
+ if(GetSessionDataManager().hasSecurity(style.minRank)) styleIds.push(style.styleId);
+
+ continue;
+ }
+
+ if(style.isSystemStyle)
+ {
+ if(GetSessionDataManager().hasSecurity(RoomControllerLevel.MODERATOR))
+ {
+ styleIds.push(style.styleId);
+
+ continue;
+ }
+ }
+
+ if(GetConfiguration('chat.styles.disabled').indexOf(style.styleId) >= 0) continue;
+
+ if(style.isHcOnly && (GetSessionDataManager().clubLevel >= HabboClubLevelEnum.CLUB))
+ {
+ styleIds.push(style.styleId);
+
+ continue;
+ }
+
+ if(style.isAmbassadorOnly && GetSessionDataManager().isAmbassador)
+ {
+ styleIds.push(style.styleId);
+
+ continue;
+ }
+
+ if(!style.isHcOnly && !style.isAmbassadorOnly) styleIds.push(style.styleId);
+ }
+
+ return styleIds;
+ }, []);
+
useEffect(() =>
{
if(isTyping)
@@ -273,7 +324,7 @@ export const ChatInputView: FC<{}> = props =>
updateChatInput(event.target.value) } onMouseDown={ event => setInputFocus() } />
-
+
, document.getElementById('toolbar-chat-input-container'))
);
}
diff --git a/src/views/room/widgets/chat-input/style-selector/ChatInputStyleSelectorView.scss b/src/views/room/widgets/chat-input/style-selector/ChatInputStyleSelectorView.scss
deleted file mode 100644
index 5be93b7c..00000000
--- a/src/views/room/widgets/chat-input/style-selector/ChatInputStyleSelectorView.scss
+++ /dev/null
@@ -1,61 +0,0 @@
-.nitro-chat-style-selector-button {
- position: absolute;
- display: flex;
- justify-content: center;
- align-items: center;
- height: 100%;
- right: 7px;
-}
-
-.nitro-chat-style-selector-test {
- display: flex;
- position: relative;
- right: 30px;
- pointer-events: all;
- height: 100%;
-
- i.icon {
- cursor: pointer;
- align-self: center;
- }
-
- .nitro-chatstyle-selector {
- position: absolute;
- width: 250px;
- top: -4px;
- transition: transform 0.22s ease-in-out;
- transform: translate(-81px, -50%) scale(0);
-
- &.active {
- visibility: visible;
- transform: translate(-160px, -100%) scale(1);
- }
-
- .grid-container {
-
- .grid-items {
- margin-top: -7px;
-
- .item-detail {
- height: 30px;
- max-height: 30px;
- width: calc(1 / 3 * 100% - (1 - 1 / 3) * 7px);
- margin: 7px 7px 0 0;
- overflow: visible;
-
- &:hover {
- cursor: pointer;
- }
-
- .detail-info {
-
- .bubble-container {
- visibility: visible;
- width: 75%;
- }
- }
- }
- }
- }
- }
-}
diff --git a/src/views/room/widgets/chat-input/style-selector/ChatInputStyleSelectorView.tsx b/src/views/room/widgets/chat-input/style-selector/ChatInputStyleSelectorView.tsx
index 52d1d43f..04bcb68f 100644
--- a/src/views/room/widgets/chat-input/style-selector/ChatInputStyleSelectorView.tsx
+++ b/src/views/room/widgets/chat-input/style-selector/ChatInputStyleSelectorView.tsx
@@ -1,20 +1,69 @@
-import { FC, useState } from 'react';
+import { FC, MouseEvent, useCallback, useEffect, useState } from 'react';
+import { Overlay, Popover } from 'react-bootstrap';
+import { BatchUpdates } from '../../../../../hooks';
+import { NitroCardContentView, NitroCardGridItemView, NitroCardGridView } from '../../../../../layout';
import { ChatInputStyleSelectorViewProps } from './ChatInputStyleSelectorView.types';
export const ChatInputStyleSelectorView: FC