Conversation
…ters - exclude i, o, e, 0 and 1 from the connection ID alphabet so an ID can be read aloud or copied by eye without ambiguity - accept an optional connection_id on session create, validated against the same length and alphabet and rejected with 409 when already in use - expose the alphabet through /session/id-length so the client filters input against the server's rule instead of its own copy
- add a host-only is_public flag, off by default, toggled through /session/toggle_public and mirrored to members over the session socket - serve the five newest published sessions from /sessions/public and stream the same list over an unauthenticated /ws/lobby socket - push appearances and disappearances immediately; fold timestamp-only churn into the existing 2s flush loop - drop the persisted snapshot between tests so state stops leaking forward
- send an optional connection_id on create and read the ID alphabet from /session/id-length instead of assuming a-z0-9 - add toggleSessionPublic and getPublicSessions - move the http-to-ws scheme swap into config so both sockets share it
- let New request its own connection ID, filtered to the server's alphabet as it is typed, with the rule in the label so New and Join stay the same height - keep the user name in local storage and reuse it every visit, an empty field included, so clearing it means "assign me one" rather than reverting - list the five newest public Clippys under the form with name, created, ID and last update, fed by the lobby socket so rooms appear and vanish live
- padlock toggle next to the QR code, locked by default, that publishes the session to the home page and turns green while it is listed - non-hosts see the current state but cannot change it, matching the other session switches - follow public_changed over the socket so every member sees the flip
- window-level drag handlers so a drop outside the card uploads instead of navigating the tab to the file - upload dropped files one after another, each held in memory whole while it is encrypted, and report per-file failures - overlay shows the drop hint and the remaining count, with pointer events off so the composer's own drop zone still works
- decrypt image blocks in the tab and paint them from an object URL, typed from the file extension since the stored bytes carry no content type - cap the preview at 12 MB so a large upload is downloaded, not rendered - render through <img> only, never a link to the blob, so an uploaded SVG cannot run as a document on this origin - allow blob: in the nginx img-src for the object URLs
- expiry, a destroyed session and a socket that stopped reconnecting all go straight home instead of raising a dialog or a toast first - skip the "open the new connection?" prompt when the stored session is already gone; the URL simply wins - clear the stored session synchronously, since every caller reloads right after and could otherwise outrun the effect that removes it
- attach the HTTP status to API errors - treat 404 and 403 as gone, and leave a session alone when the request never landed, so an offline moment no longer discards live blocks
- retry quietly when the socket cannot be constructed at all - drop the per-row guard; the form's loading state already blocks a second join
- keep the lock green while hovered; the plain hover rule is more specific than the public rule and was repainting a listed session as private - drop maxLength from the custom ID field: it truncated a paste before the filter ran, so "Oi3E-x9k7m2" landed as "3x" instead of "3x9k7m"
- fix the label at creation instead of reading the current host, so a listed room keeps its name through a host transfer and after everyone has left - fall back to the host in the loader for snapshots written without a name
- custom and confusion-free connection IDs - public Clippy listing on the entry page - drag-and-drop uploads and image thumbnails - remembered user name and silent return to the dashboard
- keep i, o, e, 0 and 1 out of what the server mints, and accept them in an ID the caller names itself - validate a requested ID against the full a-z0-9 set, which is still the filesystem guard for the session directory
- Connection ID sits at the top of both tabs and renders from one shared field component, so New and Join are the same markup and the same height - drop the hint text next to the label; the ID rule is no longer worth saying - filter the ID inputs on composition end as well as on change, so a Bopomofo keyboard commits nothing into a field that only accepts a-z0-9 - remove the "Last update" label and align its time to the right edge
- render it as a password input, the only control macOS and Windows leave a Chinese IME for; no web API exposes that switch - Chrome ignores -webkit-text-security on password inputs, so hide the mask dots by painting the text transparent and echo the value in a span that mirrors the input's font, padding and letter-spacing - keep password managers and the reveal button out of a field that holds a share code rather than a secret
- confusable characters are avoided only in server-generated IDs - note the password-typed ID field and why it exists
Destroying a connection now lands on the dashboard directly, verified in the browser: path '/', stored session cleared.
- add the Bitwarden, Proton Pass and Dashlane ignore attributes alongside the existing 1Password and LastPass ones - note that no attribute suppresses the browser's own save prompt
…leness - a ping fires every 30s regardless of whether anyone is there, so an abandoned open tab kept a session alive past SESSION_TIMEOUT_SECONDS forever - idleness now measures real actions, matching what the README promises
- render from the path: a stored session opens only when the path names it - offer the stored session as a Resume button at the top right of the entry page instead of taking over the route or asking which one to open - reload on popstate so back and forward move between connections - drop the now-unreachable URL normalisation in ClipboardInterface
A host whose laptop slept for ten seconds lost the session to whoever had joined from the lobby: the seat auto-transfers after the disconnect grace, and with it went the only token that could unpublish or destroy the room — which kept advertising the original host's name to strangers. A room that emptied was worse, since no host token existed at all and nothing but the one-hour timeout could take it down. - clear is_public whenever the host seat moves, whether by explicit transfer or by auto-promotion, and when the last member leaves - push public_changed and the lobby update on both paths - cover the takeover and the abandoned room in tests
- send lobby timestamps with the server's UTC offset: a bare local time is read by the browser as its own, so a UTC container told a UTC+8 reader a room was created 8 hours ago the moment it appeared - reject "lobby" as a custom ID; at CONNECTION_ID_LENGTH=5 such a session would never receive an event, since /ws/lobby is matched first - strip whitespace-only names instead of rendering a blank lobby row - skip the fan-out when toggle_public is called with the value it already has
…erlay Dropping a file on the composer's own zone left the full-screen overlay painted over the page until a reload: the zone stopped propagation, and a drop fires no matching dragleave, so the only code that resets the counter never ran. The window handler now always resets and skips the upload when the composer already claimed the event. Losing the socket was treated as losing the session. Five failed connects take about fifteen seconds, after which the stored session was deleted and the ID wiped from the address bar — reachable by anything that stops a handshake, including a neighbour on the same IP exhausting the proxy's per-address WebSocket slots. - probe over HTTP before giving up: 404 goes home, 403 keeps the path so the reload rejoins, and a request that never landed changes nothing - reset the URL when destroying from the menu, which otherwise raced the session_destroyed frame and came back trying to join what it just destroyed - leave via goHome so Back no longer walks into the connection just left - count pending uploads as a delta, so a second drop cannot zero the first
Every image block fetched and decrypted itself on mount, so a room filled with them made each member's tab download and AES-GCM the lot on join — a member could push a session to its quota in 12 MB images and reliably take every other tab down. - wait for an IntersectionObserver hit before fetching, 300px ahead of the viewport so scrolling still feels instant - key the effect on the block's id and contents instead of the object, which changed identity on every reconnect and re-downloaded everything on screen - drop svg from the preview table: an <img> will not run it, but opening the object URL as a document would run it on this origin
The lobby socket is unauthenticated and every visitor idling on the entry page holds one, so sharing the 20-per-IP WebSocket limit with real sessions meant twenty colleagues behind one NAT could lock the twenty-first out of their own connection — and one attacker could do it deliberately.
- offer Resume whenever the stored session is not the one on screen, not only at /: following a link to an expired connection left the entry page showing an error with no way back to the session still held - reuse the remembered name when joining from a URL, the one path that still asked the server for a random one
- Slide one highlight between the New and Join tabs instead of blinking a background across. The fields below are identical on both tabs, so they stay put and unkeyed. - Cross-fade the entry page into the connection via a view transition, wrapped once around the session setter so every way in — create, join, shared link — gets it for free. - Blocks rise in on arrival, staggered down the list and capped so a long list reads as one sweep. Deleting runs a view transition, so the block fades while the rest close the gap. - Public rooms rise in as they are published. - Honour prefers-reduced-motion globally and in the transition helper.
- upload files from Ctrl+V through the same path as drag-and-drop - bind to the window so a screenshot pasted with nothing focused still lands - leave text paste alone when the caret is in a field and the clipboard carries a bitmap alongside it, as Word and Excel do
The dependency bump moved eslint to 10, but eslint-plugin-react has no release that accepts it — its peer range still stops at ^9.7, and the config uses the plugin's recommended rules. npm ci refused to resolve, so the Docker build has failed on dev since the bump landed. - pin eslint to ^9.39.5 and regenerate the lock - every other package from the bump stays where it was
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings
devup to date with 31 commits of work: a public session lobby, custom connection IDs, and a round of frontend fixes.Backend
/lobbyis reserved as a route.Frontend
Ops
uv.locksynced, READMEs corrected.Test plan