Merge pull request #2397 from element-hq/feature/fga/mark_room_as_favorite
Feature/fga/mark room as favorite
This commit is contained in:
commit
8fab748c9a
61 changed files with 405 additions and 205 deletions
1
changelog.d/2208.feature
Normal file
1
changelog.d/2208.feature
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Mark a room or dm as favourite.
|
||||||
|
|
@ -20,4 +20,5 @@ sealed interface RoomDetailsEvent {
|
||||||
data object LeaveRoom : RoomDetailsEvent
|
data object LeaveRoom : RoomDetailsEvent
|
||||||
data object MuteNotification : RoomDetailsEvent
|
data object MuteNotification : RoomDetailsEvent
|
||||||
data object UnmuteNotification : RoomDetailsEvent
|
data object UnmuteNotification : RoomDetailsEvent
|
||||||
|
data class SetFavorite(val isFavorite: Boolean) : RoomDetailsEvent
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
|
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
|
||||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.core.bool.orFalse
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
|
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
|
|
@ -65,12 +66,13 @@ class RoomDetailsPresenter @Inject constructor(
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val leaveRoomState = leaveRoomPresenter.present()
|
val leaveRoomState = leaveRoomPresenter.present()
|
||||||
val canShowNotificationSettings = remember { mutableStateOf(false) }
|
val canShowNotificationSettings = remember { mutableStateOf(false) }
|
||||||
val roomInfo = room.roomInfoFlow.collectAsState(initial = null).value
|
val roomInfo by room.roomInfoFlow.collectAsState(initial = null)
|
||||||
|
|
||||||
val roomAvatar by remember { derivedStateOf { roomInfo?.avatarUrl ?: room.avatarUrl } }
|
val roomAvatar by remember { derivedStateOf { roomInfo?.avatarUrl ?: room.avatarUrl } }
|
||||||
|
|
||||||
val roomName by remember { derivedStateOf { (roomInfo?.name ?: room.name ?: room.displayName).trim() } }
|
val roomName by remember { derivedStateOf { (roomInfo?.name ?: room.name ?: room.displayName).trim() } }
|
||||||
val roomTopic by remember { derivedStateOf { roomInfo?.topic ?: room.topic } }
|
val roomTopic by remember { derivedStateOf { roomInfo?.topic ?: room.topic } }
|
||||||
|
val isFavorite by remember { derivedStateOf { roomInfo?.isFavorite.orFalse() } }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
canShowNotificationSettings.value = featureFlagService.isFeatureEnabled(FeatureFlags.NotificationSettings)
|
canShowNotificationSettings.value = featureFlagService.isFeatureEnabled(FeatureFlags.NotificationSettings)
|
||||||
|
|
@ -122,6 +124,11 @@ class RoomDetailsPresenter @Inject constructor(
|
||||||
client.notificationSettingsService().unmuteRoom(room.roomId, room.isEncrypted, room.isOneToOne)
|
client.notificationSettingsService().unmuteRoom(room.roomId, room.isEncrypted, room.isOneToOne)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is RoomDetailsEvent.SetFavorite -> {
|
||||||
|
scope.launch {
|
||||||
|
room.setIsFavorite(event.isFavorite)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -142,6 +149,7 @@ class RoomDetailsPresenter @Inject constructor(
|
||||||
roomMemberDetailsState = roomMemberDetailsState,
|
roomMemberDetailsState = roomMemberDetailsState,
|
||||||
leaveRoomState = leaveRoomState,
|
leaveRoomState = leaveRoomState,
|
||||||
roomNotificationSettings = roomNotificationSettingsState.roomNotificationSettings(),
|
roomNotificationSettings = roomNotificationSettingsState.roomNotificationSettings(),
|
||||||
|
isFavorite = isFavorite,
|
||||||
eventSink = ::handleEvents,
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ data class RoomDetailsState(
|
||||||
val canShowNotificationSettings: Boolean,
|
val canShowNotificationSettings: Boolean,
|
||||||
val leaveRoomState: LeaveRoomState,
|
val leaveRoomState: LeaveRoomState,
|
||||||
val roomNotificationSettings: RoomNotificationSettings?,
|
val roomNotificationSettings: RoomNotificationSettings?,
|
||||||
|
val isFavorite: Boolean,
|
||||||
val eventSink: (RoomDetailsEvent) -> Unit
|
val eventSink: (RoomDetailsEvent) -> Unit
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ open class RoomDetailsStateProvider : PreviewParameterProvider<RoomDetailsState>
|
||||||
aDmRoomDetailsState().copy(roomName = "Daniel"),
|
aDmRoomDetailsState().copy(roomName = "Daniel"),
|
||||||
aDmRoomDetailsState(isDmMemberIgnored = true).copy(roomName = "Daniel"),
|
aDmRoomDetailsState(isDmMemberIgnored = true).copy(roomName = "Daniel"),
|
||||||
aRoomDetailsState().copy(canInvite = true),
|
aRoomDetailsState().copy(canInvite = true),
|
||||||
|
aRoomDetailsState().copy(isFavorite = true),
|
||||||
aRoomDetailsState().copy(
|
aRoomDetailsState().copy(
|
||||||
canEdit = true,
|
canEdit = true,
|
||||||
// Also test the roomNotificationSettings ALL_MESSAGES in the same screenshot. Icon 'Mute' should be displayed
|
// Also test the roomNotificationSettings ALL_MESSAGES in the same screenshot. Icon 'Mute' should be displayed
|
||||||
|
|
@ -86,6 +87,7 @@ fun aRoomDetailsState() = RoomDetailsState(
|
||||||
roomMemberDetailsState = null,
|
roomMemberDetailsState = null,
|
||||||
leaveRoomState = aLeaveRoomState(),
|
leaveRoomState = aLeaveRoomState(),
|
||||||
roomNotificationSettings = RoomNotificationSettings(mode = RoomNotificationMode.MUTE, isDefault = false),
|
roomNotificationSettings = RoomNotificationSettings(mode = RoomNotificationMode.MUTE, isDefault = false),
|
||||||
|
isFavorite = false,
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.components.button.MainActionButton
|
import io.element.android.libraries.designsystem.components.button.MainActionButton
|
||||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
|
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
|
||||||
|
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
|
||||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
|
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
|
@ -163,6 +164,13 @@ fun RoomDetailsView(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FavoriteSection(
|
||||||
|
isFavorite = state.isFavorite,
|
||||||
|
onFavoriteChanges = {
|
||||||
|
state.eventSink(RoomDetailsEvent.SetFavorite(it))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if (state.roomType is RoomDetailsType.Room) {
|
if (state.roomType is RoomDetailsType.Room) {
|
||||||
MembersSection(
|
MembersSection(
|
||||||
memberCount = state.memberCount,
|
memberCount = state.memberCount,
|
||||||
|
|
@ -356,6 +364,22 @@ private fun NotificationSection(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun FavoriteSection(
|
||||||
|
isFavorite: Boolean,
|
||||||
|
onFavoriteChanges: (Boolean) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
PreferenceCategory(modifier = modifier) {
|
||||||
|
PreferenceSwitch(
|
||||||
|
icon = CompoundIcons.Favourite(),
|
||||||
|
title = stringResource(id = CommonStrings.common_favourite),
|
||||||
|
isChecked = isFavorite,
|
||||||
|
onCheckedChange = onFavoriteChanges
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MembersSection(
|
private fun MembersSection(
|
||||||
memberCount: Long,
|
memberCount: Long,
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ package io.element.android.features.roomdetails
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import app.cash.molecule.RecompositionMode
|
import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
|
import app.cash.turbine.TurbineTestContext
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.createroom.test.FakeStartDMAction
|
import io.element.android.features.createroom.test.FakeStartDMAction
|
||||||
|
|
@ -27,6 +28,7 @@ import io.element.android.features.leaveroom.api.LeaveRoomPresenter
|
||||||
import io.element.android.features.leaveroom.fake.FakeLeaveRoomPresenter
|
import io.element.android.features.leaveroom.fake.FakeLeaveRoomPresenter
|
||||||
import io.element.android.features.roomdetails.impl.RoomDetailsEvent
|
import io.element.android.features.roomdetails.impl.RoomDetailsEvent
|
||||||
import io.element.android.features.roomdetails.impl.RoomDetailsPresenter
|
import io.element.android.features.roomdetails.impl.RoomDetailsPresenter
|
||||||
|
import io.element.android.features.roomdetails.impl.RoomDetailsState
|
||||||
import io.element.android.features.roomdetails.impl.RoomDetailsType
|
import io.element.android.features.roomdetails.impl.RoomDetailsType
|
||||||
import io.element.android.features.roomdetails.impl.RoomTopicState
|
import io.element.android.features.roomdetails.impl.RoomTopicState
|
||||||
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
||||||
|
|
@ -71,10 +73,10 @@ class RoomDetailsPresenterTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun TestScope.createRoomDetailsPresenter(
|
private fun TestScope.createRoomDetailsPresenter(
|
||||||
room: MatrixRoom,
|
room: MatrixRoom = aMatrixRoom(),
|
||||||
leaveRoomPresenter: LeaveRoomPresenter = FakeLeaveRoomPresenter(),
|
leaveRoomPresenter: LeaveRoomPresenter = FakeLeaveRoomPresenter(),
|
||||||
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
||||||
notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService()
|
notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(),
|
||||||
): RoomDetailsPresenter {
|
): RoomDetailsPresenter {
|
||||||
val matrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService)
|
val matrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService)
|
||||||
val roomMemberDetailsPresenterFactory = object : RoomMemberDetailsPresenter.Factory {
|
val roomMemberDetailsPresenterFactory = object : RoomMemberDetailsPresenter.Factory {
|
||||||
|
|
@ -86,25 +88,29 @@ class RoomDetailsPresenterTests {
|
||||||
mapOf(FeatureFlags.NotificationSettings.key to true)
|
mapOf(FeatureFlags.NotificationSettings.key to true)
|
||||||
)
|
)
|
||||||
return RoomDetailsPresenter(
|
return RoomDetailsPresenter(
|
||||||
matrixClient,
|
client = matrixClient,
|
||||||
room,
|
room = room,
|
||||||
featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
matrixClient.notificationSettingsService(),
|
notificationSettingsService = matrixClient.notificationSettingsService(),
|
||||||
roomMemberDetailsPresenterFactory,
|
roomMembersDetailsPresenterFactory = roomMemberDetailsPresenterFactory,
|
||||||
leaveRoomPresenter,
|
leaveRoomPresenter = leaveRoomPresenter,
|
||||||
dispatchers
|
dispatchers = dispatchers,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun RoomDetailsPresenter.test(validate: suspend TurbineTestContext<RoomDetailsState>.() -> Unit) {
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
withFakeLifecycleOwner(fakeLifecycleOwner) {
|
||||||
|
present()
|
||||||
|
}
|
||||||
|
}.test(validate = validate)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state is created from room if roomInfo is null`() = runTest {
|
fun `present - initial state is created from room if roomInfo is null`() = runTest {
|
||||||
val room = aMatrixRoom()
|
val room = aMatrixRoom()
|
||||||
val presenter = createRoomDetailsPresenter(room)
|
val presenter = createRoomDetailsPresenter(room)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
withFakeLifecycleOwner(fakeLifecycleOwner) {
|
|
||||||
presenter.present()
|
|
||||||
}
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.roomId).isEqualTo(room.roomId.value)
|
assertThat(initialState.roomId).isEqualTo(room.roomId.value)
|
||||||
assertThat(initialState.roomName).isEqualTo(room.name)
|
assertThat(initialState.roomName).isEqualTo(room.name)
|
||||||
|
|
@ -124,11 +130,7 @@ class RoomDetailsPresenterTests {
|
||||||
givenRoomInfo(roomInfo)
|
givenRoomInfo(roomInfo)
|
||||||
}
|
}
|
||||||
val presenter = createRoomDetailsPresenter(room)
|
val presenter = createRoomDetailsPresenter(room)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
withFakeLifecycleOwner(fakeLifecycleOwner) {
|
|
||||||
presenter.present()
|
|
||||||
}
|
|
||||||
}.test {
|
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
val updatedState = awaitItem()
|
val updatedState = awaitItem()
|
||||||
assertThat(updatedState.roomName).isEqualTo(roomInfo.name)
|
assertThat(updatedState.roomName).isEqualTo(roomInfo.name)
|
||||||
|
|
@ -143,11 +145,7 @@ class RoomDetailsPresenterTests {
|
||||||
fun `present - initial state with no room name`() = runTest {
|
fun `present - initial state with no room name`() = runTest {
|
||||||
val room = aMatrixRoom(name = null)
|
val room = aMatrixRoom(name = null)
|
||||||
val presenter = createRoomDetailsPresenter(room)
|
val presenter = createRoomDetailsPresenter(room)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
withFakeLifecycleOwner(fakeLifecycleOwner) {
|
|
||||||
presenter.present()
|
|
||||||
}
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.roomName).isEqualTo(room.displayName)
|
assertThat(initialState.roomName).isEqualTo(room.displayName)
|
||||||
|
|
||||||
|
|
@ -167,11 +165,7 @@ class RoomDetailsPresenterTests {
|
||||||
givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers))
|
givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers))
|
||||||
}
|
}
|
||||||
val presenter = createRoomDetailsPresenter(room)
|
val presenter = createRoomDetailsPresenter(room)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
withFakeLifecycleOwner(fakeLifecycleOwner) {
|
|
||||||
presenter.present()
|
|
||||||
}
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.roomType).isEqualTo(RoomDetailsType.Dm(otherRoomMember))
|
assertThat(initialState.roomType).isEqualTo(RoomDetailsType.Dm(otherRoomMember))
|
||||||
|
|
||||||
|
|
@ -185,11 +179,7 @@ class RoomDetailsPresenterTests {
|
||||||
givenCanInviteResult(Result.success(true))
|
givenCanInviteResult(Result.success(true))
|
||||||
}
|
}
|
||||||
val presenter = createRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers())
|
val presenter = createRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers())
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
withFakeLifecycleOwner(fakeLifecycleOwner) {
|
|
||||||
presenter.present()
|
|
||||||
}
|
|
||||||
}.test {
|
|
||||||
// Initially false
|
// Initially false
|
||||||
assertThat(awaitItem().canInvite).isFalse()
|
assertThat(awaitItem().canInvite).isFalse()
|
||||||
// Then the asynchronous check completes and it becomes true
|
// Then the asynchronous check completes and it becomes true
|
||||||
|
|
@ -205,11 +195,7 @@ class RoomDetailsPresenterTests {
|
||||||
givenCanInviteResult(Result.success(false))
|
givenCanInviteResult(Result.success(false))
|
||||||
}
|
}
|
||||||
val presenter = createRoomDetailsPresenter(room)
|
val presenter = createRoomDetailsPresenter(room)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
withFakeLifecycleOwner(fakeLifecycleOwner) {
|
|
||||||
presenter.present()
|
|
||||||
}
|
|
||||||
}.test {
|
|
||||||
assertThat(awaitItem().canInvite).isFalse()
|
assertThat(awaitItem().canInvite).isFalse()
|
||||||
|
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
|
@ -222,11 +208,7 @@ class RoomDetailsPresenterTests {
|
||||||
givenCanInviteResult(Result.failure(Throwable("Whoops")))
|
givenCanInviteResult(Result.failure(Throwable("Whoops")))
|
||||||
}
|
}
|
||||||
val presenter = createRoomDetailsPresenter(room)
|
val presenter = createRoomDetailsPresenter(room)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
withFakeLifecycleOwner(fakeLifecycleOwner) {
|
|
||||||
presenter.present()
|
|
||||||
}
|
|
||||||
}.test {
|
|
||||||
assertThat(awaitItem().canInvite).isFalse()
|
assertThat(awaitItem().canInvite).isFalse()
|
||||||
|
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
|
@ -242,11 +224,7 @@ class RoomDetailsPresenterTests {
|
||||||
givenCanInviteResult(Result.success(false))
|
givenCanInviteResult(Result.success(false))
|
||||||
}
|
}
|
||||||
val presenter = createRoomDetailsPresenter(room)
|
val presenter = createRoomDetailsPresenter(room)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
withFakeLifecycleOwner(fakeLifecycleOwner) {
|
|
||||||
presenter.present()
|
|
||||||
}
|
|
||||||
}.test {
|
|
||||||
// Initially false
|
// Initially false
|
||||||
assertThat(awaitItem().canEdit).isFalse()
|
assertThat(awaitItem().canEdit).isFalse()
|
||||||
// Then the asynchronous check completes and it becomes true
|
// Then the asynchronous check completes and it becomes true
|
||||||
|
|
@ -273,11 +251,7 @@ class RoomDetailsPresenterTests {
|
||||||
givenCanInviteResult(Result.success(false))
|
givenCanInviteResult(Result.success(false))
|
||||||
}
|
}
|
||||||
val presenter = createRoomDetailsPresenter(room)
|
val presenter = createRoomDetailsPresenter(room)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
withFakeLifecycleOwner(fakeLifecycleOwner) {
|
|
||||||
presenter.present()
|
|
||||||
}
|
|
||||||
}.test {
|
|
||||||
// Initially false
|
// Initially false
|
||||||
assertThat(awaitItem().canEdit).isFalse()
|
assertThat(awaitItem().canEdit).isFalse()
|
||||||
// Then the asynchronous check completes, but editing is still disallowed because it's a DM
|
// Then the asynchronous check completes, but editing is still disallowed because it's a DM
|
||||||
|
|
@ -305,11 +279,7 @@ class RoomDetailsPresenterTests {
|
||||||
givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.success(true))
|
givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.success(true))
|
||||||
}
|
}
|
||||||
val presenter = createRoomDetailsPresenter(room)
|
val presenter = createRoomDetailsPresenter(room)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
withFakeLifecycleOwner(fakeLifecycleOwner) {
|
|
||||||
presenter.present()
|
|
||||||
}
|
|
||||||
}.test {
|
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
||||||
// There's no topic, so we hide the entire UI for DMs
|
// There's no topic, so we hide the entire UI for DMs
|
||||||
|
|
@ -328,11 +298,7 @@ class RoomDetailsPresenterTests {
|
||||||
givenCanInviteResult(Result.success(false))
|
givenCanInviteResult(Result.success(false))
|
||||||
}
|
}
|
||||||
val presenter = createRoomDetailsPresenter(room)
|
val presenter = createRoomDetailsPresenter(room)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
withFakeLifecycleOwner(fakeLifecycleOwner) {
|
|
||||||
presenter.present()
|
|
||||||
}
|
|
||||||
}.test {
|
|
||||||
// Initially false
|
// Initially false
|
||||||
assertThat(awaitItem().canEdit).isFalse()
|
assertThat(awaitItem().canEdit).isFalse()
|
||||||
// Then the asynchronous check completes and it becomes true
|
// Then the asynchronous check completes and it becomes true
|
||||||
|
|
@ -351,11 +317,7 @@ class RoomDetailsPresenterTests {
|
||||||
givenCanInviteResult(Result.success(false))
|
givenCanInviteResult(Result.success(false))
|
||||||
}
|
}
|
||||||
val presenter = createRoomDetailsPresenter(room)
|
val presenter = createRoomDetailsPresenter(room)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
withFakeLifecycleOwner(fakeLifecycleOwner) {
|
|
||||||
presenter.present()
|
|
||||||
}
|
|
||||||
}.test {
|
|
||||||
// Initially false, and no further events
|
// Initially false, and no further events
|
||||||
assertThat(awaitItem().canEdit).isFalse()
|
assertThat(awaitItem().canEdit).isFalse()
|
||||||
|
|
||||||
|
|
@ -371,11 +333,7 @@ class RoomDetailsPresenterTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
val presenter = createRoomDetailsPresenter(room)
|
val presenter = createRoomDetailsPresenter(room)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
withFakeLifecycleOwner(fakeLifecycleOwner) {
|
|
||||||
presenter.present()
|
|
||||||
}
|
|
||||||
}.test {
|
|
||||||
// The initial state is "hidden" and no further state changes happen
|
// The initial state is "hidden" and no further state changes happen
|
||||||
assertThat(awaitItem().roomTopic).isEqualTo(RoomTopicState.Hidden)
|
assertThat(awaitItem().roomTopic).isEqualTo(RoomTopicState.Hidden)
|
||||||
|
|
||||||
|
|
@ -392,11 +350,7 @@ class RoomDetailsPresenterTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
val presenter = createRoomDetailsPresenter(room)
|
val presenter = createRoomDetailsPresenter(room)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
withFakeLifecycleOwner(fakeLifecycleOwner) {
|
|
||||||
presenter.present()
|
|
||||||
}
|
|
||||||
}.test {
|
|
||||||
// Ignore the initial state
|
// Ignore the initial state
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
||||||
|
|
@ -416,11 +370,7 @@ class RoomDetailsPresenterTests {
|
||||||
leaveRoomPresenter = leaveRoomPresenter,
|
leaveRoomPresenter = leaveRoomPresenter,
|
||||||
dispatchers = testCoroutineDispatchers()
|
dispatchers = testCoroutineDispatchers()
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
withFakeLifecycleOwner(fakeLifecycleOwner) {
|
|
||||||
presenter.present()
|
|
||||||
}
|
|
||||||
}.test {
|
|
||||||
awaitItem().eventSink(RoomDetailsEvent.LeaveRoom)
|
awaitItem().eventSink(RoomDetailsEvent.LeaveRoom)
|
||||||
|
|
||||||
assertThat(leaveRoomPresenter.events).contains(LeaveRoomEvent.ShowConfirmation(room.roomId))
|
assertThat(leaveRoomPresenter.events).contains(LeaveRoomEvent.ShowConfirmation(room.roomId))
|
||||||
|
|
@ -439,11 +389,7 @@ class RoomDetailsPresenterTests {
|
||||||
leaveRoomPresenter = leaveRoomPresenter,
|
leaveRoomPresenter = leaveRoomPresenter,
|
||||||
notificationSettingsService = notificationSettingsService,
|
notificationSettingsService = notificationSettingsService,
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
withFakeLifecycleOwner(fakeLifecycleOwner) {
|
|
||||||
presenter.present()
|
|
||||||
}
|
|
||||||
}.test {
|
|
||||||
notificationSettingsService.setRoomNotificationMode(room.roomId, RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY)
|
notificationSettingsService.setRoomNotificationMode(room.roomId, RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY)
|
||||||
val updatedState = consumeItemsUntilPredicate {
|
val updatedState = consumeItemsUntilPredicate {
|
||||||
it.roomNotificationSettings?.mode == RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY
|
it.roomNotificationSettings?.mode == RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY
|
||||||
|
|
@ -458,11 +404,7 @@ class RoomDetailsPresenterTests {
|
||||||
val notificationSettingsService = FakeNotificationSettingsService(initialRoomMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY)
|
val notificationSettingsService = FakeNotificationSettingsService(initialRoomMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY)
|
||||||
val room = aMatrixRoom(notificationSettingsService = notificationSettingsService)
|
val room = aMatrixRoom(notificationSettingsService = notificationSettingsService)
|
||||||
val presenter = createRoomDetailsPresenter(room = room, notificationSettingsService = notificationSettingsService)
|
val presenter = createRoomDetailsPresenter(room = room, notificationSettingsService = notificationSettingsService)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
withFakeLifecycleOwner(fakeLifecycleOwner) {
|
|
||||||
presenter.present()
|
|
||||||
}
|
|
||||||
}.test {
|
|
||||||
awaitItem().eventSink(RoomDetailsEvent.MuteNotification)
|
awaitItem().eventSink(RoomDetailsEvent.MuteNotification)
|
||||||
val updatedState = consumeItemsUntilPredicate(timeout = 250.milliseconds) {
|
val updatedState = consumeItemsUntilPredicate(timeout = 250.milliseconds) {
|
||||||
it.roomNotificationSettings?.mode == RoomNotificationMode.MUTE
|
it.roomNotificationSettings?.mode == RoomNotificationMode.MUTE
|
||||||
|
|
@ -480,11 +422,7 @@ class RoomDetailsPresenterTests {
|
||||||
)
|
)
|
||||||
val room = aMatrixRoom(notificationSettingsService = notificationSettingsService)
|
val room = aMatrixRoom(notificationSettingsService = notificationSettingsService)
|
||||||
val presenter = createRoomDetailsPresenter(room = room, notificationSettingsService = notificationSettingsService)
|
val presenter = createRoomDetailsPresenter(room = room, notificationSettingsService = notificationSettingsService)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
withFakeLifecycleOwner(fakeLifecycleOwner) {
|
|
||||||
presenter.present()
|
|
||||||
}
|
|
||||||
}.test {
|
|
||||||
awaitItem().eventSink(RoomDetailsEvent.UnmuteNotification)
|
awaitItem().eventSink(RoomDetailsEvent.UnmuteNotification)
|
||||||
val updatedState = consumeItemsUntilPredicate {
|
val updatedState = consumeItemsUntilPredicate {
|
||||||
it.roomNotificationSettings?.mode == RoomNotificationMode.ALL_MESSAGES
|
it.roomNotificationSettings?.mode == RoomNotificationMode.ALL_MESSAGES
|
||||||
|
|
@ -493,6 +431,37 @@ class RoomDetailsPresenterTests {
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - when set is favorite event is emitted, then the action is called`() = runTest {
|
||||||
|
val room = FakeMatrixRoom()
|
||||||
|
val presenter = createRoomDetailsPresenter(room = room)
|
||||||
|
presenter.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink(RoomDetailsEvent.SetFavorite(true))
|
||||||
|
assertThat(room.setIsFavoriteCalls).isEqualTo(listOf(true))
|
||||||
|
initialState.eventSink(RoomDetailsEvent.SetFavorite(false))
|
||||||
|
assertThat(room.setIsFavoriteCalls).isEqualTo(listOf(true, false))
|
||||||
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - changes in room info updates the is favorite flag`() = runTest {
|
||||||
|
val room = aMatrixRoom()
|
||||||
|
val presenter = createRoomDetailsPresenter(room = room)
|
||||||
|
presenter.test {
|
||||||
|
room.givenRoomInfo(aRoomInfo(isFavorite = true))
|
||||||
|
consumeItemsUntilPredicate { it.isFavorite }.last().let { state ->
|
||||||
|
assertThat(state.isFavorite).isTrue()
|
||||||
|
}
|
||||||
|
room.givenRoomInfo(aRoomInfo(isFavorite = false))
|
||||||
|
consumeItemsUntilPredicate { !it.isFavorite }.last().let { state ->
|
||||||
|
assertThat(state.isFavorite).isFalse()
|
||||||
|
}
|
||||||
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aMatrixRoom(
|
fun aMatrixRoom(
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
@Composable
|
@Composable
|
||||||
fun RoomListContextMenu(
|
fun RoomListContextMenu(
|
||||||
contextMenu: RoomListState.ContextMenu.Shown,
|
contextMenu: RoomListState.ContextMenu.Shown,
|
||||||
eventSink: (RoomListEvents.RoomListBottomSheetEvents) -> Unit,
|
eventSink: (RoomListEvents.ContextMenuEvents) -> Unit,
|
||||||
onRoomSettingsClicked: (roomId: RoomId) -> Unit,
|
onRoomSettingsClicked: (roomId: RoomId) -> Unit,
|
||||||
) {
|
) {
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
|
|
@ -64,7 +64,10 @@ fun RoomListContextMenu(
|
||||||
onLeaveRoomClicked = {
|
onLeaveRoomClicked = {
|
||||||
eventSink(RoomListEvents.HideContextMenu)
|
eventSink(RoomListEvents.HideContextMenu)
|
||||||
eventSink(RoomListEvents.LeaveRoom(contextMenu.roomId))
|
eventSink(RoomListEvents.LeaveRoom(contextMenu.roomId))
|
||||||
}
|
},
|
||||||
|
onFavoriteChanged = { isFavorite ->
|
||||||
|
eventSink(RoomListEvents.SetRoomIsFavorite(contextMenu.roomId, isFavorite))
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -72,10 +75,11 @@ fun RoomListContextMenu(
|
||||||
@Composable
|
@Composable
|
||||||
private fun RoomListModalBottomSheetContent(
|
private fun RoomListModalBottomSheetContent(
|
||||||
contextMenu: RoomListState.ContextMenu.Shown,
|
contextMenu: RoomListState.ContextMenu.Shown,
|
||||||
onRoomMarkReadClicked: () -> Unit,
|
|
||||||
onRoomMarkUnreadClicked: () -> Unit,
|
|
||||||
onRoomSettingsClicked: () -> Unit,
|
onRoomSettingsClicked: () -> Unit,
|
||||||
onLeaveRoomClicked: () -> Unit,
|
onLeaveRoomClicked: () -> Unit,
|
||||||
|
onFavoriteChanged: (isFavorite: Boolean) -> Unit,
|
||||||
|
onRoomMarkReadClicked: () -> Unit,
|
||||||
|
onRoomMarkUnreadClicked: () -> Unit,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
|
@ -120,6 +124,30 @@ private fun RoomListModalBottomSheetContent(
|
||||||
style = ListItemStyle.Primary,
|
style = ListItemStyle.Primary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = CommonStrings.common_favourite),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingContent = ListItemContent.Icon(
|
||||||
|
iconSource = IconSource.Vector(
|
||||||
|
CompoundIcons.Favourite(),
|
||||||
|
contentDescription = stringResource(id = CommonStrings.common_favourite),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
trailingContent = ListItemContent.Switch(
|
||||||
|
checked = contextMenu.isFavorite,
|
||||||
|
onChange = { isFavorite ->
|
||||||
|
onFavoriteChanged(isFavorite)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
onClick = {
|
||||||
|
onFavoriteChanged(!contextMenu.isFavorite)
|
||||||
|
},
|
||||||
|
style = ListItemStyle.Primary,
|
||||||
|
)
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -170,7 +198,8 @@ internal fun RoomListModalBottomSheetContentPreview() = ElementPreview {
|
||||||
onRoomMarkReadClicked = {},
|
onRoomMarkReadClicked = {},
|
||||||
onRoomMarkUnreadClicked = {},
|
onRoomMarkUnreadClicked = {},
|
||||||
onRoomSettingsClicked = {},
|
onRoomSettingsClicked = {},
|
||||||
onLeaveRoomClicked = {}
|
onLeaveRoomClicked = {},
|
||||||
|
onFavoriteChanged = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -182,6 +211,7 @@ internal fun RoomListModalBottomSheetContentForDmPreview() = ElementPreview {
|
||||||
onRoomMarkReadClicked = {},
|
onRoomMarkReadClicked = {},
|
||||||
onRoomMarkUnreadClicked = {},
|
onRoomMarkUnreadClicked = {},
|
||||||
onRoomSettingsClicked = {},
|
onRoomSettingsClicked = {},
|
||||||
onLeaveRoomClicked = {}
|
onLeaveRoomClicked = {},
|
||||||
|
onFavoriteChanged = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,10 @@ sealed interface RoomListEvents {
|
||||||
data object ToggleSearchResults : RoomListEvents
|
data object ToggleSearchResults : RoomListEvents
|
||||||
data class ShowContextMenu(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents
|
data class ShowContextMenu(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents
|
||||||
|
|
||||||
sealed interface RoomListBottomSheetEvents : RoomListEvents
|
sealed interface ContextMenuEvents : RoomListEvents
|
||||||
data object HideContextMenu : RoomListBottomSheetEvents
|
data object HideContextMenu : ContextMenuEvents
|
||||||
data class LeaveRoom(val roomId: RoomId) : RoomListBottomSheetEvents
|
data class LeaveRoom(val roomId: RoomId) : ContextMenuEvents
|
||||||
data class MarkAsRead(val roomId: RoomId) : RoomListBottomSheetEvents
|
data class MarkAsRead(val roomId: RoomId) : ContextMenuEvents
|
||||||
data class MarkAsUnread(val roomId: RoomId) : RoomListBottomSheetEvents
|
data class MarkAsUnread(val roomId: RoomId) : ContextMenuEvents
|
||||||
|
data class SetRoomIsFavorite(val roomId: RoomId, val isFavorite: Boolean) : ContextMenuEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
|
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
|
||||||
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||||
|
|
@ -51,7 +52,14 @@ 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.user.getCurrentUser
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.takeWhile
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -111,15 +119,11 @@ class RoomListPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val markAsUnreadFeatureFlagEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.MarkAsUnread)
|
|
||||||
.collectAsState(initial = null)
|
|
||||||
|
|
||||||
// Avatar indicator
|
// Avatar indicator
|
||||||
val showAvatarIndicator by indicatorService.showRoomListTopBarIndicator()
|
val showAvatarIndicator by indicatorService.showRoomListTopBarIndicator()
|
||||||
|
|
||||||
var displaySearchResults by rememberSaveable { mutableStateOf(false) }
|
var displaySearchResults by rememberSaveable { mutableStateOf(false) }
|
||||||
|
val contextMenu = remember { mutableStateOf<RoomListState.ContextMenu>(RoomListState.ContextMenu.Hidden) }
|
||||||
var contextMenu by remember { mutableStateOf<RoomListState.ContextMenu>(RoomListState.ContextMenu.Hidden) }
|
|
||||||
|
|
||||||
fun handleEvents(event: RoomListEvents) {
|
fun handleEvents(event: RoomListEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
|
|
@ -134,16 +138,18 @@ class RoomListPresenter @Inject constructor(
|
||||||
displaySearchResults = !displaySearchResults
|
displaySearchResults = !displaySearchResults
|
||||||
}
|
}
|
||||||
is RoomListEvents.ShowContextMenu -> {
|
is RoomListEvents.ShowContextMenu -> {
|
||||||
contextMenu = RoomListState.ContextMenu.Shown(
|
coroutineScope.showContextMenu(event, contextMenu)
|
||||||
roomId = event.roomListRoomSummary.roomId,
|
}
|
||||||
roomName = event.roomListRoomSummary.name,
|
is RoomListEvents.HideContextMenu -> {
|
||||||
isDm = event.roomListRoomSummary.isDm,
|
contextMenu.value = RoomListState.ContextMenu.Hidden
|
||||||
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.LeaveRoom -> leaveRoomState.eventSink(LeaveRoomEvent.ShowConfirmation(event.roomId))
|
||||||
|
|
||||||
|
is RoomListEvents.SetRoomIsFavorite -> coroutineScope.launch {
|
||||||
|
client.getRoom(event.roomId)?.use { room ->
|
||||||
|
room.setIsFavorite(event.isFavorite)
|
||||||
|
}
|
||||||
|
}
|
||||||
is RoomListEvents.MarkAsRead -> coroutineScope.launch {
|
is RoomListEvents.MarkAsRead -> coroutineScope.launch {
|
||||||
client.getRoom(event.roomId)?.use { room ->
|
client.getRoom(event.roomId)?.use { room ->
|
||||||
room.setUnreadFlag(isUnread = false)
|
room.setUnreadFlag(isUnread = false)
|
||||||
|
|
@ -177,7 +183,7 @@ class RoomListPresenter @Inject constructor(
|
||||||
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
|
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
|
||||||
invitesState = inviteStateDataSource.inviteState(),
|
invitesState = inviteStateDataSource.inviteState(),
|
||||||
displaySearchResults = displaySearchResults,
|
displaySearchResults = displaySearchResults,
|
||||||
contextMenu = contextMenu,
|
contextMenu = contextMenu.value,
|
||||||
leaveRoomState = leaveRoomState,
|
leaveRoomState = leaveRoomState,
|
||||||
displayMigrationStatus = isMigrating,
|
displayMigrationStatus = isMigrating,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
|
|
@ -188,6 +194,37 @@ class RoomListPresenter @Inject constructor(
|
||||||
matrixUser.value = client.getCurrentUser()
|
matrixUser.value = client.getCurrentUser()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
private fun CoroutineScope.showContextMenu(event: RoomListEvents.ShowContextMenu, contextMenuState: MutableState<RoomListState.ContextMenu>) = launch {
|
||||||
|
val initialState = RoomListState.ContextMenu.Shown(
|
||||||
|
roomId = event.roomListRoomSummary.roomId,
|
||||||
|
roomName = event.roomListRoomSummary.name,
|
||||||
|
isDm = event.roomListRoomSummary.isDm,
|
||||||
|
isFavorite = event.roomListRoomSummary.isFavorite,
|
||||||
|
markAsUnreadFeatureFlagEnabled = featureFlagService.isFeatureEnabled(FeatureFlags.MarkAsUnread),
|
||||||
|
hasNewContent = event.roomListRoomSummary.hasNewContent
|
||||||
|
)
|
||||||
|
contextMenuState.value = initialState
|
||||||
|
|
||||||
|
client.getRoom(event.roomListRoomSummary.roomId)?.use { room ->
|
||||||
|
|
||||||
|
val isShowingContextMenuFlow = snapshotFlow { contextMenuState.value is RoomListState.ContextMenu.Shown }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
|
||||||
|
val isFavoriteFlow = room.roomInfoFlow
|
||||||
|
.map { it.isFavorite }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
|
||||||
|
isFavoriteFlow
|
||||||
|
.onEach { isFavorite ->
|
||||||
|
contextMenuState.value = initialState.copy(isFavorite = isFavorite)
|
||||||
|
}
|
||||||
|
.flatMapLatest { isShowingContextMenuFlow }
|
||||||
|
.takeWhile { isShowingContextMenu -> isShowingContextMenu }
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateVisibleRange(range: IntRange) {
|
private fun updateVisibleRange(range: IntRange) {
|
||||||
if (range.isEmpty()) return
|
if (range.isEmpty()) return
|
||||||
val midExtendedRangeSize = EXTENDED_RANGE_SIZE / 2
|
val midExtendedRangeSize = EXTENDED_RANGE_SIZE / 2
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ data class RoomListState(
|
||||||
val roomId: RoomId,
|
val roomId: RoomId,
|
||||||
val roomName: String,
|
val roomName: String,
|
||||||
val isDm: Boolean,
|
val isDm: Boolean,
|
||||||
|
val isFavorite: Boolean,
|
||||||
val markAsUnreadFeatureFlagEnabled: Boolean,
|
val markAsUnreadFeatureFlagEnabled: Boolean,
|
||||||
val hasNewContent: Boolean,
|
val hasNewContent: Boolean,
|
||||||
) : ContextMenu
|
) : ContextMenu
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
|
||||||
aRoomListState().copy(displaySearchResults = true, filter = "", filteredRoomList = persistentListOf()),
|
aRoomListState().copy(displaySearchResults = true, filter = "", filteredRoomList = persistentListOf()),
|
||||||
aRoomListState().copy(displaySearchResults = true),
|
aRoomListState().copy(displaySearchResults = true),
|
||||||
aRoomListState().copy(contextMenu = aContextMenuShown(roomName = "A nice room name")),
|
aRoomListState().copy(contextMenu = aContextMenuShown(roomName = "A nice room name")),
|
||||||
|
aRoomListState().copy(contextMenu = aContextMenuShown(isFavorite = true)),
|
||||||
aRoomListState().copy(displayRecoveryKeyPrompt = true),
|
aRoomListState().copy(displayRecoveryKeyPrompt = true),
|
||||||
aRoomListState().copy(roomList = AsyncData.Success(persistentListOf())),
|
aRoomListState().copy(roomList = AsyncData.Success(persistentListOf())),
|
||||||
aRoomListState().copy(roomList = AsyncData.Loading(prevData = RoomListRoomSummaryFactory.createFakeList())),
|
aRoomListState().copy(roomList = AsyncData.Loading(prevData = RoomListRoomSummaryFactory.createFakeList())),
|
||||||
|
|
@ -102,10 +103,12 @@ internal fun aContextMenuShown(
|
||||||
roomName: String = "aRoom",
|
roomName: String = "aRoom",
|
||||||
isDm: Boolean = false,
|
isDm: Boolean = false,
|
||||||
hasNewContent: Boolean = false,
|
hasNewContent: Boolean = false,
|
||||||
|
isFavorite: Boolean = false,
|
||||||
) = RoomListState.ContextMenu.Shown(
|
) = RoomListState.ContextMenu.Shown(
|
||||||
roomId = RoomId("!aRoom:aDomain"),
|
roomId = RoomId("!aRoom:aDomain"),
|
||||||
roomName = roomName,
|
roomName = roomName,
|
||||||
isDm = isDm,
|
isDm = isDm,
|
||||||
markAsUnreadFeatureFlagEnabled = true,
|
markAsUnreadFeatureFlagEnabled = true,
|
||||||
hasNewContent = hasNewContent,
|
hasNewContent = hasNewContent,
|
||||||
|
isFavorite = isFavorite,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ class RoomListRoomSummaryFactory @Inject constructor(
|
||||||
userDefinedNotificationMode = null,
|
userDefinedNotificationMode = null,
|
||||||
hasRoomCall = false,
|
hasRoomCall = false,
|
||||||
isDm = false,
|
isDm = false,
|
||||||
|
isFavorite = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,6 +85,7 @@ class RoomListRoomSummaryFactory @Inject constructor(
|
||||||
userDefinedNotificationMode = roomSummary.details.userDefinedNotificationMode,
|
userDefinedNotificationMode = roomSummary.details.userDefinedNotificationMode,
|
||||||
hasRoomCall = roomSummary.details.hasRoomCall,
|
hasRoomCall = roomSummary.details.hasRoomCall,
|
||||||
isDm = roomSummary.details.isDm,
|
isDm = roomSummary.details.isDm,
|
||||||
|
isFavorite = roomSummary.details.isFavorite,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ data class RoomListRoomSummary(
|
||||||
val userDefinedNotificationMode: RoomNotificationMode?,
|
val userDefinedNotificationMode: RoomNotificationMode?,
|
||||||
val hasRoomCall: Boolean,
|
val hasRoomCall: Boolean,
|
||||||
val isDm: Boolean,
|
val isDm: Boolean,
|
||||||
|
val isFavorite: Boolean,
|
||||||
) {
|
) {
|
||||||
val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE &&
|
val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE &&
|
||||||
(numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0) ||
|
(numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0) ||
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,7 @@ internal fun aRoomListRoomSummary(
|
||||||
hasRoomCall: Boolean = false,
|
hasRoomCall: Boolean = false,
|
||||||
avatarData: AvatarData = AvatarData(id, name, size = AvatarSize.RoomListItem),
|
avatarData: AvatarData = AvatarData(id, name, size = AvatarSize.RoomListItem),
|
||||||
isDm: Boolean = false,
|
isDm: Boolean = false,
|
||||||
|
isFavorite: Boolean = false,
|
||||||
) = RoomListRoomSummary(
|
) = RoomListRoomSummary(
|
||||||
id = id,
|
id = id,
|
||||||
roomId = RoomId(id),
|
roomId = RoomId(id),
|
||||||
|
|
@ -112,4 +113,5 @@ internal fun aRoomListRoomSummary(
|
||||||
userDefinedNotificationMode = notificationMode,
|
userDefinedNotificationMode = notificationMode,
|
||||||
hasRoomCall = hasRoomCall,
|
hasRoomCall = hasRoomCall,
|
||||||
isDm = isDm,
|
isDm = isDm,
|
||||||
|
isFavorite = isFavorite,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ package io.element.android.features.roomlist.impl
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import io.element.android.tests.testutils.EnsureCalledOnceWithParam
|
import io.element.android.tests.testutils.EnsureCalledOnceWithParam
|
||||||
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
||||||
|
|
@ -128,4 +129,24 @@ class RoomListContextMenuTest {
|
||||||
eventsRecorder.assertSingle(RoomListEvents.HideContextMenu)
|
eventsRecorder.assertSingle(RoomListEvents.HideContextMenu)
|
||||||
callback.assertSuccess()
|
callback.assertSuccess()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on Favourites generates expected Event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
||||||
|
val contextMenu = aContextMenuShown(isDm = false, isFavorite = false)
|
||||||
|
val callback = EnsureNeverCalledWithParam<RoomId>()
|
||||||
|
rule.setContent {
|
||||||
|
RoomListContextMenu(
|
||||||
|
contextMenu = contextMenu,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
onRoomSettingsClicked = callback,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
rule.clickOn(CommonStrings.common_favourite)
|
||||||
|
eventsRecorder.assertList(
|
||||||
|
listOf(
|
||||||
|
RoomListEvents.SetRoomIsFavorite(contextMenu.roomId, true),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
||||||
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
|
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.FakeMatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled
|
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.roomlist.FakeRoomListService
|
||||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||||
|
|
@ -340,26 +341,49 @@ class RoomListPresenterTests {
|
||||||
@Test
|
@Test
|
||||||
fun `present - show context menu`() = runTest {
|
fun `present - show context menu`() = runTest {
|
||||||
val scope = CoroutineScope(coroutineContext + SupervisorJob())
|
val scope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||||
val presenter = createRoomListPresenter(coroutineScope = scope)
|
val room = FakeMatrixRoom()
|
||||||
|
val client = FakeMatrixClient().apply {
|
||||||
|
givenGetRoomResult(A_ROOM_ID, room)
|
||||||
|
}
|
||||||
|
val presenter = createRoomListPresenter(client = client, coroutineScope = scope)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
val summary = createRoomListRoomSummary()
|
val summary = createRoomListRoomSummary()
|
||||||
initialState.eventSink(RoomListEvents.ShowContextMenu(summary))
|
initialState.eventSink(RoomListEvents.ShowContextMenu(summary))
|
||||||
|
|
||||||
val shownState = awaitItem()
|
awaitItem().also { state ->
|
||||||
assertThat(shownState.contextMenu).isEqualTo(
|
assertThat(state.contextMenu)
|
||||||
RoomListState.ContextMenu.Shown(
|
.isEqualTo(
|
||||||
roomId = summary.roomId,
|
RoomListState.ContextMenu.Shown(
|
||||||
roomName = summary.name,
|
roomId = summary.roomId,
|
||||||
isDm = false,
|
roomName = summary.name,
|
||||||
markAsUnreadFeatureFlagEnabled = true,
|
isDm = false,
|
||||||
hasNewContent = false,
|
isFavorite = false,
|
||||||
)
|
markAsUnreadFeatureFlagEnabled = true,
|
||||||
|
hasNewContent = false,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
room.givenRoomInfo(
|
||||||
|
aRoomInfo(isFavorite = true)
|
||||||
)
|
)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.contextMenu)
|
||||||
|
.isEqualTo(
|
||||||
|
RoomListState.ContextMenu.Shown(
|
||||||
|
roomId = summary.roomId,
|
||||||
|
roomName = summary.name,
|
||||||
|
isDm = false,
|
||||||
|
isFavorite = true,
|
||||||
|
markAsUnreadFeatureFlagEnabled = true,
|
||||||
|
hasNewContent = false,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -367,7 +391,11 @@ class RoomListPresenterTests {
|
||||||
@Test
|
@Test
|
||||||
fun `present - hide context menu`() = runTest {
|
fun `present - hide context menu`() = runTest {
|
||||||
val scope = CoroutineScope(coroutineContext + SupervisorJob())
|
val scope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||||
val presenter = createRoomListPresenter(coroutineScope = scope)
|
val room = FakeMatrixRoom()
|
||||||
|
val client = FakeMatrixClient().apply {
|
||||||
|
givenGetRoomResult(A_ROOM_ID, room)
|
||||||
|
}
|
||||||
|
val presenter = createRoomListPresenter(client = client, coroutineScope = scope)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -378,15 +406,18 @@ class RoomListPresenterTests {
|
||||||
initialState.eventSink(RoomListEvents.ShowContextMenu(summary))
|
initialState.eventSink(RoomListEvents.ShowContextMenu(summary))
|
||||||
|
|
||||||
val shownState = awaitItem()
|
val shownState = awaitItem()
|
||||||
assertThat(shownState.contextMenu).isEqualTo(
|
assertThat(shownState.contextMenu)
|
||||||
RoomListState.ContextMenu.Shown(
|
.isEqualTo(
|
||||||
roomId = summary.roomId,
|
RoomListState.ContextMenu.Shown(
|
||||||
roomName = summary.name,
|
roomId = summary.roomId,
|
||||||
isDm = false,
|
roomName = summary.name,
|
||||||
markAsUnreadFeatureFlagEnabled = true,
|
isDm = false,
|
||||||
hasNewContent = false,
|
isFavorite = false,
|
||||||
|
markAsUnreadFeatureFlagEnabled = true,
|
||||||
|
hasNewContent = false,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
shownState.eventSink(RoomListEvents.HideContextMenu)
|
shownState.eventSink(RoomListEvents.HideContextMenu)
|
||||||
|
|
||||||
val hiddenState = awaitItem()
|
val hiddenState = awaitItem()
|
||||||
|
|
@ -440,6 +471,26 @@ class RoomListPresenterTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
fun `present - when set is favorite event is emitted, then the action is called`() = runTest {
|
||||||
|
val scope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||||
|
val room = FakeMatrixRoom()
|
||||||
|
val client = FakeMatrixClient().apply {
|
||||||
|
givenGetRoomResult(A_ROOM_ID, room)
|
||||||
|
}
|
||||||
|
val presenter = createRoomListPresenter(client = client, coroutineScope = scope)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink(RoomListEvents.SetRoomIsFavorite(A_ROOM_ID, true))
|
||||||
|
assertThat(room.setIsFavoriteCalls).isEqualTo(listOf(true))
|
||||||
|
initialState.eventSink(RoomListEvents.SetRoomIsFavorite(A_ROOM_ID, false))
|
||||||
|
assertThat(room.setIsFavoriteCalls).isEqualTo(listOf(true, false))
|
||||||
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
scope.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun `present - change in migration presenter state modifies isMigrating`() = runTest {
|
fun `present - change in migration presenter state modifies isMigrating`() = runTest {
|
||||||
val client = FakeMatrixClient(sessionId = A_SESSION_ID)
|
val client = FakeMatrixClient(sessionId = A_SESSION_ID)
|
||||||
val migrationStore = InMemoryMigrationScreenStore()
|
val migrationStore = InMemoryMigrationScreenStore()
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,7 @@ internal fun createRoomListRoomSummary(
|
||||||
numberOfUnreadNotifications: Int = 0,
|
numberOfUnreadNotifications: Int = 0,
|
||||||
isMarkedUnread: Boolean = false,
|
isMarkedUnread: Boolean = false,
|
||||||
userDefinedNotificationMode: RoomNotificationMode? = null,
|
userDefinedNotificationMode: RoomNotificationMode? = null,
|
||||||
|
isFavorite: Boolean = false,
|
||||||
) = RoomListRoomSummary(
|
) = RoomListRoomSummary(
|
||||||
id = A_ROOM_ID.value,
|
id = A_ROOM_ID.value,
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
|
|
@ -95,4 +96,5 @@ internal fun createRoomListRoomSummary(
|
||||||
userDefinedNotificationMode = userDefinedNotificationMode,
|
userDefinedNotificationMode = userDefinedNotificationMode,
|
||||||
hasRoomCall = false,
|
hasRoomCall = false,
|
||||||
isDm = false,
|
isDm = false,
|
||||||
|
isFavorite = isFavorite,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,8 @@ interface MatrixRoom : Closeable {
|
||||||
|
|
||||||
suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result<Unit>
|
suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result<Unit>
|
||||||
|
|
||||||
|
suspend fun setIsFavorite(isFavorite: Boolean): Result<Unit>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark the room as read by trying to attach an unthreaded read receipt to the latest room event.
|
* Mark the room as read by trying to attach an unthreaded read receipt to the latest room event.
|
||||||
* @param receiptType The type of receipt to send.
|
* @param receiptType The type of receipt to send.
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ data class MatrixRoomInfo(
|
||||||
val isPublic: Boolean,
|
val isPublic: Boolean,
|
||||||
val isSpace: Boolean,
|
val isSpace: Boolean,
|
||||||
val isTombstoned: Boolean,
|
val isTombstoned: Boolean,
|
||||||
|
val isFavorite: Boolean,
|
||||||
val canonicalAlias: String?,
|
val canonicalAlias: String?,
|
||||||
val alternativeAliases: ImmutableList<String>,
|
val alternativeAliases: ImmutableList<String>,
|
||||||
val currentUserMembership: CurrentUserMembership,
|
val currentUserMembership: CurrentUserMembership,
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ data class RoomSummaryDetails(
|
||||||
val userDefinedNotificationMode: RoomNotificationMode?,
|
val userDefinedNotificationMode: RoomNotificationMode?,
|
||||||
val hasRoomCall: Boolean,
|
val hasRoomCall: Boolean,
|
||||||
val isDm: Boolean,
|
val isDm: Boolean,
|
||||||
|
val isFavorite: Boolean,
|
||||||
) {
|
) {
|
||||||
val lastMessageTimestamp = lastMessage?.originServerTs
|
val lastMessageTimestamp = lastMessage?.originServerTs
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ class MatrixRoomInfoMapper(
|
||||||
isPublic = it.isPublic,
|
isPublic = it.isPublic,
|
||||||
isSpace = it.isSpace,
|
isSpace = it.isSpace,
|
||||||
isTombstoned = it.isTombstoned,
|
isTombstoned = it.isTombstoned,
|
||||||
|
isFavorite = it.isFavourite,
|
||||||
canonicalAlias = it.canonicalAlias,
|
canonicalAlias = it.canonicalAlias,
|
||||||
alternativeAliases = it.alternativeAliases.toImmutableList(),
|
alternativeAliases = it.alternativeAliases.toImmutableList(),
|
||||||
currentUserMembership = it.membership.map(),
|
currentUserMembership = it.membership.map(),
|
||||||
|
|
|
||||||
|
|
@ -442,6 +442,12 @@ class RustMatrixRoom(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun setIsFavorite(isFavorite: Boolean): Result<Unit> = withContext(roomDispatcher) {
|
||||||
|
runCatching {
|
||||||
|
innerRoom.setIsFavourite(isFavorite, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun markAsRead(receiptType: ReceiptType): Result<Unit> = withContext(roomDispatcher) {
|
override suspend fun markAsRead(receiptType: ReceiptType): Result<Unit> = withContext(roomDispatcher) {
|
||||||
runCatching {
|
runCatching {
|
||||||
innerRoom.markAsRead(receiptType.toRustReceiptType())
|
innerRoom.markAsRead(receiptType.toRustReceiptType())
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ class RoomSummaryDetailsFactory(private val roomMessageFactory: RoomMessageFacto
|
||||||
userDefinedNotificationMode = roomInfo.userDefinedNotificationMode?.let(RoomNotificationSettingsMapper::mapMode),
|
userDefinedNotificationMode = roomInfo.userDefinedNotificationMode?.let(RoomNotificationSettingsMapper::mapMode),
|
||||||
hasRoomCall = roomInfo.hasRoomCall,
|
hasRoomCall = roomInfo.hasRoomCall,
|
||||||
isDm = roomInfo.isDirect && roomInfo.activeMembersCount.toLong() == 2L,
|
isDm = roomInfo.isDirect && roomInfo.activeMembersCount.toLong() == 2L,
|
||||||
|
isFavorite = roomInfo.isFavourite,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,7 @@ class FakeMatrixRoom(
|
||||||
private var getWidgetDriverResult: Result<MatrixWidgetDriver> = Result.success(FakeWidgetDriver())
|
private var getWidgetDriverResult: Result<MatrixWidgetDriver> = Result.success(FakeWidgetDriver())
|
||||||
private var canUserTriggerRoomNotificationResult: Result<Boolean> = Result.success(true)
|
private var canUserTriggerRoomNotificationResult: Result<Boolean> = Result.success(true)
|
||||||
private var canUserJoinCallResult: Result<Boolean> = Result.success(true)
|
private var canUserJoinCallResult: Result<Boolean> = Result.success(true)
|
||||||
|
private var setIsFavoriteResult = Result.success(Unit)
|
||||||
var sendMessageMentions = emptyList<Mention>()
|
var sendMessageMentions = emptyList<Mention>()
|
||||||
val editMessageCalls = mutableListOf<Pair<String, String?>>()
|
val editMessageCalls = mutableListOf<Pair<String, String?>>()
|
||||||
private val _typingRecord = mutableListOf<Boolean>()
|
private val _typingRecord = mutableListOf<Boolean>()
|
||||||
|
|
@ -378,6 +379,14 @@ class FakeMatrixRoom(
|
||||||
return reportContentResult
|
return reportContentResult
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val setIsFavoriteCalls = mutableListOf<Boolean>()
|
||||||
|
|
||||||
|
override suspend fun setIsFavorite(isFavorite: Boolean): Result<Unit> {
|
||||||
|
return setIsFavoriteResult.also {
|
||||||
|
setIsFavoriteCalls.add(isFavorite)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val markAsReadCalls = mutableListOf<ReceiptType>()
|
val markAsReadCalls = mutableListOf<ReceiptType>()
|
||||||
|
|
||||||
override suspend fun markAsRead(receiptType: ReceiptType): Result<Unit> {
|
override suspend fun markAsRead(receiptType: ReceiptType): Result<Unit> {
|
||||||
|
|
@ -590,6 +599,10 @@ class FakeMatrixRoom(
|
||||||
getWidgetDriverResult = result
|
getWidgetDriverResult = result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun givenSetIsFavoriteResult(result: Result<Unit>) {
|
||||||
|
setIsFavoriteResult = result
|
||||||
|
}
|
||||||
|
|
||||||
fun givenRoomInfo(roomInfo: MatrixRoomInfo) {
|
fun givenRoomInfo(roomInfo: MatrixRoomInfo) {
|
||||||
_roomInfoFlow.tryEmit(roomInfo)
|
_roomInfoFlow.tryEmit(roomInfo)
|
||||||
}
|
}
|
||||||
|
|
@ -633,6 +646,7 @@ fun aRoomInfo(
|
||||||
isPublic: Boolean = true,
|
isPublic: Boolean = true,
|
||||||
isSpace: Boolean = false,
|
isSpace: Boolean = false,
|
||||||
isTombstoned: Boolean = false,
|
isTombstoned: Boolean = false,
|
||||||
|
isFavorite: Boolean = false,
|
||||||
canonicalAlias: String? = null,
|
canonicalAlias: String? = null,
|
||||||
alternativeAliases: List<String> = emptyList(),
|
alternativeAliases: List<String> = emptyList(),
|
||||||
currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED,
|
currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED,
|
||||||
|
|
@ -655,6 +669,7 @@ fun aRoomInfo(
|
||||||
isPublic = isPublic,
|
isPublic = isPublic,
|
||||||
isSpace = isSpace,
|
isSpace = isSpace,
|
||||||
isTombstoned = isTombstoned,
|
isTombstoned = isTombstoned,
|
||||||
|
isFavorite = isFavorite,
|
||||||
canonicalAlias = canonicalAlias,
|
canonicalAlias = canonicalAlias,
|
||||||
alternativeAliases = alternativeAliases.toImmutableList(),
|
alternativeAliases = alternativeAliases.toImmutableList(),
|
||||||
currentUserMembership = currentUserMembership,
|
currentUserMembership = currentUserMembership,
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ fun aRoomSummaryDetails(
|
||||||
canonicalAlias: String? = null,
|
canonicalAlias: String? = null,
|
||||||
hasRoomCall: Boolean = false,
|
hasRoomCall: Boolean = false,
|
||||||
isDm: Boolean = false,
|
isDm: Boolean = false,
|
||||||
|
isFavorite: Boolean = false,
|
||||||
) = RoomSummaryDetails(
|
) = RoomSummaryDetails(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
name = name,
|
name = name,
|
||||||
|
|
@ -83,6 +84,7 @@ fun aRoomSummaryDetails(
|
||||||
canonicalAlias = canonicalAlias,
|
canonicalAlias = canonicalAlias,
|
||||||
hasRoomCall = hasRoomCall,
|
hasRoomCall = hasRoomCall,
|
||||||
isDm = isDm,
|
isDm = isDm,
|
||||||
|
isFavorite = isFavorite,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun aRoomMessage(
|
fun aRoomMessage(
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,7 @@ fun aRoomSummaryDetails(
|
||||||
numUnreadMessages: Int = 0,
|
numUnreadMessages: Int = 0,
|
||||||
numUnreadNotifications: Int = 0,
|
numUnreadNotifications: Int = 0,
|
||||||
isMarkedUnread: Boolean = false,
|
isMarkedUnread: Boolean = false,
|
||||||
|
isFavorite: Boolean = false,
|
||||||
) = RoomSummaryDetails(
|
) = RoomSummaryDetails(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
name = name,
|
name = name,
|
||||||
|
|
@ -132,4 +133,5 @@ fun aRoomSummaryDetails(
|
||||||
numUnreadMessages = numUnreadMessages,
|
numUnreadMessages = numUnreadMessages,
|
||||||
numUnreadNotifications = numUnreadNotifications,
|
numUnreadNotifications = numUnreadNotifications,
|
||||||
isMarkedUnread = isMarkedUnread,
|
isMarkedUnread = isMarkedUnread,
|
||||||
|
isFavorite = isFavorite,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f2dd92e3cf76367608276fd10030d161ff370294b474c3e08fbf6463508fb1b0
|
oid sha256:10c67ba7b16d81e030cb44719cc68a0a3fd57acba75e488a37c3bdf4f739cf95
|
||||||
size 42064
|
size 44537
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:bb09fb2b24e1597e07898562b29cee3e62ad63040ab77895c234a13f4836ab08
|
oid sha256:a17d073bf7c3e993c81e51e234c4f885d2264f6de8bf54a93306f14ee5a02a66
|
||||||
size 30442
|
size 33354
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a8e482480e6103d81fa6e08857c3308bde87255ec2bed55ea616eca1cf3941cc
|
oid sha256:ea52227dcdf0806e292971e1b7332db28964eadd242882a80b6486d691e9c2c9
|
||||||
size 32698
|
size 35219
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:63e7bf82b003a869d6ccfbc5057ddfe0f447bd7eb31801df828c410744d15223
|
oid sha256:0163f939dfc3103eff7ded67b2fbd31dd050f6a4150245c9f0fbf3c7d13daf3e
|
||||||
size 32008
|
size 34806
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:00ca19bd4146e4b7937627a9aadd900cbf928e809996d7e1c950c0ed03aa888f
|
oid sha256:ab7c7c6ae6951bf173f24fddb70e99feacd92286b088a6921900c6b7e881000a
|
||||||
size 39635
|
size 42122
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6ea77bd41442b6686fcbe53a4ea1bb29a6040a78ceb8f6e5453fddc0b4ae6b35
|
oid sha256:4063c0c9f763304aea54a73aaf95efc636d029cab45369b24a3bfc0b3bf3731d
|
||||||
size 39892
|
size 42774
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6ea77bd41442b6686fcbe53a4ea1bb29a6040a78ceb8f6e5453fddc0b4ae6b35
|
oid sha256:4063c0c9f763304aea54a73aaf95efc636d029cab45369b24a3bfc0b3bf3731d
|
||||||
size 39892
|
size 42774
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:beeb97ce692ae6592dd61769cfdf87f3fc3518548b02443e9ef6134e3dec01bb
|
oid sha256:dabfea9a1b7604977444e8fedc977a61ab5576c6115aa97bffd77ad1d5c65038
|
||||||
size 43479
|
size 46198
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7a5ef5d83bc9dd2e3bef092d6620baaf0f0f94657f41961bb1cf2b547ae10415
|
oid sha256:d1dfdb2a8cca036992e06a4d2a626c32d40c899e85bd5d5cf3aaba4272d339d8
|
||||||
size 41931
|
size 44244
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:fd7974c00bce66a8a1715e956c64aa578db303ff287aefb9dc5bb7906e4e27d5
|
||||||
|
size 44406
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1a6e83b3b6880ee6eec8731e42e744c0fe6f806cbad6c1925cb297e19c1b811b
|
oid sha256:258605b56cbfb5ade4ce390b7e38a37b0602612d45a751b0032e4ba757d8d76f
|
||||||
size 43222
|
size 45766
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e3d669d780e1b7a3c741d5f6c8f20e8ffe80b283291f133672221cddcca192dc
|
oid sha256:c984e6333a0db11f1e52f246f019b927d97910dd35f5b79aae6d7c4f3094f913
|
||||||
size 31530
|
size 34667
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ee48e602a928b83bb547efe6352c419d1bf71ea8490226884f715f3266d493e5
|
oid sha256:613b0bd1053c46b45fe6b58c754b03c69bab0a6cc2d90cd99278b4786f1253b0
|
||||||
size 34080
|
size 36681
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:22e8f719df1249e31f26022815930577f9baf135454ff260664fa65045c1b690
|
oid sha256:97416e171d87d7290924270c175b44a10c2b25fd3541156bf9e5258975f0c3a4
|
||||||
size 32687
|
size 35627
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ee5dde97c04f0ccdc21349158150e11ad1a1b91c97bfd3aaefacc4e0e1a17a8a
|
oid sha256:28a271552ea0e61954646c47bd8e87048c92cace7ca4176901c7b4d000bbc53f
|
||||||
size 40828
|
size 43375
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1bfd34cc11af2c77b25e19d4493db998d8cad0efed379dce3a9537b2c49fda28
|
oid sha256:7a4ddad1a6f2afad344e8e88fd297c91a5064206b2dbcb1130dd15b5fe22fbc9
|
||||||
size 41039
|
size 44086
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1bfd34cc11af2c77b25e19d4493db998d8cad0efed379dce3a9537b2c49fda28
|
oid sha256:7a4ddad1a6f2afad344e8e88fd297c91a5064206b2dbcb1130dd15b5fe22fbc9
|
||||||
size 41039
|
size 44086
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:567e1b09ae9383c8af71c7d66d2877091c0ce3d98eab1d92c7bc9d2f896875bc
|
oid sha256:36e579b569043783e8890a6cf5ffa1ae511c524c20f923765825ab9a9ccf9bb4
|
||||||
size 44629
|
size 47592
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:bb8863bcb78be69e715ec9bbf3ef7ee51fd142b484a52a22b381373708d5049e
|
oid sha256:2b27ecb48f63242353d750c5fb2eb5cf2fcb3c11edd046cc9c03db57f3d26122
|
||||||
size 43101
|
size 45472
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:6fb2205cfbb68ae2e808fe62765311b10921a8bd579e23803484a492de36d028
|
||||||
|
size 45645
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d636ef74890668b0d2cc49af71b1e3734237fa1f9944afb89d8152cb9a839cd4
|
oid sha256:404fc390042c6d7870e2fbef176eb497ac3fb474c53483c085cd04038d0192ef
|
||||||
size 17391
|
size 21918
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3c883b6e8c7ae3570f2d7370a929cc9afd2a1983bc6064f50090ad8e8c9295d4
|
oid sha256:4791d9b6fa50a29c10b618d27786033b69983229a9ce2e10b83e35cbe4a6365a
|
||||||
size 16289
|
size 20671
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b6b6fe672b31db43d1aeed7d0835d638b729c09ac01b03fcdfd814b2e1d861d5
|
oid sha256:d3b0fe4d5ca9f4b735ed43ea167c238d2086a2aa6f8760fe74841bc6f0a1be6c
|
||||||
size 15553
|
size 20289
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:178e6f2f9aa771831eb3f672cf212d5b3a37532c02dcc5126446965b49be7bdd
|
oid sha256:1d80d0580828e0f657ff2f36915c2d0a7a9d56612007da87c3b9676f882131bc
|
||||||
size 14558
|
size 19023
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:2523a28a889fd1739fe1e0c94edce5d86a5ebcbe6c3973a3c49fbb8fb8a19f79
|
oid sha256:5f8802121f779f48dd556a9a3a08e6ac1a63e86d5695f6f8c133fbb346d76ef6
|
||||||
size 55617
|
size 89779
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:13dadbd502163a9bfe81cb1e67f9aa0933c4b7f4fd3c8f731e930a28709485d2
|
oid sha256:2523a28a889fd1739fe1e0c94edce5d86a5ebcbe6c3973a3c49fbb8fb8a19f79
|
||||||
size 51948
|
size 55617
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:64c4eb481f40871925405ae317cb80927caf31cb552a8aa9549bfb5658ca91e4
|
oid sha256:13dadbd502163a9bfe81cb1e67f9aa0933c4b7f4fd3c8f731e930a28709485d2
|
||||||
size 137589
|
size 51948
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:64c4eb481f40871925405ae317cb80927caf31cb552a8aa9549bfb5658ca91e4
|
||||||
|
size 137589
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5f8802121f779f48dd556a9a3a08e6ac1a63e86d5695f6f8c133fbb346d76ef6
|
oid sha256:3764d8bd7dc2783a8af43aad65a217d7e533ed17c4d4367b7994470bf35b62b0
|
||||||
size 89779
|
size 4462
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:2b25e023153dd094299a0c794d5e488bdc6fc0c478abbfbce7337e52b233715d
|
oid sha256:7cdd93c157565f7f8d3fa23a43c30e02531dab40578a315c5d0d10a99e74f259
|
||||||
size 57463
|
size 91394
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:0a00391ef16a762a14dd7e348ce97b500ca1b42a32ee3ff926a10865f46cd06c
|
oid sha256:2b25e023153dd094299a0c794d5e488bdc6fc0c478abbfbce7337e52b233715d
|
||||||
size 53590
|
size 57463
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a7bf47b0a25c455b108d7b3585a42849f03c6652af73a175fe2147cb1ad62a66
|
oid sha256:0a00391ef16a762a14dd7e348ce97b500ca1b42a32ee3ff926a10865f46cd06c
|
||||||
size 161125
|
size 53590
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a7bf47b0a25c455b108d7b3585a42849f03c6652af73a175fe2147cb1ad62a66
|
||||||
|
size 161125
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7cdd93c157565f7f8d3fa23a43c30e02531dab40578a315c5d0d10a99e74f259
|
oid sha256:3764d8bd7dc2783a8af43aad65a217d7e533ed17c4d4367b7994470bf35b62b0
|
||||||
size 91394
|
size 4462
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue