User reported empty fields (Email, Last Purchase, Lock Expires, Banned
Accs, Abusive CFHs) showing what looks like a music-note glyph next to
the label. They aren't censored — they're genuinely empty (a rank-7
Administrator account has none of that data populated). The em-dash
"—" (U+2014) used as the placeholder doesn't have a glyph in the
Habbo pixel font (Volter / Volter-Goldfish), so the engine falls
through to a placeholder glyph that on some font stacks looks like a
music note.
Two-part fix in ModToolsUserView (Field), ModToolsIssueInfoView
(Field) and ModToolsRoomView (owner fallback):
1. Replace the U+2014 em-dash with a plain ASCII `-`. Hyphen-minus is
safely in Volter, so the placeholder renders correctly across the
whole client.
2. The `value || placeholder` guard is now `(value || value === 0)`.
Stat fields whose value is the literal number 0 — a clean account
with cfhCount=0, banCount=0, cautionCount=0 — were rendering the
placeholder because 0 is falsy. Treat 0 as a real value.
Also dropped the `italic` class on the placeholder span — the
hyphen does the job on its own and italic on a single-character
glyph in a pixel font was making it look like a tilted line.
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.
Replace the flat striped table with a structured layout that surfaces
the moderation signal at a glance:
Identity header
Username + ID + classification, presence pill (In room / Online /
Offline) with colour coding (emerald / sky / zinc) and a matching
dot, plus a manual refresh button. The pill source-of-truth is
useRoomUserListSnapshot for the "in room" case (reactive) falling
back to userInfo.online — tooltip discloses which path produced
the value.
Stat strip
Four counter cards in a single row — CFH, Cautions, Bans, Trade
locks — tinted warn (amber) or danger (rose) when value > 0, neutral
(zinc) when zero. Big tabular-nums numbers so the moderator sees a
problem account immediately without parsing rows.
Sectioned body
Account / Activity / Sanctions / Trading as labelled dl groups
(grid-cols-[auto_1fr]) replacing the 14-row striped table. Missing
values render as a dim em-dash instead of an empty cell.
Action bar
2×2 button grid with react-icons/fa glyphs (FaCommentDots,
FaEnvelope, FaDoorOpen, FaGavel). Mod Action keeps variant="danger"
so the destructive action stands out from the three info actions
(variant="secondary").
No behaviour changes — the same composer / event listeners /
sub-views are wired up; this is a presentation rewrite. Card grows
to min-w-[420px] max-w-[480px] to fit the new layout without
horizontal scroll on mod laptops.
ModToolsUserView used a one-shot ModeratorUserInfoData snapshot taken at
panel-open time. Two consequences:
- The online/offline icon (rendered next to userName) was frozen on the
value at open. If the target user joined/left while the panel stayed
open, the icon kept lying.
- After the moderator applied a sanction via ModToolsUserModActionView
the user info window stayed open with stale cfhCount / banCount /
cautionCount / lastSanctionTime; you had to close and reopen to see
the bump.
Fix shape mirrors the ModToolsView selected-user dot from yesterday:
- Read useRoomUserListSnapshot in the component (outside any useBetween
scope — useSyncExternalStore constraint). If the target user is in
the current room they're online; fall back to userInfo.online
otherwise. Tooltip surfaces which path produced the value.
- Subscribe to ModeratorActionResultMessageEvent (parser carries
userId + success). On a successful action targeting THIS userId,
re-send GetModeratorUserInfoMessageComposer so the table re-fetches.