Listening in the house
Listening in the house is an opt-in server feature (capability household): people
on one server who choose to share see what the others who share are listening to.
The server owns every privacy rule
(what other people see;
the wire is in the reference). The
player only decides what to show of the answer, where, and how often to ask.
The wire and the hooks (src/api/)
Types HouseholdSettings, HouseListeningState, HouseListeningRow and
HouseListening in types.ts; client.household(signal?),
client.setHousehold(share) and client.houseListening(signal?) (which reads a
missing listening as []) in client.ts. The hooks in hooks.ts, each gated on
household (useCapabilityQuery / useCapabilityMutation, so a server without the
flag is never asked):
useHousehold(cid?)- the caller's own{ share_listening },qk.household(cid), withstaleTime: Infinity: it never goes stale by time, since this device only changes it throughuseSetHousehold(which stores the server's answer). The foreground refresh (refreshAfterResume) and a reconnect still refetch it, like every active query, so a change made on another device shows the next time the app (or the web tab) comes back to the foreground.useSetHousehold(cid?)- the switch. Rejects withCapabilityError(sending nothing) while the flag is off or unknown; never hand that tonoteError. It stores the server's answer underqk.household(cid)and invalidatesqk.houseListening(cid), since a change of the caller's own choice opens or empties the house (it is reciprocal). A demo account'sPUTis a403.useHouseListening(cid?, { enabled })- the house,qk.houseListening(cid).enabled: falsesends no request at all.
When the house is asked
Polling only; there is no realtime channel.
- Every minute while the app is in the foreground (
HOUSE_POLL_MS, 60 s).housePollInterval()answersfalseunlessAppStateisactive, so a suspended or hidden native app asks nothing (React Query's own focus check covers a hidden web tab); the foreground refresh reads it again on return, which restarts the interval.useCapabilityQuerygained arefetchIntervaloption for this. - Only while the listener shares.
useHouseRows(cid, active)(src/components/house/use-house-rows.ts) asksuseHouseholdfirst and enablesuseHouseListeningonly whenshare_listeningistrue: most listeners never turn sharing on, and they never poll. - Only while the surface is on screen.
activeisuseIsFocused()of the Home screen or the book page, so a Home kept mounted under another tab stops asking.
Loading, an error and "not sharing" all read as no rows: the house is an extra, never an empty section or an error banner.
The house model (house-model.ts)
Pure and tested (house-model.test.ts):
visibleRows(house, sharing)- nothing while the caller doesn't share (a cached answer can outlive turning sharing off), one row per person (the first, newest, wins), in the server's newest-first order.rowsForBook(rows, libraryId, path)- the housemates on one book (path is the identity).houseWhen(row, today)-playingandpausedas the server says; forrecent, the local calendar day of the newest save, as oftoday(the local day's start fromuseToday(), which ticks over just after midnight), worked out by hand (Hermes has noIntl.RelativeTimeFormat):today,yesterday, orearlierfor a clock that disagrees with the server's 24 hours. A save in the future or one that doesn't parse counts as today.chapterName(row, t)- the chapter's title, else "Chapter N" fromchapter_index(a chapter titled "Track 01"), else nothing.houseCopy(row, today, t)- the title (the server's, else the path's last segment), the state line ("Listening now · Chapter 4") and one sentence for a screen reader ("sam is listening to Unsouled, Chapter 4").houseCardWidth(width, layout)andHOUSE_GAP- the Home grid (aHOUSE_GRIDspec for the sharedgridMetrics): cards at least 250 wide, 12 apart, laid out by the row's measured width (the Up next drawer can take part of the window), one per row on a phone.
Strings live in the house i18n namespace.
The components (src/components/house/)
HouseRow({ connections })- Home's "Listening in the house", placed after "Next in your series" and before the smart shelves (home-screen.tsx). OneHomeSectionper connection whose server hashousehold, where the listener shares and someone else who shares listened in the last day; it renders nothing otherwise. The section's sub is "<server name>· shared by choice" and its action "Sharing settings" opens Settings at the household pane (openSettings('household')). EachHouseCardis the person's avatar and name (a static green dot after the name while they are listening now), the book's title, the state line and its cover; tapping it opens the book on that connection (openBook).InTheHouseCard({ connectionId, libraryId, path })- the book page's "In the house" card, last in the aside (book-aside.tsx): the housemates on this book, each an avatar, the name and the state line, read from the same cached answer as Home. Renders nothing when nobody is.HouseSharingRow({ connectionId, first? })- "Share what I'm listening to with the house" for one connection, with the privacy sentence "Your current book shows on the Home page of other people on<server>who share theirs too. Off by default." Hidden when the connection is gone; it does not checkhouseholditself (the Household pane renders it only for servers that advertise the flag, below). The switch moves at once (optimistic) and settles on the server's answer; a refusal puts it back with a toast (a demo account gets its own words); while it loads or saves the switch waits disabled, and a failed load offers Retry.- The avatars are
Portraitwithkind="user"(src/components/series/portrait.tsx): a gradient disc with white initials, coloured from the name (the account page's own portrait, so a person looks the same everywhere). Decorative: the name is always written beside it or in the control's label. Passring(true or false) wherever the pink "who is listening on this device" ring can show: the portrait then keepsRING_ROOM(4 pt) around itself and draws the ring in it, so ringing one never moves the layout. Leaveringout where no ring ever shows.
Whose connections. Everything here is the person listening's: Home hands
HouseRow the active profile's connections (useProfileConnections), and
Settings > Household and sharing renders one HouseSharingRow per connection of the
active profile whose server has household, under a "Listening in the house" heading
(a note when none of them offers it, once every server has answered). See
Profiles.
Testing
house-model.test.ts (the rules above), house-row.test.tsx,
in-the-house-card.test.tsx and house-sharing-row.test.tsx beside the components;
the avatar and its ring in src/components/series/portrait.test.tsx; the hooks' gating and polling in
src/api/hooks-household.test.tsx; the client calls in client.test.ts.