Update mod tools layout

This commit is contained in:
Bill 2022-02-20 23:28:21 -05:00
parent 0c01bb2e37
commit 8aa7279e7c
51 changed files with 1025 additions and 1028 deletions

View File

@ -69,6 +69,8 @@ $camera-checkout-width: 350px;
$room-info-width: 325px; $room-info-width: 325px;
$nitro-mod-tools-width: 175px;
.nitro-app { .nitro-app {
width: 100%; width: 100%;
height: 100%; height: 100%;

View File

@ -732,7 +732,7 @@ $table-cell-padding-x-sm: .25rem !default;
$table-cell-vertical-align: top !default; $table-cell-vertical-align: top !default;
$table-color: $body-color !default; $table-color: $black !default;
$table-bg: transparent !default; $table-bg: transparent !default;
$table-accent-bg: transparent !default; $table-accent-bg: transparent !default;

View File

@ -90,3 +90,10 @@ ul {
.flex-basis-max-content { .flex-basis-max-content {
flex-basis: max-content; flex-basis: max-content;
} }
.striped-children {
> :nth-child(1) {
background-color: $table-striped-bg;
}
}

View File

@ -0,0 +1,20 @@
import { createContext, Dispatch, FC, ProviderProps, useContext } from 'react';
import { IModToolsAction, IModToolsState } from './reducers/ModToolsReducer';
export interface IModToolsContext
{
modToolsState: IModToolsState;
dispatchModToolsState: Dispatch<IModToolsAction>;
}
const ModToolsContext = createContext<IModToolsContext>({
modToolsState: null,
dispatchModToolsState: null
});
export const ModToolsContextProvider: FC<ProviderProps<IModToolsContext>> = props =>
{
return <ModToolsContext.Provider value={ props.value }>{ props.children }</ModToolsContext.Provider>
}
export const useModToolsContext = () => useContext(ModToolsContext);

View File

@ -10,7 +10,7 @@ import { CreateMessageHook, useRoomEngineEvent, useUiEvent } from '../../hooks';
import { NotificationAlertType } from '../../views/notification-center/common/NotificationAlertType'; import { NotificationAlertType } from '../../views/notification-center/common/NotificationAlertType';
import { NotificationUtilities } from '../../views/notification-center/common/NotificationUtilities'; import { NotificationUtilities } from '../../views/notification-center/common/NotificationUtilities';
import { SetCfhCategories } from './common/GetCFHCategories'; import { SetCfhCategories } from './common/GetCFHCategories';
import { useModToolsContext } from './context/ModToolsContext'; import { useModToolsContext } from './ModToolsContext';
import { ModToolsActions } from './reducers/ModToolsReducer'; import { ModToolsActions } from './reducers/ModToolsReducer';
export const ModToolsMessageHandler: FC<{}> = props => export const ModToolsMessageHandler: FC<{}> = props =>

View File

@ -1,9 +1,93 @@
.nitro-mod-tools { .nitro-mod-tools {
width: 200px; width: $nitro-mod-tools-width;
} }
@import './views/room/room-tools/ModToolsRoomView'; .nitro-mod-tools-room {
@import './views/chatlog/ChatlogView'; width: 240px;
@import './views/user/user-info/ModToolsUserView';
@import './views/user/user-room-visits/ModToolsUserRoomVisitsView'; .username {
@import './views/tickets/ModToolsTicketView'; color: #1E7295;
text-decoration: underline;
}
}
.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);
}
}
}
.nitro-mod-tools-user-visits {
min-width: 300px;
.user-visits {
min-height: 200px;
.roomvisits-container {
div.room-visit {
}
}
}
}
.nitro-mod-tools-chatlog {
width: 400px;
}
.nitro-mod-tools-user-visits {
width: 250px;
}
.nitro-mod-tools-tickets {
width: 400px;
height: 200px;
}
.nitro-mod-tools-handle-issue {
width: 400px;
}
.nitro-mod-tools-chatlog,
.nitro-mod-tools-user-visits {
.log-container {
min-height: 200px;
.log-entry-container {
.log-entry {
&.highlighted {
border: 1px solid $red;
}
}
&.highlighted {
border: 1px solid $red;
}
}
&:first-child {
padding-top: 0;
}
}
}

View File

@ -1,6 +1,8 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { RoomEngineObjectEvent, RoomObjectCategory } from '@nitrots/nitro-renderer'; import { RoomEngineObjectEvent, RoomObjectCategory } from '@nitrots/nitro-renderer';
import { FC, useCallback, useReducer, useState } from 'react'; import { FC, useCallback, useReducer, useState } from 'react';
import { GetRoomSession } from '../../api'; import { GetRoomSession } from '../../api';
import { Button } from '../../common';
import { ModToolsEvent } from '../../events/mod-tools/ModToolsEvent'; import { ModToolsEvent } from '../../events/mod-tools/ModToolsEvent';
import { ModToolsOpenRoomChatlogEvent } from '../../events/mod-tools/ModToolsOpenRoomChatlogEvent'; import { ModToolsOpenRoomChatlogEvent } from '../../events/mod-tools/ModToolsOpenRoomChatlogEvent';
import { ModToolsOpenRoomInfoEvent } from '../../events/mod-tools/ModToolsOpenRoomInfoEvent'; import { ModToolsOpenRoomInfoEvent } from '../../events/mod-tools/ModToolsOpenRoomInfoEvent';
@ -8,23 +10,23 @@ import { ModToolsOpenUserInfoEvent } from '../../events/mod-tools/ModToolsOpenUs
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 './ModToolsContext';
import { ModToolsMessageHandler } from './ModToolsMessageHandler'; import { ModToolsMessageHandler } from './ModToolsMessageHandler';
import { initialModTools, ModToolsActions, ModToolsReducer } from './reducers/ModToolsReducer'; import { initialModTools, ModToolsActions, ModToolsReducer } from './reducers/ModToolsReducer';
import { ISelectedUser } from './utils/ISelectedUser'; import { ISelectedUser } from './utils/ISelectedUser';
import { ModToolsChatlogView } from './views/room/room-chatlog/ModToolsChatlogView'; import { ModToolsChatlogView } from './views/room/ModToolsChatlogView';
import { ModToolsRoomView } from './views/room/room-tools/ModToolsRoomView'; import { ModToolsRoomView } from './views/room/ModToolsRoomView';
import { ModToolsTicketsView } from './views/tickets/ModToolsTicketsView'; import { ModToolsTicketsView } from './views/tickets/ModToolsTicketsView';
import { ModToolsUserChatlogView } from './views/user/user-chatlog/ModToolsUserChatlogView'; import { ModToolsUserChatlogView } from './views/user/ModToolsUserChatlogView';
import { ModToolsUserView } from './views/user/user-info/ModToolsUserView'; import { ModToolsUserView } from './views/user/ModToolsUserView';
export const ModToolsView: FC<{}> = props => export const ModToolsView: FC<{}> = props =>
{ {
const [ isVisible, setIsVisible ] = useState(false); const [ isVisible, setIsVisible ] = useState(false);
const [ modToolsState, dispatchModToolsState ] = useReducer(ModToolsReducer, initialModTools);
const { currentRoomId = null, openRooms = null, openRoomChatlogs = null, openUserChatlogs = null, openUserInfo = null } = modToolsState;
const [ selectedUser, setSelectedUser] = useState<ISelectedUser>(null); const [ selectedUser, setSelectedUser] = useState<ISelectedUser>(null);
const [ isTicketsVisible, setIsTicketsVisible ] = useState(false); const [ isTicketsVisible, setIsTicketsVisible ] = useState(false);
const [ modToolsState, dispatchModToolsState ] = useReducer(ModToolsReducer, initialModTools);
const { currentRoomId = null, openRooms = null, openRoomChatlogs = null, openUserChatlogs = null, openUserInfo = null } = modToolsState;
const onModToolsEvent = useCallback((event: ModToolsEvent) => const onModToolsEvent = useCallback((event: ModToolsEvent) =>
{ {
@ -190,11 +192,19 @@ export const ModToolsView: FC<{}> = props =>
{ 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" gap={ 1 }>
<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 gap={ 1 } onClick={ event => handleClick('toggle_room') } disabled={ !currentRoomId }>
<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> <FontAwesomeIcon icon="home" /> Room Tool
<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>
<button className="btn btn-primary btn-sm w-100" onClick={ () => setIsTicketsVisible(value => !value) }><i className="fas fa-exclamation-circle"></i> Report Tool</button> <Button gap={ 1 } onClick={ event => handleClick('toggle_room_chatlog') } disabled={ !currentRoomId }>
<FontAwesomeIcon icon="comments" /> Chatlog Tool
</Button>
<Button gap={ 1 } onClick={ () => handleClick('toggle_user_info') } disabled={ !selectedUser }>
<FontAwesomeIcon icon="user" /> User: { selectedUser ? selectedUser.username : '' }
</Button>
<Button gap={ 1 } onClick={ () => setIsTicketsVisible(value => !value) }>
<FontAwesomeIcon icon="exclamation-circle" /> Report Tool
</Button>
</NitroCardContentView> </NitroCardContentView>
</NitroCardView> } </NitroCardView> }
{ openRooms && openRooms.map(roomId => { openRooms && openRooms.map(roomId =>

View File

@ -1,14 +0,0 @@
import { createContext, FC, useContext } from 'react';
import { IModToolsContext, ModToolsContextProps } from './ModToolsContext.types';
const ModToolsContext = createContext<IModToolsContext>({
modToolsState: null,
dispatchModToolsState: null
});
export const ModToolsContextProvider: FC<ModToolsContextProps> = props =>
{
return <ModToolsContext.Provider value={ props.value }>{ props.children }</ModToolsContext.Provider>
}
export const useModToolsContext = () => useContext(ModToolsContext);

View File

@ -1,13 +0,0 @@
import { Dispatch, ProviderProps } from 'react';
import { IModToolsAction, IModToolsState } from '../reducers/ModToolsReducer';
export interface IModToolsContext
{
modToolsState: IModToolsState;
dispatchModToolsState: Dispatch<IModToolsAction>;
}
export interface ModToolsContextProps extends ProviderProps<IModToolsContext>
{
}

View File

@ -1,40 +0,0 @@
.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

@ -1,18 +1,23 @@
import { ChatlineData, ChatRecordData, UserProfileComposer } from '@nitrots/nitro-renderer'; import { ChatRecordData, UserProfileComposer } from '@nitrots/nitro-renderer';
import { FC, useCallback } from 'react'; import { CSSProperties, FC, Key, useCallback } from 'react';
import { AutoSizer, CellMeasurer, CellMeasurerCache, List, ListRowProps, ListRowRenderer } from 'react-virtualized'; import { AutoSizer, CellMeasurer, CellMeasurerCache, List, ListRowProps } from 'react-virtualized';
import { TryVisitRoom } from '../../../../api'; import { TryVisitRoom } from '../../../../api';
import { Base, Button, Column, Flex, Grid, Text } from '../../../../common';
import { ModToolsOpenRoomInfoEvent } from '../../../../events/mod-tools/ModToolsOpenRoomInfoEvent'; import { ModToolsOpenRoomInfoEvent } from '../../../../events/mod-tools/ModToolsOpenRoomInfoEvent';
import { dispatchUiEvent, SendMessageHook } from '../../../../hooks'; import { dispatchUiEvent, SendMessageHook } from '../../../../hooks';
import { ChatlogViewProps } from './ChatlogView.types';
interface ChatlogViewProps
{
records: ChatRecordData[];
}
export const ChatlogView: FC<ChatlogViewProps> = props => export const ChatlogView: FC<ChatlogViewProps> = props =>
{ {
const { records = null } = props; const { records = null } = props;
const simpleRowRenderer: ListRowRenderer = (props: ListRowProps) => const rowRenderer = (props: ListRowProps) =>
{ {
const item = records[0].chatlog[props.index]; let chatlogEntry = records[0].chatlog[props.index];
return ( return (
<CellMeasurer <CellMeasurer
@ -22,41 +27,42 @@ export const ChatlogView: FC<ChatlogViewProps> = props =>
parent={ props.parent } parent={ props.parent }
rowIndex={ props.index } rowIndex={ props.index }
> >
<div key={props.key} style={props.style} className={'row chatlog-entry justify-content-start ' + (item.hasHighlighting ? 'highlighted' : '')}> <Grid key={ props.key } fullHeight={ false } style={ props.style } gap={ 1 } alignItems="center" className="log-entry py-1 border-bottom">
<div className="col-auto text-center">{item.timestamp}</div> <Text className="g-col-2">{ chatlogEntry.timestamp }</Text>
<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> <Text className="g-col-3" bold underline pointer onClick={ event => SendMessageHook(new UserProfileComposer(chatlogEntry.userId)) }>{ chatlogEntry.userName }</Text>
<div className="col justify-content-start h-100"><span className="text-break text-wrap h-100">{item.message}</span></div> <Text textBreak wrap className="g-col-7">{ chatlogEntry.message }</Text>
</div> </Grid>
</CellMeasurer> </CellMeasurer>
); );
}; };
const advancedRowRenderer: ListRowRenderer = (props: ListRowProps) => const advancedRowRenderer = (props: ListRowProps) =>
{ {
let chatlogEntry: ChatlineData; let chatlogEntry = null;
let currentRecord: ChatRecordData; let currentRecord: ChatRecordData = null;
let isRoomInfo = false; let isRoomInfo = false;
let totalIndex = 0; let totalIndex = 0;
for(let i = 0; i < records.length; i++) for(let i = 0; i < records.length; i++)
{ {
currentRecord = records[i]; currentRecord = records[i];
totalIndex++; // row for room info totalIndex++; // row for room info
totalIndex = totalIndex + currentRecord.chatlog.length; totalIndex = (totalIndex + currentRecord.chatlog.length);
if(props.index > (totalIndex - 1)) if(props.index > (totalIndex - 1)) continue;
{
continue; // it is not in current one
}
if((props.index + 1) === (totalIndex - currentRecord.chatlog.length)) if((props.index + 1) === (totalIndex - currentRecord.chatlog.length))
{ {
isRoomInfo = true; isRoomInfo = true;
break; break;
} }
const index = props.index - (totalIndex - currentRecord.chatlog.length);
const index = (props.index - (totalIndex - currentRecord.chatlog.length));
chatlogEntry = currentRecord.chatlog[index]; chatlogEntry = currentRecord.chatlog[index];
break; break;
} }
@ -68,15 +74,14 @@ export const ChatlogView: FC<ChatlogViewProps> = props =>
parent={ props.parent } parent={ props.parent }
rowIndex={ props.index } rowIndex={ props.index }
> >
{isRoomInfo && <RoomInfo roomId={currentRecord.roomId} roomName={currentRecord.roomName} uniqueKey={props.key} style={props.style}/>} { (isRoomInfo && currentRecord) &&
<RoomInfo roomId={ currentRecord.roomId } roomName={ currentRecord.roomName } uniqueKey={ props.key } style={ props.style } /> }
{ !isRoomInfo && { !isRoomInfo &&
<div key={props.key} style={props.style} className="row chatlog-entry justify-content-start"> <Grid key={ props.key } style={ props.style } gap={ 1 } alignItems="center" className="log-entry py-1 border-bottom">
<div className="col-auto text-center">{chatlogEntry.timestamp}</div> <Text className="g-col-2">{ chatlogEntry.timestamp }</Text>
<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> <Text className="g-col-3" bold underline pointer onClick={ event => SendMessageHook(new UserProfileComposer(chatlogEntry.userId)) }>{ chatlogEntry.userName }</Text>
<div className="col justify-content-start h-100"><span className="text-break text-wrap h-100">{chatlogEntry.message}</span></div> <Text textBreak wrap className="g-col-7">{ chatlogEntry.message }</Text>
</div> </Grid> }
}
</CellMeasurer> </CellMeasurer>
); );
} }
@ -94,35 +99,41 @@ export const ChatlogView: FC<ChatlogViewProps> = props =>
return count; return count;
}, [records]); }, [records]);
const RoomInfo = (props: { roomId: number, roomName: string, uniqueKey: Key, style: CSSProperties }) =>
{
return (
<Flex key={ props.uniqueKey } gap={ 2 } alignItems="center" justifyContent="between" className="room-info bg-muted rounded p-1" style={ props.style }>
<Flex gap={ 1 }>
<Text bold>Room name:</Text>
<Text>{ props.roomName }</Text>
</Flex>
<Flex gap={ 1 }>
<Button onClick={ event => TryVisitRoom(props.roomId) }>Visit Room</Button>
<Button onClick={ event => dispatchUiEvent(new ModToolsOpenRoomInfoEvent(props.roomId)) }>Room Tools</Button>
</Flex>
</Flex>
);
}
const cache = new CellMeasurerCache({ const cache = new CellMeasurerCache({
defaultHeight: 25, defaultHeight: 25,
fixedWidth: true 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-auto" onClick={() => TryVisitRoom(roomId)}>Visit Room</button>
<button className="btn btn-sm btn-primary col-auto" onClick={() => dispatchUiEvent(new ModToolsOpenRoomInfoEvent(roomId))}>Room Tools</button>
</div>
);
}, []);
return ( return (
<> <>
{ { (records && (records.length === 1)) &&
(records && records.length) && <RoomInfo roomId={records[0].roomId} roomName={records[0].roomName} uniqueKey={ null } style={ {} } /> }
<> <Column fit gap={ 0 } overflow="hidden">
{(records.length === 1) && <RoomInfo roomId={records[0].roomId} roomName={records[0].roomName} uniqueKey={records[0].roomId} style={{}} />} <Column gap={ 2 }>
<div className="chatlog-messages w-100 h-100 overflow-hidden"> <Grid gap={ 1 } className="text-black fw-bold border-bottom pb-1">
<div className="row align-items-start w-100"> <Base className="g-col-2">Time</Base>
<div className="col-auto text-center fw-bold">Time</div> <Base className="g-col-3">User</Base>
<div className="col-sm-2 username-label fw-bold">User</div> <Base className="g-col-7">Message</Base>
<div className="col fw-bold">Message</div> </Grid>
</div> </Column>
<div className="row w-100 h-100 chatlog"> { (records && (records.length > 0)) &&
<Column className="log-container striped-children" overflow="auto" gap={ 0 }>
<AutoSizer defaultWidth={ 400 } defaultHeight={ 200 }> <AutoSizer defaultWidth={ 400 } defaultHeight={ 200 }>
{ ({ height, width }) => { ({ height, width }) =>
{ {
@ -132,19 +143,16 @@ export const ChatlogView: FC<ChatlogViewProps> = props =>
<List <List
width={ width } width={ width }
height={ height } height={ height }
rowCount={records.length > 1 ? getNumRowsForAdvanced() : records[0].chatlog.length} rowCount={ (records.length > 1) ? getNumRowsForAdvanced() : records[0].chatlog.length }
rowHeight={ cache.rowHeight } rowHeight={ cache.rowHeight }
className={'chatlog-container'} className={ 'log-entry-container' }
rowRenderer={records.length > 1 ? advancedRowRenderer : simpleRowRenderer} rowRenderer={ (records.length > 1) ? advancedRowRenderer : rowRenderer }
deferredMeasurementCache={ cache } /> deferredMeasurementCache={ cache } />
) );
} } }
}
</AutoSizer> </AutoSizer>
</div> </Column> }
</div> </Column>
</>
}
</> </>
); );
} }

View File

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

View File

@ -1,21 +1,20 @@
import { ChatRecordData, GetRoomChatlogMessageComposer, RoomChatlogEvent } from '@nitrots/nitro-renderer'; import { ChatRecordData, GetRoomChatlogMessageComposer, RoomChatlogEvent } 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 { CreateMessageHook, SendMessageHook } from '../../../../hooks/messages';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../../layout'; import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
import { ChatlogView } from '../../chatlog/ChatlogView'; import { ChatlogView } from '../chatlog/ChatlogView';
import { ModToolsChatlogViewProps } from './ModToolsChatlogView.types';
interface ModToolsChatlogViewProps
{
roomId: number;
onCloseClick: () => void;
}
export const ModToolsChatlogView: FC<ModToolsChatlogViewProps> = props => export const ModToolsChatlogView: FC<ModToolsChatlogViewProps> = props =>
{ {
const { roomId = null, onCloseClick = null } = props; const { roomId = null, onCloseClick = null } = props;
const [ roomChatlog, setRoomChatlog ] = useState<ChatRecordData>(null); const [ roomChatlog, setRoomChatlog ] = useState<ChatRecordData>(null);
useEffect(() =>
{
SendMessageHook(new GetRoomChatlogMessageComposer(roomId));
}, [roomId]);
const onModtoolRoomChatlogEvent = useCallback((event: RoomChatlogEvent) => const onModtoolRoomChatlogEvent = useCallback((event: RoomChatlogEvent) =>
{ {
const parser = event.getParser(); const parser = event.getParser();
@ -23,17 +22,23 @@ export const ModToolsChatlogView: FC<ModToolsChatlogViewProps> = props =>
if(!parser || parser.data.roomId !== roomId) return; if(!parser || parser.data.roomId !== roomId) return;
setRoomChatlog(parser.data); setRoomChatlog(parser.data);
}, [roomId, setRoomChatlog]); }, [ roomId ]);
CreateMessageHook(RoomChatlogEvent, onModtoolRoomChatlogEvent); CreateMessageHook(RoomChatlogEvent, onModtoolRoomChatlogEvent);
useEffect(() =>
{
SendMessageHook(new GetRoomChatlogMessageComposer(roomId));
}, [ roomId ]);
if(!roomChatlog) return null;
return ( return (
<NitroCardView className="nitro-mod-tools-room-chatlog" simple={true}> <NitroCardView className="nitro-mod-tools-chatlog" simple={true}>
<NitroCardHeaderView headerText={'Room Chatlog' + (roomChatlog ? ': ' + roomChatlog.roomName : '')} onCloseClick={() => onCloseClick()} /> <NitroCardHeaderView headerText={ `Room Chatlog ${ roomChatlog.roomName }` } onCloseClick={ onCloseClick } />
<NitroCardContentView className="text-black h-100"> <NitroCardContentView className="text-black h-100">
{ roomChatlog && { roomChatlog &&
<ChatlogView records={[roomChatlog]} /> <ChatlogView records={ [ roomChatlog ] } /> }
}
</NitroCardContentView> </NitroCardContentView>
</NitroCardView> </NitroCardView>
); );

View File

@ -0,0 +1,128 @@
import { GetModeratorRoomInfoMessageComposer, ModerateRoomMessageComposer, ModeratorActionMessageComposer, ModeratorRoomInfoEvent } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react';
import { TryVisitRoom } from '../../../../api';
import { Button, Column, Flex, Text } from '../../../../common';
import { ModToolsOpenRoomChatlogEvent } from '../../../../events/mod-tools/ModToolsOpenRoomChatlogEvent';
import { BatchUpdates, dispatchUiEvent } from '../../../../hooks';
import { CreateMessageHook, SendMessageHook } from '../../../../hooks/messages';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
interface ModToolsRoomViewProps
{
roomId: number;
onCloseClick: () => void;
}
export const ModToolsRoomView: FC<ModToolsRoomViewProps> = props =>
{
const { roomId = null, onCloseClick = null } = props;
const [ infoRequested, setInfoRequested ] = useState(false);
const [ loadedRoomId, setLoadedRoomId ] = useState(null);
const [ name, setName ] = useState(null);
const [ ownerId, setOwnerId ] = useState(null);
const [ ownerName, setOwnerName ] = useState(null);
const [ ownerInRoom, setOwnerInRoom ] = useState(false);
const [ usersInRoom, setUsersInRoom ] = useState(0);
//form data
const [ kickUsers, setKickUsers ] = useState(false);
const [ lockRoom, setLockRoom ] = useState(false);
const [ changeRoomName, setChangeRoomName ] = useState(false);
const [ message, setMessage ] = useState('');
const onModtoolRoomInfoEvent = useCallback((event: ModeratorRoomInfoEvent) =>
{
const parser = event.getParser();
if(!parser || parser.data.flatId !== roomId) return;
BatchUpdates(() =>
{
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);
});
}, [ roomId ]);
CreateMessageHook(ModeratorRoomInfoEvent, onModtoolRoomInfoEvent);
const handleClick = useCallback((action: string, value?: string) =>
{
if(!action) return;
switch(action)
{
case 'alert_only':
if(message.trim().length === 0) return;
SendMessageHook(new ModeratorActionMessageComposer(ModeratorActionMessageComposer.ACTION_ALERT, message, ''));
return;
case 'send_message':
if(message.trim().length === 0) return;
SendMessageHook(new ModeratorActionMessageComposer(ModeratorActionMessageComposer.ACTION_MESSAGE, message, ''));
SendMessageHook(new ModerateRoomMessageComposer(roomId, lockRoom ? 1 : 0, changeRoomName ? 1 : 0, kickUsers ? 1 : 0))
return;
}
}, [ changeRoomName, kickUsers, lockRoom, message, roomId ]);
useEffect(() =>
{
if(infoRequested) return;
SendMessageHook(new GetModeratorRoomInfoMessageComposer(roomId));
setInfoRequested(true);
}, [ roomId, infoRequested, setInfoRequested ]);
return (
<NitroCardView className="nitro-mod-tools-room" simple>
<NitroCardHeaderView headerText={ 'Room Info' + (name ? ': ' + name : '') } onCloseClick={ event => onCloseClick() } />
<NitroCardContentView className="text-black">
<Flex gap={ 2 }>
<Column justifyContent="center" grow gap={ 1 }>
<Flex alignItems="center" gap={ 2 }>
<Text bold align="end" className="col-7">Room Owner:</Text>
<Text underline pointer truncate>{ ownerName }</Text>
</Flex>
<Flex alignItems="center" gap={ 2 }>
<Text bold align="end" className="col-7">Users in room:</Text>
<Text>{ usersInRoom }</Text>
</Flex>
<Flex alignItems="center" gap={ 2 }>
<Text bold align="end" className="col-7">Owner in room:</Text>
<Text>{ ownerInRoom ? 'Yes' : 'No' }</Text>
</Flex>
</Column>
<Column gap={ 1 }>
<Button onClick={ event => TryVisitRoom(roomId) }>Visit Room</Button>
<Button onClick={ event => dispatchUiEvent(new ModToolsOpenRoomChatlogEvent(roomId)) }>Chatlog</Button>
</Column>
</Flex>
<Column className="bg-muted rounded p-2" gap={ 1 }>
<Flex alignItems="center" gap={ 1 }>
<input className="form-check-input" type="checkbox" checked={ kickUsers } onChange={ event => setKickUsers(event.target.checked) } />
<Text small>Kick everyone out</Text>
</Flex>
<Flex alignItems="center" gap={ 1 }>
<input className="form-check-input" type="checkbox" checked={ lockRoom } onChange={ event => setLockRoom(event.target.checked) } />
<Text small>Enable the doorbell</Text>
</Flex>
<Flex alignItems="center" gap={ 1 }>
<input className="form-check-input" type="checkbox" checked={ changeRoomName } onChange={ event => setChangeRoomName(event.target.checked) }/>
<Text small>Change room name</Text>
</Flex>
</Column>
<textarea className="form-control" placeholder="Type a mandatory message to the users in this text box..." value={ message } onChange={ event => setMessage(event.target.value) }></textarea>
<Flex justifyContent="between">
<Button variant="danger" onClick={ event => handleClick('send_message') }>Send Caution</Button>
<Button onClick={ event => handleClick('alert_only') }>Send Alert only</Button>
</Flex>
</NitroCardContentView>
</NitroCardView>
);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,14 @@
import { CfhChatlogData, CfhChatlogEvent, GetCfhChatlogMessageComposer } from '@nitrots/nitro-renderer'; import { CfhChatlogData, CfhChatlogEvent, GetCfhChatlogMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react'; import { FC, useCallback, useEffect, useState } from 'react';
import { CreateMessageHook, SendMessageHook } from '../../../../../hooks'; import { CreateMessageHook, SendMessageHook } from '../../../../hooks';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../../layout'; import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
import { ChatlogView } from '../../chatlog/ChatlogView'; import { ChatlogView } from '../chatlog/ChatlogView';
import { CfhChatlogViewProps } from './CfhChatlogView.types';
interface CfhChatlogViewProps
{
issueId: number;
onCloseClick(): void;
}
export const CfhChatlogView: FC<CfhChatlogViewProps> = props => export const CfhChatlogView: FC<CfhChatlogViewProps> = props =>
{ {

View File

@ -0,0 +1,99 @@
import { CloseIssuesMessageComposer, ReleaseIssuesMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useMemo, useState } from 'react';
import { LocalizeText } from '../../../../api';
import { Button, Column, Grid, Text } from '../../../../common';
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 '../../ModToolsContext';
import { CfhChatlogView } from './CfhChatlogView';
interface IssueInfoViewProps
{
issueId: number;
onIssueInfoClosed(issueId: number): void;
}
export const ModToolsIssueInfoView: 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(() =>
{
if(!tickets || !tickets.length) return null;
return tickets.find(issue => issue.issueId === issueId);
}, [ issueId, tickets ]);
const releaseIssue = (issueId: number) =>
{
SendMessageHook(new ReleaseIssuesMessageComposer([ issueId ]));
onIssueInfoClosed(issueId);
}
const closeIssue = (resolutionType: number) =>
{
SendMessageHook(new CloseIssuesMessageComposer([ issueId ], resolutionType));
onIssueInfoClosed(issueId)
}
const openUserInfo = (userId: number) => dispatchUiEvent(new ModToolsOpenUserInfoEvent(userId));
return (
<>
<NitroCardView className="nitro-mod-tools-handle-issue" simple>
<NitroCardHeaderView headerText={'Resolving issue ' + issueId} onCloseClick={() => onIssueInfoClosed(issueId)} />
<NitroCardContentView className="text-black">
<Text fontSize={ 4 }>Issue Information</Text>
<Grid>
<Column size={ 8 }>
<table className="table table-striped table-sm table-text-small text-black m-0">
<tbody>
<tr>
<th>Source</th>
<td>{ getSourceName(ticket.categoryId) }</td>
</tr>
<tr>
<th>Category</th>
<td>{ LocalizeText('help.cfh.topic.' + ticket.reportedCategoryId) }</td>
</tr>
<tr>
<th>Description</th>
<td>{ ticket.message }</td>
</tr>
<tr>
<th>Caller</th>
<td>
<Text bold underline pointer onClick={ event => openUserInfo(ticket.reporterUserId) }>{ ticket.reporterUserName }</Text>
</td>
</tr>
<tr>
<th>Reported User</th>
<td>
<Text bold underline pointer onClick={ event => openUserInfo(ticket.reportedUserId) }>{ ticket.reportedUserName }</Text>
</td>
</tr>
</tbody>
</table>
</Column>
<Column size={ 4 } gap={ 1 }>
<Button variant="secondary" onClick={ () => setcfhChatlogOpen(!cfhChatlogOpen) }>Chatlog</Button>
<Button onClick={ event => closeIssue(CloseIssuesMessageComposer.RESOLUTION_USELESS) }>Close as useless</Button>
<Button variant="danger" onClick={ event => closeIssue(CloseIssuesMessageComposer.RESOLUTION_ABUSIVE) }>Close as abusive</Button>
<Button variant="success" onClick={ event => closeIssue(CloseIssuesMessageComposer.RESOLUTION_RESOLVED) }>Close as resolved</Button>
<Button variant="secondary" onClick={ event => releaseIssue(issueId)} >Release</Button>
</Column>
</Grid>
</NitroCardContentView>
</NitroCardView>
{ cfhChatlogOpen &&
<CfhChatlogView issueId={ issueId } onCloseClick={ () => setcfhChatlogOpen(false) }/> }
</>
);
}

View File

@ -0,0 +1,49 @@
import { IssueMessageData, ReleaseIssuesMessageComposer } from '@nitrots/nitro-renderer';
import { FC } from 'react';
import { Base, Button, Column, Grid } from '../../../../common';
import { SendMessageHook } from '../../../../hooks';
interface ModToolsMyIssuesTabViewProps
{
myIssues: IssueMessageData[];
onIssueHandleClick(issueId: number): void;
}
export const ModToolsMyIssuesTabView: FC<ModToolsMyIssuesTabViewProps> = props =>
{
const { myIssues = null, onIssueHandleClick = null } = props;
const onReleaseIssue = (issueId: number) => SendMessageHook(new ReleaseIssuesMessageComposer([issueId]));
return (
<Column gap={ 0 } overflow="hidden">
<Column gap={ 2 }>
<Grid gap={ 1 } className="text-black fw-bold border-bottom pb-1">
<Base className="g-col-2">Type</Base>
<Base className="g-col-3">Room/Player</Base>
<Base className="g-col-3">Opened</Base>
<Base className="g-col-2"></Base>
<Base className="g-col-2"></Base>
</Grid>
</Column>
<Column overflow="auto" className="striped-children" gap={ 0 }>
{ myIssues && (myIssues.length > 0) && myIssues.map(issue =>
{
return (
<Grid key={ issue.issueId } gap={ 1 } alignItems="center" className="text-black py-1 border-bottom">
<Base className="g-col-2">{ issue.categoryId }</Base>
<Base className="g-col-3">{ issue.reportedUserName }</Base>
<Base className="g-col-3">{ new Date(Date.now() - issue.issueAgeInMilliseconds).toLocaleTimeString() }</Base>
<Base className="g-col-2">
<Button variant="primary" onClick={ event => onIssueHandleClick(issue.issueId) }>Handle</Button>
</Base>
<Base className="g-col-2">
<Button variant="danger" onClick={ event => onReleaseIssue(issue.issueId) }>Release</Button>
</Base>
</Grid>
);
}) }
</Column>
</Column>
);
}

View File

@ -0,0 +1,44 @@
import { IssueMessageData, PickIssuesMessageComposer } from '@nitrots/nitro-renderer';
import { FC } from 'react';
import { Base, Button, Column, Grid } from '../../../../common';
import { SendMessageHook } from '../../../../hooks';
interface ModToolsOpenIssuesTabViewProps
{
openIssues: IssueMessageData[];
}
export const ModToolsOpenIssuesTabView: FC<ModToolsOpenIssuesTabViewProps> = props =>
{
const { openIssues = null } = props;
const onPickIssue = (issueId: number) => SendMessageHook(new PickIssuesMessageComposer([issueId], false, 0, 'pick issue button'));
return (
<Column gap={ 0 } overflow="hidden">
<Column gap={ 2 }>
<Grid gap={ 1 } className="text-black fw-bold border-bottom pb-1">
<Base className="g-col-2">Type</Base>
<Base className="g-col-3">Room/Player</Base>
<Base className="g-col-4">Opened</Base>
<Base className="g-col-3"></Base>
</Grid>
</Column>
<Column overflow="auto" className="striped-children" gap={ 0 }>
{ openIssues && (openIssues.length > 0) && openIssues.map(issue =>
{
return (
<Grid key={ issue.issueId } gap={ 1 } alignItems="center" className="text-black py-1 border-bottom">
<Base className="g-col-2">{ issue.categoryId }</Base>
<Base className="g-col-3">{ issue.reportedUserName }</Base>
<Base className="g-col-4">{ new Date(Date.now() - issue.issueAgeInMilliseconds).toLocaleTimeString() }</Base>
<Base className="g-col-3">
<Button variant="success" onClick={ event => onPickIssue(issue.issueId) }>Pick Issue</Button>
</Base>
</Grid>
);
}) }
</Column>
</Column>
);
}

View File

@ -0,0 +1,39 @@
import { IssueMessageData } from '@nitrots/nitro-renderer';
import { FC } from 'react';
import { Base, Column, Grid } from '../../../../common';
interface ModToolsPickedIssuesTabViewProps
{
pickedIssues: IssueMessageData[];
}
export const ModToolsPickedIssuesTabView: FC<ModToolsPickedIssuesTabViewProps> = props =>
{
const { pickedIssues = null } = props;
return (
<Column gap={ 0 } overflow="hidden">
<Column gap={ 2 }>
<Grid gap={ 1 } className="text-black fw-bold border-bottom pb-1">
<Base className="g-col-2">Type</Base>
<Base className="g-col-3">Room/Player</Base>
<Base className="g-col-4">Opened</Base>
<Base className="g-col-3">Picker</Base>
</Grid>
</Column>
<Column overflow="auto" className="striped-children" gap={ 0 }>
{ pickedIssues && (pickedIssues.length > 0) && pickedIssues.map(issue =>
{
return (
<Grid key={ issue.issueId } gap={ 1 } alignItems="center" className="text-black py-1 border-bottom">
<Base className="g-col-2">{ issue.categoryId }</Base>
<Base className="g-col-3">{ issue.reportedUserName }</Base>
<Base className="g-col-4">{ new Date(Date.now() - issue.issueAgeInMilliseconds).toLocaleTimeString() }</Base>
<Base className="g-col-3">{ issue.pickerUserName }</Base>
</Grid>
);
}) }
</Column>
</Column>
);
}

View File

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

View File

@ -2,12 +2,16 @@ import { IssueMessageData } from '@nitrots/nitro-renderer';
import { FC, useCallback, useMemo, useState } from 'react'; import { FC, useCallback, useMemo, useState } from 'react';
import { GetSessionDataManager } from '../../../../api'; 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 { useModToolsContext } from '../../ModToolsContext';
import { IssueInfoView } from './issue-info/IssueInfoView'; import { ModToolsIssueInfoView } from './ModToolsIssueInfoView';
import { ModToolsTicketsViewProps } from './ModToolsTicketsView.types'; import { ModToolsMyIssuesTabView } from './ModToolsMyIssuesTabView';
import { ModToolsMyIssuesTabView } from './my-issues/ModToolsMyIssuesTabView'; import { ModToolsOpenIssuesTabView } from './ModToolsOpenIssuesTabView';
import { ModToolsOpenIssuesTabView } from './open-issues/ModToolsOpenIssuesTabView'; import { ModToolsPickedIssuesTabView } from './ModToolsPickedIssuesTabView';
import { ModToolsPickedIssuesTabView } from './picked-issues/ModToolsPickedIssuesTabView';
interface ModToolsTicketsViewProps
{
onCloseClick: () => void;
}
const TABS: string[] = [ const TABS: string[] = [
'Open Issues', 'Open Issues',
@ -82,25 +86,21 @@ export const ModToolsTicketsView: FC<ModToolsTicketsViewProps> = props =>
return ( return (
<> <>
<NitroCardView className="nitro-mod-tools-tickets" simple={ false }> <NitroCardView className="nitro-mod-tools-tickets">
<NitroCardHeaderView headerText={ 'Tickets' } onCloseClick={ onCloseClick } /> <NitroCardHeaderView headerText={ 'Tickets' } onCloseClick={ onCloseClick } />
<NitroCardContentView className="p-0 text-black">
<NitroCardTabsView> <NitroCardTabsView>
{ TABS.map((tab, index) => { TABS.map((tab, index) =>
{ {
return (<NitroCardTabsItemView key={ index } isActive={ currentTab === index } onClick={ () => setCurrentTab(index) }> return (<NitroCardTabsItemView key={ index } isActive={ (currentTab === index) } onClick={ event => setCurrentTab(index) }>
{ tab } { tab }
</NitroCardTabsItemView>); </NitroCardTabsItemView>);
}) } }) }
</NitroCardTabsView> </NitroCardTabsView>
<div className="p-2"> <NitroCardContentView gap={ 1 }>
<CurrentTabComponent /> <CurrentTabComponent />
</div>
</NitroCardContentView> </NitroCardContentView>
</NitroCardView> </NitroCardView>
{ { issueInfoWindows && (issueInfoWindows.length > 0) && issueInfoWindows.map(issueId => <ModToolsIssueInfoView key={ issueId } issueId={ issueId } onIssueInfoClosed={ onIssueInfoClosed } />) }
issueInfoWindows && issueInfoWindows.map(issueId => <IssueInfoView key={issueId} issueId={issueId} onIssueInfoClosed={onIssueInfoClosed}/>)
}
</> </>
); );
} }

View File

@ -1,4 +0,0 @@
export interface ModToolsTicketsViewProps
{
onCloseClick: () => void;
}

View File

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

View File

@ -1,72 +0,0 @@
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 text-primary" onClick={() => openUserInfo(ticket.reporterUserId)}>{ticket.reporterUserName}</button></div>
<div><span className="fw-bold">Reported User: </span><button className="btn btn-link fw-bold text-danger" 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

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

View File

@ -1,45 +0,0 @@
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

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

View File

@ -1,42 +0,0 @@
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

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

View File

@ -1,35 +0,0 @@
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

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

View File

@ -1,9 +1,14 @@
import { ChatRecordData, GetUserChatlogMessageComposer, UserChatlogEvent } from '@nitrots/nitro-renderer'; import { ChatRecordData, GetUserChatlogMessageComposer, UserChatlogEvent } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react'; import { FC, useCallback, useEffect, useState } from 'react';
import { CreateMessageHook, SendMessageHook } from '../../../../../hooks'; import { BatchUpdates, CreateMessageHook, SendMessageHook } from '../../../../hooks';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../../layout'; import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
import { ChatlogView } from '../../chatlog/ChatlogView'; import { ChatlogView } from '../chatlog/ChatlogView';
import { ModToolsUserChatlogViewProps } from './ModToolsUserChatlogView.types';
interface ModToolsUserChatlogViewProps
{
userId: number;
onCloseClick: () => void;
}
export const ModToolsUserChatlogView: FC<ModToolsUserChatlogViewProps> = props => export const ModToolsUserChatlogView: FC<ModToolsUserChatlogViewProps> = props =>
{ {
@ -11,30 +16,32 @@ export const ModToolsUserChatlogView: FC<ModToolsUserChatlogViewProps> = props =
const [ userChatlog, setUserChatlog ] = useState<ChatRecordData[]>(null); const [ userChatlog, setUserChatlog ] = useState<ChatRecordData[]>(null);
const [ username, setUsername ] = useState<string>(null); const [ username, setUsername ] = useState<string>(null);
useEffect(() =>
{
SendMessageHook(new GetUserChatlogMessageComposer(userId));
}, [userId]);
const onModtoolUserChatlogEvent = useCallback((event: UserChatlogEvent) => const onModtoolUserChatlogEvent = useCallback((event: UserChatlogEvent) =>
{ {
const parser = event.getParser(); const parser = event.getParser();
if(!parser || parser.data.userId !== userId) return; if(!parser || parser.data.userId !== userId) return;
BatchUpdates(() =>
{
setUsername(parser.data.username); setUsername(parser.data.username);
setUserChatlog(parser.data.roomChatlogs); setUserChatlog(parser.data.roomChatlogs);
}, [setUsername, setUserChatlog, userId]); });
}, [ userId ]);
CreateMessageHook(UserChatlogEvent, onModtoolUserChatlogEvent); CreateMessageHook(UserChatlogEvent, onModtoolUserChatlogEvent);
useEffect(() =>
{
SendMessageHook(new GetUserChatlogMessageComposer(userId));
}, [ userId ]);
return ( return (
<NitroCardView className="nitro-mod-tools-user-chatlog" simple={true}> <NitroCardView className="nitro-mod-tools-chatlog" simple>
<NitroCardHeaderView headerText={'User Chatlog' + (username ? ': ' + username : '')} onCloseClick={() => onCloseClick()} /> <NitroCardHeaderView headerText={ `User Chatlog: ${ username || '' }` } onCloseClick={ onCloseClick } />
<NitroCardContentView className="text-black h-100"> <NitroCardContentView className="text-black h-100">
{ userChatlog && { userChatlog &&
<ChatlogView records={userChatlog} /> <ChatlogView records={userChatlog} /> }
}
</NitroCardContentView> </NitroCardContentView>
</NitroCardView> </NitroCardView>
); );

View File

@ -0,0 +1,182 @@
import { CallForHelpTopicData, DefaultSanctionMessageComposer, ModAlertMessageComposer, ModBanMessageComposer, ModKickMessageComposer, ModMessageMessageComposer, ModMuteMessageComposer, ModTradingLockMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useMemo, useState } from 'react';
import { LocalizeText } from '../../../../api';
import { Button, Column, Flex, Text } from '../../../../common';
import { SendMessageHook } from '../../../../hooks';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
import { NotificationAlertType } from '../../../../views/notification-center/common/NotificationAlertType';
import { NotificationUtilities } from '../../../../views/notification-center/common/NotificationUtilities';
import { useModToolsContext } from '../../ModToolsContext';
import { ISelectedUser } from '../../utils/ISelectedUser';
import { ModActionDefinition } from '../../utils/ModActionDefinition';
interface ModToolsUserModActionViewProps
{
user: ISelectedUser;
onCloseClick: () => void;
}
const MOD_ACTION_DEFINITIONS = [
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 [ selectedTopic, setSelectedTopic ] = useState(-1);
const [ selectedAction, setSelectedAction ] = useState(-1);
const [ message, setMessage ] = useState<string>('');
const { modToolsState = null } = useModToolsContext();
const { cfhCategories = null, settings = null } = modToolsState;
const topics = useMemo(() =>
{
const values: CallForHelpTopicData[] = [];
if(cfhCategories && cfhCategories.length)
{
for(const category of cfhCategories)
{
for(const topic of category.topics) values.push(topic);
}
}
return values;
}, [ cfhCategories ]);
const sendAlert = (message: string) =>
{
NotificationUtilities.simpleAlert(message, NotificationAlertType.DEFAULT, null, null, 'Error');
}
const sendDefaultSanction = () =>
{
SendMessageHook(new DefaultSanctionMessageComposer(user.userId, selectedTopic, message));
onCloseClick();
}
const sendSanction = () =>
{
let errorMessage: string = null;
const category = topics[selectedTopic];
const sanction = MOD_ACTION_DEFINITIONS[selectedAction];
if((selectedTopic === -1) || (selectedAction === -1)) errorMessage = 'You must select a CFH topic and Sanction';
else if(!settings || !settings.cfhPermission) errorMessage = 'You do not have permission to do this';
else if(!category) errorMessage = 'You must select a CFH topic';
else if(!sanction) errorMessage = 'You must select a sanction';
if(errorMessage)
{
sendAlert('You must select a sanction');
return;
}
const messageOrDefault = (message.trim().length === 0) ? LocalizeText(`help.cfh.topic.${ category.id }`) : message;
switch(sanction.actionType)
{
case ModActionDefinition.ALERT: {
if(!settings.alertPermission)
{
sendAlert('You have insufficient permissions');
return;
}
if(message.trim().length === 0)
{
sendAlert('Please write a message to user');
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)
{
sendAlert('You have insufficient permissions');
return;
}
SendMessageHook(new ModBanMessageComposer(user.userId, messageOrDefault, category.id, selectedAction, (sanction.actionId === 106)));
break;
}
case ModActionDefinition.KICK: {
if(!settings.kickPermission)
{
sendAlert('You have insufficient permissions');
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)
{
sendAlert('Please write a message to user');
return;
}
SendMessageHook(new ModMessageMessageComposer(user.userId, message, category.id));
break;
}
}
onCloseClick();
}
if(!user) return null;
return (
<NitroCardView className="nitro-mod-tools-user-action" simple={true}>
<NitroCardHeaderView headerText={'Mod Action: ' + (user ? user.username : '')} onCloseClick={ () => onCloseClick() } />
<NitroCardContentView className="text-black">
<select className="form-select form-select-sm" value={ selectedTopic } onChange={ event => setSelectedTopic(parseInt(event.target.value)) }>
<option value={ -1 } disabled>CFH Topic</option>
{ topics.map((topic, index) => <option key={ index } value={ index }>{LocalizeText('help.cfh.topic.' + topic.id)}</option>) }
</select>
<select className="form-select form-select-sm" value={ selectedAction } onChange={ event => setSelectedAction(parseInt(event.target.value)) }>
<option value={ -1 } disabled>Sanction Type</option>
{ MOD_ACTION_DEFINITIONS.map((action, index) => <option key={ index } value={ index }>{ action.name }</option>) }
</select>
<Column gap={ 1 }>
<Text small>Optional message type, overrides default</Text>
<textarea className="form-control" value={ message } onChange={ event => setMessage(event.target.value) }/>
</Column>
<Flex justifyContent="between" gap={ 1 }>
<Button variant="danger" onClick={ sendSanction }>Sanction</Button>
<Button variant="success" onClick={ sendDefaultSanction }>Default Sanction</Button>
</Flex>
</NitroCardContentView>
</NitroCardView>
);
}

View File

@ -0,0 +1,85 @@
import { GetRoomVisitsMessageComposer, RoomVisitsData, RoomVisitsEvent } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react';
import { AutoSizer, List, ListRowProps } from 'react-virtualized';
import { TryVisitRoom } from '../../../../api';
import { Base, Column, Grid, Text } from '../../../../common';
import { CreateMessageHook, SendMessageHook } from '../../../../hooks';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
interface ModToolsUserRoomVisitsViewProps
{
userId: number;
onCloseClick: () => void;
}
export const ModToolsUserRoomVisitsView: FC<ModToolsUserRoomVisitsViewProps> = props =>
{
const { userId = null, onCloseClick = null } = props;
const [ roomVisitData, setRoomVisitData ] = useState<RoomVisitsData>(null);
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 = (props: ListRowProps) =>
{
const item = roomVisitData.rooms[props.index];
return (
<Grid key={ props.key } style={ props.style } gap={ 1 } alignItems="center" className="text-black py-1 border-bottom">
<Text className="g-col-2">{ item.enterHour.toString().padStart(2, '0') }: { item.enterMinute.toString().padStart(2, '0') }</Text>
<Text className="g-col-7">{ item.roomName }</Text>
<Text bold underline pointer variant="primary" className="g-col-3" onClick={ event => TryVisitRoom(item.roomId) }>Visit Room</Text>
</Grid>
);
}
useEffect(() =>
{
SendMessageHook(new GetRoomVisitsMessageComposer(userId));
}, [userId]);
if(!userId) return null;
return (
<NitroCardView className="nitro-mod-tools-user-visits" simple>
<NitroCardHeaderView headerText={ 'User Visits' } onCloseClick={ onCloseClick } />
<NitroCardContentView className="text-black" gap={ 1 }>
<Column gap={ 0 } overflow="hidden">
<Column gap={ 2 }>
<Grid gap={ 1 } className="text-black fw-bold border-bottom pb-1">
<Base className="g-col-2">Time</Base>
<Base className="g-col-7">Room name</Base>
<Base className="g-col-3">Visit</Base>
</Grid>
</Column>
<Column className="log-container striped-children" overflow="auto" gap={ 0 }>
{ roomVisitData &&
<AutoSizer defaultWidth={ 400 } defaultHeight={ 200 }>
{ ({ height, width }) =>
{
return (
<List
width={ width }
height={ height }
rowCount={ roomVisitData.rooms.length }
rowHeight={ 20 }
className={'log-entry-container' }
rowRenderer={ RowRenderer }
/>
);
} }
</AutoSizer> }
</Column>
</Column>
</NitroCardContentView>
</NitroCardView>
);
}

View File

@ -0,0 +1,46 @@
import { ModMessageMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useCallback, useState } from 'react';
import { Button, Text } from '../../../../common';
import { NotificationAlertEvent } from '../../../../events';
import { dispatchUiEvent, SendMessageHook } from '../../../../hooks';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
import { ISelectedUser } from '../../utils/ISelectedUser';
interface ModToolsUserSendMessageViewProps
{
user: ISelectedUser;
onCloseClick: () => void;
}
export const ModToolsUserSendMessageView: FC<ModToolsUserSendMessageViewProps> = 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, user, onCloseClick ]);
if(!user) return null;
return (
<NitroCardView className="nitro-mod-tools-user-message" simple>
<NitroCardHeaderView headerText={'Send Message'} onCloseClick={ () => onCloseClick() } />
<NitroCardContentView className="text-black">
<Text>Message To: { user.username }</Text>
<textarea className="form-control" value={ message } onChange={ event => setMessage(event.target.value) }></textarea>
<Button fullWidth onClick={ sendMessage }>Send message</Button>
</NitroCardContentView>
</NitroCardView>
);
}

View File

@ -1,13 +1,19 @@
import { FriendlyTime, GetModeratorUserInfoMessageComposer, ModeratorUserInfoData, ModeratorUserInfoEvent } from '@nitrots/nitro-renderer'; import { FriendlyTime, GetModeratorUserInfoMessageComposer, ModeratorUserInfoData, ModeratorUserInfoEvent } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { LocalizeText } from '../../../../../api'; import { LocalizeText } from '../../../../api';
import { ModToolsOpenUserChatlogEvent } from '../../../../../events/mod-tools/ModToolsOpenUserChatlogEvent'; import { Button, Column, Grid } from '../../../../common';
import { CreateMessageHook, dispatchUiEvent, SendMessageHook } from '../../../../../hooks'; import { ModToolsOpenUserChatlogEvent } from '../../../../events/mod-tools/ModToolsOpenUserChatlogEvent';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView, NitroLayoutButton, NitroLayoutGrid, NitroLayoutGridColumn } from '../../../../../layout'; import { CreateMessageHook, dispatchUiEvent, SendMessageHook } from '../../../../hooks';
import { ModToolsUserModActionView } from '../user-mod-action/ModToolsUserModActionView'; import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
import { ModToolsUserRoomVisitsView } from '../user-room-visits/ModToolsUserRoomVisitsView'; import { ModToolsUserModActionView } from './ModToolsUserModActionView';
import { ModToolsSendUserMessageView } from '../user-sendmessage/ModToolsSendUserMessageView'; import { ModToolsUserRoomVisitsView } from './ModToolsUserRoomVisitsView';
import { ModToolsUserViewProps } from './ModToolsUserView.types'; import { ModToolsUserSendMessageView } from './ModToolsUserSendMessageView';
interface ModToolsUserViewProps
{
userId: number;
onCloseClick: () => void;
}
export const ModToolsUserView: FC<ModToolsUserViewProps> = props => export const ModToolsUserView: FC<ModToolsUserViewProps> = props =>
{ {
@ -17,11 +23,6 @@ export const ModToolsUserView: FC<ModToolsUserViewProps> = props =>
const [ modActionVisible, setModActionVisible ] = useState(false); const [ modActionVisible, setModActionVisible ] = useState(false);
const [ roomVisitsVisible, setRoomVisitsVisible ] = useState(false); const [ roomVisitsVisible, setRoomVisitsVisible ] = useState(false);
useEffect(() =>
{
SendMessageHook(new GetModeratorUserInfoMessageComposer(userId));
}, [ userId ]);
const onModtoolUserInfoEvent = useCallback((event: ModeratorUserInfoEvent) => const onModtoolUserInfoEvent = useCallback((event: ModeratorUserInfoEvent) =>
{ {
const parser = event.getParser(); const parser = event.getParser();
@ -29,7 +30,7 @@ export const ModToolsUserView: FC<ModToolsUserViewProps> = props =>
if(!parser || parser.data.userId !== userId) return; if(!parser || parser.data.userId !== userId) return;
setUserInfo(parser.data); setUserInfo(parser.data);
}, [setUserInfo, userId]); }, [ userId ]);
CreateMessageHook(ModeratorUserInfoEvent, onModtoolUserInfoEvent); CreateMessageHook(ModeratorUserInfoEvent, onModtoolUserInfoEvent);
@ -98,15 +99,20 @@ export const ModToolsUserView: FC<ModToolsUserViewProps> = props =>
]; ];
}, [ userInfo ]); }, [ userInfo ]);
useEffect(() =>
{
SendMessageHook(new GetModeratorUserInfoMessageComposer(userId));
}, [ userId ]);
if(!userInfo) return null; if(!userInfo) return null;
return ( return (
<> <>
<NitroCardView className="nitro-mod-tools-user" simple={true}> <NitroCardView className="nitro-mod-tools-user" simple>
<NitroCardHeaderView headerText={ LocalizeText('modtools.userinfo.title', [ 'username' ], [ userInfo.userName ]) } onCloseClick={ () => onCloseClick() } /> <NitroCardHeaderView headerText={ LocalizeText('modtools.userinfo.title', [ 'username' ], [ userInfo.userName ]) } onCloseClick={ () => onCloseClick() } />
<NitroCardContentView className="text-black"> <NitroCardContentView className="text-black">
<NitroLayoutGrid> <Grid overflow="hidden">
<NitroLayoutGridColumn size={ 8 }> <Column size={ 8 } overflow="auto">
<table className="table table-striped table-sm table-text-small text-black m-0"> <table className="table table-striped table-sm table-text-small text-black m-0">
<tbody> <tbody>
{ userProperties.map( (property, index) => { userProperties.map( (property, index) =>
@ -117,33 +123,34 @@ export const ModToolsUserView: FC<ModToolsUserViewProps> = props =>
<th scope="row">{ LocalizeText(property.localeKey) }</th> <th scope="row">{ LocalizeText(property.localeKey) }</th>
<td> <td>
{ property.value } { property.value }
{ property.showOnline && <i className={ `icon icon-pf-${ userInfo.online ? 'online' : 'offline' } ms-2` } /> } { property.showOnline &&
<i className={ `icon icon-pf-${ userInfo.online ? 'online' : 'offline' } ms-2` } /> }
</td> </td>
</tr> </tr>
); );
}) } }) }
</tbody> </tbody>
</table> </table>
</NitroLayoutGridColumn> </Column>
<NitroLayoutGridColumn size={ 4 }> <Column size={ 4 } gap={ 1 }>
<NitroLayoutButton variant="primary" size="sm" onClick={ event => dispatchUiEvent(new ModToolsOpenUserChatlogEvent(userId)) }> <Button onClick={ event => dispatchUiEvent(new ModToolsOpenUserChatlogEvent(userId)) }>
Room Chat Room Chat
</NitroLayoutButton> </Button>
<NitroLayoutButton variant="primary" size="sm" onClick={ event => setSendMessageVisible(!sendMessageVisible) }> <Button onClick={ event => setSendMessageVisible(!sendMessageVisible) }>
Send Message Send Message
</NitroLayoutButton> </Button>
<NitroLayoutButton variant="primary" size="sm" onClick={ event => setRoomVisitsVisible(!roomVisitsVisible) }> <Button onClick={ event => setRoomVisitsVisible(!roomVisitsVisible) }>
Room Visits Room Visits
</NitroLayoutButton> </Button>
<NitroLayoutButton variant="primary" size="sm" onClick={ event => setModActionVisible(!modActionVisible) }> <Button onClick={ event => setModActionVisible(!modActionVisible) }>
Mod Action Mod Action
</NitroLayoutButton> </Button>
</NitroLayoutGridColumn> </Column>
</NitroLayoutGrid> </Grid>
</NitroCardContentView> </NitroCardContentView>
</NitroCardView> </NitroCardView>
{ sendMessageVisible && { sendMessageVisible &&
<ModToolsSendUserMessageView user={ { userId: userId, username: userInfo.userName } } onCloseClick={ () => setSendMessageVisible(false) } /> } <ModToolsUserSendMessageView user={ { userId: userId, username: userInfo.userName } } onCloseClick={ () => setSendMessageVisible(false) } /> }
{ modActionVisible && { modActionVisible &&
<ModToolsUserModActionView user={ { userId: userId, username: userInfo.userName } } onCloseClick={ () => setModActionVisible(false) } /> } <ModToolsUserModActionView user={ { userId: userId, username: userInfo.userName } } onCloseClick={ () => setModActionVisible(false) } /> }
{ roomVisitsVisible && { roomVisitsVisible &&

View File

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

View File

@ -1,23 +0,0 @@
.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

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

View File

@ -1,202 +0,0 @@
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 { SendMessageHook } from '../../../../../hooks';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../../layout';
import { NotificationAlertType } from '../../../../../views/notification-center/common/NotificationAlertType';
import { NotificationUtilities } from '../../../../../views/notification-center/common/NotificationUtilities';
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) )
{
NotificationUtilities.simpleAlert('You must select a CFH topic and Sanction', NotificationAlertType.DEFAULT, null, null, 'Error');
return;
}
if(!settings || !settings.cfhPermission)
{
NotificationUtilities.simpleAlert('You do not have permission to do this', NotificationAlertType.DEFAULT, null, null, 'Error');
return;
}
const category = topics[selectedTopic];
const sanction = actions[selectedAction];
if(!category)
{
NotificationUtilities.simpleAlert('You must select a CFH topic', NotificationAlertType.DEFAULT, null, null, 'Error');
return;
}
if(!sanction)
{
NotificationUtilities.simpleAlert('You must select a sanction', NotificationAlertType.DEFAULT, null, null, 'Error');
return;
}
const messageOrDefault = message.trim().length === 0 ? LocalizeText('help.cfh.topic.' + category.id) : message;
switch(sanction.actionType)
{
case ModActionDefinition.ALERT:
if(!settings.alertPermission)
{
NotificationUtilities.simpleAlert('You have insufficient permissions', NotificationAlertType.DEFAULT, null, null, 'Error');
return;
}
if(message.trim().length === 0)
{
NotificationUtilities.simpleAlert('Please write a message to user', NotificationAlertType.DEFAULT, null, null, 'Error');
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)
{
NotificationUtilities.simpleAlert('You have insufficient permissions', NotificationAlertType.DEFAULT, null, null, 'Error');
return;
}
SendMessageHook(new ModBanMessageComposer(user.userId, messageOrDefault, category.id, selectedAction, (sanction.actionId === 106)));
break;
case ModActionDefinition.KICK:
if(!settings.kickPermission)
{
NotificationUtilities.simpleAlert('You have insufficient permissions', NotificationAlertType.DEFAULT, null, null, 'Error');
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)
{
NotificationUtilities.simpleAlert('Please write a message to user', NotificationAlertType.DEFAULT, null, null, 'Error');
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

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

View File

@ -1,14 +0,0 @@
.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

@ -1,69 +0,0 @@
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 align-items-center">
<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-auto fw-bold text-primary" 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

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

View File

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

View File

@ -1,44 +0,0 @@
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

@ -9,6 +9,7 @@ body {
user-select: none; user-select: none;
image-rendering: pixelated; image-rendering: pixelated;
image-rendering: -moz-crisp-edges; image-rendering: -moz-crisp-edges;
scrollbar-width: thin;
} }
img { img {