Merge pull request #2354 from element-hq/feature/bma/markUnread
Mark room as unread
This commit is contained in:
commit
d5c123622b
31 changed files with 542 additions and 76 deletions
|
|
@ -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(
|
|||
) {
|
||||
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(
|
|||
@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(
|
|||
)
|
||||
}
|
||||
)
|
||||
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(
|
|||
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(
|
|||
)
|
||||
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(
|
|||
@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 {
|
|||
@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 = {}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|||
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(
|
|||
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(
|
|||
}
|
||||
}
|
||||
|
||||
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(
|
|||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ data class RoomListState(
|
|||
val roomId: RoomId,
|
||||
val roomName: String,
|
||||
val isDm: Boolean,
|
||||
val markAsUnreadFeatureFlagEnabled: Boolean,
|
||||
val hasNewContent: Boolean,
|
||||
) : ContextMenu
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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> {
|
|||
),
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
numberOfUnreadMessages = numberOfUnreadMessages,
|
||||
numberOfUnreadMentions = numberOfUnreadMentions,
|
||||
numberOfUnreadNotifications = numberOfUnreadNotifications,
|
||||
isMarkedUnread = isMarkedUnread,
|
||||
timestamp = timestamp,
|
||||
lastMessage = lastMessage,
|
||||
avatarData = avatarData,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|||
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 {
|
|||
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 {
|
|||
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 {
|
|||
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 {
|
|||
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 {
|
|||
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 {
|
|||
}
|
||||
}
|
||||
|
||||
@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 {
|
|||
},
|
||||
roomLastMessageFormatter: RoomLastMessageFormatter = FakeRoomLastMessageFormatter(),
|
||||
encryptionService: EncryptionService = FakeEncryptionService(),
|
||||
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
|
||||
coroutineScope: CoroutineScope,
|
||||
migrationScreenPresenter: MigrationScreenPresenter = MigrationScreenPresenter(
|
||||
matrixClient = client,
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue