mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
75815fa022
The ModTools template refresh introduced ~80 hardcoded English strings
(labels, placeholders, tooltips, empty-state copy, button text). Move
every one of them onto the modtools.* namespace and read via
LocalizeText so the panels translate alongside the rest of the client.
UITexts.example (versioned template) extended with the full set:
modtools.window.* Launcher box (toolbar item, tools,
selected-user state, ticket count)
modtools.userinfo.* User info card — already had the
legacy modtools.userinfo.{userName,
cfhCount, …} keys from before; added
refresh tooltip, presence pill labels
(in_room / online / offline with
matching .title tooltips), section
headings, action button labels, stat
card labels
modtools.roominfo.* Room info card — title, refresh, loading,
owner pill (here/away + tooltips), stat
labels, action buttons, moderate panel
heading + checkboxes + textarea
placeholder + caution/alert CTAs
modtools.user.message.* Send-message dialog (recipient label,
body label, placeholder, char counter,
empty state, send button)
modtools.user.modaction.* Mod Action form — header, sanctioning
label, 3-step section titles, select
placeholders, message label + optional
note, message placeholder, preview
heading, default/apply buttons, every
sendAlert error message
modtools.user.visits.* Room visits — title, header strip
heading, entry count (singular/plural),
empty state, column headers, visit
button + tooltip
modtools.user.chatlog.* User chatlog — title (with username
variant), loading state
modtools.room.chatlog.* Room chatlog title
modtools.chatlog.* Shared ChatlogView — column headers,
empty state, room-separator Visit/Tools
buttons
modtools.tickets.* Tickets window — title, tab labels
(open/mine/picked), column headers,
empty states, action buttons (pick/
handle/release), issue resolution
window (title, label, details heading,
field labels, chatlog toggle, resolve-as
heading, resolution buttons, release
back to queue), CFH chatlog title
The same 130 entries land in Nitro-Files/.../UITexts.json (runtime).
Both files validate as JSON. The runtime additions take effect on
next client reload; the template additions ship the strings to any
fresh deploy.
Notes:
- The MOD_ACTION_DEFINITIONS sanction names ("Alert", "Mute 1h",
"Ban 18h" …) stay hardcoded for now since they're keyed off
server-side action IDs that don't have an existing locale key
convention. Worth a follow-up if needed.
- help.cfh.topic.* keys (CFH topic display names) are already in
ExternalTexts.json and were already read via LocalizeText, so
they didn't need changes.
typecheck + vitest 214/214 + lint:hooks all clean.
223 lines
13 KiB
TypeScript
223 lines
13 KiB
TypeScript
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, NitroCardContentView, NitroCardHeaderView, NitroCardView } 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),
|
|
];
|
|
|
|
const ACTION_ICONS: Record<number, React.ReactNode> = {
|
|
[ModActionDefinition.ALERT]: <FaExclamationTriangle size={ 10 } />,
|
|
[ModActionDefinition.MUTE]: <FaVolumeMute size={ 10 } />,
|
|
[ModActionDefinition.BAN]: <FaBan size={ 10 } />,
|
|
[ModActionDefinition.KICK]: <FaUserSlash size={ 10 } />,
|
|
[ModActionDefinition.TRADE_LOCK]: <FaGavel size={ 10 } />,
|
|
[ModActionDefinition.MESSAGE]: <FaEnvelope size={ 10 } />,
|
|
};
|
|
|
|
const ACTION_TONE: Record<number, string> = {
|
|
[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<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 isSendingRef = useRef<boolean>(false);
|
|
|
|
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 = (m: string) => simpleAlert(m, NotificationAlertType.DEFAULT, null, null, 'Error');
|
|
|
|
const sendDefaultSanction = () =>
|
|
{
|
|
if(isSendingRef.current) return;
|
|
|
|
const category = topics[selectedTopic];
|
|
|
|
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();
|
|
};
|
|
|
|
const sendSanction = () =>
|
|
{
|
|
if(isSendingRef.current) return;
|
|
|
|
let errorMessage: string = null;
|
|
const category = topics[selectedTopic];
|
|
const sanction = MOD_ACTION_DEFINITIONS[selectedAction];
|
|
|
|
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) 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) return sendAlert(LocalizeText('modtools.user.modaction.error.no.permission.alert'));
|
|
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) 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) 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) 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 (
|
|
<NitroCardView className="nitro-mod-tools-user-action min-w-[420px] max-w-[460px]" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
|
|
<NitroCardHeaderView headerText={ LocalizeText('modtools.user.modaction.title', [ 'username' ], [ user.username ]) } onCloseClick={ () => onCloseClick() } />
|
|
<NitroCardContentView className="text-black" gap={ 2 }>
|
|
{/* Target header */}
|
|
<div className="flex items-center gap-2 bg-gradient-to-r from-rose-50 to-transparent rounded p-2 border border-rose-100">
|
|
<FaGavel className="text-rose-600 shrink-0" size={ 16 } />
|
|
<div className="flex flex-col grow min-w-0">
|
|
<div className="text-[.7rem] uppercase tracking-wide opacity-60 font-semibold">{ LocalizeText('modtools.user.modaction.sanctioning') }</div>
|
|
<div className="font-semibold leading-tight truncate">{ user.username }</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* CFH topic */}
|
|
<div className="flex flex-col gap-1">
|
|
<label className="text-[.7rem] uppercase tracking-wide opacity-60 font-semibold">{ LocalizeText('modtools.user.modaction.step.topic') }</label>
|
|
<select className="form-select form-select-sm" value={ selectedTopic } onChange={ event => setSelectedTopic(parseInt(event.target.value)) }>
|
|
<option disabled value={ -1 }>{ LocalizeText('modtools.user.modaction.step.topic.placeholder') }</option>
|
|
{ topics.map((topic, index) => <option key={ index } value={ index }>{ LocalizeText('help.cfh.topic.' + topic.id) }</option>) }
|
|
</select>
|
|
</div>
|
|
|
|
{/* Sanction type */}
|
|
<div className="flex flex-col gap-1">
|
|
<label className="text-[.7rem] uppercase tracking-wide opacity-60 font-semibold">{ LocalizeText('modtools.user.modaction.step.sanction') }</label>
|
|
<select className="form-select form-select-sm" value={ selectedAction } onChange={ event => setSelectedAction(parseInt(event.target.value)) }>
|
|
<option disabled value={ -1 }>{ LocalizeText('modtools.user.modaction.step.sanction.placeholder') }</option>
|
|
{ MOD_ACTION_DEFINITIONS.map((action, index) => <option key={ index } value={ index }>{ action.name }</option>) }
|
|
</select>
|
|
</div>
|
|
|
|
{/* Message */}
|
|
<div className="flex flex-col gap-1">
|
|
<label className="text-[.7rem] uppercase tracking-wide opacity-60 font-semibold">
|
|
{ LocalizeText('modtools.user.modaction.step.message') } <span className="opacity-50 normal-case font-normal">{ LocalizeText('modtools.user.modaction.step.message.optional') }</span>
|
|
</label>
|
|
<textarea
|
|
className="min-h-[60px] px-2 py-1.5 rounded text-sm border border-zinc-300 focus:outline-none focus:ring-2 focus:ring-rose-300"
|
|
placeholder={ LocalizeText('modtools.user.modaction.message.placeholder') }
|
|
value={ message }
|
|
onChange={ event => setMessage(event.target.value) }
|
|
/>
|
|
</div>
|
|
|
|
{/* Preview */}
|
|
{ (selectedSanction || selectedTopicName) &&
|
|
<div className="flex flex-col gap-1 bg-zinc-50 border border-zinc-200 rounded p-2">
|
|
<div className="text-[.7rem] uppercase tracking-wide opacity-60 font-semibold">{ LocalizeText('modtools.user.modaction.preview') }</div>
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
{ selectedTopicName &&
|
|
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium border bg-white border-zinc-200">
|
|
{ selectedTopicName }
|
|
</span> }
|
|
{ selectedSanction &&
|
|
<span className={ `inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium border ${ sanctionTone }` }>
|
|
{ sanctionIcon } { selectedSanction.name }
|
|
</span> }
|
|
</div>
|
|
</div> }
|
|
|
|
{/* Action buttons */}
|
|
<div className="flex gap-1.5 pt-1 border-t border-zinc-200">
|
|
<Button className="grow" disabled={ !canSubmit } gap={ 1 } variant="primary" onClick={ sendDefaultSanction }>
|
|
<FaBolt size={ 12 } /> { LocalizeText('modtools.user.modaction.button.default') }
|
|
</Button>
|
|
<Button className="grow" disabled={ !canSubmit || selectedAction === -1 } gap={ 1 } variant="success" onClick={ sendSanction }>
|
|
<FaGavel size={ 12 } /> { LocalizeText('modtools.user.modaction.button.apply') }
|
|
</Button>
|
|
</div>
|
|
</NitroCardContentView>
|
|
</NitroCardView>
|
|
);
|
|
};
|