mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 21:32:34 +00:00
Merge remote-tracking branch 'upstream/develop' into feature/scroll-to-unread-messages
Resolved conflicts: - libraries/matrix/impl/.../fakes/FakeFfiSpaceRoomList.kt: took upstream's direct return for rooms() — the simulateLongTask wrapper is for IO-style operations like paginate/reset, not a quick read. - libraries/ui-strings/src/main/res/values/temporary.xml: kept the file with only the live strings (action_mark_as_read, a11y_jump_to_unread_messages). Dropped common_black (upstream moved theme strings to module-local R.string) and a11y_jump_to_unread_messages_count (badge count was removed earlier). - RoomInfo.kt and its fixtures/tests: took upstream's version since both branches independently added fullyReadEventId via the same SDK bump (matrix-rust-components-kotlin 26.05.20). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+3
-2
@@ -83,8 +83,8 @@ class RoomListDataSource(
|
||||
|
||||
val loadingState = roomList.loadingState
|
||||
|
||||
fun launchIn(coroutineScope: CoroutineScope) {
|
||||
roomList
|
||||
fun launchIn(coroutineScope: CoroutineScope): Job {
|
||||
return roomList
|
||||
.summaries
|
||||
.onEach { roomSummaries ->
|
||||
replaceWith(roomSummaries)
|
||||
@@ -212,6 +212,7 @@ class RoomListDataSource(
|
||||
private suspend fun rebuildAllRoomSummaries() {
|
||||
lock.withLock {
|
||||
roomList.summaries.replayCache.firstOrNull()?.let { roomSummaries ->
|
||||
diffCacheUpdater.updateWith(roomSummaries)
|
||||
buildAndEmitAllRooms(roomSummaries, useCache = false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
<string name="banner_battery_optimization_title_android">"Sa ei näe kõiki teavitusi?"</string>
|
||||
<string name="banner_new_sound_message">"Sinu nutiseadme teavituste heli on uuenenud - see on nüüd selgem, kiirem ja vähem häiriv."</string>
|
||||
<string name="banner_new_sound_title">"Oleme sinu helisid värskendanud"</string>
|
||||
<string name="banner_set_up_recovery_content">"Loo uus taastevõti, mida saad kasutada oma krüptitud sõnumite ajaloo taastamisel olukorras, kus kaotad ligipääsu oma seadmetele."</string>
|
||||
<string name="banner_set_up_recovery_submit">"Seadista andmete taastamine"</string>
|
||||
<string name="banner_set_up_recovery_content">"Sinu vestlused on automaatselt varundatud kasutades läbivat krüptimist. Kui peaksid kaotama ligipääsu kõikidele oma seadmetele, siis selle varukoopia taastamiseks ja oma digitaalse identiteedi säilitamiseks, on vaja taastevõtit."</string>
|
||||
<string name="banner_set_up_recovery_submit">"Seadista taastevõti"</string>
|
||||
<string name="banner_set_up_recovery_title">"Varunda oma vestlused"</string>
|
||||
<string name="confirm_recovery_key_banner_message">"Säilitamaks ligipääsu vestluste ja krüptovõtmete varukoopiale, palun sisesta kinnituseks oma taastevõti."</string>
|
||||
<string name="confirm_recovery_key_banner_primary_button_title">"Sisesta oma taastevõti"</string>
|
||||
|
||||
@@ -50,6 +50,7 @@ Nu aveți mesaje necitite!"</string>
|
||||
<string name="screen_roomlist_mark_as_read">"Marcați ca citită"</string>
|
||||
<string name="screen_roomlist_mark_as_unread">"Marcați ca necitită"</string>
|
||||
<string name="screen_roomlist_tombstoned_room_description">"Această cameră a fost modernizată."</string>
|
||||
<string name="screen_roomlist_your_spaces">"Spațiile dumneavoastră"</string>
|
||||
<string name="session_verification_banner_message">"Se pare că folosiți un dispozitiv nou. Verificați-vă identitatea cu un alt dispozitiv pentru a accesa mesajele dumneavoastră criptate."</string>
|
||||
<string name="session_verification_banner_title">"Verificați că sunteți dumneavoastră"</string>
|
||||
</resources>
|
||||
|
||||
+162
-1
@@ -14,6 +14,9 @@ import io.element.android.features.home.impl.FakeDateTimeObserver
|
||||
import io.element.android.libraries.androidutils.system.DateTimeObserver
|
||||
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_3
|
||||
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.FakeDynamicRoomList
|
||||
@@ -100,11 +103,169 @@ class RoomListDataSourceTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracking issue #4182: rooms duplicated in the room list around midnight.
|
||||
*
|
||||
* If the SDK ever leaks a list containing the same roomId twice (the suspected cause of #4182),
|
||||
* the UI mapper's `distinctBy` safety net in [RoomListDataSource.buildAndEmitAllRooms] must
|
||||
* remove the duplicate AND `analyticsService.trackError` must fire so the team can root-cause
|
||||
* it via Sentry.
|
||||
*/
|
||||
@Test
|
||||
fun `when SDK summaries source contains duplicate roomIds, UI layer dedupes and reports trackError`() = runTest {
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
val duplicatedSummaries = listOf(
|
||||
aRoomSummary(roomId = A_ROOM_ID),
|
||||
aRoomSummary(roomId = A_ROOM_ID),
|
||||
aRoomSummary(roomId = A_ROOM_ID_2),
|
||||
)
|
||||
val roomList = FakeDynamicRoomList(summaries = MutableStateFlow(duplicatedSummaries))
|
||||
val roomListService = FakeRoomListService(
|
||||
createRoomListLambda = { roomList }
|
||||
).apply {
|
||||
postState(RoomListService.State.Running)
|
||||
}
|
||||
val roomListDataSource = createRoomListDataSource(
|
||||
roomListService = roomListService,
|
||||
analyticsService = analyticsService,
|
||||
)
|
||||
|
||||
roomListDataSource.roomSummariesFlow.test {
|
||||
roomListDataSource.launchIn(backgroundScope)
|
||||
val list = awaitItem()
|
||||
assertThat(list.map { it.roomId }).containsExactly(A_ROOM_ID, A_ROOM_ID_2).inOrder()
|
||||
assertThat(analyticsService.trackedErrors).hasSize(1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracking issue #4182.
|
||||
*
|
||||
* Targeted scenario: a `DateChanged` tick fires after an initial SDK emit, then a follow-up
|
||||
* SDK emit lands (mimicking "midnight, then a new message arrives"). Even though the diffCache
|
||||
* is bypassed during the rebuild (`useCache = false`), the final state must contain each
|
||||
* roomId exactly once and trackError must not fire on a happy path.
|
||||
*/
|
||||
@Test
|
||||
fun `interleaved date change and SDK update with overlapping content does not produce duplicates`() = runTest {
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
val summariesFlow = MutableStateFlow(
|
||||
listOf(
|
||||
aRoomSummary(roomId = A_ROOM_ID),
|
||||
aRoomSummary(roomId = A_ROOM_ID_2),
|
||||
)
|
||||
)
|
||||
val roomList = FakeDynamicRoomList(summaries = summariesFlow)
|
||||
val roomListService = FakeRoomListService(
|
||||
createRoomListLambda = { roomList }
|
||||
).apply {
|
||||
postState(RoomListService.State.Running)
|
||||
}
|
||||
val dateTimeObserver = FakeDateTimeObserver()
|
||||
val roomListDataSource = createRoomListDataSource(
|
||||
roomListService = roomListService,
|
||||
dateTimeObserver = dateTimeObserver,
|
||||
analyticsService = analyticsService,
|
||||
)
|
||||
|
||||
roomListDataSource.roomSummariesFlow.test {
|
||||
roomListDataSource.launchIn(backgroundScope)
|
||||
val initial = awaitItem()
|
||||
assertThat(initial.map { it.roomId }).containsExactly(A_ROOM_ID, A_ROOM_ID_2).inOrder()
|
||||
|
||||
// Midnight ticks while the cache holds [A_ROOM_ID, A_ROOM_ID_2]
|
||||
dateTimeObserver.given(DateTimeObserver.Event.DateChanged(Instant.MIN, Instant.now()))
|
||||
val afterMidnight = awaitItem()
|
||||
assertThat(afterMidnight.map { it.roomId }).containsExactly(A_ROOM_ID, A_ROOM_ID_2).inOrder()
|
||||
|
||||
// A new message bumps A_ROOM_ID — different unread count makes the StateFlow see this
|
||||
// as a new value
|
||||
summariesFlow.value = listOf(
|
||||
aRoomSummary(roomId = A_ROOM_ID, numUnreadMessages = 1),
|
||||
aRoomSummary(roomId = A_ROOM_ID_2),
|
||||
)
|
||||
val afterMessage = awaitItem()
|
||||
assertThat(afterMessage.map { it.roomId }).containsExactly(A_ROOM_ID, A_ROOM_ID_2).inOrder()
|
||||
assertThat(afterMessage.map { it.roomId }.toSet()).hasSize(afterMessage.size)
|
||||
|
||||
// Second midnight rebuild after the new message
|
||||
dateTimeObserver.given(DateTimeObserver.Event.DateChanged(Instant.MIN, Instant.now()))
|
||||
val afterSecondMidnight = awaitItem()
|
||||
assertThat(afterSecondMidnight.map { it.roomId }).containsExactly(A_ROOM_ID, A_ROOM_ID_2).inOrder()
|
||||
assertThat(afterSecondMidnight.map { it.roomId }.toSet()).hasSize(afterSecondMidnight.size)
|
||||
|
||||
assertThat(analyticsService.trackedErrors).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `regression test for race with DateTimeObserver and new items`() = runTest {
|
||||
val roomList = FakeDynamicRoomList(summaries = MutableStateFlow(listOf(aRoomSummary(), aRoomSummary(A_ROOM_ID_2))))
|
||||
val roomListService = FakeRoomListService(
|
||||
createRoomListLambda = { roomList }
|
||||
).apply {
|
||||
postState(RoomListService.State.Running)
|
||||
}
|
||||
val dateTimeObserver = FakeDateTimeObserver()
|
||||
var dateFormatterResult = "Today"
|
||||
val dateFormatter = FakeDateFormatter({ _, _, _ -> dateFormatterResult })
|
||||
val roomListDataSource = createRoomListDataSource(
|
||||
roomListService = roomListService,
|
||||
roomListRoomSummaryFactory = aRoomListRoomSummaryFactory(
|
||||
dateFormatter = dateFormatter,
|
||||
),
|
||||
dateTimeObserver = dateTimeObserver,
|
||||
)
|
||||
roomListDataSource.roomSummariesFlow.test {
|
||||
// Observe room list items changes
|
||||
val job = roomListDataSource.launchIn(backgroundScope)
|
||||
// Get the initial room list
|
||||
val initialRoomList = awaitItem()
|
||||
assertThat(initialRoomList).hasSize(2)
|
||||
assertThat(initialRoomList[0].roomId).isEqualTo(A_ROOM_ID)
|
||||
assertThat(initialRoomList[0].timestamp).isEqualTo(dateFormatterResult)
|
||||
assertThat(initialRoomList[1].roomId).isEqualTo(A_ROOM_ID_2)
|
||||
assertThat(initialRoomList[1].timestamp).isEqualTo(dateFormatterResult)
|
||||
|
||||
// Stop processing room list updates so we can force a race condition with the date time observer updates
|
||||
job.cancel()
|
||||
|
||||
// Trigger a date change and a new item at the same time
|
||||
dateFormatterResult = "Yesterday"
|
||||
roomList.summaries.tryEmit(listOf(aRoomSummary(roomId = A_ROOM_ID), aRoomSummary(roomId = A_ROOM_ID_3), aRoomSummary(roomId = A_ROOM_ID_2)))
|
||||
dateTimeObserver.given(DateTimeObserver.Event.DateChanged(Instant.MIN, Instant.now()))
|
||||
|
||||
// The race condition would have caused the cache indices to be corrupted and only 2 items would be emitted
|
||||
val rebuiltRoomList = awaitItem()
|
||||
assertThat(rebuiltRoomList).hasSize(3)
|
||||
assertThat(rebuiltRoomList[0].roomId).isEqualTo(A_ROOM_ID)
|
||||
assertThat(rebuiltRoomList[0].timestamp).isEqualTo(dateFormatterResult)
|
||||
assertThat(rebuiltRoomList[1].roomId).isEqualTo(A_ROOM_ID_3)
|
||||
assertThat(rebuiltRoomList[1].timestamp).isEqualTo(dateFormatterResult)
|
||||
assertThat(rebuiltRoomList[2].roomId).isEqualTo(A_ROOM_ID_2)
|
||||
assertThat(rebuiltRoomList[2].timestamp).isEqualTo(dateFormatterResult)
|
||||
|
||||
// Restart processing room list updates
|
||||
roomListDataSource.launchIn(backgroundScope)
|
||||
|
||||
// Check there is a new list and it's not the same as the previous one
|
||||
val newRoomList = awaitItem()
|
||||
assertThat(newRoomList).hasSize(3)
|
||||
assertThat(newRoomList[0].roomId).isEqualTo(A_ROOM_ID)
|
||||
assertThat(newRoomList[0].timestamp).isEqualTo(dateFormatterResult)
|
||||
assertThat(newRoomList[1].roomId).isEqualTo(A_ROOM_ID_3)
|
||||
assertThat(newRoomList[1].timestamp).isEqualTo(dateFormatterResult)
|
||||
assertThat(newRoomList[2].roomId).isEqualTo(A_ROOM_ID_2)
|
||||
assertThat(newRoomList[2].timestamp).isEqualTo(dateFormatterResult)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TestScope.createRoomListDataSource(
|
||||
roomListService: FakeRoomListService = FakeRoomListService(),
|
||||
roomListRoomSummaryFactory: RoomListRoomSummaryFactory = aRoomListRoomSummaryFactory(),
|
||||
notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(),
|
||||
dateTimeObserver: FakeDateTimeObserver = FakeDateTimeObserver(),
|
||||
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
|
||||
) = RoomListDataSource(
|
||||
roomListService = roomListService,
|
||||
roomListRoomSummaryFactory = roomListRoomSummaryFactory,
|
||||
@@ -112,6 +273,6 @@ class RoomListDataSourceTest {
|
||||
notificationSettingsService = notificationSettingsService,
|
||||
sessionCoroutineScope = backgroundScope,
|
||||
dateTimeObserver = dateTimeObserver,
|
||||
analyticsService = FakeAnalyticsService(),
|
||||
analyticsService = analyticsService,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user