Skip to content

Commit 4b01d0d

Browse files
committed
feat(contacts): add join timestamp and sort feed by recency
- Add requireCoinbaseEmailVerification to UserFlags (editable override) - Add joinTs to FlipcashContact, persist to Room, and use in contact feed sorting so recently-joined contacts appear prominently while new messages still sort to the top - Extract ContactMapper for proto-to-domain contact mapping - Return raw proto list from ContactListService, map in repository Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 3256b98 commit 4b01d0d

21 files changed

Lines changed: 765 additions & 22 deletions

File tree

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

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -301,18 +301,23 @@ internal class SendFlowViewModel @Inject constructor(
301301
chatId = chatId,
302302
lastActivity = summary.metadata.lastActivity,
303303
)
304-
}.sortedWith(
305-
compareByDescending<ContactListItem.ContactRow> { it.lastActivity }
306-
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.contact.displayName }
307-
)
304+
}
308305

309-
// On Flipcash — contacts that haven't chatted yet
306+
// On Flipcash — contacts that haven't chatted yet, use joinedAt as their sort timestamp
310307
val flipcashRows = filtered
311308
.filter { it.e164 in contactState.flipcashE164s && it.e164 !in recentsE164s }
312-
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.displayName })
313-
.map { ContactListItem.ContactRow(contact = it, isOnFlipcash = true) }
309+
.map { contact ->
310+
ContactListItem.ContactRow(
311+
contact = contact,
312+
isOnFlipcash = true,
313+
lastActivity = contactState.joinedAtByE164[contact.e164],
314+
)
315+
}
314316

315-
val flipcashCombined = (recentRows + flipcashRows)
317+
val flipcashCombined = (recentRows + flipcashRows).sortedWith(
318+
compareByDescending<ContactListItem.ContactRow> { it.lastActivity }
319+
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.contact.displayName }
320+
)
316321

317322
val excludedE164s = recentsE164s + contactState.flipcashE164s
318323
val other = filtered

apps/flipcash/features/userflags/src/main/kotlin/com/flipcash/app/userflags/internal/UserFlagsViewModel.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,5 @@ private fun ResolvedUserFlags.editableEntries(): List<EditableEntry<*>> = listOf
142142
EditableEntry(Field.WithdrawalFeeAmount, withdrawalFeeAmount),
143143
EditableEntry(Field.PreferredUsdcOnRampLiquidityPool, usdcOnRampLiquidityPool),
144144
EditableEntry(Field.MinimumHolderAmountForLeaderboard, minimumHolderAmountForLeaderboard),
145+
EditableEntry(Field.RequireCoinbaseEmailVerification, requireCoinbaseEmailVerification),
145146
)

apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/ContactCoordinator.kt

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import kotlinx.coroutines.launch
6161
import javax.inject.Inject
6262
import javax.inject.Singleton
6363
import kotlin.collections.map
64+
import kotlin.time.Instant
6465
import kotlin.collections.mapValues
6566

6667

@@ -98,6 +99,7 @@ class ContactCoordinator @Inject constructor(
9899
val contacts: Map<String, DeviceContact> = emptyMap(),
99100
val flipcashE164s: Set<String> = emptySet(),
100101
val dmChatIds: Map<String, String> = emptyMap(),
102+
val joinedAtByE164: Map<String, Instant> = emptyMap(),
101103
val syncState: SyncState = SyncState.Idle,
102104
val hasEverSynced: Boolean = false,
103105
val hasDiscoveredFlipcashContacts: Boolean = false,
@@ -370,12 +372,16 @@ class ContactCoordinator @Inject constructor(
370372
val dmChatIds = mappings
371373
.filter { it.dmChatId.isNotEmpty() }
372374
.associate { it.e164 to it.dmChatId }
375+
val joinedAtByE164 = mappings
376+
.filter { it.joinedAtEpochSeconds > 0 }
377+
.associate { it.e164 to Instant.fromEpochSeconds(it.joinedAtEpochSeconds) }
373378

374379
_state.update {
375380
it.copy(
376381
contacts = contacts,
377382
flipcashE164s = flipcashE164s,
378383
dmChatIds = dmChatIds,
384+
joinedAtByE164 = joinedAtByE164,
379385
hasEverSynced = true,
380386
hasDiscoveredFlipcashContacts = hasDiscoveredFlipcashContacts,
381387
)
@@ -551,12 +557,21 @@ class ContactCoordinator @Inject constructor(
551557
}
552558
}
553559
val dmChatIds = mutableMapOf<String, String>()
560+
val joinedAts = mutableMapOf<String, Instant>()
554561
entries.forEach { entry ->
562+
contactDataSource.updateJoinedAt(entry.phoneNumber, entry.joinedAt.epochSeconds)
563+
joinedAts[entry.phoneNumber] = entry.joinedAt
555564
val chatIdStr = entry.dmChatId?.toString() ?: return@forEach
556565
contactDataSource.updateDmChatId(entry.phoneNumber, chatIdStr)
557566
dmChatIds[entry.phoneNumber] = chatIdStr
558567
}
559-
_state.update { it.copy(flipcashE164s = flipcashE164s, dmChatIds = it.dmChatIds + dmChatIds) }
568+
_state.update {
569+
it.copy(
570+
flipcashE164s = flipcashE164s,
571+
dmChatIds = it.dmChatIds + dmChatIds,
572+
joinedAtByE164 = it.joinedAtByE164 + joinedAts,
573+
)
574+
}
560575
trace(tag = TAG, message = "Found ${flipcashE164s.size} contacts on Flipcash", type = TraceType.Process)
561576
}?.onFailure { error ->
562577
when (error) {

0 commit comments

Comments
 (0)