From 2e9dce391b974cc4b47b6fbcf6aed361e06717e5 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Wed, 16 Oct 2024 11:10:58 +0200 Subject: [PATCH] Refresh room summaries when date or time changes in the device (#3683) * Add `DateTimeObserver` to rebuild the room summary data when the date/time changes. * Add time changed action too, to trigger when the user manually changes date/time * Fix timezone issue by adding `TimezoneProvider`, fix tests * Create test for `DateTimeObserver` usage in `RoomListDataSource` * Create aRoomListRoomSummaryFactory function. * Improve test by faking the lastMessageTimestampFormatter --------- Co-authored-by: Benoit Marty --- .../roomlist/impl/RoomListPresenter.kt | 2 - .../impl/datasource/RoomListDataSource.kt | 30 ++++- .../roomlist/impl/RoomListPresenterTest.kt | 17 ++- .../impl/datasource/RoomListDataSourceTest.kt | 106 ++++++++++++++++++ .../RoomListRoomSummaryFactoryTest.kt | 19 ++++ .../search/RoomListSearchPresenterTest.kt | 4 +- .../androidutils/system/DateTimeObserver.kt | 61 ++++++++++ .../api/LastMessageTimestampFormatter.kt | 2 +- .../dateformatter/impl/DateFormatters.kt | 5 +- .../impl/LocalDateTimeProvider.kt | 7 +- .../dateformatter/impl/TimezoneProvider.kt | 14 +++ .../impl/di/DateFormatterModule.kt | 3 +- ...efaultLastMessageTimestampFormatterTest.kt | 6 +- .../api/RoomLastMessageFormatter.kt | 2 +- samples/minimal/build.gradle.kts | 1 + .../android/samples/minimal/RoomListScreen.kt | 9 +- 16 files changed, 262 insertions(+), 26 deletions(-) create mode 100644 features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSourceTest.kt create mode 100644 features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactoryTest.kt create mode 100644 libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/DateTimeObserver.kt create mode 100644 libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/TimezoneProvider.kt diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt index f8e468637b..248d788f6c 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt @@ -51,7 +51,6 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.libraries.matrix.api.roomlist.RoomList -import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.preferences.api.store.SessionPreferencesStore import io.element.android.libraries.push.api.notifications.NotificationCleaner @@ -93,7 +92,6 @@ class RoomListPresenter @Inject constructor( private val logoutPresenter: Presenter, ) : Presenter { private val encryptionService: EncryptionService = client.encryptionService() - private val syncService: SyncService = client.syncService() @Composable override fun present(): RoomListState { diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSource.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSource.kt index 08c12fb5c4..3ea2549934 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSource.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSource.kt @@ -10,6 +10,7 @@ package io.element.android.features.roomlist.impl.datasource import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.libraries.androidutils.diff.DiffCacheUpdater import io.element.android.libraries.androidutils.diff.MutableListDiffCache +import io.element.android.libraries.androidutils.system.DateTimeObserver import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService @@ -36,9 +37,11 @@ class RoomListDataSource @Inject constructor( private val coroutineDispatchers: CoroutineDispatchers, private val notificationSettingsService: NotificationSettingsService, private val appScope: CoroutineScope, + private val dateTimeObserver: DateTimeObserver, ) { init { observeNotificationSettings() + observeDateTimeChanges() } private val _allRooms = MutableSharedFlow>(replay = 1) @@ -77,6 +80,17 @@ class RoomListDataSource @Inject constructor( .launchIn(appScope) } + private fun observeDateTimeChanges() { + dateTimeObserver.changes + .onEach { event -> + when (event) { + is DateTimeObserver.Event.TimeZoneChanged -> rebuildAllRoomSummaries() + is DateTimeObserver.Event.DateChanged -> rebuildAllRoomSummaries() + } + } + .launchIn(appScope) + } + private suspend fun replaceWith(roomSummaries: List) = withContext(coroutineDispatchers.computation) { lock.withLock { diffCacheUpdater.updateWith(roomSummaries) @@ -84,9 +98,13 @@ class RoomListDataSource @Inject constructor( } } - private suspend fun buildAndEmitAllRooms(roomSummaries: List) { + private suspend fun buildAndEmitAllRooms(roomSummaries: List, useCache: Boolean = true) { val roomListRoomSummaries = diffCache.indices().mapNotNull { index -> - diffCache.get(index) ?: buildAndCacheItem(roomSummaries, index) + if (useCache) { + diffCache.get(index) ?: buildAndCacheItem(roomSummaries, index) + } else { + buildAndCacheItem(roomSummaries, index) + } } _allRooms.emit(roomListRoomSummaries.toImmutableList()) } @@ -96,4 +114,12 @@ class RoomListDataSource @Inject constructor( diffCache[index] = roomListSummary return roomListSummary } + + private suspend fun rebuildAllRoomSummaries() { + lock.withLock { + roomListService.allRooms.summaries.replayCache.firstOrNull()?.let { roomSummaries -> + buildAndEmitAllRooms(roomSummaries, useCache = false) + } + } + } } diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt index 6dfd1b0a0a..cb7ef2852a 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt @@ -22,13 +22,14 @@ import io.element.android.features.logout.api.direct.aDirectLogoutState import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.test.FakeNetworkMonitor import io.element.android.features.roomlist.impl.datasource.RoomListDataSource -import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory +import io.element.android.features.roomlist.impl.datasource.aRoomListRoomSummaryFactory import io.element.android.features.roomlist.impl.filters.RoomListFiltersState import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState import io.element.android.features.roomlist.impl.model.createRoomListRoomSummary import io.element.android.features.roomlist.impl.search.RoomListSearchEvents import io.element.android.features.roomlist.impl.search.RoomListSearchState import io.element.android.features.roomlist.impl.search.aRoomListSearchState +import io.element.android.libraries.androidutils.system.DateTimeObserver import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter import io.element.android.libraries.dateformatter.test.A_FORMATTED_DATE @@ -83,6 +84,7 @@ import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.test import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceTimeBy @@ -649,13 +651,14 @@ class RoomListPresenterTest { leaveRoomPresenter = { leaveRoomState }, roomListDataSource = RoomListDataSource( roomListService = client.roomListService, - roomListRoomSummaryFactory = RoomListRoomSummaryFactory( + roomListRoomSummaryFactory = aRoomListRoomSummaryFactory( lastMessageTimestampFormatter = lastMessageTimestampFormatter, roomLastMessageFormatter = roomLastMessageFormatter, ), coroutineDispatchers = testCoroutineDispatchers(), notificationSettingsService = client.notificationSettingsService(), - appScope = backgroundScope + appScope = backgroundScope, + dateTimeObserver = FakeDateTimeObserver(), ), featureFlagService = featureFlagService, indicatorService = DefaultIndicatorService( @@ -672,3 +675,11 @@ class RoomListPresenterTest { logoutPresenter = { aDirectLogoutState() }, ) } + +class FakeDateTimeObserver : DateTimeObserver { + override val changes = MutableSharedFlow(extraBufferCapacity = 1) + + fun given(event: DateTimeObserver.Event) { + changes.tryEmit(event) + } +} diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSourceTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSourceTest.kt new file mode 100644 index 0000000000..f02c53e6f6 --- /dev/null +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSourceTest.kt @@ -0,0 +1,106 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.roomlist.impl.datasource + +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.features.roomlist.impl.FakeDateTimeObserver +import io.element.android.libraries.androidutils.system.DateTimeObserver +import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter +import io.element.android.libraries.matrix.api.roomlist.RoomListService +import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService +import io.element.android.libraries.matrix.test.room.aRoomSummary +import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test +import java.time.Instant + +class RoomListDataSourceTest { + @Test + fun `when DateTimeObserver gets a date change, the room summaries are refreshed`() = runTest { + val roomListService = FakeRoomListService().apply { + postState(RoomListService.State.Running) + postAllRooms(listOf(aRoomSummary())) + } + val dateTimeObserver = FakeDateTimeObserver() + val lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter() + lastMessageTimestampFormatter.givenFormat("Today") + val roomListDataSource = createRoomListDataSource( + roomListService = roomListService, + roomListRoomSummaryFactory = aRoomListRoomSummaryFactory( + lastMessageTimestampFormatter = lastMessageTimestampFormatter, + ), + dateTimeObserver = dateTimeObserver, + ) + + roomListDataSource.allRooms.test { + // Observe room list items changes + roomListDataSource.launchIn(backgroundScope) + // Get the initial room list + val initialRoomList = awaitItem() + assertThat(initialRoomList).isNotEmpty() + assertThat(initialRoomList.first().timestamp).isEqualTo("Today") + lastMessageTimestampFormatter.givenFormat("Yesterday") + // Trigger a date change + dateTimeObserver.given(DateTimeObserver.Event.DateChanged(Instant.MIN, Instant.now())) + // Check there is a new list and it's not the same as the previous one + val newRoomList = awaitItem() + assertThat(newRoomList).isNotSameInstanceAs(initialRoomList) + assertThat(newRoomList.first().timestamp).isEqualTo("Yesterday") + } + } + + @Test + fun `when DateTimeObserver gets a time zone change, the room summaries are refreshed`() = runTest { + val roomListService = FakeRoomListService().apply { + postState(RoomListService.State.Running) + postAllRooms(listOf(aRoomSummary())) + } + val dateTimeObserver = FakeDateTimeObserver() + val lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter() + lastMessageTimestampFormatter.givenFormat("Today") + val roomListDataSource = createRoomListDataSource( + roomListService = roomListService, + roomListRoomSummaryFactory = aRoomListRoomSummaryFactory( + lastMessageTimestampFormatter = lastMessageTimestampFormatter, + ), + dateTimeObserver = dateTimeObserver, + ) + roomListDataSource.allRooms.test { + // Observe room list items changes + roomListDataSource.launchIn(backgroundScope) + // Get the initial room list + val initialRoomList = awaitItem() + assertThat(initialRoomList).isNotEmpty() + assertThat(initialRoomList.first().timestamp).isEqualTo("Today") + lastMessageTimestampFormatter.givenFormat("Yesterday") + // Trigger a timezone change + dateTimeObserver.given(DateTimeObserver.Event.TimeZoneChanged) + // Check there is a new list and it's not the same as the previous one + val newRoomList = awaitItem() + assertThat(newRoomList).isNotSameInstanceAs(initialRoomList) + assertThat(newRoomList.first().timestamp).isEqualTo("Yesterday") + } + } + + private fun TestScope.createRoomListDataSource( + roomListService: FakeRoomListService = FakeRoomListService(), + roomListRoomSummaryFactory: RoomListRoomSummaryFactory = aRoomListRoomSummaryFactory(), + notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), + dateTimeObserver: FakeDateTimeObserver = FakeDateTimeObserver(), + ) = RoomListDataSource( + roomListService = roomListService, + roomListRoomSummaryFactory = roomListRoomSummaryFactory, + coroutineDispatchers = testCoroutineDispatchers(), + notificationSettingsService = notificationSettingsService, + appScope = backgroundScope, + dateTimeObserver = dateTimeObserver, + ) +} diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactoryTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactoryTest.kt new file mode 100644 index 0000000000..8a26120a9e --- /dev/null +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactoryTest.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.roomlist.impl.datasource + +import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter +import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter + +fun aRoomListRoomSummaryFactory( + lastMessageTimestampFormatter: LastMessageTimestampFormatter = LastMessageTimestampFormatter { _ -> "Today" }, + roomLastMessageFormatter: RoomLastMessageFormatter = RoomLastMessageFormatter { _, _ -> "Hey" } +) = RoomListRoomSummaryFactory( + lastMessageTimestampFormatter = lastMessageTimestampFormatter, + roomLastMessageFormatter = roomLastMessageFormatter +) diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenterTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenterTest.kt index 98e65c830b..6ede9544ec 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenterTest.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenterTest.kt @@ -11,7 +11,7 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory +import io.element.android.features.roomlist.impl.datasource.aRoomListRoomSummaryFactory import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter import io.element.android.libraries.featureflag.api.FeatureFlagService @@ -142,7 +142,7 @@ fun TestScope.createRoomListSearchPresenter( return RoomListSearchPresenter( dataSource = RoomListSearchDataSource( roomListService = roomListService, - roomSummaryFactory = RoomListRoomSummaryFactory( + roomSummaryFactory = aRoomListRoomSummaryFactory( lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter(), roomLastMessageFormatter = FakeRoomLastMessageFormatter(), ), diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/DateTimeObserver.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/DateTimeObserver.kt new file mode 100644 index 0000000000..e37cee7794 --- /dev/null +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/DateTimeObserver.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.androidutils.system + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.androidutils.system.DateTimeObserver.Event +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.SingleIn +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import java.time.Instant +import javax.inject.Inject + +interface DateTimeObserver { + val changes: Flow + + sealed interface Event { + data object TimeZoneChanged : Event + data class DateChanged(val previous: Instant, val new: Instant) : Event + } +} + +@ContributesBinding(AppScope::class) +@SingleIn(AppScope::class) +class DefaultDateTimeObserver @Inject constructor( + @ApplicationContext context: Context +) : DateTimeObserver { + private val dateTimeReceiver = object : BroadcastReceiver() { + private var lastTime = Instant.now() + + override fun onReceive(context: Context, intent: Intent) { + val newDate = Instant.now() + when (intent.action) { + Intent.ACTION_TIMEZONE_CHANGED -> changes.tryEmit(Event.TimeZoneChanged) + Intent.ACTION_DATE_CHANGED -> changes.tryEmit(Event.DateChanged(lastTime, newDate)) + Intent.ACTION_TIME_CHANGED -> changes.tryEmit(Event.DateChanged(lastTime, newDate)) + } + lastTime = newDate + } + } + + override val changes = MutableSharedFlow(extraBufferCapacity = 10) + + init { + context.registerReceiver(dateTimeReceiver, IntentFilter().apply { + addAction(Intent.ACTION_TIMEZONE_CHANGED) + addAction(Intent.ACTION_DATE_CHANGED) + addAction(Intent.ACTION_TIME_CHANGED) + }) + } +} diff --git a/libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/LastMessageTimestampFormatter.kt b/libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/LastMessageTimestampFormatter.kt index b3f82b1efe..c5b9778669 100644 --- a/libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/LastMessageTimestampFormatter.kt +++ b/libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/LastMessageTimestampFormatter.kt @@ -7,6 +7,6 @@ package io.element.android.libraries.dateformatter.api -interface LastMessageTimestampFormatter { +fun interface LastMessageTimestampFormatter { fun format(timestamp: Long?): String } diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatters.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatters.kt index d15b376d3a..a78cc81c24 100644 --- a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatters.kt +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatters.kt @@ -11,7 +11,6 @@ import android.text.format.DateFormat import android.text.format.DateUtils import kotlinx.datetime.Clock import kotlinx.datetime.LocalDateTime -import kotlinx.datetime.TimeZone import kotlinx.datetime.toInstant import kotlinx.datetime.toJavaLocalDate import kotlinx.datetime.toJavaLocalDateTime @@ -25,7 +24,7 @@ import kotlin.math.absoluteValue class DateFormatters @Inject constructor( private val locale: Locale, private val clock: Clock, - private val timeZone: TimeZone, + private val timeZoneProvider: TimezoneProvider, ) { private val onlyTimeFormatter: DateTimeFormatter by lazy { DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(locale) @@ -70,7 +69,7 @@ class DateFormatters @Inject constructor( return if (period.years.absoluteValue >= 1) { formatDateWithYear(dateToFormat) } else if (useRelative && period.days.absoluteValue < 2 && period.months.absoluteValue < 1) { - getRelativeDay(dateToFormat.toInstant(timeZone).toEpochMilliseconds()) + getRelativeDay(dateToFormat.toInstant(timeZoneProvider.provide()).toEpochMilliseconds()) } else { formatDateWithMonth(dateToFormat) } diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/LocalDateTimeProvider.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/LocalDateTimeProvider.kt index 5779b390ef..19e1407f67 100644 --- a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/LocalDateTimeProvider.kt +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/LocalDateTimeProvider.kt @@ -10,21 +10,20 @@ package io.element.android.libraries.dateformatter.impl import kotlinx.datetime.Clock import kotlinx.datetime.Instant import kotlinx.datetime.LocalDateTime -import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime import javax.inject.Inject class LocalDateTimeProvider @Inject constructor( private val clock: Clock, - private val timezone: TimeZone, + private val timezoneProvider: TimezoneProvider, ) { fun providesNow(): LocalDateTime { val now: Instant = clock.now() - return now.toLocalDateTime(timezone) + return now.toLocalDateTime(timezoneProvider.provide()) } fun providesFromTimestamp(timestamp: Long): LocalDateTime { val tsInstant = Instant.fromEpochMilliseconds(timestamp) - return tsInstant.toLocalDateTime(timezone) + return tsInstant.toLocalDateTime(timezoneProvider.provide()) } } diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/TimezoneProvider.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/TimezoneProvider.kt new file mode 100644 index 0000000000..8809422e4e --- /dev/null +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/TimezoneProvider.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.dateformatter.impl + +import kotlinx.datetime.TimeZone + +fun interface TimezoneProvider { + fun provide(): TimeZone +} diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/di/DateFormatterModule.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/di/DateFormatterModule.kt index dc7055a3bc..568bee5378 100644 --- a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/di/DateFormatterModule.kt +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/di/DateFormatterModule.kt @@ -10,6 +10,7 @@ package io.element.android.libraries.dateformatter.impl.di import com.squareup.anvil.annotations.ContributesTo import dagger.Module import dagger.Provides +import io.element.android.libraries.dateformatter.impl.TimezoneProvider import io.element.android.libraries.di.AppScope import kotlinx.datetime.Clock import kotlinx.datetime.TimeZone @@ -25,5 +26,5 @@ object DateFormatterModule { fun providesLocale(): Locale = Locale.getDefault() @Provides - fun providesTimezone(): TimeZone = TimeZone.currentSystemDefault() + fun providesTimezone(): TimezoneProvider = TimezoneProvider { TimeZone.currentSystemDefault() } } diff --git a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageTimestampFormatterTest.kt b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageTimestampFormatterTest.kt index c1a684cdf5..5c8de4462b 100644 --- a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageTimestampFormatterTest.kt +++ b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageTimestampFormatterTest.kt @@ -93,7 +93,7 @@ class DefaultLastMessageTimestampFormatterTest { val now = "1980-04-06T18:35:24.00Z" val dat = "1979-04-06T18:35:24.00Z" val clock = FakeClock().apply { givenInstant(Instant.parse(now)) } - val dateFormatters = DateFormatters(Locale.US, clock, TimeZone.UTC) + val dateFormatters = DateFormatters(Locale.US, clock) { TimeZone.UTC } assertThat(dateFormatters.formatDateWithFullFormat(Instant.parse(dat).toLocalDateTime(TimeZone.UTC))).isEqualTo("Friday, April 6, 1979") } @@ -102,8 +102,8 @@ class DefaultLastMessageTimestampFormatterTest { */ private fun createFormatter(@Suppress("SameParameterValue") currentDate: String): LastMessageTimestampFormatter { val clock = FakeClock().apply { givenInstant(Instant.parse(currentDate)) } - val localDateTimeProvider = LocalDateTimeProvider(clock, TimeZone.UTC) - val dateFormatters = DateFormatters(Locale.US, clock, TimeZone.UTC) + val localDateTimeProvider = LocalDateTimeProvider(clock) { TimeZone.UTC } + val dateFormatters = DateFormatters(Locale.US, clock) { TimeZone.UTC } return DefaultLastMessageTimestampFormatter(localDateTimeProvider, dateFormatters) } } diff --git a/libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api/RoomLastMessageFormatter.kt b/libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api/RoomLastMessageFormatter.kt index e5eabdb9d4..e6815e5bcf 100644 --- a/libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api/RoomLastMessageFormatter.kt +++ b/libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api/RoomLastMessageFormatter.kt @@ -9,6 +9,6 @@ package io.element.android.libraries.eventformatter.api import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem -interface RoomLastMessageFormatter { +fun interface RoomLastMessageFormatter { fun format(event: EventTimelineItem, isDmRoom: Boolean): CharSequence? } diff --git a/samples/minimal/build.gradle.kts b/samples/minimal/build.gradle.kts index ae7b6d58d4..e32054a272 100644 --- a/samples/minimal/build.gradle.kts +++ b/samples/minimal/build.gradle.kts @@ -43,6 +43,7 @@ dependencies { implementation(projects.libraries.permissions.noop) implementation(projects.libraries.sessionStorage.implMemory) implementation(projects.libraries.designsystem) + implementation(projects.libraries.androidutils) implementation(projects.libraries.architecture) implementation(projects.libraries.core) implementation(projects.libraries.network) diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt index 59969ec622..0db3c9da51 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt @@ -24,6 +24,7 @@ import io.element.android.features.roomlist.impl.filters.RoomListFiltersPresente import io.element.android.features.roomlist.impl.filters.selection.DefaultFilterSelectionStrategy import io.element.android.features.roomlist.impl.search.RoomListSearchDataSource import io.element.android.features.roomlist.impl.search.RoomListSearchPresenter +import io.element.android.libraries.androidutils.system.DefaultDateTimeObserver import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.dateformatter.impl.DateFormatters import io.element.android.libraries.dateformatter.impl.DefaultLastMessageTimestampFormatter @@ -59,9 +60,8 @@ class RoomListScreen( ) { private val clock = Clock.System private val locale = Locale.getDefault() - private val timeZone = TimeZone.currentSystemDefault() - private val dateTimeProvider = LocalDateTimeProvider(clock, timeZone) - private val dateFormatters = DateFormatters(locale, clock, timeZone) + private val dateTimeProvider = LocalDateTimeProvider(clock) { TimeZone.currentSystemDefault() } + private val dateFormatters = DateFormatters(locale, clock) { TimeZone.currentSystemDefault() } private val sessionVerificationService = matrixClient.sessionVerificationService() private val encryptionService = matrixClient.encryptionService() private val stringProvider = AndroidStringProvider(context.resources) @@ -92,7 +92,8 @@ class RoomListScreen( roomListRoomSummaryFactory = roomListRoomSummaryFactory, coroutineDispatchers = coroutineDispatchers, notificationSettingsService = matrixClient.notificationSettingsService(), - appScope = Singleton.appScope + appScope = Singleton.appScope, + dateTimeObserver = DefaultDateTimeObserver(context), ), indicatorService = DefaultIndicatorService( sessionVerificationService = sessionVerificationService,