diff --git a/.claude/state/handoff.md b/.claude/state/handoff.md
new file mode 100644
index 0000000000..b32687f62f
--- /dev/null
+++ b/.claude/state/handoff.md
@@ -0,0 +1,27 @@
+# Handoff — group-chat Edit UI
+
+**Done and shipped.** [code-android-app#1525](https://github.com/code-payments/code-android-app/pull/1525),
+stacked on #1524 (`feat/chat-roster-edit-rpcs`). Worktree `.claude/worktrees/edit-chat-ui`,
+branch `feat/edit-chat-ui`, commit `e0479e2e9`.
+
+## Verified green
+`:services:flipcash:compileDebugKotlin`, `:apps:flipcash:core:`, `:apps:flipcash:shared:chat:`,
+messenger + tipping `testDebugUnitTest`, and `:apps:flipcash:app:assembleDebug` (the last one
+proves the two new `@HiltViewModel`s wire up). 12 new tests, all ran.
+
+## Key decisions
+- `canEdit` gate is passed in, never derived. `groupProfileOverflowItems(canEdit)` returns a list
+ so no-permission means no overflow button.
+- `titleOnly` / `pictureOnly` in `PartialEdits.kt` encode "send only what changed" — EditChat
+ leaves unset fields alone, so an over-filled request is destructive.
+- Title length counted in **code points** (protovalidate max_len semantics), on the trimmed value.
+- Echo is idempotent: `TitleChanged`/`PictureChanged` carry absolute values, so persisting the
+ editChat response and then receiving the stream event writes the same thing.
+- `moderationDescription` extracted from `CreateGroupViewModel` to `core/moderation`, unchanged.
+- Built Icon + Name only; the other three node rows have no flipcash2 0.11.0 contract.
+- Used repo `MenuList`/`ListItem` metrics (24dp/17sp), not the node's 26dp/20sp — flagged in the PR.
+- Terminology: "Group", not the node's "Community".
+
+## Environment gotcha
+`google-services.json` is gitignored and absent from fresh worktrees; copy it from the main
+checkout (`apps/flipcash/app/`) before any `assembleDebug`.
diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/chat/ChatStep.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/chat/ChatStep.kt
index 0efc8db555..a4d24633c8 100644
--- a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/chat/ChatStep.kt
+++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/chat/ChatStep.kt
@@ -82,4 +82,31 @@ sealed interface ChatStep : FlowStep, Parcelable {
@Parcelize
@Serializable
data object GroupProfile : ChatStep
+
+ /**
+ * Node 10187:110373 — what about the group can be changed, as a list of the things that can.
+ *
+ * A pushed screen rather than a sheet, unlike [InviteToGroup] and [MuteChat]: those end on the
+ * choice they present, while this one is a way through to another screen and has to be
+ * somewhere back can return to. Carries nothing for the same reason [GroupProfile] does not —
+ * the flow is already open on the group, and every step here reads it off the conversation's
+ * view model.
+ *
+ * The design node lists four rows; only [EditGroupPicture] and [EditGroupName] are built.
+ * Membership card, description and social links have no field on `EditChatRequest` in
+ * flipcash2 0.11.0, so there is nothing for them to write.
+ */
+ @Parcelize
+ @Serializable
+ data object EditGroup : ChatStep
+
+ /** The group's title, on the same entry screen shape the profile name uses. */
+ @Parcelize
+ @Serializable
+ data object EditGroupName : ChatStep
+
+ /** The group's picture — pick, upload, then `EditChat` with the READY blob. */
+ @Parcelize
+ @Serializable
+ data object EditGroupPicture : ChatStep
}
diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/moderation/ModerationMessages.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/moderation/ModerationMessages.kt
new file mode 100644
index 0000000000..4effe9e89b
--- /dev/null
+++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/moderation/ModerationMessages.kt
@@ -0,0 +1,38 @@
+package com.flipcash.app.core.moderation
+
+import androidx.annotation.StringRes
+import com.flipcash.core.R
+import com.flipcash.services.models.ModerationResult
+
+/**
+ * The app's wording for a moderation refusal, one message per [ModerationResult.FlaggedCategory].
+ *
+ * Extracted from `CreateGroupViewModel` so the group *edit* flow refuses a title in the same words
+ * group *creation* does. The strings are deliberately the profile-name ones: they are written
+ * about a rejected piece of user text rather than about a profile, and the caller supplies the
+ * title that says which piece it was.
+ *
+ * [NONE] means the server flagged the content without naming a category, so it gets the generic
+ * "try a different one" rather than a reason the response never gave.
+ */
+@StringRes
+fun moderationDescription(category: ModerationResult.FlaggedCategory): Int =
+ when (category) {
+ ModerationResult.FlaggedCategory.NONE ->
+ R.string.error_description_imageNotAllowed
+
+ ModerationResult.FlaggedCategory.OTHER ->
+ R.string.error_description_profileNameNotAllowedFlaggedOther
+
+ ModerationResult.FlaggedCategory.NSFW ->
+ R.string.error_description_profileNameNotAllowedFlaggedNsfw
+
+ ModerationResult.FlaggedCategory.IMPERSONATION ->
+ R.string.error_description_profileNameNotAllowedFlaggedImpersonation
+
+ ModerationResult.FlaggedCategory.MISLEADING ->
+ R.string.error_description_profileNameNotAllowedFlaggedMisleading
+
+ ModerationResult.FlaggedCategory.SPAM ->
+ R.string.error_description_profileNameNotAllowedFlaggedSpam
+ }
diff --git a/apps/flipcash/core/src/main/res/values/strings.xml b/apps/flipcash/core/src/main/res/values/strings.xml
index a3cd5afd1e..52a0a97b6d 100644
--- a/apps/flipcash/core/src/main/res/values/strings.xml
+++ b/apps/flipcash/core/src/main/res/values/strings.xml
@@ -1039,6 +1039,41 @@
Something Went Wrong
We couldn\'t create this group. Please try again.
+
+ Edit Group
+ Picture
+ Name
+
+ Set Picture
+ Set Name
+
+
+ Change Group Name?
+ Everyone in this group will see it as \"%1$s\"
+ Change Name
+ Change Group Picture?
+ Everyone in this group will see the new picture
+ Change Picture
+
+ Up to 64 characters
+
+ Can\'t Edit This Group
+ You don\'t have permission to edit this group
+
+ Something Went Wrong
+ We couldn\'t save your changes. Please try again.
+
Invite to Join Group
Send Invite Link
diff --git a/apps/flipcash/features/messenger/build.gradle.kts b/apps/flipcash/features/messenger/build.gradle.kts
index 20760fb700..d14d74190e 100644
--- a/apps/flipcash/features/messenger/build.gradle.kts
+++ b/apps/flipcash/features/messenger/build.gradle.kts
@@ -8,6 +8,7 @@ android {
dependencies {
implementation(project(":apps:flipcash:shared:analytics"))
+ implementation(project(":apps:flipcash:shared:blob"))
implementation(project(":apps:flipcash:shared:blocklist"))
implementation(project(":apps:flipcash:shared:chat"))
implementation(project(":apps:flipcash:shared:chat-ui"))
diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/ChatFlowScreen.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/ChatFlowScreen.kt
index fb6b7c5f21..1ca0210814 100644
--- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/ChatFlowScreen.kt
+++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/ChatFlowScreen.kt
@@ -28,6 +28,9 @@ import com.flipcash.app.messenger.internal.screens.cash.ChatInitPaymentSheet
import com.flipcash.app.messenger.internal.screens.profile.ChatProfileScreen
import com.flipcash.app.messenger.internal.screens.profile.ChatProfileViewModel
import com.flipcash.app.messenger.internal.screens.profile.GroupProfileScreen
+import com.flipcash.app.messenger.internal.screens.profile.edit.EditGroupNameScreen
+import com.flipcash.app.messenger.internal.screens.profile.edit.EditGroupPictureScreen
+import com.flipcash.app.messenger.internal.screens.profile.edit.EditGroupScreen
import com.flipcash.shared.chat.ui.rememberIsMuted
import com.getcode.navigation.annotatedEntry
import com.getcode.navigation.core.LocalCodeNavigator
@@ -105,6 +108,15 @@ private fun chatEntryProvider(
annotatedEntry {
FlowGroupProfileScreen()
}
+ annotatedEntry {
+ EditGroupScreen()
+ }
+ annotatedEntry {
+ FlowEditGroupNameScreen()
+ }
+ annotatedEntry {
+ FlowEditGroupPictureScreen()
+ }
}
@Composable
@@ -308,3 +320,16 @@ private fun FlowChatProfileScreen(participant: ChatParticipant) {
private fun FlowGroupProfileScreen() {
GroupProfileScreen(flowSharedViewModel())
}
+
+// Both edit steps read the chat they are editing off the flow's shared ChatViewModel, the same way
+// the profile above them does, and keep the edit itself in their own nav-entry-scoped view model.
+// EditGroupScreen needs neither and is registered directly.
+@Composable
+private fun FlowEditGroupNameScreen() {
+ EditGroupNameScreen(flowSharedViewModel())
+}
+
+@Composable
+private fun FlowEditGroupPictureScreen() {
+ EditGroupPictureScreen(flowSharedViewModel())
+}
diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/profile/GroupProfileMenuItems.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/profile/GroupProfileMenuItems.kt
index 9c4c0fc4b4..59946e410f 100644
--- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/profile/GroupProfileMenuItems.kt
+++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/profile/GroupProfileMenuItems.kt
@@ -2,6 +2,7 @@ package com.flipcash.app.messenger.internal.screens.profile
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.Logout
+import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.outlined.Flag
import androidx.compose.material.icons.outlined.PersonAdd
import androidx.compose.runtime.Composable
@@ -9,6 +10,7 @@ import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.stringResource
import com.flipcash.app.menu.FullMenuItem
+import com.flipcash.app.menu.MenuItem
import com.flipcash.features.messenger.R
/**
@@ -24,6 +26,9 @@ internal sealed interface GroupProfileAction {
data object Mute : GroupProfileAction
data object Leave : GroupProfileAction
data object Report : GroupProfileAction
+
+ /** Opens node 10187:110373's list of what about the group can be changed. */
+ data object Edit : GroupProfileAction
}
/**
@@ -68,3 +73,36 @@ internal data object LeaveChat : FullMenuItem() {
/** The group's mute row. Shared definition; see [MuteChatItem]. */
internal val MuteChat = MuteChatItem(GroupProfileAction.Mute)
+
+/**
+ * The group's edit row, in the profile's top-right overflow rather than in the list below it.
+ *
+ * A pencil, the same glyph the message overflow uses for editing a message — one verb, one icon.
+ * Not `ic_group_3`, which names a group rather than the act of changing one.
+ *
+ * Node 10187:110373 is the screen this opens; the overflow it hangs from is the message
+ * long-press menu's shape reused (see `ChatTopBar.MessageOverflow`).
+ */
+internal data object EditGroup : FullMenuItem() {
+ override val icon: Painter
+ @Composable get() = rememberVectorPainter(Icons.Outlined.Edit)
+
+ override val name: String
+ @Composable get() = stringResource(R.string.action_edit)
+
+ override val action: GroupProfileAction = GroupProfileAction.Edit
+}
+
+/**
+ * What the profile's top-right overflow holds, for a viewer the server says may edit.
+ *
+ * [canEdit] comes from `ViewerState.Permissions` and is server-computed. It is passed in rather
+ * than derived here on purpose: membership, being the creator, and the chat's kind all *look* like
+ * they answer the question and none of them is the answer the server gives, so a local guess would
+ * offer an Edit that `EditChat` then refuses with `DENIED`.
+ *
+ * Returns a list so the overflow has nothing to show — and so draws nothing — when the answer is
+ * no. A single-item menu is still a menu; an empty one is not a button.
+ */
+internal fun groupProfileOverflowItems(canEdit: Boolean): List