🆙 Init V3

This commit is contained in:
DuckieTM
2026-01-31 09:10:52 +01:00
commit 7feb10ab15
1733 changed files with 53405 additions and 0 deletions
+147
View File
@@ -0,0 +1,147 @@
import { AddLinkEventTracker, CreateLinkEvent, ILinkEventTracker, RemoveLinkEventTracker, RoomEngineEvent, RoomId, RoomObjectCategory, RoomObjectType } from '@nitrots/nitro-renderer';
import { FC, useEffect, useRef, useState } from 'react';
import { GetRoomSession, ISelectedUser } from '../../api';
import { Button, DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../common';
import { useModTools, useNitroEvent, useObjectSelectedEvent } from '../../hooks';
import { ModToolsChatlogView } from './views/room/ModToolsChatlogView';
import { ModToolsRoomView } from './views/room/ModToolsRoomView';
import { ModToolsTicketsView } from './views/tickets/ModToolsTicketsView';
import { ModToolsUserChatlogView } from './views/user/ModToolsUserChatlogView';
import { ModToolsUserView } from './views/user/ModToolsUserView';
export const ModToolsView: FC<{}> = props =>
{
const [ isVisible, setIsVisible ] = useState(false);
const [ currentRoomId, setCurrentRoomId ] = useState<number>(-1);
const [ selectedUser, setSelectedUser ] = useState<ISelectedUser>(null);
const [ isTicketsVisible, setIsTicketsVisible ] = useState(false);
const { openRooms = [], openRoomChatlogs = [], openUserChatlogs = [], openUserInfos = [], openRoomInfo = null, closeRoomInfo = null, toggleRoomInfo = null, openRoomChatlog = null, closeRoomChatlog = null, toggleRoomChatlog = null, openUserInfo = null, closeUserInfo = null, toggleUserInfo = null, openUserChatlog = null, closeUserChatlog = null, toggleUserChatlog = null } = useModTools();
const elementRef = useRef<HTMLDivElement>(null);
useNitroEvent<RoomEngineEvent>([
RoomEngineEvent.INITIALIZED,
RoomEngineEvent.DISPOSED
], event =>
{
if(RoomId.isRoomPreviewerId(event.roomId)) return;
switch(event.type)
{
case RoomEngineEvent.INITIALIZED:
setCurrentRoomId(event.roomId);
return;
case RoomEngineEvent.DISPOSED:
setCurrentRoomId(-1);
return;
}
});
useObjectSelectedEvent(event =>
{
if(event.category !== RoomObjectCategory.UNIT) return;
const roomSession = GetRoomSession();
if(!roomSession) return;
const userData = roomSession.userDataManager.getUserDataByIndex(event.id);
if(!userData || userData.type !== RoomObjectType.USER) return;
setSelectedUser({ userId: userData.webID, username: userData.name });
});
useEffect(() =>
{
const linkTracker: ILinkEventTracker = {
linkReceived: (url: string) =>
{
const parts = url.split('/');
if(parts.length < 2) return;
switch(parts[1])
{
case 'show':
setIsVisible(true);
return;
case 'hide':
setIsVisible(false);
return;
case 'toggle':
setIsVisible(prevValue => !prevValue);
return;
case 'open-room-info':
openRoomInfo(Number(parts[2]));
return;
case 'close-room-info':
closeRoomInfo(Number(parts[2]));
return;
case 'toggle-room-info':
toggleRoomInfo(Number(parts[2]));
return;
case 'open-room-chatlog':
openRoomChatlog(Number(parts[2]));
return;
case 'close-room-chatlog':
closeRoomChatlog(Number(parts[2]));
return;
case 'toggle-room-chatlog':
toggleRoomChatlog(Number(parts[2]));
return;
case 'open-user-info':
openUserInfo(Number(parts[2]));
return;
case 'close-user-info':
closeUserInfo(Number(parts[2]));
return;
case 'toggle-user-info':
toggleUserInfo(Number(parts[2]));
return;
case 'open-user-chatlog':
openUserChatlog(Number(parts[2]));
return;
case 'close-user-chatlog':
closeUserChatlog(Number(parts[2]));
return;
case 'toggle-user-chatlog':
toggleUserChatlog(Number(parts[2]));
return;
}
},
eventUrlPrefix: 'mod-tools/'
};
AddLinkEventTracker(linkTracker);
return () => RemoveLinkEventTracker(linkTracker);
}, [ openRoomInfo, closeRoomInfo, toggleRoomInfo, openRoomChatlog, closeRoomChatlog, toggleRoomChatlog, openUserInfo, closeUserInfo, toggleUserInfo, openUserChatlog, closeUserChatlog, toggleUserChatlog ]);
return (
<>
{ isVisible &&
<NitroCardView className="nitro-mod-tools" theme="primary-slim" uniqueKey="mod-tools" windowPosition={ DraggableWindowPosition.TOP_LEFT } >
<NitroCardHeaderView headerText={ 'Mod Tools' } onCloseClick={ event => setIsVisible(false) } />
<NitroCardContentView className="text-black" gap={ 1 }>
<Button className="relative" disabled={ (currentRoomId <= 0) } gap={ 1 } onClick={ event => CreateLinkEvent(`mod-tools/toggle-room-info/${ currentRoomId }`) }>
<div className="nitro-icon icon-small-room absolute start-1" /> Room Tool
</Button>
<Button className="relative" disabled={ (currentRoomId <= 0) } gap={ 1 } innerRef={ elementRef } onClick={ event => CreateLinkEvent(`mod-tools/toggle-room-chatlog/${ currentRoomId }`) }>
<div className="nitro-icon icon-chat-history absolute start-1" /> Chatlog Tool
</Button>
<Button className="relative" disabled={ !selectedUser } gap={ 1 } onClick={ () => CreateLinkEvent(`mod-tools/toggle-user-info/${ selectedUser.userId }`) }>
<div className="nitro-icon icon-user absolute start-1" /> User: { selectedUser ? selectedUser.username : '' }
</Button>
<Button className="relative" gap={ 1 } onClick={ () => setIsTicketsVisible(prevValue => !prevValue) }>
<div className="nitro-icon icon-tickets absolute start-1" /> Report Tool
</Button>
</NitroCardContentView>
</NitroCardView> }
{ (openRooms.length > 0) && openRooms.map(roomId => <ModToolsRoomView key={ roomId } roomId={ roomId } onCloseClick={ () => CreateLinkEvent(`mod-tools/close-room-info/${ roomId }`) } />) }
{ (openRoomChatlogs.length > 0) && openRoomChatlogs.map(roomId => <ModToolsChatlogView key={ roomId } roomId={ roomId } onCloseClick={ () => CreateLinkEvent(`mod-tools/close-room-chatlog/${ roomId }`) } />) }
{ (openUserInfos.length > 0) && openUserInfos.map(userId => <ModToolsUserView key={ userId } userId={ userId } onCloseClick={ () => CreateLinkEvent(`mod-tools/close-user-info/${ userId }`) } />) }
{ (openUserChatlogs.length > 0) && openUserChatlogs.map(userId => <ModToolsUserChatlogView key={ userId } userId={ userId } onCloseClick={ () => CreateLinkEvent(`mod-tools/close-user-chatlog/${ userId }`) } />) }
{ isTicketsVisible && <ModToolsTicketsView onCloseClick={ () => setIsTicketsVisible(false) } /> }
</>
);
};
@@ -0,0 +1,11 @@
export interface ChatlogRecord
{
timestamp?: string;
habboId?: number;
username?: string;
message?: string;
hasHighlighting?: boolean;
isRoomInfo?: boolean;
roomId?: number;
roomName?: string;
}
@@ -0,0 +1,91 @@
import { ChatRecordData, CreateLinkEvent } from '@nitrots/nitro-renderer';
import { FC, useMemo } from 'react';
import { TryVisitRoom } from '../../../../api';
import { Button, Column, Flex, Grid, InfiniteScroll, Text } from '../../../../common';
import { useModTools } from '../../../../hooks';
import { ChatlogRecord } from './ChatlogRecord';
interface ChatlogViewProps
{
records: ChatRecordData[];
}
export const ChatlogView: FC<ChatlogViewProps> = props =>
{
const { records = null } = props;
const { openRoomInfo = null } = useModTools();
const allRecords = useMemo(() =>
{
const results: ChatlogRecord[] = [];
records.forEach(record =>
{
results.push({
isRoomInfo: true,
roomId: record.roomId,
roomName: record.roomName
});
record.chatlog.forEach(chatlog =>
{
results.push({
timestamp: chatlog.timestamp,
habboId: chatlog.userId,
username: chatlog.userName,
hasHighlighting: chatlog.hasHighlighting,
message: chatlog.message,
isRoomInfo: false
});
});
});
return results;
}, [ records ]);
const RoomInfo = (props: { roomId: number, roomName: string }) =>
{
return (
<Flex alignItems="center" className="bg-muted rounded p-1" gap={ 2 } justifyContent="between">
<div className="flex gap-1">
<Text bold>Room name:</Text>
<Text>{ props.roomName }</Text>
</div>
<div className="flex gap-1">
<Button onClick={ event => TryVisitRoom(props.roomId) }>Visit Room</Button>
<Button onClick={ event => openRoomInfo(props.roomId) }>Room Tools</Button>
</div>
</Flex>
);
};
return (
<>
<Column fit gap={ 0 } overflow="hidden">
<Column gap={ 2 }>
<Grid className="text-black font-bold border-bottom pb-1" gap={ 1 }>
<div className="col-span-2">Time</div>
<div className="col-span-3">User</div>
<div className="col-span-7">Message</div>
</Grid>
</Column>
{ (records && (records.length > 0)) &&
<InfiniteScroll rowRender={ (row: ChatlogRecord) =>
{
return (
<>
{ row.isRoomInfo &&
<RoomInfo roomId={ row.roomId } roomName={ row.roomName } /> }
{ !row.isRoomInfo &&
<Grid alignItems="center" className="log-entry py-1 border-bottom" fullHeight={ false } gap={ 1 }>
<Text className="col-span-2">{ row.timestamp }</Text>
<Text bold pointer underline className="col-span-3" onClick={ event => CreateLinkEvent(`mod-tools/open-user-info/${ row.habboId }`) }>{ row.username }</Text>
<Text textBreak wrap className="col-span-7">{ row.message }</Text>
</Grid> }
</>
);
} } rows={ allRecords } /> }
</Column>
</>
);
};
@@ -0,0 +1,44 @@
import { ChatRecordData, GetRoomChatlogMessageComposer, RoomChatlogEvent } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { SendMessageComposer } from '../../../../api';
import { DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common';
import { useMessageEvent } from '../../../../hooks';
import { ChatlogView } from '../chatlog/ChatlogView';
interface ModToolsChatlogViewProps
{
roomId: number;
onCloseClick: () => void;
}
export const ModToolsChatlogView: FC<ModToolsChatlogViewProps> = props =>
{
const { roomId = null, onCloseClick = null } = props;
const [ roomChatlog, setRoomChatlog ] = useState<ChatRecordData>(null);
useMessageEvent<RoomChatlogEvent>(RoomChatlogEvent, event =>
{
const parser = event.getParser();
if(!parser || parser.data.roomId !== roomId) return;
setRoomChatlog(parser.data);
});
useEffect(() =>
{
SendMessageComposer(new GetRoomChatlogMessageComposer(roomId));
}, [ roomId ]);
if(!roomChatlog) return null;
return (
<NitroCardView className="nitro-mod-tools-chatlog" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
<NitroCardHeaderView headerText={ `Room Chatlog ${ roomChatlog.roomName }` } onCloseClick={ onCloseClick } />
<NitroCardContentView className="text-black" overflow="hidden">
{ roomChatlog &&
<ChatlogView records={ [ roomChatlog ] } /> }
</NitroCardContentView>
</NitroCardView>
);
};
@@ -0,0 +1,117 @@
import { CreateLinkEvent, GetModeratorRoomInfoMessageComposer, ModerateRoomMessageComposer, ModeratorActionMessageComposer, ModeratorRoomInfoEvent } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { SendMessageComposer, TryVisitRoom } from '../../../../api';
import { Button, Column, DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
import { useMessageEvent } from '../../../../hooks';
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);
const [ kickUsers, setKickUsers ] = useState(false);
const [ lockRoom, setLockRoom ] = useState(false);
const [ changeRoomName, setChangeRoomName ] = useState(false);
const [ message, setMessage ] = useState('');
const handleClick = (action: string, value?: string) =>
{
if(!action) return;
switch(action)
{
case 'alert_only':
if(message.trim().length === 0) return;
SendMessageComposer(new ModeratorActionMessageComposer(ModeratorActionMessageComposer.ACTION_ALERT, message, ''));
SendMessageComposer(new ModerateRoomMessageComposer(roomId, lockRoom ? 1 : 0, changeRoomName ? 1 : 0, kickUsers ? 1 : 0));
return;
case 'send_message':
if(message.trim().length === 0) return;
SendMessageComposer(new ModeratorActionMessageComposer(ModeratorActionMessageComposer.ACTION_MESSAGE, message, ''));
SendMessageComposer(new ModerateRoomMessageComposer(roomId, lockRoom ? 1 : 0, changeRoomName ? 1 : 0, kickUsers ? 1 : 0));
return;
}
};
useMessageEvent<ModeratorRoomInfoEvent>(ModeratorRoomInfoEvent, event =>
{
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);
});
useEffect(() =>
{
if(infoRequested) return;
SendMessageComposer(new GetModeratorRoomInfoMessageComposer(roomId));
setInfoRequested(true);
}, [ roomId, infoRequested, setInfoRequested ]);
return (
<NitroCardView className="nitro-mod-tools-room" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
<NitroCardHeaderView headerText={ 'Room Info' + (name ? ': ' + name : '') } onCloseClick={ event => onCloseClick() } />
<NitroCardContentView className="text-black">
<div className="flex gap-2">
<Column grow gap={ 1 } justifyContent="center">
<div className="items-center gap-2">
<Text bold align="end" className="col-span-7">Room Owner:</Text>
<Text pointer truncate underline>{ ownerName }</Text>
</div>
<div className="items-center gap-2">
<Text bold align="end" className="col-span-7">Users in room:</Text>
<Text>{ usersInRoom }</Text>
</div>
<div className="items-center gap-2">
<Text bold align="end" className="col-span-7">Owner in room:</Text>
<Text>{ ownerInRoom ? 'Yes' : 'No' }</Text>
</div>
</Column>
<div className="flex flex-col gap-1">
<Button onClick={ event => TryVisitRoom(roomId) }>Visit Room</Button>
<Button onClick={ event => CreateLinkEvent(`mod-tools/open-room-chatlog/${ roomId }`) }>Chatlog</Button>
</div>
</div>
<Column className="bg-muted rounded p-2" gap={ 1 }>
<div className="flex items-center gap-1">
<input checked={ kickUsers } className="form-check-input" type="checkbox" onChange={ event => setKickUsers(event.target.checked) } />
<Text small>Kick everyone out</Text>
</div>
<div className="flex items-center gap-1">
<input checked={ lockRoom } className="form-check-input" type="checkbox" onChange={ event => setLockRoom(event.target.checked) } />
<Text small>Enable the doorbell</Text>
</div>
<div className="flex items-center gap-1">
<input checked={ changeRoomName } className="form-check-input" type="checkbox" onChange={ event => setChangeRoomName(event.target.checked) } />
<Text small>Change room name</Text>
</div>
</Column>
<textarea className="min-h-[calc(1.5em+ .5rem+2px)] px-[.5rem] py-[.25rem] rounded-[.2rem]" placeholder="Type a mandatory message to the users in this text box..." value={ message } onChange={ event => setMessage(event.target.value) }></textarea>
<div className="flex justify-between">
<Button variant="danger" onClick={ event => handleClick('send_message') }>Send Caution</Button>
<Button onClick={ event => handleClick('alert_only') }>Send Alert only</Button>
</div>
</NitroCardContentView>
</NitroCardView>
);
};
@@ -0,0 +1,41 @@
import { CfhChatlogData, CfhChatlogEvent, GetCfhChatlogMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { SendMessageComposer } from '../../../../api';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common';
import { useMessageEvent } from '../../../../hooks';
import { ChatlogView } from '../chatlog/ChatlogView';
interface CfhChatlogViewProps
{
issueId: number;
onCloseClick(): void;
}
export const CfhChatlogView: FC<CfhChatlogViewProps> = props =>
{
const { onCloseClick = null, issueId = null } = props;
const [ chatlogData, setChatlogData ] = useState<CfhChatlogData>(null);
useMessageEvent<CfhChatlogEvent>(CfhChatlogEvent, event =>
{
const parser = event.getParser();
if(!parser || parser.data.issueId !== issueId) return;
setChatlogData(parser.data);
});
useEffect(() =>
{
SendMessageComposer(new GetCfhChatlogMessageComposer(issueId));
}, [ issueId ]);
return (
<NitroCardView className="nitro-mod-tools-chatlog" theme="primary-slim">
<NitroCardHeaderView headerText={ 'Issue Chatlog' } onCloseClick={ onCloseClick } />
<NitroCardContentView className="text-black">
{ chatlogData && <ChatlogView records={ [ chatlogData.chatRecord ] } /> }
</NitroCardContentView>
</NitroCardView>
);
};
@@ -0,0 +1,86 @@
import { CloseIssuesMessageComposer, ReleaseIssuesMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useState } from 'react';
import { GetIssueCategoryName, LocalizeText, SendMessageComposer } from '../../../../api';
import { Button, Column, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
import { useModTools } from '../../../../hooks';
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 [ cfhChatlogOpen, setcfhChatlogOpen ] = useState(false);
const { tickets = [], openUserInfo = null } = useModTools();
const ticket = tickets.find(issue => (issue.issueId === issueId));
const releaseIssue = (issueId: number) =>
{
SendMessageComposer(new ReleaseIssuesMessageComposer([ issueId ]));
onIssueInfoClosed(issueId);
};
const closeIssue = (resolutionType: number) =>
{
SendMessageComposer(new CloseIssuesMessageComposer([ issueId ], resolutionType));
onIssueInfoClosed(issueId);
};
return (
<>
<NitroCardView className="nitro-mod-tools-handle-issue" theme="primary-slim">
<NitroCardHeaderView headerText={ 'Resolving issue ' + issueId } onCloseClick={ () => onIssueInfoClosed(issueId) } />
<NitroCardContentView className="text-black">
<Text fontSize={ 4 }>Issue Information</Text>
<Grid overflow="auto">
<Column size={ 8 }>
<table className="table table-striped table-sm table-text-small text-black m-0">
<tbody>
<tr>
<th>Source</th>
<td>{ GetIssueCategoryName(ticket.categoryId) }</td>
</tr>
<tr>
<th>Category</th>
<td className="text-break">{ LocalizeText('help.cfh.topic.' + ticket.reportedCategoryId) }</td>
</tr>
<tr>
<th>Description</th>
<td className="text-break">{ ticket.message }</td>
</tr>
<tr>
<th>Caller</th>
<td>
<Text bold pointer underline onClick={ event => openUserInfo(ticket.reporterUserId) }>{ ticket.reporterUserName }</Text>
</td>
</tr>
<tr>
<th>Reported User</th>
<td>
<Text bold pointer underline onClick={ event => openUserInfo(ticket.reportedUserId) }>{ ticket.reportedUserName }</Text>
</td>
</tr>
</tbody>
</table>
</Column>
<Column gap={ 1 } size={ 4 }>
<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) }/> }
</>
);
};
@@ -0,0 +1,47 @@
import { IssueMessageData, ReleaseIssuesMessageComposer } from '@nitrots/nitro-renderer';
import { FC } from 'react';
import { SendMessageComposer } from '../../../../api';
import { Button, Column, Grid } from '../../../../common';
interface ModToolsMyIssuesTabViewProps
{
myIssues: IssueMessageData[];
handleIssue: (issueId: number) => void;
}
export const ModToolsMyIssuesTabView: FC<ModToolsMyIssuesTabViewProps> = props =>
{
const { myIssues = null, handleIssue = null } = props;
return (
<Column gap={ 0 } overflow="hidden">
<Column gap={ 2 }>
<Grid className="text-black font-bold border-bottom pb-1" gap={ 1 }>
<div className="col-span-2">Type</div>
<div className="col-span-3">Room/Player</div>
<div className="col-span-3">Opened</div>
<div className="col-span-2"></div>
<div className="col-span-2"></div>
</Grid>
</Column>
<Column className="striped-children" gap={ 0 } overflow="auto">
{ myIssues && (myIssues.length > 0) && myIssues.map(issue =>
{
return (
<Grid key={ issue.issueId } alignItems="center" className="text-black py-1 border-bottom" gap={ 1 }>
<div className="col-span-2">{ issue.categoryId }</div>
<div className="col-span-3">{ issue.reportedUserName }</div>
<div className="col-span-3">{ new Date(Date.now() - issue.issueAgeInMilliseconds).toLocaleTimeString() }</div>
<div className="col-span-2">
<Button variant="primary" onClick={ event => handleIssue(issue.issueId) }>Handle</Button>
</div>
<div className="col-span-2">
<Button variant="danger" onClick={ event => SendMessageComposer(new ReleaseIssuesMessageComposer([ issue.issueId ])) }>Release</Button>
</div>
</Grid>
);
}) }
</Column>
</Column>
);
};
@@ -0,0 +1,42 @@
import { IssueMessageData, PickIssuesMessageComposer } from '@nitrots/nitro-renderer';
import { FC } from 'react';
import { SendMessageComposer } from '../../../../api';
import { Button, Column, Grid } from '../../../../common';
interface ModToolsOpenIssuesTabViewProps
{
openIssues: IssueMessageData[];
}
export const ModToolsOpenIssuesTabView: FC<ModToolsOpenIssuesTabViewProps> = props =>
{
const { openIssues = null } = props;
return (
<Column gap={ 0 } overflow="hidden">
<Column gap={ 2 }>
<Grid className="text-black font-bold border-bottom pb-1" gap={ 1 }>
<div className="col-span-2">Type</div>
<div className="col-span-3">Room/Player</div>
<div className="col-span-4">Opened</div>
<div className="col-span-3"></div>
</Grid>
</Column>
<Column className="striped-children" gap={ 0 } overflow="auto">
{ openIssues && (openIssues.length > 0) && openIssues.map(issue =>
{
return (
<Grid key={ issue.issueId } alignItems="center" className="text-black py-1 border-bottom" gap={ 1 }>
<div className="col-span-2">{ issue.categoryId }</div>
<div className="col-span-3">{ issue.reportedUserName }</div>
<div className="col-span-4">{ new Date(Date.now() - issue.issueAgeInMilliseconds).toLocaleTimeString() }</div>
<div className="col-span-3">
<Button variant="success" onClick={ event => SendMessageComposer(new PickIssuesMessageComposer([ issue.issueId ], false, 0, 'pick issue button')) }>Pick Issue</Button>
</div>
</Grid>
);
}) }
</Column>
</Column>
);
};
@@ -0,0 +1,39 @@
import { IssueMessageData } from '@nitrots/nitro-renderer';
import { FC } from 'react';
import { 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 className="text-black font-bold border-bottom pb-1" gap={ 1 }>
<div className="col-span-2">Type</div>
<div className="col-span-3">Room/Player</div>
<div className="col-span-4">Opened</div>
<div className="col-span-3">Picker</div>
</Grid>
</Column>
<Column className="striped-children" gap={ 0 } overflow="auto">
{ pickedIssues && (pickedIssues.length > 0) && pickedIssues.map(issue =>
{
return (
<Grid key={ issue.issueId } alignItems="center" className="text-black py-1 border-bottom" gap={ 1 }>
<div className="col-span-2">{ issue.categoryId }</div>
<div className="col-span-3">{ issue.reportedUserName }</div>
<div className="col-span-4">{ new Date(Date.now() - issue.issueAgeInMilliseconds).toLocaleTimeString() }</div>
<div className="col-span-3">{ issue.pickerUserName }</div>
</Grid>
);
}) }
</Column>
</Column>
);
};
@@ -0,0 +1,90 @@
import { GetSessionDataManager, IssueMessageData } from '@nitrots/nitro-renderer';
import { FC, useState } from 'react';
import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../../../common';
import { useModTools } from '../../../../hooks';
import { ModToolsIssueInfoView } from './ModToolsIssueInfoView';
import { ModToolsMyIssuesTabView } from './ModToolsMyIssuesTabView';
import { ModToolsOpenIssuesTabView } from './ModToolsOpenIssuesTabView';
import { ModToolsPickedIssuesTabView } from './ModToolsPickedIssuesTabView';
interface ModToolsTicketsViewProps
{
onCloseClick: () => void;
}
const TABS: string[] = [
'Open Issues',
'My Issues',
'Picked Issues'
];
export const ModToolsTicketsView: FC<ModToolsTicketsViewProps> = props =>
{
const { onCloseClick = null } = props;
const [ currentTab, setCurrentTab ] = useState<number>(0);
const [ issueInfoWindows, setIssueInfoWindows ] = useState<number[]>([]);
const { tickets = [] } = useModTools();
const openIssues = tickets.filter(issue => issue.state === IssueMessageData.STATE_OPEN);
const myIssues = tickets.filter(issue => (issue.state === IssueMessageData.STATE_PICKED) && (issue.pickerUserId === GetSessionDataManager().userId));
const pickedIssues = tickets.filter(issue => issue.state === IssueMessageData.STATE_PICKED);
const closeIssue = (issueId: number) =>
{
setIssueInfoWindows(prevValue =>
{
const newValue = [ ...prevValue ];
const existingIndex = newValue.indexOf(issueId);
if(existingIndex >= 0) newValue.splice(existingIndex, 1);
return newValue;
});
};
const handleIssue = (issueId: number) =>
{
setIssueInfoWindows(prevValue =>
{
const newValue = [ ...prevValue ];
const existingIndex = newValue.indexOf(issueId);
if(existingIndex === -1) newValue.push(issueId);
else newValue.splice(existingIndex, 1);
return newValue;
});
};
const CurrentTabComponent = () =>
{
switch(currentTab)
{
case 0: return <ModToolsOpenIssuesTabView openIssues={ openIssues }/>;
case 1: return <ModToolsMyIssuesTabView handleIssue={ handleIssue } myIssues={ myIssues }/>;
case 2: return <ModToolsPickedIssuesTabView pickedIssues={ pickedIssues }/>;
}
return null;
};
return (
<>
<NitroCardView className="nitro-mod-tools-tickets">
<NitroCardHeaderView headerText={ 'Tickets' } onCloseClick={ onCloseClick } />
<NitroCardTabsView>
{ TABS.map((tab, index) =>
{
return (<NitroCardTabsItemView key={ index } isActive={ (currentTab === index) } onClick={ event => setCurrentTab(index) }>
{ tab }
</NitroCardTabsItemView>);
}) }
</NitroCardTabsView>
<NitroCardContentView gap={ 1 }>
<CurrentTabComponent />
</NitroCardContentView>
</NitroCardView>
{ issueInfoWindows && (issueInfoWindows.length > 0) && issueInfoWindows.map(issueId => <ModToolsIssueInfoView key={ issueId } issueId={ issueId } onIssueInfoClosed={ closeIssue } />) }
</>
);
};
@@ -0,0 +1,44 @@
import { ChatRecordData, GetUserChatlogMessageComposer, UserChatlogEvent } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { SendMessageComposer } from '../../../../api';
import { DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common';
import { useMessageEvent } from '../../../../hooks';
import { ChatlogView } from '../chatlog/ChatlogView';
interface ModToolsUserChatlogViewProps
{
userId: number;
onCloseClick: () => void;
}
export const ModToolsUserChatlogView: FC<ModToolsUserChatlogViewProps> = props =>
{
const { userId = null, onCloseClick = null } = props;
const [ userChatlog, setUserChatlog ] = useState<ChatRecordData[]>(null);
const [ username, setUsername ] = useState<string>(null);
useMessageEvent<UserChatlogEvent>(UserChatlogEvent, event =>
{
const parser = event.getParser();
if(!parser || parser.data.userId !== userId) return;
setUsername(parser.data.username);
setUserChatlog(parser.data.roomChatlogs);
});
useEffect(() =>
{
SendMessageComposer(new GetUserChatlogMessageComposer(userId));
}, [ userId ]);
return (
<NitroCardView className="nitro-mod-tools-chatlog" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
<NitroCardHeaderView headerText={ `User Chatlog: ${ username || '' }` } onCloseClick={ onCloseClick } />
<NitroCardContentView className="text-black h-full">
{ userChatlog &&
<ChatlogView records={ userChatlog } /> }
</NitroCardContentView>
</NitroCardView>
);
};
@@ -0,0 +1,176 @@
import { CallForHelpTopicData, DefaultSanctionMessageComposer, ModAlertMessageComposer, ModBanMessageComposer, ModKickMessageComposer, ModMessageMessageComposer, ModMuteMessageComposer, ModTradingLockMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useMemo, useState } from 'react';
import { ISelectedUser, LocalizeText, ModActionDefinition, NotificationAlertType, SendMessageComposer } from '../../../../api';
import { Button, DraggableWindowPosition, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
import { useModTools, useNotification } from '../../../../hooks';
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(3, 'Ban 18h', ModActionDefinition.BAN, 3, 0),
new ModActionDefinition(4, 'Ban 7 days', ModActionDefinition.BAN, 4, 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 { cfhCategories = null, settings = null } = useModTools();
const { simpleAlert = null } = useNotification();
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) => simpleAlert(message, NotificationAlertType.DEFAULT, null, null, 'Error');
const sendDefaultSanction = () =>
{
let errorMessage: string = null;
const category = topics[selectedTopic];
if(selectedTopic === -1) errorMessage = 'You must select a CFH topic';
if(errorMessage) return sendAlert(errorMessage);
const messageOrDefault = (message.trim().length === 0) ? LocalizeText(`help.cfh.topic.${ category.id }`) : message;
SendMessageComposer(new DefaultSanctionMessageComposer(user.userId, selectedTopic, messageOrDefault));
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(errorMessage);
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;
}
SendMessageComposer(new ModAlertMessageComposer(user.userId, messageOrDefault, category.id));
break;
}
case ModActionDefinition.MUTE:
SendMessageComposer(new ModMuteMessageComposer(user.userId, messageOrDefault, category.id));
break;
case ModActionDefinition.BAN: {
if(!settings.banPermission)
{
sendAlert('You have insufficient permissions');
return;
}
SendMessageComposer(new ModBanMessageComposer(user.userId, messageOrDefault, category.id, selectedAction, (sanction.actionId === 106)));
break;
}
case ModActionDefinition.KICK: {
if(!settings.kickPermission)
{
sendAlert('You have insufficient permissions');
return;
}
SendMessageComposer(new ModKickMessageComposer(user.userId, messageOrDefault, category.id));
break;
}
case ModActionDefinition.TRADE_LOCK: {
const numSeconds = (sanction.actionLengthHours * 60);
SendMessageComposer(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;
}
SendMessageComposer(new ModMessageMessageComposer(user.userId, message, category.id));
break;
}
}
onCloseClick();
};
if(!user) return null;
return (
<NitroCardView className="nitro-mod-tools-user-action" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
<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 disabled value={ -1 }>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 disabled value={ -1 }>Sanction Type</option>
{ MOD_ACTION_DEFINITIONS.map((action, index) => <option key={ index } value={ index }>{ action.name }</option>) }
</select>
<div className="flex flex-col gap-1">
<Text small>Optional message type, overrides default</Text>
<textarea className="min-h-[calc(1.5em+ .5rem+2px)] px-[.5rem] py-[.25rem] rounded-[.2rem]" value={ message } onChange={ event => setMessage(event.target.value) } />
</div>
<Flex gap={ 1 } justifyContent="between">
<Button variant="primary" onClick={ sendDefaultSanction }>Default Sanction</Button>
<Button variant="success" onClick={ sendSanction }>Sanction</Button>
</Flex>
</NitroCardContentView>
</NitroCardView>
);
};
@@ -0,0 +1,60 @@
import { GetRoomVisitsMessageComposer, RoomVisitsData, RoomVisitsEvent } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { SendMessageComposer, TryVisitRoom } from '../../../../api';
import { Column, DraggableWindowPosition, Grid, InfiniteScroll, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
import { useMessageEvent } from '../../../../hooks';
interface ModToolsUserRoomVisitsViewProps
{
userId: number;
onCloseClick: () => void;
}
export const ModToolsUserRoomVisitsView: FC<ModToolsUserRoomVisitsViewProps> = props =>
{
const { userId = null, onCloseClick = null } = props;
const [ roomVisitData, setRoomVisitData ] = useState<RoomVisitsData>(null);
useMessageEvent<RoomVisitsEvent>(RoomVisitsEvent, event =>
{
const parser = event.getParser();
if(parser.data.userId !== userId) return;
setRoomVisitData(parser.data);
});
useEffect(() =>
{
SendMessageComposer(new GetRoomVisitsMessageComposer(userId));
}, [ userId ]);
if(!userId) return null;
return (
<NitroCardView className="nitro-mod-tools-user-visits" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
<NitroCardHeaderView headerText={ 'User Visits' } onCloseClick={ onCloseClick } />
<NitroCardContentView className="text-black" gap={ 1 }>
<Column fullHeight gap={ 0 } overflow="hidden">
<Column gap={ 2 }>
<Grid className="text-black font-bold border-bottom pb-1" gap={ 1 }>
<div className="col-span-2">Time</div>
<div className="col-span-7">Room name</div>
<div className="col-span-3">Visit</div>
</Grid>
</Column>
<InfiniteScroll rowRender={ row =>
{
return (
<Grid alignItems="center" className="text-black py-1 border-bottom" fullHeight={ false } gap={ 1 }>
<Text className="col-span-2">{ row.enterHour.toString().padStart(2, '0') }: { row.enterMinute.toString().padStart(2, '0') }</Text>
<Text className="col-span-7">{ row.roomName }</Text>
<Text bold pointer underline className="col-span-3" variant="primary" onClick={ event => TryVisitRoom(row.roomId) }>Visit Room</Text>
</Grid>
);
} } rows={ roomVisitData?.rooms ?? [] } />
</Column>
</NitroCardContentView>
</NitroCardView>
);
};
@@ -0,0 +1,45 @@
import { ModMessageMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useState } from 'react';
import { ISelectedUser, SendMessageComposer } from '../../../../api';
import { Button, DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
import { useNotification } from '../../../../hooks';
interface ModToolsUserSendMessageViewProps
{
user: ISelectedUser;
onCloseClick: () => void;
}
export const ModToolsUserSendMessageView: FC<ModToolsUserSendMessageViewProps> = props =>
{
const { user = null, onCloseClick = null } = props;
const [ message, setMessage ] = useState('');
const { simpleAlert = null } = useNotification();
if(!user) return null;
const sendMessage = () =>
{
if(message.trim().length === 0)
{
simpleAlert('Please write a message to user.', null, null, null, 'Error', null);
return;
}
SendMessageComposer(new ModMessageMessageComposer(user.userId, message, -999));
onCloseClick();
};
return (
<NitroCardView className="nitro-mod-tools-user-message" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
<NitroCardHeaderView headerText={ 'Send Message' } onCloseClick={ () => onCloseClick() } />
<NitroCardContentView className="text-black">
<Text>Message To: { user.username }</Text>
<textarea className="min-h-[calc(1.5em+ .5rem+2px)] px-[.5rem] py-[.25rem] rounded-[.2rem]" value={ message } onChange={ event => setMessage(event.target.value) }></textarea>
<Button fullWidth onClick={ sendMessage }>Send message</Button>
</NitroCardContentView>
</NitroCardView>
);
};
@@ -0,0 +1,156 @@
import { CreateLinkEvent, GetModeratorUserInfoMessageComposer, ModeratorUserInfoData, ModeratorUserInfoEvent } from '@nitrots/nitro-renderer';
import { FC, useEffect, useMemo, useState } from 'react';
import { FriendlyTime, LocalizeText, SendMessageComposer } from '../../../../api';
import { Button, Column, DraggableWindowPosition, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common';
import { useMessageEvent } from '../../../../hooks';
import { ModToolsUserModActionView } from './ModToolsUserModActionView';
import { ModToolsUserRoomVisitsView } from './ModToolsUserRoomVisitsView';
import { ModToolsUserSendMessageView } from './ModToolsUserSendMessageView';
interface ModToolsUserViewProps
{
userId: number;
onCloseClick: () => void;
}
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);
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 ]);
useMessageEvent<ModeratorUserInfoEvent>(ModeratorUserInfoEvent, event =>
{
const parser = event.getParser();
if(!parser || parser.data.userId !== userId) return;
setUserInfo(parser.data);
});
useEffect(() =>
{
SendMessageComposer(new GetModeratorUserInfoMessageComposer(userId));
}, [ userId ]);
if(!userInfo) return null;
return (
<>
<NitroCardView className="nitro-mod-tools-user" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
<NitroCardHeaderView headerText={ LocalizeText('modtools.userinfo.title', [ 'username' ], [ userInfo.userName ]) } onCloseClick={ () => onCloseClick() } />
<NitroCardContentView className="text-black">
<Grid overflow="hidden">
<Column overflow="auto" 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>
</Column>
<Column gap={ 1 } size={ 4 }>
<Button onClick={ event => CreateLinkEvent(`mod-tools/open-user-chatlog/${ userId }`) }>
Room Chat
</Button>
<Button onClick={ event => setSendMessageVisible(!sendMessageVisible) }>
Send Message
</Button>
<Button onClick={ event => setRoomVisitsVisible(!roomVisitsVisible) }>
Room Visits
</Button>
<Button onClick={ event => setModActionVisible(!modActionVisible) }>
Mod Action
</Button>
</Column>
</Grid>
</NitroCardContentView>
</NitroCardView>
{ sendMessageVisible &&
<ModToolsUserSendMessageView 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) } /> }
</>
);
};