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 @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
Expand All @@ -18,32 +17,21 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource
import com.gemwallet.android.ext.toIdentifier
import com.gemwallet.android.features.asset_select.viewmodels.models.RecentsEmptyState
import com.gemwallet.android.features.asset_select.viewmodels.models.RecentsSheetUIModel
import com.gemwallet.android.ui.components.empty.EmptyContentType
import com.gemwallet.android.ui.components.empty.EmptyContentView
import com.gemwallet.android.model.RecentAsset
import com.gemwallet.android.ui.R
import com.gemwallet.android.ui.components.SearchBar
import com.gemwallet.android.ui.components.list_item.AssetListItem
import com.gemwallet.android.ui.components.list_item.SubheaderItem
import com.gemwallet.android.ui.components.list_item.property.itemsPositioned
import com.gemwallet.android.ui.components.list_item.dateGroupedList
import com.gemwallet.android.ui.components.screen.ModalBottomSheet
import com.gemwallet.android.ui.theme.paddingDefault
import com.gemwallet.android.ui.theme.paddingHalfSmall
import com.wallet.core.primitives.AssetId
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.Locale

@Composable
fun RecentsBottomSheet(
Expand All @@ -54,10 +42,6 @@ fun RecentsBottomSheet(
onClear: () -> Unit,
onSelect: (AssetId) -> Unit,
) {
val todayLabel = stringResource(R.string.date_today)
val yesterdayLabel = stringResource(R.string.date_yesterday)
val locale = LocalConfiguration.current.locales[0]

ModalBottomSheet(
isVisible = isVisible,
onDismissRequest = onDismissRequest,
Expand Down Expand Up @@ -101,38 +85,24 @@ fun RecentsBottomSheet(
if (empty != null) {
RecentsEmptyStateView(empty)
} else {
val sections = remember(uiModel.items, locale, todayLabel, yesterdayLabel) {
buildDateSections(uiModel.items, locale, todayLabel, yesterdayLabel)
}
LazyColumn(modifier = Modifier.fillMaxSize()) {
sections.forEach { section ->
recentsSection(section, onSelect)
dateGroupedList(
items = uiModel.items.sortedByDescending { it.addedAt },
createdAt = { it.addedAt },
key = { _, recent -> "${recent.addedAt}-${recent.asset.id.toIdentifier()}" },
) { position, recent ->
AssetListItem(
asset = recent.asset,
listPosition = position,
modifier = Modifier.clickable { onSelect(recent.asset.id) },
)
}
}
}
}
}
}

private fun LazyListScope.recentsSection(
section: RecentsDateSection,
onSelect: (AssetId) -> Unit,
) {
item(key = "header-${section.id}") {
SubheaderItem(section.title)
}
itemsPositioned(
section.items,
key = { _, recent -> "${section.id}-${recent.asset.id.toIdentifier()}" },
) { position, recent ->
AssetListItem(
asset = recent.asset,
listPosition = position,
modifier = Modifier.clickable { onSelect(recent.asset.id) },
)
}
}

@Composable
private fun RecentsEmptyStateView(state: RecentsEmptyState) {
val type = when (state) {
Expand All @@ -141,42 +111,3 @@ private fun RecentsEmptyStateView(state: RecentsEmptyState) {
}
EmptyContentView(type = type, modifier = Modifier.fillMaxSize())
}

private data class RecentsDateSection(
val id: String,
val title: String,
val items: List<RecentAsset>,
)

private fun buildDateSections(
items: List<RecentAsset>,
locale: Locale,
todayLabel: String,
yesterdayLabel: String,
): List<RecentsDateSection> {
if (items.isEmpty()) return emptyList()
val zone = ZoneId.systemDefault()
val today = LocalDate.now(zone)
val yesterday = today.minusDays(1)
val longFormatter = DateTimeFormatter
.ofLocalizedDate(FormatStyle.LONG)
.withLocale(locale)

return items.groupBy { recent ->
Instant.ofEpochMilli(recent.addedAt).atZone(zone).toLocalDate()
}
.entries
.sortedByDescending { it.key }
.map { (date, values) ->
val title = when (date) {
today -> todayLabel
yesterday -> yesterdayLabel
else -> longFormatter.format(date)
}
RecentsDateSection(
id = date.toString(),
title = title,
items = values.sortedByDescending { it.addedAt },
)
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
package com.gemwallet.android.ui.components.list_item

import android.icu.util.Calendar
import android.text.format.DateUtils
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource
import com.gemwallet.android.ui.R
import com.gemwallet.android.ui.components.list_item.property.itemsPositioned
import com.gemwallet.android.ui.format.SectionDateFormatter
import com.gemwallet.android.ui.models.ListPosition
import java.text.DateFormat
import java.util.Date
import java.time.Instant
import java.time.ZoneId

@OptIn(ExperimentalFoundationApi::class)
fun <T> LazyListScope.dateGroupedList(
Expand All @@ -21,28 +24,20 @@ fun <T> LazyListScope.dateGroupedList(
key: (Int, T) -> Any,
itemContent: @Composable LazyItemScope.(ListPosition, T) -> Unit,
) {
val dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM)
val calendar = Calendar.getInstance()

items.groupBy { item ->
calendar.timeInMillis = createdAt(item)
calendar[Calendar.MILLISECOND] = 999
calendar[Calendar.SECOND] = 59
calendar[Calendar.MINUTE] = 59
calendar[Calendar.HOUR_OF_DAY] = 23
calendar.time.time
}.forEach { (timestamp, entries) ->
stickyHeader {
val title = if (DateUtils.isToday(timestamp) || DateUtils.isToday(timestamp + DateUtils.DAY_IN_MILLIS)) {
DateUtils.getRelativeTimeSpanString(timestamp, System.currentTimeMillis(), DateUtils.DAY_IN_MILLIS).toString()
} else {
dateFormat.format(Date(timestamp))
val zone = ZoneId.systemDefault()
items.groupBy { Instant.ofEpochMilli(createdAt(it)).atZone(zone).toLocalDate() }
.forEach { (date, entries) ->
stickyHeader {
val todayLabel = stringResource(R.string.date_today)
val yesterdayLabel = stringResource(R.string.date_yesterday)
val formatter = remember(todayLabel, yesterdayLabel) {
SectionDateFormatter(todayLabel, yesterdayLabel)
}
SubheaderItem(
title = formatter.format(date, LocalConfiguration.current.locales[0]),
modifier = Modifier.background(MaterialTheme.colorScheme.surface),
)
}
SubheaderItem(
title = title,
modifier = Modifier.background(MaterialTheme.colorScheme.surface),
)
itemsPositioned(entries, key = key, itemContent = itemContent)
}
itemsPositioned(entries, key = key, itemContent = itemContent)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.gemwallet.android.ui.format

import java.time.Clock
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.Locale

class SectionDateFormatter(
private val todayLabel: String,
private val yesterdayLabel: String,
private val clock: Clock = Clock.systemDefaultZone(),
) {

fun format(date: LocalDate, locale: Locale): String {
val today = LocalDate.now(clock)
return when (date) {
today -> todayLabel
today.minusDays(1) -> yesterdayLabel
else -> DateTimeFormatter
.ofLocalizedDate(FormatStyle.LONG)
.withLocale(locale)
.format(date)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.gemwallet.android.ui.format

import org.junit.Assert.assertEquals
import org.junit.Test
import java.time.Clock
import java.time.LocalDate
import java.time.ZoneId
import java.time.ZonedDateTime
import java.util.Locale

class SectionDateFormatterTest {

private val zone = ZoneId.of("UTC")
private val clock = Clock.fixed(
ZonedDateTime.of(2026, 5, 12, 10, 0, 0, 0, zone).toInstant(),
zone,
)
private val locale = Locale.US
private val formatter = SectionDateFormatter(
todayLabel = TODAY,
yesterdayLabel = YESTERDAY,
clock = clock,
)

@Test
fun test_format() {
assertEquals(TODAY, formatter.format(LocalDate.of(2026, 5, 12), locale))
assertEquals(YESTERDAY, formatter.format(LocalDate.of(2026, 5, 11), locale))
assertEquals("May 10, 2026", formatter.format(LocalDate.of(2026, 5, 10), locale))
assertEquals("March 5, 2026", formatter.format(LocalDate.of(2026, 3, 5), locale))
}

companion object {
private const val TODAY = "Today"
private const val YESTERDAY = "Yesterday"
}
}