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
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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() },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 },
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand All @@ -193,7 +197,7 @@ fun GroupInviteLinkCard(
enabled = onStart != null,
onClick = { onStart?.invoke() },
)
} else {
} else if (resolved == null) {
CodeButton(
modifier = Modifier
.fillMaxWidth()
Expand Down Expand Up @@ -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 = {},
)
}
Expand All @@ -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)
Expand Down
Loading