Skip to content

feat(mobile): view a space's own people from inside a space - #980

Open
Deeds67 wants to merge 14 commits into
mainfrom
feat/mobile-space-people
Open

feat(mobile): view a space's own people from inside a space#980
Deeds67 wants to merge 14 commits into
mainfrom
feat/mobile-space-people

Conversation

@Deeds67

@Deeds67 Deeds67 commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

On the web, a shared space has its own People tab. On mobile there was no equivalent — the global People page mixes every space's people together, and nothing inside a space scoped that list down. This adds a space-scoped People page and the way into it.

What you get

A face icon in the space detail app bar (next to Members) opens the space's own people: the same grid, search and sort as the global People page, scoped to one space. Tapping a face opens that person's photos. Editors can rename and set birthdays; viewers see read-only names.

The icon is hidden when a space has face recognition explicitly disabled, mirroring the web SpaceTabs.

Why it is mobile-only

No server, SDK or translation work was needed. GET /shared-spaces/{id}/people already exists, the Dart SDK bindings are already generated, the space-aware write routing already exists, and every string the page needs is already in all nine locales.

Notable design points

  • The space-scoped endpoint, not a filter over the global list. PersonResponseDto.primaryProfile is singular, so filtering getAllPeopleWithSharedSpaces by spaceId would silently drop a person who belongs to two spaces from their non-primary one.
  • No offline fallback, deliberately. The person/asset_face sync streams are owner-scoped, so space people have no local Drift rows at all — and the owner-scoped local list contains people who are not in this space. Degrading to it would show wrong data, not stale data, so a load failure gets a real error state with Retry.
  • The grid is shared, and its edit gating stayed reactive. PeopleGrid is extracted from the global People page. Editability had to keep resolving inside a ConsumerWidget — it starts optimistically true and rebuilds to false once a viewer's role arrives — because flattening it into a precomputed bool would leave viewers holding rename affordances that fail server-side.
  • The provider is autoDispose. This page's entry path (Spaces tab → space detail → app-bar icon) invalidates nothing, unlike the Library tab which busts the global list on every entry. Without disposal the list stayed frozen for the whole app session.

Paging

The endpoint returns a bare array with no hasNextPage envelope, so termination is batch.length < pageSize. The awkward case is a person count that is an exact multiple of the page size, which needs one extra probe returning empty — covered by its own test.

Tests

112 tests across 9 files, including the paging boundary, Optional absent-field handling (Absent.value throws), the reactive edit path, all three visibility cases for the entry point, and the empty / no-match / error states. The two pre-existing regression guards — drift_people_collection_test.dart and person_api_repository_test.dart — pass unmodified, which is the signal that both extractions were behaviour-preserving.

Design and plan: docs/superpowers/specs/2026-08-12-mobile-space-people-design.md.

Out of scope

Hide/unhide, merge, the face-statistics header and deduplicate are web-only today and absent from every mobile people surface. Pets are not a separate surface — the server includes them in this list when a space enables them.

Deeds67 added 14 commits August 12, 2026 22:12
Adds a design for viewing a shared space's own people from inside the
space on mobile, matching the web space People tab.

The feature is mobile-client-only: the server endpoint, the Dart SDK
bindings, the space-aware write routing and every i18n key already
exist. What is missing is the list fetch, the page and the entry point.

Specifies BDD acceptance criteria (38 scenarios) with a TDD slice
order, and records the traps the implementation has to survive:
Optional.value throwing on absent fields, paging termination without a
hasNextPage envelope, the reactive editability the PeopleGrid
extraction must preserve, and the absence of any correct offline
fallback for people who have no local Drift rows.
Seven tasks, each a full RED-GREEN-commit cycle with the real test and
implementation code inline. Covers all 38 spec scenarios.

Also refines the spec: B27 is asserted on the PeopleGrid callback rather
than through a router harness, which would have to build the whole
DriftPersonPage and its timeline providers just to observe a push.
- The PeopleGrid extraction renamed the CircleAvatar key, which
  drift_people_collection_test.dart:168 resolves the avatar by. That
  would have tripped the plan's own must-pass-unmodified guard.
- B26 was claimed but never asserted: the sort test only proved the
  search filter survived a re-sort, not that the order changed.
- The max-page guard test allocated 100k DTOs; pageSize and maxPages
  are now defaulted test seams.
- The LayoutBuilder move was described as verbatim when it changes the
  measured width by the horizontal safe-area insets.
Caught in the pre-flight scan: every type person.model.dart would have
provided there is inferred, and unused_import is fatal under
dart analyze --fatal-infos.
Both People repositories need this ordering. Homing it in people.utils.dart
would pull flutter/material and the person-edit modals into the repository
layer, so it gets its own dependency-free file.
Pages the membership-gated shared-space people endpoint and maps the
profiles onto DriftPerson. The endpoint returns a bare array with no
hasNextPage envelope, so a short page is the only end-of-list signal —
the exactly-a-full-page case needs one extra probe to terminate.
Deliberately has no local fallback: the owner-scoped Drift list holds
people who are not in this space, so failing loudly beats showing the
wrong ones.
Both People pages need the same grid; the only difference is how
editability is answered. That stays reactive on the global page — a
viewer's role arrives after first paint, so flattening it into a plain
predicate would leave rename affordances that fail server-side.
- dart format the two lib files and the grid test (review finding 1):
  the tall-style formatter collapses a no-longer-necessary split in the
  PeopleGrid constructor and the search-filter contains() call once the
  outer LayoutBuilder indentation is gone.
- add positive anchors to the "no add-a-name" and hidden-person grid
  tests so they can't pass by rendering nothing (finding 2).
- cover PerPersonSpaceRole directly: a space person whose editability
  resolves to false renders read-only, and a personal (null spaceId)
  person is editable without any provider override — the reactive path
  the sealed-class design exists for previously had no direct test
  (finding 3).
- point the ValueKey-preservation comment at the avatarUrl helper
  instead of a line number that had already drifted by one (finding 4).
dart format --set-exit-if-changed over lib/ is a CI gate; the
declaration was written pre-split and Dart 3.12's tall style
lays it out differently.
Mirrors the global People page's app bar and grid, scoped to one space.
Unlike that page it has no local list to fall back on, so a load failure
gets a real error state with retry instead of a silent degrade.

Also imports the new page into router.dart: router.gr.dart is a `part of`
router.dart, so the generated SpacePeopleRoute needs the page type in
scope to compile at all. This is not route registration (no AutoRoute
entry is added to the routes list) — that stays Task 6's job.
The visibility rule lives in its own widget so it can be tested without
pumping SpaceDetailPage, which loads network metadata, members and a
Drift timeline.
The edit modals are shared, so renaming a space person from the global
People page has to refresh the space page as well.
The page's entry path invalidates nothing, so a non-autoDispose
provider froze the list for the session. Also asserts the no-match
interpolation, logs the load failure, and covers the comparator's
tiebreak branches.
@Deeds67 Deeds67 added the changelog:feat Feature change for changelog label Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog:feat Feature change for changelog 📱mobile

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant