Browse Source

Merge pull request #2354 from element-hq/feature/bma/markUnread

Mark room as unread
pull/2373/head
Benoit Marty 7 months ago committed by GitHub
parent
commit
d5c123622b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      changelog.d/2261.feature
  2. 8
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt
  3. 17
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt
  4. 4
      features/roomlist/impl/build.gradle.kts
  5. 82
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenu.kt
  6. 8
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt
  7. 22
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt
  8. 2
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt
  9. 20
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt
  10. 2
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactory.kt
  11. 7
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummary.kt
  12. 2
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryProvider.kt
  13. 131
      features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenuTest.kt
  14. 124
      features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt
  15. 98
      features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryTest.kt
  16. 2
      libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeLastMessageTimestampFormatter.kt
  17. 7
      libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt
  18. 1
      libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt
  19. 12
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt
  20. 1
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomSummary.kt
  21. 18
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt
  22. 1
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryDetailsFactory.kt
  23. 15
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt
  24. 6
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt
  25. 2
      libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedRoom.kt
  26. 1
      samples/minimal/build.gradle.kts
  27. 8
      samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt
  28. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContentForDm_null_RoomListModalBottomSheetContentForDm-Day-2_3_null,NEXUS_5,1.0,en].png
  29. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContentForDm_null_RoomListModalBottomSheetContentForDm-Night-2_4_null,NEXUS_5,1.0,en].png
  30. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContent_null_RoomListModalBottomSheetContent-Day-1_2_null,NEXUS_5,1.0,en].png
  31. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContent_null_RoomListModalBottomSheetContent-Night-1_3_null,NEXUS_5,1.0,en].png

1
changelog.d/2261.feature

@ -0,0 +1 @@ @@ -0,0 +1 @@
Manually mark a room as unread

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

@ -155,6 +155,14 @@ class MessagesPresenter @AssistedInject constructor( @@ -155,6 +155,14 @@ class MessagesPresenter @AssistedInject constructor(
mutableStateOf(false)
}
LaunchedEffect(Unit) {
// Mark the room as read on entering but don't send read receipts
// as those will be handled by the timeline.
withContext(dispatchers.io) {
room.markAsRead(null)
}
}
LaunchedEffect(syncUpdateFlow.value) {
withContext(dispatchers.io) {
canJoinCall = room.canUserJoinCall(room.sessionId).getOrDefault(false)

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

@ -96,7 +96,9 @@ import io.element.android.tests.testutils.consumeItemsUntilTimeout @@ -96,7 +96,9 @@ import io.element.android.tests.testutils.consumeItemsUntilTimeout
import io.element.android.tests.testutils.testCoroutineDispatchers
import io.mockk.mockk
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
@ -129,6 +131,21 @@ class MessagesPresenterTest { @@ -129,6 +131,21 @@ class MessagesPresenterTest {
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `present - check that the room is marked as read`() = runTest {
val room = FakeMatrixRoom()
assertThat(room.markAsReadCalls).isEmpty()
val presenter = createMessagesPresenter(matrixRoom = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
runCurrent()
assertThat(room.markAsReadCalls).isEqualTo(listOf(null))
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - call is disabled if user cannot join it even if there is an ongoing call`() = runTest {
val room = FakeMatrixRoom().apply {

4
features/roomlist/impl/build.gradle.kts

@ -52,6 +52,7 @@ dependencies { @@ -52,6 +52,7 @@ dependencies {
implementation(projects.libraries.eventformatter.api)
implementation(projects.libraries.indicator.api)
implementation(projects.libraries.deeplink)
implementation(projects.libraries.preferences.api)
implementation(projects.features.invitelist.api)
implementation(projects.features.networkmonitor.api)
implementation(projects.features.leaveroom.api)
@ -59,6 +60,8 @@ dependencies { @@ -59,6 +60,8 @@ dependencies {
api(projects.features.roomlist.api)
ksp(libs.showkase.processor)
testImplementation(libs.androidx.compose.ui.test.junit)
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test)
testImplementation(libs.molecule.runtime)
@ -71,6 +74,7 @@ dependencies { @@ -71,6 +74,7 @@ dependencies {
testImplementation(projects.libraries.eventformatter.test)
testImplementation(projects.libraries.indicator.impl)
testImplementation(projects.libraries.permissions.noop)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.features.invitelist.test)
testImplementation(projects.features.networkmonitor.test)
testImplementation(projects.tests.testutils)

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

@ -41,7 +41,7 @@ import io.element.android.libraries.ui.strings.CommonStrings @@ -41,7 +41,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun RoomListContextMenu(
contextMenu: RoomListState.ContextMenu.Shown,
eventSink: (RoomListEvents) -> Unit,
eventSink: (RoomListEvents.RoomListBottomSheetEvents) -> Unit,
onRoomSettingsClicked: (roomId: RoomId) -> Unit,
) {
ModalBottomSheet(
@ -49,9 +49,17 @@ fun RoomListContextMenu( @@ -49,9 +49,17 @@ fun RoomListContextMenu(
) {
RoomListModalBottomSheetContent(
contextMenu = contextMenu,
onRoomMarkReadClicked = {
eventSink(RoomListEvents.HideContextMenu)
eventSink(RoomListEvents.MarkAsRead(contextMenu.roomId))
},
onRoomMarkUnreadClicked = {
eventSink(RoomListEvents.HideContextMenu)
eventSink(RoomListEvents.MarkAsUnread(contextMenu.roomId))
},
onRoomSettingsClicked = {
eventSink(RoomListEvents.HideContextMenu)
onRoomSettingsClicked(it)
onRoomSettingsClicked(contextMenu.roomId)
},
onLeaveRoomClicked = {
eventSink(RoomListEvents.HideContextMenu)
@ -64,8 +72,10 @@ fun RoomListContextMenu( @@ -64,8 +72,10 @@ fun RoomListContextMenu(
@Composable
private fun RoomListModalBottomSheetContent(
contextMenu: RoomListState.ContextMenu.Shown,
onRoomSettingsClicked: (roomId: RoomId) -> Unit,
onLeaveRoomClicked: (roomId: RoomId) -> Unit,
onRoomMarkReadClicked: () -> Unit,
onRoomMarkUnreadClicked: () -> Unit,
onRoomSettingsClicked: () -> Unit,
onLeaveRoomClicked: () -> Unit,
) {
Column(
modifier = Modifier.fillMaxWidth()
@ -78,6 +88,38 @@ private fun RoomListModalBottomSheetContent( @@ -78,6 +88,38 @@ private fun RoomListModalBottomSheetContent(
)
}
)
if (contextMenu.markAsUnreadFeatureFlagEnabled) {
ListItem(
headlineContent = {
Text(
text = stringResource(
id = if (contextMenu.hasNewContent) {
R.string.screen_roomlist_mark_as_read
} else {
R.string.screen_roomlist_mark_as_unread
}
),
style = MaterialTheme.typography.bodyLarge,
)
},
modifier = Modifier.clickable {
if (contextMenu.hasNewContent) {
onRoomMarkReadClicked()
} else {
onRoomMarkUnreadClicked()
}
},
/* TODO Design
leadingContent = ListItemContent.Icon(
iconSource = IconSource.Vector(
CompoundIcons.Settings,
contentDescription = stringResource(id = CommonStrings.common_settings)
)
),
*/
style = ListItemStyle.Primary,
)
}
ListItem(
headlineContent = {
Text(
@ -85,7 +127,7 @@ private fun RoomListModalBottomSheetContent( @@ -85,7 +127,7 @@ private fun RoomListModalBottomSheetContent(
style = MaterialTheme.typography.bodyLarge,
)
},
modifier = Modifier.clickable { onRoomSettingsClicked(contextMenu.roomId) },
modifier = Modifier.clickable { onRoomSettingsClicked() },
leadingContent = ListItemContent.Icon(
iconSource = IconSource.Vector(
CompoundIcons.Settings,
@ -96,14 +138,16 @@ private fun RoomListModalBottomSheetContent( @@ -96,14 +138,16 @@ private fun RoomListModalBottomSheetContent(
)
ListItem(
headlineContent = {
val leaveText = stringResource(id = if (contextMenu.isDm) {
CommonStrings.action_leave_conversation
} else {
CommonStrings.action_leave_room
})
val leaveText = stringResource(
id = if (contextMenu.isDm) {
CommonStrings.action_leave_conversation
} else {
CommonStrings.action_leave_room
}
)
Text(text = leaveText)
},
modifier = Modifier.clickable { onLeaveRoomClicked(contextMenu.roomId) },
modifier = Modifier.clickable { onLeaveRoomClicked() },
leadingContent = ListItemContent.Icon(
iconSource = IconSource.Vector(
CompoundIcons.Leave,
@ -122,11 +166,9 @@ private fun RoomListModalBottomSheetContent( @@ -122,11 +166,9 @@ private fun RoomListModalBottomSheetContent(
@Composable
internal fun RoomListModalBottomSheetContentPreview() = ElementPreview {
RoomListModalBottomSheetContent(
contextMenu = RoomListState.ContextMenu.Shown(
roomId = RoomId(value = "!aRoom:aDomain"),
roomName = "aRoom",
isDm = false,
),
contextMenu = aContextMenuShown(hasNewContent = true),
onRoomMarkReadClicked = {},
onRoomMarkUnreadClicked = {},
onRoomSettingsClicked = {},
onLeaveRoomClicked = {}
)
@ -136,11 +178,9 @@ internal fun RoomListModalBottomSheetContentPreview() = ElementPreview { @@ -136,11 +178,9 @@ internal fun RoomListModalBottomSheetContentPreview() = ElementPreview {
@Composable
internal fun RoomListModalBottomSheetContentForDmPreview() = ElementPreview {
RoomListModalBottomSheetContent(
contextMenu = RoomListState.ContextMenu.Shown(
roomId = RoomId(value = "!aRoom:aDomain"),
roomName = "aRoom",
isDm = true,
),
contextMenu = aContextMenuShown(isDm = true),
onRoomMarkReadClicked = {},
onRoomMarkUnreadClicked = {},
onRoomSettingsClicked = {},
onLeaveRoomClicked = {}
)

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

@ -26,6 +26,10 @@ sealed interface RoomListEvents { @@ -26,6 +26,10 @@ sealed interface RoomListEvents {
data object DismissRecoveryKeyPrompt : RoomListEvents
data object ToggleSearchResults : RoomListEvents
data class ShowContextMenu(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents
data object HideContextMenu : RoomListEvents
data class LeaveRoom(val roomId: RoomId) : RoomListEvents
sealed interface RoomListBottomSheetEvents : RoomListEvents
data object HideContextMenu : RoomListBottomSheetEvents
data class LeaveRoom(val roomId: RoomId) : RoomListBottomSheetEvents
data class MarkAsRead(val roomId: RoomId) : RoomListBottomSheetEvents
data class MarkAsUnread(val roomId: RoomId) : RoomListBottomSheetEvents
}

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

@ -25,12 +25,14 @@ import androidx.compose.runtime.getValue @@ -25,12 +25,14 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import io.element.android.features.leaveroom.api.LeaveRoomEvent
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.preferences.api.store.SessionPreferencesStore
import io.element.android.features.roomlist.impl.datasource.InviteStateDataSource
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter
@ -44,10 +46,12 @@ import io.element.android.libraries.indicator.api.IndicatorService @@ -44,10 +46,12 @@ import io.element.android.libraries.indicator.api.IndicatorService
import io.element.android.libraries.matrix.api.MatrixClient
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.timeline.ReceiptType
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.api.user.getCurrentUser
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -65,9 +69,11 @@ class RoomListPresenter @Inject constructor( @@ -65,9 +69,11 @@ class RoomListPresenter @Inject constructor(
private val featureFlagService: FeatureFlagService,
private val indicatorService: IndicatorService,
private val migrationScreenPresenter: MigrationScreenPresenter,
private val sessionPreferencesStore: SessionPreferencesStore,
) : Presenter<RoomListState> {
@Composable
override fun present(): RoomListState {
val coroutineScope = rememberCoroutineScope()
val leaveRoomState = leaveRoomPresenter.present()
val matrixUser: MutableState<MatrixUser?> = rememberSaveable {
mutableStateOf(null)
@ -105,6 +111,9 @@ class RoomListPresenter @Inject constructor( @@ -105,6 +111,9 @@ class RoomListPresenter @Inject constructor(
}
}
val markAsUnreadFeatureFlagEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.MarkAsUnread)
.collectAsState(initial = null)
// Avatar indicator
val showAvatarIndicator by indicatorService.showRoomListTopBarIndicator()
@ -129,10 +138,23 @@ class RoomListPresenter @Inject constructor( @@ -129,10 +138,23 @@ class RoomListPresenter @Inject constructor(
roomId = event.roomListRoomSummary.roomId,
roomName = event.roomListRoomSummary.name,
isDm = event.roomListRoomSummary.isDm,
markAsUnreadFeatureFlagEnabled = markAsUnreadFeatureFlagEnabled == true,
hasNewContent = event.roomListRoomSummary.hasNewContent
)
}
is RoomListEvents.HideContextMenu -> contextMenu = RoomListState.ContextMenu.Hidden
is RoomListEvents.LeaveRoom -> leaveRoomState.eventSink(LeaveRoomEvent.ShowConfirmation(event.roomId))
is RoomListEvents.MarkAsRead -> coroutineScope.launch {
val receiptType = if (sessionPreferencesStore.isSendPublicReadReceiptsEnabled().first()) {
ReceiptType.READ
} else {
ReceiptType.READ_PRIVATE
}
client.getRoom(event.roomId)?.markAsRead(receiptType)
}
is RoomListEvents.MarkAsUnread -> coroutineScope.launch {
client.getRoom(event.roomId)?.markAsUnread()
}
}
}

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

@ -49,6 +49,8 @@ data class RoomListState( @@ -49,6 +49,8 @@ data class RoomListState(
val roomId: RoomId,
val roomName: String,
val isDm: Boolean,
val markAsUnreadFeatureFlagEnabled: Boolean,
val hasNewContent: Boolean,
) : ContextMenu
}
}

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

@ -43,13 +43,7 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> { @@ -43,13 +43,7 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
aRoomListState().copy(invitesState = InvitesState.NewInvites),
aRoomListState().copy(displaySearchResults = true, filter = "", filteredRoomList = persistentListOf()),
aRoomListState().copy(displaySearchResults = true),
aRoomListState().copy(
contextMenu = RoomListState.ContextMenu.Shown(
roomId = RoomId("!aRoom:aDomain"),
roomName = "A nice room name",
isDm = false,
)
),
aRoomListState().copy(contextMenu = aContextMenuShown(roomName = "A nice room name")),
aRoomListState().copy(displayRecoveryKeyPrompt = true),
aRoomListState().copy(roomList = AsyncData.Success(persistentListOf())),
aRoomListState().copy(roomList = AsyncData.Loading(prevData = RoomListRoomSummaryFactory.createFakeList())),
@ -103,3 +97,15 @@ internal fun aRoomListRoomSummaryList(): ImmutableList<RoomListRoomSummary> { @@ -103,3 +97,15 @@ internal fun aRoomListRoomSummaryList(): ImmutableList<RoomListRoomSummary> {
),
)
}
internal fun aContextMenuShown(
roomName: String = "aRoom",
isDm: Boolean = false,
hasNewContent: Boolean = false,
) = RoomListState.ContextMenu.Shown(
roomId = RoomId("!aRoom:aDomain"),
roomName = roomName,
isDm = isDm,
markAsUnreadFeatureFlagEnabled = true,
hasNewContent = hasNewContent,
)

2
features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactory.kt

@ -45,6 +45,7 @@ class RoomListRoomSummaryFactory @Inject constructor( @@ -45,6 +45,7 @@ class RoomListRoomSummaryFactory @Inject constructor(
numberOfUnreadMessages = 0,
numberOfUnreadMentions = 0,
numberOfUnreadNotifications = 0,
isMarkedUnread = false,
userDefinedNotificationMode = null,
hasRoomCall = false,
isDm = false,
@ -73,6 +74,7 @@ class RoomListRoomSummaryFactory @Inject constructor( @@ -73,6 +74,7 @@ class RoomListRoomSummaryFactory @Inject constructor(
numberOfUnreadMessages = roomSummary.details.numUnreadMessages,
numberOfUnreadMentions = roomSummary.details.numUnreadMentions,
numberOfUnreadNotifications = roomSummary.details.numUnreadNotifications,
isMarkedUnread = roomSummary.details.isMarkedUnread,
timestamp = lastMessageTimestampFormatter.format(roomSummary.details.lastMessageTimestamp),
lastMessage = roomSummary.details.lastMessage?.let { message ->
roomLastMessageFormatter.format(message.event, roomSummary.details.isDirect)

7
features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummary.kt

@ -29,6 +29,7 @@ data class RoomListRoomSummary( @@ -29,6 +29,7 @@ data class RoomListRoomSummary(
val numberOfUnreadMessages: Int,
val numberOfUnreadMentions: Int,
val numberOfUnreadNotifications: Int,
val isMarkedUnread: Boolean,
val timestamp: String?,
val lastMessage: CharSequence?,
val avatarData: AvatarData,
@ -38,9 +39,11 @@ data class RoomListRoomSummary( @@ -38,9 +39,11 @@ data class RoomListRoomSummary(
val isDm: Boolean,
) {
val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE &&
(numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0)
(numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0) ||
isMarkedUnread
val hasNewContent = numberOfUnreadMessages > 0 ||
numberOfUnreadMentions > 0 ||
numberOfUnreadNotifications > 0
numberOfUnreadNotifications > 0 ||
isMarkedUnread
}

2
features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryProvider.kt

@ -89,6 +89,7 @@ internal fun aRoomListRoomSummary( @@ -89,6 +89,7 @@ internal fun aRoomListRoomSummary(
numberOfUnreadMessages: Int = 0,
numberOfUnreadMentions: Int = 0,
numberOfUnreadNotifications: Int = 0,
isMarkedUnread: Boolean = false,
lastMessage: String? = "Last message",
timestamp: String? = lastMessage?.let { "88:88" },
isPlaceholder: Boolean = false,
@ -103,6 +104,7 @@ internal fun aRoomListRoomSummary( @@ -103,6 +104,7 @@ internal fun aRoomListRoomSummary(
numberOfUnreadMessages = numberOfUnreadMessages,
numberOfUnreadMentions = numberOfUnreadMentions,
numberOfUnreadNotifications = numberOfUnreadNotifications,
isMarkedUnread = isMarkedUnread,
timestamp = timestamp,
lastMessage = lastMessage,
avatarData = avatarData,

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

@ -0,0 +1,131 @@ @@ -0,0 +1,131 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.roomlist.impl
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureCalledOnceWithParam
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class RoomListContextMenuTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `clicking on Mark as read generates expected Events`() {
val eventsRecorder = EventsRecorder<RoomListEvents>()
val contextMenu = aContextMenuShown(hasNewContent = true)
rule.setContent {
RoomListContextMenu(
contextMenu = contextMenu,
eventSink = eventsRecorder,
onRoomSettingsClicked = EnsureNeverCalledWithParam(),
)
}
rule.clickOn(R.string.screen_roomlist_mark_as_read)
eventsRecorder.assertList(
listOf(
RoomListEvents.HideContextMenu,
RoomListEvents.MarkAsRead(contextMenu.roomId),
)
)
}
@Test
fun `clicking on Mark as unread generates expected Events`() {
val eventsRecorder = EventsRecorder<RoomListEvents>()
val contextMenu = aContextMenuShown(hasNewContent = false)
rule.setContent {
RoomListContextMenu(
contextMenu = contextMenu,
eventSink = eventsRecorder,
onRoomSettingsClicked = EnsureNeverCalledWithParam(),
)
}
rule.clickOn(R.string.screen_roomlist_mark_as_unread)
eventsRecorder.assertList(
listOf(
RoomListEvents.HideContextMenu,
RoomListEvents.MarkAsUnread(contextMenu.roomId),
)
)
}
@Test
fun `clicking on Leave dm generates expected Events`() {
val eventsRecorder = EventsRecorder<RoomListEvents>()
val contextMenu = aContextMenuShown(isDm = true)
rule.setContent {
RoomListContextMenu(
contextMenu = contextMenu,
eventSink = eventsRecorder,
onRoomSettingsClicked = EnsureNeverCalledWithParam(),
)
}
rule.clickOn(CommonStrings.action_leave_conversation)
eventsRecorder.assertList(
listOf(
RoomListEvents.HideContextMenu,
RoomListEvents.LeaveRoom(contextMenu.roomId),
)
)
}
@Test
fun `clicking on Leave room generates expected Events`() {
val eventsRecorder = EventsRecorder<RoomListEvents>()
val contextMenu = aContextMenuShown(isDm = false)
rule.setContent {
RoomListContextMenu(
contextMenu = contextMenu,
eventSink = eventsRecorder,
onRoomSettingsClicked = EnsureNeverCalledWithParam(),
)
}
rule.clickOn(CommonStrings.action_leave_room)
eventsRecorder.assertList(
listOf(
RoomListEvents.HideContextMenu,
RoomListEvents.LeaveRoom(contextMenu.roomId),
)
)
}
@Test
fun `clicking on Settings invokes the expected callback and generates expected Event`() {
val eventsRecorder = EventsRecorder<RoomListEvents>()
val contextMenu = aContextMenuShown()
val callback = EnsureCalledOnceWithParam(contextMenu.roomId)
rule.setContent {
RoomListContextMenu(
contextMenu = contextMenu,
eventSink = eventsRecorder,
onRoomSettingsClicked = callback,
)
}
rule.clickOn(CommonStrings.common_settings)
eventsRecorder.assertSingle(RoomListEvents.HideContextMenu)
callback.assertSuccess()
}
}

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

@ -25,28 +25,30 @@ import io.element.android.features.leaveroom.api.LeaveRoomPresenter @@ -25,28 +25,30 @@ import io.element.android.features.leaveroom.api.LeaveRoomPresenter
import io.element.android.features.leaveroom.fake.FakeLeaveRoomPresenter
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
import io.element.android.features.preferences.api.store.SessionPreferencesStore
import io.element.android.features.roomlist.impl.datasource.FakeInviteDataSource
import io.element.android.features.roomlist.impl.datasource.InviteStateDataSource
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.migration.InMemoryMigrationScreenStore
import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.features.roomlist.impl.model.createRoomListRoomSummary
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
import io.element.android.libraries.dateformatter.test.A_FORMATTED_DATE
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesStore
import io.element.android.libraries.indicator.impl.DefaultIndicatorService
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.encryption.BackupState
import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
@ -59,6 +61,7 @@ import io.element.android.libraries.matrix.test.A_USER_NAME @@ -59,6 +61,7 @@ import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
@ -175,12 +178,23 @@ class RoomListPresenterTests { @@ -175,12 +178,23 @@ class RoomListPresenterTests {
val initialItems = initialState.roomList.dataOrNull().orEmpty()
assertThat(initialItems.size).isEqualTo(16)
assertThat(initialItems.all { it.isPlaceholder }).isTrue()
roomListService.postAllRooms(listOf(aRoomSummaryFilled()))
roomListService.postAllRooms(
listOf(
aRoomSummaryFilled(
numUnreadMentions = 1,
numUnreadMessages = 2,
)
)
)
val withRoomState = consumeItemsUntilPredicate { state -> state.roomList.dataOrNull()?.size == 1 }.last()
val withRoomStateItems = withRoomState.roomList.dataOrNull().orEmpty()
assertThat(withRoomStateItems.size).isEqualTo(1)
assertThat(withRoomStateItems.first())
.isEqualTo(aRoomListRoomSummary)
assertThat(withRoomStateItems.first()).isEqualTo(
createRoomListRoomSummary(
numberOfUnreadMentions = 1,
numberOfUnreadMessages = 2,
)
)
scope.cancel()
}
}
@ -196,7 +210,14 @@ class RoomListPresenterTests { @@ -196,7 +210,14 @@ class RoomListPresenterTests {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
roomListService.postAllRooms(listOf(aRoomSummaryFilled()))
roomListService.postAllRooms(
listOf(
aRoomSummaryFilled(
numUnreadMentions = 1,
numUnreadMessages = 2,
)
)
)
skipItems(3)
val loadedState = awaitItem()
// Test filtering with result
@ -207,8 +228,12 @@ class RoomListPresenterTests { @@ -207,8 +228,12 @@ class RoomListPresenterTests {
assertThat(withFilteredRoomState.filteredRoomList.size).isEqualTo(1)
assertThat(withFilteredRoomState.filter).isEqualTo(A_ROOM_NAME.substring(0, 3))
assertThat(withFilteredRoomState.filteredRoomList.size).isEqualTo(1)
assertThat(withFilteredRoomState.filteredRoomList.first())
.isEqualTo(aRoomListRoomSummary)
assertThat(withFilteredRoomState.filteredRoomList.first()).isEqualTo(
createRoomListRoomSummary(
numberOfUnreadMentions = 1,
numberOfUnreadMessages = 2,
)
)
// Test filtering without result
withFilteredRoomState.eventSink.invoke(RoomListEvents.UpdateFilter("tada"))
skipItems(1)
@ -322,12 +347,19 @@ class RoomListPresenterTests { @@ -322,12 +347,19 @@ class RoomListPresenterTests {
skipItems(1)
val initialState = awaitItem()
val summary = aRoomListRoomSummary
val summary = createRoomListRoomSummary()
initialState.eventSink(RoomListEvents.ShowContextMenu(summary))
val shownState = awaitItem()
assertThat(shownState.contextMenu)
.isEqualTo(RoomListState.ContextMenu.Shown(summary.roomId, summary.name, false))
assertThat(shownState.contextMenu).isEqualTo(
RoomListState.ContextMenu.Shown(
roomId = summary.roomId,
roomName = summary.name,
isDm = false,
markAsUnreadFeatureFlagEnabled = true,
hasNewContent = false,
)
)
scope.cancel()
}
}
@ -342,12 +374,19 @@ class RoomListPresenterTests { @@ -342,12 +374,19 @@ class RoomListPresenterTests {
skipItems(1)
val initialState = awaitItem()
val summary = aRoomListRoomSummary
val summary = createRoomListRoomSummary()
initialState.eventSink(RoomListEvents.ShowContextMenu(summary))
val shownState = awaitItem()
assertThat(shownState.contextMenu)
.isEqualTo(RoomListState.ContextMenu.Shown(summary.roomId, summary.name, false))
assertThat(shownState.contextMenu).isEqualTo(
RoomListState.ContextMenu.Shown(
roomId = summary.roomId,
roomName = summary.name,
isDm = false,
markAsUnreadFeatureFlagEnabled = true,
hasNewContent = false,
)
)
shownState.eventSink(RoomListEvents.HideContextMenu)
val hiddenState = awaitItem()
@ -430,6 +469,41 @@ class RoomListPresenterTests { @@ -430,6 +469,41 @@ class RoomListPresenterTests {
}
}
@Test
fun `present - check that the room is marked as read with correct RR and as unread`() = runTest {
val room = FakeMatrixRoom()
val sessionPreferencesStore = InMemorySessionPreferencesStore()
val matrixClient = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, room)
}
val scope = CoroutineScope(coroutineContext + SupervisorJob())
val presenter = createRoomListPresenter(
client = matrixClient,
coroutineScope = scope,
sessionPreferencesStore = sessionPreferencesStore,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(room.markAsReadCalls).isEmpty()
assertThat(room.markAsUnreadReadCallCount).isEqualTo(0)
initialState.eventSink.invoke(RoomListEvents.MarkAsRead(A_ROOM_ID))
assertThat(room.markAsReadCalls).isEqualTo(listOf(ReceiptType.READ))
assertThat(room.markAsUnreadReadCallCount).isEqualTo(0)
initialState.eventSink.invoke(RoomListEvents.MarkAsUnread(A_ROOM_ID))
assertThat(room.markAsReadCalls).isEqualTo(listOf(ReceiptType.READ))
assertThat(room.markAsUnreadReadCallCount).isEqualTo(1)
// Test again with private read receipts
sessionPreferencesStore.setSendPublicReadReceipts(false)
initialState.eventSink.invoke(RoomListEvents.MarkAsRead(A_ROOM_ID))
assertThat(room.markAsReadCalls).isEqualTo(listOf(ReceiptType.READ, ReceiptType.READ_PRIVATE))
assertThat(room.markAsUnreadReadCallCount).isEqualTo(1)
cancelAndIgnoreRemainingEvents()
scope.cancel()
}
}
private fun TestScope.createRoomListPresenter(
client: MatrixClient = FakeMatrixClient(),
sessionVerificationService: SessionVerificationService = FakeSessionVerificationService(),
@ -442,6 +516,7 @@ class RoomListPresenterTests { @@ -442,6 +516,7 @@ class RoomListPresenterTests {
},
roomLastMessageFormatter: RoomLastMessageFormatter = FakeRoomLastMessageFormatter(),
encryptionService: EncryptionService = FakeEncryptionService(),
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
coroutineScope: CoroutineScope,
migrationScreenPresenter: MigrationScreenPresenter = MigrationScreenPresenter(
matrixClient = client,
@ -472,23 +547,6 @@ class RoomListPresenterTests { @@ -472,23 +547,6 @@ class RoomListPresenterTests {
featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.SecureStorage.key to true)),
),
migrationScreenPresenter = migrationScreenPresenter,
sessionPreferencesStore = sessionPreferencesStore,
)
}
private const val A_FORMATTED_DATE = "formatted_date"
private val aRoomListRoomSummary = RoomListRoomSummary(
id = A_ROOM_ID.value,
roomId = A_ROOM_ID,
name = A_ROOM_NAME,
numberOfUnreadMentions = 1,
numberOfUnreadMessages = 2,
numberOfUnreadNotifications = 0,
timestamp = A_FORMATTED_DATE,
lastMessage = "",
avatarData = AvatarData(id = A_ROOM_ID.value, name = A_ROOM_NAME, size = AvatarSize.RoomListItem),
isPlaceholder = false,
userDefinedNotificationMode = null,
hasRoomCall = false,
isDm = false,
)

98
features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryTest.kt

@ -0,0 +1,98 @@ @@ -0,0 +1,98 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.roomlist.impl.model
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.dateformatter.test.A_FORMATTED_DATE
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME
import org.junit.Test
class RoomListRoomSummaryTest {
@Test
fun `test default value`() {
val sut = createRoomListRoomSummary(
isMarkedUnread = false,
)
assertThat(sut.isHighlighted).isFalse()
assertThat(sut.hasNewContent).isFalse()
}
@Test
fun `test muted room`() {
val sut = createRoomListRoomSummary(
userDefinedNotificationMode = RoomNotificationMode.MUTE,
)
assertThat(sut.isHighlighted).isFalse()
assertThat(sut.hasNewContent).isFalse()
}
@Test
fun `test muted room isMarkedUnread set to true`() {
val sut = createRoomListRoomSummary(
isMarkedUnread = true,
userDefinedNotificationMode = RoomNotificationMode.MUTE,
)
assertThat(sut.isHighlighted).isTrue()
assertThat(sut.hasNewContent).isTrue()
}
@Test
fun `test muted room with unread message`() {
val sut = createRoomListRoomSummary(
numberOfUnreadNotifications = 1,
userDefinedNotificationMode = RoomNotificationMode.MUTE,
)
assertThat(sut.isHighlighted).isFalse()
assertThat(sut.hasNewContent).isTrue()
}
@Test
fun `test isMarkedUnread set to true`() {
val sut = createRoomListRoomSummary(
isMarkedUnread = true,
)
assertThat(sut.isHighlighted).isTrue()
assertThat(sut.hasNewContent).isTrue()
}
}
internal fun createRoomListRoomSummary(
numberOfUnreadMentions: Int = 0,
numberOfUnreadMessages: Int = 0,
numberOfUnreadNotifications: Int = 0,
isMarkedUnread: Boolean = false,
userDefinedNotificationMode: RoomNotificationMode? = null,
) = RoomListRoomSummary(
id = A_ROOM_ID.value,
roomId = A_ROOM_ID,
name = A_ROOM_NAME,
numberOfUnreadMentions = numberOfUnreadMentions,
numberOfUnreadMessages = numberOfUnreadMessages,
numberOfUnreadNotifications = numberOfUnreadNotifications,
isMarkedUnread = isMarkedUnread,
timestamp = A_FORMATTED_DATE,
lastMessage = "",
avatarData = AvatarData(id = A_ROOM_ID.value, name = A_ROOM_NAME, size = AvatarSize.RoomListItem),
isPlaceholder = false,
userDefinedNotificationMode = userDefinedNotificationMode,
hasRoomCall = false,
isDm = false,
)

2
libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeLastMessageTimestampFormatter.kt

@ -18,6 +18,8 @@ package io.element.android.libraries.dateformatter.test @@ -18,6 +18,8 @@ package io.element.android.libraries.dateformatter.test
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
const val A_FORMATTED_DATE = "formatted_date"
class FakeLastMessageTimestampFormatter : LastMessageTimestampFormatter {
private var format = ""
fun givenFormat(format: String) {

7
libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt

@ -75,4 +75,11 @@ enum class FeatureFlags( @@ -75,4 +75,11 @@ enum class FeatureFlags(
defaultValue = true,
isFinished = false,
),
MarkAsUnread(
key = "feature.markAsUnread",
title = "Mark as unread",
description = "Allow user to mark a room as unread",
defaultValue = true,
isFinished = false,
),
}

1
libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt

@ -40,6 +40,7 @@ class StaticFeatureFlagProvider @Inject constructor() : @@ -40,6 +40,7 @@ class StaticFeatureFlagProvider @Inject constructor() :
FeatureFlags.PinUnlock -> true
FeatureFlags.Mentions -> true
FeatureFlags.SecureStorage -> true
FeatureFlags.MarkAsUnread -> false
}
} else {
false

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

@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.api.media.VideoInfo @@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
import kotlinx.coroutines.flow.Flow
@ -150,6 +151,17 @@ interface MatrixRoom : Closeable { @@ -150,6 +151,17 @@ interface MatrixRoom : Closeable {
suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result<Unit>
/**
* Reverts a previously set unread flag, and eventually send a Read Receipt.
* @param receiptType The type of receipt to send. If null, no Read Receipt will be sent.
*/
suspend fun markAsRead(receiptType: ReceiptType?): Result<Unit>
/**
* Sets a flag on the room to indicate that the user has explicitly marked it as unread.
*/
suspend fun markAsUnread(): Result<Unit>
/**
* Share a location message in the room.
*

1
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomSummary.kt

@ -43,6 +43,7 @@ data class RoomSummaryDetails( @@ -43,6 +43,7 @@ data class RoomSummaryDetails(
val numUnreadMessages: Int,
val numUnreadMentions: Int,
val numUnreadNotifications: Int,
val isMarkedUnread: Boolean,
val inviter: RoomMember?,
val userDefinedNotificationMode: RoomNotificationMode?,
val hasRoomCall: Boolean,

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

@ -40,6 +40,7 @@ import io.element.android.libraries.matrix.api.room.StateEventType @@ -40,6 +40,7 @@ import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.room.roomNotificationSettings
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
import io.element.android.libraries.matrix.impl.core.toProgressWatcher
@ -51,6 +52,7 @@ import io.element.android.libraries.matrix.impl.poll.toInner @@ -51,6 +52,7 @@ import io.element.android.libraries.matrix.impl.poll.toInner
import io.element.android.libraries.matrix.impl.room.location.toInner
import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
import io.element.android.libraries.matrix.impl.timeline.toRustReceiptType
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
import io.element.android.libraries.matrix.impl.widget.RustWidgetDriver
import io.element.android.libraries.matrix.impl.widget.generateWidgetWebViewUrl
@ -423,6 +425,22 @@ class RustMatrixRoom( @@ -423,6 +425,22 @@ class RustMatrixRoom(
}
}
override suspend fun markAsRead(receiptType: ReceiptType?): Result<Unit> = withContext(roomDispatcher) {
runCatching {
if (receiptType != null) {
innerRoom.markAsReadAndSendReadReceipt(receiptType.toRustReceiptType())
} else {
innerRoom.markAsRead()
}
}
}
override suspend fun markAsUnread(): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.markAsUnread()
}
}
override suspend fun sendLocation(
body: String,
geoUri: String,

1
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryDetailsFactory.kt

@ -38,6 +38,7 @@ class RoomSummaryDetailsFactory(private val roomMessageFactory: RoomMessageFacto @@ -38,6 +38,7 @@ class RoomSummaryDetailsFactory(private val roomMessageFactory: RoomMessageFacto
numUnreadMentions = roomInfo.numUnreadMentions.toInt(),
numUnreadMessages = roomInfo.numUnreadMessages.toInt(),
numUnreadNotifications = roomInfo.numUnreadNotifications.toInt(),
isMarkedUnread = roomInfo.isMarkedUnread,
lastMessage = latestRoomMessage,
inviter = roomInfo.inviter?.let(RoomMemberMapper::map),
userDefinedNotificationMode = roomInfo.userDefinedNotificationMode?.let(RoomNotificationSettingsMapper::mapMode),

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

@ -41,6 +41,7 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationMode @@ -41,6 +41,7 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
@ -374,6 +375,20 @@ class FakeMatrixRoom( @@ -374,6 +375,20 @@ class FakeMatrixRoom(
return reportContentResult
}
val markAsReadCalls = mutableListOf<ReceiptType?>()
override suspend fun markAsRead(receiptType: ReceiptType?): Result<Unit> {
markAsReadCalls.add(receiptType)
return Result.success(Unit)
}
var markAsUnreadReadCallCount = 0
private set
override suspend fun markAsUnread(): Result<Unit> {
markAsUnreadReadCallCount++
return Result.success(Unit)
}
override suspend fun sendLocation(
body: String,
geoUri: String,

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

@ -37,8 +37,8 @@ fun aRoomSummaryFilled( @@ -37,8 +37,8 @@ fun aRoomSummaryFilled(
isDirect: Boolean = false,
avatarUrl: String? = null,
lastMessage: RoomMessage? = aRoomMessage(),
numUnreadMentions: Int = 1,
numUnreadMessages: Int = 2,
numUnreadMentions: Int = 0,
numUnreadMessages: Int = 0,
notificationMode: RoomNotificationMode? = null,
) = RoomSummary.Filled(
aRoomSummaryDetails(
@ -62,6 +62,7 @@ fun aRoomSummaryDetails( @@ -62,6 +62,7 @@ fun aRoomSummaryDetails(
numUnreadMentions: Int = 0,
numUnreadMessages: Int = 0,
numUnreadNotifications: Int = 0,
isMarkedUnread: Boolean = false,
notificationMode: RoomNotificationMode? = null,
inviter: RoomMember? = null,
canonicalAlias: String? = null,
@ -76,6 +77,7 @@ fun aRoomSummaryDetails( @@ -76,6 +77,7 @@ fun aRoomSummaryDetails(
numUnreadMentions = numUnreadMentions,
numUnreadMessages = numUnreadMessages,
numUnreadNotifications = numUnreadNotifications,
isMarkedUnread = isMarkedUnread,
userDefinedNotificationMode = notificationMode,
inviter = inviter,
canonicalAlias = canonicalAlias,

2
libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedRoom.kt

@ -116,6 +116,7 @@ fun aRoomSummaryDetails( @@ -116,6 +116,7 @@ fun aRoomSummaryDetails(
numUnreadMentions: Int = 0,
numUnreadMessages: Int = 0,
numUnreadNotifications: Int = 0,
isMarkedUnread: Boolean = false,
) = RoomSummaryDetails(
roomId = roomId,
name = name,
@ -130,4 +131,5 @@ fun aRoomSummaryDetails( @@ -130,4 +131,5 @@ fun aRoomSummaryDetails(
numUnreadMentions = numUnreadMentions,
numUnreadMessages = numUnreadMessages,
numUnreadNotifications = numUnreadNotifications,
isMarkedUnread = isMarkedUnread,
)

1
samples/minimal/build.gradle.kts

@ -54,6 +54,7 @@ dependencies { @@ -54,6 +54,7 @@ dependencies {
implementation(projects.libraries.network)
implementation(projects.libraries.dateformatter.impl)
implementation(projects.libraries.eventformatter.impl)
implementation(projects.libraries.preferences.impl)
implementation(projects.libraries.indicator.impl)
implementation(projects.features.invitelist.impl)
implementation(projects.features.roomlist.impl)

8
samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt

@ -45,6 +45,7 @@ import io.element.android.libraries.indicator.impl.DefaultIndicatorService @@ -45,6 +45,7 @@ import io.element.android.libraries.indicator.impl.DefaultIndicatorService
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.preferences.impl.store.DefaultSessionPreferencesStore
import io.element.android.services.toolbox.impl.strings.AndroidStringProvider
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@ -108,7 +109,12 @@ class RoomListScreen( @@ -108,7 +109,12 @@ class RoomListScreen(
migrationScreenPresenter = MigrationScreenPresenter(
matrixClient = matrixClient,
migrationScreenStore = SharedPrefsMigrationScreenStore(context.getSharedPreferences("migration", Context.MODE_PRIVATE))
)
),
sessionPreferencesStore = DefaultSessionPreferencesStore(
context = context,
sessionId = matrixClient.sessionId,
sessionCoroutineScope = Singleton.appScope
),
)
@Composable

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContentForDm_null_RoomListModalBottomSheetContentForDm-Day-2_3_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContentForDm_null_RoomListModalBottomSheetContentForDm-Night-2_4_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContent_null_RoomListModalBottomSheetContent-Day-1_2_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContent_null_RoomListModalBottomSheetContent-Night-1_3_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.
Loading…
Cancel
Save