Skip to content

feat(chat): edit a group's name and icon - #1525

Merged
bmc08gt merged 9 commits into
code/cashfrom
feat/edit-chat-ui
Sep 22, 2026
Merged

bmc08gt merged 9 commits into
code/cashfrom
feat/edit-chat-ui

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #1524 — base is feat/chat-roster-edit-rpcs, which carries the editChat/canEdit scaffolding this builds on. Review after that one; rebase onto code/cash once it merges.

Entry point

The group profile gains a top-right overflow with a single row, Edit. It is drawn only when ViewerState.Permissions.canEdit says so, and nothing at all is drawn otherwise — an overflow button that opens an empty menu is worse than no button.

canEdit is server-computed and passed in rather than derived locally. Membership, being the creator, and the chat's kind all look like they answer the question and none of them is the answer, so a local guess would offer an Edit whose Save comes back DENIED — which reads as the app being broken rather than as a permission.

The overflow reuses the message long-press menu's shape from ChatTopBar.

Scope

Edit opens a list of what can be changed. Node 10187:110373 shows four rows; this builds Icon and Name only. Membership Card, Description and Social Links have no contract support in flipcash2 0.11.0, so they are not scaffolded. Name is not in the node either and is styled to match the Icon row.

The node titles the screen "Edit Community". It says "Edit Group" here, matching hint_groupName, title_newPublicGroup and the rest of the repo's user-facing vocabulary.

The rows use the existing MenuList/ListItem, so this screen matches the profile menu one tap above it. That means the repo's settings-row metrics — 24dp icon, 17sp Demi — rather than the node's 26dp/20sp. ListItem's own doc comment records 17sp as a deliberate choice for settings rows. Worth a look if the node should win instead; it would mean a bespoke row.

Partial updates

EditChat leaves a field unchanged when it is unset, which makes an over-filled request destructive rather than merely wasteful: renaming a group while also sending the picture field would rewrite the picture, and sending it as null would clear it.

titleOnly and pictureOnly name that rule in one place. Built inline at each call site it held by inspection only, and an EditChatParameters(title = ..., picture = state.picture) written later — the shape most object updates take — would quietly overwrite the other field with whatever the screen happened to be holding.

Title length

Checked in code points, not String.length. protovalidate's max_len counts Unicode code points while Kotlin counts UTF-16 units, so a title of 64 emoji measures 128 in Kotlin. Using String.length would refuse titles the contract accepts, and only for non-BMP characters — the case least likely to be noticed. The bound applies to the trimmed title, and the trimmed title is what is sent.

Picture

Upload runs through the existing BlobStorageCoordinator, which returns only once the blob is READY, so awaiting it is what orders the call correctly — a PENDING blob would come back PICTURE_BLOB_NOT_ACCEPTED. No new uploader. EditGroupPictureViewModel mirrors CreateGroupViewModel's pick-and-upload path, including its error wording.

Echo

On OK the response metadata is persisted directly rather than refetched — the response already carries the chat as it now stands. The titleChanged/pictureChanged stream events carry absolute values (TitleChanged(newTitle), PictureChanged(newPicture)), not deltas, so the echo arriving afterwards writes the same value and is idempotent.

Moderation wording

moderationDescription moves out of CreateGroupViewModel into core/moderation unchanged, so an edit refuses a title in the same words creation does. CreateGroupViewModel now calls the shared one; TITLE_MODERATED keeps the user on the edit screen with the field intact.

Tests

ChatTitleTest covers the 1..64 bound including the code-point case, PartialEditsTest covers name-only sending no picture and vice versa, and GroupProfileOverflowTest covers canEdit gating the row.

@bmc08gt bmc08gt self-assigned this Sep 22, 2026
@github-actions github-actions Bot added the type: feature New functionality label 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
bmc08gt changed the base branch from feat/chat-roster-edit-rpcs to code/cash September 22, 2026 15:00
…ver 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.
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.
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.
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.
…al 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.
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.
Most of the bottom bar's prompt descriptions end without one — 16 of the
20 prompt_description_* strings — so these two were the outliers.
…ther 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 merged commit 17bbc1f into code/cash Sep 22, 2026
3 checks passed
bmc08gt added a commit that referenced this pull request Sep 22, 2026
…tate (#1527)

* feat(messenger): use the currency creator's glyphs on the edit rows

The picture and name rows drew Icons.Outlined.Image and Icons.Outlined.TextFields
— stock Material vectors in a list whose neighbouring rows all use drawables the
app ships. They now use ic_currencycreator_icon and ic_currencycreator_name, the
pair the currency creator's info screen puts against its own Icon and Name steps.

Reached through com.flipcash.core.R as CoreR, the alias MyAccountMenuItems — the
file this one mirrors — already uses to pull core drawables into a feature.

* fix(messenger): state the consequence in the confirmation bodies

Both bodies described what the other members would see — "Everyone in this group
will see it as ..." — rather than what the button is about to do. They now read
"This will change the group name/picture for everyone in it", which names the
action and keeps the reach.

Dropping the quoted title takes the name body's format argument with it. The
field it quoted sits on screen behind the prompt.

* chore: keep agent session state out of the repo

.gitignore covers .claude/worktrees/, .claude/plans/ and settings.local.json
but not .claude/state/, which is where an agent session writes the working
note it reads back after a context compaction. The note was picked up as a
new file and merged in #1525.

Untracked here and the directory ignored alongside the other .claude paths
that are already local-only. The file stays on disk.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: feature New functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant