Adopt upstream wheel redo (Settings popup gated by acc_wheeladmin,
RareValues becomes view-only) and the radio enable/disable config gate.
Drop the broken orphaned duplicates under user-settings/fortune-wheel
and user-settings/rare-values (wrong relative import depth, unused,
failed typecheck). Soundboard / radio / background editor untouched.
NavigatorFilterChipsView renders the 5 SearchFilterOptions (anything /
room.name / owner / tag / group) as pill chips instead of a <select>.
Active chip uses bg-primary; the chips sit on their own row above the
search input. Drives the existing searchFilterIndex local state — the
debounce effect already rebuilds the query:value string from it, so no
behavioural change to how searches are issued.
Deferred to wave 1c: saved-search chip row (replacing the 600px sidebar).
That one needs care — a saved-search click sends NavigatorSearchComposer
directly, which P2's accept-filter (result.code must match currentTabCode)
can reject; the chip version must route through setTab/setFilter instead.
Wants browser verification.
navigator suites 28/28, lint:hooks clean, no new typecheck errors.
Visual polish, first wave:
- NavigatorEmptyStateView: replaces the bare "No rooms found" text with a
centered icon + message + a Create-room CTA. Reuses existing i18n keys
(navigator.search.returned.no.results / .roomsettings.moderation.none /
.createroom.create) so no new localization entries are needed.
- NavigatorSearchSkeletonView: animate-pulse placeholder rows shown while a
search is in flight and no result is cached yet (matches the HK dashboard
skeleton pattern). Replaces the NitroCard.Content spinner overlay for the
result list.
Bug fix bundled in: NavigatorSearchView called useNavigatorSearch() a second
time purely to read searchResult for its input-sync effect. Since the hook is
not a useBetween singleton, that registered a duplicate NavigatorSearchEvent
listener AND fired a duplicate NavigatorSearchComposer on every search.
NavigatorView now owns the single useNavigatorSearch() call and passes
searchResult to NavigatorSearchView via prop.
Test maintenance: useNavigatorSearch.test.tsx was written for the original
useNitroQuery implementation, which upstream reverted (05d71dd1) to
useMessageEvent + useState. Removed the dead QueryClient scaffolding, fixed
case 1 (assert no fetch starts with empty tab), dropped case 7 (the query
invalidator no longer exists). 6 cases, all green.
Full suite 471/471. Typecheck: only the environmental renderer-mismatch
errors (soundboard / rare-values / floorplan APIs absent from the linked
renderer), none in navigator files.
useNavigatorSearch had two gaps its tests cover:
- with no active tab the query is disabled, but a NavigatorSearchEvent still
updated the data; now such events are ignored until a tab is active
- a newly created room (FlatCreatedEvent) now invalidates the
['navigator','search'] query and refetches the current search
Fixes the 2 failing useNavigatorSearch tests; full suite 472/472.
Adds an "Editor Posizione" button to the furni infostand action bar for
branding / MPU furni, opening a dialog to position and zoom the image:
- draggable dot moves offsetX/Y (live, local preview only)
- slider zooms the image (scale, via the renderer's per-sprite scale)
- offsetZ kept as z-index; Save persists + broadcasts via SetObjectData
- radio "Live" + all editor labels go through LocalizeText (external texts)
Pairs with the renderer branding scale/offset support and Arcturus' `scale`
default on InteractionRoomAds.
This mirrors what the old god-hook used to do and what the rest of the codebase still uses for everything else. The TanStack one-shot listener pattern (awaitNitroResponse registers a listener, awaits one matching response, removes itself) is fragile against renderer-bundle quirks — the parser fires but the listener never matches, so the promise never resolves and query.data stays undefined forever. That's exactly the symptom you saw: server logs show the response arriving, client UI stays blank.
Adds a compact collapsible radio widget (top-left) that plays internet
radio streams with the HTML5 Audio API — no server/renderer changes.
- station list loaded from a JSON5 config file (loadGamedata: JSON + JSON5),
shipped as radio-stations.json5.example so each hotel fills in its own
- shows the selected station + a dropdown (3 visible, scrolls if more) to
switch; volume slider; animated equalizer + LIVE indicator
- first station autostarts quietly (5%) on load, with a resume-on-first-
gesture fallback for browser autoplay policy
When the server (soundboard_sounds table) returns no pads, the client now
loads them from a JSON5 config file (loadGamedata accepts plain JSON and
JSON5). Useful when the DB / CMS isn't set up yet.
File-defined pads play locally for the clicker; DB-backed pads still go
through the server broadcast so everyone in the room hears them. Ships a
radio-style soundboard-sounds.json5.example template.
The editor never requested occupied tiles, so tiles holding furniture
were indistinguishable from empty floor and could be edited/voided.
- request GetOccupiedTilesMessageComposer when the editor opens
- handle RoomOccupiedTilesMessageEvent -> SET_OCCUPIED_TILES
- new Tile.occupied flag (kept separate from `blocked`/void): occupied
tiles render with a distinct marker and are protected from PAINT/
ERASE/ADJUST and brush-to-selection edits
- occupied is purely informational and never changes the saved tilemap
(no accidental voiding of floor under furni)
Tests: reducer cases for SET_OCCUPIED_TILES + edit protection; container
test asserts the occupied event is non-destructive on save; route the
canvas pointer test through elementFromPoint (jsdom has no getScreenCTM).
Client side of the soundboard. Room owners enable it in Room Settings >
Misc (next to the YouTube TV toggle). When enabled, a soundboard icon
appears in the toolbar for everyone in the room; pressing a pad broadcasts
the sound so all occupants hear it. Incoming SoundboardPlay is played via
the HTML5 Audio API.
Also: fix FloorplanCanvasSVG to use ReactElement instead of the removed
global JSX namespace (React 19), and pair the client Dev branch with the
renderer fork that carries the custom features in CI.
How sounds are managed (works with any CMS):
Sounds are rows in the `soundboard_sounds` table:
id, name, url, enabled, sort_order
The emulator loads every row with enabled=1 (ordered by sort_order, id)
and sends the list to clients on room enter; the client plays `url`
directly, so any publicly reachable audio URL works (mp3/ogg/wav).
To add a sound from an admin/housekeeping panel of any CMS:
1. Upload the audio file to wherever the CMS stores public assets
(same approach as custom badge images).
2. INSERT a row into `soundboard_sounds` with the display name and the
public URL of the uploaded file, enabled = 1.
3. Reload the emulator soundboard (or restart) to pick it up.
Relative urls resolve against the `soundboard.url.prefix` config key
(falls back to `asset.url`); absolute urls are used as-is.
NavigatorView reads searchResult/isFetching from useNavigatorSearch
instead of useNavigatorData/useNavigatorUiState. Tab clicks call
setTab(code) on the UI store, which atomically updates the query key
and triggers refetch. The 4 lifecycle useEffect blocks driving the
old imperative flow (needsSearch / reloadCurrentSearch / markReady)
are removed — the query handles all of it now.
NavigatorSearchView has a debounced (300ms) onChange -> setFilter
that drives the same query refetch. Explicit submit (Enter / button)
skips the debounce and calls setFilter immediately.
linkTracker case 'search' now setTab + setFilter + show — no more
pendingSearch ref.
useNavigatorSearch.test.tsx: cast constructors as any to satisfy tsgo
against real renderer types while keeping runtime stubs no-arg-safe.
yarn typecheck / test / lint:hooks all clean (only pre-existing
floorplan environmental failures).
P2 core surgery: search result + NavigatorSearchEvent listener +
sendSearch + reloadCurrentSearch all leave useNavigatorStore. The new
useNavigatorSearch query hook owns the cache. useNavigatorActions is
deleted entirely — the only two actions it exposed are gone, and no
consumer outside Navigator depended on it.
NavigatorMetadataEvent handler now seeds the UI store's currentTabCode
on first arrival, activating the query the moment top-level contexts
land.
useNavigatorData: searchResult removed from closure and return.
useNavigatorUiState: currentTabCode + currentFilter added.
index.ts: useNavigatorActions removed, useNavigatorSearch added.
NavigatorView.tsx is intentionally broken at this commit and gets
fixed in the next.
useNitroQuery keyed on [currentTabCode, currentFilter] from
navigatorUiStore. Fires NavigatorSearchComposer; subscribes to
NavigatorSearchEvent with an accept-filter that rejects results whose
code does not match the current tab. Invalidates on FlatCreatedEvent
and RoomSettingsUpdatedEvent for server-driven refresh.
nitro-renderer.mock.ts: add connection.send stub to GetCommunication
so SendMessageComposer (which calls GetCommunication().connection.send)
does not throw in tests that exercise useNitroQuery.
TDD: 7 cases incl. enabled-gating, accept-filter rejection on
mismatched tab, invalidator round-trip.
setTab(code) atomically updates currentTabCode and resets currentFilter
to '' — switching tabs starts a fresh search context. setFilter(value)
updates only the filter — the user is typing in the same tab.
TDD: 3 new cases (16 total in navigatorUiStore.test).
Each of the 5 Navigator sub-views (RoomCreator, DoorState, RoomInfo,
RoomLink, RoomSettings) is now wrapped in its own WidgetErrorBoundary so
a crash inside one no longer takes down the others. Matches the pattern
already applied to the 13 room widgets + 20 furniture widgets.
Zero behavioural change in the happy path. yarn typecheck +
yarn test --run + yarn lint:hooks all clean (only the 3 pre-existing
floorplan failures remain, unrelated to Navigator).
Commit 8ab0021a introduced an unjustified deviation: it removed the
useNotification() call from inside useNavigatorStore and replaced it
with a module-level _simpleAlert ref + _injectSimpleAlert() exported
function, on the theory that nested useBetween calls corrupt
use-between's state.
That diagnosis is wrong. Production proof:
- useCatalog.ts:56 calls useNotification() inside useCatalogStore
- useWiredToolsStore.ts:131 calls useNotification() inside its store
- The original useNavigator.ts:32 calls useNotification() inside its
state closure
All three have been in production for ages without issue. Nested
useBetween calls work fine.
The smoke-test failure that prompted the workaround was a mock issue,
not a real bug. Reverting to the standard pattern — useNotification()
direct inside the useBetween store closure. Production alerts work
again immediately without requiring an explicit injection call from
consumers.
Mock additions (src/nitro-renderer.mock.ts):
- Added 23 notification MessageEvent subclasses (AchievementNotification-
MessageEvent, ActivityPoint..., BadgeReceived, ClubGiftNotification,
ClubGiftSelected, ConnectionError, HabboBroadcast, HotelClosedAndOpens,
HotelClosesAndWillOpenAt, HotelWillCloseInMinutes, InfoFeedEnable,
MaintenanceStatus, ModeratorCaution, ModeratorMessage, MOTD,
NotificationDialog, PetLevel, PetReceived, RespectReceived, RoomEnter,
SimpleAlert, UserBanned, WiredRewardResult) so useNotificationStore
can register its listeners without throwing.
- Added RoomEnterEffect stub (isRunning: false, totalRunningTime: 0).
- Added WiredRewardResultMessageEvent static constants.
Splits the 492-line useNavigator god-hook into a useBetween-backed
useNavigatorStore closure plus three flat-shape filters
(useNavigatorData, useNavigatorUiState, useNavigatorActions), mirroring
the wired-tools layout. sendSearch + reloadCurrentSearch are extracted
as named actions out of NavigatorView locals.
Door-mode handling is removed from this store and lives in useDoorState
(committed previously) - see GetGuestRoomResultEvent and
GenericErrorEvent dual-subscription with mutually exclusive filters.
The simpleAlert dependency is lifted out of the useBetween scope via a
module-level _simpleAlert ref + _injectSimpleAlert() to avoid nested
useBetween calls that corrupt use-between's module-level dispatcher
state. The ref is null in tests (no events fire during smoke tests) and
is populated in production by the navigator consumer before any alert
is needed.
The barrel index.ts no longer re-exports useNavigator. The 13 consumers
will fail typecheck until the next commit migrates them; the hook files
themselves are clean. Smoke test covers filter shapes.
INTENTIONAL INTERMEDIATE-BROKEN COMMIT: yarn typecheck is RED at this
SHA on the 13 consumer files. The next commit (consumer migration sweep)
brings it back to green.
Code review of Task 2 (commit 07bbc0c7) found two real issues:
1. The GetGuestRoomResultEvent handler did not handle parser.roomEnter,
so after the consumer migration (Tasks 5-8) a successful room entry
would no longer dismiss the door dialog. Fix: reset to INITIAL when
parser.roomEnter is true, before the roomForward branch.
2. The test suite was order-dependent — the useBetween singleton
persisted state across tests, so 'exposes the initial NONE snapshot'
passed only because it ran first. Fix: beforeEach renders the hook
once, calls reset(), then unmounts; afterEach calls cleanup().
Plus one new test case verifying the roomEnter -> reset behavior.
- Add `src/hooks/rooms/widgets/useDoorState.ts`: useBetween-based
singleton wrapping DoorbellMessageEvent / RoomDoorbellAcceptedEvent /
FlatAccessDeniedMessageEvent / GenericErrorEvent /
GetGuestRoomResultEvent; all 5 handlers wrapped in useCallback([])
so their references are stable across useBetween tick() calls and
the effect dep-array never triggers re-registration.
- Add `src/hooks/rooms/widgets/useDoorState.test.tsx`: 11-case Vitest
suite (initial state, 5 event transitions, 2 no-op guards,
GetGuestRoomResultEvent doorbell/password paths, reset()).
- Extend `src/nitro-renderer.mock.ts`: new MessageEvent base class with
callBack/type/getParser; DoorbellMessageEvent / RoomDoorbellAcceptedEvent /
FlatAccessDeniedMessageEvent / GenericErrorEvent / GetGuestRoomResultEvent
concrete stubs; RoomDataParser.DOORBELL_STATE + PASSWORD_STATE; separate
msgListeners map (cleared independently of NitroEvent listeners so
useBetween subscriptions survive between test cases); WeakMap wrapper
for correct removeMessageEvent; GetCommunication routes to msgListeners.
All 11 useDoorState tests pass; full suite 453/456 (3 pre-existing
FloorplanCanvasSVG jsdom/SVG-CTM failures unrelated to this task).
Hoists the 9 useState in NavigatorView (isVisible, isReady, isCreatorOpen,
isRoomInfoOpen, isRoomLinkOpen, isOpenSavesSearches, isLoading, needsInit,
needsSearch) into a createNitroStore-backed Zustand store with named
actions. Future linkTracker / lifecycle wiring will call these actions
instead of mutating local component state.
TDD: 14 cases on each action's transitions + idempotency.
The left-nav container is `max-w-[calc(50vw-242px)]` (reserves the chat
frame width) and uses `overflow-x: clip`. With the full icon set
(habbo, rooms, game, catalog, buildersclub, inventory, ME, wired-tools,
camera, youtube, modtools, furnieditor, housekeeping) the icons exceed
the available 528-608px around the 1540-1700px viewport range, so the
last icons get silently clipped on the right.
Raising the desktop breakpoint from 1540px to 1700px makes the client
fall back to the mobile-scrollable layout (`.tb-bar-scroll`) below
1700px, which scrolls horizontally and doesn't clip.
Above 1700px the desktop fixed-icon layout still applies, now with
enough horizontal room for every icon even with mod+HK enabled.
Touch devices are unaffected (already forced onto the mobile layout
via `pointer: coarse`).