Repository navigation
Player redesign 8b-1: household members - #100
Merged
Merged
Conversation
Mirror the household_members routes in src/api: HouseholdMember and
friends, managed_by/member_kind on User, member_sign_ins on MyDevice,
members_signed_out on revoke. Client methods for the eight member routes
and revokeMyDevice(id, { alsoMembers }) sending ?also_members=1 only when
asked. Hooks gated on household_members (skipToken queries,
CapabilityError mutations) with connection-scoped keys; useRevokeMyDevice
also takes { id, alsoMembers }, sent only to a server with the flag.
…s accounts
setSession(..., { makeDefault: false }) leaves the default connection where
it is (it still becomes the default when there is none, or the default was
retired). placeInDoc/placeConnection take keepActive and a name: a new
profile is added without changing the person listening. mayTurnOnKids
allows kids mode with no grown-up on the device when every account of the
profile is a kid's (user.member_kind === 'kid', isKidsAccount); the last
grown-up rule still holds for every other account.
…gn-in
addMemberHere(session, { name, kids }) signs a household member in beside
the owner: stored on the owner's address and server name, the addresses
learnt, the default connection unchanged, a profile of their own named as
typed, the owner still active (never switchProfile), kids mode on for a
kid. Refuses with AddMemberHereError ('already_here') when the device holds
that user on that server (memberSignedInHere, to ask before minting).
finishConnect turns kids mode on at a kid account's first placement on a
device (its own phone paired by QR included), never for a profile that
also holds a grown-up's account. Locale keys connect.member* and
kids.stories.askGrownUp in all six languages.
A household member's kids mode shows the collections their owner (user.managed_by) shares with them, in the list's order: one run as the whole shelf, several under their names, stories in progress first; empty collections ask a grown-up to add stories. No such collection, an older server, or an account nobody manages keeps the one-library view. Offline, the downloads of the kid's account.
The car for a kids profile lists the kids shelf as the Library tab (Your stories), plus Continue listening for its stories in progress; no Up next, no Downloads, nothing of anyone else's. Built without React from the same reads (collections, else one library, else the account's downloads), and rebuilt at once when kids mode turns on or off.
usePairingCode takes any mint (a member's own code reuses it), PairingCode is the QR, countdown and copy or share view PairDeviceCard now renders, CheckRow moves out of the collection dialogs for the household pickers and the My devices tick, and RenameDialog takes an onSave for a member's name on the server.
A device whose member_sign_ins is non-empty offers "Also sign out Evie and Ellis on that device", ticked by default (decision B), and passes alsoMembers; the toast counts the members signed out. Names are joined by hand (Hermes has no Intl.ListFormat). This device and devices with no member sign-ins are unchanged.
When a picked share target is the listener's own household member on "Only collections I share", a line says "Evie can listen to everything in this collection." The members list is asked for only while the dialog is open. Uses the shared CheckRow.
…rs UI The Household pane leaves Settings and becomes a page (decision 4): the phone's sixth Me segment between Settings and Account, and /household, a page of the array group pushed by openHousehold from the profile menu, Who's listening? > Manage household, Settings, Home's house row and the palette. /settings?section=household replaces itself with the page. The page holds the people on this device (Add someone is now "Sign in someone who already has an account"), the household members of each server with household_members, and house sharing. Members get a card each (access with collection links and Change, the Kids switch with its confirmation, devices with Sign out, Set up their own device, Sign in on this device, Rename, Delete their account confirmed twice, apart from Remove from this device), and "Add someone to your household" opens the add dialog (name, Kids, What can they listen to? defaulting per decision J; a kid is signed in here through addMemberHere and the owner offered a PIN, a grown-up is shown their pairing code with "They'll also use this phone"). Each refusal reason has its line. Strings under household.* in all six languages.
…, and a member device sign-out refreshes whatever the answer
… their display name
…ies, and pairing never shows the server's words writeError reads ApiError.code (member_limit, username_taken, invalid_name, invalid_collections, feature_off, not_allowed, member_cannot_manage, kid_account, demo_account) with the status as the fallback; 429 is "Too many tries. Wait a few minutes and try again." on every member route, the member's pairing code, Pair another device and the link exchange. The locales for this round (all six), including de/fr "Also sign out ... on that device".
…g, no gaps from closed dialogs Remove from this device for a household member names who can sign them in again instead of the new-invite warning, and reads the owner's household again (member devices, the list, my devices) on that server. A member's kids mode lines name who manages them, never the server's admin. Settings links to Household by the page's own name.
…ber's manager, and the QR and access dialogs - Create <Name>'s books is a ticked new collection made on submit, before addMember or the change (its id in collection_ids), deleted again when the write is refused; Cancel makes nothing. - Renaming a member renames their profile on this device too. - A member's Household says who manages them. - The access dialog asks its question once; the QR step has Done in the dialog footer and the link on one line. - Dialogs sit outside the gapped columns.
…gns them in again A kid whose sign-in ended sees "Ask a grown-up to sign <Name> in again." (no Reconnect button), the stories say the sign-in ended instead of "isn't answering", and no bare Try again is left. /connect is no longer a kids-mode route: a link into the flow (audiosilo://connect, /web/connect?token=) goes back to /kids with a toast, and the connect layout runs none of the flow, so the link's token stays unspent. Only a grown-up's Add someone on a device with no grown-up gets through (openAddSomeone, kidsConnectAllowed). The Household page's On this device card says when a person's sign-in ended and signs them in again: a member the person listening manages with a session the owner mints (renewMemberHere), anyone else through the sign-in step. A kid's account's first sign-in goes straight to kids mode, a second tap on a story still starting is the same start, the kids player shows the spinner while a story loads in, the kids library row moves under its label on a phone and is hidden while the shelf is the owner's shared collections, and Who's listening? says the device is shared only once it is.
The default speed is a grown-up's device-wide setting and kids mode has no speed control, so a kid inherited 1.4x. listenerDefaultRate() is 1 while a kids profile listens; playBook (one line, with its regression test) and the car's native start specs read it. A speed saved for the kid on that story still wins.
…books The password card says only what is true of an account without a password (neutral, or that its household owner set it up), and offers a kid's account no password at all (the server refuses one). "Create <Name>'s books" becomes "Share <Name>'s books" when the owner already has a collection of that name, ticking it instead of making a second one.
… after Sign in again
useKidsSharedShelf now says where the shelf comes from ('shared', 'library',
'unknown'): the library row is hidden while the kid's sign-in ended or the
member's shared collections couldn't be read. The card reads a connection's
queries again once its token changes, in an effect so the queries already hold
the new client. Sign in again says too many tries on a 429, and Remove from this
device is labelled with the action, not the confirm dialog's question.
The wide Settings nav item and the narrow Household card wired
onPress={openHousehold}, so the press event arrived as the nav state and
focusedRoute threw on it. openHousehold now reads the nav state itself, so it
can be any onPress as it is.
… household members alike
…errors branch on it The known refusal codes are a typed union; household wording (writeError, writeErrorText) maps the kinds, and isTooMany and the raw 404/409/429 checks in the 8b-1 code read the kind instead.
…le, the screens don't useCapabilityMutation's one-caller failed hook becomes settled (whatever the answer). A refused add or delete reads the members list again in the hook; a write that shares collections reads them again in the hook; one invalidateHousehold for the three household keys. A member change reads the list and devices again only when they become a kid (the answer is stored), a delete no longer reads again the list it just filtered. RevokeMyDeviceVars is an object only; memberDevices drops the wrapper shape the server never sends.
…erver says kids mode The card, "They'll also use this phone" and a kid just added all go through signInMemberHere (the pre-mint check, the session, addMemberHere, one classification). AddMemberHereError carries a code (already_here, not_ready, choose_adult) that hereFailure says in words, so no translated message travels in an error. addMemberHere turns kids mode on through kidsOnForKidAccount (the server's member_kind) instead of a caller's kids flag, and stores the session the same way as renewMemberHere (storeMemberSession). The kid step reuses hereFailure; household.here.failed no longer repeats failedCard.
…ed_by and member_kind kidsManager moves to the kids model beside kidsConnectionId.
…and the car kidsShelfMode and kidsOfflineScope (pure, in the kids model) decide where the shelf comes from and which downloads stand in offline; one hook (useKidsShelfSource) feeds useKidsStories and useKidsSharedShelf, and the car's readKidsShelf calls the same rules. A signed-out kid is part of the stories (ready.signedOut, or the signed-out state) instead of a flag beside them. collectionsQuery and collectionQuery factories; one newest-first helper for the stories in progress; the sections' books memoised and deduplicated by a Set; KidsSection titles always set (one run is shown untitled by the list). The car reads the shelf alongside the progress (libraries alongside the collections), rebuilds only for the kids connection's collections and never for an answer read again unchanged; retry leaves the libraries alone on a collections shelf; onPlay reads the latest render through useLatest.
The kids screen's module-level de-dup (8b-1, D2) moves into startBookInPlace, keyed by the book and its start options, so a second tap anywhere joins the first start instead of starting the book twice.
…e flow connect/_layout.tsx runs none of the flow for a kids profile without a grown-up's pass, says why when the flow opened in kids mode (a link) and goes back to /kids; the route guard leaves /connect to it. One connect intent object (newPerson, grownUpFromKids) ended when the flow is left (endConnectFlow). finishConnect takes a kids profile straight to /kids. The guard's D1 tests run against the real connect layout now.
…n, not the Household card ApiProvider notices a connection's new token in its effect (after every screen's, so the queries hold the new client) and reads again what failed and the server, libraries and collections lists of that connection only. The card's useRefetchWhenSignedInAgain goes; its regression test moves to provider.test.
… listens src/playback/listener-settings.ts replaces default-rate.ts and the auto sleep controller's effectiveAutoSleepConfig: a pure listenerSettingsFor, the getter playBook, the car and the controller read, and useListenerDefaultRate for screens (time left and a book's speed now follow kids mode's 1x too). Playback internals only change where they read the input.
…de openSettings DevicePeople moves to components/household/device-people.tsx and HouseSharing to house-sharing.tsx (their test, which mounts the Household page, becomes household-page.test.tsx); CheckRow, used by the share sheet and My devices too, moves to components/ui; openHousehold joins openSettings in lib/open.
…s (no duplicate keys)
…/testing/household-fixtures)
…ack, start join, connect gate, car scope - member_disabled (and a gone member) read as such on sign in here, sign in again and the member QR; one list builds the known error codes. - Member writes refresh collection details; sharing changes and collection deletes refresh the members list; refused session, pairing and changes re-read it. - The access picker never adopts the collection its own save created; the new books collection is deleted again only after a definite 4xx. - A start reuses a running one only if no other book started since. - The kids connect gate waits for the profiles to be read. - The car's kids shelf keeps its library for the offline scope and uses the session order; paused reads count as unreachable; member cards list API keys; the Change dialog uses the body cap.
…ollection_ids: [])
…/me read on the account page)
…akes the dead-token path
…server (folded into removeConnection's hook)
…s connect gate; a Household members section
…file here, else display_name, else username) managerName takes the member's connection and this device's people (useDevicePeople, devicePeople, useManagerName in the profiles store): when the owner's own sign-in to that server is on this device, the copy uses the name of the profile holding it, the name "Who's listening?" shows. Kids mode, the Household page, removing a person, the password card and the sign-out warning all read it.
…403 kid_account) The password card already says who signs a kid's account in on a new device. The page also hands the password card its connection, so it names the owner as this device knows them.
A person who leaves a household is signed out of what their old owner set up, so their stored connection keeps the managed_by from before: the On this device card then said "The stories <old owner> shares" on a profile no longer in that household. kidsManager names no one while the kids connection needs a reconnect; each card already reads its own profile's manager, which the new page test pins across several profiles.
A short story's automatic download can finish before its stream reaches playing (the snapshot still loading, or the engine just started with its playing not yet in JS). The hot-swap to the local copy played on only from playing, and iOS's load pauses the player, so a kid's single tap on a story loaded it and stopped on Play with no error. The swap now plays on from the listener's intent (wantsPlayback) too.
This was referenced Oct 13, 2026
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.
Player redesign Phase 8b-1 (★): household members in the player. Pairs with KodeStar/audiosilo-server#127 (merge first), KodeStar/audiosilo-docs#130 and KodeStar/audiosilo#54. No release.
What it does
src/api, capabilityhousehold_members):HouseholdMember(s),AddMemberBody,UpdateMemberBody,MemberKind,MemberAccess;User.managed_by/member_kind/display_name;MyDevice.member_sign_ins; client methods and hooks for the eight member routes (mutations reject withCapabilityErrorwithout the flag);useRevokeMyDevice({ id, alsoMembers });ApiError.code+apiErrorKind(the app never shows a server message)./householdpage elsewhere (/settings?section=householdredirects); On this device (people, PINs, kids mode, Sign in again), Your household (members, Add someone to your household or the reason it isn't offered), Listening in the house.addMemberHere) with a PIN offer; a grown-up gets a pairing QR and "They'll also use this phone".addMemberHere/renewMemberHere/signInMemberHerewithsetSession(..., { makeDefault: false })andplaceConnection(id, { newProfile, keepActive, name }); neverswitchProfile.connect/_layout.tsxis the gate: a link bounces with "Ask a grown-up to open this link", the token unspent); a dead sign-in says "Ask a grown-up to sign in again."; the car's kids snapshot is the shelf only (max 50).managerName).also_members; everything Phase 8 shipped works as before.Playback internals (decision 7), each with its own regression test
playBookreads its default rate fromlistenerSettings()(1x in kids mode).switchCurrentBookToLocal:playingorwantsPlayback). Pre-existing bug the device pass found: a short story's automatic download landed while its start was stillloading, and iOS'sloadpauses, so a kid's first tap loaded and stopped on Play. Reproduced on demand in the iOS Simulator before and after.Gates
tsc, lint, prettier,npm test(400 suites / 4677 tests),expo export -p web; Android and iOS Release device builds.Passes
Deliberately not fixed (follow-ups)
memberConnectionwithpriorConnection(the latter matches a legacy connection, which could point Remove from this device at the owner)./code-review ultranot run (optional).