diff --git a/public/configuration/UITexts.example b/public/configuration/UITexts.example index acf246e..423d2a8 100644 --- a/public/configuration/UITexts.example +++ b/public/configuration/UITexts.example @@ -98,6 +98,148 @@ "catalog.prefix.price.amount": "5 Credits", "catalog.prefix.purchased": "? Purchased!", "catalog.prefix.purchase": "Purchase", + "modtools.userinfo.title": "User Info: %username%", + "modtools.userinfo.userName": "Name", + "modtools.userinfo.cfhCount": "CFHs", + "modtools.userinfo.abusiveCfhCount": "Abusive CFHs", + "modtools.userinfo.cautionCount": "Cautions", + "modtools.userinfo.banCount": "Bans", + "modtools.userinfo.lastSanctionTime": "Last Sanction", + "modtools.userinfo.tradingLockCount": "Trade Locks", + "modtools.userinfo.tradingExpiryDate": "Lock Expires", + "modtools.userinfo.minutesSinceLastLogin": "Last Login", + "modtools.userinfo.lastPurchaseDate": "Last Purchase", + "modtools.userinfo.primaryEmailAddress": "Email", + "modtools.userinfo.identityRelatedBanCount": "Banned Accs", + "modtools.userinfo.registrationAgeInMinutes": "Registered", + "modtools.userinfo.userClassification": "Rank", + "modtools.window.title": "Mod Tools", + "modtools.window.tools.room": "Room Tool", + "modtools.window.tools.chatlog": "Chatlog Tool", + "modtools.window.tools.report": "Report Tool", + "modtools.window.select.user": "Select a user", + "modtools.window.no.room": "Enter a room first", + "modtools.window.user.in_room": "Still in this room", + "modtools.window.user.left_room": "No longer in this room", + "modtools.window.user.clear": "Clear selection", + "modtools.window.tickets.open": "%count% open ticket", + "modtools.window.tickets.open.many": "%count% open tickets", + "modtools.window.section.room": "Room", + "modtools.window.section.user": "User", + "modtools.window.section.reports": "Reports", + "modtools.window.user.open_info": "Open Info", + "modtools.userinfo.refresh": "Refresh user info", + "modtools.userinfo.presence.in_room": "In room", + "modtools.userinfo.presence.in_room.title": "In the room you are observing", + "modtools.userinfo.presence.online": "Online", + "modtools.userinfo.presence.online.title": "Online on the hotel", + "modtools.userinfo.presence.offline": "Offline", + "modtools.userinfo.presence.offline.title": "Offline at panel open", + "modtools.userinfo.section.account": "Account", + "modtools.userinfo.section.activity": "Activity", + "modtools.userinfo.section.sanctions": "Sanctions", + "modtools.userinfo.section.trading": "Trading", + "modtools.userinfo.button.room.chat": "Room Chat", + "modtools.userinfo.button.send.message": "Send Message", + "modtools.userinfo.button.room.visits": "Room Visits", + "modtools.userinfo.button.mod.action": "Mod Action", + "modtools.userinfo.stat.cfh": "CFH", + "modtools.userinfo.stat.cautions": "Cautions", + "modtools.userinfo.stat.bans": "Bans", + "modtools.userinfo.stat.trade.locks": "Trade locks", + "modtools.roominfo.title": "Room Info", + "modtools.roominfo.refresh": "Refresh room info", + "modtools.roominfo.loading": "Loading…", + "modtools.roominfo.owner.here": "Owner here", + "modtools.roominfo.owner.away": "Owner away", + "modtools.roominfo.owner.title.here": "The room owner is currently inside", + "modtools.roominfo.owner.title.away": "The room owner is NOT inside", + "modtools.roominfo.stat.users": "Users", + "modtools.roominfo.stat.owner": "Owner", + "modtools.roominfo.owner.open": "Open %username%'s info", + "modtools.roominfo.button.visit": "Visit Room", + "modtools.roominfo.button.chatlog": "Chatlog", + "modtools.roominfo.moderate.title": "Moderate room", + "modtools.roominfo.moderate.kick": "Kick everyone out", + "modtools.roominfo.moderate.doorbell": "Enable the doorbell", + "modtools.roominfo.moderate.rename": "Change room name", + "modtools.roominfo.moderate.message.placeholder": "Mandatory message to deliver with the action…", + "modtools.roominfo.moderate.send.caution": "Send Caution", + "modtools.roominfo.moderate.send.alert": "Send Alert", + "modtools.user.message.title": "Send Message", + "modtools.user.message.recipient": "Message to", + "modtools.user.message.label": "Message", + "modtools.user.message.placeholder": "Write something useful — the user will see it as a moderator message.", + "modtools.user.message.empty": "Empty", + "modtools.user.message.chars": "%count% chars", + "modtools.user.message.send": "Send Message", + "modtools.user.modaction.title": "Mod Action: %username%", + "modtools.user.modaction.sanctioning": "Sanctioning", + "modtools.user.modaction.step.topic": "1. CFH Topic", + "modtools.user.modaction.step.topic.placeholder": "Select a topic…", + "modtools.user.modaction.step.sanction": "2. Sanction", + "modtools.user.modaction.step.sanction.placeholder": "Select a sanction…", + "modtools.user.modaction.step.message": "3. Custom message", + "modtools.user.modaction.step.message.optional": "(optional — overrides default)", + "modtools.user.modaction.message.placeholder": "Leave empty to use the default topic message", + "modtools.user.modaction.preview": "Preview", + "modtools.user.modaction.button.default": "Default Sanction", + "modtools.user.modaction.button.apply": "Apply Sanction", + "modtools.user.modaction.error.no.topic": "You must select a CFH topic", + "modtools.user.modaction.error.no.action": "You must select a CFH topic and Sanction", + "modtools.user.modaction.error.no.permission": "You do not have permission to do this", + "modtools.user.modaction.error.no.message": "Please write a message to user", + "modtools.user.modaction.error.no.permission.alert": "You have insufficient permissions", + "modtools.user.visits.title": "User Visits", + "modtools.user.visits.recent": "Recent visited rooms", + "modtools.user.visits.entries.one": "%count% entry", + "modtools.user.visits.entries.many": "%count% entries", + "modtools.user.visits.empty": "No recent visits", + "modtools.user.visits.time": "Time", + "modtools.user.visits.room": "Room name", + "modtools.user.visits.action": "Action", + "modtools.user.visits.visit": "Visit", + "modtools.user.visits.visit.title": "Visit room", + "modtools.user.chatlog.title": "User Chatlog", + "modtools.user.chatlog.title.with": "User Chatlog: %username%", + "modtools.user.chatlog.loading": "Loading chatlog…", + "modtools.room.chatlog.title": "Room Chatlog", + "modtools.chatlog.column.time": "Time", + "modtools.chatlog.column.user": "User", + "modtools.chatlog.column.message": "Message", + "modtools.chatlog.empty": "No messages", + "modtools.chatlog.visit": "Visit", + "modtools.chatlog.tools": "Tools", + "modtools.tickets.title": "Tickets", + "modtools.tickets.tab.open": "Open", + "modtools.tickets.tab.mine": "Mine", + "modtools.tickets.tab.picked": "All picked", + "modtools.tickets.column.type": "Type", + "modtools.tickets.column.reported": "Reported", + "modtools.tickets.column.opened": "Opened", + "modtools.tickets.column.picker": "Picker", + "modtools.tickets.empty.open": "No open issues", + "modtools.tickets.empty.mine": "No issues picked by you", + "modtools.tickets.empty.picked": "No picked issues", + "modtools.tickets.action.pick": "Pick", + "modtools.tickets.action.handle": "Handle", + "modtools.tickets.action.release": "Release", + "modtools.tickets.issue.title": "Resolving issue #%issueId%", + "modtools.tickets.issue.label": "Issue #%issueId%", + "modtools.tickets.issue.details": "Details", + "modtools.tickets.issue.field.source": "Source", + "modtools.tickets.issue.field.category": "Category", + "modtools.tickets.issue.field.description": "Description", + "modtools.tickets.issue.field.caller": "Caller", + "modtools.tickets.issue.field.reported": "Reported", + "modtools.tickets.issue.chatlog.view": "View chatlog", + "modtools.tickets.issue.chatlog.close": "Close chatlog", + "modtools.tickets.issue.resolve.heading": "Resolve as", + "modtools.tickets.issue.resolve.resolved": "Resolved", + "modtools.tickets.issue.resolve.useless": "Useless", + "modtools.tickets.issue.resolve.abusive": "Abusive", + "modtools.tickets.issue.release": "Release back to queue", + "modtools.tickets.cfh.chatlog.title": "Issue #%issueId% Chatlog", "groupforum.list.tab.most_active": "Most active threads", "groupforum.list.tab.my_forums": "My group forums", "groupforum.list.no_forums": "There are no forums", diff --git a/src/components/mod-tools/ModToolsView.tsx b/src/components/mod-tools/ModToolsView.tsx index 181eb0c..fa602ff 100644 --- a/src/components/mod-tools/ModToolsView.tsx +++ b/src/components/mod-tools/ModToolsView.tsx @@ -1,6 +1,6 @@ import { AddLinkEventTracker, CreateLinkEvent, ILinkEventTracker, RemoveLinkEventTracker, RoomEngineEvent, RoomId, RoomObjectCategory, RoomObjectType } from '@nitrots/nitro-renderer'; import { FC, useEffect, useMemo, useRef, useState } from 'react'; -import { FaTimes } from 'react-icons/fa'; +import { FaTimes, FaUserSlash } from 'react-icons/fa'; import { GetRoomSession, ISelectedUser, LocalizeText } from '../../api'; import { Button, DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../common'; import { useModTools, useNitroEvent, useObjectSelectedEvent, useRoomUserListSnapshot } from '../../hooks'; @@ -134,63 +134,82 @@ export const ModToolsView: FC<{}> = props => return () => RemoveLinkEventTracker(linkTracker); }, [ openRoomInfo, closeRoomInfo, toggleRoomInfo, openRoomChatlog, closeRoomChatlog, toggleRoomChatlog, openUserInfo, closeUserInfo, toggleUserInfo, openUserChatlog, closeUserChatlog, toggleUserChatlog ]); - const isRoomInfoOpen = currentRoomId > 0 && openRooms.includes(currentRoomId); - const isRoomChatlogOpen = currentRoomId > 0 && openRoomChatlogs.includes(currentRoomId); + const isInRoom = currentRoomId > 0; + const isRoomInfoOpen = isInRoom && openRooms.includes(currentRoomId); + const isRoomChatlogOpen = isInRoom && openRoomChatlogs.includes(currentRoomId); const isUserInfoOpen = selectedUser && openUserInfos.includes(selectedUser.userId); - const noRoomHint = LocalizeText('mod.tools.no.room') || 'Enter a room first'; + const noRoomHint = LocalizeText('modtools.window.no.room'); return ( <> { isVisible && - - setIsVisible(false) } /> + + setIsVisible(false) } /> - - - + + + + {/* Selected user */} +
+
{ LocalizeText('modtools.window.section.user') }
{ selectedUser ? ( - <> - { selectedUser.username } - - - { - event.stopPropagation(); - setSelectedUser(null); - } } - role="button" - tabIndex={ 0 } - title="Clear selection"> - - - +
+
+ + { selectedUser.username } + +
+ +
+ ) + : ( +
+ + { LocalizeText('modtools.window.select.user') } +
) - : Select a user } - - +
+ + {/* Reports */} +
+
{ LocalizeText('modtools.window.section.reports') }
+ +
} { (openRooms.length > 0) && openRooms.map(roomId => CreateLinkEvent(`mod-tools/close-room-info/${ roomId }`) } />) } diff --git a/src/components/mod-tools/views/chatlog/ChatlogView.tsx b/src/components/mod-tools/views/chatlog/ChatlogView.tsx index 63e5201..ffd489e 100644 --- a/src/components/mod-tools/views/chatlog/ChatlogView.tsx +++ b/src/components/mod-tools/views/chatlog/ChatlogView.tsx @@ -1,7 +1,8 @@ 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 { FaCommentDots, FaDoorOpen, FaSignInAlt, FaTools } from 'react-icons/fa'; +import { LocalizeText, TryVisitRoom } from '../../../../api'; +import { Column, InfiniteScroll } from '../../../../common'; import { useModTools } from '../../../../hooks'; import { ChatlogRecord } from './ChatlogRecord'; @@ -43,46 +44,61 @@ export const ChatlogView: FC = props => return results; }, [ records ]); - const RoomInfo = (props: { roomId: number, roomName: string }) => - { - return ( - - { props.roomName } -
- - -
-
- ); - }; + const totalMessages = useMemo( + () => allRecords.filter(r => !r.isRoomInfo).length, + [ allRecords ] + ); + + const RoomInfo = (props: { roomId: number, roomName: string }) => ( +
+ +
{ props.roomName }
+
+ + +
+
+ ); + + const isEmpty = !records || records.length === 0 || totalMessages === 0; return ( - <> - - - -
Time
-
User
-
Message
-
-
- { (records && (records.length > 0)) && - - { - return ( - <> - { row.isRoomInfo && - } - { !row.isRoomInfo && - - { row.timestamp } - CreateLinkEvent(`mod-tools/open-user-info/${ row.habboId }`) }>{ row.username } - { row.message } - } - - ); - } } rows={ allRecords } /> } -
- + + {/* Column headers */} +
+
{ LocalizeText('modtools.chatlog.column.time') }
+
{ LocalizeText('modtools.chatlog.column.user') }
+
{ LocalizeText('modtools.chatlog.column.message') }
+
+ { isEmpty + ?
+ + { LocalizeText('modtools.chatlog.empty') } +
+ : + { + if(row.isRoomInfo) return ; + + return ( +
+ { row.timestamp } + + { row.message } +
+ ); + } } rows={ allRecords } /> } +
); }; diff --git a/src/components/mod-tools/views/room/ModToolsChatlogView.tsx b/src/components/mod-tools/views/room/ModToolsChatlogView.tsx index 65061be..137d225 100644 --- a/src/components/mod-tools/views/room/ModToolsChatlogView.tsx +++ b/src/components/mod-tools/views/room/ModToolsChatlogView.tsx @@ -1,7 +1,9 @@ import { ChatRecordData, GetRoomChatlogMessageComposer, RoomChatlogEvent } from '@nitrots/nitro-renderer'; -import { FC } from 'react'; -import { useNitroQuery } from '../../../../api/nitro-query'; +import { FC, useEffect, useState } from 'react'; +import { FaSpinner } from 'react-icons/fa'; +import { LocalizeText, SendMessageComposer } from '../../../../api'; import { DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common'; +import { useMessageEvent } from '../../../../hooks'; import { ChatlogView } from '../chatlog/ChatlogView'; interface ModToolsChatlogViewProps @@ -13,24 +15,32 @@ interface ModToolsChatlogViewProps export const ModToolsChatlogView: FC = props => { const { roomId = null, onCloseClick = null } = props; + const [ roomChatlog, setRoomChatlog ] = useState(null); - const { data: roomChatlog } = useNitroQuery({ - key: [ 'nitro', 'mod-tools', 'room-chatlog', roomId ], - request: () => new GetRoomChatlogMessageComposer(roomId), - parser: RoomChatlogEvent, - accept: e => e.getParser()?.data.roomId === roomId, - select: e => e.getParser().data, - enabled: roomId !== null + useMessageEvent(RoomChatlogEvent, event => + { + const parser = event.getParser(); + + if(!parser || parser.data.roomId !== roomId) return; + + setRoomChatlog(parser.data); }); - if(!roomChatlog) return null; + useEffect(() => + { + SendMessageComposer(new GetRoomChatlogMessageComposer(roomId)); + }, [ roomId ]); return ( - - - - { roomChatlog && - } + + + + { roomChatlog + ? + :
+ + { LocalizeText('modtools.user.chatlog.loading') } +
}
); diff --git a/src/components/mod-tools/views/room/ModToolsRoomView.tsx b/src/components/mod-tools/views/room/ModToolsRoomView.tsx index 37d9fc5..951413f 100644 --- a/src/components/mod-tools/views/room/ModToolsRoomView.tsx +++ b/src/components/mod-tools/views/room/ModToolsRoomView.tsx @@ -1,7 +1,8 @@ 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 { FaBullhorn, FaCommentDots, FaDoorOpen, FaExclamationTriangle, FaSignInAlt, FaSync, FaUserShield, FaUsers } from 'react-icons/fa'; +import { LocalizeText, SendMessageComposer, TryVisitRoom } from '../../../../api'; +import { Button, DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common'; import { useMessageEvent } from '../../../../hooks'; interface ModToolsRoomViewProps @@ -25,7 +26,9 @@ export const ModToolsRoomView: FC = props => const [ changeRoomName, setChangeRoomName ] = useState(false); const [ message, setMessage ] = useState(''); - const handleClick = (action: string, value?: string) => + const refresh = () => SendMessageComposer(new GetModeratorRoomInfoMessageComposer(roomId)); + + const handleClick = (action: string) => { if(!action) return; @@ -66,55 +69,102 @@ export const ModToolsRoomView: FC = props => SendMessageComposer(new GetModeratorRoomInfoMessageComposer(roomId)); setInfoRequested(true); - }, [ roomId, infoRequested, setInfoRequested ]); + }, [ roomId, infoRequested ]); + + const isLoaded = loadedRoomId !== null; + const hasMessage = message.trim().length > 0; + const ownerPillClass = ownerInRoom + ? 'bg-emerald-100 text-emerald-700 border-emerald-200' + : 'bg-zinc-100 text-zinc-600 border-zinc-200'; + const ownerDotClass = ownerInRoom ? 'bg-emerald-500' : 'bg-zinc-400'; return ( - - onCloseClick() } /> + + onCloseClick() } /> - { name && -
- { name } + {/* Identity header */} +
+ +
+ { name || LocalizeText('modtools.roominfo.loading') } + #{ roomId }
- } -
- -
- Owner: - CreateLinkEvent(`mod-tools/open-user-info/${ ownerId }`) }>{ ownerName } + + + { ownerInRoom ? LocalizeText('modtools.roominfo.owner.here') : LocalizeText('modtools.roominfo.owner.away') } + + +
+ + {/* Stat strip */} +
+
+
+ { LocalizeText('modtools.roominfo.stat.users') }
-
- Users in room: - { usersInRoom } +
{ usersInRoom }
+
+
+
+ { LocalizeText('modtools.roominfo.stat.owner') }
-
- Owner here: - { ownerInRoom ? 'Yes' : 'No' } +
ownerId && CreateLinkEvent(`mod-tools/open-user-info/${ ownerId }`) } + title={ ownerName ? LocalizeText('modtools.roominfo.owner.open', [ 'username' ], [ ownerName ]) : '' }> + { ownerName || '-' }
- -
- -
- -
+ + {/* Quick actions */} +
+ + +
+ + {/* Moderate panel */} +
+
+ { LocalizeText('modtools.roominfo.moderate.title') } +
+
-
+ { LocalizeText('modtools.roominfo.moderate.kick') } + +
-
+ { LocalizeText('modtools.roominfo.moderate.doorbell') } + + + -
- -
diff --git a/src/components/mod-tools/views/tickets/CfhChatlogView.tsx b/src/components/mod-tools/views/tickets/CfhChatlogView.tsx index 33cc52d..e80b70a 100644 --- a/src/components/mod-tools/views/tickets/CfhChatlogView.tsx +++ b/src/components/mod-tools/views/tickets/CfhChatlogView.tsx @@ -1,7 +1,9 @@ import { CfhChatlogData, CfhChatlogEvent, GetCfhChatlogMessageComposer } from '@nitrots/nitro-renderer'; -import { FC } from 'react'; -import { useNitroQuery } from '../../../../api/nitro-query'; -import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common'; +import { FC, useEffect, useState } from 'react'; +import { FaSpinner } from 'react-icons/fa'; +import { LocalizeText, SendMessageComposer } from '../../../../api'; +import { DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common'; +import { useMessageEvent } from '../../../../hooks'; import { ChatlogView } from '../chatlog/ChatlogView'; interface CfhChatlogViewProps @@ -13,21 +15,32 @@ interface CfhChatlogViewProps export const CfhChatlogView: FC = props => { const { onCloseClick = null, issueId = null } = props; + const [ chatlogData, setChatlogData ] = useState(null); - const { data: chatlogData } = useNitroQuery({ - key: [ 'nitro', 'mod-tools', 'cfh-chatlog', issueId ], - request: () => new GetCfhChatlogMessageComposer(issueId), - parser: CfhChatlogEvent, - accept: e => e.getParser()?.data.issueId === issueId, - select: e => e.getParser().data, - enabled: issueId !== null + useMessageEvent(CfhChatlogEvent, event => + { + const parser = event.getParser(); + + if(!parser || parser.data.issueId !== issueId) return; + + setChatlogData(parser.data); }); + useEffect(() => + { + SendMessageComposer(new GetCfhChatlogMessageComposer(issueId)); + }, [ issueId ]); + return ( - - - - { chatlogData && } + + + + { chatlogData + ? + :
+ + { LocalizeText('modtools.user.chatlog.loading') } +
}
); diff --git a/src/components/mod-tools/views/tickets/ModToolsIssueInfoView.tsx b/src/components/mod-tools/views/tickets/ModToolsIssueInfoView.tsx index 7444a73..02911d4 100644 --- a/src/components/mod-tools/views/tickets/ModToolsIssueInfoView.tsx +++ b/src/components/mod-tools/views/tickets/ModToolsIssueInfoView.tsx @@ -1,7 +1,8 @@ import { CloseIssuesMessageComposer, ReleaseIssuesMessageComposer } from '@nitrots/nitro-renderer'; import { FC, useState } from 'react'; +import { FaBan, FaCheck, FaCommentDots, FaExternalLinkAlt, FaSignOutAlt, FaTrashAlt } from 'react-icons/fa'; import { GetIssueCategoryName, LocalizeText, SendMessageComposer } from '../../../../api'; -import { Button, Column, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common'; +import { Button, DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common'; import { useModTools } from '../../../../hooks'; import { CfhChatlogView } from './CfhChatlogView'; @@ -11,76 +12,102 @@ interface IssueInfoViewProps onIssueInfoClosed(issueId: number): void; } +const Field: FC<{ label: string; children: React.ReactNode }> = ({ label, children }) => ( + <> +
{ label }
+
{ children || - }
+ +); + export const ModToolsIssueInfoView: FC = props => { const { issueId = null, onIssueInfoClosed = null } = props; - const [ cfhChatlogOpen, setcfhChatlogOpen ] = useState(false); + const [ cfhChatlogOpen, setCfhChatlogOpen ] = useState(false); const { tickets = [], openUserInfo = null } = useModTools(); const ticket = tickets.find(issue => (issue.issueId === issueId)); - const releaseIssue = (issueId: number) => + const releaseIssue = () => { SendMessageComposer(new ReleaseIssuesMessageComposer([ issueId ])); - onIssueInfoClosed(issueId); }; const closeIssue = (resolutionType: number) => { SendMessageComposer(new CloseIssuesMessageComposer([ issueId ], resolutionType)); - onIssueInfoClosed(issueId); }; + if(!ticket) return null; + return ( <> - - onIssueInfoClosed(issueId) } /> - - Issue Information - - - - - - - - - - - - - - - - - - - - - - - - - -
Source{ GetIssueCategoryName(ticket.categoryId) }
Category{ LocalizeText('help.cfh.topic.' + ticket.reportedCategoryId) }
Description{ ticket.message }
Caller - openUserInfo(ticket.reporterUserId) }>{ ticket.reporterUserName } -
Reported User - openUserInfo(ticket.reportedUserId) }>{ ticket.reportedUserName } -
-
- - - - - - - -
+ + onIssueInfoClosed(issueId) } /> + + {/* Issue header */} +
+ +
+
{ LocalizeText('modtools.tickets.issue.label', [ 'issueId' ], [ issueId.toString() ]) }
+
{ GetIssueCategoryName(ticket.categoryId) }
+
+ + { LocalizeText('help.cfh.topic.' + ticket.reportedCategoryId) } + +
+ + {/* Details */} +
+
{ LocalizeText('modtools.tickets.issue.details') }
+
+ { GetIssueCategoryName(ticket.categoryId) } + { LocalizeText('help.cfh.topic.' + ticket.reportedCategoryId) } + { ticket.message } + + + + + + +
+
+ + {/* Tools */} + + + {/* Resolution buttons */} +
+
{ LocalizeText('modtools.tickets.issue.resolve.heading') }
+
+ + + +
+ +
{ cfhChatlogOpen && - setcfhChatlogOpen(false) }/> } + setCfhChatlogOpen(false) } /> } ); }; diff --git a/src/components/mod-tools/views/tickets/ModToolsMyIssuesTabView.tsx b/src/components/mod-tools/views/tickets/ModToolsMyIssuesTabView.tsx index 9aaa441..e474775 100644 --- a/src/components/mod-tools/views/tickets/ModToolsMyIssuesTabView.tsx +++ b/src/components/mod-tools/views/tickets/ModToolsMyIssuesTabView.tsx @@ -1,7 +1,7 @@ import { IssueMessageData, ReleaseIssuesMessageComposer } from '@nitrots/nitro-renderer'; import { FC, useRef } from 'react'; -import { SendMessageComposer } from '../../../../api'; -import { Button, Column, Grid } from '../../../../common'; +import { FaClock, FaInbox, FaSignOutAlt, FaTools, FaUser } from 'react-icons/fa'; +import { GetIssueCategoryName, LocalizeText, SendMessageComposer } from '../../../../api'; interface ModToolsMyIssuesTabViewProps { @@ -24,35 +24,45 @@ export const ModToolsMyIssuesTabView: FC = props = setTimeout(() => pendingReleasesRef.current.delete(issueId), 2000); }; + const isEmpty = !myIssues || myIssues.length === 0; + return ( - - - -
Type
-
Room/Player
-
Opened
-
-
-
-
- - { myIssues && (myIssues.length > 0) && myIssues.map(issue => - { - return ( - -
{ issue.categoryId }
-
{ issue.reportedUserName }
-
{ new Date(Date.now() - issue.issueAgeInMilliseconds).toLocaleTimeString() }
-
- -
-
- -
-
- ); - }) } -
-
+
+
+
{ LocalizeText('modtools.tickets.column.type') }
+
{ LocalizeText('modtools.tickets.column.reported') }
+
{ LocalizeText('modtools.tickets.column.opened') }
+
+
+
+ { isEmpty + ?
+ + { LocalizeText('modtools.tickets.empty.mine') } +
+ :
+ { myIssues.map(issue => ( +
+ + { GetIssueCategoryName(issue.categoryId) } + + { issue.reportedUserName } + + { new Date(Date.now() - issue.issueAgeInMilliseconds).toLocaleTimeString() } + + + +
+ )) } +
} +
); }; diff --git a/src/components/mod-tools/views/tickets/ModToolsOpenIssuesTabView.tsx b/src/components/mod-tools/views/tickets/ModToolsOpenIssuesTabView.tsx index 387580b..a3b3c54 100644 --- a/src/components/mod-tools/views/tickets/ModToolsOpenIssuesTabView.tsx +++ b/src/components/mod-tools/views/tickets/ModToolsOpenIssuesTabView.tsx @@ -1,7 +1,7 @@ import { IssueMessageData, PickIssuesMessageComposer } from '@nitrots/nitro-renderer'; import { FC, useRef } from 'react'; -import { SendMessageComposer } from '../../../../api'; -import { Button, Column, Grid } from '../../../../common'; +import { FaClock, FaHandPointer, FaInbox, FaUser } from 'react-icons/fa'; +import { GetIssueCategoryName, LocalizeText, SendMessageComposer } from '../../../../api'; interface ModToolsOpenIssuesTabViewProps { @@ -23,31 +23,39 @@ export const ModToolsOpenIssuesTabView: FC = pro setTimeout(() => pendingPicksRef.current.delete(issueId), 2000); }; + const isEmpty = !openIssues || openIssues.length === 0; + return ( - - - -
Type
-
Room/Player
-
Opened
-
-
-
- - { openIssues && (openIssues.length > 0) && openIssues.map(issue => - { - return ( - -
{ issue.categoryId }
-
{ issue.reportedUserName }
-
{ new Date(Date.now() - issue.issueAgeInMilliseconds).toLocaleTimeString() }
-
- -
-
- ); - }) } -
-
+
+
+
{ LocalizeText('modtools.tickets.column.type') }
+
{ LocalizeText('modtools.tickets.column.reported') }
+
{ LocalizeText('modtools.tickets.column.opened') }
+
+
+ { isEmpty + ?
+ + { LocalizeText('modtools.tickets.empty.open') } +
+ :
+ { openIssues.map(issue => ( +
+ + { GetIssueCategoryName(issue.categoryId) } + + { issue.reportedUserName } + + { new Date(Date.now() - issue.issueAgeInMilliseconds).toLocaleTimeString() } + + +
+ )) } +
} +
); }; diff --git a/src/components/mod-tools/views/tickets/ModToolsPickedIssuesTabView.tsx b/src/components/mod-tools/views/tickets/ModToolsPickedIssuesTabView.tsx index ca6003e..f8c1194 100644 --- a/src/components/mod-tools/views/tickets/ModToolsPickedIssuesTabView.tsx +++ b/src/components/mod-tools/views/tickets/ModToolsPickedIssuesTabView.tsx @@ -1,6 +1,7 @@ import { IssueMessageData } from '@nitrots/nitro-renderer'; import { FC } from 'react'; -import { Column, Grid } from '../../../../common'; +import { FaClock, FaInbox, FaUser, FaUserShield } from 'react-icons/fa'; +import { GetIssueCategoryName, LocalizeText } from '../../../../api'; interface ModToolsPickedIssuesTabViewProps { @@ -10,30 +11,35 @@ interface ModToolsPickedIssuesTabViewProps export const ModToolsPickedIssuesTabView: FC = props => { const { pickedIssues = null } = props; + const isEmpty = !pickedIssues || pickedIssues.length === 0; return ( - - - -
Type
-
Room/Player
-
Opened
-
Picker
-
-
- - { pickedIssues && (pickedIssues.length > 0) && pickedIssues.map(issue => - { - return ( - -
{ issue.categoryId }
-
{ issue.reportedUserName }
-
{ new Date(Date.now() - issue.issueAgeInMilliseconds).toLocaleTimeString() }
-
{ issue.pickerUserName }
-
- ); - }) } -
-
+
+
+
{ LocalizeText('modtools.tickets.column.type') }
+
{ LocalizeText('modtools.tickets.column.reported') }
+
{ LocalizeText('modtools.tickets.column.opened') }
+
{ LocalizeText('modtools.tickets.column.picker') }
+
+ { isEmpty + ?
+ + { LocalizeText('modtools.tickets.empty.picked') } +
+ :
+ { pickedIssues.map(issue => ( +
+ + { GetIssueCategoryName(issue.categoryId) } + + { issue.reportedUserName } + + { new Date(Date.now() - issue.issueAgeInMilliseconds).toLocaleTimeString() } + + { issue.pickerUserName } +
+ )) } +
} +
); }; diff --git a/src/components/mod-tools/views/tickets/ModToolsTicketsView.tsx b/src/components/mod-tools/views/tickets/ModToolsTicketsView.tsx index ab7ac35..aa7d43f 100644 --- a/src/components/mod-tools/views/tickets/ModToolsTicketsView.tsx +++ b/src/components/mod-tools/views/tickets/ModToolsTicketsView.tsx @@ -1,5 +1,7 @@ import { GetSessionDataManager, IssueMessageData } from '@nitrots/nitro-renderer'; -import { FC, useState } from 'react'; +import { FC, useMemo, useState } from 'react'; +import { FaCheckSquare, FaListUl, FaUserCheck } from 'react-icons/fa'; +import { LocalizeText } from '../../../../api'; import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../../../common'; import { useModTools } from '../../../../hooks'; import { ModToolsIssueInfoView } from './ModToolsIssueInfoView'; @@ -12,11 +14,30 @@ interface ModToolsTicketsViewProps onCloseClick: () => void; } -const TABS: string[] = [ - 'Open Issues', - 'My Issues', - 'Picked Issues' -]; +interface TabBadgeProps +{ + label: string; + count: number; + icon: React.ReactNode; + tone: 'amber' | 'sky' | 'zinc'; +} + +const TONE_MAP: Record = { + amber: 'bg-amber-500 text-white', + sky: 'bg-sky-500 text-white', + zinc: 'bg-zinc-400 text-white' +}; + +const TabLabel: FC = ({ label, count, icon, tone }) => ( + + { icon } + { label } + { count > 0 && + + { count > 99 ? '99+' : count } + } + +); export const ModToolsTicketsView: FC = props => { @@ -25,9 +46,15 @@ export const ModToolsTicketsView: FC = props => const [ issueInfoWindows, setIssueInfoWindows ] = useState([]); 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 { openIssues, myIssues, pickedIssues } = useMemo(() => + { + const ownId = GetSessionDataManager()?.userId; + return { + openIssues: tickets.filter(issue => issue.state === IssueMessageData.STATE_OPEN), + myIssues: tickets.filter(issue => (issue.state === IssueMessageData.STATE_PICKED) && (issue.pickerUserId === ownId)), + pickedIssues: tickets.filter(issue => issue.state === IssueMessageData.STATE_PICKED) + }; + }, [ tickets ]); const closeIssue = (issueId: number) => { @@ -56,32 +83,34 @@ export const ModToolsTicketsView: FC = props => }); }; - const CurrentTabComponent = () => + const renderTab = () => { switch(currentTab) { - case 0: return ; - case 1: return ; - case 2: return ; + case 0: return ; + case 1: return ; + case 2: return ; } - return null; }; return ( <> - - + + - { TABS.map((tab, index) => - { - return ( setCurrentTab(index) }> - { tab } - ); - }) } + setCurrentTab(0) }> + } tone="amber" /> + + setCurrentTab(1) }> + } tone="sky" /> + + setCurrentTab(2) }> + } tone="zinc" /> + - + { renderTab() } { issueInfoWindows && (issueInfoWindows.length > 0) && issueInfoWindows.map(issueId => ) } diff --git a/src/components/mod-tools/views/user/ModToolsUserChatlogView.tsx b/src/components/mod-tools/views/user/ModToolsUserChatlogView.tsx index acae308..ef7622e 100644 --- a/src/components/mod-tools/views/user/ModToolsUserChatlogView.tsx +++ b/src/components/mod-tools/views/user/ModToolsUserChatlogView.tsx @@ -1,6 +1,7 @@ import { ChatRecordData, GetUserChatlogMessageComposer, UserChatlogEvent } from '@nitrots/nitro-renderer'; import { FC, useEffect, useState } from 'react'; -import { SendMessageComposer } from '../../../../api'; +import { FaSpinner } from 'react-icons/fa'; +import { LocalizeText, SendMessageComposer } from '../../../../api'; import { DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common'; import { useMessageEvent } from '../../../../hooks'; import { ChatlogView } from '../chatlog/ChatlogView'; @@ -33,11 +34,15 @@ export const ModToolsUserChatlogView: FC = props = }, [ userId ]); return ( - - - - { userChatlog && - } + + + + { userChatlog + ? + :
+ + { LocalizeText('modtools.user.chatlog.loading') } +
}
); diff --git a/src/components/mod-tools/views/user/ModToolsUserModActionView.tsx b/src/components/mod-tools/views/user/ModToolsUserModActionView.tsx index 1bea10a..76c265f 100644 --- a/src/components/mod-tools/views/user/ModToolsUserModActionView.tsx +++ b/src/components/mod-tools/views/user/ModToolsUserModActionView.tsx @@ -1,7 +1,8 @@ import { CallForHelpTopicData, DefaultSanctionMessageComposer, ModAlertMessageComposer, ModBanMessageComposer, ModKickMessageComposer, ModMessageMessageComposer, ModMuteMessageComposer, ModTradingLockMessageComposer } from '@nitrots/nitro-renderer'; import { FC, useMemo, useRef, useState } from 'react'; +import { FaBan, FaBolt, FaEnvelope, FaExclamationTriangle, FaGavel, FaUserSlash, FaVolumeMute } from 'react-icons/fa'; import { ISelectedUser, LocalizeText, ModActionDefinition, NotificationAlertType, SendMessageComposer } from '../../../../api'; -import { Button, DraggableWindowPosition, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common'; +import { Button, DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common'; import { useModTools, useNotification } from '../../../../hooks'; interface ModToolsUserModActionViewProps @@ -25,6 +26,24 @@ const MOD_ACTION_DEFINITIONS = [ new ModActionDefinition(105, 'Message', ModActionDefinition.MESSAGE, 0, 0), ]; +const ACTION_ICONS: Record = { + [ModActionDefinition.ALERT]: , + [ModActionDefinition.MUTE]: , + [ModActionDefinition.BAN]: , + [ModActionDefinition.KICK]: , + [ModActionDefinition.TRADE_LOCK]: , + [ModActionDefinition.MESSAGE]: , +}; + +const ACTION_TONE: Record = { + [ModActionDefinition.ALERT]: 'bg-amber-100 text-amber-800 border-amber-200', + [ModActionDefinition.MUTE]: 'bg-sky-100 text-sky-800 border-sky-200', + [ModActionDefinition.BAN]: 'bg-rose-100 text-rose-800 border-rose-200', + [ModActionDefinition.KICK]: 'bg-orange-100 text-orange-800 border-orange-200', + [ModActionDefinition.TRADE_LOCK]: 'bg-fuchsia-100 text-fuchsia-800 border-fuchsia-200', + [ModActionDefinition.MESSAGE]: 'bg-zinc-100 text-zinc-800 border-zinc-200', +}; + export const ModToolsUserModActionView: FC = props => { const { user = null, onCloseClick = null } = props; @@ -50,26 +69,20 @@ export const ModToolsUserModActionView: FC = pro return values; }, [ cfhCategories ]); - const sendAlert = (message: string) => simpleAlert(message, NotificationAlertType.DEFAULT, null, null, 'Error'); + const sendAlert = (m: string) => simpleAlert(m, NotificationAlertType.DEFAULT, null, null, 'Error'); const sendDefaultSanction = () => { if(isSendingRef.current) return; - let errorMessage: string = null; - const category = topics[selectedTopic]; - if(selectedTopic === -1) errorMessage = 'You must select a CFH topic'; - - if(errorMessage) return sendAlert(errorMessage); + if(selectedTopic === -1) return sendAlert(LocalizeText('modtools.user.modaction.error.no.topic')); const messageOrDefault = (message.trim().length === 0) ? LocalizeText(`help.cfh.topic.${ category.id }`) : message; isSendingRef.current = true; - SendMessageComposer(new DefaultSanctionMessageComposer(user.userId, selectedTopic, messageOrDefault)); - onCloseClick(); }; @@ -78,34 +91,22 @@ export const ModToolsUserModActionView: FC = pro if(isSendingRef.current) return; 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((selectedTopic === -1) || (selectedAction === -1)) errorMessage = LocalizeText('modtools.user.modaction.error.no.action'); + else if(!settings || !settings.cfhPermission) errorMessage = LocalizeText('modtools.user.modaction.error.no.permission'); + else if(!category) errorMessage = LocalizeText('modtools.user.modaction.error.no.topic'); + else if(!sanction) errorMessage = LocalizeText('modtools.user.modaction.error.no.action'); - if(errorMessage) - { - sendAlert(errorMessage); - - return; - } + if(errorMessage) return sendAlert(errorMessage); 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(!settings.alertPermission) return sendAlert(LocalizeText('modtools.user.modaction.error.no.permission.alert')); SendMessageComposer(new ModAlertMessageComposer(user.userId, messageOrDefault, category.id)); break; } @@ -113,72 +114,108 @@ export const ModToolsUserModActionView: FC = pro SendMessageComposer(new ModMuteMessageComposer(user.userId, messageOrDefault, category.id)); break; case ModActionDefinition.BAN: { - if(!settings.banPermission) - { - sendAlert('You have insufficient permissions'); - - return; - } - + if(!settings.banPermission) return sendAlert(LocalizeText('modtools.user.modaction.error.no.permission.alert')); 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; - } - + if(!settings.kickPermission) return sendAlert(LocalizeText('modtools.user.modaction.error.no.permission.alert')); 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; - } - + if(message.trim().length === 0) return sendAlert(LocalizeText('modtools.user.modaction.error.no.message')); SendMessageComposer(new ModMessageMessageComposer(user.userId, message, category.id)); break; } } isSendingRef.current = true; - onCloseClick(); }; if(!user) return null; + const selectedSanction = selectedAction >= 0 ? MOD_ACTION_DEFINITIONS[selectedAction] : null; + const selectedTopicName = selectedTopic >= 0 && topics[selectedTopic] ? LocalizeText('help.cfh.topic.' + topics[selectedTopic].id) : null; + const sanctionTone = selectedSanction ? ACTION_TONE[selectedSanction.actionType] : ''; + const sanctionIcon = selectedSanction ? ACTION_ICONS[selectedSanction.actionType] : null; + const canSubmit = (selectedTopic !== -1); + return ( - - onCloseClick() } /> - - - -
- Optional message type, overrides default - - + + onCloseClick() } /> + + {/* Recipient header */} +
+ +
+
{ LocalizeText('modtools.user.message.recipient') }
+
+ + { user.username } +
+
+
+ + {/* Body */} +
+ +