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
0562b2ffdb
commit
2e9dce391b
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.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<DirectLogoutState>,
|
||||
) : Presenter<RoomListState> {
|
||||
private val encryptionService: EncryptionService = client.encryptionService()
|
||||
private val syncService: SyncService = client.syncService()
|
||||
|
||||
@Composable
|
||||
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.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<ImmutableList<RoomListRoomSummary>>(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<RoomSummary>) = withContext(coroutineDispatchers.computation) {
|
||||
lock.withLock {
|
||||
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 ->
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<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.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(),
|
||||
),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue