Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8d29ce7
feat(groups): create a private group
bmc08gt Oct 9, 2026
d0404c2
fix(groups): drop "can't be changed later" from the private-group foo…
bmc08gt Oct 9, 2026
bce2f4a
feat(chat): request to join a private group
bmc08gt Oct 9, 2026
8a3ae9c
fix(chat): show a join request's age on its button
bmc08gt Oct 9, 2026
0219a31
fix(chat): drop the period from the pending-request caption
bmc08gt Oct 9, 2026
f6f6c69
feat(chat): review join requests for a private group you host
bmc08gt Oct 9, 2026
4f91358
feat(chat): show pending join requests under a private group's top bar
bmc08gt Oct 9, 2026
7a60eb9
feat(chat): private group members, encryption sheet and leave copy
bmc08gt Oct 9, 2026
1875728
feat(chat): list reactions as unencrypted in the private group E2EE s…
bmc08gt Oct 9, 2026
2334425
fix(groups): say private groups when a private create is denied
bmc08gt Oct 9, 2026
fea6580
feat(groups): members read and send in private groups
bmc08gt Oct 9, 2026
517afa5
fix(groups): open the private group encryption sheet from the Encrypt…
bmc08gt Oct 9, 2026
3f6ca6f
feat(groups): move join requests behind a Requests button in the Chat…
bmc08gt Oct 12, 2026
163f1af
feat(groups): put Requests behind an overflow menu in the Chats header
bmc08gt Oct 13, 2026
e512d98
feat(groups): always show the Chats overflow menu and dot its Request…
bmc08gt Oct 13, 2026
af73b5a
feat(groups): show a private group's posters as Members until roster …
bmc08gt Oct 13, 2026
874e2fa
fix(chats): match the Chats overflow menu to group info and the empty…
bmc08gt Oct 13, 2026
5c895bc
fix(groups): keep a member's group key when storing its self-wrapped …
bmc08gt Oct 13, 2026
6a56fa5
fix(notifications): show a private group message's text in its push
bmc08gt Oct 13, 2026
734eb62
fix(groups): mark messages under an untrusted group key unsupported
bmc08gt Oct 13, 2026
3c505e8
fix(chat): wait for the key when a sealed photo's chat isn't stored yet
bmc08gt Oct 13, 2026
e8b9e9c
test(groups): cover sealing, sending and pushes in private groups
bmc08gt Oct 13, 2026
9748fc5
fix(groups): name a private group message's author in the resend hint
bmc08gt Oct 13, 2026
d57ad4f
fix(groups): open Join Requests from Requests without the group profi…
bmc08gt Oct 13, 2026
3916c03
test(chat): pass a group crypto to the share gateway's outgoing encry…
bmc08gt Oct 13, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import com.flipcash.app.contact.verification.VerificationFlowScreen
import com.flipcash.app.currencycreator.CurrencyCreatorFlowScreen
import com.flipcash.app.tipping.FindByUsernameScreen
import com.flipcash.app.tipping.ArchivedChatsScreen
import com.flipcash.app.tipping.RequestsScreen
import com.flipcash.app.tipping.NewChatScreen
import com.flipcash.app.tipping.NewGroupFlowScreen
import com.flipcash.app.tipping.ChatsScreen
Expand Down Expand Up @@ -138,6 +139,7 @@ fun appEntryProvider(
annotatedEntry<AppRoute.Messaging.Profile> { key -> ProfileScreen(key.address, key.origin) }
annotatedEntry<AppRoute.Messaging.NewChat> { NewChatScreen() }
annotatedEntry<AppRoute.Messaging.ArchivedChats> { ArchivedChatsScreen() }
annotatedEntry<AppRoute.Messaging.Requests> { RequestsScreen() }
annotatedEntry<AppRoute.Messaging.FindByUsername> { FindByUsernameScreen() }
annotatedEntry<AppRoute.Messaging.NewGroup> { key ->
NewGroupFlowScreen(route = key, resultStateRegistry = resultStateRegistry)
Expand All @@ -151,6 +153,9 @@ fun appEntryProvider(
annotatedEntry<AppRoute.Messaging.PhotoViewer> { key -> ChatPhotoViewerScreen(key.chatId, key.messageId) }
annotatedEntry<AppRoute.Messaging.E2eeDmInfo> { E2eeLearnMoreScreen(forGroup = false) }
annotatedEntry<AppRoute.Messaging.E2eeGroupInfo> { E2eeLearnMoreScreen(forGroup = true) }
annotatedEntry<AppRoute.Messaging.E2eePrivateGroupInfo> {
E2eeLearnMoreScreen(forGroup = true, isPrivate = true)
}

// Tokens
annotatedEntry<AppRoute.Token.Info>(testTag = "token_info_screen") { key ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -363,9 +363,16 @@ sealed interface AppRoute : NavKey, Parcelable {
// profile is the flow's root, so back returns to wherever the tap came from, and Open
// Chat pushes the transcript on top of it.
val openOnProfile: Boolean = false,
// Open on a private group's Join Requests as the flow's root, so back returns to the
// Requests screen that set it, the way iOS does.
val openOnJoinRequests: Boolean = false,
) : Messaging, FlowRoute {
override val initialStack: List<NavKey>
get() = listOf(if (openOnProfile) ChatStep.GroupProfile else ChatStep.Conversation)
get() = when {
openOnJoinRequests -> listOf(ChatStep.JoinRequests)
openOnProfile -> listOf(ChatStep.GroupProfile)
else -> listOf(ChatStep.Conversation)
}
}

/**
Expand All @@ -389,6 +396,14 @@ sealed interface AppRoute : NavKey, Parcelable {
@Parcelize
data object ArchivedChats : Messaging

/**
* Join requests in both directions, behind the Requests button on the Chats header. Pushed
* for the reason [ArchivedChats] is.
*/
@Serializable
@Parcelize
data object Requests : Messaging

/**
* Another person's profile, opened on its own rather than from inside a chat — the
* destination of a `flipcash.com/{username}` or `flipcash.com/{uuid}` link.
Expand All @@ -412,14 +427,18 @@ sealed interface AppRoute : NavKey, Parcelable {
data object FindByUsername : Messaging

/**
* Node 10153:22901 — creating a public group, from the empty form to its invite link.
* Node 10153:22901 — creating a public group, from the empty form to its invite link; node
* 11203:65 is the same flow for a private one.
*
* The three screens share one draft, so they are steps of a flow rather than routes; see
* [NewGroupStep].
*/
@Serializable
@Parcelize
data object NewGroup : Messaging, FlowRoute {
data class NewGroup(
/** Node 11203:65: approval to join, end-to-end encrypted, no balance requirement. */
val isPrivate: Boolean = false,
) : Messaging, FlowRoute {
override val initialStack: List<NavKey>
get() = listOf(NewGroupStep.Form)
}
Expand Down Expand Up @@ -526,6 +545,14 @@ sealed interface AppRoute : NavKey, Parcelable {
Messaging,
com.getcode.navigation.Sheet,
com.getcode.navigation.WrapContentSheet

/** [E2eeGroupInfo] for a private group, which is end-to-end encrypted (node 11210:71). */
@Serializable
@Parcelize
data object E2eePrivateGroupInfo :
Messaging,
com.getcode.navigation.Sheet,
com.getcode.navigation.WrapContentSheet
}

@Serializable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,23 @@ sealed interface ChatStep : FlowStep, Parcelable {
@Serializable
data object EditGroup : ChatStep

/**
* Node 11206:60 — who is waiting to join a private group, for its creator to approve or deny.
* Carries nothing for the same reason [GroupProfile] does not; the lobby itself comes from the
* shared host-lobby store.
*/
@Parcelize
@Serializable
data object JoinRequests : ChatStep

/**
* Node 11210:70 — every member of a private group, the creator pinned first with a Host tag.
* The roster comes from the shared roster store.
*/
@Parcelize
@Serializable
data object Members : ChatStep

/** The group's title, on the same entry screen shape the profile name uses. */
@Parcelize
@Serializable
Expand Down
81 changes: 81 additions & 0 deletions apps/flipcash/core/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -932,6 +932,8 @@
<string name="action_addToChat">Add %1$s to Chat</string>
<string name="action_leaveChat">Leave Chat</string>
<string name="prompt_title_leaveChat">Leave %1$s?</string>
<!-- A private group: coming back means a new request, not an invite link (node 11206:98). -->
<string name="prompt_description_leavePrivateChat">You’ll lose access to the conversation. To rejoin, you’ll need to request again and wait for the host to approve</string>
<string name="prompt_description_leaveChat">You won’t receive messages from this group any more. You can join again with an invite link</string>
<string name="error_title_failedToLeave">Something Went Wrong</string>
<string name="error_description_failedToLeave">We were unable to leave this group. Please try again</string>
Expand Down Expand Up @@ -1020,6 +1022,10 @@
<string name="item_e2eeSheet_beforeEncryption">Messages sent before encryption became available</string>
<string name="item_e2eeSheet_messagesAndMediaInDms">Messages and media in 1:1 chats</string>
<string name="item_e2eeSheet_publicGroupChatsIncludingThis">Public group chats, including this one</string>
<!-- A private group (node 11210:71). -->
<string name="subtitle_e2eeSheet_privateGroup">Messages and media in this group are end-to-end encrypted. Only its members can read them, and the host approves everyone who joins.</string>
<string name="item_e2eeSheet_groupDetails">The group’s name, photo, description and cover</string>
<string name="item_e2eeSheet_groupMembers">Who is in the group, and how many</string>

<!-- Cash link card: the state of the link a message carries, read without claiming it -->
<string name="label_linkCard_cash">Cash</string>
Expand Down Expand Up @@ -1104,6 +1110,80 @@
(node 10127:117987). -->
<string name="title_startNewChat">New Chat</string>
<string name="action_createPublicGroup">Create a Public Group</string>
<string name="action_createPrivateGroup">Create a Private Group</string>
<string name="title_newPrivateGroup">New Private Group</string>
<string name="action_requestToJoin">Request to Join</string>
<string name="action_requestSent">Request Sent</string>
<string name="action_cancelRequest">Cancel Request</string>
<string name="subtitle_privateGroupMembers">Private group · %1$s</string>
<string name="subtitle_hostApprovesJoins">The host approves who joins</string>
<string name="action_requestedMinutesAgo">Requested %1$dm ago</string>
<string name="action_requestedHoursAgo">Requested %1$dh ago</string>
<string name="action_requestedDaysAgo">Requested %1$dd ago</string>
<string name="subtitle_requestPendingHost">You\'ll be able to open the chat once %1$s approves</string>
<string name="subtitle_requestPendingHostUnnamed">You\'ll be able to open the chat once the host approves</string>
<string name="prompt_title_lobbyClosed">%1$s isn\'t taking requests right now</string>
<string name="prompt_description_lobbyFull">Too many people are waiting for the host to respond. Try again later</string>
<string name="prompt_description_tooManyLobbies">You have too many pending requests. Cancel one to request another</string>
<string name="error_title_failedToRequestJoin">Couldn\'t Send Request</string>
<string name="error_description_failedToRequestJoin">Something went wrong. Please try again</string>
<string name="error_title_failedToCancelJoinRequest">Couldn\'t Cancel Request</string>
<string name="error_description_failedToCancelJoinRequest">Something went wrong. Please try again</string>
<string name="error_title_failedToApproveJoinRequest">Couldn\'t Approve</string>
<string name="error_description_failedToApproveJoinRequest">Something went wrong. Please try again</string>
<string name="error_title_failedToDenyJoinRequest">Couldn\'t Deny</string>
<string name="error_description_failedToDenyJoinRequest">Something went wrong. Please try again</string>
<string name="title_joinRequests">Join Requests</string>
<!-- The Requests button on the Chats header and the screen behind it. -->
<string name="title_requests">Requests</string>
<string name="header_waitingOnYou">Waiting on you</string>
<string name="header_sentRequests">Sent</string>
<string name="title_noRequests">No requests right now</string>
<string name="subtitle_noRequests">Requests to join your private groups, and the ones you send, show up here</string>
<plurals name="subtitle_joinRequestsWaiting">
<item quantity="one">%1$d person is waiting</item>
<item quantity="other">%1$d people are waiting</item>
</plurals>
<string name="content_description_moreWithRequests">More, people waiting to join</string>
<string name="content_description_requestsWaiting">Requests, people waiting to join</string>
<string name="subtitle_requestedJustNow">requested just now</string>
<string name="subtitle_requestedMinutesAgo">requested %1$dm ago</string>
<string name="subtitle_requestedHoursAgo">requested %1$dh ago</string>
<string name="subtitle_requestedYesterday">requested yesterday</string>
<string name="subtitle_requestedDaysAgo">requested %1$dd ago</string>
<string name="action_deny">Deny</string>
<string name="action_approve">Approve</string>
<string name="subtitle_joinRequestsFooter">Approving shares the group key with them from this device.</string>
<string name="title_noJoinRequests">No requests right now</string>
<string name="subtitle_noJoinRequests">When someone asks to join %1$s, they\'ll show up here</string>
<string name="action_shareGroup">Share Group</string>
<!-- In-chat Join Requests anchor (nodes 11354:70, 11354:81). -->
<string name="title_wantsToJoin">%1$s wants to join</string>
<plurals name="title_peopleWantToJoin">
<item quantity="one">%1$d person wants to join</item>
<item quantity="other">%1$d people want to join</item>
</plurals>
<string name="subtitle_namesAnd">and</string>
<string name="title_requestedJustNow">Requested just now</string>
<string name="title_requestedYesterday">Requested yesterday</string>
<string name="action_review">Review</string>
<!-- A private group's members (nodes 11210:68, 11210:70). -->
<string name="title_members">Members</string>
<plurals name="header_memberCount">
<item quantity="one">%1$d MEMBER</item>
<item quantity="other">%1$d MEMBERS</item>
</plurals>
<string name="label_host">Host</string>
<string name="subtitle_joinedJustNow">Joined just now</string>
<string name="subtitle_joinedMinutesAgo">Joined %1$dm ago</string>
<string name="subtitle_joinedHoursAgo">Joined %1$dh ago</string>
<string name="subtitle_joinedYesterday">Joined yesterday</string>
<string name="subtitle_joinedDaysAgo">Joined %1$dd ago</string>
<string name="subtitle_joinedLastWeek">Joined last week</string>
<string name="subtitle_joinedWeeksAgo">Joined %1$d weeks ago</string>
<string name="subtitle_joinedLastMonth">Joined last month</string>
<string name="subtitle_joinedMonthsAgo">Joined %1$d months ago</string>
<string name="subtitle_privateGroupFootnote">Only people you approve can join. Messages are end-to-end encrypted.</string>
<string name="action_findByUsername">Find Someone by Username</string>

<!-- Creating a public group (node 10153:22901). -->
Expand Down Expand Up @@ -1134,6 +1214,7 @@
<string name="error_description_groupRulesInvalid">Try a different balance requirement</string>
<string name="error_title_groupCreateDenied">Can\'t Create This Group</string>
<string name="error_description_groupCreateDenied">Your account isn\'t able to create public groups</string>
<string name="error_description_privateGroupCreateDenied">Your account isn\'t able to create private groups</string>
<!-- Anything StartChat can fail on that the form cannot explain: an unrecognised result, or the
transport. The key survives a retry, so trying again resumes the same attempt. -->
<string name="error_title_groupCreateFailed">Something Went Wrong</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ 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.EditGroupCoverScreen
import com.flipcash.app.messenger.internal.screens.profile.edit.EditGroupDescriptionScreen
import com.flipcash.app.messenger.internal.screens.profile.JoinRequestsScreen
import com.flipcash.app.messenger.internal.screens.profile.MembersScreen
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
Expand Down Expand Up @@ -119,6 +121,12 @@ private fun chatEntryProvider(
annotatedEntry<ChatStep.EditGroup> {
FlowEditGroupScreen()
}
annotatedEntry<ChatStep.JoinRequests> {
JoinRequestsScreen(flowSharedViewModel<ChatViewModel>())
}
annotatedEntry<ChatStep.Members> {
MembersScreen(flowSharedViewModel<ChatViewModel>())
}
annotatedEntry<ChatStep.EditGroupName> {
FlowEditGroupNameScreen()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,19 @@ import com.flipcash.app.messenger.internal.screens.E2eeSheetKind
import com.getcode.navigation.scenes.LocalBottomSheetDismissDispatcher

/**
* The DM or group encryption explainer, behind `AppRoute.Messaging.E2eeDmInfo` and `E2eeGroupInfo`.
* The DM or group encryption explainer, behind `AppRoute.Messaging.E2eeDmInfo`, `E2eeGroupInfo` and
* `E2eePrivateGroupInfo`.
*/
@Composable
fun E2eeLearnMoreScreen(forGroup: Boolean) {
fun E2eeLearnMoreScreen(forGroup: Boolean, isPrivate: Boolean = false) {
// Exit through the sheet so it animates down rather than having its scene deleted mid-frame.
val dismissSheet = LocalBottomSheetDismissDispatcher.current
E2eeLearnMoreSheet(
kind = if (forGroup) E2eeSheetKind.Group else E2eeSheetKind.Dm,
kind = when {
!forGroup -> E2eeSheetKind.Dm
isPrivate -> E2eeSheetKind.PrivateGroup
else -> E2eeSheetKind.Group
},
onDismiss = dismissSheet,
)
}
Loading
Loading