The "Edit Furni" button was nested inside the `(!avatarInfo.isWallItem
&& canMove)` guard, together with the floor-only Buildtools position/
height/rotation controls, so it never rendered for wall furni (e.g.
`ads_campguitar`). Move it out so it shows for any furni when
`godMode` + `isModerator`, leaving the position controls floor-only.
The onClick already resolved WALL vs FLOOR correctly.
While touching this file, clean up two pre-existing lint errors:
- hoist `getValidRoomObjectDirection` to module scope (it is pure and
uses no component state) so it is no longer accessed before its
declaration (react-hooks/immutability)
- expand the inline `'scale'` branch to Allman braces (brace-style)
The messenger rendered the participant figure straight from the frozen
thread participant, so offline friends (whose look used to be empty)
showed the anonymous/standard avatar. Read the live look from the friend
list via getFriend() - the same source the friends list renders - with
resolveAvatarFigure() as the final fallback, so the real avatar shows
even when offline (pairs with the server fix that now sends offline
looks). Applied to both the avatar-bar tab and the in-thread avatars.
Also fix the avatar-tab head framing: it positioned the head-only image
with full-body geometry (90x130, top:-31px), clipping the head. Render
the head at native size (background-size:auto, no scaling -> not grainy)
and centre it in the 36x36 tab.
Chat tagging:
- Any @user is a visible tag in chat bubbles (the .mention-tag CSS never
existed, so highlighting was invisible); self/alias mentions get a gold
emphasis. Fixes cross-room tags not being highlighted.
Mentions window:
- Redesigned: unread count in the header, restyled filter chips + a refresh
button, CSS-driven list/date-groups, adaptive height (compact when few,
capped + scroll when many), polished empty state.
- Rows: framed avatar (friends-list head crop so the face is never clipped),
per-row unread dot, type marker, icon action buttons (goto / remove).
- Re-requests from the server each time it opens.
Autocomplete:
- Never suggests the viewer themselves; suggests room users + online friends +
aliases.
Notifications:
- Mention toast removed; mentions flow through the client's standard
notification stream via a dedicated mention bubble (avatar + actions) in the
default position. EVERY received mention surfaces (independent of the generic
info-feed toggle, gated only by mentions_ui.enabled).
Refactor (behaviour-preserving):
- Centralised @-token classification in api/mentions/mentionTokens.
- Moved mentionsFormat -> api/mentions, useMentionActions -> hooks/mentions.
- Extracted ChatInputView @-autocomplete into a tested useChatMentions hook +
pure helper; removed the dead duplicate useMentionAutocomplete.
The toolbar renders a mentions button (`ToolbarItemView icon="mentions"` ->
`<div class="nitro-icon icon-mentions">`) and the asset
`assets/images/toolbar/icons/mentions.png` (72x64) exists, but icons.css had
no `.nitro-icon.icon-mentions` rule - so the element had no background image
and, since the base `.nitro-icon` sets no size, rendered at 0x0 (invisible).
Add the rule, sized at half the asset (36x32, preserving the 9:8 aspect) with
`background-size: contain`, matching the other toolbar icon definitions.
The leaderboard rows pulled each avatar head from habbo.com's imaging
service (`https://www.habbo.com/habbo-imaging/avatarimage?...headonly=1`)
via a plain <img>. The avatar `figure` is already present in the leaderboard
data (served by the CMS `/api/badges/leaderboard` endpoint), so there is no
need for an external request - render it locally instead.
Swap the <img> for the renderer-backed `LayoutAvatarImageView` (headOnly),
which draws the head through `GetAvatarRenderManager().createAvatarImage(...)`.
The head-only render is an absolutely-positioned background div rather than an
<img>, so the avatar CSS is reworked to frame it (relative, overflow-hidden
box with a head crop mirrored from the friends list), and the now-unused
`getAvatarHeadUrl` helper is removed.
Removes the last runtime dependency on habbo.com for this panel; avatars now
come entirely from the local renderer.
The online-friends bar is portaled into the right toolbar nav, which sits
inside `tb-nav-clip` (fixed, `max-w-[calc(50vw-242px)]`, `overflow-x: clip`).
Each online friend adds a fixed `w-[132px]` chip, so the bar grew with every
friend up to MAX_DISPLAY_COUNT (3). Once it exceeded the clip width the right
edge was silently cut off - the scroll arrow and part of the search button
disappeared. The portal slot is `shrink-0`, so the chips never compressed;
they just overflowed and got clipped. Net effect: "more friends online =
broken bar".
Measure the room actually available between the bar's left edge and the
viewport's right edge (re-measured on resize / ResizeObserver) and derive an
effective visible count clamped 1..3, always reserving space for both arrows
and the search chip so nothing clips at any width or friend count. The bar's
left edge is stable (it follows fixed-width toolbar icons), so changing the
chip count never moves it - no measurement feedback loop.
Scroll offset now derives a clamped safeOffset used by every read, so a
stale indexOffset after the list shrinks / the fit grows renders correctly
and self-corrects on the next arrow click (no write-back effect).
Show the furni's resolved furnidata JSON entry (the editable display
name's source of truth) in a collapsible read-only section under Basic
Info. Uses furniDataEntry already available client-side — no extra
packet. Supersedes the old read-only resolver-preview slice.
Show the furni's resolved furnidata JSON entry (the editable display
name's source of truth) in a collapsible read-only section under Basic
Info. Uses furniDataEntry already available client-side — no extra
packet. Supersedes the old read-only resolver-preview slice.
useFurniEditor gains importText(id) + importResult (10049 round-trip); the
edit view shows an 'Import from Habbo' button that fills Display Name/
Description with the official text for review before Save (nonce-guarded,
classname-matched), with an inline result note.
The search-bar magnifier had a too-short handle relative to the lens, so
at 16px it read like a musical note. Use a proportioned lens + longer
handle so it reads unmistakably as a magnifying glass.
- Restyle to match the editor: slate palette, rounded card, search bar
with icon + focus ring, teal filter chips, larger furni thumbnails,
loading skeleton + empty state.
- Live search (350ms debounce) with clear button; filters/sort apply
immediately.
- Pagination: first/prev/next/last + page-jump input + 'Showing X-Y of Z'
(was Prev/Next only across 3002 pages).
- Header sort now queries the server (sortField/sortDir) instead of
reordering only the 20 visible rows; useFurniEditor.searchItems passes
the sort through.
The Tip bubble was still clipped by the card's overflow-auto scroll
container when a section was scrolled near the viewport top. Render the
tooltip via createPortal to document.body with position:fixed computed
from the icon rect, so it escapes every overflow/transform ancestor.
Tip tooltips were whitespace-nowrap (one very wide line) and clipped by
the Section card's overflow-hidden. Bound the bubble width with wrapping
(w-44, whitespace-normal, centered) and drop overflow-hidden from Section
(keeping rounded corners via rounded-t/b-xl on the header) so long tips
like Custom Params render fully.
Saving an unchanged name made the server writer return false, which the
handler misreports as 'Classname not found in furnidata'. Gate the
'Save name/desc' button on a real diff (furnidataDirty) so an unchanged
save can no longer fire that misleading error.
- Merge the lone Inventory permission (allowInventoryStack) into the
Gameplay group, dropping the separate Inventory header+row to save
vertical space.
- Rework the header meta row: ID/Sprite as label+monospace-value pills,
'in use' tinted green with a dot when placed instances exist (grey at
0), 'Unsaved' as an amber pill with a dot.
~1.4k items_base rows (pets, custom items) have no matching furnidata
classname, so saving their name returned the cryptic server error
'Classname not found in furnidata'. Detect this client-side via the
resolved furniDataEntry (entry + classname match) and, when absent,
hide the Display Name/Description inputs + Save behind a clear
'NO FURNIDATA' notice instead of letting the save fail. In-furnidata
furni (~97.6%) are unchanged.
The server now mirrors the furnidata display name into
items_base.public_name, so on updateFurnidata patch selectedItem.publicName
immediately; the auto detail re-fetch that follows agrees with the DB
value (no flicker). Not applied to revertFurnidata (revert restores the
previous DB name).
Enabled chips now use a solid teal fill (bg-[#1E7295]) with white
text + a white status dot; disabled chips are muted grey with a grey
dot. The old bg-primary/10 + text-primary 'on' state was nearly
indistinguishable from 'off'. Adds aria-pressed + title for a11y.
useFurniEditor gains importText(id) + importResult (10049 round-trip); the
edit view shows an 'Import from Habbo' button that fills Display Name/
Description with the official text for review before Save (nonce-guarded,
classname-matched), with an inline result note.
The search-bar magnifier had a too-short handle relative to the lens, so
at 16px it read like a musical note. Use a proportioned lens + longer
handle so it reads unmistakably as a magnifying glass.
- Restyle to match the editor: slate palette, rounded card, search bar
with icon + focus ring, teal filter chips, larger furni thumbnails,
loading skeleton + empty state.
- Live search (350ms debounce) with clear button; filters/sort apply
immediately.
- Pagination: first/prev/next/last + page-jump input + 'Showing X-Y of Z'
(was Prev/Next only across 3002 pages).
- Header sort now queries the server (sortField/sortDir) instead of
reordering only the 20 visible rows; useFurniEditor.searchItems passes
the sort through.
The Tip bubble was still clipped by the card's overflow-auto scroll
container when a section was scrolled near the viewport top. Render the
tooltip via createPortal to document.body with position:fixed computed
from the icon rect, so it escapes every overflow/transform ancestor.
Tip tooltips were whitespace-nowrap (one very wide line) and clipped by
the Section card's overflow-hidden. Bound the bubble width with wrapping
(w-44, whitespace-normal, centered) and drop overflow-hidden from Section
(keeping rounded corners via rounded-t/b-xl on the header) so long tips
like Custom Params render fully.
Saving an unchanged name made the server writer return false, which the
handler misreports as 'Classname not found in furnidata'. Gate the
'Save name/desc' button on a real diff (furnidataDirty) so an unchanged
save can no longer fire that misleading error.
- Merge the lone Inventory permission (allowInventoryStack) into the
Gameplay group, dropping the separate Inventory header+row to save
vertical space.
- Rework the header meta row: ID/Sprite as label+monospace-value pills,
'in use' tinted green with a dot when placed instances exist (grey at
0), 'Unsaved' as an amber pill with a dot.
~1.4k items_base rows (pets, custom items) have no matching furnidata
classname, so saving their name returned the cryptic server error
'Classname not found in furnidata'. Detect this client-side via the
resolved furniDataEntry (entry + classname match) and, when absent,
hide the Display Name/Description inputs + Save behind a clear
'NO FURNIDATA' notice instead of letting the save fail. In-furnidata
furni (~97.6%) are unchanged.
The server now mirrors the furnidata display name into
items_base.public_name, so on updateFurnidata patch selectedItem.publicName
immediately; the auto detail re-fetch that follows agrees with the DB
value (no flicker). Not applied to revertFurnidata (revert restores the
previous DB name).