Merge pull request #2389 from element-hq/renovate/org.matrix.rustcomponents-sdk-android-0.x

* Update dependency org.matrix.rustcomponents:sdk-android to v0.2.1

* read : use the new apis

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: ganfra <francoisg@matrix.org>
Co-authored-by: Jorge Martín <jorgem@element.io>
This commit is contained in:
Jorge Martin Espinosa 2024-02-14 13:38:09 +01:00 committed by GitHub
commit cbad781090
10 changed files with 111 additions and 51 deletions

View file

@ -159,10 +159,10 @@ class MessagesPresenter @AssistedInject constructor(
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
// Mark the room as read on entering but don't send read receipts // Remove the unread flag on entering but don't send read receipts
// as those will be handled by the timeline. // as those will be handled by the timeline.
withContext(dispatchers.io) { withContext(dispatchers.io) {
room.markAsRead(null) room.setUnreadFlag(isUnread = false)
} }
} }

View file

@ -22,7 +22,6 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
@ -90,7 +89,6 @@ class TimelinePresenter @AssistedInject constructor(
mutableStateOf(null) mutableStateOf(null)
} }
val lastReadReceiptIndex = rememberSaveable { mutableIntStateOf(Int.MAX_VALUE) }
val lastReadReceiptId = rememberSaveable { mutableStateOf<EventId?>(null) } val lastReadReceiptId = rememberSaveable { mutableStateOf<EventId?>(null) }
val timelineItems by timelineItemsFactory.collectItemsAsState() val timelineItems by timelineItemsFactory.collectItemsAsState()
@ -128,7 +126,6 @@ class TimelinePresenter @AssistedInject constructor(
appScope.sendReadReceiptIfNeeded( appScope.sendReadReceiptIfNeeded(
firstVisibleIndex = event.firstIndex, firstVisibleIndex = event.firstIndex,
timelineItems = timelineItems, timelineItems = timelineItems,
lastReadReceiptIndex = lastReadReceiptIndex,
lastReadReceiptId = lastReadReceiptId, lastReadReceiptId = lastReadReceiptId,
readReceiptType = if (isSendPublicReadReceiptsEnabled) ReceiptType.READ else ReceiptType.READ_PRIVATE, readReceiptType = if (isSendPublicReadReceiptsEnabled) ReceiptType.READ else ReceiptType.READ_PRIVATE,
) )
@ -228,16 +225,19 @@ class TimelinePresenter @AssistedInject constructor(
private fun CoroutineScope.sendReadReceiptIfNeeded( private fun CoroutineScope.sendReadReceiptIfNeeded(
firstVisibleIndex: Int, firstVisibleIndex: Int,
timelineItems: ImmutableList<TimelineItem>, timelineItems: ImmutableList<TimelineItem>,
lastReadReceiptIndex: MutableState<Int>,
lastReadReceiptId: MutableState<EventId?>, lastReadReceiptId: MutableState<EventId?>,
readReceiptType: ReceiptType, readReceiptType: ReceiptType,
) = launch(dispatchers.computation) { ) = launch(dispatchers.computation) {
// Get last valid EventId seen by the user, as the first index might refer to a Virtual item // If we are at the bottom of timeline, we mark the room as read.
val eventId = getLastEventIdBeforeOrAt(firstVisibleIndex, timelineItems) if (firstVisibleIndex == 0) {
if (eventId != null && firstVisibleIndex <= lastReadReceiptIndex.value && eventId != lastReadReceiptId.value) { room.markAsRead(receiptType = readReceiptType)
lastReadReceiptIndex.value = firstVisibleIndex } else {
lastReadReceiptId.value = eventId // Get last valid EventId seen by the user, as the first index might refer to a Virtual item
timeline.sendReadReceipt(eventId = eventId, receiptType = readReceiptType) val eventId = getLastEventIdBeforeOrAt(firstVisibleIndex, timelineItems)
if (eventId != null && eventId != lastReadReceiptId.value) {
lastReadReceiptId.value = eventId
timeline.sendReadReceipt(eventId = eventId, receiptType = readReceiptType)
}
} }
} }

View file

@ -134,7 +134,7 @@ class MessagesPresenterTest {
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@Test @Test
fun `present - check that the room is marked as read`() = runTest { fun `present - check that the room's unread flag is removed`() = runTest {
val room = FakeMatrixRoom() val room = FakeMatrixRoom()
assertThat(room.markAsReadCalls).isEmpty() assertThat(room.markAsReadCalls).isEmpty()
val presenter = createMessagesPresenter(matrixRoom = room) val presenter = createMessagesPresenter(matrixRoom = room)
@ -142,7 +142,7 @@ class MessagesPresenterTest {
presenter.present() presenter.present()
}.test { }.test {
runCurrent() runCurrent()
assertThat(room.markAsReadCalls).isEqualTo(listOf(null)) assertThat(room.setUnreadFlagCalls).isEqualTo(listOf(false))
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
} }
} }

View file

@ -45,6 +45,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSende
import io.element.android.libraries.matrix.api.timeline.item.event.Receipt import io.element.android.libraries.matrix.api.timeline.item.event.Receipt
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
@ -60,8 +61,11 @@ import io.element.android.tests.testutils.awaitWithLatch
import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.consumeItemsUntilPredicate
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -69,6 +73,7 @@ import java.util.Date
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
private const val FAKE_UNIQUE_ID = "FAKE_UNIQUE_ID" private const val FAKE_UNIQUE_ID = "FAKE_UNIQUE_ID"
private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
class TimelinePresenterTest { class TimelinePresenterTest {
@get:Rule @get:Rule
@ -125,11 +130,45 @@ class TimelinePresenterTest {
} }
} }
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `present - on scroll finished mark a room as read if the first visible index is 0`() = runTest(StandardTestDispatcher()) {
val timeline = FakeMatrixTimeline(
initialTimelineItems = listOf(
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem())
)
)
val sessionPreferencesStore = InMemorySessionPreferencesStore(isSendPublicReadReceiptsEnabled = false)
val room = FakeMatrixRoom(matrixTimeline = timeline)
val presenter = createTimelinePresenter(
timeline = timeline,
room = room,
sessionPreferencesStore = sessionPreferencesStore,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
assertThat(timeline.sentReadReceipts).isEmpty()
val initialState = awaitFirstItem()
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0))
runCurrent()
assertThat(room.markAsReadCalls).isNotEmpty()
cancelAndIgnoreRemainingEvents()
}
}
@Test @Test
fun `present - on scroll finished send read receipt if an event is before the index`() = runTest { fun `present - on scroll finished send read receipt if an event is before the index`() = runTest {
val timeline = FakeMatrixTimeline( val timeline = FakeMatrixTimeline(
initialTimelineItems = listOf( initialTimelineItems = listOf(
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()) MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()),
MatrixTimelineItem.Event(
uniqueId = FAKE_UNIQUE_ID_2,
event = anEventTimelineItem(
eventId = AN_EVENT_ID_2,
content = aMessageContent("Test message")
)
)
) )
) )
val presenter = createTimelinePresenter(timeline) val presenter = createTimelinePresenter(timeline)
@ -140,7 +179,7 @@ class TimelinePresenterTest {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
awaitWithLatch { latch -> awaitWithLatch { latch ->
timeline.sendReadReceiptLatch = latch timeline.sendReadReceiptLatch = latch
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0)) initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1))
} }
assertThat(timeline.sentReadReceipts).isNotEmpty() assertThat(timeline.sentReadReceipts).isNotEmpty()
assertThat(timeline.sentReadReceipts.first().second).isEqualTo(ReceiptType.READ) assertThat(timeline.sentReadReceipts.first().second).isEqualTo(ReceiptType.READ)
@ -149,10 +188,17 @@ class TimelinePresenterTest {
} }
@Test @Test
fun `present - on scroll finished send a private read receipt if an event is before the index and public read receipts are disabled`() = runTest { fun `present - on scroll finished send a private read receipt if an event is at an index other than 0 and public read receipts are disabled`() = runTest {
val timeline = FakeMatrixTimeline( val timeline = FakeMatrixTimeline(
initialTimelineItems = listOf( initialTimelineItems = listOf(
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()) MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()),
MatrixTimelineItem.Event(
uniqueId = FAKE_UNIQUE_ID_2,
event = anEventTimelineItem(
eventId = AN_EVENT_ID_2,
content = aMessageContent("Test message")
)
)
) )
) )
val sessionPreferencesStore = InMemorySessionPreferencesStore(isSendPublicReadReceiptsEnabled = false) val sessionPreferencesStore = InMemorySessionPreferencesStore(isSendPublicReadReceiptsEnabled = false)
@ -168,6 +214,7 @@ class TimelinePresenterTest {
awaitWithLatch { latch -> awaitWithLatch { latch ->
timeline.sendReadReceiptLatch = latch timeline.sendReadReceiptLatch = latch
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0)) initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0))
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1))
} }
assertThat(timeline.sentReadReceipts).isNotEmpty() assertThat(timeline.sentReadReceipts).isNotEmpty()
assertThat(timeline.sentReadReceipts.first().second).isEqualTo(ReceiptType.READ_PRIVATE) assertThat(timeline.sentReadReceipts.first().second).isEqualTo(ReceiptType.READ_PRIVATE)
@ -176,10 +223,17 @@ class TimelinePresenterTest {
} }
@Test @Test
fun `present - on scroll finished will not send read receipt if no event is before the index`() = runTest { fun `present - on scroll finished will not send read receipt the first visible event is the same as before`() = runTest {
val timeline = FakeMatrixTimeline( val timeline = FakeMatrixTimeline(
initialTimelineItems = listOf( initialTimelineItems = listOf(
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()) MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()),
MatrixTimelineItem.Event(
uniqueId = FAKE_UNIQUE_ID_2,
event = anEventTimelineItem(
eventId = AN_EVENT_ID_2,
content = aMessageContent("Test message")
)
)
) )
) )
val presenter = createTimelinePresenter(timeline) val presenter = createTimelinePresenter(timeline)
@ -191,8 +245,9 @@ class TimelinePresenterTest {
awaitWithLatch { latch -> awaitWithLatch { latch ->
timeline.sendReadReceiptLatch = latch timeline.sendReadReceiptLatch = latch
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1)) initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1))
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1))
} }
assertThat(timeline.sentReadReceipts).isEmpty() assertThat(timeline.sentReadReceipts).hasSize(1)
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
} }
} }
@ -201,6 +256,7 @@ class TimelinePresenterTest {
fun `present - on scroll finished will not send read receipt only virtual events exist before the index`() = runTest { fun `present - on scroll finished will not send read receipt only virtual events exist before the index`() = runTest {
val timeline = FakeMatrixTimeline( val timeline = FakeMatrixTimeline(
initialTimelineItems = listOf( initialTimelineItems = listOf(
MatrixTimelineItem.Virtual(FAKE_UNIQUE_ID, VirtualTimelineItem.ReadMarker),
MatrixTimelineItem.Virtual(FAKE_UNIQUE_ID, VirtualTimelineItem.ReadMarker) MatrixTimelineItem.Virtual(FAKE_UNIQUE_ID, VirtualTimelineItem.ReadMarker)
) )
) )
@ -212,7 +268,7 @@ class TimelinePresenterTest {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
awaitWithLatch { latch -> awaitWithLatch { latch ->
timeline.sendReadReceiptLatch = latch timeline.sendReadReceiptLatch = latch
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0)) initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1))
} }
assertThat(timeline.sentReadReceipts).isEmpty() assertThat(timeline.sentReadReceipts).isEmpty()
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()

View file

@ -145,15 +145,20 @@ class RoomListPresenter @Inject constructor(
is RoomListEvents.HideContextMenu -> contextMenu = RoomListState.ContextMenu.Hidden is RoomListEvents.HideContextMenu -> contextMenu = RoomListState.ContextMenu.Hidden
is RoomListEvents.LeaveRoom -> leaveRoomState.eventSink(LeaveRoomEvent.ShowConfirmation(event.roomId)) is RoomListEvents.LeaveRoom -> leaveRoomState.eventSink(LeaveRoomEvent.ShowConfirmation(event.roomId))
is RoomListEvents.MarkAsRead -> coroutineScope.launch { is RoomListEvents.MarkAsRead -> coroutineScope.launch {
val receiptType = if (sessionPreferencesStore.isSendPublicReadReceiptsEnabled().first()) { client.getRoom(event.roomId)?.use { room ->
ReceiptType.READ room.setUnreadFlag(isUnread = false)
} else { val receiptType = if (sessionPreferencesStore.isSendPublicReadReceiptsEnabled().first()) {
ReceiptType.READ_PRIVATE ReceiptType.READ
} else {
ReceiptType.READ_PRIVATE
}
room.markAsRead(receiptType)
} }
client.getRoom(event.roomId)?.markAsRead(receiptType)
} }
is RoomListEvents.MarkAsUnread -> coroutineScope.launch { is RoomListEvents.MarkAsUnread -> coroutineScope.launch {
client.getRoom(event.roomId)?.markAsUnread() client.getRoom(event.roomId)?.use { room ->
room.setUnreadFlag(isUnread = true)
}
} }
} }
} }

View file

@ -487,18 +487,18 @@ class RoomListPresenterTests {
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(room.markAsReadCalls).isEmpty() assertThat(room.markAsReadCalls).isEmpty()
assertThat(room.markAsUnreadReadCallCount).isEqualTo(0) assertThat(room.setUnreadFlagCalls).isEmpty()
initialState.eventSink.invoke(RoomListEvents.MarkAsRead(A_ROOM_ID)) initialState.eventSink.invoke(RoomListEvents.MarkAsRead(A_ROOM_ID))
assertThat(room.markAsReadCalls).isEqualTo(listOf(ReceiptType.READ)) assertThat(room.markAsReadCalls).isEqualTo(listOf(ReceiptType.READ))
assertThat(room.markAsUnreadReadCallCount).isEqualTo(0) assertThat(room.setUnreadFlagCalls).isEqualTo(listOf(false))
initialState.eventSink.invoke(RoomListEvents.MarkAsUnread(A_ROOM_ID)) initialState.eventSink.invoke(RoomListEvents.MarkAsUnread(A_ROOM_ID))
assertThat(room.markAsReadCalls).isEqualTo(listOf(ReceiptType.READ)) assertThat(room.markAsReadCalls).isEqualTo(listOf(ReceiptType.READ))
assertThat(room.markAsUnreadReadCallCount).isEqualTo(1) assertThat(room.setUnreadFlagCalls).isEqualTo(listOf(false, true))
// Test again with private read receipts // Test again with private read receipts
sessionPreferencesStore.setSendPublicReadReceipts(false) sessionPreferencesStore.setSendPublicReadReceipts(false)
initialState.eventSink.invoke(RoomListEvents.MarkAsRead(A_ROOM_ID)) initialState.eventSink.invoke(RoomListEvents.MarkAsRead(A_ROOM_ID))
assertThat(room.markAsReadCalls).isEqualTo(listOf(ReceiptType.READ, ReceiptType.READ_PRIVATE)) assertThat(room.markAsReadCalls).isEqualTo(listOf(ReceiptType.READ, ReceiptType.READ_PRIVATE))
assertThat(room.markAsUnreadReadCallCount).isEqualTo(1) assertThat(room.setUnreadFlagCalls).isEqualTo(listOf(false, true, false))
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
scope.cancel() scope.cancel()
} }

View file

@ -152,7 +152,7 @@ jsoup = "org.jsoup:jsoup:1.17.2"
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
molecule-runtime = "app.cash.molecule:molecule-runtime:1.3.2" molecule-runtime = "app.cash.molecule:molecule-runtime:1.3.2"
timber = "com.jakewharton.timber:timber:5.0.1" timber = "com.jakewharton.timber:timber:5.0.1"
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.0" matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.1"
matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" } matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" } matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" }
sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" } sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }

View file

@ -153,15 +153,17 @@ interface MatrixRoom : Closeable {
suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result<Unit> suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result<Unit>
/** /**
* Reverts a previously set unread flag, and eventually send a Read Receipt. * Mark the room as read by trying to attach an unthreaded read receipt to the latest room event.
* @param receiptType The type of receipt to send. If null, no Read Receipt will be sent. * @param receiptType The type of receipt to send.
*/ */
suspend fun markAsRead(receiptType: ReceiptType?): Result<Unit> suspend fun markAsRead(receiptType: ReceiptType): Result<Unit>
/** /**
* Sets a flag on the room to indicate that the user has explicitly marked it as unread. * Sets a flag on the room to indicate that the user has explicitly marked it as unread, or reverts the flag.
* @param isUnread true to mark the room as unread, false to remove the flag.
*
*/ */
suspend fun markAsUnread(): Result<Unit> suspend fun setUnreadFlag(isUnread: Boolean): Result<Unit>
/** /**
* Share a location message in the room. * Share a location message in the room.

View file

@ -442,19 +442,15 @@ class RustMatrixRoom(
} }
} }
override suspend fun markAsRead(receiptType: ReceiptType?): Result<Unit> = withContext(roomDispatcher) { override suspend fun markAsRead(receiptType: ReceiptType): Result<Unit> = withContext(roomDispatcher) {
runCatching { runCatching {
if (receiptType != null) { innerRoom.markAsRead(receiptType.toRustReceiptType())
innerRoom.markAsReadAndSendReadReceipt(receiptType.toRustReceiptType())
} else {
innerRoom.markAsRead()
}
} }
} }
override suspend fun markAsUnread(): Result<Unit> = withContext(roomDispatcher) { override suspend fun setUnreadFlag(isUnread: Boolean): Result<Unit> = withContext(roomDispatcher) {
runCatching { runCatching {
innerRoom.markAsUnread() innerRoom.setUnreadFlag(isUnread)
} }
} }

View file

@ -378,17 +378,18 @@ class FakeMatrixRoom(
return reportContentResult return reportContentResult
} }
val markAsReadCalls = mutableListOf<ReceiptType?>() val markAsReadCalls = mutableListOf<ReceiptType>()
override suspend fun markAsRead(receiptType: ReceiptType?): Result<Unit> {
override suspend fun markAsRead(receiptType: ReceiptType): Result<Unit> {
markAsReadCalls.add(receiptType) markAsReadCalls.add(receiptType)
return Result.success(Unit) return Result.success(Unit)
} }
var markAsUnreadReadCallCount = 0 var setUnreadFlagCalls = mutableListOf<Boolean>()
private set private set
override suspend fun markAsUnread(): Result<Unit> { override suspend fun setUnreadFlag(isUnread: Boolean): Result<Unit> {
markAsUnreadReadCallCount++ setUnreadFlagCalls.add(isUnread)
return Result.success(Unit) return Result.success(Unit)
} }