import { CreateLinkEvent, GetModeratorUserInfoMessageComposer, ModeratorActionResultMessageEvent, ModeratorUserInfoData, ModeratorUserInfoEvent } from '@nitrots/nitro-renderer'; import { FC, useEffect, useMemo, useState } from 'react'; import { FaBan, FaCommentDots, FaDoorOpen, FaEnvelope, FaExchangeAlt, FaExclamationTriangle, FaGavel, FaSync } from 'react-icons/fa'; import { FriendlyTime, LocalizeText, SendMessageComposer } from '../../../../api'; import { Button, DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common'; import { useMessageEvent, useRoomUserListSnapshot } from '../../../../hooks'; import { ModToolsUserModActionView } from './ModToolsUserModActionView'; import { ModToolsUserRoomVisitsView } from './ModToolsUserRoomVisitsView'; import { ModToolsUserSendMessageView } from './ModToolsUserSendMessageView'; interface ModToolsUserViewProps { userId: number; onCloseClick: () => void; } interface StatCardProps { icon: React.ReactNode; label: string; value: number | string; tone?: 'neutral' | 'warn' | 'danger'; } const StatCard: FC = ({ icon, label, value, tone = 'neutral' }) => { const numericValue = typeof value === 'number' ? value : parseInt(value as string, 10); const isElevated = !Number.isNaN(numericValue) && numericValue > 0; const toneClasses = (() => { if(tone === 'danger' && isElevated) return 'bg-rose-50 border-rose-200 text-rose-700'; if(tone === 'warn' && isElevated) return 'bg-amber-50 border-amber-200 text-amber-700'; return 'bg-zinc-50 border-zinc-200 text-zinc-700'; })(); return (
{ icon } { label }
{ value }
); }; const Section: FC<{ title: string; children: React.ReactNode }> = ({ title, children }) => (
{ title }
{ children }
); const Field: FC<{ label: string; value: React.ReactNode }> = ({ label, value }) => ( <>
{ label }
{ (value || value === 0) ? value : - }
); export const ModToolsUserView: FC = props => { const { onCloseClick = null, userId = null } = props; const [ userInfo, setUserInfo ] = useState(null); const [ sendMessageVisible, setSendMessageVisible ] = useState(false); const [ modActionVisible, setModActionVisible ] = useState(false); const [ roomVisitsVisible, setRoomVisitsVisible ] = useState(false); // Reactive presence: if the target user is currently in the room // we're observing, they're online — irrespective of what the // one-shot ModeratorUserInfoData.online said when the panel opened. const roomUserList = useRoomUserListSnapshot(); const isPresentInCurrentRoom = useMemo( () => roomUserList.some(user => user && (user.webID === userId)), [ roomUserList, userId ] ); const isOnline = isPresentInCurrentRoom || !!(userInfo && userInfo.online); const presenceLabel = isPresentInCurrentRoom ? LocalizeText('modtools.userinfo.presence.in_room') : (isOnline ? LocalizeText('modtools.userinfo.presence.online') : LocalizeText('modtools.userinfo.presence.offline')); const presenceTitle = isPresentInCurrentRoom ? LocalizeText('modtools.userinfo.presence.in_room.title') : (isOnline ? LocalizeText('modtools.userinfo.presence.online.title') : LocalizeText('modtools.userinfo.presence.offline.title')); const presencePillClass = isPresentInCurrentRoom ? 'bg-emerald-100 text-emerald-700 border-emerald-200' : isOnline ? 'bg-sky-100 text-sky-700 border-sky-200' : 'bg-zinc-100 text-zinc-600 border-zinc-200'; const presenceDotClass = isPresentInCurrentRoom ? 'bg-emerald-500' : isOnline ? 'bg-sky-500' : 'bg-zinc-400'; const refresh = () => SendMessageComposer(new GetModeratorUserInfoMessageComposer(userId)); useMessageEvent(ModeratorUserInfoEvent, event => { const parser = event.getParser(); if(!parser || parser.data.userId !== userId) return; setUserInfo(parser.data); }); // Refresh counters (cfhCount / banCount / cautionCount / // lastSanctionTime) after the moderator applies a sanction on THIS // user — otherwise the table stays frozen on the values at panel // open. Parser carries userId so we can filter precisely. useMessageEvent(ModeratorActionResultMessageEvent, event => { const parser = event.getParser(); if(!parser || !parser.success || parser.userId !== userId) return; refresh(); }); useEffect(() => { SendMessageComposer(new GetModeratorUserInfoMessageComposer(userId)); }, [ userId ]); if(!userInfo) return null; return ( <> onCloseClick() } /> {/* Identity header: name + presence pill + manual refresh */}
{ userInfo.userName } ID #{ userInfo.userId }{ userInfo.userClassification ? ` · ${ userInfo.userClassification }` : '' }
{ presenceLabel }
{/* Moderation stat strip */}
} label={ LocalizeText('modtools.userinfo.stat.cfh') } tone="warn" value={ userInfo.cfhCount } /> } label={ LocalizeText('modtools.userinfo.stat.cautions') } tone="warn" value={ userInfo.cautionCount } /> } label={ LocalizeText('modtools.userinfo.stat.bans') } tone="danger" value={ userInfo.banCount } /> } label={ LocalizeText('modtools.userinfo.stat.trade.locks') } tone="danger" value={ userInfo.tradingLockCount } />
{/* Body sections */}
{/* Action bar */}
{ sendMessageVisible && setSendMessageVisible(false) } /> } { modActionVisible && setModActionVisible(false) } /> } { roomVisitsVisible && setRoomVisitsVisible(false) } /> } ); };