Browse Source

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 <benoit@matrix.org>
pull/3673/head
Jorge Martin Espinosa 2 days ago committed by GitHub
parent
commit
2e9dce391b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt
  2. 28
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSource.kt
  3. 17
      features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt
  4. 106
      features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSourceTest.kt
  5. 19
      features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactoryTest.kt
  6. 4
      features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenterTest.kt
  7. 61
      libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/DateTimeObserver.kt
  8. 2
      libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/LastMessageTimestampFormatter.kt
  9. 5
      libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatters.kt
  10. 7
      libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/LocalDateTimeProvider.kt
  11. 14
      libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/TimezoneProvider.kt
  12. 3
      libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/di/DateFormatterModule.kt
  13. 6
      libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageTimestampFormatterTest.kt
  14. 2
      libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api/RoomLastMessageFormatter.kt
  15. 1
      samples/minimal/build.gradle.kts
  16. 9
      samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt

2
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.EncryptionService
import io.element.android.libraries.matrix.api.encryption.RecoveryState 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.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.matrix.api.timeline.ReceiptType
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.push.api.notifications.NotificationCleaner import io.element.android.libraries.push.api.notifications.NotificationCleaner
@ -93,7 +92,6 @@ class RoomListPresenter @Inject constructor(
private val logoutPresenter: Presenter<DirectLogoutState>, private val logoutPresenter: Presenter<DirectLogoutState>,
) : Presenter<RoomListState> { ) : Presenter<RoomListState> {
private val encryptionService: EncryptionService = client.encryptionService() private val encryptionService: EncryptionService = client.encryptionService()
private val syncService: SyncService = client.syncService()
@Composable @Composable
override fun present(): RoomListState { override fun present(): RoomListState {

28
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.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.libraries.androidutils.diff.DiffCacheUpdater import io.element.android.libraries.androidutils.diff.DiffCacheUpdater
import io.element.android.libraries.androidutils.diff.MutableListDiffCache 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.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
@ -36,9 +37,11 @@ class RoomListDataSource @Inject constructor(
private val coroutineDispatchers: CoroutineDispatchers, private val coroutineDispatchers: CoroutineDispatchers,
private val notificationSettingsService: NotificationSettingsService, private val notificationSettingsService: NotificationSettingsService,
private val appScope: CoroutineScope, private val appScope: CoroutineScope,
private val dateTimeObserver: DateTimeObserver,
) { ) {
init { init {
observeNotificationSettings() observeNotificationSettings()
observeDateTimeChanges()
} }
private val _allRooms = MutableSharedFlow<ImmutableList<RoomListRoomSummary>>(replay = 1) private val _allRooms = MutableSharedFlow<ImmutableList<RoomListRoomSummary>>(replay = 1)
@ -77,6 +80,17 @@ class RoomListDataSource @Inject constructor(
.launchIn(appScope) .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<RoomSummary>) = withContext(coroutineDispatchers.computation) { private suspend fun replaceWith(roomSummaries: List<RoomSummary>) = withContext(coroutineDispatchers.computation) {
lock.withLock { lock.withLock {
diffCacheUpdater.updateWith(roomSummaries) diffCacheUpdater.updateWith(roomSummaries)
@ -84,9 +98,13 @@ class RoomListDataSource @Inject constructor(
} }
} }
private suspend fun buildAndEmitAllRooms(roomSummaries: List<RoomSummary>) { private suspend fun buildAndEmitAllRooms(roomSummaries: List<RoomSummary>, useCache: Boolean = true) {
val roomListRoomSummaries = diffCache.indices().mapNotNull { index -> val roomListRoomSummaries = diffCache.indices().mapNotNull { index ->
if (useCache) {
diffCache.get(index) ?: buildAndCacheItem(roomSummaries, index) diffCache.get(index) ?: buildAndCacheItem(roomSummaries, index)
} else {
buildAndCacheItem(roomSummaries, index)
}
} }
_allRooms.emit(roomListRoomSummaries.toImmutableList()) _allRooms.emit(roomListRoomSummaries.toImmutableList())
} }
@ -96,4 +114,12 @@ class RoomListDataSource @Inject constructor(
diffCache[index] = roomListSummary diffCache[index] = roomListSummary
return roomListSummary return roomListSummary
} }
private suspend fun rebuildAllRoomSummaries() {
lock.withLock {
roomListService.allRooms.summaries.replayCache.firstOrNull()?.let { roomSummaries ->
buildAndEmitAllRooms(roomSummaries, useCache = false)
}
}
}
} }

17
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.api.NetworkMonitor
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor 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.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.RoomListFiltersState
import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState 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.model.createRoomListRoomSummary
import io.element.android.features.roomlist.impl.search.RoomListSearchEvents 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.RoomListSearchState
import io.element.android.features.roomlist.impl.search.aRoomListSearchState 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.architecture.Presenter
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
import io.element.android.libraries.dateformatter.test.A_FORMATTED_DATE 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.test
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.advanceTimeBy
@ -649,13 +651,14 @@ class RoomListPresenterTest {
leaveRoomPresenter = { leaveRoomState }, leaveRoomPresenter = { leaveRoomState },
roomListDataSource = RoomListDataSource( roomListDataSource = RoomListDataSource(
roomListService = client.roomListService, roomListService = client.roomListService,
roomListRoomSummaryFactory = RoomListRoomSummaryFactory( roomListRoomSummaryFactory = aRoomListRoomSummaryFactory(
lastMessageTimestampFormatter = lastMessageTimestampFormatter, lastMessageTimestampFormatter = lastMessageTimestampFormatter,
roomLastMessageFormatter = roomLastMessageFormatter, roomLastMessageFormatter = roomLastMessageFormatter,
), ),
coroutineDispatchers = testCoroutineDispatchers(), coroutineDispatchers = testCoroutineDispatchers(),
notificationSettingsService = client.notificationSettingsService(), notificationSettingsService = client.notificationSettingsService(),
appScope = backgroundScope appScope = backgroundScope,
dateTimeObserver = FakeDateTimeObserver(),
), ),
featureFlagService = featureFlagService, featureFlagService = featureFlagService,
indicatorService = DefaultIndicatorService( indicatorService = DefaultIndicatorService(
@ -672,3 +675,11 @@ class RoomListPresenterTest {
logoutPresenter = { aDirectLogoutState() }, logoutPresenter = { aDirectLogoutState() },
) )
} }
class FakeDateTimeObserver : DateTimeObserver {
override val changes = MutableSharedFlow<DateTimeObserver.Event>(extraBufferCapacity = 1)
fun given(event: DateTimeObserver.Event) {
changes.tryEmit(event)
}
}

106
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,
)
}

19
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
)

4
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.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat 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.dateformatter.test.FakeLastMessageTimestampFormatter
import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter
import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlagService
@ -142,7 +142,7 @@ fun TestScope.createRoomListSearchPresenter(
return RoomListSearchPresenter( return RoomListSearchPresenter(
dataSource = RoomListSearchDataSource( dataSource = RoomListSearchDataSource(
roomListService = roomListService, roomListService = roomListService,
roomSummaryFactory = RoomListRoomSummaryFactory( roomSummaryFactory = aRoomListRoomSummaryFactory(
lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter(), lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter(),
roomLastMessageFormatter = FakeRoomLastMessageFormatter(), roomLastMessageFormatter = FakeRoomLastMessageFormatter(),
), ),

61
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<Event>
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<Event>(extraBufferCapacity = 10)
init {
context.registerReceiver(dateTimeReceiver, IntentFilter().apply {
addAction(Intent.ACTION_TIMEZONE_CHANGED)
addAction(Intent.ACTION_DATE_CHANGED)
addAction(Intent.ACTION_TIME_CHANGED)
})
}
}

2
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 package io.element.android.libraries.dateformatter.api
interface LastMessageTimestampFormatter { fun interface LastMessageTimestampFormatter {
fun format(timestamp: Long?): String fun format(timestamp: Long?): String
} }

5
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 android.text.format.DateUtils
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant import kotlinx.datetime.toInstant
import kotlinx.datetime.toJavaLocalDate import kotlinx.datetime.toJavaLocalDate
import kotlinx.datetime.toJavaLocalDateTime import kotlinx.datetime.toJavaLocalDateTime
@ -25,7 +24,7 @@ import kotlin.math.absoluteValue
class DateFormatters @Inject constructor( class DateFormatters @Inject constructor(
private val locale: Locale, private val locale: Locale,
private val clock: Clock, private val clock: Clock,
private val timeZone: TimeZone, private val timeZoneProvider: TimezoneProvider,
) { ) {
private val onlyTimeFormatter: DateTimeFormatter by lazy { private val onlyTimeFormatter: DateTimeFormatter by lazy {
DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(locale) DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(locale)
@ -70,7 +69,7 @@ class DateFormatters @Inject constructor(
return if (period.years.absoluteValue >= 1) { return if (period.years.absoluteValue >= 1) {
formatDateWithYear(dateToFormat) formatDateWithYear(dateToFormat)
} else if (useRelative && period.days.absoluteValue < 2 && period.months.absoluteValue < 1) { } else if (useRelative && period.days.absoluteValue < 2 && period.months.absoluteValue < 1) {
getRelativeDay(dateToFormat.toInstant(timeZone).toEpochMilliseconds()) getRelativeDay(dateToFormat.toInstant(timeZoneProvider.provide()).toEpochMilliseconds())
} else { } else {
formatDateWithMonth(dateToFormat) formatDateWithMonth(dateToFormat)
} }

7
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.Clock
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime import kotlinx.datetime.toLocalDateTime
import javax.inject.Inject import javax.inject.Inject
class LocalDateTimeProvider @Inject constructor( class LocalDateTimeProvider @Inject constructor(
private val clock: Clock, private val clock: Clock,
private val timezone: TimeZone, private val timezoneProvider: TimezoneProvider,
) { ) {
fun providesNow(): LocalDateTime { fun providesNow(): LocalDateTime {
val now: Instant = clock.now() val now: Instant = clock.now()
return now.toLocalDateTime(timezone) return now.toLocalDateTime(timezoneProvider.provide())
} }
fun providesFromTimestamp(timestamp: Long): LocalDateTime { fun providesFromTimestamp(timestamp: Long): LocalDateTime {
val tsInstant = Instant.fromEpochMilliseconds(timestamp) val tsInstant = Instant.fromEpochMilliseconds(timestamp)
return tsInstant.toLocalDateTime(timezone) return tsInstant.toLocalDateTime(timezoneProvider.provide())
} }
} }

14
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
}

3
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 com.squareup.anvil.annotations.ContributesTo
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import io.element.android.libraries.dateformatter.impl.TimezoneProvider
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import kotlinx.datetime.TimeZone import kotlinx.datetime.TimeZone
@ -25,5 +26,5 @@ object DateFormatterModule {
fun providesLocale(): Locale = Locale.getDefault() fun providesLocale(): Locale = Locale.getDefault()
@Provides @Provides
fun providesTimezone(): TimeZone = TimeZone.currentSystemDefault() fun providesTimezone(): TimezoneProvider = TimezoneProvider { TimeZone.currentSystemDefault() }
} }

6
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 now = "1980-04-06T18:35:24.00Z"
val dat = "1979-04-06T18:35:24.00Z" val dat = "1979-04-06T18:35:24.00Z"
val clock = FakeClock().apply { givenInstant(Instant.parse(now)) } 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") 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 { private fun createFormatter(@Suppress("SameParameterValue") currentDate: String): LastMessageTimestampFormatter {
val clock = FakeClock().apply { givenInstant(Instant.parse(currentDate)) } val clock = FakeClock().apply { givenInstant(Instant.parse(currentDate)) }
val localDateTimeProvider = LocalDateTimeProvider(clock, TimeZone.UTC) val localDateTimeProvider = LocalDateTimeProvider(clock) { TimeZone.UTC }
val dateFormatters = DateFormatters(Locale.US, clock, TimeZone.UTC) val dateFormatters = DateFormatters(Locale.US, clock) { TimeZone.UTC }
return DefaultLastMessageTimestampFormatter(localDateTimeProvider, dateFormatters) return DefaultLastMessageTimestampFormatter(localDateTimeProvider, dateFormatters)
} }
} }

2
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 import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
interface RoomLastMessageFormatter { fun interface RoomLastMessageFormatter {
fun format(event: EventTimelineItem, isDmRoom: Boolean): CharSequence? fun format(event: EventTimelineItem, isDmRoom: Boolean): CharSequence?
} }

1
samples/minimal/build.gradle.kts

@ -43,6 +43,7 @@ dependencies {
implementation(projects.libraries.permissions.noop) implementation(projects.libraries.permissions.noop)
implementation(projects.libraries.sessionStorage.implMemory) implementation(projects.libraries.sessionStorage.implMemory)
implementation(projects.libraries.designsystem) implementation(projects.libraries.designsystem)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.libraries.network) implementation(projects.libraries.network)

9
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.filters.selection.DefaultFilterSelectionStrategy
import io.element.android.features.roomlist.impl.search.RoomListSearchDataSource import io.element.android.features.roomlist.impl.search.RoomListSearchDataSource
import io.element.android.features.roomlist.impl.search.RoomListSearchPresenter 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.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.dateformatter.impl.DateFormatters import io.element.android.libraries.dateformatter.impl.DateFormatters
import io.element.android.libraries.dateformatter.impl.DefaultLastMessageTimestampFormatter import io.element.android.libraries.dateformatter.impl.DefaultLastMessageTimestampFormatter
@ -59,9 +60,8 @@ class RoomListScreen(
) { ) {
private val clock = Clock.System private val clock = Clock.System
private val locale = Locale.getDefault() private val locale = Locale.getDefault()
private val timeZone = TimeZone.currentSystemDefault() private val dateTimeProvider = LocalDateTimeProvider(clock) { TimeZone.currentSystemDefault() }
private val dateTimeProvider = LocalDateTimeProvider(clock, timeZone) private val dateFormatters = DateFormatters(locale, clock) { TimeZone.currentSystemDefault() }
private val dateFormatters = DateFormatters(locale, clock, timeZone)
private val sessionVerificationService = matrixClient.sessionVerificationService() private val sessionVerificationService = matrixClient.sessionVerificationService()
private val encryptionService = matrixClient.encryptionService() private val encryptionService = matrixClient.encryptionService()
private val stringProvider = AndroidStringProvider(context.resources) private val stringProvider = AndroidStringProvider(context.resources)
@ -92,7 +92,8 @@ class RoomListScreen(
roomListRoomSummaryFactory = roomListRoomSummaryFactory, roomListRoomSummaryFactory = roomListRoomSummaryFactory,
coroutineDispatchers = coroutineDispatchers, coroutineDispatchers = coroutineDispatchers,
notificationSettingsService = matrixClient.notificationSettingsService(), notificationSettingsService = matrixClient.notificationSettingsService(),
appScope = Singleton.appScope appScope = Singleton.appScope,
dateTimeObserver = DefaultDateTimeObserver(context),
), ),
indicatorService = DefaultIndicatorService( indicatorService = DefaultIndicatorService(
sessionVerificationService = sessionVerificationService, sessionVerificationService = sessionVerificationService,

Loading…
Cancel
Save