feat(mobile): view a space's own people from inside a space - #980
Open
Deeds67 wants to merge 14 commits into
Open
feat(mobile): view a space's own people from inside a space#980Deeds67 wants to merge 14 commits into
Deeds67 wants to merge 14 commits into
Conversation
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.
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.
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}/peoplealready 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
PersonResponseDto.primaryProfileis singular, so filteringgetAllPeopleWithSharedSpacesbyspaceIdwould silently drop a person who belongs to two spaces from their non-primary one.person/asset_facesync 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.PeopleGridis extracted from the global People page. Editability had to keep resolving inside aConsumerWidget— it starts optimisticallytrueand rebuilds tofalseonce a viewer's role arrives — because flattening it into a precomputed bool would leave viewers holding rename affordances that fail server-side.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
hasNextPageenvelope, so termination isbatch.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,
Optionalabsent-field handling (Absent.valuethrows), 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.dartandperson_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.