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>
This commit is contained in:
parent
b15ebfda87
commit
6b2aa7eb8c
16 changed files with 262 additions and 26 deletions
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 ->
|
||||||
diffCache.get(index) ?: buildAndCacheItem(roomSummaries, index)
|
if (useCache) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
@ -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(),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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() }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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?
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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…
Add table
Add a link
Reference in a new issue