Merge pull request #6 from billsonnn/feature/mod-tools

Feature/mod tools
This commit is contained in:
Bill 2021-11-12 00:16:25 -05:00 committed by GitHub
commit 5a7f758cde
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
92 changed files with 2825 additions and 303 deletions

View File

@ -2,6 +2,7 @@ import { IFurnitureData, NitroEvent, ObjectDataFactory, PetFigureData, PetRespec
import { GetNitroInstance, GetRoomEngine, GetSessionDataManager, IsOwnerOfFurniture } from '../../../..'; import { GetNitroInstance, GetRoomEngine, GetSessionDataManager, IsOwnerOfFurniture } from '../../../..';
import { InventoryTradeRequestEvent, WiredSelectObjectEvent } from '../../../../../events'; import { InventoryTradeRequestEvent, WiredSelectObjectEvent } from '../../../../../events';
import { FriendsSendFriendRequestEvent } from '../../../../../events/friends/FriendsSendFriendRequestEvent'; import { FriendsSendFriendRequestEvent } from '../../../../../events/friends/FriendsSendFriendRequestEvent';
import { HelpReportUserEvent } from '../../../../../events/help/HelpReportUserEvent';
import { dispatchUiEvent } from '../../../../../hooks/events'; import { dispatchUiEvent } from '../../../../../hooks/events';
import { SendMessageHook } from '../../../../../hooks/messages'; import { SendMessageHook } from '../../../../../hooks/messages';
import { PetSupplementEnum } from '../../../../../views/room/widgets/avatar-info/common/PetSupplementEnum'; import { PetSupplementEnum } from '../../../../../views/room/widgets/avatar-info/common/PetSupplementEnum';
@ -164,6 +165,7 @@ export class RoomWidgetInfostandHandler extends RoomWidgetHandler
case RoomWidgetUserActionMessage.REPORT: case RoomWidgetUserActionMessage.REPORT:
return; return;
case RoomWidgetUserActionMessage.REPORT_CFH_OTHER: case RoomWidgetUserActionMessage.REPORT_CFH_OTHER:
dispatchUiEvent(new HelpReportUserEvent(userId));
return; return;
case RoomWidgetUserActionMessage.AMBASSADOR_ALERT_USER: case RoomWidgetUserActionMessage.AMBASSADOR_ALERT_USER:
this.container.roomSession.sendAmbassadorAlertMessage(userId); this.container.roomSession.sendAmbassadorAlertMessage(userId);

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,9 @@
import { NitroEvent } from '@nitrots/nitro-renderer';
export class ChatHistoryEvent extends NitroEvent
{
public static SHOW_CHAT_HISTORY: string = 'CHE_SHOW_CHAT_HISTORY';
public static HIDE_CHAT_HISTORY: string = 'CHE_HIDE_CHAT_HISTORY';
public static TOGGLE_CHAT_HISTORY: string = 'CHE_TOGGLE_CHAT_HISTORY';
public static CHAT_HISTORY_CHANGED: string = 'CHE_CHAT_HISTORY_CHANGED';
}

View File

@ -0,0 +1,8 @@
import { NitroEvent } from '@nitrots/nitro-renderer';
export class HelpEvent extends NitroEvent
{
public static SHOW_HELP_CENTER: string = 'HCE_SHOW_HELP_CENTER';
public static HIDE_HELP_CENTER: string = 'HCE_HIDE_HELP_CENTER';
public static TOGGLE_HELP_CENTER: string = 'HCE_TOGGLE_HELP_CENTER';
}

View File

@ -0,0 +1,20 @@
import { HelpEvent } from './HelpEvent';
export class HelpReportUserEvent extends HelpEvent
{
public static REPORT_USER: string = 'HCE_HELP_CENTER_REPORT_USER';
private _reportedUserId: number;
constructor(userId: number)
{
super(HelpReportUserEvent.REPORT_USER);
this._reportedUserId = userId;
}
public get reportedUserId(): number
{
return this._reportedUserId;
}
}

View File

@ -5,6 +5,8 @@ export class ModToolsEvent extends NitroEvent
public static SHOW_MOD_TOOLS: string = 'MTE_SHOW_MOD_TOOLS'; public static SHOW_MOD_TOOLS: string = 'MTE_SHOW_MOD_TOOLS';
public static HIDE_MOD_TOOLS: string = 'MTE_HIDE_MOD_TOOLS'; public static HIDE_MOD_TOOLS: string = 'MTE_HIDE_MOD_TOOLS';
public static TOGGLE_MOD_TOOLS: string = 'MTE_TOGGLE_MOD_TOOLS'; public static TOGGLE_MOD_TOOLS: string = 'MTE_TOGGLE_MOD_TOOLS';
public static SELECT_USER: string = 'MTE_SELECT_USER';
public static OPEN_ROOM_INFO: string = 'MTE_OPEN_ROOM_INFO'; public static OPEN_ROOM_INFO: string = 'MTE_OPEN_ROOM_INFO';
public static OPEN_ROOM_CHATLOG: string = 'MTE_OPEN_ROOM_CHATLOG';
public static OPEN_USER_INFO: string = 'MTE_OPEN_USER_INFO';
public static OPEN_USER_CHATLOG: string = 'MTE_OPEN_USER_CHATLOG';
} }

View File

@ -0,0 +1,18 @@
import { ModToolsEvent } from './ModToolsEvent';
export class ModToolsOpenRoomChatlogEvent extends ModToolsEvent
{
private _roomId: number;
constructor(roomId: number)
{
super(ModToolsEvent.OPEN_ROOM_CHATLOG);
this._roomId = roomId;
}
public get roomId(): number
{
return this._roomId;
}
}

View File

@ -0,0 +1,18 @@
import { ModToolsEvent } from './ModToolsEvent';
export class ModToolsOpenUserChatlogEvent extends ModToolsEvent
{
private _userId: number;
constructor(userId: number)
{
super(ModToolsEvent.OPEN_USER_CHATLOG);
this._userId = userId;
}
public get userId(): number
{
return this._userId;
}
}

View File

@ -0,0 +1,18 @@
import { ModToolsEvent } from './ModToolsEvent';
export class ModToolsOpenUserInfoEvent extends ModToolsEvent
{
private _userId: number;
constructor(userId: number)
{
super(ModToolsEvent.OPEN_USER_INFO);
this._userId = userId;
}
public get userId(): number
{
return this._userId;
}
}

View File

@ -1,25 +0,0 @@
import { ModToolsEvent } from './ModToolsEvent';
export class ModToolsSelectUserEvent extends ModToolsEvent
{
private _webID: number;
private _name: string;
constructor(webID: number, name: string)
{
super(ModToolsEvent.SELECT_USER);
this._webID = webID;
this._name = name;
}
public get webID(): number
{
return this._webID;
}
public get name(): string
{
return this._name;
}
}

View File

@ -4,6 +4,17 @@ $nitro-card-tabs-height: 33px;
.nitro-card { .nitro-card {
pointer-events: all; pointer-events: all;
resize: both; resize: both;
&.theme-dark {
padding: 2px;
background-color: #1C323F;
border: 2px solid rgba(255, 255, 255, 0.5);
border-radius: 0.25rem;
}
&.theme-primary {
border: $border-width solid $border-color;
}
} }
.nitro-card-responsive { .nitro-card-responsive {
@ -15,10 +26,6 @@ $nitro-card-tabs-height: 33px;
pointer-events: none; pointer-events: none;
overflow: hidden; overflow: hidden;
.theme-primary {
border: $border-width solid $border-color;
}
@include media-breakpoint-down(lg) { @include media-breakpoint-down(lg) {
.draggable-window { .draggable-window {

View File

@ -3,6 +3,10 @@
padding-top: $container-padding-x; padding-top: $container-padding-x;
padding-bottom: $container-padding-x; padding-bottom: $container-padding-x;
overflow: auto; overflow: auto;
&.theme-dark {
background-color: #1C323F !important;
}
} }
@include media-breakpoint-down(lg) { @include media-breakpoint-down(lg) {

View File

@ -4,11 +4,11 @@ import { NitroCardContentViewProps } from './NitroCardContextView.types';
export const NitroCardContentView: FC<NitroCardContentViewProps> = props => export const NitroCardContentView: FC<NitroCardContentViewProps> = props =>
{ {
const { children = null, className = '', ...rest } = props; const { theme = 'primary', children = null, className = '', ...rest } = props;
const { simple = false } = useNitroCardContext(); const { simple = false } = useNitroCardContext();
return ( return (
<div className={ `container-fluid bg-light content-area d-flex flex-column overflow-auto ${ (simple ? 'simple' : '') } ${ className || '' }` } { ...rest }> <div className={ `container-fluid ${ theme === 'primary' ? 'bg-light' : ''} content-area d-flex flex-column overflow-auto theme-${theme} ${ (simple ? 'simple' : '') } ${ className || '' }` } { ...rest }>
{ children } { children }
</div> </div>
); );

View File

@ -2,4 +2,6 @@ import { DetailsHTMLAttributes } from 'react';
export interface NitroCardContentViewProps extends DetailsHTMLAttributes<HTMLDivElement> export interface NitroCardContentViewProps extends DetailsHTMLAttributes<HTMLDivElement>
{} {
theme?: string;
}

View File

@ -15,6 +15,11 @@
} }
} }
&.theme-dark {
background-color: #3d5f6e !important;
color: #fff;
}
.bg-tertiary-split { .bg-tertiary-split {
position: relative; position: relative;
border-bottom: 2px solid darken($quaternary, 5); border-bottom: 2px solid darken($quaternary, 5);

View File

@ -4,7 +4,7 @@ import { NitroCardHeaderViewProps } from './NitroCardHeaderView.types';
export const NitroCardHeaderView: FC<NitroCardHeaderViewProps> = props => export const NitroCardHeaderView: FC<NitroCardHeaderViewProps> = props =>
{ {
const { headerText = null, onCloseClick = null } = props; const { headerText = null, onCloseClick = null, theme= 'primary' } = props;
const { simple = false } = useNitroCardContext(); const { simple = false } = useNitroCardContext();
const onMouseDown = useCallback((event: MouseEvent<HTMLDivElement>) => const onMouseDown = useCallback((event: MouseEvent<HTMLDivElement>) =>
@ -31,7 +31,7 @@ export const NitroCardHeaderView: FC<NitroCardHeaderViewProps> = props =>
return ( return (
<div className="drag-handler container-fluid bg-primary"> <div className="drag-handler container-fluid bg-primary">
<div className="row nitro-card-header overflow-hidden"> <div className={`row nitro-card-header overflow-hidden theme-${theme}`}>
<div className="d-flex justify-content-center align-items-center w-100 position-relative"> <div className="d-flex justify-content-center align-items-center w-100 position-relative">
<div className="h4 text-white text-shadow header-text">{ headerText }</div> <div className="h4 text-white text-shadow header-text">{ headerText }</div>
<div className="position-absolute header-close" onMouseDownCapture={ onMouseDown } onClick={ onCloseClick }> <div className="position-absolute header-close" onMouseDownCapture={ onMouseDown } onClick={ onCloseClick }>

View File

@ -3,5 +3,6 @@ import { MouseEvent } from 'react';
export interface NitroCardHeaderViewProps export interface NitroCardHeaderViewProps
{ {
headerText: string; headerText: string;
theme?: string;
onCloseClick: (event: MouseEvent) => void; onCloseClick: (event: MouseEvent) => void;
} }

View File

@ -20,3 +20,5 @@
@import './achievements/AchievementsView'; @import './achievements/AchievementsView';
@import './user-settings/UserSettingsView'; @import './user-settings/UserSettingsView';
@import './user-profile/UserProfileVew'; @import './user-profile/UserProfileVew';
@import './chat-history/ChatHistoryView';
@import './help/HelpView';

View File

@ -0,0 +1,107 @@
import { RoomInfoEvent, 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 onRoomInfoEvent = useCallback((event: RoomInfoEvent) =>
{
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(RoomInfoEvent, onRoomInfoEvent);
return null;
}

View File

@ -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;
}
}
}
}
}
}

View File

@ -0,0 +1,171 @@
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<List>(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;
case ChatHistoryEvent.CHAT_HISTORY_CHANGED:
break;
}
}, [isVisible]);
useUiEvent(ChatHistoryEvent.HIDE_CHAT_HISTORY, onChatHistoryEvent);
useUiEvent(ChatHistoryEvent.SHOW_CHAT_HISTORY, onChatHistoryEvent);
useUiEvent(ChatHistoryEvent.TOGGLE_CHAT_HISTORY, onChatHistoryEvent);
useUiEvent(ChatHistoryEvent.CHAT_HISTORY_CHANGED, 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 (
<CellMeasurer
cache={cache}
columnIndex={0}
key={props.key}
parent={props.parent}
rowIndex={props.index}
>
<div key={props.key} style={props.style} className="row chathistory-entry justify-content-start">
{(item.type === ChatEntryType.TYPE_CHAT) &&
<div className={`col d-flex flex-wrap text-break text-wrap ${isDark ? 'dark' : 'light'}`}>
<div className="p-1">{item.timestamp}</div>
<div className="p-1 fw-bold cursor-pointer" dangerouslySetInnerHTML={ { __html: (item.name + ':') }} />
<div className="p-1 text-break text-wrap">{item.message}</div>
</div>
}
{(item.type === ChatEntryType.TYPE_ROOM_INFO) &&
<div className={`col d-flex flex-wrap text-break text-wrap ${isDark ? 'dark' : 'light'}`}>
<div className="p-1">{item.timestamp}</div>
<div className="p-1 fw-bold cursor-pointer">{item.name}</div>
</div>
}
</div>
</CellMeasurer>
);
};
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 (
<ChatHistoryContextProvider value={ { chatHistoryState, roomHistoryState } }>
<ChatHistoryMessageHandler />
{isVisible &&
<NitroCardView uniqueKey="chat-history" className="nitro-chat-history" simple={ false } theme={'dark'} >
<NitroCardHeaderView headerText={ 'Chat History' } onCloseClick={ event => setIsVisible(false) } theme={'dark'}/>
<NitroCardContentView className="chat-history-content" theme={'dark'}>
<div className="row w-100 h-100 chat-history-container">
<AutoSizer defaultWidth={300} defaultHeight={200} onResize={onResize}>
{({ height, width }) =>
{
return (
<List
ref={elementRef}
width={width}
height={height}
rowCount={chatHistoryState.chats.length}
rowHeight={cache.rowHeight}
className={'chat-history-list'}
rowRenderer={RowRenderer}
onRowsRendered={onRowsRendered}
deferredMeasurementCache={cache}
/>
)
}
}
</AutoSizer>
</div>
</NitroCardContentView>
</NitroCardView>
}
</ChatHistoryContextProvider>
);
}

View File

@ -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();
}
}

View File

@ -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;

View File

@ -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();
}
}

View File

@ -0,0 +1,14 @@
import { createContext, FC, useContext } from 'react';
import { ChatHistoryContextProps, IChatHistoryContext } from './ChatHistoryContext.types';
const ChatHistoryContext = createContext<IChatHistoryContext>({
chatHistoryState: null,
roomHistoryState: null
});
export const ChatHistoryContextProvider: FC<ChatHistoryContextProps> = props =>
{
return <ChatHistoryContext.Provider value={ props.value }>{ props.children }</ChatHistoryContext.Provider>
}
export const useChatHistoryContext = () => useContext(ChatHistoryContext);

View File

@ -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<IChatHistoryContext>
{
}

View File

@ -0,0 +1,5 @@
export const currentDate = () =>
{
const currentTime = new Date();
return `${currentTime.getHours().toString().padStart(2, '0')}:${currentTime.getMinutes().toString().padStart(2, '0')}`;
}

View File

@ -1,12 +1,10 @@
import { CallForHelpResultMessageEvent, FurnitureListItemParser, PetData } from '@nitrots/nitro-renderer'; import { CallForHelpResultMessageEvent } from '@nitrots/nitro-renderer';
import { FC, useCallback } from 'react'; import { FC, useCallback } from 'react';
import { LocalizeText } from '../../api'; import { LocalizeText } from '../../api';
import { CreateMessageHook } from '../../hooks/messages/message-event'; import { CreateMessageHook } from '../../hooks/messages/message-event';
import { NotificationAlertType } from '../notification-center/common/NotificationAlertType'; import { NotificationAlertType } from '../notification-center/common/NotificationAlertType';
import { NotificationUtilities } from '../notification-center/common/NotificationUtilities'; import { NotificationUtilities } from '../notification-center/common/NotificationUtilities';
import { GetCloseReasonKey } from './common/GetCloseReasonKey'; import { GetCloseReasonKey } from './common/GetCloseReasonKey';
let furniMsgFragments: Map<number, FurnitureListItemParser>[] = null;
let petMsgFragments: Map<number, PetData>[] = null;
export const HelpMessageHandler: FC<{}> = props => export const HelpMessageHandler: FC<{}> = props =>
{ {

View File

@ -0,0 +1,10 @@
.nitro-help {
height: 430px;
width: 300px;
.index-image {
background: url('../../assets/images/help/help_index.png');
width: 126px;
height: 105px;
}
}

View File

@ -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 { 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 => export const HelpView: FC<{}> = props =>
{ {
const [isVisible, setIsVisible] = useState(false);
const [ helpReportState, setHelpReportState ] = useState<IHelpReportState>(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 <HelpIndexView />
case 1: return <SelectReportedUserView />
case 2: return <SelectReportedChatsView />
case 3: return <SelectTopicView />
case 4: return <DescribeReportView />
}
return null;
}, [helpReportState.currentStep]);
return ( return (
<> <HelpContextProvider value={ { helpReportState, setHelpReportState } }>
<HelpMessageHandler /> <HelpMessageHandler />
</> {isVisible &&
<NitroCardView className="nitro-help">
<NitroCardHeaderView headerText={LocalizeText('help.button.cfh')} onCloseClick={() => setIsVisible(false)} />
<NitroCardContentView className="text-black">
<CurrentStepView />
</NitroCardContentView>
</NitroCardView>
}
</HelpContextProvider>
); );
} }

View File

@ -0,0 +1,5 @@
export interface IUser
{
id: number;
username: string;
}

View File

@ -0,0 +1,14 @@
import { createContext, FC, useContext } from 'react';
import { HelpContextProps, IHelpContext } from './HelpContext.types';
const HelpContext = createContext<IHelpContext>({
helpReportState: null,
setHelpReportState: null
});
export const HelpContextProvider: FC<HelpContextProps> = props =>
{
return <HelpContext.Provider value={ props.value }>{ props.children }</HelpContext.Provider>
}
export const useHelpContext = () => useContext(HelpContext);

View File

@ -0,0 +1,33 @@
import { ProviderProps } from 'react';
import { IChatEntry } from '../../chat-history/context/ChatHistoryContext.types';
export interface IHelpContext
{
helpReportState: IHelpReportState;
setHelpReportState: React.Dispatch<React.SetStateAction<IHelpReportState>>;
}
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<IHelpContext>
{
}

View File

@ -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 (
<>
<div className="d-grid col-12 mx-auto justify-content-center">
<div className="col-12"><h3 className="fw-bold">{LocalizeText('help.emergency.chat_report.subtitle')}</h3></div>
<div className="text-wrap">{LocalizeText('help.cfh.input.text')}</div>
</div>
<div className="form-group mb-2">
<textarea className="form-control" value={message} onChange={event => setMessage(event.target.value)} />
</div>
<button className="btn btn-danger mt-2" type="button" disabled={message.length < 15} onClick={submitReport}>{LocalizeText('help.bully.submit')}</button>
</>
);
}

View File

@ -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 (
<>
<div className="d-grid col-12 mx-auto justify-content-center">
<div className="col-12"><h3>{LocalizeText('help.main.frame.title')}</h3></div>
<div className="index-image align-self-center"></div>
<p className="text-center">{LocalizeText('help.main.self.description')}</p>
</div>
<div className="d-grid gap-2 col-8 mx-auto">
<button className="btn btn-primary" type="button" onClick={onReportClick}>{LocalizeText('help.main.bully.subtitle')}</button>
<button className="btn btn-primary" type="button">{LocalizeText('help.main.help.title')}</button>
<button className="btn btn-primary" type="button">{LocalizeText('help.main.self.tips.title')}</button>
</div>
<div className="d-grid gap-2 col-8 mx-auto">
<button className="btn btn-link" type="button">{LocalizeText('help.main.my.sanction.status')}</button>
</div>
</>
)
}

View File

@ -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<Map<number, IChatEntry>>(new Map());
const userChats = useMemo(() =>
{
return GetChatHistory().chats.filter(chat => (chat.type === ChatEntryType.TYPE_CHAT) && (chat.entityId === helpReportState.reportedUserId) && (chat.entityType === RoomObjectType.USER))
}, [helpReportState.reportedUserId]);
const selectChat = useCallback((chatEntry: IChatEntry) =>
{
const chats = new Map(selectedChats);
if(chats.has(chatEntry.id))
{
chats.delete(chatEntry.id);
}
else
{
chats.set(chatEntry.id, chatEntry);
}
setSelectedChats(chats);
}, [selectedChats]);
const submitChats = useCallback(() =>
{
if(!selectedChats || selectedChats.size <= 0) return;
const reportState = Object.assign({}, helpReportState);
reportState.reportedChats = Array.from(selectedChats.values());
reportState.currentStep = 3;
setHelpReportState(reportState);
}, [helpReportState, selectedChats, setHelpReportState]);
const back = useCallback(() =>
{
const reportState = Object.assign({}, helpReportState);
reportState.currentStep = --reportState.currentStep;
setHelpReportState(reportState);
}, [helpReportState, setHelpReportState]);
return (
<>
<div className="d-grid col-12 mx-auto justify-content-center">
<div className="col-12"><h3 className="fw-bold">{LocalizeText('help.emergency.chat_report.subtitle')}</h3></div>
{ userChats.length > 0 &&
<div className="text-wrap">{LocalizeText('help.emergency.chat_report.description')}</div>
}
</div>
{
(userChats.length === 0) && <div>{LocalizeText('help.cfh.error.no_user_data')}</div>
}
{ userChats.length > 0 &&
<>
<NitroCardGridView columns={1}>
{userChats.map((chat, index) =>
{
return (
<NitroCardGridItemView key={chat.id} onClick={() => selectChat(chat)} itemActive={selectedChats.has(chat.id)}>
<span>{chat.message}</span>
</NitroCardGridItemView>
)
})}
</NitroCardGridView>
<div className="d-flex gap-2 justify-content-between mt-auto">
<button className="btn btn-secondary mt-2" type="button" onClick={back}>{LocalizeText('generic.back')}</button>
<button className="btn btn-primary mt-2" type="button" disabled={selectedChats.size <= 0} onClick={submitChats}>{LocalizeText('help.emergency.main.submit.button')}</button>
</div>
</>
}
</>
);
}

View File

@ -0,0 +1,88 @@
import { RoomObjectType } from '@nitrots/nitro-renderer';
import { FC, useCallback, useMemo, useState } from 'react';
import { GetSessionDataManager, LocalizeText } from '../../../api';
import { NitroCardGridItemView, NitroCardGridView } from '../../../layout';
import { GetChatHistory } from '../../chat-history/common/GetChatHistory';
import { ChatEntryType } from '../../chat-history/context/ChatHistoryContext.types';
import { IUser } from '../common/IUser';
import { useHelpContext } from '../context/HelpContext';
export const SelectReportedUserView: FC<{}> = props =>
{
const { helpReportState = null, setHelpReportState = null } = useHelpContext();
const [selectedUserId, setSelectedUserId] = useState(-1);
const availableUsers = useMemo(() =>
{
const users: Map<number, IUser> = new Map();
GetChatHistory().chats
.forEach(chat =>
{
if((chat.type === ChatEntryType.TYPE_CHAT) && (chat.entityType === RoomObjectType.USER) && (chat.entityId !== GetSessionDataManager().userId))
{
if(!users.has(chat.entityId))
{
users.set(chat.entityId, { id: chat.entityId, username: chat.name })
}
}
});
return Array.from(users.values());
}, []);
const submitUser = useCallback(() =>
{
if(selectedUserId <= 0) return;
const reportState = Object.assign({}, helpReportState);
reportState.reportedUserId = selectedUserId;
reportState.currentStep = 2;
setHelpReportState(reportState);
}, [helpReportState, selectedUserId, setHelpReportState]);
const selectUser = useCallback((userId: number) =>
{
if(selectedUserId === userId) setSelectedUserId(-1);
else setSelectedUserId(userId);
}, [selectedUserId]);
const back = useCallback(() =>
{
const reportState = Object.assign({}, helpReportState);
reportState.currentStep = --reportState.currentStep;
setHelpReportState(reportState);
}, [helpReportState, setHelpReportState]);
return (
<>
<div className="d-grid col-12 mx-auto justify-content-center">
<h3 className="fw-bold">{LocalizeText('help.emergency.main.step.two.title')}</h3>
<p>{(availableUsers.length > 0) ? LocalizeText('report.user.pick.user') : ''}</p>
</div>
{
(availableUsers.length <= 0) && <div>{LocalizeText('report.user.error.nolist')}</div>
}
{
(availableUsers.length > 0) &&
<>
<NitroCardGridView columns={1}>
{availableUsers.map((user, index) =>
{
return (
<NitroCardGridItemView key={user.id} onClick={() => selectUser(user.id)} itemActive={(selectedUserId === user.id)}>
<span dangerouslySetInnerHTML={{ __html: (user.username) }} />
</NitroCardGridItemView>
)
})}
</NitroCardGridView>
<div className="d-flex gap-2 justify-content-between mt-auto">
<button className="btn btn-secondary mt-2" type="button" onClick={back}>{LocalizeText('generic.back')}</button>
<button className="btn btn-primary mt-2" type="button" disabled={selectedUserId <= 0} onClick={submitUser}>{LocalizeText('help.emergency.main.submit.button')}</button>
</div>
</>
}
</>
)
}

View File

@ -0,0 +1,64 @@
import { FC, useCallback, useMemo, useState } from 'react';
import { LocalizeText } from '../../../api';
import { GetCfhCategories } from '../../mod-tools/common/GetCFHCategories';
import { useHelpContext } from '../context/HelpContext';
export const SelectTopicView: FC<{}> = props =>
{
const { helpReportState = null, setHelpReportState = null } = useHelpContext();
const [selectedCategory, setSelectedCategory] = useState(-1);
const [selectedTopic, setSelectedTopic] = useState(-1);
const cfhCategories = useMemo(() =>
{
return GetCfhCategories();
}, []);
const submitTopic = useCallback(() =>
{
if(selectedCategory < 0) return;
if(selectedTopic < 0) return;
const reportState = Object.assign({}, helpReportState);
reportState.cfhCategory = selectedCategory;
reportState.cfhTopic = cfhCategories[selectedCategory].topics[selectedTopic].id;
reportState.currentStep = 4;
setHelpReportState(reportState);
}, [cfhCategories, helpReportState, selectedCategory, selectedTopic, setHelpReportState]);
const back = useCallback(() =>
{
const reportState = Object.assign({}, helpReportState);
reportState.currentStep = --reportState.currentStep;
setHelpReportState(reportState);
}, [helpReportState, setHelpReportState]);
return (
<>
<div className="d-grid col-12 mx-auto justify-content-center">
<div className="col-12"><h3 className="fw-bold">{LocalizeText('help.emergency.chat_report.subtitle')}</h3></div>
<div className="text-wrap">{LocalizeText('help.cfh.pick.topic')}</div>
</div>
<div className="d-grid gap-2 col-8 mx-auto">
{(selectedCategory < 0) &&
cfhCategories.map((category, index) =>
{
return <button key={index} className="btn btn-danger" type="button" onClick={() => setSelectedCategory(index)}>{LocalizeText(`help.cfh.reason.${category.name}`)}</button>
})
}
{(selectedCategory >= 0) &&
cfhCategories[selectedCategory].topics.map((topic, index) =>
{
return <button key={index} className="btn btn-danger" type="button" onClick={() => setSelectedTopic(index)}>{LocalizeText('help.cfh.topic.' + topic.id)}</button>
})
}
</div>
<div className="d-flex gap-2 justify-content-between mt-auto">
<button className="btn btn-secondary mt-2" type="button" onClick={back}>{LocalizeText('generic.back')}</button>
<button className="btn btn-primary mt-2" type="button" disabled={selectedTopic < 0} onClick={submitTopic}>{LocalizeText('help.emergency.main.submit.button')}</button>
</div>
</>
);
}

View File

@ -7,6 +7,7 @@ import { AchievementsView } from '../achievements/AchievementsView';
import { AvatarEditorView } from '../avatar-editor/AvatarEditorView'; import { AvatarEditorView } from '../avatar-editor/AvatarEditorView';
import { CameraWidgetView } from '../camera/CameraWidgetView'; import { CameraWidgetView } from '../camera/CameraWidgetView';
import { CatalogView } from '../catalog/CatalogView'; import { CatalogView } from '../catalog/CatalogView';
import { ChatHistoryView } from '../chat-history/ChatHistoryView';
import { FriendsView } from '../friends/FriendsView'; import { FriendsView } from '../friends/FriendsView';
import { GroupsView } from '../groups/GroupsView'; import { GroupsView } from '../groups/GroupsView';
import { HelpView } from '../help/HelpView'; import { HelpView } from '../help/HelpView';
@ -58,6 +59,7 @@ export const MainView: FC<MainViewProps> = props =>
<ToolbarView isInRoom={ !landingViewVisible } /> <ToolbarView isInRoom={ !landingViewVisible } />
<ModToolsView /> <ModToolsView />
<RoomHostView /> <RoomHostView />
<ChatHistoryView />
<WiredView /> <WiredView />
<AvatarEditorView /> <AvatarEditorView />
<AchievementsView /> <AchievementsView />

View File

@ -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;
}

View File

@ -2,5 +2,8 @@
width: 200px; width: 200px;
} }
@import './views/chatlog/ModToolsChatlogView'; @import './views/room/room-tools/ModToolsRoomView';
@import './views/room/ModToolsRoomView'; @import './views/chatlog/ChatlogView';
@import './views/user/user-info/ModToolsUserView';
@import './views/user/user-room-visits/ModToolsUserRoomVisitsView';
@import './views/tickets/ModToolsTicketView';

View File

@ -1,26 +1,30 @@
import { RoomEngineEvent } from '@nitrots/nitro-renderer'; import { RoomEngineObjectEvent, RoomObjectCategory } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useReducer, useState } from 'react'; import { FC, useCallback, useReducer, useState } from 'react';
import { GetRoomSession } from '../../api';
import { ModToolsEvent } from '../../events/mod-tools/ModToolsEvent'; import { ModToolsEvent } from '../../events/mod-tools/ModToolsEvent';
import { ModToolsOpenRoomChatlogEvent } from '../../events/mod-tools/ModToolsOpenRoomChatlogEvent';
import { ModToolsOpenRoomInfoEvent } from '../../events/mod-tools/ModToolsOpenRoomInfoEvent'; 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 { useRoomEngineEvent } from '../../hooks/events';
import { dispatchUiEvent, useUiEvent } from '../../hooks/events/ui/ui-event'; import { dispatchUiEvent, useUiEvent } from '../../hooks/events/ui/ui-event';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../layout'; import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../layout';
import { ModToolsContextProvider } from './context/ModToolsContext'; import { ModToolsContextProvider } from './context/ModToolsContext';
import { ModToolsMessageHandler } from './ModToolsMessageHandler';
import { ModToolsViewProps } from './ModToolsView.types'; import { ModToolsViewProps } from './ModToolsView.types';
import { initialModTools, ModToolsActions, ModToolsReducer } from './reducers/ModToolsReducer'; 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 { 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<ModToolsViewProps> = props => export const ModToolsView: FC<ModToolsViewProps> = props =>
{ {
const [ isVisible, setIsVisible ] = useState(false); const [ isVisible, setIsVisible ] = useState(false);
const [ modToolsState, dispatchModToolsState ] = useReducer(ModToolsReducer, initialModTools); const [ modToolsState, dispatchModToolsState ] = useReducer(ModToolsReducer, initialModTools);
const { currentRoomId = null, selectedUser = null, openRooms = null, openChatlogs = null } = modToolsState; const { currentRoomId = null, openRooms = null, openRoomChatlogs = null, openUserChatlogs = null, openUserInfo = null } = modToolsState;
const [ selectedUser, setSelectedUser] = useState<ISelectedUser>(null);
const [ isRoomVisible, setIsRoomVisible ] = useState(false);
const [ isUserVisible, setIsUserVisible ] = useState(false);
const [ isTicketsVisible, setIsTicketsVisible ] = useState(false); const [ isTicketsVisible, setIsTicketsVisible ] = useState(false);
const onModToolsEvent = useCallback((event: ModToolsEvent) => const onModToolsEvent = useCallback((event: ModToolsEvent) =>
@ -36,67 +40,29 @@ export const ModToolsView: FC<ModToolsViewProps> = props =>
case ModToolsEvent.TOGGLE_MOD_TOOLS: case ModToolsEvent.TOGGLE_MOD_TOOLS:
setIsVisible(value => !value); setIsVisible(value => !value);
return; 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.SHOW_MOD_TOOLS, onModToolsEvent);
useUiEvent(ModToolsEvent.HIDE_MOD_TOOLS, onModToolsEvent); useUiEvent(ModToolsEvent.HIDE_MOD_TOOLS, onModToolsEvent);
useUiEvent(ModToolsEvent.TOGGLE_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) if(event.category !== RoomObjectCategory.UNIT) return;
{
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); const roomSession = GetRoomSession();
useRoomEngineEvent(RoomEngineEvent.DISPOSED, onRoomEngineEvent);
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) => const handleClick = useCallback((action: string, value?: string) =>
{ {
@ -111,9 +77,7 @@ export const ModToolsView: FC<ModToolsViewProps> = props =>
return; return;
} }
const itemIndex = openRooms.indexOf(currentRoomId); if(openRooms.indexOf(currentRoomId) > -1)
if(itemIndex > -1)
{ {
handleClick('close_room', currentRoomId.toString()); handleClick('close_room', currentRoomId.toString());
} }
@ -137,46 +101,124 @@ export const ModToolsView: FC<ModToolsViewProps> = props =>
}); });
return; return;
} }
case 'close_chatlog': { case 'toggle_room_chatlog': {
const itemIndex = openChatlogs.indexOf(Number(value)); 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); clone.splice(itemIndex, 1);
dispatchModToolsState({ dispatchModToolsState({
type: ModToolsActions.SET_OPEN_CHATLOGS, type: ModToolsActions.SET_OPEN_ROOM_CHATLOGS,
payload: { 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; return;
} }
} }
}, [ dispatchModToolsState, openRooms, openChatlogs, currentRoomId ]); }, [openRooms, currentRoomId, openRoomChatlogs, selectedUser, openUserInfo, openUserChatlogs]);
useEffect(() =>
{
if(!isVisible) return;
}, [ isVisible ]);
return ( return (
<ModToolsContextProvider value={ { modToolsState, dispatchModToolsState } }> <ModToolsContextProvider value={ { modToolsState, dispatchModToolsState } }>
<ModToolsMessageHandler />
{ isVisible && { isVisible &&
<NitroCardView uniqueKey="mod-tools" className="nitro-mod-tools" simple={ false }> <NitroCardView uniqueKey="mod-tools" className="nitro-mod-tools" simple={ false }>
<NitroCardHeaderView headerText={ 'Mod Tools' } onCloseClick={ event => setIsVisible(false) } /> <NitroCardHeaderView headerText={ 'Mod Tools' } onCloseClick={ event => setIsVisible(false) } />
<NitroCardContentView className="text-black"> <NitroCardContentView className="text-black">
<button className="btn btn-primary btn-sm w-100 mb-2" onClick={ () => handleClick('toggle_room') } disabled={ !currentRoomId }><i className="fas fa-home"></i> Room Tool</button> <button className="btn btn-primary btn-sm w-100 mb-2" onClick={ () => handleClick('toggle_room') } disabled={ !currentRoomId }><i className="fas fa-home"></i> Room Tool</button>
<button className="btn btn-primary btn-sm w-100 mb-2" onClick={ () => {} } disabled={ !currentRoomId }><i className="fas fa-comments"></i> Chatlog Tool</button> <button className="btn btn-primary btn-sm w-100 mb-2" onClick={ () => handleClick('toggle_room_chatlog') } disabled={ !currentRoomId }><i className="fas fa-comments"></i> Chatlog Tool</button>
<button className="btn btn-primary btn-sm w-100 mb-2" onClick={ () => setIsUserVisible(value => !value) } disabled={ !selectedUser }><i className="fas fa-user"></i> User: { selectedUser ? selectedUser.name : '' }</button> <button className="btn btn-primary btn-sm w-100 mb-2" onClick={ () => handleClick('toggle_user_info') } disabled={ !selectedUser }><i className="fas fa-user"></i> User: { selectedUser ? selectedUser.username : '' }</button>
<button className="btn btn-primary btn-sm w-100" onClick={ () => setIsTicketsVisible(value => !value) }><i className="fas fa-exclamation-circle"></i> Report Tool</button> <button className="btn btn-primary btn-sm w-100" onClick={ () => setIsTicketsVisible(value => !value) }><i className="fas fa-exclamation-circle"></i> Report Tool</button>
</NitroCardContentView> </NitroCardContentView>
</NitroCardView> } </NitroCardView> }
{ openRooms && openRooms.map(roomId => { openRooms && openRooms.map(roomId =>
{ {
return <ModToolsRoomView key={ roomId } roomId={ roomId } onCloseClick={ () => handleClick('close_room', roomId.toString()) } />; return <ModToolsRoomView key={ roomId } roomId={ roomId } onCloseClick={ () => handleClick('close_room', roomId.toString()) } />;
}) } })
}
{ openRoomChatlogs && openRoomChatlogs.map(roomId =>
{
return <ModToolsChatlogView key={ roomId } roomId={ roomId } onCloseClick={ () => handleClick('close_room_chatlog', roomId.toString()) } />;
})
}
{ openUserInfo && openUserInfo.map(userId =>
{
return <ModToolsUserView key={userId} userId={userId} onCloseClick={ () => handleClick('close_user_info', userId.toString())}/>
})
}
{ openUserChatlogs && openUserChatlogs.map(userId =>
{
return <ModToolsUserChatlogView key={userId} userId={userId} onCloseClick={ () => handleClick('close_user_chatlog', userId.toString())}/>
})
}
{ isUserVisible && <ModToolsUserView /> }
{ isTicketsVisible && <ModToolsTicketsView onCloseClick={ () => setIsTicketsVisible(false) } /> } { isTicketsVisible && <ModToolsTicketsView onCloseClick={ () => setIsTicketsVisible(false) } /> }
</ModToolsContextProvider> </ModToolsContextProvider>
); );

View File

@ -0,0 +1,7 @@
import { CallForHelpCategoryData } from '@nitrots/nitro-renderer';
let cfhCategories: CallForHelpCategoryData[] = [];
export const SetCfhCategories = (categories: CallForHelpCategoryData[]) => (cfhCategories = categories);
export const GetCfhCategories = () => cfhCategories;

View File

@ -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';
}
}

View File

@ -1,48 +1,65 @@
import { CallForHelpCategoryData, IssueMessageData, ModeratorInitData } from '@nitrots/nitro-renderer';
import { Reducer } from 'react'; import { Reducer } from 'react';
export interface IModToolsState export interface IModToolsState
{ {
selectedUser: {webID: number, name: string}; settings: ModeratorInitData;
currentRoomId: number; currentRoomId: number;
openRooms: number[]; openRooms: number[];
openChatlogs: number[]; openRoomChatlogs: number[];
openUserInfo: number[];
openUserChatlogs: number[];
tickets: IssueMessageData[]
cfhCategories: CallForHelpCategoryData[];
} }
export interface IModToolsAction export interface IModToolsAction
{ {
type: string; type: string;
payload: { payload: {
selectedUser?: {webID: number, name: string}; settings?: ModeratorInitData;
currentRoomId?: number; currentRoomId?: number;
openRooms?: number[]; openRooms?: number[];
openChatlogs?: number[]; openRoomChatlogs?: number[];
openUserInfo?: number[];
openUserChatlogs?: number[];
tickets?: IssueMessageData[];
cfhCategories?: CallForHelpCategoryData[];
} }
} }
export class ModToolsActions 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_CURRENT_ROOM_ID: string = 'MTA_SET_CURRENT_ROOM_ID';
public static SET_OPEN_ROOMS: string = 'MTA_SET_OPEN_ROOMS'; 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'; public static RESET_STATE: string = 'MTA_RESET_STATE';
} }
export const initialModTools: IModToolsState = { export const initialModTools: IModToolsState = {
selectedUser: null, settings: null,
currentRoomId: null, currentRoomId: null,
openRooms: null, openRooms: null,
openChatlogs: null openRoomChatlogs: null,
openUserChatlogs: null,
openUserInfo: null,
tickets: null,
cfhCategories: null
}; };
export const ModToolsReducer: Reducer<IModToolsState, IModToolsAction> = (state, action) => export const ModToolsReducer: Reducer<IModToolsState, IModToolsAction> = (state, action) =>
{ {
switch(action.type) switch(action.type)
{ {
case ModToolsActions.SET_SELECTED_USER: { case ModToolsActions.SET_INIT_DATA: {
const selectedUser = (action.payload.selectedUser || state.selectedUser || null); const settings = (action.payload.settings || state.settings || null);
return { ...state, selectedUser }; return { ...state, settings };
} }
case ModToolsActions.SET_CURRENT_ROOM_ID: { case ModToolsActions.SET_CURRENT_ROOM_ID: {
const currentRoomId = (action.payload.currentRoomId || state.currentRoomId || null); const currentRoomId = (action.payload.currentRoomId || state.currentRoomId || null);
@ -54,6 +71,31 @@ export const ModToolsReducer: Reducer<IModToolsState, IModToolsAction> = (state,
return { ...state, openRooms }; 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: { case ModToolsActions.RESET_STATE: {
return { ...initialModTools }; return { ...initialModTools };
} }

View File

@ -0,0 +1,4 @@
export interface ISelectedUser {
userId: number;
username: string;
}

View File

@ -0,0 +1,6 @@
export interface IUserInfo
{
nameKey: string;
nameKeyFallback: string;
value: string;
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}
}

View File

@ -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<ChatlogViewProps> = props =>
{
const { records = null } = props;
const simpleRowRenderer: ListRowRenderer = (props: ListRowProps) =>
{
const item = records[0].chatlog[props.index];
return (
<CellMeasurer
cache={cache}
columnIndex={0}
key={props.key}
parent={props.parent}
rowIndex={props.index}
>
<div key={props.key} style={props.style} className={'row chatlog-entry justify-content-start ' + (item.hasHighlighting ? 'highlighted' : '')}>
<div className="col-auto text-center">{item.timestamp}</div>
<div className="col-sm-2 justify-content-start username"><span className="fw-bold cursor-pointer" onClick={() => SendMessageHook(new UserProfileComposer(item.userId))}>{item.userName}</span></div>
<div className="col justify-content-start h-100"><span className="text-break text-wrap h-100">{item.message}</span></div>
</div>
</CellMeasurer>
);
};
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 (
<CellMeasurer
cache={cache}
columnIndex={0}
key={props.key}
parent={props.parent}
rowIndex={props.index}
>
{isRoomInfo && <RoomInfo roomId={currentRecord.roomId} roomName={currentRecord.roomName} uniqueKey={props.key} style={props.style}/>}
{!isRoomInfo &&
<div key={props.key} style={props.style} className="row chatlog-entry justify-content-start">
<div className="col-auto text-center">{chatlogEntry.timestamp}</div>
<div className="col-sm-2 justify-content-start username"><span className="fw-bold cursor-pointer" onClick={() => SendMessageHook(new UserProfileComposer(chatlogEntry.userId))}>{chatlogEntry.userName}</span></div>
<div className="col justify-content-start h-100"><span className="text-break text-wrap h-100">{chatlogEntry.message}</span></div>
</div>
}
</CellMeasurer>
);
}
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 (
<div key={uniqueKey} style={style} className="row justify-content-start gap-2 room-info">
<div className="col-7"><span className="fw-bold">Room: </span>{roomName}</div>
<button className="btn btn-sm btn-primary col-sm-auto" onClick={() => TryVisitRoom(roomId)}>Visit Room</button>
<button className="btn btn-sm btn-primary col-sm-auto" onClick={() => dispatchUiEvent(new ModToolsOpenRoomInfoEvent(roomId))}>Room Tools</button>
</div>
);
}, []);
return (
<>
{
(records && records.length) &&
<>
{(records.length === 1) && <RoomInfo roomId={records[0].roomId} roomName={records[0].roomName} uniqueKey={records[0].roomId} style={{}} />}
<div className="chatlog-messages w-100 h-100 overflow-hidden">
<div className="row align-items-start w-100">
<div className="col-auto text-center fw-bold">Time</div>
<div className="col-sm-2 username-label fw-bold">User</div>
<div className="col fw-bold">Message</div>
</div>
<div className="row w-100 h-100 chatlog">
<AutoSizer defaultWidth={400} defaultHeight={200}>
{({ height, width }) =>
{
cache.clearAll();
return (
<List
width={width}
height={height}
rowCount={records.length > 1 ? getNumRowsForAdvanced() : records[0].chatlog.length}
rowHeight={cache.rowHeight}
className={'chatlog-container'}
rowRenderer={records.length > 1 ? advancedRowRenderer : simpleRowRenderer}
deferredMeasurementCache={cache} />
)
}
}
</AutoSizer>
</div>
</div>
</>
}
</>
);
}

View File

@ -0,0 +1,6 @@
import { ChatRecordData } from '@nitrots/nitro-renderer';
export interface ChatlogViewProps
{
records: ChatRecordData[];
}

View File

@ -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;
}
}
}
}

View File

@ -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<ModToolsChatlogViewProps> = props =>
{
const { roomId = null, onCloseClick = null } = props;
const [ roomName, setRoomName ] = useState(null);
const [ messages, setMessages ] = useState<ModtoolRoomChatlogLine[]>(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.data.roomName);
setMessages(parser.data.chatlog);
setLoadedRoomId(parser.data.roomId);
}, [ 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 (
<NitroCardView className="nitro-mod-tools-chatlog" simple={ true }>
<NitroCardHeaderView headerText={ 'Room Chatlog' + (roomName ? ': ' + roomName : '') } onCloseClick={ event => handleClick('close') } />
<NitroCardContentView className="text-black h-100">
<div className="w-100 d-flex justify-content-end">
<button className="btn btn-sm btn-primary me-2" onClick={ event => handleClick('visit_room') }>Visit Room</button>
<button className="btn btn-sm btn-primary">Room Tools</button>
</div>
<div className="chatlog-messages overflow-auto">
{ messages && <table className="table table-striped">
<thead>
<tr>
<th className="text-center">Time</th>
<th className="text-center">User</th>
<th>Message</th>
</tr>
</thead>
<tbody>
{ messages.map((message, index) =>
{
return <tr key={ index }>
<td className="text-center">{ message.timestamp }</td>
<td className="text-center"><a href="#" className="fw-bold">{ message.userName }</a></td>
<td className="word-break">{ message.message }</td>
</tr>;
}) }
</tbody>
</table> }
</div>
</NitroCardContentView>
</NitroCardView>
);
}

View File

@ -1,3 +0,0 @@
.nitro-mod-tools-room {
width: 240px;
}

View File

@ -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<ModToolsChatlogViewProps> = props =>
{
const { roomId = null, onCloseClick = null } = props;
const [roomChatlog, setRoomChatlog] = useState<ChatRecordData>(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 (
<NitroCardView className="nitro-mod-tools-room-chatlog" simple={true}>
<NitroCardHeaderView headerText={'Room Chatlog' + (roomChatlog ? ': ' + roomChatlog.roomName : '')} onCloseClick={() => onCloseClick()} />
<NitroCardContentView className="text-black h-100">
{roomChatlog &&
<ChatlogView records={[roomChatlog]} />
}
</NitroCardContentView>
</NitroCardView>
);
}

View File

@ -0,0 +1,8 @@
.nitro-mod-tools-room {
width: 240px;
.username {
color: #1E7295;
text-decoration: underline;
}
}

View File

@ -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 { FC, useCallback, useEffect, useState } from 'react';
import { CreateMessageHook, SendMessageHook } from '../../../../hooks/messages'; import { TryVisitRoom } from '../../../../../api';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout'; 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'; import { ModToolsRoomViewProps } from './ModToolsRoomView.types';
export const ModToolsRoomView: FC<ModToolsRoomViewProps> = props => export const ModToolsRoomView: FC<ModToolsRoomViewProps> = props =>
@ -17,27 +20,35 @@ export const ModToolsRoomView: FC<ModToolsRoomViewProps> = props =>
const [ ownerInRoom, setOwnerInRoom ] = useState(false); const [ ownerInRoom, setOwnerInRoom ] = useState(false);
const [ usersInRoom, setUsersInRoom ] = useState(0); 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(() => useEffect(() =>
{ {
if(infoRequested) return; if(infoRequested) return;
SendMessageHook(new ModtoolRequestRoomInfoComposer(roomId)); SendMessageHook(new GetModeratorRoomInfoMessageComposer(roomId));
setInfoRequested(true); setInfoRequested(true);
}, [ roomId, infoRequested, setInfoRequested ]); }, [ roomId, infoRequested, setInfoRequested ]);
const onModtoolRoomInfoEvent = useCallback((event: ModtoolRoomInfoEvent) => const onModtoolRoomInfoEvent = useCallback((event: ModeratorRoomInfoEvent) =>
{ {
const parser = event.getParser(); const parser = event.getParser();
setLoadedRoomId(parser.id); if(!parser || parser.data.flatId !== roomId) return;
setName(parser.name);
setOwnerId(parser.ownerId);
setOwnerName(parser.ownerName);
setOwnerInRoom(parser.ownerInRoom);
setUsersInRoom(parser.playerAmount);
}, [ setLoadedRoomId, setName, setOwnerId, setOwnerName, setOwnerInRoom, setUsersInRoom ]);
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) => const handleClick = useCallback((action: string, value?: string) =>
{ {
@ -45,27 +56,33 @@ export const ModToolsRoomView: FC<ModToolsRoomViewProps> = props =>
switch(action) switch(action)
{ {
case 'close': case 'alert_only':
onCloseClick(); 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; return;
} }
}, [ onCloseClick ]); }, [changeRoomName, kickUsers, lockRoom, message, roomId]);
return ( return (
<NitroCardView className="nitro-mod-tools-room" simple={ true }> <NitroCardView className="nitro-mod-tools-room" simple={ true }>
<NitroCardHeaderView headerText={ 'Room Info' + (name ? ': ' + name : '') } onCloseClick={ event => handleClick('close') } /> <NitroCardHeaderView headerText={ 'Room Info' + (name ? ': ' + name : '') } onCloseClick={ () => onCloseClick() } />
<NitroCardContentView className="text-black"> <NitroCardContentView className="text-black">
<div className="d-flex justify-content-between align-items-center mb-1"> <div className="d-flex justify-content-between align-items-center mb-1">
<div> <div>
<b>Room Owner:</b> <a href="#" className="fw-bold">{ ownerName }</a> <b>Room Owner:</b> <span className="username fw-bold cursor-pointer">{ ownerName }</span>
</div> </div>
<button className="btn btn-sm btn-primary">Visit Room</button> <button className="btn btn-sm btn-primary" onClick={() => TryVisitRoom(roomId)}>Visit Room</button>
</div> </div>
<div className="d-flex justify-content-between align-items-center mb-1"> <div className="d-flex justify-content-between align-items-center mb-1">
<div> <div>
<b>Users in room:</b> { usersInRoom } <b>Users in room:</b> { usersInRoom }
</div> </div>
<button className="btn btn-sm btn-primary">Chatlog</button> <button className="btn btn-sm btn-primary" onClick={() => dispatchUiEvent(new ModToolsOpenRoomChatlogEvent(roomId))}>Chatlog</button>
</div> </div>
<div className="d-flex justify-content-between align-items-center mb-2"> <div className="d-flex justify-content-between align-items-center mb-2">
<div> <div>
@ -75,28 +92,28 @@ export const ModToolsRoomView: FC<ModToolsRoomViewProps> = props =>
</div> </div>
<div className="bg-muted rounded py-1 px-2 mb-2"> <div className="bg-muted rounded py-1 px-2 mb-2">
<div className="form-check"> <div className="form-check">
<input className="form-check-input" type="checkbox" id="kickUsers" /> <input className="form-check-input" type="checkbox" id="kickUsers" checked={ kickUsers } onChange={e => setKickUsers(e.target.checked)}/>
<label className="form-check-label" htmlFor="kickUsers"> <label className="form-check-label" htmlFor="kickUsers">
Kick users out of the room Kick users out of the room
</label> </label>
</div> </div>
<div className="form-check"> <div className="form-check">
<input className="form-check-input" type="checkbox" id="lockRoom" /> <input className="form-check-input" type="checkbox" id="lockRoom" checked={ lockRoom } onChange={e => setLockRoom(e.target.checked)}/>
<label className="form-check-label" htmlFor="lockRoom"> <label className="form-check-label" htmlFor="lockRoom">
Change room lock to doorbell Change room lock to doorbell
</label> </label>
</div> </div>
<div className="form-check"> <div className="form-check">
<input className="form-check-input" type="checkbox" id="lockRoom" /> <input className="form-check-input" type="checkbox" id="roomName" checked={ changeRoomName } onChange={e => setChangeRoomName(e.target.checked)}/>
<label className="form-check-label" htmlFor="lockRoom"> <label className="form-check-label" htmlFor="roomName">
Change room name to "Inappro- priate to Hotel Management" Change room name to "Inappro- priate to Hotel Management"
</label> </label>
</div> </div>
</div> </div>
<textarea className="form-control mb-2" placeholder="Type a mandatory message to the users in this text box..."></textarea> <textarea className="form-control mb-2" placeholder="Type a mandatory message to the users in this text box..." value={message} onChange={e => setMessage(e.target.value)}></textarea>
<div className="d-flex justify-content-between"> <div className="d-flex justify-content-between">
<button className="btn btn-danger w-100 me-2">Send Caution</button> <button className="btn btn-danger w-100 me-2" onClick={() => handleClick('send_message')}>Send Caution</button>
<button className="btn btn-success w-100">Send Alert only</button> <button className="btn btn-success w-100" onClick={() => handleClick('alert_only')}>Send Alert only</button>
</div> </div>
</NitroCardContentView> </NitroCardContentView>
</NitroCardView> </NitroCardView>

View File

@ -0,0 +1,11 @@
.nitro-mod-tools-tickets
{
width: 400px;
height: 200px;
}
.nitro-mod-tools-handle-issue
{
width: 400px;
height: 300px;
}

View File

@ -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 { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../../../layout';
import { useModToolsContext } from '../../context/ModToolsContext';
import { IssueInfoView } from './issue-info/IssueInfoView';
import { ModToolsTicketsViewProps } from './ModToolsTicketsView.types'; 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[] = [ const TABS: string[] = [
'Open Issues', 'Open Issues',
@ -11,10 +18,70 @@ const TABS: string[] = [
export const ModToolsTicketsView: FC<ModToolsTicketsViewProps> = props => export const ModToolsTicketsView: FC<ModToolsTicketsViewProps> = props =>
{ {
const { onCloseClick = null } = props; const { onCloseClick = null } = props;
const { modToolsState = null } = useModToolsContext();
const { tickets= null } = modToolsState;
const [ currentTab, setCurrentTab ] = useState<number>(0); const [ currentTab, setCurrentTab ] = useState<number>(0);
const [ issueInfoWindows, setIssueInfoWindows ] = useState<number[]>([]);
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 <ModToolsOpenIssuesTabView openIssues={openIssues}/>;
case 1: return <ModToolsMyIssuesTabView myIssues={myIssues} onIssueHandleClick={onIssueHandleClicked}/>;
case 2: return <ModToolsPickedIssuesTabView pickedIssues={pickedIssues}/>;
default: return null;
}
}, [currentTab, myIssues, onIssueHandleClicked, openIssues, pickedIssues]);
return ( return (
<>
<NitroCardView className="nitro-mod-tools-tickets" simple={ false }> <NitroCardView className="nitro-mod-tools-tickets" simple={ false }>
<NitroCardHeaderView headerText={ 'Tickets' } onCloseClick={ onCloseClick } /> <NitroCardHeaderView headerText={ 'Tickets' } onCloseClick={ onCloseClick } />
<NitroCardContentView className="p-0 text-black"> <NitroCardContentView className="p-0 text-black">
@ -26,8 +93,14 @@ export const ModToolsTicketsView: FC<ModToolsTicketsViewProps> = props =>
</NitroCardTabsItemView>); </NitroCardTabsItemView>);
}) } }) }
</NitroCardTabsView> </NitroCardTabsView>
<div className="p-2"></div> <div className="p-2">
<CurrentTabComponent />
</div>
</NitroCardContentView> </NitroCardContentView>
</NitroCardView> </NitroCardView>
{
issueInfoWindows && issueInfoWindows.map(issueId => <IssueInfoView key={issueId} issueId={issueId} onIssueInfoClosed={onIssueInfoClosed}/>)
}
</>
); );
} }

View File

@ -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<CfhChatlogViewProps> = props =>
{
const { onCloseClick = null, issueId = null } = props;
const [ chatlogData, setChatlogData ] = useState<CfhChatlogData>(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 (
<NitroCardView className="nitro-mod-tools-cfh-chatlog" simple={true}>
<NitroCardHeaderView headerText={'Issue Chatlog'} onCloseClick={onCloseClick} />
<NitroCardContentView className="text-black">
{ chatlogData && <ChatlogView records={[chatlogData.chatRecord]} />}
</NitroCardContentView>
</NitroCardView>
);
}

View File

@ -0,0 +1,5 @@
export interface CfhChatlogViewProps
{
issueId: number;
onCloseClick(): void;
}

View File

@ -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<IssueInfoViewProps> = 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 (
<>
<NitroCardView className="nitro-mod-tools-handle-issue" simple={true}>
<NitroCardHeaderView headerText={'Resolving issue ' + issueId} onCloseClick={() => onIssueInfoClosed(issueId)} />
<NitroCardContentView className="text-black">
<div className="row">
<div className="col-8">
<h3>Issue Information</h3>
<div><span className="fw-bold">Source: </span>{getSourceName(ticket.categoryId)}</div>
<div><span className="fw-bold">Category: </span>{LocalizeText('help.cfh.topic.' + ticket.reportedCategoryId)}</div>
<div><span className="fw-bold">Description: </span>{ticket.message}</div>
<div><span className="fw-bold">Caller: </span><button className="btn btn-link fw-bold" onClick={() => openUserInfo(ticket.reporterUserId)}>{ticket.reporterUserName}</button></div>
<div><span className="fw-bold">Reported User: </span><button className="btn btn-link fw-bold" onClick={() => openUserInfo(ticket.reportedUserId)}>{ticket.reportedUserName}</button></div>
</div>
<div className="col-4">
<div className="d-grid gap-2 mb-4">
<button className="btn btn-secondary" onClick={() => setcfhChatlogOpen(!cfhChatlogOpen)}>Chatlog</button>
</div>
<div className="d-grid gap-2">
<button className="btn btn-primary" onClick={() => closeIssue(CloseIssuesMessageComposer.RESOLUTION_USELESS)}>Close as useless</button>
<button className="btn btn-danger" onClick={() => closeIssue(CloseIssuesMessageComposer.RESOLUTION_ABUSIVE)}>Close as abusive</button>
<button className="btn btn-success" onClick={() => closeIssue(CloseIssuesMessageComposer.RESOLUTION_RESOLVED)}>Close as resolved</button>
<button className="btn btn-secondary" onClick={() => onReleaseIssue(issueId)}>Release</button>
</div>
</div>
</div>
</NitroCardContentView>
</NitroCardView>
{ cfhChatlogOpen && <CfhChatlogView issueId={issueId} onCloseClick={() => setcfhChatlogOpen(false) }/>}
</>
);
}

View File

@ -0,0 +1,5 @@
export interface IssueInfoViewProps
{
issueId: number;
onIssueInfoClosed(issueId: number): void;
}

View File

@ -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<ModToolsMyIssuesTabViewProps> = props =>
{
const { myIssues = null, onIssueHandleClick = null } = props;
const onReleaseIssue = useCallback((issueId: number) =>
{
SendMessageHook(new ReleaseIssuesMessageComposer([issueId]));
}, []);
return (
<>
<table className="table text-black table-striped">
<thead>
<tr>
<th scope="col">Type</th>
<th scope="col">Room/Player</th>
<th scope="col">Opened</th>
<th scope="col"></th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{myIssues.map(issue =>
{
return (
<tr className="text-black" key={issue.issueId}>
<td>{issue.categoryId}</td>
<td>{issue.reportedUserName}</td>
<td>{new Date(Date.now() - issue.issueAgeInMilliseconds).toLocaleTimeString()}</td>
<td><button className="btn btn-sm btn-primary" onClick={() => onIssueHandleClick(issue.issueId)}>Handle</button></td>
<td><button className="btn btn-sm btn-danger" onClick={() => onReleaseIssue(issue.issueId)}>Release</button></td>
</tr>)
})
}
</tbody>
</table>
</>
);
}

View File

@ -0,0 +1,7 @@
import { IssueMessageData } from '@nitrots/nitro-renderer';
export interface ModToolsMyIssuesTabViewProps
{
myIssues: IssueMessageData[];
onIssueHandleClick(issueId: number): void;
}

View File

@ -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<ModToolsOpenIssuesTabViewProps> = props =>
{
const { openIssues = null } = props;
const onPickIssue = useCallback((issueId: number) =>
{
SendMessageHook(new PickIssuesMessageComposer([issueId], false, 0, 'pick issue button'));
}, []);
return (
<>
<table className="table text-black table-striped">
<thead>
<tr>
<th scope="col">Type</th>
<th scope="col">Room/Player</th>
<th scope="col">Opened</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{openIssues.map(issue =>
{
return (
<tr className="text-black" key={issue.issueId}>
<td>{issue.categoryId}</td>
<td>{issue.reportedUserName}</td>
<td>{new Date(Date.now() - issue.issueAgeInMilliseconds).toLocaleTimeString()}</td>
<td><button className="btn btn-sm btn-success" onClick={() => onPickIssue(issue.issueId)}>Pick Issue</button></td>
</tr>)
})
}
</tbody>
</table>
</>
);
}

View File

@ -0,0 +1,6 @@
import { IssueMessageData } from '@nitrots/nitro-renderer';
export interface ModToolsOpenIssuesTabViewProps
{
openIssues: IssueMessageData[];
}

View File

@ -0,0 +1,35 @@
import { FC } from 'react';
import { ModToolsPickedIssuesTabViewProps } from './ModToolsPickedIssuesTabView.types';
export const ModToolsPickedIssuesTabView: FC<ModToolsPickedIssuesTabViewProps> = props =>
{
const { pickedIssues = null } = props;
return (
<>
<table className="table text-black table-striped">
<thead>
<tr>
<th scope="col">Type</th>
<th scope="col">Room/Player</th>
<th scope="col">Opened</th>
<th scope="col">Picker</th>
</tr>
</thead>
<tbody>
{pickedIssues.map(issue =>
{
return (
<tr className="text-black" key={issue.issueId}>
<td>{issue.categoryId}</td>
<td>{issue.reportedUserName}</td>
<td>{new Date(Date.now() - issue.issueAgeInMilliseconds).toLocaleTimeString()}</td>
<td>{issue.pickerUserName}</td>
</tr>)
})
}
</tbody>
</table>
</>
);
}

View File

@ -0,0 +1,6 @@
import { IssueMessageData } from '@nitrots/nitro-renderer';
export interface ModToolsPickedIssuesTabViewProps
{
pickedIssues: IssueMessageData[];
}

View File

@ -1,15 +0,0 @@
import { FC } from 'react';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
import { ModToolsUserViewProps } from './ModToolsUserView.types';
export const ModToolsUserView: FC<ModToolsUserViewProps> = props =>
{
return (
<NitroCardView className="nitro-mod-tools-user" simple={ true }>
<NitroCardHeaderView headerText={ 'User Info' } onCloseClick={ event => {} } />
<NitroCardContentView className="text-black">
</NitroCardContentView>
</NitroCardView>
);
}

View File

@ -1,2 +0,0 @@
export interface ModToolsUserViewProps
{}

View File

@ -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<ModToolsUserChatlogViewProps> = props =>
{
const { userId = null, onCloseClick = null } = props;
const [userChatlog, setUserChatlog] = useState<ChatRecordData[]>(null);
const [username, setUsername] = useState<string>(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 (
<NitroCardView className="nitro-mod-tools-user-chatlog" simple={true}>
<NitroCardHeaderView headerText={'User Chatlog' + (username ? ': ' + username : '')} onCloseClick={() => onCloseClick()} />
<NitroCardContentView className="text-black h-100">
{userChatlog &&
<ChatlogView records={userChatlog} />
}
</NitroCardContentView>
</NitroCardView>
);
}

View File

@ -0,0 +1,5 @@
export interface ModToolsUserChatlogViewProps
{
userId: number;
onCloseClick: () => void;
}

View File

@ -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);
}
}
}

View File

@ -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<ModToolsUserViewProps> = props =>
{
const { onCloseClick = null, userId = null } = props;
const [ userInfo, setUserInfo ] = useState<ModeratorUserInfoData>(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 (
<>
<NitroCardView className="nitro-mod-tools-user" simple={true}>
<NitroCardHeaderView headerText={ LocalizeText('modtools.userinfo.title', [ 'username' ], [ userInfo.userName ]) } onCloseClick={ () => onCloseClick() } />
<NitroCardContentView className="text-black">
<NitroLayoutGrid>
<NitroLayoutGridColumn size={ 8 }>
<table className="table table-striped table-sm table-text-small text-black m-0">
<tbody>
{ userProperties.map( (property, index) =>
{
return (
<tr key={index}>
<th scope="row">{ LocalizeText(property.localeKey) }</th>
<td>
{ property.value }
{ property.showOnline && <i className={ `icon icon-pf-${ userInfo.online ? 'online' : 'offline' } ms-2` } /> }
</td>
</tr>
);
}) }
</tbody>
</table>
</NitroLayoutGridColumn>
<NitroLayoutGridColumn size={ 4 }>
<NitroLayoutButton variant="primary" size="sm" onClick={ event => dispatchUiEvent(new ModToolsOpenUserChatlogEvent(userId)) }>
Room Chat
</NitroLayoutButton>
<NitroLayoutButton variant="primary" size="sm" onClick={ event => setSendMessageVisible(!sendMessageVisible) }>
Send Message
</NitroLayoutButton>
<NitroLayoutButton variant="primary" size="sm" onClick={ event => setRoomVisitsVisible(!roomVisitsVisible) }>
Room Visits
</NitroLayoutButton>
<NitroLayoutButton variant="primary" size="sm" onClick={ event => setModActionVisible(!modActionVisible) }>
Mod Action
</NitroLayoutButton>
</NitroLayoutGridColumn>
</NitroLayoutGrid>
</NitroCardContentView>
</NitroCardView>
{ sendMessageVisible &&
<ModToolsSendUserMessageView user={ { userId: userId, username: userInfo.userName } } onCloseClick={ () => setSendMessageVisible(false) } /> }
{ modActionVisible &&
<ModToolsUserModActionView user={ { userId: userId, username: userInfo.userName } } onCloseClick={ () => setModActionVisible(false) } /> }
{ roomVisitsVisible &&
<ModToolsUserRoomVisitsView userId={ userId } onCloseClick={ () => setRoomVisitsVisible(false) } /> }
</>
);
}

View File

@ -0,0 +1,5 @@
export interface ModToolsUserViewProps
{
userId: number;
onCloseClick: () => void;
}

View File

@ -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<ModToolsUserModActionViewProps> = 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<string>('');
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 (
<NitroCardView className="nitro-mod-tools-user-action" simple={true}>
<NitroCardHeaderView headerText={'Mod Action: ' + (user ? user.username : '')} onCloseClick={ () => onCloseClick() } />
<NitroCardContentView className="text-black">
{ user &&
<>
<div className="form-group mb-2">
<select className="form-control form-control-sm" value={selectedTopic} onChange={event => setSelectedTopic(parseInt(event.target.value))}>
<option value={-1}>CFH Topic:</option>
{ topics.map( (topic,index) =>
{
return (<option key={index} value={index}>{LocalizeText('help.cfh.topic.' + topic.id)}</option>)
})}
</select>
</div>
<div className="form-group mb-2">
<select className="form-control form-control-sm" value={selectedAction} onChange={event => setSelectedAction(parseInt(event.target.value))}>
<option value={-1}>Sanction type:</option>
{ actions.map( (action, index) =>
{
return (<option key={index} value={index}>{ action.name }</option>)
})}
</select>
</div>
<div className="form-group mb-2">
<label>Optional message type, overrides default</label>
<textarea className="form-control" value={message} onChange={event => setMessage(event.target.value)}/>
</div>
<div className="form-group mb-2">
<div className="d-flex justify-content-between">
<button type="button" className="btn btn-danger w-100 me-2" onClick={ () => sendSanction()}>Sanction</button>
<button className="btn btn-success w-100" onClick={ () => sendDefaultSanction()}>Default Sanction</button>
</div>
</div>
</>
}
</NitroCardContentView>
</NitroCardView>
);
}

View File

@ -0,0 +1,7 @@
import { ISelectedUser } from '../../../utils/ISelectedUser';
export interface ModToolsUserModActionViewProps
{
user: ISelectedUser;
onCloseClick: () => void;
}

View File

@ -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);
}
}
}
}

View File

@ -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<ModToolsUserRoomVisitsViewProps> = props =>
{
const { userId = null, onCloseClick = null } = props;
const [roomVisitData, setRoomVisitData] = useState<RoomVisitsData>(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 (
<div style={props.style} key={props.key} className="row room-visit">
<div className="col-auto text-center">{item.enterHour.toString().padStart(2, '0')}:{item.enterMinute.toString().padStart(2, '0')}</div>
<div className="col-7"><span className="fw-bold">Room: </span>{item.roomName}</div>
<button className="btn btn-sm btn-link col-sm-auto fw-bold" onClick={() => TryVisitRoom(item.roomId)}>Visit Room</button>
</div>);
}
return (
<NitroCardView className="nitro-mod-tools-user-visits" simple={true}>
<NitroCardHeaderView headerText={'User Visits'} onCloseClick={ () => onCloseClick() } />
<NitroCardContentView className="text-black">
{roomVisitData &&
<div className="row h-100 w-100 user-visits">
<AutoSizer defaultWidth={400} defaultHeight={200}>
{({ height, width }) =>
{
return (
<List
width={width}
height={height}
rowCount={roomVisitData.rooms.length}
rowHeight={30}
className={'roomvisits-container'}
rowRenderer={RowRenderer}
/>
)
}}
</AutoSizer>
</div>
}
</NitroCardContentView>
</NitroCardView>
);
}

View File

@ -0,0 +1,6 @@
export interface ModToolsUserRoomVisitsViewProps
{
userId: number;
onCloseClick: () => void;
}

View File

@ -0,0 +1,7 @@
import { ISelectedUser } from '../../../utils/ISelectedUser';
export interface ModToolsSendUserMessageViewProps
{
user: ISelectedUser;
onCloseClick: () => void;
}

View File

@ -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<ModToolsSendUserMessageViewProps> = 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 (
<NitroCardView className="nitro-mod-tools-user-message" simple={true}>
<NitroCardHeaderView headerText={'Send Message'} onCloseClick={ () => onCloseClick() } />
<NitroCardContentView className="text-black">
{user && <>
<div>Message To: {user.username}</div>
<div className="form-group mb-2">
<textarea className="form-control" value={message} onChange={e => setMessage(e.target.value)}></textarea>
</div>
<div className="form-group mb-2">
<button type="button" className="btn btn-primary" onClick={ () => sendMessage()}>Send message</button>
</div>
</>}
</NitroCardContentView>
</NitroCardView>
);
}

View File

@ -60,7 +60,7 @@ export const NotificationCenterMessageHandler: FC<INotificationCenterMessageHand
{ {
const parser = event.getParser(); const parser = event.getParser();
NotificationUtilities.handleModeratorMessage(parser.message, parser.link); NotificationUtilities.handleModeratorMessage(parser.message, parser.url);
}, []); }, []);
CreateMessageHook(ModeratorMessageEvent, onModeratorMessageEvent); CreateMessageHook(ModeratorMessageEvent, onModeratorMessageEvent);

View File

@ -1,6 +1,7 @@
import { FriendlyTime, HabboClubLevelEnum, UserCurrencyComposer, UserSubscriptionComposer } from '@nitrots/nitro-renderer'; import { FriendlyTime, HabboClubLevelEnum, UserCurrencyComposer, UserSubscriptionComposer } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { GetConfiguration, LocalizeText } from '../../api'; import { GetConfiguration, LocalizeText } from '../../api';
import { HelpEvent } from '../../events/help/HelpEvent';
import { UserSettingsUIEvent } from '../../events/user-settings/UserSettingsUIEvent'; import { UserSettingsUIEvent } from '../../events/user-settings/UserSettingsUIEvent';
import { dispatchUiEvent } from '../../hooks'; import { dispatchUiEvent } from '../../hooks';
import { SendMessageHook } from '../../hooks/messages/message-event'; import { SendMessageHook } from '../../hooks/messages/message-event';
@ -24,6 +25,11 @@ export const PurseView: FC<{}> = props =>
dispatchUiEvent(new UserSettingsUIEvent(UserSettingsUIEvent.TOGGLE_USER_SETTINGS)); dispatchUiEvent(new UserSettingsUIEvent(UserSettingsUIEvent.TOGGLE_USER_SETTINGS));
}, []); }, []);
const handleHelpCenterClick = useCallback(() =>
{
dispatchUiEvent(new HelpEvent(HelpEvent.TOGGLE_HELP_CENTER));
}, []);
const displayedCurrencies = useMemo(() => const displayedCurrencies = useMemo(() =>
{ {
return GetConfiguration<number[]>('system.currency.types', []); return GetConfiguration<number[]>('system.currency.types', []);
@ -140,7 +146,7 @@ export const PurseView: FC<{}> = props =>
</div> </div>
<div className="col-2 px-0"> <div className="col-2 px-0">
<div className="d-flex flex-column nitro-purse-buttons h-100 justify-content-center"> <div className="d-flex flex-column nitro-purse-buttons h-100 justify-content-center">
<div className="nitro-purse-button text-white h-100 text-center d-flex align-items-center justify-content-center cursor-pointer"><i className="icon icon-help"/></div> <div className="nitro-purse-button text-white h-100 text-center d-flex align-items-center justify-content-center cursor-pointer" onClick={ handleHelpCenterClick }><i className="icon icon-help"/></div>
<div className="nitro-purse-button text-white h-100 text-center d-flex align-items-center justify-content-center cursor-pointer" onClick={ handleUserSettingsClick } ><i className="fas fa-cogs"/></div> <div className="nitro-purse-button text-white h-100 text-center d-flex align-items-center justify-content-center cursor-pointer" onClick={ handleUserSettingsClick } ><i className="fas fa-cogs"/></div>
</div> </div>
</div> </div>

View File

@ -10,6 +10,14 @@
padding-right: 10px; padding-right: 10px;
width: 100%; width: 100%;
@include media-breakpoint-down(sm) {
display: flex;
position: absolute;
bottom: 70px;
left: calc(100% / 3);
width: 200px;
}
&:before { &:before {
content: ""; content: "";
position: absolute; position: absolute;

View File

@ -1,6 +1,6 @@
import { NitroEvent, RoomEngineTriggerWidgetEvent, RoomObjectVariable } from '@nitrots/nitro-renderer'; import { NitroEvent, RoomEngineTriggerWidgetEvent, RoomObjectVariable } from '@nitrots/nitro-renderer';
import { FC, useCallback, useState } from 'react'; import { FC, useCallback, useState } from 'react';
import { GetRoomEngine, RoomWidgetRoomObjectUpdateEvent } from '../../../../../api'; import { GetRoomEngine, RoomWidgetUpdateRoomObjectEvent } from '../../../../../api';
import { CreateEventDispatcherHook } from '../../../../../hooks/events/event-dispatcher.base'; import { CreateEventDispatcherHook } from '../../../../../hooks/events/event-dispatcher.base';
import { useRoomEngineEvent } from '../../../../../hooks/events/nitro/room/room-engine-event'; import { useRoomEngineEvent } from '../../../../../hooks/events/nitro/room/room-engine-event';
import { NitroLayoutTrophyView } from '../../../../../layout'; import { NitroLayoutTrophyView } from '../../../../../layout';
@ -9,6 +9,7 @@ import { FurnitureTrophyData } from './FurnitureTrophyData';
export const FurnitureTrophyView: FC<{}> = props => export const FurnitureTrophyView: FC<{}> = props =>
{ {
const { eventDispatcher = null, widgetHandler = null } = useRoomContext(); const { eventDispatcher = null, widgetHandler = null } = useRoomContext();
const [ trophyData, setTrophyData ] = useState<FurnitureTrophyData>(null); const [ trophyData, setTrophyData ] = useState<FurnitureTrophyData>(null);
@ -39,8 +40,8 @@ export const FurnitureTrophyView: FC<{}> = props =>
setTrophyData(new FurnitureTrophyData(widgetEvent.objectId, widgetEvent.category, color, ownerName, trophyDate, trophyText)); setTrophyData(new FurnitureTrophyData(widgetEvent.objectId, widgetEvent.category, color, ownerName, trophyDate, trophyText));
return; return;
} }
case RoomWidgetRoomObjectUpdateEvent.FURNI_REMOVED: { case RoomWidgetUpdateRoomObjectEvent.FURNI_REMOVED: {
const widgetEvent = (event as RoomWidgetRoomObjectUpdateEvent); const widgetEvent = (event as RoomWidgetUpdateRoomObjectEvent);
setTrophyData(prevState => setTrophyData(prevState =>
{ {
@ -54,7 +55,7 @@ export const FurnitureTrophyView: FC<{}> = props =>
}, []); }, []);
useRoomEngineEvent(RoomEngineTriggerWidgetEvent.REQUEST_TROPHY, onNitroEvent); useRoomEngineEvent(RoomEngineTriggerWidgetEvent.REQUEST_TROPHY, onNitroEvent);
CreateEventDispatcherHook(RoomWidgetRoomObjectUpdateEvent.FURNI_REMOVED, widgetHandler.eventDispatcher, onNitroEvent); CreateEventDispatcherHook(RoomWidgetUpdateRoomObjectEvent.FURNI_REMOVED, widgetHandler.eventDispatcher, onNitroEvent);
const processAction = useCallback((type: string, value: string = null) => const processAction = useCallback((type: string, value: string = null) =>
{ {

View File

@ -3,6 +3,7 @@ import classNames from 'classnames';
import { FC, useCallback, useState } from 'react'; import { FC, useCallback, useState } from 'react';
import { LocalizeText, RoomWidgetZoomToggleMessage } from '../../../../api'; import { LocalizeText, RoomWidgetZoomToggleMessage } from '../../../../api';
import { NavigatorEvent } from '../../../../events'; import { NavigatorEvent } from '../../../../events';
import { ChatHistoryEvent } from '../../../../events/chat-history/ChatHistoryEvent';
import { dispatchUiEvent } from '../../../../hooks/events'; import { dispatchUiEvent } from '../../../../hooks/events';
import { SendMessageHook } from '../../../../hooks/messages'; import { SendMessageHook } from '../../../../hooks/messages';
import { useRoomContext } from '../../context/RoomContext'; import { useRoomContext } from '../../context/RoomContext';
@ -27,6 +28,8 @@ export const RoomToolsWidgetView: FC<{}> = props =>
setIsZoomedIn(value => !value); setIsZoomedIn(value => !value);
return; return;
case 'chat_history': case 'chat_history':
dispatchUiEvent(new ChatHistoryEvent(ChatHistoryEvent.TOGGLE_CHAT_HISTORY));
//setIsExpanded(false); close this ??
return; return;
case 'like_room': case 'like_room':
if(isLiked) return; if(isLiked) return;

View File

@ -14,6 +14,11 @@
#toolbar-chat-input-container { #toolbar-chat-input-container {
margin: 0 10px; margin: 0 10px;
@include media-breakpoint-down(sm) {
width: 0px;
height: 0px
}
} }
.navigation-items { .navigation-items {

View File

@ -29,6 +29,7 @@ export const ToolbarView: FC<ToolbarViewProps> = props =>
const [ unseenInventoryCount, setUnseenInventoryCount ] = useState(0); const [ unseenInventoryCount, setUnseenInventoryCount ] = useState(0);
const [ unseenAchievementCount, setUnseenAchievementCount ] = useState(0); const [ unseenAchievementCount, setUnseenAchievementCount ] = useState(0);
const isMod = GetSessionDataManager().isModerator;
const unseenFriendListCount = 0; const unseenFriendListCount = 0;
const onUserInfoEvent = useCallback((event: UserInfoEvent) => const onUserInfoEvent = useCallback((event: UserInfoEvent) =>
@ -200,9 +201,10 @@ export const ToolbarView: FC<ToolbarViewProps> = props =>
<div className="navigation-item" onClick={ event => handleToolbarItemClick(ToolbarViewItems.CAMERA_ITEM) }> <div className="navigation-item" onClick={ event => handleToolbarItemClick(ToolbarViewItems.CAMERA_ITEM) }>
<i className="icon icon-camera"></i> <i className="icon icon-camera"></i>
</div>) } </div>) }
<div className="navigation-item" onClick={ event => handleToolbarItemClick(ToolbarViewItems.MOD_TOOLS_ITEM) }> { isMod && (
<div className="navigation-item" onClick={ event => handleToolbarItemClick(ToolbarViewItems.MOD_TOOLS_ITEM) }>
<i className="icon icon-modtools"></i> <i className="icon icon-modtools"></i>
</div> </div>) }
</div> </div>
<div id="toolbar-chat-input-container" className="d-flex align-items-center" /> <div id="toolbar-chat-input-container" className="d-flex align-items-center" />
</div> </div>