Files
Nitro-V3/src/components/mod-tools/views/tickets/ModToolsIssueInfoView.tsx
T
simoleo89 46daa96100 fix(mod-tools): empty-value placeholder no longer renders as music note
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.
2026-05-20 21:41:52 +02:00

114 lines
7.1 KiB
TypeScript

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, DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common';
import { useModTools } from '../../../../hooks';
import { CfhChatlogView } from './CfhChatlogView';
interface IssueInfoViewProps
{
issueId: number;
onIssueInfoClosed(issueId: number): void;
}
const Field: FC<{ label: string; children: React.ReactNode }> = ({ label, children }) => (
<>
<dt className="opacity-60 whitespace-nowrap">{ label }</dt>
<dd className="m-0 break-words font-medium">{ children || <span className="opacity-40">-</span> }</dd>
</>
);
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 = () =>
{
SendMessageComposer(new ReleaseIssuesMessageComposer([ issueId ]));
onIssueInfoClosed(issueId);
};
const closeIssue = (resolutionType: number) =>
{
SendMessageComposer(new CloseIssuesMessageComposer([ issueId ], resolutionType));
onIssueInfoClosed(issueId);
};
if(!ticket) return null;
return (
<>
<NitroCardView className="nitro-mod-tools-handle-issue min-w-[440px] max-w-[500px]" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
<NitroCardHeaderView headerText={ LocalizeText('modtools.tickets.issue.title', [ 'issueId' ], [ issueId.toString() ]) } onCloseClick={ () => onIssueInfoClosed(issueId) } />
<NitroCardContentView className="text-black" gap={ 2 }>
{/* Issue header */}
<div className="flex items-center gap-2 bg-gradient-to-r from-amber-50 to-transparent rounded p-2 border border-amber-100">
<FaCommentDots className="text-amber-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.tickets.issue.label', [ 'issueId' ], [ issueId.toString() ]) }</div>
<div className="font-semibold leading-tight truncate">{ GetIssueCategoryName(ticket.categoryId) }</div>
</div>
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium border bg-white border-amber-200 text-amber-800">
{ LocalizeText('help.cfh.topic.' + ticket.reportedCategoryId) }
</span>
</div>
{/* Details */}
<div className="flex flex-col gap-1">
<div className="text-[.7rem] uppercase tracking-wide opacity-60 font-semibold border-b border-zinc-200 pb-1 mb-0.5">{ LocalizeText('modtools.tickets.issue.details') }</div>
<dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-[.8rem] m-0">
<Field label={ LocalizeText('modtools.tickets.issue.field.source') }>{ GetIssueCategoryName(ticket.categoryId) }</Field>
<Field label={ LocalizeText('modtools.tickets.issue.field.category') }>{ LocalizeText('help.cfh.topic.' + ticket.reportedCategoryId) }</Field>
<Field label={ LocalizeText('modtools.tickets.issue.field.description') }>{ ticket.message }</Field>
<Field label={ LocalizeText('modtools.tickets.issue.field.caller') }>
<button
className="font-semibold text-sky-700 hover:text-sky-900 hover:underline inline-flex items-center gap-1"
onClick={ () => openUserInfo(ticket.reporterUserId) }>
{ ticket.reporterUserName } <FaExternalLinkAlt size={ 8 } className="opacity-60" />
</button>
</Field>
<Field label={ LocalizeText('modtools.tickets.issue.field.reported') }>
<button
className="font-semibold text-sky-700 hover:text-sky-900 hover:underline inline-flex items-center gap-1"
onClick={ () => openUserInfo(ticket.reportedUserId) }>
{ ticket.reportedUserName } <FaExternalLinkAlt size={ 8 } className="opacity-60" />
</button>
</Field>
</dl>
</div>
{/* Tools */}
<Button gap={ 1 } variant="secondary" onClick={ () => setCfhChatlogOpen(prev => !prev) }>
<FaCommentDots size={ 12 } /> { cfhChatlogOpen ? LocalizeText('modtools.tickets.issue.chatlog.close') : LocalizeText('modtools.tickets.issue.chatlog.view') }
</Button>
{/* Resolution buttons */}
<div className="flex flex-col gap-1.5 pt-1 border-t border-zinc-200">
<div className="text-[.7rem] uppercase tracking-wide opacity-60 font-semibold">{ LocalizeText('modtools.tickets.issue.resolve.heading') }</div>
<div className="grid grid-cols-3 gap-1.5">
<Button gap={ 1 } variant="success" onClick={ () => closeIssue(CloseIssuesMessageComposer.RESOLUTION_RESOLVED) }>
<FaCheck size={ 11 } /> { LocalizeText('modtools.tickets.issue.resolve.resolved') }
</Button>
<Button gap={ 1 } variant="dark" onClick={ () => closeIssue(CloseIssuesMessageComposer.RESOLUTION_USELESS) }>
<FaTrashAlt size={ 11 } /> { LocalizeText('modtools.tickets.issue.resolve.useless') }
</Button>
<Button gap={ 1 } variant="danger" onClick={ () => closeIssue(CloseIssuesMessageComposer.RESOLUTION_ABUSIVE) }>
<FaBan size={ 11 } /> { LocalizeText('modtools.tickets.issue.resolve.abusive') }
</Button>
</div>
<Button gap={ 1 } variant="secondary" onClick={ releaseIssue }>
<FaSignOutAlt size={ 12 } /> { LocalizeText('modtools.tickets.issue.release') }
</Button>
</div>
</NitroCardContentView>
</NitroCardView>
{ cfhChatlogOpen &&
<CfhChatlogView issueId={ issueId } onCloseClick={ () => setCfhChatlogOpen(false) } /> }
</>
);
};