The connect hero read the generic 'Le tchat français, / en direct.'
Give the Tchatou deployment its own warmer French headline via the
branding config (leaves Orbit's shared i18n defaults untouched):
'Soyez au cœur des échanges, là où tout commence.'
mIRC colour codes include near-black, dark blue and grey that vanish on a
dark theme (and white/pale that vanish on light). When a coloured run has
no explicit background, clamp its lightness toward the readable side —
preserving hue and saturation, so a dark blue reads as a lighter blue.
Darkness is taken from the live --bg, cached per theme.
Removing the avatar slot entirely for pictureless users left them
flush-left while avatar-havers stayed indented, so mixed channels looked
ragged and continuation lines lost their timestamp. Always reserve the
avatar column: photo when there is one, hover timestamp on continuation
lines, otherwise an empty spacer — no bright fallback bubble, but every
row lines up again.
Mounting a full 500-line buffer is the bulk of network-switch latency. On
a buffer switch we now render only the last ~120 messages (extended to
keep the unread divider in view), then fill the rest of the buffer on the
next idle tick — off the interaction's critical path. After the fill the
list is fully rendered exactly as before, so scrolling/auto-follow are
unchanged; this never drops rows while you read. The fill prepends older
rows, so the scroll effect preserves the reading position (or stays pinned
to the bottom).
Three targeted wins for the slow network-tab click:
- Sidebar subscribed to the whole buffers map, so it re-rendered and
re-mapped every conversation row on each incoming line. Subscribe to
order only and derive channel-vs-DM from the name.
- content-visibility on the conversation rows (matching the member list)
so switching a network only lays out the on-screen rows.
- skip the whole-buffer unread scan for the active network's own tab,
whose badge is never shown.
Reverting the modern-theme prefix removal: the @/+/~ prefix on nicks is
core IRC and belongs in the member list regardless of theme. The in-chat
op marker stays; this just restores the nicklist prefix everywhere.
Message text goes back to the proportional UI font by default; the new
switch renders it in a fixed-width font (via html[data-mono]) so ASCII
art, tables and box-drawing line up on demand. Persisted per user and
translated across all locales.
You can now spot ops (and voiced/owners) while reading the conversation:
each sender's strongest channel prefix (@ ~ & % +) is shown before their
nick, coloured by role to match the member list. Factor the prefix→role
map into a shared lib so the member list and the message list can't drift.
Members are already grouped under a role header (Op, Voice, …) and the
role is colour-coded, so prefixing the nick with @/+/~ was redundant
clutter — an op now reads as "Aline", matching the message list. yomIRC
keeps the classic mIRC-style "@nick". Removes the now-unused modern
per-role prefix colours.
The message body used the proportional UI font, so ASCII art, tables and
box-drawing never aligned. It already preserves whitespace (pre-wrap); it
just needed a fixed-width font. Give .line the monospace stack (yomIRC was
already mono) and trim the size a touch since mono renders wider.
The modern layout showed a bright per-nick gradient bubble for everyone
without an uploaded avatar — a column of color-shifting blobs that's hard
on the eyes. Resolve the avatar url in the row and only render the bubble
(and its rail) when there's an actual picture; otherwise the message runs
full-width. Registered users with a photo are unchanged.
The follow effect keyed on message count, but the buffer is capped at 500
so in a busy channel the count stops changing and the effect stopped
firing entirely — auto-scroll silently died exactly in the channels that
need it. Key it on the newest message id as well, which keeps changing.
A backgrounded browser tab freezes rAF and reports stale layout while
messages keep arriving, so the bottom-follow drifted and there was no way
back to the newest line. On the tab becoming visible again we re-pin to
the bottom if following, and the jump-to-newest button now stays in sync
as lines arrive (not only on scroll).
Tighten the auto-scroll band from 140px to 64px so scrolling up even a
little to read the last line no longer gets yanked back down when a new
message arrives; the jump-to-newest button covers the rest.
Auto-follow now engages only while pinned to the very bottom (64px), not
anywhere within 140px of it — so sitting on the newest message while
people type no longer snaps you back. The read-marker still clears across
the wider band.
Three fixes for fast channels: the day-separator no longer runs Intl date
formatting for every message on every render (one cheap day key instead),
scroll handling is coalesced to one layout read per frame so a Mac
trackpad's rapid events don't cause jank, and the floating button now
appears whenever you're scrolled up and snaps straight to the newest line
— so a busy channel can't strand you away from the bottom.
A busy channel no longer shows one status line per presence event: a run
of joins and parts collapses into a single compact line (nicks coloured,
joins and leaves netted, long lists truncated with a +N hover). The
classic mIRC theme and the server console keep one line per event.
Make each message body a size container and stack the link card (image over
text) when that column, not the viewport, is too narrow for the side-by-side
thumbnail. One rule covers a wide desktop column, a phone, and a member-list
squeezed column alike; browsers without container queries keep the horizontal
card as before.
Wrap sidebar room and DM selection in document.startViewTransition so the
message region cross-fades to the newly chosen channel. The root is opted out,
so only .messages morphs while the sidebar, topbar and composer stay instant.
Falls back to a plain switch where the API is unavailable or the user prefers
reduced motion.
color-mix() derives --accent-soft from --accent so the soft tint tracks a
config.branding.accent override instead of a fixed hex. Add a
prefers-contrast: more query — heavier text (Hanken/Geist are variable
fonts, so weight tracks the wght axis) and underlined links.
Friends opened as a centred modal; convert it to a nav-panel that pops up
anchored above its footer tab, matching the radio/games window via a shared
.opanel card (soft shadow, borderless input + accent button, rise entrance).
It joins the 'orbit:panel' exclusion, so opening it closes the others and
vice versa. Removes the old FriendsModal.
The exclusion compared the incoming id against the guest's DECLARED plugin
name ('orbit-radio'), but panel.opened emits the HOST id
('sbx:orbit-radio.js'), so a sandboxed panel closed itself the instant it
opened. The init message already carries the host name — read it and compare
against that.
Opening the games or radio panel left the other open — they stacked. Add a
shared 'orbit:panel' bus event: a panel announces itself on open and every
other panel closes. Sandboxed panels (radio) join through the SDK — a
null-capability panel.opened RPC emits the event host-side with the id fixed
to the plugin name, and the panel() helper closes on any other id. The games
panel emits and listens on the same event.
The footer's Rooms/Salons tab only opened the explore modal — the same
thing the sidebar's "Explore channels" button already does (and that one
refreshes the channel list first). Drop the redundant tab; the footer is
now Home · Friends · Games · Radio. Also removes the now-unused compass
icon and the dead nav.tabRooms label from all ten locales.
Compared side by side, the games panel used outlined, bordered buttons and
a lighter shadow while the radio window uses the SDK's borderless atoms
(.obx-*). Restyle the games panel to the same language: borderless soft
buttons that fill with the accent when active, a plain muted close (obx-x),
12px card padding, and the radio card's exact two-layer shadow. Same theme
now, not just similar.
The panel was pinned to a fixed bottom-right corner, detached from the
Games footer tab — unlike the radio panel, which pops up anchored above
its own tab. Capture the tab's rect on click and position the panel
centred above it, clamped to the viewport. Now it opens from the tab it
belongs to instead of the corner.
The popovers (games panel, notify / pin / overflow menus, status menu) had
a flat fade and a single-layer shadow, unlike the radio window. Route them
through the app's two-layer --shadow-pop token and a fade + rise entrance.
Also define @keyframes rise, which was referenced app-wide (modals, emoji
and colour pickers, settings, login) but never declared — so those
entrance animations were silently no-ops and now play. The games panel
matches the radio card: 16px radius, soft shadow, rise on open.
The panel referenced its own token names (--bg2/--tx/--tx-dim/--ac/--card)
that don't exist in the app, so it always fell back to hardcoded dark
values — mismatched on the light and Night-orange/green themes. Alias
those tokens to the app's theme tokens (--bg / --ink / --muted / --accent
/ --bg-soft-2) at the panel root, fix the active white-overlay fills that
vanished on light backgrounds (rows, input, toast), map the accept action
to --online, and soften the shadows. The panel now follows the active
theme; the game-piece colours (X/O, chess, Connect-4) stay as-is.
Games is now a "Jeux" tab in the footer alongside Accueil / Salons / Amis
— icon over label, accent when its panel is open, an alert dot when it's
your move or a challenge is pending — a native bottom-nav home for it. The
panel still opens from the persistent 'overlay' slot, and the new-game /
challenge ("invite to game") flow lives inside it (plus the profile Défier
action). Drops the now-unused topbar-plugin-in-overflow machinery; the
clock stays in the desktop header.
The clock is a passive HH:MM display (a sandboxed .sbx-slot), so it doesn't
belong in the action menu — and it's redundant on phones, where the OS
status bar already shows the time. Drop sandboxed topbar plugins from the
⋮ overflow; the bar copy is already hidden on mobile, so the clock is now
gone on phones while staying in the desktop header.
A topbar plugin's popover was rendered as a child of its launcher button,
so the button had to stay mounted for the popover to survive — which is
why the games button couldn't move off the crowded mobile header. Add a
persistent 'overlay' UI slot at the app root and render the games panel
there, decoupling it from the button. The launcher is now stateless, so
on mobile it drops into the ⋮ overflow as a labelled "Jeux" row (freeing
the header for the channel name + topic) while its panel stays anchored.
Desktop keeps the icon in the bar.
The games panel is a fixed overlay rendered as a child of its button, so
the button has to stay mounted for the panel to survive — it can't move
into the ephemeral overflow menu, where it would unmount (and the panel
vanish) the moment the menu closes. Now that search / manage / leave live
in the overflow, the bar has room, so show the games plugin button inline
on mobile instead of hiding it. Reachable again, and the title and topic
still fit.
On phones the channel header crammed every button into one row: the title
sat behind the games-plugin button and the topic was pushed off screen.
Collapse the one-shot actions (search, manage, leave) into a "⋮" overflow
menu, hide topbar plugin items on mobile, and keep only the stateful notify
and pin toggles, the member count and the overflow inline. The channel
glyph, name and topic now have room to show as a proper two-line header.
The channel header's search, notify, pin, manage and close buttons — plus
the app-wide search-field magnifiers and the menu toggle — used OS emoji
glyphs. Extend the shared Icon component with search, close, menu, bell,
bell-off, pin and sliders, and use it across the topbar, the notify and
pin menus, and the explore / switcher / side-search fields. The icons
inherit each button's colour and get the same press and focus states as
the footer, so the whole chrome now reads as one icon family.
The account bar's tabs and settings button used OS emoji (home, compass,
people, gear), which render off-theme and differ across platforms. Add a
shared stroke-icon component — 24×24, currentColor, the same 1.9 stroke as
the composer glyphs — and use it for Accueil, Salons, Amis and Settings.
The icons inherit each button's colour, so the active tab now reads as a
soft accent pill, and the buttons gain a press dip and focus ring so the
bar feels native on desktop, not just touch.
All ten translations (~400 KB of JSON) were bundled into the entry
chunk, nearly all of it never used in a session. Ship only French — the
default and fallback — and fetch the other nine as separate chunks on
demand (language switch or a config-pinned default). The entry chunk
drops from 653 KB to 361 KB (200 KB to 118 KB gzipped), cutting the
parse/execute work that was delaying the largest paint.
Each IRC line arrives as its own WebSocket frame (text.ircv3.net), so
the store rendered once per line. Joining several busy channels at once
— NAMES plus a WHO reply per member — fired a render for every line and
stalled the main thread, which also inflated LCP. Buffer messages onto a
0ms timer so a whole burst drains in one render; under load the timers
back up and each drain swallows more lines. A timer rather than rAF keeps
unread counts and mention notifications live in a backgrounded tab.
Turnstile refreshed its callback ref in the render body; move it to an
effect. The gtag shim legitimately pushes the live arguments object, so
scope a disable to that line with a reason. Lint is now error-clean.
The nick field, channel +k/+l inputs and avatar hook re-synced from
props by calling setState inside an effect, which triggers an extra
render pass each time. Track the previous prop and adjust during
render instead — one pass, and the avatar now resets on account switch.
Set up an @/* -> src/* alias (tsconfig paths + vite resolve) and
convert every ../../-and-deeper import to it. Same-dir ./ and
one-level ../ stay relative. Build-time only, so the bundle is
unchanged; the deep imports were just hard to read and broke on
file moves.