Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a615592
feat(groups): create a private group
bmc08gt Oct 9, 2026
f38e0e1
fix(groups): drop "can't be changed later" from the private-group foo…
bmc08gt Oct 9, 2026
c900d86
feat(chat): request to join a private group
bmc08gt Oct 9, 2026
aa37543
fix(chat): show a join request's age on its button
bmc08gt Oct 9, 2026
a8b3404
fix(chat): drop the period from the pending-request caption
bmc08gt Oct 9, 2026
db73d21
feat(chat): review join requests for a private group you host
bmc08gt Oct 9, 2026
d52ae30
feat(chat): show pending join requests under a private group's top bar
bmc08gt Oct 9, 2026
06e5ae7
feat(chat): private group members, encryption sheet and leave copy
bmc08gt Oct 9, 2026
fd35bfd
feat(chat): list reactions as unencrypted in the private group E2EE s…
bmc08gt Oct 9, 2026
010981d
fix(groups): say private groups when a private create is denied
bmc08gt Oct 9, 2026
731da7c
feat(groups): members read and send in private groups
bmc08gt Oct 9, 2026
8115c30
fix(groups): open the private group encryption sheet from the Encrypt…
bmc08gt Oct 9, 2026
c6d56fe
feat(groups): move join requests behind a Requests button in the Chat…
bmc08gt Oct 12, 2026
6f21373
feat(groups): put Requests behind an overflow menu in the Chats header
bmc08gt Oct 13, 2026
83a36f9
feat(groups): always show the Chats overflow menu and dot its Request…
bmc08gt Oct 13, 2026
edf86d2
feat(groups): show a private group's posters as Members until roster …
bmc08gt Oct 13, 2026
61a8048
fix(chats): match the Chats overflow menu to group info and the empty…
bmc08gt Oct 13, 2026
c696304
fix(groups): keep a member's group key when storing its self-wrapped …
bmc08gt Oct 13, 2026
71bcfd4
fix(notifications): show a private group message's text in its push
bmc08gt Oct 13, 2026
db04145
fix(groups): mark messages under an untrusted group key unsupported
bmc08gt Oct 13, 2026
fa76fe0
fix(chat): wait for the key when a sealed photo's chat isn't stored yet
bmc08gt Oct 13, 2026
4ed37df
test(groups): cover sealing, sending and pushes in private groups
bmc08gt Oct 13, 2026
1b7e643
fix(groups): name a private group message's author in the resend hint
bmc08gt Oct 13, 2026
bf0570e
fix(groups): open Join Requests from Requests without the group profi…
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 @@ -137,6 +138,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 @@ -149,6 +151,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 @@ -516,6 +535,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 @@ -931,6 +931,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 @@ -1019,6 +1021,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 @@ -1103,6 +1109,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 @@ -1133,6 +1213,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