NotificationDefaultAlertView / NotificationDefaultBubbleView injected the server notification message (newline->br only) via dangerouslySetInnerHTML. Route through SanitizeHtml — the allow-list keeps the br/link/formatting these alerts use and strips anything else. Left Nitropedia (rich fetched HTML w/ tags outside the allow-list) and NitrobubbleHidden (<style> block) untouched on purpose: SanitizeHtml would break them.
URLs reached window.open from user/server-controlled content without a protocol check or noopener, allowing reverse-tabnabbing and (for the chat link handler) a javascript:/data: href running in our origin.
- add isSafeExternalUrl() (http/https only) + tests; gate the chat link opener (useOnClickChat) and external photo opener with it
- SanitizeHtml: afterSanitizeAttributes hook forces rel="noopener noreferrer" on any target=_blank anchor (overrides attacker-supplied rel)
- add noopener,noreferrer to the remaining window.open(_blank) sites (YouTube share, external photo, guide forum link); drop a stray console.log
Several dangerouslySetInnerHTML sinks rendered user-controlled strings (chat messages, usernames, chat history) without sanitisation, relying implicitly on upstream formatting or server-side charset limits. Route them all through the existing SanitizeHtml (DOMPurify) helper so the security guarantee is local to each render site.
Sinks fixed: ChatWidgetWindowView (name/message/original/translated), ChatHistoryView (name/message), AvatarInfoWidgetNameView + AvatarInfoWidgetAvatarView (username), SelectReportedUserView (username).
Add regression suites: SanitizeHtml.test.ts (XSS neutralised, chat markup preserved) and RoomChatFormatter.test.ts (pins the existing encodeHTML defence). No behaviour change: SanitizeHtml's allow-list keeps the b/i/u/span/strong/em/br markup the chat/profile UI relies on.
The flood/mute warning was rendered inside the inline-grid input-sizer with no
single-line constraint, so a long message (e.g. a large remaining time) wrapped
to a second line and, with the bar's overflow-visible, spilled out the top. Give
it its own centered, single-line, truncating container so it stays within the
bar regardless of length.
VaultView wired to the Earnings Center packets: requests data on open, renders
real amounts/claimable per category from EarningsCenterEvent, Riscatta + Richiedili
Tutti send the claim composers, refreshes on EarningsClaimResultEvent. Category
keys match the emulator contract; reward currencies derived from reward type.
Adds the real earnings_icon assets. Wired into MainView.
Move the .nitro-help blue-header / grey-body override to global CSS so it also
covers the separate SanctionStatusView card (was an inline <style> in HelpView,
so the sanctions body stayed teal). Replace the flat 'success' buttons with the
beveled Habbo-green button (.habbo-btn-green) matching the reference. Restructure
the sanctions box to a single column: text on top, safety link (left) + green
'Ho capito' (right) pinned to the bottom.
Centered single-column index (blue header + light grey body), the real
help_duck asset, two green buttons (report + player support), and three
green-arrow links: read more about safety, my sanctions, my reports. The
report-flow steps keep the original 2-column grid.
Request earnings on open (RequestEarningsCenterComposer), render real
amounts/claimable per category from EarningsCenterEvent, per-row Riscatta +
Richiedili Tutti send the claim composers, refresh on EarningsClaimResultEvent.
Category keys aligned to the emulator contract; reward currencies derived from
reward type; rows fall back to the static skeleton before data lands.
Target .nitro-card-content-shell for the grey body (the previous .content-area
selector didn't match), and tighten row height (smaller icon box + less gap) to
match the more compact Habbo reference.
Use the real gamedata keys (earnings.title, earnings.dailygift.label,
earnings.achievements.label, earnings.claim.button, ...) instead of invented
ones, so the window is properly localized. 'games'/'clubwork' have no standard
key — custom key + Italian fallback.
Replace the FontAwesome placeholders with the hotel's actual earnings_icon_*
PNGs (daily gift, games, achievements, marketplace, HC payday, level
progression, donations, bonus bag, surprise). Club e Lavoro uses the generic
earnings icon (no dedicated asset).
Each row is now a white bordered card (icon + name) with the currencies and a
gray disabled Riscatta button outside it, plus a gray disabled Richiedili Tutti
at the bottom — matching the reference screenshot.
Wire the dead 'Guadagni' purse button (habboUI/open/vault) to a new VaultView:
the 10 earning categories from the reference, each with currency icons, a 0
placeholder amount and a disabled Riscatta button, plus a disabled Richiedili
Tutti. Amounts/claims are placeholders until the emulator exposes the data.
Bring back the purse gear dropdown (Audio/Discord/Chat/Altre/Filtro) instead of
the tabbed window, and add a 'Gestione Account' item that opens the account
settings window.
Match the account window's look across all settings tabs: each control sits in
a rounded white card row with a section label, and the window width is 340px.
Replace the gear dropdown with a single tabbed window: Audio / Chat / Altre /
Account. Audio/Chat/Altre reuse the existing volume + preference controls;
Account recovers UserAccountSettingsView (now embeddable via an 'embedded' prop
that renders its body without its own card). Removes the dropdown menu + dead css.
The purse gear now opens a dropdown (Audio / Discord / Chat / Altre / Filtro
Parole). Audio/Chat/Altre open UserSettingsView focused on that section
(reusing the existing volume + preference controls) with a Back button; Discord
and Filtro Parole are placeholders for now.
Inline the modern purse markup directly into PurseView and delete the unused
PurseClassicView / PurseModernView components (+ dead PurseClassicView.css).
PurseView is now the single purse component.
Drop overflow-x: clip on .tb-nav-clip so boxes that extend past the nav edge
(e.g. the me-menu above the avatar, especially when the bar is collapsed/narrow)
are no longer cut off.
Replace the dynamic bubble-style preview with the hotel's actual chat-styles
icon (styles-icon.png) shown in color, next to the caret — matching the
reference exactly.
modtools, housekeeping and furni-editor now render outside the collapse group,
so staff still see their tools when the left side is collapsed (still gated by
the existing isMod/isHk checks).
When the right collapse button is active, keep the friends-list icon and show a
compact find-friends (magnifier) button, hiding mentions, the messenger icon
and the full friend bar.
When the left collapse button is active, keep the core icons visible
(catalog, avatar/me, builders club, inventory, camera) and hide only the
secondary ones (habbo/home, rooms, game, rare-values, fortune-wheel, wired,
youtube, soundboard, modtools, housekeeping, furni-editor).