Skip to content

Commit 4948023

Browse files
committed
feat(send): update contact/chat list row item
* Deliniate received vs sent messages * Show a message preview for contact joins (on flipcash, no chat yet) * Color timestamps to match unread indicator * Remove vertically centered chevrons for non chat rows; uses the timestamp aligned/top row chevron Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 7be4109 commit 4948023

3 files changed

Lines changed: 194 additions & 22 deletions

File tree

apps/flipcash/core/src/main/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -827,6 +827,7 @@
827827
<string name="label_chat_preview_cash_suffix">%1$s of %2$s</string>
828828
<string name="label_chat_preview_sentCash">You sent %1$s</string>
829829
<string name="label_chat_preview_receivedCash">You received %1$s</string>
830+
<string name="label_chat_preview_sentMessage">You: %1$s</string>
830831
<string name="label_isTyping">Is Typing…</string>
831832

832833
<string name="label_unknownContact">Unknown Contact</string>

apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/ContactListBuilder.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,14 @@ internal class ContactListBuilder @Inject constructor(
154154
val sentBySelf = lastMsg.senderId != null && lastMsg.senderId == selfId
155155
return lastMsg.content.firstOrNull()?.let { content ->
156156
when (content) {
157-
is MessageContent.Text -> content.text.takeIf { it.isNotEmpty() }
157+
is MessageContent.Text -> {
158+
val message = content.text.takeIf { it.isNotEmpty() } ?: return null
159+
if (sentBySelf) {
160+
resources.getString(R.string.label_chat_preview_sentMessage, message)
161+
} else {
162+
message
163+
}
164+
}
158165
is MessageContent.Cash -> {
159166
val formatted = content.amount.formatted()
160167
val name = content.tokenName.ifBlank { tokensByMint[content.mint]?.name.orEmpty() }

apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/components/ContactList.kt

Lines changed: 185 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.flipcash.app.directsend.internal.screens.components
22

33
import android.text.format.DateFormat
4+
import androidx.compose.animation.animateColorAsState
45
import androidx.compose.foundation.Image
56
import androidx.compose.foundation.background
67
import androidx.compose.foundation.border
@@ -21,7 +22,9 @@ import androidx.compose.foundation.lazy.LazyColumn
2122
import androidx.compose.foundation.lazy.LazyListState
2223
import androidx.compose.foundation.lazy.itemsIndexed
2324
import androidx.compose.foundation.lazy.rememberLazyListState
25+
import androidx.compose.foundation.rememberScrollState
2426
import androidx.compose.foundation.shape.CircleShape
27+
import androidx.compose.foundation.verticalScroll
2528
import androidx.compose.foundation.text.input.TextFieldState
2629
import androidx.compose.material.icons.Icons
2730
import androidx.compose.material.icons.filled.Add
@@ -30,6 +33,7 @@ import androidx.compose.material3.HorizontalDivider
3033
import androidx.compose.material3.Icon
3134
import androidx.compose.material3.Text
3235
import androidx.compose.runtime.Composable
36+
import androidx.compose.runtime.getValue
3337
import androidx.compose.ui.Alignment
3438
import androidx.compose.ui.Modifier
3539
import androidx.compose.ui.platform.testTag
@@ -51,9 +55,11 @@ import com.flipcash.app.contacts.ui.ContactAvatar
5155
import com.flipcash.app.core.android.extensions.launchAppSettings
5256
import com.flipcash.app.core.contacts.DeviceContact
5357
import com.flipcash.app.directsend.internal.ContactListItem
58+
import com.flipcash.app.directsend.internal.Conversation
5459
import com.flipcash.app.permissions.ContactAccessHandle
5560
import com.flipcash.app.theme.FlipcashThemeWrapper
5661
import com.flipcash.features.directsend.R
62+
import com.flipcash.services.models.chat.ChatId
5763
import com.flipcash.shared.chat.ui.AnimatedConversationPaymentsPreview
5864
import com.getcode.theme.CodeTheme
5965
import com.getcode.theme.White10
@@ -66,6 +72,9 @@ import com.getcode.util.permissions.PermissionResult
6672
import kotlinx.datetime.TimeZone
6773
import kotlinx.datetime.toLocalDateTime
6874
import kotlin.time.Clock
75+
import kotlin.time.Duration.Companion.days
76+
import kotlin.time.Duration.Companion.hours
77+
import kotlin.time.Duration.Companion.minutes
6978
import kotlin.time.Instant
7079

7180
@Composable
@@ -396,11 +405,14 @@ private fun ContactRowItem(
396405
color = CodeTheme.colors.textSecondary,
397406
)
398407
}
399-
lastActivity != null && isActiveChat -> {
408+
lastActivity != null -> {
409+
val activityTextColor by animateColorAsState(
410+
if (unreadCount > 0) CodeTheme.colors.indicator else CodeTheme.colors.textSecondary
411+
)
400412
Text(
401413
text = formatLastActivity(lastActivity),
402414
style = CodeTheme.typography.caption,
403-
color = CodeTheme.colors.textSecondary,
415+
color = activityTextColor,
404416
)
405417
}
406418
}
@@ -413,7 +425,7 @@ private fun ContactRowItem(
413425
),
414426
count = unreadCount
415427
)
416-
} else if (isActiveChat) {
428+
} else {
417429
Icon(
418430
modifier = Modifier.scale(0.6f),
419431
painter = painterResource(id = R.drawable.ic_chevron_right),
@@ -423,20 +435,18 @@ private fun ContactRowItem(
423435
}
424436
}
425437

426-
val showSubtitle = lastMessagePreview != null || !isOnFlipcash
427-
428438
when {
429439
isTyping -> Text(
430440
text = stringResource(R.string.label_isTyping),
431441
style = CodeTheme.typography.textSmall,
432442
color = CodeTheme.colors.textSecondary,
433443
)
434444

435-
showSubtitle -> Text(
445+
else -> Text(
436446
text = if (isOnFlipcash && !lastMessagePreview.isNullOrEmpty()) {
437447
lastMessagePreview
438448
} else {
439-
contact.displayNumber.ifEmpty { contact.e164 }
449+
"${contact.displayName} joined Flipcash"
440450
},
441451
style = CodeTheme.typography.textSmall,
442452
color = CodeTheme.colors.textSecondary,
@@ -446,15 +456,7 @@ private fun ContactRowItem(
446456
}
447457
}
448458

449-
if (isOnFlipcash) {
450-
if (!isActiveChat) {
451-
Icon(
452-
painter = painterResource(id = R.drawable.ic_chevron_right),
453-
contentDescription = null,
454-
tint = CodeTheme.colors.textSecondary,
455-
)
456-
}
457-
} else {
459+
if (!isOnFlipcash) {
458460
Text(
459461
modifier = Modifier
460462
.background(
@@ -583,7 +585,7 @@ private fun ContactListPreview() {
583585
val fakeNames = listOf(
584586
"Alice Anderson", "Bob Baker", "Charlie Chen", "Dana Davis",
585587
"Eli Evans", "Fiona Fisher", "George Garcia", "Hannah Hill",
586-
"Isaac Ingram", "Julia Jones", "Kevin Kim", "Latif Peracha",
588+
"Isaac Ingram", "Julia Jones", "Kevin Kim", "Lenny Johnson",
587589
"Maya Martinez", "Noah Nguyen", "Olivia Ortiz", "Paul Park",
588590
"Quinn Quinn", "Rachel Robinson", "Sam Smith", "Tina Torres",
589591
)
@@ -613,11 +615,51 @@ private fun ContactListPreview() {
613615
)
614616
}
615617

618+
val now = Clock.System.now()
619+
val recents = listOf(
620+
// Unread conversation with a message preview
621+
ContactListItem.ContactRow(
622+
contact = flipcashContacts[0].contact,
623+
isOnFlipcash = true,
624+
lastActivity = now - 5.minutes,
625+
conversation = Conversation(
626+
chatId = ChatId(byteArrayOf(1)),
627+
lastMessagePreview = "Sent you $10.00",
628+
unreadCount = 2,
629+
),
630+
),
631+
// Contact is currently typing
632+
ContactListItem.ContactRow(
633+
contact = flipcashContacts[1].contact,
634+
isOnFlipcash = true,
635+
lastActivity = now - 1.hours,
636+
conversation = Conversation(
637+
chatId = ChatId(byteArrayOf(2)),
638+
lastMessagePreview = "See you soon",
639+
isTyping = true,
640+
),
641+
),
642+
// Read conversation from yesterday
643+
ContactListItem.ContactRow(
644+
contact = flipcashContacts[2].contact,
645+
isOnFlipcash = true,
646+
lastActivity = now - 26.hours,
647+
conversation = Conversation(
648+
chatId = ChatId(byteArrayOf(3)),
649+
lastMessagePreview = "You: Thanks!",
650+
),
651+
),
652+
)
653+
616654
val items = buildList {
617-
add(ContactListItem.Header("Recents"))
618-
addAll(flipcashContacts.take(3))
619-
add(ContactListItem.Header("On Flipcash"))
620-
addAll(flipcashContacts.drop(3))
655+
addAll(recents)
656+
// On-Flipcash contacts without a chat still rank by joinedAt,
657+
// so they carry a lastActivity timestamp too.
658+
addAll(
659+
flipcashContacts.drop(3).mapIndexed { i, row ->
660+
row.copy(lastActivity = now - (i + 2).days)
661+
}
662+
)
621663
add(ContactListItem.Header("Not On Flipcash Yet"))
622664
addAll(otherContacts)
623665
}
@@ -628,4 +670,126 @@ private fun ContactListPreview() {
628670
listState = rememberLazyListState(),
629671
accessHandle = ContactAccessHandle(launch = {}),
630672
)
673+
}
674+
675+
@Preview(heightDp = 1400)
676+
@PreviewWrapper(FlipcashThemeWrapper::class)
677+
@Composable
678+
private fun ContactRowItemStatesPreview() {
679+
val now = Clock.System.now()
680+
val known = DeviceContact(
681+
e164 = "+15550001234",
682+
androidContactId = 1L,
683+
displayName = "Bob Baker",
684+
photoUri = null,
685+
displayNumber = "(555) 000-1234",
686+
)
687+
val unknown = DeviceContact.unknownContact(
688+
e164 = "+15559998888",
689+
displayName = "+1 (555) 999-8888",
690+
displayNumber = "+1 (555) 999-8888",
691+
)
692+
693+
Column(
694+
modifier = Modifier
695+
.fillMaxSize()
696+
.background(CodeTheme.colors.background)
697+
.verticalScroll(rememberScrollState()),
698+
) {
699+
// On Flipcash · active chat · read (message preview, timestamp, chevron)
700+
StateLabel("On Flipcash · active chat · read")
701+
ContactRowItem(
702+
contact = known,
703+
isOnFlipcash = true,
704+
lastMessagePreview = "See you tomorrow!",
705+
lastActivity = now - 30.minutes,
706+
) {}
707+
708+
// On Flipcash · active chat · unread (badge + indicator-colored timestamp)
709+
StateLabel("On Flipcash · active chat · unread")
710+
ContactRowItem(
711+
contact = known,
712+
isOnFlipcash = true,
713+
lastMessagePreview = "Sent you $5.00",
714+
lastActivity = now - 5.minutes,
715+
unreadCount = 3,
716+
) {}
717+
718+
// On Flipcash · typing (subtitle overridden by "is typing…")
719+
StateLabel("On Flipcash · typing")
720+
ContactRowItem(
721+
contact = known,
722+
isOnFlipcash = true,
723+
lastMessagePreview = "Old message",
724+
lastActivity = now - 2.hours,
725+
isTyping = true,
726+
) {}
727+
728+
// On Flipcash · typing + unread
729+
StateLabel("On Flipcash · typing · unread")
730+
ContactRowItem(
731+
contact = known,
732+
isOnFlipcash = true,
733+
lastMessagePreview = "Old message",
734+
lastActivity = now - 2.hours,
735+
unreadCount = 1,
736+
isTyping = true,
737+
) {}
738+
739+
// On Flipcash · no chat yet (still shows joinedAt timestamp, no subtitle)
740+
StateLabel("On Flipcash · no chat yet")
741+
ContactRowItem(
742+
contact = known,
743+
isOnFlipcash = true,
744+
lastActivity = now - 3.days,
745+
) {}
746+
747+
// On Flipcash · long preview (single line, ellipsized)
748+
StateLabel("On Flipcash · long preview (ellipsized)")
749+
ContactRowItem(
750+
contact = known,
751+
isOnFlipcash = true,
752+
lastMessagePreview = "This is a really long message preview that should be truncated with an ellipsis when it runs out of room",
753+
lastActivity = now - 26.hours,
754+
) {}
755+
756+
// On Flipcash · older activity (date formatting instead of time)
757+
StateLabel("On Flipcash · older activity")
758+
ContactRowItem(
759+
contact = known,
760+
isOnFlipcash = true,
761+
lastMessagePreview = "Thanks!",
762+
lastActivity = now - 4.days,
763+
) {}
764+
765+
// On Flipcash · unknown contact ("Unknown" label replaces timestamp)
766+
StateLabel("On Flipcash · unknown contact")
767+
ContactRowItem(
768+
contact = unknown,
769+
isOnFlipcash = true,
770+
lastMessagePreview = "Hey, is this you?",
771+
lastActivity = now - 10.minutes,
772+
) {}
773+
774+
// Not on Flipcash · invite (phone number subtitle + Invite chip)
775+
StateLabel("Not on Flipcash · invite")
776+
ContactRowItem(
777+
contact = known,
778+
isOnFlipcash = false,
779+
) {}
780+
}
781+
}
782+
783+
@Composable
784+
private fun StateLabel(text: String) {
785+
Text(
786+
modifier = Modifier.padding(
787+
start = CodeTheme.dimens.inset,
788+
top = CodeTheme.dimens.grid.x3,
789+
bottom = CodeTheme.dimens.grid.x1,
790+
),
791+
text = text,
792+
style = CodeTheme.typography.caption,
793+
color = CodeTheme.colors.textSecondary,
794+
)
631795
}

0 commit comments

Comments
 (0)