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:
Jorge Martin Espinosa 2024-10-16 11:10:58 +02:00 committed by GitHub
parent 0562b2ffdb
commit 2e9dce391b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 262 additions and 26 deletions

View file

@ -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 {

View file

@ -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)
}
}
}
}

View file

@ -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)
}
}

View file

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

View file

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

View file

@ -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(),
),