Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .claude/state/handoff.md
Original file line number Diff line number Diff line change
@@ -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`.
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
@@ -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
}
35 changes: 35 additions & 0 deletions apps/flipcash/core/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1039,6 +1039,41 @@
<string name="error_title_groupCreateFailed">Something Went Wrong</string>
<string name="error_description_groupCreateFailed">We couldn\'t create this group. Please try again.</string>

<!-- Editing a group (node 10187:110373). The design titles this "Edit Community"; the app
calls the same thing a Group everywhere else (see title_newPublicGroup, hint_groupName),
so it is a Group here too rather than introducing a second word for one concept.

The node lists four rows — Icon, Membership Card, Description, Social Links. Only the
first ships, as Picture: that is the field it writes (EditChatRequest.picture) and the
word the rest of the app uses. The other three have no field on EditChatRequest in
flipcash2 0.11.0. Name is not in the node at all and takes the first row's styling. -->
<string name="title_editGroup">Edit Group</string>
<string name="title_editGroupPicture">Picture</string>
<string name="title_editGroupName">Name</string>
<!-- Each edit screen's own title. The row above says what it changes; this says what to do.
They are placed differently because the account flow places them differently for the same
two edits: PhotoSelectionScreen titles its app bar, NameEntryScreen leaves the bar bare and
heads the field itself. -->
<string name="title_setGroupPicture">Set Picture</string>
<string name="title_setGroupName">Set Name</string>

<!-- Both edits are confirmed before the call: a rename or a new picture is visible to the whole
group, so Save opens the prompt and the prompt's action is what sends it. -->
<string name="prompt_title_changeGroupName">Change Group Name?</string>
<string name="prompt_description_changeGroupName">Everyone in this group will see it as \"%1$s\"</string>
<string name="action_changeGroupName">Change Name</string>
<string name="prompt_title_changeGroupPicture">Change Group Picture?</string>
<string name="prompt_description_changeGroupPicture">Everyone in this group will see the new picture</string>
<string name="action_changeGroupPicture">Change Picture</string>
<!-- EditChat bounds the title at 1-64 characters, enforced before the call. -->
<string name="subtitle_groupNameLength">Up to 64 characters</string>
<!-- EditChat answered DENIED: canEdit was false, or stopped being true since the menu was drawn. -->
<string name="error_title_groupEditDenied">Can\'t Edit This Group</string>
<string name="error_description_groupEditDenied">You don\'t have permission to edit this group</string>
<!-- Anything EditChat can fail on that the screen cannot explain, including the transport. -->
<string name="error_title_groupEditFailed">Something Went Wrong</string>
<string name="error_description_groupEditFailed">We couldn\'t save your changes. Please try again.</string>

<!-- Handing out the group's invite link, straight after it is created (node 10127:118315). -->
<string name="title_inviteToJoinGroup">Invite to Join Group</string>
<string name="action_sendInviteLink">Send Invite Link</string>
Expand Down
1 change: 1 addition & 0 deletions apps/flipcash/features/messenger/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -105,6 +108,15 @@ private fun chatEntryProvider(
annotatedEntry<ChatStep.GroupProfile> {
FlowGroupProfileScreen()
}
annotatedEntry<ChatStep.EditGroup> {
EditGroupScreen()
}
annotatedEntry<ChatStep.EditGroupName> {
FlowEditGroupNameScreen()
}
annotatedEntry<ChatStep.EditGroupPicture> {
FlowEditGroupPictureScreen()
}
}

@Composable
Expand Down Expand Up @@ -308,3 +320,16 @@ private fun FlowChatProfileScreen(participant: ChatParticipant) {
private fun FlowGroupProfileScreen() {
GroupProfileScreen(flowSharedViewModel<ChatViewModel>())
}

// 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<ChatViewModel>())
}

@Composable
private fun FlowEditGroupPictureScreen() {
EditGroupPictureScreen(flowSharedViewModel<ChatViewModel>())
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ 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
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

/**
Expand All @@ -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
}

/**
Expand Down Expand Up @@ -68,3 +73,36 @@ internal data object LeaveChat : FullMenuItem<GroupProfileAction>() {

/** The group's mute row. Shared definition; see [MuteChatItem]. */
internal val MuteChat = MuteChatItem<GroupProfileAction>(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<GroupProfileAction>() {
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<MenuItem<GroupProfileAction>> =
if (canEdit) listOf(EditGroup) else emptyList()
Loading
Loading