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
1 change: 1 addition & 0 deletions apps/flipcash/core/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -953,6 +953,7 @@
<string name="label_chat_preview_senderMessage">%1$s: %2$s</string>
<string name="label_chat_preview_sentCash_bySender">%1$s sent %2$s</string>
<string name="label_chat_preview_tippedCash_bySender">%1$s tipped %2$s</string>
<string name="label_chat_preview_noMessages">Nothing yet</string>
<string name="label_isTyping">Is Typing…</string>

<string name="label_edited">Edited</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,11 @@ internal fun TipChatRow(
ChatRowSubtitle(
isTyping = chat.isTyping,
preview = chat.lastMessagePreview,
hasMessages = chat.hasMessages,
fallback = {
// Blank rather than absent: a chat with a message the row cannot
// preview still occupies both lines, so it doesn't jump in height
// against its neighbours.
SubtitleText("")
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,41 @@ class TipChatRowScreenshotTest {
capture("tip_chat_row_identity.png")
}

@Test
fun rendersEmptyChatPlaceholder() {
val at = Instant.fromEpochMilliseconds(1_700_000_000_000)
val rows = listOf(
// A group created but never spoken in: italic placeholder where the preview goes.
ConversationReference(
chatId = ChatId(byteArrayOf(4)),
title = "Bad Boys",
isGroup = true,
hasMessages = false,
lastActivity = at,
),
// Its neighbour, for the weight the placeholder is read against.
ConversationReference(
chatId = ChatId(byteArrayOf(5)),
title = "Dinner Club",
isGroup = true,
lastMessagePreview = "Grace: on my way",
lastActivity = at,
),
)

composeRule.mainClock.autoAdvance = false
composeRule.setContent {
FlipcashPreview(showBackground = true) {
Column(modifier = Modifier.width(360.dp).padding(vertical = 8.dp)) {
rows.forEach { TipChatRow(chat = it, onClick = {}) }
}
}
}
repeat(10) { composeRule.mainClock.advanceTimeByFrame() }

capture("tip_chat_row_empty.png")
}

private fun capture(name: String) {
val root: View = composeRule.activity.findViewById(android.R.id.content)
val width = root.width.takeIf { it > 0 } ?: 1080
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextOverflow
import com.flipcash.core.R
import com.getcode.theme.CodeTheme
Expand Down Expand Up @@ -59,29 +60,43 @@ fun RowScope.ChatRowTrailing(
}

/**
* Subtitle line of a chat row: the typing indicator, else the message [preview], else the
* caller's [fallback] (e.g. a phone number or a "joined" line for a contact).
* Subtitle line of a chat row: the typing indicator, else the message [preview], else a placeholder
* when the chat has no messages at all ([hasMessages]), else the caller's [fallback] (e.g. a phone
* number or a "joined" line for a contact).
*
* The placeholder is keyed on [hasMessages] rather than on the absent [preview] because the two are
* not the same thing: a chat whose newest message is media or a system event previews as nothing
* but is not empty, and telling the viewer it is would be a lie. Rows that cannot tell the
* difference leave [hasMessages] alone and keep their own fallback.
*/
@Composable
fun ChatRowSubtitle(
isTyping: Boolean,
preview: String?,
hasMessages: Boolean = true,
fallback: @Composable () -> Unit = {},
) {
when {
isTyping -> SubtitleText(stringResource(R.string.label_isTyping))
!preview.isNullOrEmpty() -> SubtitleText(preview)
!hasMessages -> SubtitleText(
text = stringResource(R.string.label_chat_preview_noMessages),
// Italic marks it as the row talking about itself rather than quoting a message —
// the same distinction the tombstone bubble draws.
fontStyle = FontStyle.Italic,
)
else -> fallback()
}
}

/** Standard single-line secondary text used for chat/contact row subtitles. */
@Composable
fun SubtitleText(text: String) {
fun SubtitleText(text: String, fontStyle: FontStyle = FontStyle.Normal) {
Text(
text = text,
style = CodeTheme.typography.textSmall,
color = CodeTheme.colors.textSecondary,
fontStyle = fontStyle,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ fun ChatSummary.toConversationReference(
title = metadata.title,
isGroup = isGroup,
lastMessagePreview = formatPreview(selfId, tokensByMint, resources),
hasMessages = metadata.lastMessage != null,
lastActivity = metadata.lastActivity,
unreadCount = unreadCount,
viewerState = metadata.viewerState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ data class ConversationReference(
/** Whether this row is a group. Decides where the name and the avatar's authority come from. */
val isGroup: Boolean = false,
val lastMessagePreview: String? = null,
/**
* Whether the chat has any message at all.
*
* Separate from [lastMessagePreview] being null, which a chat with messages can also be — media
* and system events have no preview line yet. Only this says the chat was created and never
* spoken in, which is the one case a row can call empty.
*/
val hasMessages: Boolean = true,
/** The chat's last-activity timestamp; drives recency sorting and the row's trailing timestamp. */
val lastActivity: Instant? = null,
val unreadCount: Int = 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import io.mockk.every
import io.mockk.mockk
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue
import kotlin.time.Instant
Expand Down Expand Up @@ -226,4 +227,34 @@ class ChatSummaryPreviewTest {

assertEquals("$25.00", groupReference(listOf(cash), senderId = stranger).lastMessagePreview)
}

@Test
fun `a group nobody has spoken in yet reports no messages`() {
val reference = ChatSummary(
metadata = ChatMetadata(
chatId = ChatId(byteArrayOf(9)),
type = ChatType.GROUP,
members = listOf(
ChatMember(userId = self, userProfile = profile("Me"), pointers = emptyList()),
),
lastMessage = null,
lastActivity = sentAt,
title = "Bad Boys",
),
unreadCount = 0,
).toConversationReference(selfId = self, tokensByMint = emptyMap(), resources = resources)

assertFalse(reference.hasMessages)
assertNull(reference.lastMessagePreview)
}

@Test
fun `a chat whose newest message has no preview still counts as having messages`() {
// Media previews as nothing today, which is not the same as an empty chat — the row must
// not label it "Nothing yet".
val reference = groupReference(listOf(MessageContent.Media(items = emptyList(), caption = null)))

assertTrue(reference.hasMessages)
assertNull(reference.lastMessagePreview)
}
}
Loading