Skip to content

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

Merged
bmc08gt merged 2 commits into
code/cashfrom
feat/chat-roster-edit-rpcs
Sep 22, 2026
Merged

bmc08gt merged 2 commits into
code/cashfrom
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. com.flipcash:flipcash2-client-protocol:0.11.0 is not on Maven Central
yet, so CI will report FAILED on resolution until it is published. This stays a draft until
then. The work here was developed and verified against the client repo's checkout through
protoLocalRoot, which is untracked and invisible to CI.

What this adds

ChatApi / ChatService gain getRoster and editChat, surfaced through ChatController and
InternalChatRepository, with result-enum mapping in Errors.kt — DENIED/NOT_FOUND for the
first, and DENIED/NOT_FOUND/TITLE_MODERATED/PICTURE_BLOB_NOT_ACCEPTED plus the tripped
FlaggedCategory for the second. New domain models RosterPage and EditChatParameters.

ChatMember carries the new joinedAt and version; ViewerState carries canEdit. The
event-stream delegate and the metadata persistence layer handle the two new MetadataUpdate
cases, TitleChanged and PictureChanged.

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 ChatMember.version, greater
winning. RosterPage carries what that needs, but no merge exists yet because nothing consumes
getRoster — whoever builds the roster UI has to write it rather than treating a page as
authoritative.

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.

…h2 override

Adds the service/repository/controller chain for chat.v1's two new RPCs,
against the flipcash2-client-protocol composite build local.properties
already points at:

- GetRoster pages a chat's roster (result mapped to DENIED/NOT_FOUND,
  page data to a new RosterPage model).
- EditChat edits a group's title/picture (result mapped to
  DENIED/NOT_FOUND/TITLE_MODERATED/PICTURE_BLOB_NOT_ACCEPTED, the last
  carrying flagged_category, following the StartChat error shape).

Carries the model changes the proto brought with the RPCs: Member gains
joinedAt/version (the per-member merge key GetRoster's paging relies on,
greater version wins), ViewerState gains a Permissions.canEdit flag, and
MetadataUpdate gains TitleChanged/PictureChanged cases, wired into
EventStreamDelegate's existing exhaustive when-block.

Persistence gets targeted updateTitle/updatePicture queries alongside the
existing whole-row upsert, since ChatMetadataEntity already has the
columns but no single-field update path for them.

can_edit is mapped from the wire but not persisted: no column exists for
it on ChatMetadataEntity, and adding one is a schema migration this pass
doesn't attempt. UI gating on can_edit and the picture-upload flow
EditChat's Picture.blob_id assumes are both left for follow-up work.

RosterStateHolder still gates its cached roster on whole-roster version
and repairs gaps via GetChat; it doesn't call GetRoster or apply its
per-member merge rule yet, so a large group's paged roster can still lag
behind what GetRoster's own staleness contract would let a client catch
up to.
0.11.0 isn't published to Maven Central yet; it lands once the client
repo cuts the release carrying GetRoster/EditChat. Locally the build
resolves anyway, since local.properties' protoLocalRoot/protoLocalPackages
override substitutes the composite build for any version string on
`com.flipcash:flipcash2-client-protocol`. CI has no such override, so it
can't resolve 0.11.0 until the real release exists - the PR goes up as a
draft for that reason.
@bmc08gt bmc08gt self-assigned this Sep 21, 2026
@github-actions github-actions Bot added type: feature New functionality area: network gRPC, connectivity, API, exchange rates area: build-system Gradle, convention plugins, build-logic and removed type: feature New functionality labels Sep 21, 2026
@bmc08gt
bmc08gt marked this pull request as ready for review September 21, 2026 22:38
@bmc08gt
bmc08gt merged commit c412a86 into code/cash Sep 22, 2026
4 of 5 checks passed
bmc08gt added a commit that referenced this pull request Sep 22, 2026
Adds the Edit entry point and the two edits behind it, on top of the
editChat/canEdit scaffolding in #1524.

The profile's top-right overflow holds one row, Edit, and is drawn only when
ViewerState.Permissions.canEdit says so. That field is server-computed and is
passed in rather than derived: membership, being the creator, and the chat's
kind all look like they answer the question, and a local guess would offer an
Edit whose Save comes back DENIED.

Edit opens a list of what can be changed. The design node shows four rows;
only Icon and Name are built, because the other three have no contract support
in flipcash2 0.11.0.

EditChat leaves a field unchanged when it is unset, so each edit sends only its
own field. titleOnly and pictureOnly name that rule in one place -- built
inline at each call site it held by inspection, and an EditChatParameters(title
= ..., picture = state.picture) written later, in the shape most object updates
take, would overwrite the other field with whatever the screen was holding.

Title length is checked in code points, not String.length. protovalidate's
max_len counts code points, so 64 emoji measure 128 in Kotlin and a UTF-16
count would refuse titles the contract accepts.

On OK the response metadata is persisted directly rather than refetched. The
titleChanged/pictureChanged echo carries absolute values, so the stream
applying it afterwards is idempotent.

The moderation-category wording moves from CreateGroupViewModel to
core/moderation so an edit refuses a title in the same words creation does.
bmc08gt added a commit that referenced this pull request Sep 22, 2026
* feat(chat): edit a group's name and icon

Adds the Edit entry point and the two edits behind it, on top of the
editChat/canEdit scaffolding in #1524.

The profile's top-right overflow holds one row, Edit, and is drawn only when
ViewerState.Permissions.canEdit says so. That field is server-computed and is
passed in rather than derived: membership, being the creator, and the chat's
kind all look like they answer the question, and a local guess would offer an
Edit whose Save comes back DENIED.

Edit opens a list of what can be changed. The design node shows four rows;
only Icon and Name are built, because the other three have no contract support
in flipcash2 0.11.0.

EditChat leaves a field unchanged when it is unset, so each edit sends only its
own field. titleOnly and pictureOnly name that rule in one place -- built
inline at each call site it held by inspection, and an EditChatParameters(title
= ..., picture = state.picture) written later, in the shape most object updates
take, would overwrite the other field with whatever the screen was holding.

Title length is checked in code points, not String.length. protovalidate's
max_len counts code points, so 64 emoji measure 128 in Kotlin and a UTF-16
count would refuse titles the contract accepts.

On OK the response metadata is persisted directly rather than refetched. The
titleChanged/pictureChanged echo carries absolute values, so the stream
applying it afterwards is idempotent.

The moderation-category wording moves from CreateGroupViewModel to
core/moderation so an edit refuses a title in the same words creation does.

* fix(messenger): give the group profile overflow a leading icon and cover its anchor

DropdownMenu lays out to a 112dp minimum width. "Edit" fills about 24dp of
that at a 13dp inset, so the menu rendered as a mostly empty rounded
rectangle with a word in one corner. MenuItem already carries an icon for
this action; the menu just never drew it. Passing it as leadingIcon fills
the leading space and gives the trailing space something to measure
against, matching the rows on the screen behind the menu at the same 24dp.

The menu also dropped clear of the overflow button, which pushed its first
row toward the middle of the screen and left the button stranded above it.
Chrome's toolbar overflow opens over its button instead. Offsetting by the
anchor's measured height does the same here; AppBarDefaults.Overflow sizes
its own circle, so the pull-up is read from the anchor rather than
hardcoded.

ChatTopBar's message menu still drops below its anchor.

* feat(messenger): confirm a group name or photo change before sending it

Save now raises a BottomBarManager prompt, and the prompt's action is what
calls EditChat — the same shape UserProfileViewModel uses to confirm these
two edits on the user's own profile. Both changes are visible to everyone
in the group, which is the case for asking first.

The gate is a view model event rather than screen state so a test can hold
it: SaveClicked sets nothing in motion, and only the confirmation dispatches
SubmitTitle/SubmitPicture. The name screen's keyboard Done routes through
the same event and so re-reads canSubmit, which the Save button draws itself
from but Done never consulted.

Both view models now take their event dispatcher from DispatcherProvider.
BaseViewModel otherwise publishes on Dispatchers.Default, which no test
scheduler can drain, so Initialize was overwriting a draft the test had
already typed.

Titles alongside: the Icon row is Photo and its screen is Set Photo, Edit
Group is centred to match the screens under it, and the name screen drops
its title — the field is the screen, and the row that opened it said Name.

* refactor(messenger): call the group's image a Picture, not a Photo

It is what EditChatRequest.picture writes and what the rest of the app
already calls it, so the design's "Icon" resolves to Picture rather than
to a third word. Renames the row label, the screen title, the prompt and
the menu item object together, so the string ids match their values.

* fix(messenger): draw the edit confirmations neutral, not destructive

showAlert paints the bar red, which reads as a warning about something
being taken away. Both edits it guards are additive and reversible; what
justifies asking at all is that the new name or picture goes to everyone
in the group, and the prompts already say so.

NameEntryViewModel and PhotoSelectionViewModel confirm the same two edits
on the user's own profile with showMessage and keep showAlert for the
failures. These now match on both counts.

* fix(messenger): confirm group edits on the alert style, not the neutral one

The account screens confirm these same two edits with showMessage, and
matching them is what 6416369 changed. But those edits are the user's
own profile. A group's name and picture change for everyone in the chat,
and showAlert is the weight that carries.

Reverts 6416369.

* feat(messenger): head the group name edit with Set Name

The screen had no title at all, so it opened on a bare field with nothing
saying what the field was for. It now carries an inline heading the way
NameEntryScreen does: no app bar title, a textLarge Text at 70% width above
the field.

That asymmetry with the picture screen, which keeps its app bar title, is
the account flow's own split rather than an inconsistency — PhotoSelectionScreen
titles its bar and NameEntryScreen heads its content.

* chore(messenger): drop the trailing periods from the edit confirmations

Most of the bottom bar's prompt descriptions end without one — 16 of the
20 prompt_description_* strings — so these two were the outliers.

* fix(messenger): line up the picture screen's back button with every other one

The bar was nested in the CodeScaffold that carries padding(horizontal =
inset), so the back control started at inset + grid.x2 — 20dp further in
than the same control on the name screen beside it. Hoisting the bar into
a Column above the scaffold is how PhotoSelectionScreen, NameEntryScreen
and EditGroupNameScreen all place theirs, and it leaves the centred title
centred against the full screen width rather than the inset body.
@bmc08gt
bmc08gt deleted the feat/chat-roster-edit-rpcs branch September 23, 2026 16:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: build-system Gradle, convention plugins, build-logic area: network gRPC, connectivity, API, exchange rates

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant