Favorite : rework and add tests

This commit is contained in:
ganfra 2024-02-02 14:54:28 +01:00
parent a63d331f36
commit 7ca931c35f
21 changed files with 454 additions and 144 deletions

View file

@ -0,0 +1,27 @@
/*
* 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.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.features.roomactions.api"
}
dependencies {
implementation(projects.libraries.matrix.api)
}

View file

@ -0,0 +1,42 @@
/*
* 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.roomactions.api
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.MatrixRoom
/**
* Set the favorite status of a room.
* This will update the notable tags of the room.
*/
interface SetRoomIsFavoriteAction {
sealed interface Result {
data object Success : Result
data object RoomNotFound : Result
data class Exception(val inner: java.lang.Exception) : Result
}
/**
* Set the favorite status of a room by its id, it'll try to load the room from the session.
*/
suspend operator fun invoke(roomId: RoomId, isFavorite: Boolean): Result
/**
* Set the favorite status of a room using the provided instance.
*/
suspend operator fun invoke(room: MatrixRoom, isFavorite: Boolean): Result
}

View file

@ -0,0 +1,50 @@
/*
* 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.
*/
// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.anvil)
alias(libs.plugins.ksp)
id("kotlin-parcelize")
}
android {
namespace = "io.element.android.features.roomactions.impl"
}
anvil {
generateDaggerFactories.set(true)
}
dependencies {
implementation(projects.anvilannotations)
anvil(projects.anvilcodegen)
api(projects.features.roomactions.api)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test)
testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test)
ksp(libs.showkase.processor)
}

View file

@ -0,0 +1,56 @@
/*
* 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.roomactions.impl
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.roomactions.api.SetRoomIsFavoriteAction
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.MatrixRoom
import kotlinx.coroutines.flow.first
import javax.inject.Inject
import kotlin.coroutines.cancellation.CancellationException
@ContributesBinding(SessionScope::class)
class DefaultSetRoomIsFavoriteAction @Inject constructor(private val client: MatrixClient) : SetRoomIsFavoriteAction {
override suspend operator fun invoke(roomId: RoomId, isFavorite: Boolean): SetRoomIsFavoriteAction.Result {
return client.getRoom(roomId).use { room ->
room?.setIsFavorite(isFavorite) ?: SetRoomIsFavoriteAction.Result.RoomNotFound
}
}
override suspend fun invoke(room: MatrixRoom, isFavorite: Boolean): SetRoomIsFavoriteAction.Result {
return room.setIsFavorite(isFavorite)
}
private suspend fun MatrixRoom.setIsFavorite(isFavorite: Boolean): SetRoomIsFavoriteAction.Result {
val notableTags = notableTagsFlow.first().copy(isFavorite = isFavorite)
return updateNotableTags(notableTags).fold(
onSuccess = {
SetRoomIsFavoriteAction.Result.Success
},
onFailure = { throwable ->
if (throwable is Exception && throwable !is CancellationException) {
SetRoomIsFavoriteAction.Result.Exception(throwable)
} else {
throw throwable
}
}
)
}
}

View file

@ -0,0 +1,59 @@
/*
* 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.roomactions.impl
import io.element.android.features.roomactions.api.SetRoomIsFavoriteAction
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import kotlinx.coroutines.test.runTest
import org.junit.Test
class DefaultSetRoomIsFavoriteActionTests {
private val room = FakeMatrixRoom()
@Test
fun `given a room id and a client without rooms, when action is invoked, then it returns Result_RoomNotFound`() = runTest {
val action = DefaultSetRoomIsFavoriteAction(FakeMatrixClient())
val result = action(room.roomId, true)
assert(result is SetRoomIsFavoriteAction.Result.RoomNotFound)
}
@Test
fun `given a room, when action is invoked, then it returns Result_Success`() = runTest {
val action = DefaultSetRoomIsFavoriteAction(FakeMatrixClient())
val result = action(room, true)
assert(result is SetRoomIsFavoriteAction.Result.Success)
}
@Test
fun `given a room id and a client with a room, when action is invoked, then it returns Result_Success`() = runTest {
val client = FakeMatrixClient().apply {
givenGetRoomResult(room.roomId, room)
}
val action = DefaultSetRoomIsFavoriteAction(client)
val result = action(room.roomId, true)
assert(result is SetRoomIsFavoriteAction.Result.Success)
}
@Test
fun `given a room, when action is invoked and fail, then it returns Result_Exception`() = runTest {
val action = DefaultSetRoomIsFavoriteAction(FakeMatrixClient())
room.givenUpdateNotableTagsResult(Result.failure(Exception()))
val result = action(room, true)
assert(result is SetRoomIsFavoriteAction.Result.Exception)
}
}

View file

@ -0,0 +1,28 @@
/*
* Copyright (c) 2023 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.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.features.roomactions.test"
}
dependencies {
implementation(projects.features.roomactions.api)
implementation(projects.libraries.matrix.api)
implementation(libs.coroutines.core)
}

View file

@ -0,0 +1,39 @@
/*
* 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.roomactions.test
import io.element.android.features.roomactions.api.SetRoomIsFavoriteAction
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.MatrixRoom
class FakeSetRoomIsFavoriteAction : SetRoomIsFavoriteAction {
private var counter = 0
fun assertCalled(number: Int) {
assert(counter == number) { "Expected $number calls, got $counter" }
}
override suspend fun invoke(roomId: RoomId, isFavorite: Boolean): SetRoomIsFavoriteAction.Result {
counter++
return SetRoomIsFavoriteAction.Result.Success
}
override suspend fun invoke(room: MatrixRoom, isFavorite: Boolean): SetRoomIsFavoriteAction.Result {
counter++
return SetRoomIsFavoriteAction.Result.Success
}
}

View file

@ -54,6 +54,7 @@ dependencies {
implementation(projects.features.createroom.api) implementation(projects.features.createroom.api)
implementation(projects.services.analytics.api) implementation(projects.services.analytics.api)
implementation(projects.features.poll.api) implementation(projects.features.poll.api)
implementation(projects.features.roomactions.api)
testImplementation(libs.test.junit) testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test) testImplementation(libs.coroutines.test)
@ -70,6 +71,7 @@ dependencies {
testImplementation(projects.tests.testutils) testImplementation(projects.tests.testutils)
testImplementation(projects.features.leaveroom.test) testImplementation(projects.features.leaveroom.test)
testImplementation(projects.features.createroom.test) testImplementation(projects.features.createroom.test)
testImplementation(projects.features.roomactions.test)
ksp(libs.showkase.processor) ksp(libs.showkase.processor)
} }

View file

@ -29,6 +29,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
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.roomactions.api.SetRoomIsFavoriteAction
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.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
@ -44,7 +45,6 @@ import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.powerlevels.canInvite import io.element.android.libraries.matrix.api.room.powerlevels.canInvite
import io.element.android.libraries.matrix.api.room.powerlevels.canSendState import io.element.android.libraries.matrix.api.room.powerlevels.canSendState
import io.element.android.libraries.matrix.api.room.roomNotificationSettings import io.element.android.libraries.matrix.api.room.roomNotificationSettings
import io.element.android.libraries.matrix.api.room.tags.RoomNotableTags
import io.element.android.libraries.matrix.ui.room.getDirectRoomMember import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -61,6 +61,7 @@ class RoomDetailsPresenter @Inject constructor(
private val roomMembersDetailsPresenterFactory: RoomMemberDetailsPresenter.Factory, private val roomMembersDetailsPresenterFactory: RoomMemberDetailsPresenter.Factory,
private val leaveRoomPresenter: LeaveRoomPresenter, private val leaveRoomPresenter: LeaveRoomPresenter,
private val dispatchers: CoroutineDispatchers, private val dispatchers: CoroutineDispatchers,
private val setRoomIsFavorite: SetRoomIsFavoriteAction,
) : Presenter<RoomDetailsState> { ) : Presenter<RoomDetailsState> {
@Composable @Composable
override fun present(): RoomDetailsState { override fun present(): RoomDetailsState {
@ -126,9 +127,8 @@ class RoomDetailsPresenter @Inject constructor(
} }
} }
is RoomDetailsEvent.SetIsFavorite -> { is RoomDetailsEvent.SetIsFavorite -> {
scope.launch(dispatchers.io) { scope.launch {
val tags = RoomNotableTags(isFavorite = event.isFavorite) setRoomIsFavorite(room, event.isFavorite)
room.updateNotableTags(tags)
} }
} }
} }

View file

@ -19,14 +19,18 @@ 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
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.leaveroom.fake.FakeLeaveRoomPresenter import io.element.android.features.leaveroom.fake.FakeLeaveRoomPresenter
import io.element.android.features.roomactions.api.SetRoomIsFavoriteAction
import io.element.android.features.roomactions.test.FakeSetRoomIsFavoriteAction
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
@ -40,6 +44,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.tags.RoomNotableTags
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID
@ -71,10 +76,11 @@ 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(),
setRoomIsFavoriteAction: SetRoomIsFavoriteAction = FakeSetRoomIsFavoriteAction(),
): RoomDetailsPresenter { ): RoomDetailsPresenter {
val matrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService) val matrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService)
val roomMemberDetailsPresenterFactory = object : RoomMemberDetailsPresenter.Factory { val roomMemberDetailsPresenterFactory = object : RoomMemberDetailsPresenter.Factory {
@ -86,25 +92,30 @@ 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,
setRoomIsFavorite = setRoomIsFavoriteAction,
) )
} }
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 +135,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 +150,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 +170,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 +184,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 +200,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 +213,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 +229,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 +256,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 +284,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 +303,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 +322,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 +338,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 +355,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 +375,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 +394,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 +409,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 +427,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 +436,35 @@ class RoomDetailsPresenterTests {
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
} }
} }
@Test
fun `present - when set is favorite event is emitted, then the action is called`() = runTest {
val setRoomIsFavoriteAction = FakeSetRoomIsFavoriteAction()
val presenter = createRoomDetailsPresenter(setRoomIsFavoriteAction = setRoomIsFavoriteAction)
presenter.test {
val initialState = awaitItem()
initialState.eventSink(RoomDetailsEvent.SetIsFavorite(true))
setRoomIsFavoriteAction.assertCalled(1)
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - changes in notable tags updates is favorite flag`() = runTest {
val room = aMatrixRoom()
val presenter = createRoomDetailsPresenter(room = room)
presenter.test {
room.updateNotableTags(RoomNotableTags(true))
consumeItemsUntilPredicate { it.isFavorite }.last().let { state ->
assertThat(state.isFavorite).isTrue()
}
room.updateNotableTags(RoomNotableTags(false))
consumeItemsUntilPredicate { !it.isFavorite }.last().let { state ->
assertThat(state.isFavorite).isFalse()
}
cancelAndIgnoreRemainingEvents()
}
}
} }
fun aMatrixRoom( fun aMatrixRoom(

View file

@ -56,6 +56,7 @@ dependencies {
implementation(projects.features.networkmonitor.api) implementation(projects.features.networkmonitor.api)
implementation(projects.features.leaveroom.api) implementation(projects.features.leaveroom.api)
implementation(projects.services.analytics.api) implementation(projects.services.analytics.api)
implementation(projects.features.roomactions.api)
api(projects.features.roomlist.api) api(projects.features.roomlist.api)
ksp(libs.showkase.processor) ksp(libs.showkase.processor)
@ -75,4 +76,5 @@ dependencies {
testImplementation(projects.features.networkmonitor.test) testImplementation(projects.features.networkmonitor.test)
testImplementation(projects.tests.testutils) testImplementation(projects.tests.testutils)
testImplementation(projects.features.leaveroom.test) testImplementation(projects.features.leaveroom.test)
testImplementation(projects.features.roomactions.test)
} }

View file

@ -60,7 +60,7 @@ fun RoomListContextMenu(
eventSink(RoomListEvents.LeaveRoom(contextMenu.roomId)) eventSink(RoomListEvents.LeaveRoom(contextMenu.roomId))
}, },
onFavoriteChanged = { isFavorite -> onFavoriteChanged = { isFavorite ->
eventSink(RoomListEvents.MarkRoomAsFavorite(contextMenu.roomId, isFavorite)) eventSink(RoomListEvents.SetRoomIsFavorite(contextMenu.roomId, isFavorite))
}, },
) )
} }

View file

@ -28,5 +28,5 @@ sealed interface RoomListEvents {
data class ShowContextMenu(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents data class ShowContextMenu(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents
data object HideContextMenu : RoomListEvents data object HideContextMenu : RoomListEvents
data class LeaveRoom(val roomId: RoomId) : RoomListEvents data class LeaveRoom(val roomId: RoomId) : RoomListEvents
data class MarkRoomAsFavorite(val roomId: RoomId, val isFavorite: Boolean) : RoomListEvents data class SetRoomIsFavorite(val roomId: RoomId, val isFavorite: Boolean) : RoomListEvents
} }

View file

@ -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.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.roomactions.api.SetRoomIsFavoriteAction
import io.element.android.features.roomlist.impl.datasource.InviteStateDataSource 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.RoomListDataSource
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
@ -45,17 +46,14 @@ import io.element.android.libraries.indicator.api.IndicatorService
import io.element.android.libraries.matrix.api.MatrixClient 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.EncryptionService
import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.libraries.matrix.api.encryption.RecoveryState
import io.element.android.libraries.matrix.api.room.tags.RoomNotableTags
import io.element.android.libraries.matrix.api.user.MatrixUser 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.Job import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
private const val EXTENDED_RANGE_SIZE = 40 private const val EXTENDED_RANGE_SIZE = 40
@ -71,6 +69,7 @@ class RoomListPresenter @Inject constructor(
private val encryptionService: EncryptionService, private val encryptionService: EncryptionService,
private val featureFlagService: FeatureFlagService, private val featureFlagService: FeatureFlagService,
private val indicatorService: IndicatorService, private val indicatorService: IndicatorService,
private val setRoomIsFavorite: SetRoomIsFavoriteAction,
) : Presenter<RoomListState> { ) : Presenter<RoomListState> {
@Composable @Composable
override fun present(): RoomListState { override fun present(): RoomListState {
@ -134,7 +133,7 @@ class RoomListPresenter @Inject constructor(
contextMenu.value = RoomListState.ContextMenu.Hidden contextMenu.value = RoomListState.ContextMenu.Hidden
} }
is RoomListEvents.LeaveRoom -> leaveRoomState.eventSink(LeaveRoomEvent.ShowConfirmation(event.roomId)) is RoomListEvents.LeaveRoom -> leaveRoomState.eventSink(LeaveRoomEvent.ShowConfirmation(event.roomId))
is RoomListEvents.MarkRoomAsFavorite -> coroutineScope.markRoomAsFavorite(event) is RoomListEvents.SetRoomIsFavorite -> coroutineScope.setRoomIsFavorite(event)
} }
} }
@ -170,28 +169,23 @@ class RoomListPresenter @Inject constructor(
isFavorite = AsyncData.Loading(), isFavorite = AsyncData.Loading(),
) )
contextMenuState.value = initialState contextMenuState.value = initialState
val room = client.getRoom(event.roomListRoomSummary.roomId) client.getRoom(event.roomListRoomSummary.roomId).use { room ->
if (room != null) { if (room != null) {
room.notableTagsFlow room.notableTagsFlow
.distinctUntilChanged() .distinctUntilChanged()
.onEach { tags -> .onEach { tags ->
val newState = initialState.copy(isFavorite = AsyncData.Success(tags.isFavorite)) val newState = initialState.copy(isFavorite = AsyncData.Success(tags.isFavorite))
contextMenuState.value = newState contextMenuState.value = newState
} }
.launchIn(this) .collect()
} else { } else {
contextMenuState.value = initialState.copy(isFavorite = AsyncData.Failure(IllegalStateException("Room not found"))) contextMenuState.value = initialState.copy(isFavorite = AsyncData.Failure(IllegalStateException("Room not found")))
}
} }
} }
private fun CoroutineScope.markRoomAsFavorite(event: RoomListEvents.MarkRoomAsFavorite) = launch { private fun CoroutineScope.setRoomIsFavorite(event: RoomListEvents.SetRoomIsFavorite) = launch {
val room = client.getRoom(event.roomId) setRoomIsFavorite(event.roomId, event.isFavorite)
if (room != null) {
val notableTags = RoomNotableTags(isFavorite = event.isFavorite);
room.updateNotableTags(notableTags)
}else {
Timber.w("Room ${event.roomId} not found, can't mark as favorite");
}
} }
private fun updateVisibleRange(range: IntRange) { private fun updateVisibleRange(range: IntRange) {
@ -204,5 +198,3 @@ class RoomListPresenter @Inject constructor(
client.roomListService.updateAllRoomsVisibleRange(extendedRange) client.roomListService.updateAllRoomsVisibleRange(extendedRange)
} }
} }

View file

@ -25,11 +25,14 @@ 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.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
import io.element.android.features.roomactions.api.SetRoomIsFavoriteAction
import io.element.android.features.roomactions.test.FakeSetRoomIsFavoriteAction
import io.element.android.features.roomlist.impl.datasource.FakeInviteDataSource 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.InviteStateDataSource
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource 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.datasource.RoomListRoomSummaryFactory
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter 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.AvatarData
@ -44,6 +47,7 @@ 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.BackupState
import io.element.android.libraries.matrix.api.encryption.EncryptionService 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.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.tags.RoomNotableTags
import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.AN_AVATAR_URL
@ -55,6 +59,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.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.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
@ -309,19 +314,29 @@ 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 = aRoomListRoomSummary val summary = aRoomListRoomSummary
initialState.eventSink(RoomListEvents.ShowContextMenu(summary)) initialState.eventSink(RoomListEvents.ShowContextMenu(summary))
val shownState = awaitItem() awaitItem().also { state ->
assertThat(shownState.contextMenu) assertThat(state.contextMenu)
.isEqualTo(RoomListState.ContextMenu.Shown(summary.roomId, summary.name, false)) .isEqualTo(RoomListState.ContextMenu.Shown(summary.roomId, summary.name, false, AsyncData.Success(false)))
}
room.updateNotableTags(RoomNotableTags(isFavorite = true))
awaitItem().also { state ->
assertThat(state.contextMenu)
.isEqualTo(RoomListState.ContextMenu.Shown(summary.roomId, summary.name, false, AsyncData.Success(true)))
}
scope.cancel() scope.cancel()
} }
} }
@ -329,7 +344,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 {
@ -341,7 +360,7 @@ class RoomListPresenterTests {
val shownState = awaitItem() val shownState = awaitItem()
assertThat(shownState.contextMenu) assertThat(shownState.contextMenu)
.isEqualTo(RoomListState.ContextMenu.Shown(summary.roomId, summary.name, false)) .isEqualTo(RoomListState.ContextMenu.Shown(summary.roomId, summary.name, false, AsyncData.Success(false)))
shownState.eventSink(RoomListEvents.HideContextMenu) shownState.eventSink(RoomListEvents.HideContextMenu)
val hiddenState = awaitItem() val hiddenState = awaitItem()
@ -394,6 +413,22 @@ class RoomListPresenterTests {
} }
} }
@Test
fun `present - when set is favorite event is emitted, then the action is called`() = runTest {
val scope = CoroutineScope(coroutineContext + SupervisorJob())
val setRoomIsFavoriteAction = FakeSetRoomIsFavoriteAction()
val presenter = createRoomListPresenter(setRoomIsFavoriteAction = setRoomIsFavoriteAction, coroutineScope = scope)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(RoomListEvents.SetRoomIsFavorite(A_ROOM_ID, true))
setRoomIsFavoriteAction.assertCalled(1)
cancelAndIgnoreRemainingEvents()
scope.cancel()
}
}
private fun TestScope.createRoomListPresenter( private fun TestScope.createRoomListPresenter(
client: MatrixClient = FakeMatrixClient(), client: MatrixClient = FakeMatrixClient(),
sessionVerificationService: SessionVerificationService = FakeSessionVerificationService(), sessionVerificationService: SessionVerificationService = FakeSessionVerificationService(),
@ -407,6 +442,7 @@ class RoomListPresenterTests {
roomLastMessageFormatter: RoomLastMessageFormatter = FakeRoomLastMessageFormatter(), roomLastMessageFormatter: RoomLastMessageFormatter = FakeRoomLastMessageFormatter(),
encryptionService: EncryptionService = FakeEncryptionService(), encryptionService: EncryptionService = FakeEncryptionService(),
coroutineScope: CoroutineScope, coroutineScope: CoroutineScope,
setRoomIsFavoriteAction: SetRoomIsFavoriteAction = FakeSetRoomIsFavoriteAction(),
) = RoomListPresenter( ) = RoomListPresenter(
client = client, client = client,
sessionVerificationService = sessionVerificationService, sessionVerificationService = sessionVerificationService,
@ -431,6 +467,7 @@ class RoomListPresenterTests {
encryptionService = encryptionService, encryptionService = encryptionService,
featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.SecureStorage.key to true)), featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.SecureStorage.key to true)),
), ),
setRoomIsFavorite = setRoomIsFavoriteAction,
) )
} }

View file

@ -58,6 +58,9 @@ interface MatrixRoom : Closeable {
val roomInfoFlow: Flow<MatrixRoomInfo> val roomInfoFlow: Flow<MatrixRoomInfo>
/**
* The current notable tags as a Flow.
*/
val notableTagsFlow: Flow<RoomNotableTags> val notableTagsFlow: Flow<RoomNotableTags>
/** /**

View file

@ -16,6 +16,10 @@
package io.element.android.libraries.matrix.api.room.tags package io.element.android.libraries.matrix.api.room.tags
/**
* Represents the notable tags of a room.
* @param isFavorite true if the room is marked as favorite.
*/
data class RoomNotableTags( data class RoomNotableTags(
val isFavorite: Boolean, val isFavorite: Boolean = false,
) )

View file

@ -82,8 +82,8 @@ import org.matrix.rustcomponents.sdk.use
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import org.matrix.rustcomponents.sdk.Room as InnerRoom import org.matrix.rustcomponents.sdk.Room as InnerRoom
import uniffi.matrix_sdk_base.RoomNotableTags as RustRoomNotableTags
import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline
import uniffi.matrix_sdk_base.RoomNotableTags as RustRoomNotableTags
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class RustMatrixRoom( class RustMatrixRoom(
@ -118,7 +118,6 @@ class RustMatrixRoom(
override val notableTagsFlow: Flow<RoomNotableTags> = mxCallbackFlow { override val notableTagsFlow: Flow<RoomNotableTags> = mxCallbackFlow {
innerRoom.subscribeToNotableTags(object : RoomNotableTagsListener { innerRoom.subscribeToNotableTags(object : RoomNotableTagsListener {
override fun call(notableTags: RustRoomNotableTags) { override fun call(notableTags: RustRoomNotableTags) {
Timber.d("On notable tags update: $notableTags")
channel.trySend(notableTags.map()) channel.trySend(notableTags.map())
} }
}) })

View file

@ -25,7 +25,6 @@ import org.matrix.rustcomponents.sdk.RoomInfo
import org.matrix.rustcomponents.sdk.use import org.matrix.rustcomponents.sdk.use
class RoomSummaryDetailsFactory(private val roomMessageFactory: RoomMessageFactory = RoomMessageFactory()) { class RoomSummaryDetailsFactory(private val roomMessageFactory: RoomMessageFactory = RoomMessageFactory()) {
fun create(roomInfo: RoomInfo): RoomSummaryDetails { fun create(roomInfo: RoomInfo): RoomSummaryDetails {
val latestRoomMessage = roomInfo.latestEvent?.use { val latestRoomMessage = roomInfo.latestEvent?.use {
roomMessageFactory.create(it) roomMessageFactory.create(it)

View file

@ -28,7 +28,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSende
import io.element.android.libraries.matrix.api.timeline.item.event.Receipt import io.element.android.libraries.matrix.api.timeline.item.event.Receipt
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import uniffi.matrix_sdk_ui.EventItemOrigin as RustEventItemOrigin
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import org.matrix.rustcomponents.sdk.Reaction import org.matrix.rustcomponents.sdk.Reaction
@ -37,6 +36,7 @@ import org.matrix.rustcomponents.sdk.EventTimelineItem as RustEventTimelineItem
import org.matrix.rustcomponents.sdk.EventTimelineItemDebugInfo as RustEventTimelineItemDebugInfo import org.matrix.rustcomponents.sdk.EventTimelineItemDebugInfo as RustEventTimelineItemDebugInfo
import org.matrix.rustcomponents.sdk.ProfileDetails as RustProfileDetails import org.matrix.rustcomponents.sdk.ProfileDetails as RustProfileDetails
import org.matrix.rustcomponents.sdk.Receipt as RustReceipt import org.matrix.rustcomponents.sdk.Receipt as RustReceipt
import uniffi.matrix_sdk_ui.EventItemOrigin as RustEventItemOrigin
class EventTimelineItemMapper(private val contentMapper: TimelineEventContentMapper = TimelineEventContentMapper()) { class EventTimelineItemMapper(private val contentMapper: TimelineEventContentMapper = TimelineEventContentMapper()) {
fun map(eventTimelineItem: RustEventTimelineItem): EventTimelineItem = eventTimelineItem.use { fun map(eventTimelineItem: RustEventTimelineItem): EventTimelineItem = eventTimelineItem.use {

View file

@ -627,7 +627,6 @@ 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,