feat: unified invite flow — knock any Voltius user (client) - #122
Merged
Conversation
… in getUserPublicKey
…on-rule empty state
… in the People tab
…ver the 3-tab matrix Review findings on the People-tab restructure: - PeopleTab folded a failed allTeammates() fetch into the same state as an empty roster; give it its own inviteLoadFailed banner, same as the deleted InvitePeopleSection had. - Cover the teams/business and pro-with-qualifying-vault branches of the tab availability matrix, which no existing test reached. - Replace the Tailwind arbitrary animation-duration override (ambiguous against the animate-fadeIn/fadeOut shorthand tokens) with an inline style that always wins the cascade. - Move the reduced-motion keyframe override next to the keyframes it guards.
…tier flash, guard the stranger-invites toggle
The 500-from-key-lookup test asserted on the literal i18n key, which worked only while the key had no translated copy. The strings task added real English text for it, so i18n.t() now resolves the key and the assertion compared against the wrong string. Assert on the status code instead so this survives future copy changes.
…lied name
The stranger knock built its inviter from a participant's display_name, which
originates in the sender's own WebSocket query string. A sender could connect to
their own session as "Voltius Support" and put that name above a Join button
granting terminal access — walking straight past the reserved-handle list, which
refuses @voltius-support at claim time but never saw this surface.
The entry now renders @{invited_by_handle}, the value the server resolves from
its own users table, and falls back to "Someone" when it is absent (an older
server, or a race). Never to display_name: that is the hole. The teammate
sessionInvite and broadcast sessionShared entries are untouched.
joinSharedSession froze the tab title at the redacted placeholder it had at knock time. The server un-redacts on admission, but nothing renamed a multiplayer tab afterwards, so a joined knock read "Shared terminal" forever — against the spec's own live-run criterion that Join reveals the name. Refetch once after the join resolves and patch the session's connectionName.
Recent rows were built with teamIds: [], and memberHasAccess tests teamIds against the session's vaults first. Under the "Recent wins over Your teams" dedupe a teammate in Recent therefore rendered as invitable in a session scoped to their own vault; tapping issued a real grant, seatUsage counted it, and a Pro host at cap 1 lost the seat they meant for someone else. Merge the matching teammate entry in — the person still appears once, under Recent.
replaceAll is the path that takes foreign data — the sync blob and the import UI — and it neither checked its input was an array nor stripped unknown fields, so the "Recent never persists a public key" invariant held only on the one path that never sees untrusted input.
The last hand-rolled copy of that shape; it stayed out only because its error message interpolates a status. Give authedCall an optional interpolation argument instead of keeping a third copy of the call sequence.
A single refetch after Join raced the un-redaction and lost it on every attempt of a live run, so a joined knock read "Shared Terminal" forever. Poll on a bounded backoff instead, detached from the join so the inbox entry does not sit in its acting state.
replaceAll set the list without recentUpdatedAt, so a device that received Recent over the sync blob kept the epoch and lost the next last-write-wins merge. It now stamps like remember/forget, which under the remote-apply guard adopts the remote timestamp and pushes nothing.
A standing invite held a guest seat until the session ended with no way to take it back, and rendered as "Has access" — indistinguishable from someone already in the room. The row now says Invited and carries a Withdraw action that calls the un-invite endpoint and refetches, so the seat frees. memberHasAccess keeps counting pending invites for the cap; the new memberHasLiveAccess answers the row's question.
…r copy at it B4's account-menu surface was never built, and the free-tier note offered the user's email as the way to be reached — the opposite of why every account gets a handle. Copy-with-feedback is now one hook shared by both surfaces.
resetVault kept its own literal list of keychain entries, so a key that account.ts cached but the list omitted survived a sign-out and was served to the next account: the account menu showed the previous user's handle. Both sides now read one list, and a test asserts it against the writers rather than against a copy of itself.
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.
Client half of the unified invite flow. From the ShareMenu you can now find any Voltius user — teammate or not — by
@handleor full email address and knock on them; they join in one tap, and afterwards they are one tap away with nothing to type.Pairs with VoltiusApp/server#4, which must land and deploy first. This half degrades safely against an old server — search simply returns fewer rows — but the flow is inert until the server side is live.
What it adds
People | Link | Team vault— rather than a section bolted under a vault picker whose first step is irrelevant when you are inviting a person. One search field over three labelled groups: Recent, Your teams, and Elsewhere on Voltius. Every row shows the display name with the@handlebeneath it, and a stranger row is badged "Not in your teams" so the two can never be confused.Worth a reviewer's attention
The knock renders a server-owned handle, never a participant-supplied display name. An earlier revision used the display name, which arrives unvalidated in the WebSocket query string — so anyone could have knocked as "Voltius Support" above a button that grants terminal access, defeating the entire point of the reserved-handle list. Do not reintroduce a display-name fallback.
One fallback, one place.
sessionDisplayNameis the only site that supplies a label for a redacted session name; a second??anywhere else re-opens the hole a null was meant to close.Verification
3388 tests across 449 files, typecheck clean. A live two-account run against a throwaway server verified the teaching copy, the handle-only search, the knock arriving with no session name, decline blocking silently, Recent surviving a restart and a second device, and the redaction holding on the wire. Four defects that run found — a tab keeping its placeholder name after Join, a missing withdraw control, an imported Recent list dated 1970, and a pending invitee mislabelled "Has access" — are fixed here; they want one more live pass after deploy.
Not included
The link path —
voltius://, QR codes, clipboard detection — remains the only route to someone who is not on Voltius yet and is the natural follow-on. Evicting an already-joined participant still has no protocol message. The fr/ru/zh strings are non-native renderings and owe a native review pass.