Skip to content

feat(chat): edit a group's name and picture - #825

Merged
bmc08gt merged 4 commits into
mainfrom
feat/edit-chat-ui
Sep 22, 2026
Merged

bmc08gt merged 4 commits into
mainfrom
feat/edit-chat-ui

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

Adds the group-chat Edit UI: an overflow menu on the chat profile screen, and a two-row list behind it for the group's Picture and Name.

The gate

The menu item appears only while ViewerState.Permissions.canEdit is true. Conversation.canEdit forwards that field and consults nothing else — not membership, not creator identity, not chat kind. The server computes the permission from the viewer's standing against the chat's rules, so any client-side conjunct would either offer an affordance the server will deny or hide one it would have allowed. Absent viewer state means not permitted.

If the permission is withdrawn while the list is open, the screen unwinds rather than leaving the user on rows that can no longer save.

Saving asks first

Both fields change the group for everyone in it, so Save proposes and a confirmation alert commits — the pattern the My Account fields already use. Both take the red banner: the profile confirmations weigh whether the user can get their own old value back, while this edit lands on everyone at once and no later undo takes back what they have already seen.

Partial updates

EditChat treats an unset field as unchanged, so each screen sends only its own field and a rename cannot clear the picture. EditGroupModel.saveTitle passes pictureBlobID: nil by construction and savePicture passes title: nil; both are covered by tests rather than left to inspection.

The title is validated through the existing GroupTitleValidator for the contract's 1...64 before it is sent. The picture is stored, awaited to READY, and only then submitted — a pending or processing blob is answered PICTURE_BLOB_NOT_ACCEPTED. A failed edit keeps the blob reservation so a retry does not upload a second copy; picking a different picture drops it.

Not double-applying the echo

On OK the response metadata is applied through ConversationStore.applyTitleChanged and applyPictureChanged — the same mutators the titleChanged and pictureChanged stream events use — rather than through .metadataRefresh.

Two reasons. Both mutators are last-write-wins assignments, so the server's echo of this edit re-applies identical values instead of landing it twice. And .metadataRefresh upserts the whole conversation: the edit response carries chat metadata, not the locally-held membership flag, viewer state or roster, so a wholesale replace would drop them. Nothing is refetched.

A schema bump, and the rule that missed it

Database.schemaVersion goes to 39. #824 added canEdit to ConversationViewerState as a non-optional Bool, so a viewer-state blob written before it throws keyNotFound; the read is try?, which turns that into a silent nil for the whole value — losing mute state, not just the new permission. Without the bump, a chat the user created reads canEdit: false and the menu never appears.

The hard rule named SQLiteVersion in Info.plist, which has since moved into code, and covered only table definitions, which this is not. Both are fixed here, along with why a tolerant decoder is the wrong answer: it preserves a stale value where the bump fetches the correct one, and it is a migration under another name.

Error presentation

The mapping mirrors NewPublicGroupScreen's case for case, including log messages and dialog copy, so the same server answer reads the same way wherever a group name is submitted. TITLE_MODERATED keeps the user on the edit screen with the field as they typed it, since amending it there is the only way forward.

@bmc08gt bmc08gt self-assigned this Sep 22, 2026
@bmc08gt bmc08gt changed the title feat(chat): edit a group's name and icon feat(chat): edit a group's name and picture Sep 22, 2026
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.
#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
bmc08gt force-pushed the feat/edit-chat-ui branch 2 times, most recently from 784bc06 to ce9eda2 Compare September 22, 2026 17:23
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.
…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
bmc08gt merged commit 9a3e439 into main Sep 22, 2026
1 check failed
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