Skip to content

Commit aaa9b72

Browse files
jmartinesprenovate[bot]ganfra
authored
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 <[email protected]> Co-authored-by: Jorge Martín <[email protected]>
2 parents 1b690c1 + 5ce5b17 commit aaa9b72

File tree

10 files changed

+111
-51
lines changed

10 files changed

+111
-51
lines changed

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,10 +159,10 @@ class MessagesPresenter @AssistedInject constructor(
159159
}
160160

161161
LaunchedEffect(Unit) {
162-
// Mark the room as read on entering but don't send read receipts
162+
// Remove the unread flag on entering but don't send read receipts
163163
// as those will be handled by the timeline.
164164
withContext(dispatchers.io) {
165-
room.markAsRead(null)
165+
room.setUnreadFlag(isUnread = false)
166166
}
167167
}
168168

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import androidx.compose.runtime.MutableState
2222
import androidx.compose.runtime.collectAsState
2323
import androidx.compose.runtime.derivedStateOf
2424
import androidx.compose.runtime.getValue
25-
import androidx.compose.runtime.mutableIntStateOf
2625
import androidx.compose.runtime.mutableStateOf
2726
import androidx.compose.runtime.remember
2827
import androidx.compose.runtime.rememberCoroutineScope
@@ -90,7 +89,6 @@ class TimelinePresenter @AssistedInject constructor(
9089
mutableStateOf(null)
9190
}
9291

93-
val lastReadReceiptIndex = rememberSaveable { mutableIntStateOf(Int.MAX_VALUE) }
9492
val lastReadReceiptId = rememberSaveable { mutableStateOf<EventId?>(null) }
9593

9694
val timelineItems by timelineItemsFactory.collectItemsAsState()
@@ -128,7 +126,6 @@ class TimelinePresenter @AssistedInject constructor(
128126
appScope.sendReadReceiptIfNeeded(
129127
firstVisibleIndex = event.firstIndex,
130128
timelineItems = timelineItems,
131-
lastReadReceiptIndex = lastReadReceiptIndex,
132129
lastReadReceiptId = lastReadReceiptId,
133130
readReceiptType = if (isSendPublicReadReceiptsEnabled) ReceiptType.READ else ReceiptType.READ_PRIVATE,
134131
)
@@ -228,16 +225,19 @@ class TimelinePresenter @AssistedInject constructor(
228225
private fun CoroutineScope.sendReadReceiptIfNeeded(
229226
firstVisibleIndex: Int,
230227
timelineItems: ImmutableList<TimelineItem>,
231-
lastReadReceiptIndex: MutableState<Int>,
232228
lastReadReceiptId: MutableState<EventId?>,
233229
readReceiptType: ReceiptType,
234230
) = launch(dispatchers.computation) {
235-
// Get last valid EventId seen by the user, as the first index might refer to a Virtual item
236-
val eventId = getLastEventIdBeforeOrAt(firstVisibleIndex, timelineItems)
237-
if (eventId != null && firstVisibleIndex <= lastReadReceiptIndex.value && eventId != lastReadReceiptId.value) {
238-
lastReadReceiptIndex.value = firstVisibleIndex
239-
lastReadReceiptId.value = eventId
240-
timeline.sendReadReceipt(eventId = eventId, receiptType = readReceiptType)
231+
// If we are at the bottom of timeline, we mark the room as read.
232+
if (firstVisibleIndex == 0) {
233+
room.markAsRead(receiptType = readReceiptType)
234+
} else {
235+
// Get last valid EventId seen by the user, as the first index might refer to a Virtual item
236+
val eventId = getLastEventIdBeforeOrAt(firstVisibleIndex, timelineItems)
237+
if (eventId != null && eventId != lastReadReceiptId.value) {
238+
lastReadReceiptId.value = eventId
239+
timeline.sendReadReceipt(eventId = eventId, receiptType = readReceiptType)
240+
}
241241
}
242242
}
243243

features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,15 +134,15 @@ class MessagesPresenterTest {
134134

135135
@OptIn(ExperimentalCoroutinesApi::class)
136136
@Test
137-
fun `present - check that the room is marked as read`() = runTest {
137+
fun `present - check that the room's unread flag is removed`() = runTest {
138138
val room = FakeMatrixRoom()
139139
assertThat(room.markAsReadCalls).isEmpty()
140140
val presenter = createMessagesPresenter(matrixRoom = room)
141141
moleculeFlow(RecompositionMode.Immediate) {
142142
presenter.present()
143143
}.test {
144144
runCurrent()
145-
assertThat(room.markAsReadCalls).isEqualTo(listOf(null))
145+
assertThat(room.setUnreadFlagCalls).isEqualTo(listOf(false))
146146
cancelAndIgnoreRemainingEvents()
147147
}
148148
}

features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSende
4545
import io.element.android.libraries.matrix.api.timeline.item.event.Receipt
4646
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
4747
import io.element.android.libraries.matrix.test.AN_EVENT_ID
48+
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
4849
import io.element.android.libraries.matrix.test.A_USER_ID
4950
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
5051
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
@@ -60,15 +61,19 @@ import io.element.android.tests.testutils.awaitWithLatch
6061
import io.element.android.tests.testutils.consumeItemsUntilPredicate
6162
import io.element.android.tests.testutils.testCoroutineDispatchers
6263
import kotlinx.collections.immutable.persistentListOf
64+
import kotlinx.coroutines.ExperimentalCoroutinesApi
6365
import kotlinx.coroutines.delay
66+
import kotlinx.coroutines.test.StandardTestDispatcher
6467
import kotlinx.coroutines.test.TestScope
68+
import kotlinx.coroutines.test.runCurrent
6569
import kotlinx.coroutines.test.runTest
6670
import org.junit.Rule
6771
import org.junit.Test
6872
import java.util.Date
6973
import kotlin.time.Duration.Companion.seconds
7074

7175
private const val FAKE_UNIQUE_ID = "FAKE_UNIQUE_ID"
76+
private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
7277

7378
class TimelinePresenterTest {
7479
@get:Rule
@@ -125,13 +130,47 @@ class TimelinePresenterTest {
125130
}
126131
}
127132

133+
@OptIn(ExperimentalCoroutinesApi::class)
128134
@Test
129-
fun `present - on scroll finished send read receipt if an event is before the index`() = runTest {
135+
fun `present - on scroll finished mark a room as read if the first visible index is 0`() = runTest(StandardTestDispatcher()) {
130136
val timeline = FakeMatrixTimeline(
131137
initialTimelineItems = listOf(
132138
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem())
133139
)
134140
)
141+
val sessionPreferencesStore = InMemorySessionPreferencesStore(isSendPublicReadReceiptsEnabled = false)
142+
val room = FakeMatrixRoom(matrixTimeline = timeline)
143+
val presenter = createTimelinePresenter(
144+
timeline = timeline,
145+
room = room,
146+
sessionPreferencesStore = sessionPreferencesStore,
147+
)
148+
moleculeFlow(RecompositionMode.Immediate) {
149+
presenter.present()
150+
}.test {
151+
assertThat(timeline.sentReadReceipts).isEmpty()
152+
val initialState = awaitFirstItem()
153+
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0))
154+
runCurrent()
155+
assertThat(room.markAsReadCalls).isNotEmpty()
156+
cancelAndIgnoreRemainingEvents()
157+
}
158+
}
159+
160+
@Test
161+
fun `present - on scroll finished send read receipt if an event is before the index`() = runTest {
162+
val timeline = FakeMatrixTimeline(
163+
initialTimelineItems = listOf(
164+
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()),
165+
MatrixTimelineItem.Event(
166+
uniqueId = FAKE_UNIQUE_ID_2,
167+
event = anEventTimelineItem(
168+
eventId = AN_EVENT_ID_2,
169+
content = aMessageContent("Test message")
170+
)
171+
)
172+
)
173+
)
135174
val presenter = createTimelinePresenter(timeline)
136175
moleculeFlow(RecompositionMode.Immediate) {
137176
presenter.present()
@@ -140,7 +179,7 @@ class TimelinePresenterTest {
140179
val initialState = awaitFirstItem()
141180
awaitWithLatch { latch ->
142181
timeline.sendReadReceiptLatch = latch
143-
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0))
182+
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1))
144183
}
145184
assertThat(timeline.sentReadReceipts).isNotEmpty()
146185
assertThat(timeline.sentReadReceipts.first().second).isEqualTo(ReceiptType.READ)
@@ -149,10 +188,17 @@ class TimelinePresenterTest {
149188
}
150189

151190
@Test
152-
fun `present - on scroll finished send a private read receipt if an event is before the index and public read receipts are disabled`() = runTest {
191+
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 {
153192
val timeline = FakeMatrixTimeline(
154193
initialTimelineItems = listOf(
155-
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem())
194+
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()),
195+
MatrixTimelineItem.Event(
196+
uniqueId = FAKE_UNIQUE_ID_2,
197+
event = anEventTimelineItem(
198+
eventId = AN_EVENT_ID_2,
199+
content = aMessageContent("Test message")
200+
)
201+
)
156202
)
157203
)
158204
val sessionPreferencesStore = InMemorySessionPreferencesStore(isSendPublicReadReceiptsEnabled = false)
@@ -168,6 +214,7 @@ class TimelinePresenterTest {
168214
awaitWithLatch { latch ->
169215
timeline.sendReadReceiptLatch = latch
170216
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0))
217+
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1))
171218
}
172219
assertThat(timeline.sentReadReceipts).isNotEmpty()
173220
assertThat(timeline.sentReadReceipts.first().second).isEqualTo(ReceiptType.READ_PRIVATE)
@@ -176,10 +223,17 @@ class TimelinePresenterTest {
176223
}
177224

178225
@Test
179-
fun `present - on scroll finished will not send read receipt if no event is before the index`() = runTest {
226+
fun `present - on scroll finished will not send read receipt the first visible event is the same as before`() = runTest {
180227
val timeline = FakeMatrixTimeline(
181228
initialTimelineItems = listOf(
182-
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem())
229+
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()),
230+
MatrixTimelineItem.Event(
231+
uniqueId = FAKE_UNIQUE_ID_2,
232+
event = anEventTimelineItem(
233+
eventId = AN_EVENT_ID_2,
234+
content = aMessageContent("Test message")
235+
)
236+
)
183237
)
184238
)
185239
val presenter = createTimelinePresenter(timeline)
@@ -191,8 +245,9 @@ class TimelinePresenterTest {
191245
awaitWithLatch { latch ->
192246
timeline.sendReadReceiptLatch = latch
193247
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1))
248+
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1))
194249
}
195-
assertThat(timeline.sentReadReceipts).isEmpty()
250+
assertThat(timeline.sentReadReceipts).hasSize(1)
196251
cancelAndIgnoreRemainingEvents()
197252
}
198253
}
@@ -201,6 +256,7 @@ class TimelinePresenterTest {
201256
fun `present - on scroll finished will not send read receipt only virtual events exist before the index`() = runTest {
202257
val timeline = FakeMatrixTimeline(
203258
initialTimelineItems = listOf(
259+
MatrixTimelineItem.Virtual(FAKE_UNIQUE_ID, VirtualTimelineItem.ReadMarker),
204260
MatrixTimelineItem.Virtual(FAKE_UNIQUE_ID, VirtualTimelineItem.ReadMarker)
205261
)
206262
)
@@ -212,7 +268,7 @@ class TimelinePresenterTest {
212268
val initialState = awaitFirstItem()
213269
awaitWithLatch { latch ->
214270
timeline.sendReadReceiptLatch = latch
215-
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0))
271+
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1))
216272
}
217273
assertThat(timeline.sentReadReceipts).isEmpty()
218274
cancelAndIgnoreRemainingEvents()

features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -145,15 +145,20 @@ class RoomListPresenter @Inject constructor(
145145
is RoomListEvents.HideContextMenu -> contextMenu = RoomListState.ContextMenu.Hidden
146146
is RoomListEvents.LeaveRoom -> leaveRoomState.eventSink(LeaveRoomEvent.ShowConfirmation(event.roomId))
147147
is RoomListEvents.MarkAsRead -> coroutineScope.launch {
148-
val receiptType = if (sessionPreferencesStore.isSendPublicReadReceiptsEnabled().first()) {
149-
ReceiptType.READ
150-
} else {
151-
ReceiptType.READ_PRIVATE
148+
client.getRoom(event.roomId)?.use { room ->
149+
room.setUnreadFlag(isUnread = false)
150+
val receiptType = if (sessionPreferencesStore.isSendPublicReadReceiptsEnabled().first()) {
151+
ReceiptType.READ
152+
} else {
153+
ReceiptType.READ_PRIVATE
154+
}
155+
room.markAsRead(receiptType)
152156
}
153-
client.getRoom(event.roomId)?.markAsRead(receiptType)
154157
}
155158
is RoomListEvents.MarkAsUnread -> coroutineScope.launch {
156-
client.getRoom(event.roomId)?.markAsUnread()
159+
client.getRoom(event.roomId)?.use { room ->
160+
room.setUnreadFlag(isUnread = true)
161+
}
157162
}
158163
}
159164
}

features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -487,18 +487,18 @@ class RoomListPresenterTests {
487487
}.test {
488488
val initialState = awaitItem()
489489
assertThat(room.markAsReadCalls).isEmpty()
490-
assertThat(room.markAsUnreadReadCallCount).isEqualTo(0)
490+
assertThat(room.setUnreadFlagCalls).isEmpty()
491491
initialState.eventSink.invoke(RoomListEvents.MarkAsRead(A_ROOM_ID))
492492
assertThat(room.markAsReadCalls).isEqualTo(listOf(ReceiptType.READ))
493-
assertThat(room.markAsUnreadReadCallCount).isEqualTo(0)
493+
assertThat(room.setUnreadFlagCalls).isEqualTo(listOf(false))
494494
initialState.eventSink.invoke(RoomListEvents.MarkAsUnread(A_ROOM_ID))
495495
assertThat(room.markAsReadCalls).isEqualTo(listOf(ReceiptType.READ))
496-
assertThat(room.markAsUnreadReadCallCount).isEqualTo(1)
496+
assertThat(room.setUnreadFlagCalls).isEqualTo(listOf(false, true))
497497
// Test again with private read receipts
498498
sessionPreferencesStore.setSendPublicReadReceipts(false)
499499
initialState.eventSink.invoke(RoomListEvents.MarkAsRead(A_ROOM_ID))
500500
assertThat(room.markAsReadCalls).isEqualTo(listOf(ReceiptType.READ, ReceiptType.READ_PRIVATE))
501-
assertThat(room.markAsUnreadReadCallCount).isEqualTo(1)
501+
assertThat(room.setUnreadFlagCalls).isEqualTo(listOf(false, true, false))
502502
cancelAndIgnoreRemainingEvents()
503503
scope.cancel()
504504
}

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ jsoup = "org.jsoup:jsoup:1.17.2"
152152
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
153153
molecule-runtime = "app.cash.molecule:molecule-runtime:1.3.2"
154154
timber = "com.jakewharton.timber:timber:5.0.1"
155-
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.0"
155+
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.1"
156156
matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
157157
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" }
158158
sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }

libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -153,15 +153,17 @@ interface MatrixRoom : Closeable {
153153
suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result<Unit>
154154

155155
/**
156-
* Reverts a previously set unread flag, and eventually send a Read Receipt.
157-
* @param receiptType The type of receipt to send. If null, no Read Receipt will be sent.
156+
* Mark the room as read by trying to attach an unthreaded read receipt to the latest room event.
157+
* @param receiptType The type of receipt to send.
158158
*/
159-
suspend fun markAsRead(receiptType: ReceiptType?): Result<Unit>
159+
suspend fun markAsRead(receiptType: ReceiptType): Result<Unit>
160160

161161
/**
162-
* Sets a flag on the room to indicate that the user has explicitly marked it as unread.
162+
* Sets a flag on the room to indicate that the user has explicitly marked it as unread, or reverts the flag.
163+
* @param isUnread true to mark the room as unread, false to remove the flag.
164+
*
163165
*/
164-
suspend fun markAsUnread(): Result<Unit>
166+
suspend fun setUnreadFlag(isUnread: Boolean): Result<Unit>
165167

166168
/**
167169
* Share a location message in the room.

libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -442,19 +442,15 @@ class RustMatrixRoom(
442442
}
443443
}
444444

445-
override suspend fun markAsRead(receiptType: ReceiptType?): Result<Unit> = withContext(roomDispatcher) {
445+
override suspend fun markAsRead(receiptType: ReceiptType): Result<Unit> = withContext(roomDispatcher) {
446446
runCatching {
447-
if (receiptType != null) {
448-
innerRoom.markAsReadAndSendReadReceipt(receiptType.toRustReceiptType())
449-
} else {
450-
innerRoom.markAsRead()
451-
}
447+
innerRoom.markAsRead(receiptType.toRustReceiptType())
452448
}
453449
}
454450

455-
override suspend fun markAsUnread(): Result<Unit> = withContext(roomDispatcher) {
451+
override suspend fun setUnreadFlag(isUnread: Boolean): Result<Unit> = withContext(roomDispatcher) {
456452
runCatching {
457-
innerRoom.markAsUnread()
453+
innerRoom.setUnreadFlag(isUnread)
458454
}
459455
}
460456

libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -378,17 +378,18 @@ class FakeMatrixRoom(
378378
return reportContentResult
379379
}
380380

381-
val markAsReadCalls = mutableListOf<ReceiptType?>()
382-
override suspend fun markAsRead(receiptType: ReceiptType?): Result<Unit> {
381+
val markAsReadCalls = mutableListOf<ReceiptType>()
382+
383+
override suspend fun markAsRead(receiptType: ReceiptType): Result<Unit> {
383384
markAsReadCalls.add(receiptType)
384385
return Result.success(Unit)
385386
}
386387

387-
var markAsUnreadReadCallCount = 0
388+
var setUnreadFlagCalls = mutableListOf<Boolean>()
388389
private set
389390

390-
override suspend fun markAsUnread(): Result<Unit> {
391-
markAsUnreadReadCallCount++
391+
override suspend fun setUnreadFlag(isUnread: Boolean): Result<Unit> {
392+
setUnreadFlagCalls.add(isUnread)
392393
return Result.success(Unit)
393394
}
394395

0 commit comments

Comments
 (0)