diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatSubject.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatSubject.kt index dc6384f732..931154b208 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatSubject.kt +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatSubject.kt @@ -159,12 +159,14 @@ internal data class RuleCurrency( } /** - * The card an empty group shows in place of its info card: the same [LinkCard.GroupInvite] a - * transcript renders for an invite link, built from what this subject already knows rather than - * from a lookup, since a member looking at the empty group already has the chat's own record. + * The card that heads a group's transcript in place of its info card: the same + * [LinkCard.GroupInvite] a transcript renders for an invite link, built from what this subject + * already knows rather than from a lookup, since the screen already has the chat's own record. + * [inviteUrl] is null for a viewer who hasn't joined; the header never opens it, so the card only + * needs it as the link it stands for. */ internal fun ChatSubject.Group.toGroupInviteCard( - inviteUrl: String, + inviteUrl: String?, currencyName: String?, ): LinkCard.GroupInvite { val balance = rules.balanceRequirement() @@ -179,9 +181,9 @@ internal fun ChatSubject.Group.toGroupInviteCard( null } return LinkCard.GroupInvite( - url = inviteUrl, + url = inviteUrl.orEmpty(), start = 0, - end = inviteUrl.length, + end = inviteUrl.orEmpty().length, chatId = chatId, state = LinkCard.GroupInvite.State.Resolved( title = groupTitle?.takeIf { it.isNotBlank() }, diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt index 0b736967d6..25632759e3 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt @@ -469,14 +469,12 @@ internal class ChatViewModel @Inject constructor( ?.let { Linkify.groupChatInvite(it.chatId) } /** - * Whether the group's invite card heads the transcript in place of its info card: a group - * the viewer has joined that nobody else has (node 10330:19164). Decided by who is in the - * chat, not by whether it has messages, so posting into a solo group keeps the card. Once - * someone joins, the invite moves to the group's profile, as on iOS. + * Whether the invite card heading a group's transcript offers "Invite People": any group the + * viewer has joined, however many members it has (node 10330:19164). A group the viewer + * hasn't joined still gets the card, without the button, since it has no invite to share. */ - val showsGroupInviteCard: Boolean - get() = groupInviteUrl != null && - (subject as? ChatSubject.Group)?.memberCount?.let { it <= 1 } == true + val offersGroupInvite: Boolean + get() = groupInviteUrl != null } /** diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/MessageList.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/MessageList.kt index 3598efaf21..0b1a1ba6e3 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/MessageList.kt +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/MessageList.kt @@ -420,14 +420,15 @@ internal fun MessageList( } } - // Chat start shows contact info container. A group nobody else has joined shows its - // invite card there instead (node 10330:19164), whether or not it has messages: - // inviting someone is the thing left to do, and it is the same card a DM transcript - // renders for the link, with a different CTA and destination. - val inviteUrl = state.groupInviteUrl + // Chat start shows contact info container. A group shows its invite card there + // instead (node 10330:19164): the same card a DM transcript renders for the link, + // with a different CTA and destination. "Invite People" rides on it in every group + // the viewer has joined; a group they haven't has no invite to share, so its card + // has no button. val inviteGroup = state.subject as? ChatSubject.Group - if (state.showsGroupInviteCard && inviteUrl != null && inviteGroup != null) { + if (inviteGroup != null) { item(key = "group-invite-card") { + val inviteUrl = state.groupInviteUrl val card = remember(inviteGroup, inviteUrl, state.ruleCurrency) { inviteGroup.toGroupInviteCard( inviteUrl = inviteUrl, @@ -448,7 +449,8 @@ internal fun MessageList( GroupInviteLinkCard( card = card, minHeight = cardWidth * GroupInviteCardDefaults.ASPECT, - ctaLabel = stringResource(R.string.action_linkCard_invitePeople), + ctaLabel = stringResource(R.string.action_linkCard_invitePeople) + .takeIf { state.offersGroupInvite }, onStart = { onAction(ChatAction.InviteToGroup) } .takeIf { !selecting }, ) diff --git a/apps/flipcash/features/messenger/src/test/kotlin/com/flipcash/app/messenger/internal/ChatViewModelStateTest.kt b/apps/flipcash/features/messenger/src/test/kotlin/com/flipcash/app/messenger/internal/ChatViewModelStateTest.kt index 40414880db..12ed559db9 100644 --- a/apps/flipcash/features/messenger/src/test/kotlin/com/flipcash/app/messenger/internal/ChatViewModelStateTest.kt +++ b/apps/flipcash/features/messenger/src/test/kotlin/com/flipcash/app/messenger/internal/ChatViewModelStateTest.kt @@ -244,15 +244,14 @@ class ChatViewModelStateTest { } @Test - fun `the invite card heads a group only while the viewer is its only member`() { - // Messages have no say: a solo group the viewer has posted in still has nobody to talk to. - assertTrue(ChatViewModel.State(subject = group(isMember = true)).showsGroupInviteCard) - assertFalse( - ChatViewModel.State(subject = group(isMember = true, memberCount = 2L)).showsGroupInviteCard, + fun `the group header offers an invite in every group the viewer has joined`() { + assertTrue(ChatViewModel.State(subject = group(isMember = true)).offersGroupInvite) + assertTrue( + ChatViewModel.State(subject = group(isMember = true, memberCount = 2L)).offersGroupInvite, ) - assertFalse(ChatViewModel.State(subject = group(isMember = false)).showsGroupInviteCard) - assertFalse(ChatViewModel.State(subject = group(isMember = null)).showsGroupInviteCard) - assertFalse(ChatViewModel.State(subject = dm).showsGroupInviteCard) + assertFalse(ChatViewModel.State(subject = group(isMember = false)).offersGroupInvite) + assertFalse(ChatViewModel.State(subject = group(isMember = null)).offersGroupInvite) + assertFalse(ChatViewModel.State(subject = dm).offersGroupInvite) } @Test diff --git a/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/ui/GroupInviteLinkCard.kt b/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/ui/GroupInviteLinkCard.kt index c1d9062781..009a407b53 100644 --- a/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/ui/GroupInviteLinkCard.kt +++ b/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/ui/GroupInviteLinkCard.kt @@ -66,15 +66,17 @@ import com.getcode.ui.theme.CodeButton * a fallback title; an untitled group is named generically. * * Drawn in two places that want different things from the button, so [ctaLabel] and [onStart] come - * from the caller: a DM transcript's link card says "View" and opens the group, and the group's own - * empty state says "Invite People" and opens the invite sheet. [shape] is the transcript bubble's - * outline there, and the card's own radius where it stands alone. + * from the caller: a DM transcript's link card says "View" and opens the group, and the head of the + * group's own transcript says "Invite People" to a member and opens the invite sheet. A null + * [ctaLabel] leaves the button out, as it is for someone who hasn't joined, and the card is as tall + * as its content. [shape] is the transcript bubble's outline in a transcript, and the card's own + * radius where it stands alone. */ @Composable fun GroupInviteLinkCard( card: LinkCard.GroupInvite, minHeight: Dp, - ctaLabel: String, + ctaLabel: String?, onStart: (() -> Unit)?, modifier: Modifier = Modifier, shape: CornerBasedShape = GroupInviteCardDefaults.SHAPE, @@ -175,13 +177,15 @@ fun GroupInviteLinkCard( } // Unavailable draws nothing between the band and the button: there is no group to name. + if (resolved != null && ctaLabel == null) return@Column + // The least gap, then whatever the floor leaves over, so the button sits at the bottom // of a card taller than its content. Two spacers because a weight's height is exact: a // minimum on the weighted one would be clamped to nothing when the content is taller. Spacer(Modifier.height(GroupInviteCardDefaults.BUTTON_GAP)) Spacer(Modifier.weight(1f)) - if (resolved != null) { + if (resolved != null && ctaLabel != null) { CodeButton( modifier = Modifier .fillMaxWidth() @@ -193,7 +197,7 @@ fun GroupInviteLinkCard( enabled = onStart != null, onClick = { onStart?.invoke() }, ) - } else { + } else if (resolved == null) { CodeButton( modifier = Modifier .fillMaxWidth() @@ -321,12 +325,12 @@ internal val PreviewGroupResolved = LinkCard.GroupInvite.State.Resolved( ) @Composable -private fun PreviewCard(state: LinkCard.GroupInvite.State) { +private fun PreviewCard(state: LinkCard.GroupInvite.State, ctaLabel: String? = "View") { Box(Modifier.width(280.dp).padding(8.dp)) { GroupInviteLinkCard( card = previewGroupCard(state), - minHeight = 280.dp * 224f / 328f, - ctaLabel = "View", + minHeight = if (ctaLabel != null) 280.dp * 224f / 328f else 0.dp, + ctaLabel = ctaLabel, onStart = {}, ) } @@ -339,6 +343,13 @@ private fun Preview_GroupInvite_Resolved() { PreviewCard(PreviewGroupResolved) } +@Preview +@PreviewWrapper(FlipcashThemeWrapper::class) +@Composable +private fun Preview_GroupInvite_NoButton() { + PreviewCard(PreviewGroupResolved, ctaLabel = null) +} + /** No picture: the avatar's initials on its gradient, and the band tinted from that gradient. */ @Preview @PreviewWrapper(FlipcashThemeWrapper::class)