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