Skip to content

feat(chat): scaffold GetRoster and EditChat - #824

Merged
bmc08gt merged 2 commits into
mainfrom
feat/chat-roster-edit-rpcs
Sep 21, 2026
Merged

bmc08gt merged 2 commits into
mainfrom
feat/chat-roster-edit-rpcs

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator

Scaffolds the service layer for the two RPCs added in
code-payments/flipcash2-client-protocol#17, and pins the release that carries them.

Blocked on that PR. The exact: requirement on flipcash2-client-protocol now names 0.11.0,
which has no tag yet, so CI cannot resolve the package until it is published. This stays a draft
until then. The work here was developed and verified against the client repo's checkout through
FLIPCASH_PROTO_LOCAL, a shell variable CI never sees.

What this adds

ChatService gains getRoster and editChat, with ErrorGetRoster and ErrorEditChat mapping
the result enums explicitly rather than positionally. That matters here: this codebase maps
result enums through Error*(rawValue:), so a case inserted upstream renumbers everything after
it. Nothing was inserted this time, but the two new enums are written so a future insertion is a
compile error rather than a silent mis-mapping.

FlipClient+Chat wraps both; getRoster pages to exhaustion into a single RosterPage.
ConversationMember carries the new joinedAt and version, ConversationViewerState carries
canEdit, and ConversationStreamEvent / ConversationStore / ConversationController decode
and apply the two new MetadataUpdate cases at every existing switch site over that oneof.

Two things a caller has to know

A roster page is not a snapshot. A large group's page is served from an index that trails
membership writes, and the client cannot tell that case from a small group's whole-roster read.
Pages have to be merged against the event stream per member, by ConversationMember.version,
greater winning. The types carry what that needs and the rule is in the doc comments on
FlipClient.RosterPage and ChatService.getRoster, but no merge exists yet because nothing
consumes getRoster — whoever builds the roster UI has to write it.

canEdit is server-computed and not derivable. Gate the edit affordance on the flag alone,
not on membership or chat kind.

Deliberately not included

No canEdit-gated UI, and no picture-upload flow — editChat takes a BlobId for a blob already
uploaded and READY via BlobStorage. Both are feature work on top of this scaffolding.

flipcash2-protobuf-api dd5e92db -> 4ccbbe43 adds Chat.GetRoster and
Chat.EditChat, plus Member.joinedAt/version and
ViewerState.Permissions.can_edit. Wire the app side against the local
path override, ahead of the published package bump:

- ChatService: getRoster (paged) and editChat, each with an explicit
  Result-enum mapping (ErrorGetRoster, ErrorEditChat) rather than
  positional rawValue: — EditChatResponse.Result has five cases against
  this file's usual three, so a positional match would silently
  renumber if a case were inserted upstream.
- FlipClient+Chat: async wrappers, including roster paging to
  exhaustion behind a single RosterPage read.
- ConversationMember gains joinedAt/version (the GetRoster merge key);
  ConversationViewerState gains canEdit.
- ConversationStreamEvent/Store/Controller handle the two new
  MetadataUpdate cases, titleChanged and pictureChanged, applied
  best-effort since neither carries a version to compare.
- Tests for the new field mappings, event decoding, and error
  classification.

Deliberately left unscaffolded: any can_edit-gated UI affordance, and
the picture-upload flow EditChat depends on (BlobStorage upload of the
original, then passing the resulting BlobId) — both are UI/flow work
this pass doesn't touch.
0.11.0 isn't published yet — it ships with the GetRoster/EditChat
client release. FLIPCASH_PROTO_LOCAL replaces this exact: requirement
with a path dependency, so a local build ignores the pin; CI resolves
it and can't yet, which is why the PR goes up as a draft.
@bmc08gt bmc08gt self-assigned this Sep 21, 2026
@bmc08gt
bmc08gt marked this pull request as ready for review September 21, 2026 22:34
@bmc08gt
bmc08gt merged commit 49d1231 into main Sep 21, 2026
1 check failed
bmc08gt added a commit that referenced this pull request Sep 22, 2026
#824 added a non-optional `canEdit` to `ConversationViewerState` without
bumping the schema version. A blob written by an earlier build has no
`canEdit` key, so decoding throws `keyNotFound`; the read site uses `try?`,
so the whole viewer state degrades to nil. That loses mute state as well as
the new permission, and it persists until something refetches the chat.

Observed on a simulator upgraded across #824: every conversation held
`{"version":N}` with no `canEdit`, and `Conversation.canEdit` read false for
two groups the signed-in user created. After the bump the store rebuilt and
the same two rows came back `{"canEdit":true,...}`.

A rebuild costs a resync and loses nothing: every column in the affected
tables is server-derived, and drafts live in a separate file. That is the
project's stated substitute for migrations, so use it rather than teaching
the decoder to tolerate the older shape.
bmc08gt added a commit that referenced this pull request Sep 22, 2026
…e writes

The rule named `SQLiteVersion` in Info.plist, which moved into code as
`Database.schemaVersion` when the notification service extension needed the
same number. It also only covered table definitions, so #824 read as exempt: it
added a non-optional property to a `Codable` persisted as a JSON blob, leaving
the column's SQL type untouched while making every row written by an earlier
build fail to decode. Because those reads are `try?`, the whole viewer state
silently became nil — losing mute state, not just the new field.

Both facts are now in the rule, along with why a tolerant decoder is the wrong
fix: it keeps a stale value where the bump refetches the correct one, and it is
a migration under another name.
bmc08gt added a commit that referenced this pull request Sep 22, 2026
#824 added a non-optional `canEdit` to `ConversationViewerState` without
bumping the schema version. A blob written by an earlier build has no
`canEdit` key, so decoding throws `keyNotFound`; the read site uses `try?`,
so the whole viewer state degrades to nil. That loses mute state as well as
the new permission, and it persists until something refetches the chat.

Observed on a simulator upgraded across #824: every conversation held
`{"version":N}` with no `canEdit`, and `Conversation.canEdit` read false for
two groups the signed-in user created. After the bump the store rebuilt and
the same two rows came back `{"canEdit":true,...}`.

A rebuild costs a resync and loses nothing: every column in the affected
tables is server-derived, and drafts live in a separate file. That is the
project's stated substitute for migrations, so use it rather than teaching
the decoder to tolerate the older shape.
bmc08gt added a commit that referenced this pull request Sep 22, 2026
…e writes

The rule named `SQLiteVersion` in Info.plist, which moved into code as
`Database.schemaVersion` when the notification service extension needed the
same number. It also only covered table definitions, so #824 read as exempt: it
added a non-optional property to a `Codable` persisted as a JSON blob, leaving
the column's SQL type untouched while making every row written by an earlier
build fail to decode. Because those reads are `try?`, the whole viewer state
silently became nil — losing mute state, not just the new field.

Both facts are now in the rule, along with why a tolerant decoder is the wrong
fix: it keeps a stale value where the bump refetches the correct one, and it is
a migration under another name.
bmc08gt added a commit that referenced this pull request Sep 22, 2026
…e writes

The rule named `SQLiteVersion` in Info.plist, which moved into code as
`Database.schemaVersion` when the notification service extension needed the
same number. It also only covered table definitions, so #824 read as exempt: it
added a non-optional property to a `Codable` persisted as a JSON blob, leaving
the column's SQL type untouched while making every row written by an earlier
build fail to decode. Because those reads are `try?`, the whole viewer state
silently became nil — losing mute state, not just the new field.

Both facts are now in the rule, along with why a tolerant decoder is the wrong
fix: it keeps a stale value where the bump refetches the correct one, and it is
a migration under another name.
bmc08gt added a commit that referenced this pull request Sep 22, 2026
…e writes

The rule named `SQLiteVersion` in Info.plist, which moved into code as
`Database.schemaVersion` when the notification service extension needed the
same number. It also only covered table definitions, so #824 read as exempt: it
added a non-optional property to a `Codable` persisted as a JSON blob, leaving
the column's SQL type untouched while making every row written by an earlier
build fail to decode. Because those reads are `try?`, the whole viewer state
silently became nil — losing mute state, not just the new field.

Both facts are now in the rule, along with why a tolerant decoder is the wrong
fix: it keeps a stale value where the bump refetches the correct one, and it is
a migration under another name.
bmc08gt added a commit that referenced this pull request Sep 22, 2026
* feat(chat): edit a group's name and icon

Puts an overflow menu on the chat profile screen with a single Edit item, gated
on the server-computed `canEdit` permission and nothing else, and pushes a list
with two rows behind it: Icon and Name.

The design (node 10187:110373) draws four rows. Only Icon has contract support —
`EditChatRequest` carries a title and a picture, so Membership Card, Description
and Social Links would be rows that cannot save. Name is the counterpart the
design omits, styled to match Icon.

Both rows are partial updates. `EditChat` treats an unset field as unchanged, so
each screen submits only its own field, which is what keeps a rename from
clearing the picture. The title goes through the existing `GroupTitleValidator`
for the contract's 1...64 before it is sent, and the picture waits for the blob
to reach READY — a pending blob is answered `PICTURE_BLOB_NOT_ACCEPTED`.

On OK the response metadata is applied through the same store mutators the
`titleChanged` and `pictureChanged` stream events use, rather than through
`.metadataRefresh`. Both are last-write-wins assignments, so the server's echo
re-applies identical values instead of landing the edit twice, and the held
membership and viewer state survive — a wholesale upsert would drop them.

Error copy mirrors the StartChat mapping case for case, so the same server
answer reads the same way wherever a group name is submitted. A moderated title
keeps the user on the edit screen with the field as they typed it.

* fix(store): bump schema version so viewer state is refetched

#824 added a non-optional `canEdit` to `ConversationViewerState` without
bumping the schema version. A blob written by an earlier build has no
`canEdit` key, so decoding throws `keyNotFound`; the read site uses `try?`,
so the whole viewer state degrades to nil. That loses mute state as well as
the new permission, and it persists until something refetches the chat.

Observed on a simulator upgraded across #824: every conversation held
`{"version":N}` with no `canEdit`, and `Conversation.canEdit` read false for
two groups the signed-in user created. After the bump the store rebuilt and
the same two rows came back `{"canEdit":true,...}`.

A rebuild costs a resync and loses nothing: every column in the affected
tables is server-derived, and drafts live in a separate file. That is the
project's stated substitute for migrations, so use it rather than teaching
the decoder to tolerate the older shape.

* feat(chat): confirm group edits, and call the row Picture

Saving a group's name or picture changes it for everyone in the group, so
neither leaf screen sends on the first tap any more: Save opens a confirmation
alert and the alert commits, the way the My Account fields already work. Both
take the red banner — where the profile confirmations weigh whether the user
can get their own old value back, this edit lands on everyone at once and no
later undo takes back what they have seen.

The row is Picture, not Icon, which is what `EditChatRequest.picture` carries
and what Android ships. Its glyphs come from Android too — `photo` and
`textformat` against `Icons.Outlined.Image` and `Icons.Outlined.TextFields` —
so the screen reads the same on both platforms. They need `resizable()` to
hold the 26pt column: `textformat` is wider than tall, and a frame alone left
it overflowing and the labels misaligned.

The name screen loses its bar title. The field it opens on is the name, so a
title only repeated it back.

* docs(claude): state the schema-version rule in terms of what the store writes

The rule named `SQLiteVersion` in Info.plist, which moved into code as
`Database.schemaVersion` when the notification service extension needed the
same number. It also only covered table definitions, so #824 read as exempt: it
added a non-optional property to a `Codable` persisted as a JSON blob, leaving
the column's SQL type untouched while making every row written by an earlier
build fail to decode. Because those reads are `try?`, the whole viewer state
silently became nil — losing mute state, not just the new field.

Both facts are now in the rule, along with why a tolerant decoder is the wrong
fix: it keeps a stale value where the bump refetches the correct one, and it is
a migration under another name.
bmc08gt added a commit that referenced this pull request Sep 22, 2026
…#827)

main does not build from its committed pins. `AllTargets` fails with
`cannot find type 'ReportReason' in scope`, and testing is cancelled
before a single test runs.

#821 added the report flow against `SharedCoreKit.ReportReason` and
developed it through the `FLIPCASH_SHARED_CORE_LOCAL` path dependency
that the same PR introduced. `Package.resolved` was correctly restored
to the pinned 0.7.0 before committing, as that manifest's comment
instructs, but the shared-core release carrying the type was never cut,
so nothing published provided it.

shared-core 0.7.1 publishes it. `.upToNextMinor(from: "0.7.0")` already
admits 0.7.1, so only the resolved pin moves and the three manifests
consuming SharedCoreKit stay untouched.

The same file also still pinned flipcash2-client-protocol at 0.10.0.
`FlipcashAPI/Package.swift` requires the contract packages `exact:`, and
#824 moved that declaration to 0.11.0 without committing the resolution
it forces, so every build since has rewritten the file locally. Commit
that too rather than leave the checked-in state unbuildable as written.
@bmc08gt
bmc08gt deleted the feat/chat-roster-edit-rpcs branch October 5, 2026 15:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant