From abca950636b615dc36ab8b35e701472a05bfc7b6 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 4 Apr 2024 20:36:16 +0200 Subject: [PATCH 01/25] Room navigation : add a JoinedRoomFlowNode so we use RoomFlowNode for managing different routes --- .../android/appnav/LoggedInFlowNode.kt | 15 +- .../android/appnav/room/RoomFlowNode.kt | 102 +++++++------ .../appnav/room/RoomNavigationTarget.kt | 23 +++ .../appnav/room/joined/JoinedRoomFlowNode.kt | 138 ++++++++++++++++++ .../JoinedRoomLoadedFlowNode.kt} | 27 ++-- .../room/{ => joined}/LoadingRoomNodeView.kt | 4 +- .../room/{ => joined}/LoadingRoomState.kt | 4 +- .../android/appnav/RoomFlowNodeTest.kt | 18 +-- .../room/LoadingRoomStateFlowFactoryTest.kt | 2 + .../matrix/api/roomlist/RoomListService.kt | 15 ++ .../impl/roomlist/RustRoomListService.kt | 28 ++++ 11 files changed, 298 insertions(+), 78 deletions(-) create mode 100644 appnav/src/main/kotlin/io/element/android/appnav/room/RoomNavigationTarget.kt create mode 100644 appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomFlowNode.kt rename appnav/src/main/kotlin/io/element/android/appnav/room/{RoomLoadedFlowNode.kt => joined/JoinedRoomLoadedFlowNode.kt} (90%) rename appnav/src/main/kotlin/io/element/android/appnav/room/{ => joined}/LoadingRoomNodeView.kt (97%) rename appnav/src/main/kotlin/io/element/android/appnav/room/{ => joined}/LoadingRoomState.kt (96%) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index 2027f2868a..8ff7ecfaa8 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -42,7 +42,8 @@ import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.appnav.loggedin.LoggedInNode import io.element.android.appnav.room.RoomFlowNode -import io.element.android.appnav.room.RoomLoadedFlowNode +import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode +import io.element.android.appnav.room.RoomNavigationTarget import io.element.android.features.createroom.api.CreateRoomEntryPoint import io.element.android.features.ftue.api.FtueEntryPoint import io.element.android.features.ftue.api.state.FtueService @@ -213,7 +214,7 @@ class LoggedInFlowNode @AssistedInject constructor( @Parcelize data class Room( val roomId: RoomId, - val initialElement: RoomLoadedFlowNode.NavTarget = RoomLoadedFlowNode.NavTarget.Messages + val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages ) : NavTarget @Parcelize @@ -273,7 +274,7 @@ class LoggedInFlowNode @AssistedInject constructor( } override fun onRoomSettingsClicked(roomId: RoomId) { - backstack.push(NavTarget.Room(roomId, initialElement = RoomLoadedFlowNode.NavTarget.RoomDetails)) + backstack.push(NavTarget.Room(roomId, initialElement = RoomNavigationTarget.Details)) } override fun onReportBugClicked() { @@ -290,7 +291,7 @@ class LoggedInFlowNode @AssistedInject constructor( .build() } is NavTarget.Room -> { - val callback = object : RoomLoadedFlowNode.Callback { + val callback = object : JoinedRoomLoadedFlowNode.Callback { override fun onOpenRoom(roomId: RoomId) { backstack.push(NavTarget.Room(roomId)) } @@ -317,7 +318,7 @@ class LoggedInFlowNode @AssistedInject constructor( } override fun onOpenRoomNotificationSettings(roomId: RoomId) { - backstack.push(NavTarget.Room(roomId, initialElement = RoomLoadedFlowNode.NavTarget.RoomNotificationSettings)) + backstack.push(NavTarget.Room(roomId, initialElement = RoomNavigationTarget.NotificationSettings)) } } val inputs = PreferencesEntryPoint.Params(navTarget.initialElement) @@ -349,6 +350,10 @@ class LoggedInFlowNode @AssistedInject constructor( backstack.pop() } + override fun onInviteClicked(roomId: RoomId) { + backstack.push(NavTarget.Room(roomId)) + } + override fun onInviteAccepted(roomId: RoomId) { backstack.push(NavTarget.Room(roomId)) } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt index b45207b034..dd53d6168b 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 New Vector Ltd + * 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. @@ -14,15 +14,13 @@ * limitations under the License. */ -@file:OptIn(ExperimentalMaterial3Api::class) - package io.element.android.appnav.room import android.os.Parcelable -import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.lifecycle.lifecycleScope import com.bumble.appyx.core.modality.BuildContext @@ -36,26 +34,36 @@ import com.bumble.appyx.navmodel.backstack.operation.newRoot import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.appnav.room.joined.JoinedRoomFlowNode +import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode import io.element.android.features.networkmonitor.api.NetworkMonitor -import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.core.RoomId -import kotlinx.coroutines.flow.distinctUntilChanged +import io.element.android.libraries.matrix.api.room.CurrentUserMembership +import io.element.android.libraries.matrix.api.room.RoomMembershipObserver +import io.element.android.libraries.matrix.api.roomlist.RoomListService +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.parcelize.Parcelize +import timber.log.Timber +import kotlin.jvm.optionals.getOrNull @ContributesNode(SessionScope::class) class RoomFlowNode @AssistedInject constructor( @Assisted val buildContext: BuildContext, @Assisted plugins: List, - loadingRoomStateFlowFactory: LoadingRoomStateFlowFactory, + private val roomListService: RoomListService, + private val roomMembershipObserver: RoomMembershipObserver, private val networkMonitor: NetworkMonitor, ) : BaseFlowNode( @@ -68,64 +76,70 @@ class RoomFlowNode @AssistedInject constructor( ) { data class Inputs( val roomId: RoomId, - val initialElement: RoomLoadedFlowNode.NavTarget = RoomLoadedFlowNode.NavTarget.Messages, + val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages, ) : NodeInputs private val inputs: Inputs = inputs() - private val loadingRoomStateStateFlow = loadingRoomStateFlowFactory.create(lifecycleScope, inputs.roomId) sealed interface NavTarget : Parcelable { @Parcelize data object Loading : NavTarget @Parcelize - data object Loaded : NavTarget + data object JoinRoom : NavTarget + + @Parcelize + data object JoinedRoom : NavTarget } override fun onBuilt() { super.onBuilt() - loadingRoomStateStateFlow - .map { - it is LoadingRoomState.Loaded - } - .distinctUntilChanged() - .onEach { isLoaded -> - if (isLoaded) { - backstack.newRoot(NavTarget.Loaded) - } else { - backstack.newRoot(NavTarget.Loading) + roomListService.getUserMembershipForRoom( + inputs.roomId + ).onEach { membership -> + Timber.d("RoomMembership = $membership") + when { + membership.getOrNull() == CurrentUserMembership.JOINED -> { + backstack.newRoot(NavTarget.JoinedRoom) } + else -> { + backstack.newRoot(NavTarget.JoinRoom) + } + } + } + .flowOn(Dispatchers.Default) + .launchIn(lifecycleScope) + + roomMembershipObserver.updates + .filter { update -> update.roomId == inputs.roomId && !update.isUserInRoom } + .onEach { + navigateUp() } .launchIn(lifecycleScope) } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { - NavTarget.Loaded -> { - val roomFlowNodeCallback = plugins() - val awaitRoomState = loadingRoomStateStateFlow.value - if (awaitRoomState is LoadingRoomState.Loaded) { - val inputs = RoomLoadedFlowNode.Inputs(awaitRoomState.room, initialElement = inputs.initialElement) - createNode(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback) - } else { - loadingNode(buildContext, this::navigateUp) - } - } - NavTarget.Loading -> { - loadingNode(buildContext, this::navigateUp) + NavTarget.Loading -> loadingNode(buildContext) + NavTarget.JoinRoom -> joinRoomNode(buildContext) + NavTarget.JoinedRoom -> { + val roomFlowNodeCallback = plugins() + val inputs = JoinedRoomFlowNode.Inputs(inputs.roomId, initialElement = inputs.initialElement) + createNode(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback) } } } - private fun loadingNode(buildContext: BuildContext, onBackClicked: () -> Unit) = node(buildContext) { modifier -> - val loadingRoomState by loadingRoomStateStateFlow.collectAsState() - val networkStatus by networkMonitor.connectivity.collectAsState() - LoadingRoomNodeView( - state = loadingRoomState, - hasNetworkConnection = networkStatus == NetworkStatus.Online, - modifier = modifier, - onBackClicked = onBackClicked - ) + private fun loadingNode(buildContext: BuildContext) = node(buildContext) { + Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } + + private fun joinRoomNode(buildContext: BuildContext) = node(buildContext) { + Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) { + Text("Unknown Room") + } } @Composable diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomNavigationTarget.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomNavigationTarget.kt new file mode 100644 index 0000000000..901b2667be --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomNavigationTarget.kt @@ -0,0 +1,23 @@ +/* + * 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.appnav.room + +enum class RoomNavigationTarget { + Messages, + Details, + NotificationSettings, +} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomFlowNode.kt new file mode 100644 index 0000000000..36def888ac --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomFlowNode.kt @@ -0,0 +1,138 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalMaterial3Api::class) + +package io.element.android.appnav.room.joined + +import android.os.Parcelable +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.lifecycleScope +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.navigation.transition.JumpToEndTransitionHandler +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.node.node +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins +import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.newRoot +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.appnav.room.RoomNavigationTarget +import io.element.android.features.networkmonitor.api.NetworkMonitor +import io.element.android.features.networkmonitor.api.NetworkStatus +import io.element.android.libraries.architecture.BackstackView +import io.element.android.libraries.architecture.BaseFlowNode +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.core.RoomId +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.parcelize.Parcelize + +@ContributesNode(SessionScope::class) +class JoinedRoomFlowNode @AssistedInject constructor( + @Assisted val buildContext: BuildContext, + @Assisted plugins: List, + loadingRoomStateFlowFactory: LoadingRoomStateFlowFactory, + private val networkMonitor: NetworkMonitor, +) : + BaseFlowNode( + backstack = BackStack( + initialElement = NavTarget.Loading, + savedStateMap = buildContext.savedStateMap, + ), + buildContext = buildContext, + plugins = plugins + ) { + data class Inputs( + val roomId: RoomId, + val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages, + ) : NodeInputs + + private val inputs: Inputs = inputs() + private val loadingRoomStateStateFlow = loadingRoomStateFlowFactory.create(lifecycleScope, inputs.roomId) + + sealed interface NavTarget : Parcelable { + @Parcelize + data object Loading : NavTarget + + @Parcelize + data object Loaded : NavTarget + } + + override fun onBuilt() { + super.onBuilt() + loadingRoomStateStateFlow + .map { + it is LoadingRoomState.Loaded + } + .distinctUntilChanged() + .onEach { isLoaded -> + if (isLoaded) { + backstack.newRoot(NavTarget.Loaded) + } else { + backstack.newRoot(NavTarget.Loading) + } + } + .launchIn(lifecycleScope) + } + + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { + return when (navTarget) { + NavTarget.Loaded -> { + val roomFlowNodeCallback = plugins() + val awaitRoomState = loadingRoomStateStateFlow.value + if (awaitRoomState is LoadingRoomState.Loaded) { + val inputs = JoinedRoomLoadedFlowNode.Inputs(awaitRoomState.room, initialElement = inputs.initialElement) + createNode(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback) + } else { + loadingNode(buildContext, this::navigateUp) + } + } + NavTarget.Loading -> { + loadingNode(buildContext, this::navigateUp) + } + } + } + + private fun loadingNode(buildContext: BuildContext, onBackClicked: () -> Unit) = node(buildContext) { modifier -> + val loadingRoomState by loadingRoomStateStateFlow.collectAsState() + val networkStatus by networkMonitor.connectivity.collectAsState() + LoadingRoomNodeView( + state = loadingRoomState, + hasNetworkConnection = networkStatus == NetworkStatus.Online, + modifier = modifier, + onBackClicked = onBackClicked + ) + } + + @Composable + override fun View(modifier: Modifier) { + BackstackView( + transitionHandler = JumpToEndTransitionHandler(), + ) + } +} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomLoadedFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt similarity index 90% rename from appnav/src/main/kotlin/io/element/android/appnav/room/RoomLoadedFlowNode.kt rename to appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt index 21e412ef63..ade1f7e147 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomLoadedFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 New Vector Ltd + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.appnav.room +package io.element.android.appnav.room.joined import android.os.Parcelable import androidx.compose.runtime.Composable @@ -32,6 +32,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.appnav.di.RoomComponentFactory +import io.element.android.appnav.room.RoomNavigationTarget import io.element.android.features.messages.api.MessagesEntryPoint import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint import io.element.android.libraries.architecture.BackstackView @@ -46,15 +47,12 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.services.appnavstate.api.AppNavigationStateService import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import timber.log.Timber @ContributesNode(SessionScope::class) -class RoomLoadedFlowNode @AssistedInject constructor( +class JoinedRoomLoadedFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val messagesEntryPoint: MessagesEntryPoint, @@ -63,9 +61,13 @@ class RoomLoadedFlowNode @AssistedInject constructor( private val appCoroutineScope: CoroutineScope, roomComponentFactory: RoomComponentFactory, roomMembershipObserver: RoomMembershipObserver, -) : BaseFlowNode( +) : BaseFlowNode( backstack = BackStack( - initialElement = plugins.filterIsInstance(Inputs::class.java).first().initialElement, + initialElement = when(plugins.filterIsInstance(Inputs::class.java).first().initialElement){ + RoomNavigationTarget.Messages -> NavTarget.Messages + RoomNavigationTarget.Details -> NavTarget.RoomDetails + RoomNavigationTarget.NotificationSettings -> NavTarget.RoomNotificationSettings + }, savedStateMap = buildContext.savedStateMap, ), buildContext = buildContext, @@ -79,7 +81,7 @@ class RoomLoadedFlowNode @AssistedInject constructor( data class Inputs( val room: MatrixRoom, - val initialElement: NavTarget = NavTarget.Messages, + val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages, ) : NodeInputs private val inputs: Inputs = inputs() @@ -108,13 +110,6 @@ class RoomLoadedFlowNode @AssistedInject constructor( appNavigationStateService.onLeavingRoom(id) } ) - roomMembershipObserver.updates - .filter { update -> update.roomId == inputs.room.roomId && !update.isUserInRoom } - .onEach { - navigateUp() - } - .launchIn(lifecycleScope) - inputs() } private fun fetchRoomMembers() = lifecycleScope.launch { diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/LoadingRoomNodeView.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomNodeView.kt similarity index 97% rename from appnav/src/main/kotlin/io/element/android/appnav/room/LoadingRoomNodeView.kt rename to appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomNodeView.kt index 8db8d608b2..14fb955cb5 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/LoadingRoomNodeView.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomNodeView.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 New Vector Ltd + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.appnav.room +package io.element.android.appnav.room.joined import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/LoadingRoomState.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomState.kt similarity index 96% rename from appnav/src/main/kotlin/io/element/android/appnav/room/LoadingRoomState.kt rename to appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomState.kt index 0798979812..dbc190354f 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/LoadingRoomState.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomState.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 New Vector Ltd + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.appnav.room +package io.element.android.appnav.room.joined import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.di.SessionScope diff --git a/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt index ecc6a5dad0..5d5c66b32b 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt @@ -27,7 +27,7 @@ import com.bumble.appyx.testing.junit4.util.MainDispatcherRule import com.bumble.appyx.testing.unit.common.helper.parentNodeTestHelper import com.google.common.truth.Truth.assertThat import io.element.android.appnav.di.RoomComponentFactory -import io.element.android.appnav.room.RoomLoadedFlowNode +import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode import io.element.android.features.messages.api.MessagesEntryPoint import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint import io.element.android.libraries.architecture.childNode @@ -92,7 +92,7 @@ class RoomFlowNodeTest { messagesEntryPoint: MessagesEntryPoint = FakeMessagesEntryPoint(), roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(), coroutineScope: CoroutineScope, - ) = RoomLoadedFlowNode( + ) = JoinedRoomLoadedFlowNode( buildContext = BuildContext.root(savedStateMap = null), plugins = plugins, messagesEntryPoint = messagesEntryPoint, @@ -108,7 +108,7 @@ class RoomFlowNodeTest { // GIVEN val room = FakeMatrixRoom() val fakeMessagesEntryPoint = FakeMessagesEntryPoint() - val inputs = RoomLoadedFlowNode.Inputs(room) + val inputs = JoinedRoomLoadedFlowNode.Inputs(room) val roomFlowNode = aRoomFlowNode( plugins = listOf(inputs), messagesEntryPoint = fakeMessagesEntryPoint, @@ -118,9 +118,9 @@ class RoomFlowNodeTest { val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper() // THEN - assertThat(roomFlowNode.backstack.activeElement).isEqualTo(RoomLoadedFlowNode.NavTarget.Messages) - roomFlowNodeTestHelper.assertChildHasLifecycle(RoomLoadedFlowNode.NavTarget.Messages, Lifecycle.State.CREATED) - val messagesNode = roomFlowNode.childNode(RoomLoadedFlowNode.NavTarget.Messages)!! + assertThat(roomFlowNode.backstack.activeElement).isEqualTo(JoinedRoomLoadedFlowNode.NavTarget.Messages) + roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.Messages, Lifecycle.State.CREATED) + val messagesNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.Messages)!! assertThat(messagesNode.id).isEqualTo(fakeMessagesEntryPoint.nodeId) } @@ -130,7 +130,7 @@ class RoomFlowNodeTest { val room = FakeMatrixRoom() val fakeMessagesEntryPoint = FakeMessagesEntryPoint() val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint() - val inputs = RoomLoadedFlowNode.Inputs(room) + val inputs = JoinedRoomLoadedFlowNode.Inputs(room) val roomFlowNode = aRoomFlowNode( plugins = listOf(inputs), messagesEntryPoint = fakeMessagesEntryPoint, @@ -141,8 +141,8 @@ class RoomFlowNodeTest { // WHEN fakeMessagesEntryPoint.callback?.onRoomDetailsClicked() // THEN - roomFlowNodeTestHelper.assertChildHasLifecycle(RoomLoadedFlowNode.NavTarget.RoomDetails, Lifecycle.State.CREATED) - val roomDetailsNode = roomFlowNode.childNode(RoomLoadedFlowNode.NavTarget.RoomDetails)!! + roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.RoomDetails, Lifecycle.State.CREATED) + val roomDetailsNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.RoomDetails)!! assertThat(roomDetailsNode.id).isEqualTo(fakeRoomDetailsEntryPoint.nodeId) } } diff --git a/appnav/src/test/kotlin/io/element/android/appnav/room/LoadingRoomStateFlowFactoryTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/room/LoadingRoomStateFlowFactoryTest.kt index 58e35b2d58..cb8ed2da32 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/room/LoadingRoomStateFlowFactoryTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/room/LoadingRoomStateFlowFactoryTest.kt @@ -18,6 +18,8 @@ package io.element.android.appnav.room import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import io.element.android.appnav.room.joined.LoadingRoomState +import io.element.android.appnav.room.joined.LoadingRoomStateFlowFactory import io.element.android.libraries.matrix.api.roomlist.RoomList import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt index 5c526870e5..4534f10a74 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt @@ -17,7 +17,15 @@ package io.element.android.libraries.matrix.api.roomlist import androidx.compose.runtime.Immutable +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.CurrentUserMembership +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import java.util.Optional /** * Entry point for the room list api. @@ -77,4 +85,11 @@ interface RoomListService { * The state of the service as a flow. */ val state: StateFlow + + /** + * Get a flow of the room summary for a given room id. + */ + fun getUserMembershipForRoom(roomId: RoomId): Flow> } + + diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt index 70310e472e..09e0a514b1 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt @@ -16,16 +16,24 @@ package io.element.android.libraries.matrix.impl.roomlist +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.roomlist.DynamicRoomList import io.element.android.libraries.matrix.api.roomlist.RoomList import io.element.android.libraries.matrix.api.roomlist.RoomListFilter import io.element.android.libraries.matrix.api.roomlist.RoomListService +import io.element.android.libraries.matrix.api.roomlist.awaitLoaded import io.element.android.libraries.matrix.api.roomlist.loadAllIncrementally +import io.element.android.libraries.matrix.impl.room.map import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn @@ -35,7 +43,9 @@ import org.matrix.rustcomponents.sdk.RoomListInput import org.matrix.rustcomponents.sdk.RoomListRange import org.matrix.rustcomponents.sdk.RoomListServiceState import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicator +import org.matrix.rustcomponents.sdk.use import timber.log.Timber +import java.util.Optional import org.matrix.rustcomponents.sdk.RoomListService as InnerRustRoomListService private const val DEFAULT_PAGE_SIZE = 20 @@ -112,6 +122,24 @@ internal class RustRoomListService( } .distinctUntilChanged() .stateIn(sessionCoroutineScope, SharingStarted.Eagerly, RoomListService.State.Idle) + + override fun getUserMembershipForRoom(roomId: RoomId): Flow> { + return combine( + allRooms.loadedStateFlow(), + invites.loadedStateFlow(), + ) { _, _ -> + val membership = innerRoomListService.roomOrNull(roomId.value)?.use { + it.roomInfo().use { roomInfo -> + roomInfo.membership.map() + } + } + Optional.ofNullable(membership) + }.distinctUntilChanged() + } + + private fun RoomList.loadedStateFlow(): Flow { + return loadingState.filterIsInstance() + } } private fun RoomListServiceState.toRoomListState(): RoomListService.State { From d4e7345a01fa7fc532a95fc76db35f5a3dd399df Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 5 Apr 2024 17:57:02 +0200 Subject: [PATCH 02/25] Room navigation : introduce the JoinRoomNode --- .../android/appnav/room/RoomFlowNode.kt | 49 ++-- .../appnav/room/join/JoinRoomEvents.kt | 23 ++ .../android/appnav/room/join/JoinRoomNode.kt | 55 +++++ .../appnav/room/join/JoinRoomPresenter.kt | 104 +++++++++ .../android/appnav/room/join/JoinRoomState.kt | 59 +++++ .../appnav/room/join/JoinRoomStateProvider.kt | 60 +++++ .../android/appnav/room/join/JoinRoomView.kt | 211 ++++++++++++++++++ .../appnav/room/join/di/JoinRoomModule.kt | 46 ++++ .../atomic/molecules/ButtonRowMolecule.kt | 6 +- 9 files changed, 583 insertions(+), 30 deletions(-) create mode 100644 appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomEvents.kt create mode 100644 appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomNode.kt create mode 100644 appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomPresenter.kt create mode 100644 appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomState.kt create mode 100644 appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomStateProvider.kt create mode 100644 appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomView.kt create mode 100644 appnav/src/main/kotlin/io/element/android/appnav/room/join/di/JoinRoomModule.kt diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt index dd53d6168b..beef8a883c 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt @@ -34,9 +34,9 @@ import com.bumble.appyx.navmodel.backstack.operation.newRoot import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.appnav.room.join.JoinRoomNode import io.element.android.appnav.room.joined.JoinedRoomFlowNode import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode -import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.NodeInputs @@ -64,16 +64,14 @@ class RoomFlowNode @AssistedInject constructor( @Assisted plugins: List, private val roomListService: RoomListService, private val roomMembershipObserver: RoomMembershipObserver, - private val networkMonitor: NetworkMonitor, -) : - BaseFlowNode( - backstack = BackStack( - initialElement = NavTarget.Loading, - savedStateMap = buildContext.savedStateMap, - ), - buildContext = buildContext, - plugins = plugins - ) { +) : BaseFlowNode( + backstack = BackStack( + initialElement = NavTarget.Loading, + savedStateMap = buildContext.savedStateMap, + ), + buildContext = buildContext, + plugins = plugins +) { data class Inputs( val roomId: RoomId, val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages, @@ -96,18 +94,16 @@ class RoomFlowNode @AssistedInject constructor( super.onBuilt() roomListService.getUserMembershipForRoom( inputs.roomId - ).onEach { membership -> - Timber.d("RoomMembership = $membership") - when { - membership.getOrNull() == CurrentUserMembership.JOINED -> { + ).flowOn(Dispatchers.Default) + .onEach { membership -> + Timber.d("RoomMembership = $membership") + if (membership.getOrNull() == CurrentUserMembership.JOINED) { backstack.newRoot(NavTarget.JoinedRoom) - } - else -> { + } else { backstack.newRoot(NavTarget.JoinRoom) } } - } - .flowOn(Dispatchers.Default) + .flowOn(Dispatchers.Main) .launchIn(lifecycleScope) roomMembershipObserver.updates @@ -121,7 +117,10 @@ class RoomFlowNode @AssistedInject constructor( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { NavTarget.Loading -> loadingNode(buildContext) - NavTarget.JoinRoom -> joinRoomNode(buildContext) + NavTarget.JoinRoom -> { + val inputs = JoinRoomNode.Inputs(inputs.roomId) + createNode(buildContext, plugins = listOf(inputs)) + } NavTarget.JoinedRoom -> { val roomFlowNodeCallback = plugins() val inputs = JoinedRoomFlowNode.Inputs(inputs.roomId, initialElement = inputs.initialElement) @@ -136,16 +135,8 @@ class RoomFlowNode @AssistedInject constructor( } } - private fun joinRoomNode(buildContext: BuildContext) = node(buildContext) { - Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) { - Text("Unknown Room") - } - } - @Composable override fun View(modifier: Modifier) { - BackstackView( - transitionHandler = JumpToEndTransitionHandler(), - ) + BackstackView(transitionHandler = JumpToEndTransitionHandler()) } } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomEvents.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomEvents.kt new file mode 100644 index 0000000000..7679e77c63 --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomEvents.kt @@ -0,0 +1,23 @@ +/* + * 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.appnav.room.join + +sealed interface JoinRoomEvents { + data object JoinRoom: JoinRoomEvents + data object AcceptInvite : JoinRoomEvents + data object DeclineInvite : JoinRoomEvents +} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomNode.kt new file mode 100644 index 0000000000..cccc66ddfb --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomNode.kt @@ -0,0 +1,55 @@ +/* + * 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.appnav.room.join + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.core.RoomId + +@ContributesNode(SessionScope::class) +class JoinRoomNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + presenterFactory: JoinRoomPresenter.Factory, +) : Node(buildContext, plugins = plugins) { + + data class Inputs( + val roomId: RoomId, + ) : NodeInputs + + private val inputs: Inputs = inputs() + private val presenter = presenterFactory.create(inputs.roomId) + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + JoinRoomView( + state = state, + onBackPressed = ::navigateUp, + modifier = modifier + ) + } +} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomPresenter.kt new file mode 100644 index 0000000000..5117238c6c --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomPresenter.kt @@ -0,0 +1,104 @@ +/* + * 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.appnav.room.join + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import androidx.compose.runtime.rememberCoroutineScope +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.architecture.Presenter +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.CurrentUserMembership +import io.element.android.libraries.matrix.api.roomlist.RoomListService +import kotlinx.coroutines.launch +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +class JoinRoomPresenter @AssistedInject constructor( + @Assisted private val roomId: RoomId, + private val matrixClient: MatrixClient, + private val roomListService: RoomListService, +) : Presenter { + + interface Factory { + fun create(roomId: RoomId): JoinRoomPresenter + } + + @Composable + override fun present(): JoinRoomState { + val userMembership by roomListService.getUserMembershipForRoom(roomId).collectAsState(initial = Optional.empty()) + val joinAuthorisationStatus = joinAuthorisationStatus(userMembership) + val roomInfo by produceState>(initialValue = AsyncData.Uninitialized, key1 = userMembership) { + when { + userMembership.isPresent -> { + val roomInfo = matrixClient.getRoom(roomId)?.let { + RoomInfo( + roomId = it.roomId, + roomName = it.displayName, + roomAlias = it.alias, + memberCount = it.activeMemberCount, + roomAvatarUrl = it.avatarUrl + ) + } + value = roomInfo?.let { AsyncData.Success(it) } ?: AsyncData.Failure(Exception("Failed to load room info")) + } + else -> { + value = AsyncData.Uninitialized + } + } + } + + val coroutineScope = rememberCoroutineScope() + + fun handleEvents(event: JoinRoomEvents) { + when (event) { + JoinRoomEvents.AcceptInvite, JoinRoomEvents.JoinRoom -> { + coroutineScope.launch { + matrixClient.joinRoom(roomId) + } + } + JoinRoomEvents.DeclineInvite -> { + coroutineScope.launch { + matrixClient.getRoom(roomId)?.use { + it.leave() + } + } + } + } + } + + return JoinRoomState( + roomInfo = roomInfo, + joinAuthorisationStatus = joinAuthorisationStatus, + currentAction = CurrentAction.None, + eventSink = ::handleEvents + ) + } + + @Composable + private fun joinAuthorisationStatus(userMembership: Optional): JoinAuthorisationStatus { + return when { + userMembership.getOrNull() == CurrentUserMembership.INVITED -> return JoinAuthorisationStatus.IsInvited + else -> JoinAuthorisationStatus.Unknown + } + } +} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomState.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomState.kt new file mode 100644 index 0000000000..481fb13642 --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomState.kt @@ -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.appnav.room.join + +import androidx.compose.runtime.Immutable +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.matrix.api.core.RoomId + +@Immutable +data class JoinRoomState( + val roomInfo: AsyncData, + val joinAuthorisationStatus: JoinAuthorisationStatus, + val currentAction: CurrentAction, + val eventSink: (JoinRoomEvents) -> Unit +) + +data class RoomInfo( + val roomId: RoomId, + val roomName: String, + val roomAlias: String?, + val memberCount: Long?, + val roomAvatarUrl: String?, +) { + fun avatarData(size: AvatarSize): AvatarData { + return AvatarData( + id = roomId.value, + name = roomName, + url = roomAvatarUrl, + size = size, + ) + } +} + +enum class JoinAuthorisationStatus { + IsInvited, + CanKnock, + CanJoin, + Unknown, +} + +sealed interface CurrentAction { + data object None : CurrentAction +} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomStateProvider.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomStateProvider.kt new file mode 100644 index 0000000000..928edb4ce4 --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomStateProvider.kt @@ -0,0 +1,60 @@ +/* + * 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.appnav.room.join + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.core.RoomId + +open class JoinRoomStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aJoinRoomState( + roomInfo = AsyncData.Uninitialized + ), + aJoinRoomState( + joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin + ), + aJoinRoomState( + joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock + ), + aJoinRoomState( + joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited + ), + ) +} + +fun aJoinRoomState( + roomInfo: AsyncData = AsyncData.Success( + RoomInfo( + roomId = RoomId("@exa:matrix.org"), + roomName = "Element x android", + roomAlias = "#exa:matrix.org", + memberCount = null, + roomAvatarUrl = null + ) + ), + joinAuthorisationStatus: JoinAuthorisationStatus = JoinAuthorisationStatus.Unknown, + currentAction: CurrentAction = CurrentAction.None, + eventSink: (JoinRoomEvents) -> Unit = {} +) = JoinRoomState( + roomInfo = roomInfo, + joinAuthorisationStatus = joinAuthorisationStatus, + currentAction = currentAction, + eventSink = eventSink +) + diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomView.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomView.kt new file mode 100644 index 0000000000..9fcda27e25 --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomView.kt @@ -0,0 +1,211 @@ +/* + * 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.appnav.room.join + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom +import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule +import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule +import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage +import io.element.android.libraries.designsystem.components.avatar.Avatar +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.ButtonSize +import io.element.android.libraries.designsystem.theme.components.OutlinedButton +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun JoinRoomView( + state: JoinRoomState, + onBackPressed: () -> Unit, + modifier: Modifier = Modifier, +) { + HeaderFooterPage( + modifier = modifier, + topBar = { + JoinRoomTopBar(asyncRoomInfo = state.roomInfo, onBackClicked = onBackPressed) + }, + content = { + JoinRoomContent(state = state) + }, + footer = { + JoinRoomFooter( + joinAuthorisationStatus = state.joinAuthorisationStatus, + onAcceptInvite = { + state.eventSink(JoinRoomEvents.AcceptInvite) + }, + onDeclineInvite = { + state.eventSink(JoinRoomEvents.DeclineInvite) + }, + onJoinRoom = { + state.eventSink(JoinRoomEvents.JoinRoom) + }, + ) + } + ) +} + +@Composable +private fun JoinRoomFooter( + joinAuthorisationStatus: JoinAuthorisationStatus, + onAcceptInvite: () -> Unit, + onDeclineInvite: () -> Unit, + onJoinRoom: () -> Unit, + modifier: Modifier = Modifier, +) { + when (joinAuthorisationStatus) { + JoinAuthorisationStatus.IsInvited -> { + ButtonRowMolecule(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(20.dp)) { + OutlinedButton( + text = stringResource(CommonStrings.action_decline), + onClick = onDeclineInvite, + modifier = Modifier.weight(1f), + size = ButtonSize.Medium, + ) + Button( + text = stringResource(CommonStrings.action_accept), + onClick = onAcceptInvite, + modifier = Modifier.weight(1f), + size = ButtonSize.Medium, + ) + } + } + // TODO handle all cases properly + else -> { + Button( + text = stringResource(CommonStrings.action_join), + onClick = onJoinRoom, + modifier = modifier.fillMaxWidth(), + size = ButtonSize.Medium, + ) + } + } +} + +@Composable +private fun JoinRoomContent( + state: JoinRoomState, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.padding(all = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(80.dp)) + when (state.roomInfo) { + is AsyncData.Success -> { + val roomInfo = state.roomInfo.data + Avatar(avatarData = roomInfo.avatarData(AvatarSize.RoomHeader)) + } + else -> { + PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp) + } + } + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "Preview is not available", + style = ElementTheme.typography.fontHeadingMdBold, + textAlign = TextAlign.Center, + color = ElementTheme.colors.textPrimary, + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "You must be a member of this room to view the message history.", + style = ElementTheme.typography.fontBodyMdRegular, + textAlign = TextAlign.Center, + color = ElementTheme.colors.textSecondary, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun JoinRoomTopBar( + asyncRoomInfo: AsyncData, + onBackClicked: () -> Unit, +) { + TopAppBar( + navigationIcon = { + BackButton(onClick = onBackClicked) + }, + title = { + when (asyncRoomInfo) { + is AsyncData.Success -> { + val roomInfo = asyncRoomInfo.data + RoomAvatarAndNameRow(roomName = roomInfo.roomName, roomAvatar = roomInfo.avatarData(AvatarSize.TimelineRoom)) + } + else -> { + IconTitlePlaceholdersRowMolecule(iconSize = AvatarSize.TimelineRoom.dp) + } + } + }, + ) +} + +@Composable +private fun RoomAvatarAndNameRow( + roomName: String, + roomAvatar: AvatarData, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically + ) { + Avatar(roomAvatar) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = roomName, + style = ElementTheme.typography.fontBodyLgMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } +} + +@PreviewLightDark +@Composable +fun JoinRoomViewPreview(@PreviewParameter(JoinRoomStateProvider::class) state: JoinRoomState) = ElementPreview { + JoinRoomView( + state = state, + onBackPressed = { } + ) +} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/join/di/JoinRoomModule.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/join/di/JoinRoomModule.kt new file mode 100644 index 0000000000..fa7635a312 --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/join/di/JoinRoomModule.kt @@ -0,0 +1,46 @@ +/* + * 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.appnav.room.join.di + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Module +import dagger.Provides +import io.element.android.appnav.room.join.JoinRoomPresenter +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.roomlist.RoomListService + +@Module +@ContributesTo(SessionScope::class) +object JoinRoomModule { + @Provides + fun providesJoinRoomPresenterFactory( + roomListService: RoomListService, + client: MatrixClient, + ): JoinRoomPresenter.Factory { + return object : JoinRoomPresenter.Factory { + override fun create(roomId: RoomId): JoinRoomPresenter { + return JoinRoomPresenter( + roomId = roomId, + matrixClient = client, + roomListService = roomListService + ) + } + } + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ButtonRowMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ButtonRowMolecule.kt index 9388b54880..f6f5c3cb81 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ButtonRowMolecule.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ButtonRowMolecule.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -29,11 +30,14 @@ import io.element.android.libraries.designsystem.theme.components.TextButton @Composable fun ButtonRowMolecule( modifier: Modifier = Modifier, + horizontalArrangement: Arrangement.Horizontal = Arrangement.SpaceBetween, + verticalAlignment: Alignment.Vertical = Alignment.Top, content: @Composable RowScope.() -> Unit ) { Row( modifier = modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween + horizontalArrangement = horizontalArrangement, + verticalAlignment = verticalAlignment, ) { content() } From 64ef638f26a7340030f7964d82b99f010b62f8f2 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 5 Apr 2024 17:57:38 +0200 Subject: [PATCH 03/25] Room navigation : do not replay RoomMembership Changes --- .../libraries/matrix/api/room/RoomMembershipObserver.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembershipObserver.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembershipObserver.kt index ed6f3fae26..24610a64c9 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembershipObserver.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembershipObserver.kt @@ -28,10 +28,10 @@ class RoomMembershipObserver { val change: MembershipChange, ) - private val _updates = MutableSharedFlow(replay = 1) + private val _updates = MutableSharedFlow(extraBufferCapacity = 10) val updates = _updates.asSharedFlow() - fun notifyUserLeftRoom(roomId: RoomId) { - _updates.tryEmit(RoomMembershipUpdate(roomId, false, MembershipChange.LEFT)) + suspend fun notifyUserLeftRoom(roomId: RoomId) { + _updates.emit(RoomMembershipUpdate(roomId, false, MembershipChange.LEFT)) } } From 6d86dbd996b676a7090eb2ae6196c57f79467297 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 5 Apr 2024 18:41:15 +0200 Subject: [PATCH 04/25] Join Room : add member count --- .../appnav/room/join/JoinRoomPresenter.kt | 2 +- .../android/appnav/room/join/JoinRoomState.kt | 4 ++- .../android/appnav/room/join/JoinRoomView.kt | 29 ++++++++++++++++++- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomPresenter.kt index 5117238c6c..3dda190d33 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomPresenter.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomPresenter.kt @@ -50,7 +50,7 @@ class JoinRoomPresenter @AssistedInject constructor( val roomInfo by produceState>(initialValue = AsyncData.Uninitialized, key1 = userMembership) { when { userMembership.isPresent -> { - val roomInfo = matrixClient.getRoom(roomId)?.let { + val roomInfo = matrixClient.getRoom(roomId)?.use { RoomInfo( roomId = it.roomId, roomName = it.displayName, diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomState.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomState.kt index 481fb13642..2e95987d97 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomState.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomState.kt @@ -28,7 +28,9 @@ data class JoinRoomState( val joinAuthorisationStatus: JoinAuthorisationStatus, val currentAction: CurrentAction, val eventSink: (JoinRoomEvents) -> Unit -) +){ + val showMemberCount = roomInfo.dataOrNull()?.memberCount != null +} data class RoomInfo( val roomId: RoomId, diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomView.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomView.kt index 9fcda27e25..9a4b805176 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomView.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomView.kt @@ -16,14 +16,18 @@ package io.element.android.appnav.room.join +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -35,6 +39,7 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule @@ -47,6 +52,7 @@ import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.ButtonSize +import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.OutlinedButton import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar @@ -129,7 +135,6 @@ private fun JoinRoomContent( modifier = modifier.padding(all = 16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - Spacer(modifier = Modifier.height(80.dp)) when (state.roomInfo) { is AsyncData.Success -> { val roomInfo = state.roomInfo.data @@ -153,6 +158,28 @@ private fun JoinRoomContent( textAlign = TextAlign.Center, color = ElementTheme.colors.textSecondary, ) + if (state.showMemberCount) { + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier + .background(color = ElementTheme.colors.bgSubtleSecondary, shape = CircleShape) + .widthIn(min = 48.dp) + .padding(all = 2.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Icon( + imageVector = CompoundIcons.UserProfile(), + contentDescription = null, + tint = ElementTheme.colors.iconSecondary, + ) + Text( + text = "${state.roomInfo.dataOrNull()?.memberCount}", + style = ElementTheme.typography.fontBodySmMedium, + color = ElementTheme.colors.textSecondary, + ) + } + } } } From 59df98dcac5f2c12b15c39ef67adf06841727283 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 8 Apr 2024 15:54:08 +0200 Subject: [PATCH 05/25] Join Room : improve a bit --- .../appnav/room/join/JoinRoomPresenter.kt | 8 ++- .../android/appnav/room/join/JoinRoomView.kt | 50 +++++++++++-------- 2 files changed, 31 insertions(+), 27 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomPresenter.kt index 3dda190d33..d80fb99b3e 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomPresenter.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomPresenter.kt @@ -48,7 +48,7 @@ class JoinRoomPresenter @AssistedInject constructor( val userMembership by roomListService.getUserMembershipForRoom(roomId).collectAsState(initial = Optional.empty()) val joinAuthorisationStatus = joinAuthorisationStatus(userMembership) val roomInfo by produceState>(initialValue = AsyncData.Uninitialized, key1 = userMembership) { - when { + value = when { userMembership.isPresent -> { val roomInfo = matrixClient.getRoom(roomId)?.use { RoomInfo( @@ -59,11 +59,9 @@ class JoinRoomPresenter @AssistedInject constructor( roomAvatarUrl = it.avatarUrl ) } - value = roomInfo?.let { AsyncData.Success(it) } ?: AsyncData.Failure(Exception("Failed to load room info")) - } - else -> { - value = AsyncData.Uninitialized + roomInfo?.let { AsyncData.Success(it) } ?: AsyncData.Failure(Exception("Failed to load room info")) } + else -> AsyncData.Uninitialized } } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomView.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomView.kt index 9a4b805176..eff9181c11 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomView.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomView.kt @@ -23,7 +23,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn @@ -132,7 +131,9 @@ private fun JoinRoomContent( modifier: Modifier = Modifier, ) { Column( - modifier = modifier.padding(all = 16.dp), + modifier = modifier + .fillMaxWidth() + .padding(all = 16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { when (state.roomInfo) { @@ -159,30 +160,35 @@ private fun JoinRoomContent( color = ElementTheme.colors.textSecondary, ) if (state.showMemberCount) { - Spacer(modifier = Modifier.height(8.dp)) - Row( - modifier = Modifier - .background(color = ElementTheme.colors.bgSubtleSecondary, shape = CircleShape) - .widthIn(min = 48.dp) - .padding(all = 2.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - Icon( - imageVector = CompoundIcons.UserProfile(), - contentDescription = null, - tint = ElementTheme.colors.iconSecondary, - ) - Text( - text = "${state.roomInfo.dataOrNull()?.memberCount}", - style = ElementTheme.typography.fontBodySmMedium, - color = ElementTheme.colors.textSecondary, - ) - } + JoinRoomMembersCount(memberCount = state.roomInfo.dataOrNull()?.memberCount ?: 0) } } } +@Composable +fun JoinRoomMembersCount(memberCount: Long) { + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier + .background(color = ElementTheme.colors.bgSubtleSecondary, shape = CircleShape) + .widthIn(min = 48.dp) + .padding(all = 2.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Icon( + imageVector = CompoundIcons.UserProfile(), + contentDescription = null, + tint = ElementTheme.colors.iconSecondary, + ) + Text( + text = "$memberCount", + style = ElementTheme.typography.fontBodySmMedium, + color = ElementTheme.colors.textSecondary, + ) + } +} + @OptIn(ExperimentalMaterial3Api::class) @Composable private fun JoinRoomTopBar( From 28361be6e80238bb657e6fb84dddacdf92acae5c Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 8 Apr 2024 17:22:13 +0200 Subject: [PATCH 06/25] Room navigation : rename module invitelist to invite --- .../io/element/android/appnav/LoggedInFlowNode.kt | 6 +----- features/{invitelist => invite}/api/build.gradle.kts | 2 +- .../features/invite}/api/InviteListEntryPoint.kt | 2 +- .../android/features/invite}/api/SeenInvitesStore.kt | 2 +- features/{invitelist => invite}/impl/build.gradle.kts | 6 +++--- .../invite}/impl/DefaultInviteListEntryPoint.kt | 4 ++-- .../features/invite}/impl/DefaultSeenInvitesStore.kt | 4 ++-- .../android/features/invite}/impl/InviteListEvents.kt | 4 ++-- .../android/features/invite}/impl/InviteListNode.kt | 4 ++-- .../features/invite}/impl/InviteListPresenter.kt | 8 ++++---- .../android/features/invite}/impl/InviteListState.kt | 4 ++-- .../features/invite}/impl/InviteListStateProvider.kt | 6 +++--- .../android/features/invite}/impl/InviteListView.kt | 4 ++-- .../invite}/impl/components/InviteSummaryRow.kt | 10 +++++----- .../invite}/impl/model/InviteListInviteSummary.kt | 2 +- .../impl/model/InviteListInviteSummaryProvider.kt | 2 +- .../impl/src/main/res/values-be/translations.xml | 0 .../impl/src/main/res/values-bg/translations.xml | 0 .../impl/src/main/res/values-cs/translations.xml | 0 .../impl/src/main/res/values-de/translations.xml | 0 .../impl/src/main/res/values-es/translations.xml | 0 .../impl/src/main/res/values-fr/translations.xml | 0 .../impl/src/main/res/values-hu/translations.xml | 0 .../impl/src/main/res/values-in/translations.xml | 0 .../impl/src/main/res/values-it/translations.xml | 0 .../impl/src/main/res/values-ro/translations.xml | 0 .../impl/src/main/res/values-ru/translations.xml | 0 .../impl/src/main/res/values-sk/translations.xml | 0 .../impl/src/main/res/values-sv/translations.xml | 0 .../impl/src/main/res/values-uk/translations.xml | 0 .../impl/src/main/res/values-zh-rTW/translations.xml | 0 .../impl/src/main/res/values/localazy.xml | 0 .../features/invite}/impl/InviteListPresenterTests.kt | 6 +++--- features/{invitelist => invite}/test/build.gradle.kts | 4 ++-- .../features/invite}/test/FakeSeenInvitesStore.kt | 4 ++-- features/roomlist/impl/build.gradle.kts | 4 ++-- .../impl/datasource/DefaultInviteStateDataSource.kt | 2 +- .../datasource/DefaultInviteStateDataSourceTest.kt | 2 +- samples/minimal/build.gradle.kts | 2 +- .../element/android/samples/minimal/RoomListScreen.kt | 2 +- tools/localazy/config.json | 2 +- 41 files changed, 47 insertions(+), 51 deletions(-) rename features/{invitelist => invite}/api/build.gradle.kts (92%) rename features/{invitelist/api/src/main/kotlin/io/element/android/features/invitelist => invite/api/src/main/kotlin/io/element/android/features/invite}/api/InviteListEntryPoint.kt (96%) rename features/{invitelist/api/src/main/kotlin/io/element/android/features/invitelist => invite/api/src/main/kotlin/io/element/android/features/invite}/api/SeenInvitesStore.kt (94%) rename features/{invitelist => invite}/impl/build.gradle.kts (91%) rename features/{invitelist/impl/src/main/kotlin/io/element/android/features/invitelist => invite/impl/src/main/kotlin/io/element/android/features/invite}/impl/DefaultInviteListEntryPoint.kt (92%) rename features/{invitelist/impl/src/main/kotlin/io/element/android/features/invitelist => invite/impl/src/main/kotlin/io/element/android/features/invite}/impl/DefaultSeenInvitesStore.kt (94%) rename features/{invitelist/impl/src/main/kotlin/io/element/android/features/invitelist => invite/impl/src/main/kotlin/io/element/android/features/invite}/impl/InviteListEvents.kt (88%) rename features/{invitelist/impl/src/main/kotlin/io/element/android/features/invitelist => invite/impl/src/main/kotlin/io/element/android/features/invite}/impl/InviteListNode.kt (93%) rename features/{invitelist/impl/src/main/kotlin/io/element/android/features/invitelist => invite/impl/src/main/kotlin/io/element/android/features/invite}/impl/InviteListPresenter.kt (96%) rename features/{invitelist/impl/src/main/kotlin/io/element/android/features/invitelist => invite/impl/src/main/kotlin/io/element/android/features/invite}/impl/InviteListState.kt (90%) rename features/{invitelist/impl/src/main/kotlin/io/element/android/features/invitelist => invite/impl/src/main/kotlin/io/element/android/features/invite}/impl/InviteListStateProvider.kt (93%) rename features/{invitelist/impl/src/main/kotlin/io/element/android/features/invitelist => invite/impl/src/main/kotlin/io/element/android/features/invite}/impl/InviteListView.kt (98%) rename features/{invitelist/impl/src/main/kotlin/io/element/android/features/invitelist => invite/impl/src/main/kotlin/io/element/android/features/invite}/impl/components/InviteSummaryRow.kt (94%) rename features/{invitelist/impl/src/main/kotlin/io/element/android/features/invitelist => invite/impl/src/main/kotlin/io/element/android/features/invite}/impl/model/InviteListInviteSummary.kt (96%) rename features/{invitelist/impl/src/main/kotlin/io/element/android/features/invitelist => invite/impl/src/main/kotlin/io/element/android/features/invite}/impl/model/InviteListInviteSummaryProvider.kt (96%) rename features/{invitelist => invite}/impl/src/main/res/values-be/translations.xml (100%) rename features/{invitelist => invite}/impl/src/main/res/values-bg/translations.xml (100%) rename features/{invitelist => invite}/impl/src/main/res/values-cs/translations.xml (100%) rename features/{invitelist => invite}/impl/src/main/res/values-de/translations.xml (100%) rename features/{invitelist => invite}/impl/src/main/res/values-es/translations.xml (100%) rename features/{invitelist => invite}/impl/src/main/res/values-fr/translations.xml (100%) rename features/{invitelist => invite}/impl/src/main/res/values-hu/translations.xml (100%) rename features/{invitelist => invite}/impl/src/main/res/values-in/translations.xml (100%) rename features/{invitelist => invite}/impl/src/main/res/values-it/translations.xml (100%) rename features/{invitelist => invite}/impl/src/main/res/values-ro/translations.xml (100%) rename features/{invitelist => invite}/impl/src/main/res/values-ru/translations.xml (100%) rename features/{invitelist => invite}/impl/src/main/res/values-sk/translations.xml (100%) rename features/{invitelist => invite}/impl/src/main/res/values-sv/translations.xml (100%) rename features/{invitelist => invite}/impl/src/main/res/values-uk/translations.xml (100%) rename features/{invitelist => invite}/impl/src/main/res/values-zh-rTW/translations.xml (100%) rename features/{invitelist => invite}/impl/src/main/res/values/localazy.xml (100%) rename features/{invitelist/impl/src/test/kotlin/io/element/android/features/invitelist => invite/impl/src/test/kotlin/io/element/android/features/invite}/impl/InviteListPresenterTests.kt (99%) rename features/{invitelist => invite}/test/build.gradle.kts (88%) rename features/{invitelist/test/src/main/kotlin/io/element/android/features/invitelist => invite/test/src/main/kotlin/io/element/android/features/invite}/test/FakeSeenInvitesStore.kt (90%) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index 8ff7ecfaa8..8ba98e5a6b 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -48,7 +48,7 @@ import io.element.android.features.createroom.api.CreateRoomEntryPoint import io.element.android.features.ftue.api.FtueEntryPoint import io.element.android.features.ftue.api.state.FtueService import io.element.android.features.ftue.api.state.FtueState -import io.element.android.features.invitelist.api.InviteListEntryPoint +import io.element.android.features.invite.api.InviteListEntryPoint import io.element.android.features.lockscreen.api.LockScreenEntryPoint import io.element.android.features.lockscreen.api.LockScreenLockState import io.element.android.features.lockscreen.api.LockScreenService @@ -350,10 +350,6 @@ class LoggedInFlowNode @AssistedInject constructor( backstack.pop() } - override fun onInviteClicked(roomId: RoomId) { - backstack.push(NavTarget.Room(roomId)) - } - override fun onInviteAccepted(roomId: RoomId) { backstack.push(NavTarget.Room(roomId)) } diff --git a/features/invitelist/api/build.gradle.kts b/features/invite/api/build.gradle.kts similarity index 92% rename from features/invitelist/api/build.gradle.kts rename to features/invite/api/build.gradle.kts index 6ea2b8a49d..1781ee93ae 100644 --- a/features/invitelist/api/build.gradle.kts +++ b/features/invite/api/build.gradle.kts @@ -19,7 +19,7 @@ plugins { } android { - namespace = "io.element.android.features.invitelist.api" + namespace = "io.element.android.features.invite.api" } dependencies { diff --git a/features/invitelist/api/src/main/kotlin/io/element/android/features/invitelist/api/InviteListEntryPoint.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/InviteListEntryPoint.kt similarity index 96% rename from features/invitelist/api/src/main/kotlin/io/element/android/features/invitelist/api/InviteListEntryPoint.kt rename to features/invite/api/src/main/kotlin/io/element/android/features/invite/api/InviteListEntryPoint.kt index 95d2c94f50..6abfe8f518 100644 --- a/features/invitelist/api/src/main/kotlin/io/element/android/features/invitelist/api/InviteListEntryPoint.kt +++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/InviteListEntryPoint.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.invitelist.api +package io.element.android.features.invite.api import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node diff --git a/features/invitelist/api/src/main/kotlin/io/element/android/features/invitelist/api/SeenInvitesStore.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/SeenInvitesStore.kt similarity index 94% rename from features/invitelist/api/src/main/kotlin/io/element/android/features/invitelist/api/SeenInvitesStore.kt rename to features/invite/api/src/main/kotlin/io/element/android/features/invite/api/SeenInvitesStore.kt index ac143b8740..e34b17cee8 100644 --- a/features/invitelist/api/src/main/kotlin/io/element/android/features/invitelist/api/SeenInvitesStore.kt +++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/SeenInvitesStore.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.invitelist.api +package io.element.android.features.invite.api import io.element.android.libraries.matrix.api.core.RoomId import kotlinx.coroutines.flow.Flow diff --git a/features/invitelist/impl/build.gradle.kts b/features/invite/impl/build.gradle.kts similarity index 91% rename from features/invitelist/impl/build.gradle.kts rename to features/invite/impl/build.gradle.kts index 110eb7946e..7a0b0db372 100644 --- a/features/invitelist/impl/build.gradle.kts +++ b/features/invite/impl/build.gradle.kts @@ -22,7 +22,7 @@ plugins { } android { - namespace = "io.element.android.features.invitelist.impl" + namespace = "io.element.android.features.invite.impl" } anvil { @@ -32,7 +32,7 @@ anvil { dependencies { implementation(projects.anvilannotations) anvil(projects.anvilcodegen) - api(projects.features.invitelist.api) + api(projects.features.invite.api) implementation(libs.androidx.datastore.preferences) implementation(projects.libraries.core) implementation(projects.libraries.architecture) @@ -50,7 +50,7 @@ dependencies { testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.push.test) - testImplementation(projects.features.invitelist.test) + testImplementation(projects.features.invite.test) testImplementation(projects.services.analytics.test) testImplementation(projects.tests.testutils) diff --git a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/DefaultInviteListEntryPoint.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DefaultInviteListEntryPoint.kt similarity index 92% rename from features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/DefaultInviteListEntryPoint.kt rename to features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DefaultInviteListEntryPoint.kt index 5c2fc780d7..03e7a57dae 100644 --- a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/DefaultInviteListEntryPoint.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DefaultInviteListEntryPoint.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package io.element.android.features.invitelist.impl +package io.element.android.features.invite.impl import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.invitelist.api.InviteListEntryPoint +import io.element.android.features.invite.api.InviteListEntryPoint import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.AppScope import javax.inject.Inject diff --git a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/DefaultSeenInvitesStore.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DefaultSeenInvitesStore.kt similarity index 94% rename from features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/DefaultSeenInvitesStore.kt rename to features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DefaultSeenInvitesStore.kt index 1564b2fa83..bf0423914e 100644 --- a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/DefaultSeenInvitesStore.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DefaultSeenInvitesStore.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.invitelist.impl +package io.element.android.features.invite.impl import android.content.Context import androidx.datastore.core.DataStore @@ -23,7 +23,7 @@ import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringSetPreferencesKey import androidx.datastore.preferences.preferencesDataStore import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.invitelist.api.SeenInvitesStore +import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.core.RoomId diff --git a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListEvents.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/InviteListEvents.kt similarity index 88% rename from features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListEvents.kt rename to features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/InviteListEvents.kt index ae4b74bb2c..7bf58aba7a 100644 --- a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListEvents.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/InviteListEvents.kt @@ -14,9 +14,9 @@ * limitations under the License. */ -package io.element.android.features.invitelist.impl +package io.element.android.features.invite.impl -import io.element.android.features.invitelist.impl.model.InviteListInviteSummary +import io.element.android.features.invite.impl.model.InviteListInviteSummary sealed interface InviteListEvents { data class AcceptInvite(val invite: InviteListInviteSummary) : InviteListEvents diff --git a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListNode.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/InviteListNode.kt similarity index 93% rename from features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListNode.kt rename to features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/InviteListNode.kt index 31d742e4e7..1f3236e916 100644 --- a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListNode.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/InviteListNode.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.invitelist.impl +package io.element.android.features.invite.impl import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -25,7 +25,7 @@ import com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode -import io.element.android.features.invitelist.api.InviteListEntryPoint +import io.element.android.features.invite.api.InviteListEntryPoint import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.core.RoomId diff --git a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/InviteListPresenter.kt similarity index 96% rename from features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt rename to features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/InviteListPresenter.kt index aa7cead66e..f616450d25 100644 --- a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/InviteListPresenter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.invitelist.impl +package io.element.android.features.invite.impl import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -26,9 +26,9 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import im.vector.app.features.analytics.plan.JoinedRoom -import io.element.android.features.invitelist.api.SeenInvitesStore -import io.element.android.features.invitelist.impl.model.InviteListInviteSummary -import io.element.android.features.invitelist.impl.model.InviteSender +import io.element.android.features.invite.api.SeenInvitesStore +import io.element.android.features.invite.impl.model.InviteListInviteSummary +import io.element.android.features.invite.impl.model.InviteSender import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState diff --git a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListState.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/InviteListState.kt similarity index 90% rename from features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListState.kt rename to features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/InviteListState.kt index b576861475..42d73de547 100644 --- a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListState.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/InviteListState.kt @@ -14,10 +14,10 @@ * limitations under the License. */ -package io.element.android.features.invitelist.impl +package io.element.android.features.invite.impl import androidx.compose.runtime.Immutable -import io.element.android.features.invitelist.impl.model.InviteListInviteSummary +import io.element.android.features.invite.impl.model.InviteListInviteSummary import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.core.RoomId import kotlinx.collections.immutable.ImmutableList diff --git a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListStateProvider.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/InviteListStateProvider.kt similarity index 93% rename from features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListStateProvider.kt rename to features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/InviteListStateProvider.kt index e61bd9ff93..5f1d5dcc0f 100644 --- a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListStateProvider.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/InviteListStateProvider.kt @@ -14,11 +14,11 @@ * limitations under the License. */ -package io.element.android.features.invitelist.impl +package io.element.android.features.invite.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.invitelist.impl.model.InviteListInviteSummary -import io.element.android.features.invitelist.impl.model.InviteSender +import io.element.android.features.invite.impl.model.InviteListInviteSummary +import io.element.android.features.invite.impl.model.InviteSender import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId diff --git a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListView.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/InviteListView.kt similarity index 98% rename from features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListView.kt rename to features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/InviteListView.kt index ff8ba55c0b..34da18f02f 100644 --- a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListView.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/InviteListView.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.invitelist.impl +package io.element.android.features.invite.impl import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -36,7 +36,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme -import io.element.android.features.invitelist.impl.components.InviteSummaryRow +import io.element.android.features.invite.impl.components.InviteSummaryRow import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog diff --git a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/components/InviteSummaryRow.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/components/InviteSummaryRow.kt similarity index 94% rename from features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/components/InviteSummaryRow.kt rename to features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/components/InviteSummaryRow.kt index 7c3c1ad1db..0987f6cf57 100644 --- a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/components/InviteSummaryRow.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/components/InviteSummaryRow.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.invitelist.impl.components +package io.element.android.features.invite.impl.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -40,10 +40,10 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme -import io.element.android.features.invitelist.impl.R -import io.element.android.features.invitelist.impl.model.InviteListInviteSummary -import io.element.android.features.invitelist.impl.model.InviteListInviteSummaryProvider -import io.element.android.features.invitelist.impl.model.InviteSender +import io.element.android.features.invite.impl.R +import io.element.android.features.invite.impl.model.InviteListInviteSummary +import io.element.android.features.invite.impl.model.InviteListInviteSummaryProvider +import io.element.android.features.invite.impl.model.InviteSender import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.preview.ElementPreview diff --git a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/model/InviteListInviteSummary.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/model/InviteListInviteSummary.kt similarity index 96% rename from features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/model/InviteListInviteSummary.kt rename to features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/model/InviteListInviteSummary.kt index 9f74b90142..e17dcc997c 100644 --- a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/model/InviteListInviteSummary.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/model/InviteListInviteSummary.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.invitelist.impl.model +package io.element.android.features.invite.impl.model import androidx.compose.runtime.Immutable import io.element.android.libraries.designsystem.components.avatar.AvatarData diff --git a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/model/InviteListInviteSummaryProvider.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/model/InviteListInviteSummaryProvider.kt similarity index 96% rename from features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/model/InviteListInviteSummaryProvider.kt rename to features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/model/InviteListInviteSummaryProvider.kt index c872d05817..11f6742345 100644 --- a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/model/InviteListInviteSummaryProvider.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/model/InviteListInviteSummaryProvider.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.invitelist.impl.model +package io.element.android.features.invite.impl.model import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.matrix.api.core.RoomId diff --git a/features/invitelist/impl/src/main/res/values-be/translations.xml b/features/invite/impl/src/main/res/values-be/translations.xml similarity index 100% rename from features/invitelist/impl/src/main/res/values-be/translations.xml rename to features/invite/impl/src/main/res/values-be/translations.xml diff --git a/features/invitelist/impl/src/main/res/values-bg/translations.xml b/features/invite/impl/src/main/res/values-bg/translations.xml similarity index 100% rename from features/invitelist/impl/src/main/res/values-bg/translations.xml rename to features/invite/impl/src/main/res/values-bg/translations.xml diff --git a/features/invitelist/impl/src/main/res/values-cs/translations.xml b/features/invite/impl/src/main/res/values-cs/translations.xml similarity index 100% rename from features/invitelist/impl/src/main/res/values-cs/translations.xml rename to features/invite/impl/src/main/res/values-cs/translations.xml diff --git a/features/invitelist/impl/src/main/res/values-de/translations.xml b/features/invite/impl/src/main/res/values-de/translations.xml similarity index 100% rename from features/invitelist/impl/src/main/res/values-de/translations.xml rename to features/invite/impl/src/main/res/values-de/translations.xml diff --git a/features/invitelist/impl/src/main/res/values-es/translations.xml b/features/invite/impl/src/main/res/values-es/translations.xml similarity index 100% rename from features/invitelist/impl/src/main/res/values-es/translations.xml rename to features/invite/impl/src/main/res/values-es/translations.xml diff --git a/features/invitelist/impl/src/main/res/values-fr/translations.xml b/features/invite/impl/src/main/res/values-fr/translations.xml similarity index 100% rename from features/invitelist/impl/src/main/res/values-fr/translations.xml rename to features/invite/impl/src/main/res/values-fr/translations.xml diff --git a/features/invitelist/impl/src/main/res/values-hu/translations.xml b/features/invite/impl/src/main/res/values-hu/translations.xml similarity index 100% rename from features/invitelist/impl/src/main/res/values-hu/translations.xml rename to features/invite/impl/src/main/res/values-hu/translations.xml diff --git a/features/invitelist/impl/src/main/res/values-in/translations.xml b/features/invite/impl/src/main/res/values-in/translations.xml similarity index 100% rename from features/invitelist/impl/src/main/res/values-in/translations.xml rename to features/invite/impl/src/main/res/values-in/translations.xml diff --git a/features/invitelist/impl/src/main/res/values-it/translations.xml b/features/invite/impl/src/main/res/values-it/translations.xml similarity index 100% rename from features/invitelist/impl/src/main/res/values-it/translations.xml rename to features/invite/impl/src/main/res/values-it/translations.xml diff --git a/features/invitelist/impl/src/main/res/values-ro/translations.xml b/features/invite/impl/src/main/res/values-ro/translations.xml similarity index 100% rename from features/invitelist/impl/src/main/res/values-ro/translations.xml rename to features/invite/impl/src/main/res/values-ro/translations.xml diff --git a/features/invitelist/impl/src/main/res/values-ru/translations.xml b/features/invite/impl/src/main/res/values-ru/translations.xml similarity index 100% rename from features/invitelist/impl/src/main/res/values-ru/translations.xml rename to features/invite/impl/src/main/res/values-ru/translations.xml diff --git a/features/invitelist/impl/src/main/res/values-sk/translations.xml b/features/invite/impl/src/main/res/values-sk/translations.xml similarity index 100% rename from features/invitelist/impl/src/main/res/values-sk/translations.xml rename to features/invite/impl/src/main/res/values-sk/translations.xml diff --git a/features/invitelist/impl/src/main/res/values-sv/translations.xml b/features/invite/impl/src/main/res/values-sv/translations.xml similarity index 100% rename from features/invitelist/impl/src/main/res/values-sv/translations.xml rename to features/invite/impl/src/main/res/values-sv/translations.xml diff --git a/features/invitelist/impl/src/main/res/values-uk/translations.xml b/features/invite/impl/src/main/res/values-uk/translations.xml similarity index 100% rename from features/invitelist/impl/src/main/res/values-uk/translations.xml rename to features/invite/impl/src/main/res/values-uk/translations.xml diff --git a/features/invitelist/impl/src/main/res/values-zh-rTW/translations.xml b/features/invite/impl/src/main/res/values-zh-rTW/translations.xml similarity index 100% rename from features/invitelist/impl/src/main/res/values-zh-rTW/translations.xml rename to features/invite/impl/src/main/res/values-zh-rTW/translations.xml diff --git a/features/invitelist/impl/src/main/res/values/localazy.xml b/features/invite/impl/src/main/res/values/localazy.xml similarity index 100% rename from features/invitelist/impl/src/main/res/values/localazy.xml rename to features/invite/impl/src/main/res/values/localazy.xml diff --git a/features/invitelist/impl/src/test/kotlin/io/element/android/features/invitelist/impl/InviteListPresenterTests.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/InviteListPresenterTests.kt similarity index 99% rename from features/invitelist/impl/src/test/kotlin/io/element/android/features/invitelist/impl/InviteListPresenterTests.kt rename to features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/InviteListPresenterTests.kt index fa0cf663a2..b9acd5eb0b 100644 --- a/features/invitelist/impl/src/test/kotlin/io/element/android/features/invitelist/impl/InviteListPresenterTests.kt +++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/InviteListPresenterTests.kt @@ -14,15 +14,15 @@ * limitations under the License. */ -package io.element.android.features.invitelist.impl +package io.element.android.features.invite.impl import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.TurbineTestContext import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.invitelist.api.SeenInvitesStore -import io.element.android.features.invitelist.test.FakeSeenInvitesStore +import io.element.android.features.invite.api.SeenInvitesStore +import io.element.android.features.invite.test.FakeSeenInvitesStore import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize diff --git a/features/invitelist/test/build.gradle.kts b/features/invite/test/build.gradle.kts similarity index 88% rename from features/invitelist/test/build.gradle.kts rename to features/invite/test/build.gradle.kts index ce9b0dabe4..44d9d3030c 100644 --- a/features/invitelist/test/build.gradle.kts +++ b/features/invite/test/build.gradle.kts @@ -19,11 +19,11 @@ plugins { } android { - namespace = "io.element.android.features.invitelist.test" + namespace = "io.element.android.features.invite.test" } dependencies { implementation(libs.coroutines.core) implementation(projects.libraries.matrix.api) - api(projects.features.invitelist.api) + api(projects.features.invite.api) } diff --git a/features/invitelist/test/src/main/kotlin/io/element/android/features/invitelist/test/FakeSeenInvitesStore.kt b/features/invite/test/src/main/kotlin/io/element/android/features/invite/test/FakeSeenInvitesStore.kt similarity index 90% rename from features/invitelist/test/src/main/kotlin/io/element/android/features/invitelist/test/FakeSeenInvitesStore.kt rename to features/invite/test/src/main/kotlin/io/element/android/features/invite/test/FakeSeenInvitesStore.kt index 94b232b2ac..f2a21ac768 100644 --- a/features/invitelist/test/src/main/kotlin/io/element/android/features/invitelist/test/FakeSeenInvitesStore.kt +++ b/features/invite/test/src/main/kotlin/io/element/android/features/invite/test/FakeSeenInvitesStore.kt @@ -14,9 +14,9 @@ * limitations under the License. */ -package io.element.android.features.invitelist.test +package io.element.android.features.invite.test -import io.element.android.features.invitelist.api.SeenInvitesStore +import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.libraries.matrix.api.core.RoomId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow diff --git a/features/roomlist/impl/build.gradle.kts b/features/roomlist/impl/build.gradle.kts index 207be1df5b..ff67ebe42a 100644 --- a/features/roomlist/impl/build.gradle.kts +++ b/features/roomlist/impl/build.gradle.kts @@ -53,7 +53,7 @@ dependencies { implementation(projects.libraries.indicator.api) implementation(projects.libraries.deeplink) implementation(projects.libraries.preferences.api) - implementation(projects.features.invitelist.api) + implementation(projects.features.invite.api) implementation(projects.features.networkmonitor.api) implementation(projects.features.leaveroom.api) implementation(projects.services.analytics.api) @@ -75,7 +75,7 @@ dependencies { testImplementation(projects.libraries.indicator.impl) testImplementation(projects.libraries.permissions.noop) testImplementation(projects.libraries.preferences.test) - testImplementation(projects.features.invitelist.test) + testImplementation(projects.features.invite.test) testImplementation(projects.services.analytics.test) testImplementation(projects.features.networkmonitor.test) testImplementation(projects.tests.testutils) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/DefaultInviteStateDataSource.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/DefaultInviteStateDataSource.kt index 9057bf23ae..16baf4e41b 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/DefaultInviteStateDataSource.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/DefaultInviteStateDataSource.kt @@ -24,7 +24,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.invitelist.api.SeenInvitesStore +import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.features.roomlist.impl.InvitesState import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.di.SessionScope diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/DefaultInviteStateDataSourceTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/DefaultInviteStateDataSourceTest.kt index 802d8ee40f..a1e08cd93b 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/DefaultInviteStateDataSourceTest.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/DefaultInviteStateDataSourceTest.kt @@ -20,7 +20,7 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.invitelist.test.FakeSeenInvitesStore +import io.element.android.features.invite.test.FakeSeenInvitesStore import io.element.android.features.roomlist.impl.InvitesState import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID_2 diff --git a/samples/minimal/build.gradle.kts b/samples/minimal/build.gradle.kts index 7a3540e7eb..402d1a48b8 100644 --- a/samples/minimal/build.gradle.kts +++ b/samples/minimal/build.gradle.kts @@ -56,7 +56,7 @@ dependencies { implementation(projects.libraries.eventformatter.impl) implementation(projects.libraries.preferences.impl) implementation(projects.libraries.indicator.impl) - implementation(projects.features.invitelist.impl) + implementation(projects.features.invite.impl) implementation(projects.features.roomlist.impl) implementation(projects.features.leaveroom.impl) implementation(projects.features.login.impl) diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt index 264a754e73..48f629af4d 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt @@ -20,7 +20,7 @@ import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Modifier -import io.element.android.features.invitelist.impl.DefaultSeenInvitesStore +import io.element.android.features.invite.impl.DefaultSeenInvitesStore import io.element.android.features.leaveroom.impl.LeaveRoomPresenterImpl import io.element.android.features.networkmonitor.impl.NetworkMonitorImpl import io.element.android.features.roomlist.impl.RoomListPresenter diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 566ea071e0..35e13730c2 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -33,7 +33,7 @@ ] }, { - "name" : ":features:invitelist:impl", + "name" : ":features:invite:impl", "includeRegex" : [ "screen_invites_.*" ] From 5b8690d32e7cf5132d170700fc60da79d5e46eea Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 8 Apr 2024 21:18:25 +0200 Subject: [PATCH 07/25] Room navigation : refactor Invites so we can use it in other places --- .../impl/DefaultInviteListEntryPoint.kt | 1 + .../impl/{ => invitelist}/InviteListEvents.kt | 10 +- .../impl/{ => invitelist}/InviteListNode.kt | 5 +- .../{ => invitelist}/InviteListPresenter.kt | 89 +++---------- .../impl/{ => invitelist}/InviteListState.kt | 16 +-- .../InviteListStateProvider.kt | 37 +++--- .../impl/{ => invitelist}/InviteListView.kt | 71 ++--------- .../response/AcceptDeclineInviteEvents.kt | 26 ++++ .../impl/response/AcceptDeclineInviteNode.kt | 46 +++++++ .../response/AcceptDeclineInvitePresenter.kt | 120 ++++++++++++++++++ .../impl/response/AcceptDeclineInviteState.kt | 34 +++++ .../AcceptDeclineInviteStateProvider.kt | 59 +++++++++ .../impl/response/AcceptDeclineInviteView.kt | 111 ++++++++++++++++ .../invite/impl/InviteListPresenterTests.kt | 3 + 14 files changed, 463 insertions(+), 165 deletions(-) rename features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/{ => invitelist}/InviteListEvents.kt (73%) rename features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/{ => invitelist}/InviteListNode.kt (93%) rename features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/{ => invitelist}/InviteListPresenter.kt (56%) rename features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/{ => invitelist}/InviteListState.kt (61%) rename features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/{ => invitelist}/InviteListStateProvider.kt (64%) rename features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/{ => invitelist}/InviteListView.kt (65%) create mode 100644 features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteEvents.kt create mode 100644 features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteNode.kt create mode 100644 features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenter.kt create mode 100644 features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteState.kt create mode 100644 features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteStateProvider.kt create mode 100644 features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteView.kt diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DefaultInviteListEntryPoint.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DefaultInviteListEntryPoint.kt index 03e7a57dae..5e464a79bc 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DefaultInviteListEntryPoint.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DefaultInviteListEntryPoint.kt @@ -21,6 +21,7 @@ import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.invite.api.InviteListEntryPoint +import io.element.android.features.invite.impl.invitelist.InviteListNode import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.AppScope import javax.inject.Inject diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/InviteListEvents.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListEvents.kt similarity index 73% rename from features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/InviteListEvents.kt rename to features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListEvents.kt index 7bf58aba7a..f4ba30844a 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/InviteListEvents.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListEvents.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 New Vector Ltd + * 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. @@ -14,17 +14,11 @@ * limitations under the License. */ -package io.element.android.features.invite.impl +package io.element.android.features.invite.impl.invitelist import io.element.android.features.invite.impl.model.InviteListInviteSummary sealed interface InviteListEvents { data class AcceptInvite(val invite: InviteListInviteSummary) : InviteListEvents data class DeclineInvite(val invite: InviteListInviteSummary) : InviteListEvents - - data object ConfirmDeclineInvite : InviteListEvents - data object CancelDeclineInvite : InviteListEvents - - data object DismissAcceptError : InviteListEvents - data object DismissDeclineError : InviteListEvents } diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/InviteListNode.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListNode.kt similarity index 93% rename from features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/InviteListNode.kt rename to features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListNode.kt index 1f3236e916..53c5b31095 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/InviteListNode.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListNode.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 New Vector Ltd + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.invite.impl +package io.element.android.features.invite.impl.invitelist import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -50,6 +50,7 @@ class InviteListNode @AssistedInject constructor( state = state, onBackClicked = ::onBackClicked, onInviteAccepted = ::onInviteAccepted, + onInviteDeclined = {} ) } } diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/InviteListPresenter.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListPresenter.kt similarity index 56% rename from features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/InviteListPresenter.kt rename to features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListPresenter.kt index f616450d25..a46f135aa3 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/InviteListPresenter.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListPresenter.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 New Vector Ltd + * 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. @@ -14,43 +14,35 @@ * limitations under the License. */ -package io.element.android.features.invite.impl +package io.element.android.features.invite.impl.invitelist import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import im.vector.app.features.analytics.plan.JoinedRoom import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.features.invite.impl.model.InviteListInviteSummary import io.element.android.features.invite.impl.model.InviteSender -import io.element.android.libraries.architecture.AsyncData +import io.element.android.features.invite.impl.response.AcceptDeclineInviteEvents +import io.element.android.features.invite.impl.response.AcceptDeclineInvitePresenter +import io.element.android.features.invite.impl.response.InviteData import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.roomlist.RoomSummary -import io.element.android.libraries.push.api.notifications.NotificationDrawerManager -import io.element.android.services.analytics.api.AnalyticsService -import io.element.android.services.analytics.api.extensions.toAnalyticsJoinedRoom import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch import javax.inject.Inject class InviteListPresenter @Inject constructor( private val client: MatrixClient, private val store: SeenInvitesStore, - private val analyticsService: AnalyticsService, - private val notificationDrawerManager: NotificationDrawerManager, + private val acceptDeclineInvitePresenter: AcceptDeclineInvitePresenter, ) : Presenter { @Composable override fun present(): InviteListState { @@ -75,40 +67,20 @@ class InviteListPresenter @Inject constructor( ) } - val localCoroutineScope = rememberCoroutineScope() - val acceptedAction: MutableState> = remember { mutableStateOf(AsyncData.Uninitialized) } - val declinedAction: MutableState> = remember { mutableStateOf(AsyncData.Uninitialized) } - val decliningInvite: MutableState = remember { mutableStateOf(null) } + val acceptDeclineInviteState = acceptDeclineInvitePresenter.present() fun handleEvent(event: InviteListEvents) { when (event) { is InviteListEvents.AcceptInvite -> { - acceptedAction.value = AsyncData.Uninitialized - localCoroutineScope.acceptInvite(event.invite.roomId, acceptedAction) + acceptDeclineInviteState.eventSink( + AcceptDeclineInviteEvents.AcceptInvite(event.invite.toInviteData()) + ) } is InviteListEvents.DeclineInvite -> { - decliningInvite.value = event.invite - } - - is InviteListEvents.ConfirmDeclineInvite -> { - declinedAction.value = AsyncData.Uninitialized - decliningInvite.value?.let { - localCoroutineScope.declineInvite(it.roomId, declinedAction) - } - decliningInvite.value = null - } - - is InviteListEvents.CancelDeclineInvite -> { - decliningInvite.value = null - } - - is InviteListEvents.DismissAcceptError -> { - acceptedAction.value = AsyncData.Uninitialized - } - - is InviteListEvents.DismissDeclineError -> { - declinedAction.value = AsyncData.Uninitialized + acceptDeclineInviteState.eventSink( + AcceptDeclineInviteEvents.DeclineInvite(event.invite.toInviteData()) + ) } } } @@ -124,38 +96,11 @@ class InviteListPresenter @Inject constructor( return InviteListState( inviteList = inviteList, - declineConfirmationDialog = decliningInvite.value?.let { - InviteDeclineConfirmationDialog.Visible( - isDirect = it.isDirect, - name = it.roomName, - ) - } ?: InviteDeclineConfirmationDialog.Hidden, - acceptedAction = acceptedAction.value, - declinedAction = declinedAction.value, + acceptDeclineInviteState = acceptDeclineInviteState, eventSink = ::handleEvent ) } - private fun CoroutineScope.acceptInvite(roomId: RoomId, acceptedAction: MutableState>) = launch { - suspend { - client.getRoom(roomId)?.use { - it.join().getOrThrow() - notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId, doRender = true) - analyticsService.capture(it.toAnalyticsJoinedRoom(JoinedRoom.Trigger.Invite)) - } - roomId - }.runCatchingUpdatingState(acceptedAction) - } - - private fun CoroutineScope.declineInvite(roomId: RoomId, declinedAction: MutableState>) = launch { - suspend { - client.getRoom(roomId)?.use { - it.leave().getOrThrow() - notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId, doRender = true) - }.let { } - }.runCatchingUpdatingState(declinedAction) - } - private fun RoomSummary.Filled.toInviteSummary(seen: Boolean) = details.run { val i = inviter val avatarData = if (isDirect && i != null) { @@ -203,4 +148,10 @@ class InviteListPresenter @Inject constructor( }, ) } + + private fun InviteListInviteSummary.toInviteData() = InviteData( + roomId = roomId, + roomName = roomName, + isDirect = isDirect, + ) } diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/InviteListState.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListState.kt similarity index 61% rename from features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/InviteListState.kt rename to features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListState.kt index 42d73de547..60d8778c33 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/InviteListState.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListState.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 New Vector Ltd + * 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. @@ -14,24 +14,16 @@ * limitations under the License. */ -package io.element.android.features.invite.impl +package io.element.android.features.invite.impl.invitelist import androidx.compose.runtime.Immutable +import io.element.android.features.invite.impl.response.AcceptDeclineInviteState import io.element.android.features.invite.impl.model.InviteListInviteSummary -import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.matrix.api.core.RoomId import kotlinx.collections.immutable.ImmutableList @Immutable data class InviteListState( val inviteList: ImmutableList, - val declineConfirmationDialog: InviteDeclineConfirmationDialog, - val acceptedAction: AsyncData, - val declinedAction: AsyncData, + val acceptDeclineInviteState: AcceptDeclineInviteState, val eventSink: (InviteListEvents) -> Unit ) - -sealed interface InviteDeclineConfirmationDialog { - data object Hidden : InviteDeclineConfirmationDialog - data class Visible(val isDirect: Boolean, val name: String) : InviteDeclineConfirmationDialog -} diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/InviteListStateProvider.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListStateProvider.kt similarity index 64% rename from features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/InviteListStateProvider.kt rename to features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListStateProvider.kt index 5f1d5dcc0f..11902e4673 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/InviteListStateProvider.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListStateProvider.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 New Vector Ltd + * 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. @@ -14,35 +14,40 @@ * limitations under the License. */ -package io.element.android.features.invite.impl +package io.element.android.features.invite.impl.invitelist import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.invite.impl.model.InviteListInviteSummary import io.element.android.features.invite.impl.model.InviteSender -import io.element.android.libraries.architecture.AsyncData +import io.element.android.features.invite.impl.response.AcceptDeclineInviteState +import io.element.android.features.invite.impl.response.AcceptDeclineInviteStateProvider +import io.element.android.features.invite.impl.response.anAcceptDeclineInviteState import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf open class InviteListStateProvider : PreviewParameterProvider { + + private val acceptDeclineInviteStateProvider = AcceptDeclineInviteStateProvider() + override val values: Sequence get() = sequenceOf( - aInviteListState(), - aInviteListState().copy(inviteList = persistentListOf()), - aInviteListState().copy(declineConfirmationDialog = InviteDeclineConfirmationDialog.Visible(true, "Alice")), - aInviteListState().copy(declineConfirmationDialog = InviteDeclineConfirmationDialog.Visible(false, "Some Room")), - aInviteListState().copy(acceptedAction = AsyncData.Failure(Throwable("Whoops"))), - aInviteListState().copy(declinedAction = AsyncData.Failure(Throwable("Whoops"))), - ) + anInviteListState(), + anInviteListState(inviteList = persistentListOf()), + ) + acceptDeclineInviteStateProvider.values.map { acceptDeclineInviteState -> + anInviteListState(acceptDeclineInviteState = acceptDeclineInviteState) + } } -internal fun aInviteListState() = InviteListState( - inviteList = aInviteListInviteSummaryList(), - declineConfirmationDialog = InviteDeclineConfirmationDialog.Hidden, - acceptedAction = AsyncData.Uninitialized, - declinedAction = AsyncData.Uninitialized, - eventSink = {}, +internal fun anInviteListState( + inviteList: ImmutableList = aInviteListInviteSummaryList(), + acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(), + eventSink: (InviteListEvents) -> Unit = {} +) = InviteListState( + inviteList = inviteList, + acceptDeclineInviteState = acceptDeclineInviteState, + eventSink = eventSink, ) internal fun aInviteListInviteSummaryList(): ImmutableList { diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/InviteListView.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListView.kt similarity index 65% rename from features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/InviteListView.kt rename to features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListView.kt index 34da18f02f..2ad75e43eb 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/InviteListView.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListView.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 New Vector Ltd + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.invite.impl +package io.element.android.features.invite.impl.invitelist import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -27,20 +27,16 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme +import io.element.android.features.invite.impl.R import io.element.android.features.invite.impl.components.InviteSummaryRow -import io.element.android.libraries.architecture.AsyncData +import io.element.android.features.invite.impl.response.AcceptDeclineInviteView import io.element.android.libraries.designsystem.components.button.BackButton -import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog -import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.aliasScreenTitle @@ -56,61 +52,19 @@ fun InviteListView( state: InviteListState, onBackClicked: () -> Unit, onInviteAccepted: (RoomId) -> Unit, + onInviteDeclined: (RoomId) -> Unit, modifier: Modifier = Modifier, ) { - if (state.acceptedAction is AsyncData.Success) { - val latestOnInviteAccepted by rememberUpdatedState(onInviteAccepted) - LaunchedEffect(state.acceptedAction) { - latestOnInviteAccepted(state.acceptedAction.data) - } - } - InviteListContent( state = state, modifier = modifier, onBackClicked = onBackClicked, ) - - if (state.declineConfirmationDialog is InviteDeclineConfirmationDialog.Visible) { - val contentResource = if (state.declineConfirmationDialog.isDirect) { - R.string.screen_invites_decline_direct_chat_message - } else { - R.string.screen_invites_decline_chat_message - } - - val titleResource = if (state.declineConfirmationDialog.isDirect) { - R.string.screen_invites_decline_direct_chat_title - } else { - R.string.screen_invites_decline_chat_title - } - - ConfirmationDialog( - content = stringResource(contentResource, state.declineConfirmationDialog.name), - title = stringResource(titleResource), - submitText = stringResource(CommonStrings.action_decline), - cancelText = stringResource(CommonStrings.action_cancel), - onSubmitClicked = { state.eventSink(InviteListEvents.ConfirmDeclineInvite) }, - onDismiss = { state.eventSink(InviteListEvents.CancelDeclineInvite) } - ) - } - - if (state.acceptedAction is AsyncData.Failure) { - ErrorDialog( - content = stringResource(CommonStrings.error_unknown), - title = stringResource(CommonStrings.common_error), - submitText = stringResource(CommonStrings.action_ok), - onDismiss = { state.eventSink(InviteListEvents.DismissAcceptError) } - ) - } - - if (state.declinedAction is AsyncData.Failure) { - ErrorDialog( - content = stringResource(CommonStrings.error_unknown), - title = stringResource(CommonStrings.common_error), - submitText = stringResource(CommonStrings.action_ok), - onDismiss = { state.eventSink(InviteListEvents.DismissDeclineError) } - ) - } + AcceptDeclineInviteView( + state = state.acceptDeclineInviteState, + onInviteAccepted = onInviteAccepted, + onInviteDeclined = onInviteDeclined, + ) } @OptIn(ExperimentalMaterial3Api::class) @@ -138,8 +92,8 @@ private fun InviteListContent( content = { padding -> Column( modifier = Modifier - .padding(padding) - .consumeWindowInsets(padding) + .padding(padding) + .consumeWindowInsets(padding) ) { if (state.inviteList.isEmpty()) { Spacer(Modifier.size(80.dp)) @@ -181,5 +135,6 @@ internal fun InviteListViewPreview(@PreviewParameter(InviteListStateProvider::cl state = state, onBackClicked = {}, onInviteAccepted = {}, + onInviteDeclined = {}, ) } diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteEvents.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteEvents.kt new file mode 100644 index 0000000000..17d60cca37 --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteEvents.kt @@ -0,0 +1,26 @@ +/* + * 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.invite.impl.response + +sealed interface AcceptDeclineInviteEvents { + data class AcceptInvite(val invite: InviteData) : AcceptDeclineInviteEvents + data class DeclineInvite(val invite: InviteData) : AcceptDeclineInviteEvents + data object ConfirmDeclineInvite : AcceptDeclineInviteEvents + data object CancelDeclineInvite : AcceptDeclineInviteEvents + data object DismissAcceptError : AcceptDeclineInviteEvents + data object DismissDeclineError : AcceptDeclineInviteEvents +} diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteNode.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteNode.kt new file mode 100644 index 0000000000..4718b38881 --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteNode.kt @@ -0,0 +1,46 @@ +/* + * 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.invite.impl.response + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.di.SessionScope + +@ContributesNode(SessionScope::class) +class AcceptDeclineInviteNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: AcceptDeclineInvitePresenter, +) : Node(buildContext, plugins = plugins) { + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + AcceptDeclineInviteView( + state = state, + onInviteAccepted = {}, + onInviteDeclined = {}, + modifier = modifier + ) + } +} diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenter.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenter.kt new file mode 100644 index 0000000000..38302377f1 --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenter.kt @@ -0,0 +1,120 @@ +/* + * 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.invite.impl.response + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import im.vector.app.features.analytics.plan.JoinedRoom +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.runCatchingUpdatingState +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.push.api.notifications.NotificationDrawerManager +import io.element.android.services.analytics.api.AnalyticsService +import io.element.android.services.analytics.api.extensions.toAnalyticsJoinedRoom +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import java.util.Optional +import javax.inject.Inject +import kotlin.jvm.optionals.getOrNull + +class AcceptDeclineInvitePresenter @Inject constructor( + private val client: MatrixClient, + private val analyticsService: AnalyticsService, + private val notificationDrawerManager: NotificationDrawerManager, +) : Presenter { + + @Composable + override fun present(): AcceptDeclineInviteState { + + val localCoroutineScope = rememberCoroutineScope() + val acceptedAction: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } + val declinedAction: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } + var currentInvite by remember { + mutableStateOf>(Optional.empty()) + } + + fun handleEvents(event: AcceptDeclineInviteEvents) { + when (event) { + is AcceptDeclineInviteEvents.AcceptInvite -> { + currentInvite = Optional.of(event.invite) + localCoroutineScope.acceptInvite(event.invite.roomId, acceptedAction) + } + + is AcceptDeclineInviteEvents.DeclineInvite -> { + currentInvite = Optional.of(event.invite) + declinedAction.value = AsyncAction.Confirming + } + + is AcceptDeclineInviteEvents.ConfirmDeclineInvite -> { + declinedAction.value = AsyncAction.Uninitialized + currentInvite.getOrNull()?.let { + localCoroutineScope.declineInvite(it.roomId, declinedAction) + } + currentInvite = Optional.empty() + } + + is AcceptDeclineInviteEvents.CancelDeclineInvite -> { + currentInvite = Optional.empty() + declinedAction.value = AsyncAction.Uninitialized + } + + is AcceptDeclineInviteEvents.DismissAcceptError -> { + acceptedAction.value = AsyncAction.Uninitialized + } + + is AcceptDeclineInviteEvents.DismissDeclineError -> { + declinedAction.value = AsyncAction.Uninitialized + } + } + } + + return AcceptDeclineInviteState( + invite = currentInvite, + acceptAction = acceptedAction.value, + declineAction = declinedAction.value, + eventSink = ::handleEvents + ) + } + + private fun CoroutineScope.acceptInvite(roomId: RoomId, acceptedAction: MutableState>) = launch { + suspend { + client.getRoom(roomId)?.use { + it.join().getOrThrow() + notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId, doRender = true) + analyticsService.capture(it.toAnalyticsJoinedRoom(JoinedRoom.Trigger.Invite)) + } + roomId + }.runCatchingUpdatingState(acceptedAction) + } + + private fun CoroutineScope.declineInvite(roomId: RoomId, declinedAction: MutableState>) = launch { + suspend { + client.getRoom(roomId)?.use { + it.leave().getOrThrow() + notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId, doRender = true) + } + roomId + }.runCatchingUpdatingState(declinedAction) + } +} diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteState.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteState.kt new file mode 100644 index 0000000000..11b960f57b --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteState.kt @@ -0,0 +1,34 @@ +/* + * 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.invite.impl.response + +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.core.RoomId +import java.util.Optional + +data class AcceptDeclineInviteState( + val invite: Optional, + val acceptAction: AsyncAction, + val declineAction: AsyncAction, + val eventSink: (AcceptDeclineInviteEvents) -> Unit +) + +data class InviteData( + val roomId: RoomId, + val roomName: String, + val isDirect: Boolean, +) diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteStateProvider.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteStateProvider.kt new file mode 100644 index 0000000000..710c8b7538 --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteStateProvider.kt @@ -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.invite.impl.response + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.core.RoomId +import java.util.Optional + +open class AcceptDeclineInviteStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + anAcceptDeclineInviteState(), + anAcceptDeclineInviteState( + invite = Optional.of( + InviteData(RoomId(""), isDirect = true, roomName = "Alice"), + ), + declineAction = AsyncAction.Confirming, + ), + anAcceptDeclineInviteState( + invite = Optional.of( + InviteData(RoomId(""), isDirect = false, roomName = "Some room"), + ), + declineAction = AsyncAction.Confirming, + ), + anAcceptDeclineInviteState( + acceptAction = AsyncAction.Failure(Throwable("Whoops")), + ), + anAcceptDeclineInviteState( + declineAction = AsyncAction.Failure(Throwable("Whoops")), + ), + ) +} + +fun anAcceptDeclineInviteState( + invite: Optional = Optional.empty(), + acceptAction: AsyncAction = AsyncAction.Uninitialized, + declineAction: AsyncAction = AsyncAction.Uninitialized, + eventSink: (AcceptDeclineInviteEvents) -> Unit = {} +) = AcceptDeclineInviteState( + invite = invite, + acceptAction = acceptAction, + declineAction = declineAction, + eventSink = eventSink, +) diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteView.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteView.kt new file mode 100644 index 0000000000..a214fed4a2 --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteView.kt @@ -0,0 +1,111 @@ +/* + * 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.invite.impl.response + +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.features.invite.impl.R +import io.element.android.libraries.designsystem.components.async.AsyncActionView +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.ui.strings.CommonStrings +import kotlin.jvm.optionals.getOrNull + +@Composable +fun AcceptDeclineInviteView( + state: AcceptDeclineInviteState, + onInviteAccepted: (RoomId) -> Unit, + onInviteDeclined: (RoomId) -> Unit, + modifier: Modifier = Modifier, +) { + Box(modifier = modifier) { + AsyncActionView( + async = state.acceptAction, + onSuccess = onInviteAccepted, + onErrorDismiss = { + state.eventSink(AcceptDeclineInviteEvents.DismissAcceptError) + }, + ) + AsyncActionView( + async = state.declineAction, + onSuccess = onInviteDeclined, + onErrorDismiss = { + state.eventSink(AcceptDeclineInviteEvents.DismissDeclineError) + }, + confirmationDialog = { + val invite = state.invite.getOrNull() + if (invite != null) { + DeclineConfirmationDialog( + invite = invite, + onConfirmClicked = { + state.eventSink(AcceptDeclineInviteEvents.ConfirmDeclineInvite) + }, + onDismissClicked = { + state.eventSink(AcceptDeclineInviteEvents.CancelDeclineInvite) + } + ) + } + } + ) + } +} + +@Composable +private fun DeclineConfirmationDialog( + invite: InviteData, + onConfirmClicked: () -> Unit, + onDismissClicked: () -> Unit, + modifier: Modifier = Modifier +) { + val contentResource = if (invite.isDirect) { + R.string.screen_invites_decline_direct_chat_message + } else { + R.string.screen_invites_decline_chat_message + } + + val titleResource = if (invite.isDirect) { + R.string.screen_invites_decline_direct_chat_title + } else { + R.string.screen_invites_decline_chat_title + } + + ConfirmationDialog( + modifier = modifier, + content = stringResource(contentResource, invite.roomName), + title = stringResource(titleResource), + submitText = stringResource(CommonStrings.action_decline), + cancelText = stringResource(CommonStrings.action_cancel), + onSubmitClicked = onConfirmClicked, + onDismiss = onDismissClicked, + ) +} + +@PreviewLightDark +@Composable +internal fun AcceptDeclineInviteViewLightPreview(@PreviewParameter(AcceptDeclineInviteStateProvider::class) state: AcceptDeclineInviteState) = + ElementPreview { + AcceptDeclineInviteView( + state = state, + onInviteAccepted = {}, + onInviteDeclined = {}, + ) + } diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/InviteListPresenterTests.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/InviteListPresenterTests.kt index b9acd5eb0b..8e192e01ed 100644 --- a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/InviteListPresenterTests.kt +++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/InviteListPresenterTests.kt @@ -22,6 +22,9 @@ import app.cash.turbine.TurbineTestContext import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.invite.api.SeenInvitesStore +import io.element.android.features.invite.impl.invitelist.InviteListEvents +import io.element.android.features.invite.impl.invitelist.InviteListPresenter +import io.element.android.features.invite.impl.invitelist.InviteListState import io.element.android.features.invite.test.FakeSeenInvitesStore import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.avatar.AvatarData From 9ff0e8eccb13aaca1850c0227b41829155fab25b Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 9 Apr 2024 14:16:17 +0200 Subject: [PATCH 08/25] Room navigation : reuse accept/decline presentation logic --- .../android/appnav/LoggedInFlowNode.kt | 4 +++ .../android/appnav/room/join/JoinRoomNode.kt | 8 +++++ .../appnav/room/join/JoinRoomPresenter.kt | 36 ++++++++++++------- .../android/appnav/room/join/JoinRoomState.kt | 8 ++--- .../appnav/room/join/JoinRoomStateProvider.kt | 7 ++-- .../appnav/room/join/di/JoinRoomModule.kt | 5 ++- features/invite/api/build.gradle.kts | 2 +- .../invite/api/InviteListEntryPoint.kt | 2 +- .../response/AcceptDeclineInviteEvents.kt | 8 ++--- .../response/AcceptDeclineInvitePresenter.kt | 21 +++++++++++ .../api}/response/AcceptDeclineInviteState.kt | 10 ++---- .../AcceptDeclineInviteStateProvider.kt | 2 +- .../api/response/AcceptDeclineInviteView.kt | 31 ++++++++++++++++ .../invite/api/response/InviteData.kt | 25 +++++++++++++ .../invite/impl/invitelist/InviteListNode.kt | 7 +++- .../impl/invitelist/InviteListPresenter.kt | 6 ++-- .../invite/impl/invitelist/InviteListState.kt | 2 +- .../invitelist/InviteListStateProvider.kt | 6 ++-- .../invite/impl/invitelist/InviteListView.kt | 8 +++++ .../impl/response/AcceptDeclineInviteView.kt | 11 +++--- .../DefaultAcceptDeclineInviteEvents.kt | 26 ++++++++++++++ ...=> DefaultAcceptDeclineInvitePresenter.kt} | 33 ++++++++++------- ...e.kt => DefaultAcceptDeclineInviteView.kt} | 31 ++++++++-------- 23 files changed, 220 insertions(+), 79 deletions(-) rename features/invite/{impl/src/main/kotlin/io/element/android/features/invite/impl => api/src/main/kotlin/io/element/android/features/invite/api}/response/AcceptDeclineInviteEvents.kt (68%) create mode 100644 features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInvitePresenter.kt rename features/invite/{impl/src/main/kotlin/io/element/android/features/invite/impl => api/src/main/kotlin/io/element/android/features/invite/api}/response/AcceptDeclineInviteState.kt (80%) rename features/invite/{impl/src/main/kotlin/io/element/android/features/invite/impl => api/src/main/kotlin/io/element/android/features/invite/api}/response/AcceptDeclineInviteStateProvider.kt (97%) create mode 100644 features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteView.kt create mode 100644 features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/InviteData.kt create mode 100644 features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/DefaultAcceptDeclineInviteEvents.kt rename features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/{AcceptDeclineInvitePresenter.kt => DefaultAcceptDeclineInvitePresenter.kt} (78%) rename features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/{AcceptDeclineInviteNode.kt => DefaultAcceptDeclineInviteView.kt} (56%) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index 8ba98e5a6b..4b82cb6992 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -350,6 +350,10 @@ class LoggedInFlowNode @AssistedInject constructor( backstack.pop() } + override fun onInviteClicked(roomId: RoomId) { + backstack.push(NavTarget.Room(roomId)) + } + override fun onInviteAccepted(roomId: RoomId) { backstack.push(NavTarget.Room(roomId)) } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomNode.kt index cccc66ddfb..3574b37e2d 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomNode.kt @@ -24,6 +24,7 @@ import com.bumble.appyx.core.plugin.Plugin import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.invite.api.response.AcceptDeclineInviteView import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.SessionScope @@ -34,6 +35,7 @@ class JoinRoomNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: JoinRoomPresenter.Factory, + private val acceptDeclineInviteView: AcceptDeclineInviteView, ) : Node(buildContext, plugins = plugins) { data class Inputs( @@ -51,5 +53,11 @@ class JoinRoomNode @AssistedInject constructor( onBackPressed = ::navigateUp, modifier = modifier ) + acceptDeclineInviteView.Render( + state = state.acceptDeclineInviteState, + onInviteAccepted = {}, + onInviteDeclined = { navigateUp() }, + modifier = Modifier + ) } } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomPresenter.kt index d80fb99b3e..60a59652bf 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomPresenter.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomPresenter.kt @@ -20,16 +20,17 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState -import androidx.compose.runtime.rememberCoroutineScope import dagger.assisted.Assisted import dagger.assisted.AssistedInject +import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents +import io.element.android.features.invite.api.response.AcceptDeclineInvitePresenter +import io.element.android.features.invite.api.response.InviteData import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter 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.CurrentUserMembership import io.element.android.libraries.matrix.api.roomlist.RoomListService -import kotlinx.coroutines.launch import java.util.Optional import kotlin.jvm.optionals.getOrNull @@ -37,6 +38,7 @@ class JoinRoomPresenter @AssistedInject constructor( @Assisted private val roomId: RoomId, private val matrixClient: MatrixClient, private val roomListService: RoomListService, + private val acceptDeclineInvitePresenter: AcceptDeclineInvitePresenter, ) : Presenter { interface Factory { @@ -47,6 +49,7 @@ class JoinRoomPresenter @AssistedInject constructor( override fun present(): JoinRoomState { val userMembership by roomListService.getUserMembershipForRoom(roomId).collectAsState(initial = Optional.empty()) val joinAuthorisationStatus = joinAuthorisationStatus(userMembership) + val acceptDeclineInviteState = acceptDeclineInvitePresenter.present() val roomInfo by produceState>(initialValue = AsyncData.Uninitialized, key1 = userMembership) { value = when { userMembership.isPresent -> { @@ -56,6 +59,7 @@ class JoinRoomPresenter @AssistedInject constructor( roomName = it.displayName, roomAlias = it.alias, memberCount = it.activeMemberCount, + isDirect = it.isDirect, roomAvatarUrl = it.avatarUrl ) } @@ -65,21 +69,17 @@ class JoinRoomPresenter @AssistedInject constructor( } } - val coroutineScope = rememberCoroutineScope() - fun handleEvents(event: JoinRoomEvents) { when (event) { JoinRoomEvents.AcceptInvite, JoinRoomEvents.JoinRoom -> { - coroutineScope.launch { - matrixClient.joinRoom(roomId) - } + acceptDeclineInviteState.eventSink( + AcceptDeclineInviteEvents.AcceptInvite(roomInfo.toInviteData()) + ) } JoinRoomEvents.DeclineInvite -> { - coroutineScope.launch { - matrixClient.getRoom(roomId)?.use { - it.leave() - } - } + acceptDeclineInviteState.eventSink( + AcceptDeclineInviteEvents.DeclineInvite(roomInfo.toInviteData()) + ) } } } @@ -87,11 +87,21 @@ class JoinRoomPresenter @AssistedInject constructor( return JoinRoomState( roomInfo = roomInfo, joinAuthorisationStatus = joinAuthorisationStatus, - currentAction = CurrentAction.None, + acceptDeclineInviteState = acceptDeclineInviteState, eventSink = ::handleEvents ) } + private fun AsyncData.toInviteData(): InviteData { + return dataOrNull().let { + InviteData( + roomId = roomId, + roomName = it?.roomName ?: "", + isDirect = it?.isDirect ?: false + ) + } + } + @Composable private fun joinAuthorisationStatus(userMembership: Optional): JoinAuthorisationStatus { return when { diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomState.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomState.kt index 2e95987d97..ca9f71baba 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomState.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomState.kt @@ -17,6 +17,7 @@ package io.element.android.appnav.room.join import androidx.compose.runtime.Immutable +import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize @@ -26,7 +27,7 @@ import io.element.android.libraries.matrix.api.core.RoomId data class JoinRoomState( val roomInfo: AsyncData, val joinAuthorisationStatus: JoinAuthorisationStatus, - val currentAction: CurrentAction, + val acceptDeclineInviteState: AcceptDeclineInviteState, val eventSink: (JoinRoomEvents) -> Unit ){ val showMemberCount = roomInfo.dataOrNull()?.memberCount != null @@ -37,6 +38,7 @@ data class RoomInfo( val roomName: String, val roomAlias: String?, val memberCount: Long?, + val isDirect: Boolean, val roomAvatarUrl: String?, ) { fun avatarData(size: AvatarSize): AvatarData { @@ -55,7 +57,3 @@ enum class JoinAuthorisationStatus { CanJoin, Unknown, } - -sealed interface CurrentAction { - data object None : CurrentAction -} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomStateProvider.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomStateProvider.kt index 928edb4ce4..6ac24cbdcb 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomStateProvider.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomStateProvider.kt @@ -17,6 +17,8 @@ package io.element.android.appnav.room.join import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.invite.api.response.AcceptDeclineInviteState +import io.element.android.features.invite.api.response.anAcceptDeclineInviteState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.core.RoomId @@ -45,16 +47,17 @@ fun aJoinRoomState( roomName = "Element x android", roomAlias = "#exa:matrix.org", memberCount = null, + isDirect = false, roomAvatarUrl = null ) ), joinAuthorisationStatus: JoinAuthorisationStatus = JoinAuthorisationStatus.Unknown, - currentAction: CurrentAction = CurrentAction.None, + acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(), eventSink: (JoinRoomEvents) -> Unit = {} ) = JoinRoomState( roomInfo = roomInfo, joinAuthorisationStatus = joinAuthorisationStatus, - currentAction = currentAction, + acceptDeclineInviteState = acceptDeclineInviteState, eventSink = eventSink ) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/join/di/JoinRoomModule.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/join/di/JoinRoomModule.kt index fa7635a312..15b7f72ef0 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/join/di/JoinRoomModule.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/join/di/JoinRoomModule.kt @@ -20,6 +20,7 @@ import com.squareup.anvil.annotations.ContributesTo import dagger.Module import dagger.Provides import io.element.android.appnav.room.join.JoinRoomPresenter +import io.element.android.features.invite.api.response.AcceptDeclineInvitePresenter import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId @@ -32,13 +33,15 @@ object JoinRoomModule { fun providesJoinRoomPresenterFactory( roomListService: RoomListService, client: MatrixClient, + acceptDeclineInvitePresenter: AcceptDeclineInvitePresenter, ): JoinRoomPresenter.Factory { return object : JoinRoomPresenter.Factory { override fun create(roomId: RoomId): JoinRoomPresenter { return JoinRoomPresenter( roomId = roomId, matrixClient = client, - roomListService = roomListService + roomListService = roomListService, + acceptDeclineInvitePresenter = acceptDeclineInvitePresenter, ) } } diff --git a/features/invite/api/build.gradle.kts b/features/invite/api/build.gradle.kts index 1781ee93ae..52df82e38a 100644 --- a/features/invite/api/build.gradle.kts +++ b/features/invite/api/build.gradle.kts @@ -15,7 +15,7 @@ */ plugins { - id("io.element.android-library") + id("io.element.android-compose-library") } android { diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/InviteListEntryPoint.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/InviteListEntryPoint.kt index 6abfe8f518..c5063cdbc8 100644 --- a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/InviteListEntryPoint.kt +++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/InviteListEntryPoint.kt @@ -32,7 +32,7 @@ interface InviteListEntryPoint : FeatureEntryPoint { interface Callback : Plugin { fun onBackClicked() - + fun onInviteClicked(roomId: RoomId) fun onInviteAccepted(roomId: RoomId) } } diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteEvents.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteEvents.kt similarity index 68% rename from features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteEvents.kt rename to features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteEvents.kt index 17d60cca37..8365ba3a90 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteEvents.kt +++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteEvents.kt @@ -14,13 +14,9 @@ * limitations under the License. */ -package io.element.android.features.invite.impl.response +package io.element.android.features.invite.api.response -sealed interface AcceptDeclineInviteEvents { +interface AcceptDeclineInviteEvents { data class AcceptInvite(val invite: InviteData) : AcceptDeclineInviteEvents data class DeclineInvite(val invite: InviteData) : AcceptDeclineInviteEvents - data object ConfirmDeclineInvite : AcceptDeclineInviteEvents - data object CancelDeclineInvite : AcceptDeclineInviteEvents - data object DismissAcceptError : AcceptDeclineInviteEvents - data object DismissDeclineError : AcceptDeclineInviteEvents } diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInvitePresenter.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInvitePresenter.kt new file mode 100644 index 0000000000..7ca0158c2b --- /dev/null +++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInvitePresenter.kt @@ -0,0 +1,21 @@ +/* + * 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.invite.api.response + +import io.element.android.libraries.architecture.Presenter + +interface AcceptDeclineInvitePresenter: Presenter diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteState.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteState.kt similarity index 80% rename from features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteState.kt rename to features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteState.kt index 11b960f57b..95980f0ac3 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteState.kt +++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteState.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.invite.impl.response +package io.element.android.features.invite.api.response import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.core.RoomId @@ -24,11 +24,5 @@ data class AcceptDeclineInviteState( val invite: Optional, val acceptAction: AsyncAction, val declineAction: AsyncAction, - val eventSink: (AcceptDeclineInviteEvents) -> Unit -) - -data class InviteData( - val roomId: RoomId, - val roomName: String, - val isDirect: Boolean, + val eventSink: (AcceptDeclineInviteEvents) -> Unit, ) diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteStateProvider.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteStateProvider.kt similarity index 97% rename from features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteStateProvider.kt rename to features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteStateProvider.kt index 710c8b7538..e6a20d9b80 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteStateProvider.kt +++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteStateProvider.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.invite.impl.response +package io.element.android.features.invite.api.response import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncAction diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteView.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteView.kt new file mode 100644 index 0000000000..c969458122 --- /dev/null +++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteView.kt @@ -0,0 +1,31 @@ +/* + * 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.invite.api.response + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import io.element.android.libraries.matrix.api.core.RoomId + +interface AcceptDeclineInviteView { + @Composable + fun Render( + state: AcceptDeclineInviteState, + onInviteAccepted: (RoomId) -> Unit, + onInviteDeclined: (RoomId) -> Unit, + modifier: Modifier, + ) +} diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/InviteData.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/InviteData.kt new file mode 100644 index 0000000000..081da0bfc6 --- /dev/null +++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/InviteData.kt @@ -0,0 +1,25 @@ +/* + * 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.invite.api.response + +import io.element.android.libraries.matrix.api.core.RoomId + +data class InviteData( + val roomId: RoomId, + val roomName: String, + val isDirect: Boolean, +) diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListNode.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListNode.kt index 53c5b31095..fe491b157e 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListNode.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListNode.kt @@ -43,6 +43,10 @@ class InviteListNode @AssistedInject constructor( plugins().forEach { it.onInviteAccepted(roomId) } } + private fun onInviteClicked(roomId: RoomId) { + plugins().forEach { it.onInviteClicked(roomId) } + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() @@ -50,7 +54,8 @@ class InviteListNode @AssistedInject constructor( state = state, onBackClicked = ::onBackClicked, onInviteAccepted = ::onInviteAccepted, - onInviteDeclined = {} + onInviteDeclined = {}, + onInviteClicked = ::onInviteClicked, ) } } diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListPresenter.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListPresenter.kt index a46f135aa3..971e255898 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListPresenter.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListPresenter.kt @@ -24,11 +24,11 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import io.element.android.features.invite.api.SeenInvitesStore +import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents +import io.element.android.features.invite.api.response.AcceptDeclineInvitePresenter +import io.element.android.features.invite.api.response.InviteData import io.element.android.features.invite.impl.model.InviteListInviteSummary import io.element.android.features.invite.impl.model.InviteSender -import io.element.android.features.invite.impl.response.AcceptDeclineInviteEvents -import io.element.android.features.invite.impl.response.AcceptDeclineInvitePresenter -import io.element.android.features.invite.impl.response.InviteData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListState.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListState.kt index 60d8778c33..8a3cd69923 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListState.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListState.kt @@ -17,7 +17,7 @@ package io.element.android.features.invite.impl.invitelist import androidx.compose.runtime.Immutable -import io.element.android.features.invite.impl.response.AcceptDeclineInviteState +import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.invite.impl.model.InviteListInviteSummary import kotlinx.collections.immutable.ImmutableList diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListStateProvider.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListStateProvider.kt index 11902e4673..e796896745 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListStateProvider.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListStateProvider.kt @@ -17,11 +17,11 @@ package io.element.android.features.invite.impl.invitelist import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.invite.impl.model.InviteListInviteSummary import io.element.android.features.invite.impl.model.InviteSender -import io.element.android.features.invite.impl.response.AcceptDeclineInviteState -import io.element.android.features.invite.impl.response.AcceptDeclineInviteStateProvider -import io.element.android.features.invite.impl.response.anAcceptDeclineInviteState +import io.element.android.features.invite.api.response.AcceptDeclineInviteStateProvider +import io.element.android.features.invite.api.response.anAcceptDeclineInviteState import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId import kotlinx.collections.immutable.ImmutableList diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListView.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListView.kt index 2ad75e43eb..16031d558c 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListView.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListView.kt @@ -16,6 +16,7 @@ package io.element.android.features.invite.impl.invitelist +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets @@ -53,11 +54,13 @@ fun InviteListView( onBackClicked: () -> Unit, onInviteAccepted: (RoomId) -> Unit, onInviteDeclined: (RoomId) -> Unit, + onInviteClicked: (RoomId) -> Unit, modifier: Modifier = Modifier, ) { InviteListContent( state = state, modifier = modifier, + onInviteClicked = onInviteClicked, onBackClicked = onBackClicked, ) AcceptDeclineInviteView( @@ -72,6 +75,7 @@ fun InviteListView( private fun InviteListContent( state: InviteListState, onBackClicked: () -> Unit, + onInviteClicked: (RoomId) -> Unit, modifier: Modifier = Modifier, ) { Scaffold( @@ -112,6 +116,9 @@ private fun InviteListContent( items = state.inviteList, ) { index, invite -> InviteSummaryRow( + modifier = Modifier.clickable( + onClick = { onInviteClicked(invite.roomId) } + ), invite = invite, onAcceptClicked = { state.eventSink(InviteListEvents.AcceptInvite(invite)) }, onDeclineClicked = { state.eventSink(InviteListEvents.DeclineInvite(invite)) }, @@ -136,5 +143,6 @@ internal fun InviteListViewPreview(@PreviewParameter(InviteListStateProvider::cl onBackClicked = {}, onInviteAccepted = {}, onInviteDeclined = {}, + onInviteClicked = {}, ) } diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteView.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteView.kt index a214fed4a2..f1462e8dba 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteView.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteView.kt @@ -22,6 +22,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.features.invite.api.response.AcceptDeclineInviteState +import io.element.android.features.invite.api.response.AcceptDeclineInviteStateProvider +import io.element.android.features.invite.api.response.InviteData import io.element.android.features.invite.impl.R import io.element.android.libraries.designsystem.components.async.AsyncActionView import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog @@ -42,14 +45,14 @@ fun AcceptDeclineInviteView( async = state.acceptAction, onSuccess = onInviteAccepted, onErrorDismiss = { - state.eventSink(AcceptDeclineInviteEvents.DismissAcceptError) + state.eventSink(DefaultAcceptDeclineInviteEvents.DismissAcceptError) }, ) AsyncActionView( async = state.declineAction, onSuccess = onInviteDeclined, onErrorDismiss = { - state.eventSink(AcceptDeclineInviteEvents.DismissDeclineError) + state.eventSink(DefaultAcceptDeclineInviteEvents.DismissDeclineError) }, confirmationDialog = { val invite = state.invite.getOrNull() @@ -57,10 +60,10 @@ fun AcceptDeclineInviteView( DeclineConfirmationDialog( invite = invite, onConfirmClicked = { - state.eventSink(AcceptDeclineInviteEvents.ConfirmDeclineInvite) + state.eventSink(DefaultAcceptDeclineInviteEvents.ConfirmDeclineInvite) }, onDismissClicked = { - state.eventSink(AcceptDeclineInviteEvents.CancelDeclineInvite) + state.eventSink(DefaultAcceptDeclineInviteEvents.CancelDeclineInvite) } ) } diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/DefaultAcceptDeclineInviteEvents.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/DefaultAcceptDeclineInviteEvents.kt new file mode 100644 index 0000000000..4698b1ba2d --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/DefaultAcceptDeclineInviteEvents.kt @@ -0,0 +1,26 @@ +/* + * 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.invite.impl.response + +import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents + +sealed interface DefaultAcceptDeclineInviteEvents: AcceptDeclineInviteEvents { + data object ConfirmDeclineInvite : DefaultAcceptDeclineInviteEvents + data object CancelDeclineInvite : DefaultAcceptDeclineInviteEvents + data object DismissAcceptError : DefaultAcceptDeclineInviteEvents + data object DismissDeclineError : DefaultAcceptDeclineInviteEvents +} diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenter.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/DefaultAcceptDeclineInvitePresenter.kt similarity index 78% rename from features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenter.kt rename to features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/DefaultAcceptDeclineInvitePresenter.kt index 38302377f1..ccdcf1e27a 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenter.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/DefaultAcceptDeclineInvitePresenter.kt @@ -23,10 +23,16 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import com.squareup.anvil.annotations.ContributesBinding import im.vector.app.features.analytics.plan.JoinedRoom +import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents +import io.element.android.features.invite.api.response.AcceptDeclineInvitePresenter +import io.element.android.features.invite.api.response.AcceptDeclineInviteState +import io.element.android.features.invite.api.response.InviteData import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState +import io.element.android.libraries.architecture.runUpdatingState +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.push.api.notifications.NotificationDrawerManager @@ -38,11 +44,12 @@ import java.util.Optional import javax.inject.Inject import kotlin.jvm.optionals.getOrNull -class AcceptDeclineInvitePresenter @Inject constructor( +@ContributesBinding(SessionScope::class) +class DefaultAcceptDeclineInvitePresenter @Inject constructor( private val client: MatrixClient, private val analyticsService: AnalyticsService, private val notificationDrawerManager: NotificationDrawerManager, -) : Presenter { +) : AcceptDeclineInvitePresenter { @Composable override fun present(): AcceptDeclineInviteState { @@ -66,7 +73,7 @@ class AcceptDeclineInvitePresenter @Inject constructor( declinedAction.value = AsyncAction.Confirming } - is AcceptDeclineInviteEvents.ConfirmDeclineInvite -> { + is DefaultAcceptDeclineInviteEvents.ConfirmDeclineInvite -> { declinedAction.value = AsyncAction.Uninitialized currentInvite.getOrNull()?.let { localCoroutineScope.declineInvite(it.roomId, declinedAction) @@ -74,16 +81,16 @@ class AcceptDeclineInvitePresenter @Inject constructor( currentInvite = Optional.empty() } - is AcceptDeclineInviteEvents.CancelDeclineInvite -> { + is DefaultAcceptDeclineInviteEvents.CancelDeclineInvite -> { currentInvite = Optional.empty() declinedAction.value = AsyncAction.Uninitialized } - is AcceptDeclineInviteEvents.DismissAcceptError -> { + is DefaultAcceptDeclineInviteEvents.DismissAcceptError -> { acceptedAction.value = AsyncAction.Uninitialized } - is AcceptDeclineInviteEvents.DismissDeclineError -> { + is DefaultAcceptDeclineInviteEvents.DismissDeclineError -> { declinedAction.value = AsyncAction.Uninitialized } } @@ -98,14 +105,14 @@ class AcceptDeclineInvitePresenter @Inject constructor( } private fun CoroutineScope.acceptInvite(roomId: RoomId, acceptedAction: MutableState>) = launch { - suspend { - client.getRoom(roomId)?.use { - it.join().getOrThrow() + acceptedAction.runUpdatingState { + client.joinRoom(roomId).onSuccess { notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId, doRender = true) - analyticsService.capture(it.toAnalyticsJoinedRoom(JoinedRoom.Trigger.Invite)) + client.getRoom(roomId)?.use { room -> + analyticsService.capture(room.toAnalyticsJoinedRoom(JoinedRoom.Trigger.Invite)) + } } - roomId - }.runCatchingUpdatingState(acceptedAction) + } } private fun CoroutineScope.declineInvite(roomId: RoomId, declinedAction: MutableState>) = launch { diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteNode.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/DefaultAcceptDeclineInviteView.kt similarity index 56% rename from features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteNode.kt rename to features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/DefaultAcceptDeclineInviteView.kt index 4718b38881..b87f5fa655 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteNode.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/DefaultAcceptDeclineInviteView.kt @@ -18,28 +18,27 @@ package io.element.android.features.invite.impl.response import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.bumble.appyx.core.modality.BuildContext -import com.bumble.appyx.core.node.Node -import com.bumble.appyx.core.plugin.Plugin -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.anvilannotations.ContributesNode +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.invite.api.response.AcceptDeclineInviteState +import io.element.android.features.invite.api.response.AcceptDeclineInviteView import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.core.RoomId +import javax.inject.Inject -@ContributesNode(SessionScope::class) -class AcceptDeclineInviteNode @AssistedInject constructor( - @Assisted buildContext: BuildContext, - @Assisted plugins: List, - private val presenter: AcceptDeclineInvitePresenter, -) : Node(buildContext, plugins = plugins) { +@ContributesBinding(SessionScope::class) +class DefaultAcceptDeclineInviteView @Inject constructor() : AcceptDeclineInviteView { @Composable - override fun View(modifier: Modifier) { - val state = presenter.present() + override fun Render( + state: AcceptDeclineInviteState, + onInviteAccepted: (RoomId) -> Unit, + onInviteDeclined: (RoomId) -> Unit, + modifier: Modifier, + ) { AcceptDeclineInviteView( state = state, - onInviteAccepted = {}, - onInviteDeclined = {}, + onInviteAccepted = onInviteAccepted, + onInviteDeclined = onInviteDeclined, modifier = modifier ) } From 9c1a0b5581a0dbcc2a1e6df56e8f37b20364b258 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 9 Apr 2024 15:03:27 +0200 Subject: [PATCH 09/25] Room navigation : extract JoinRoomNode to his own feature module --- .../android/appnav/room/RoomFlowNode.kt | 8 +-- features/joinroom/api/build.gradle.kts | 28 ++++++++++ .../joinroom/api/JoinRoomEntryPoint.kt | 33 ++++++++++++ features/joinroom/impl/build.gradle.kts | 54 +++++++++++++++++++ .../impl/DefaultJoinRoomEntryPoint.kt | 36 +++++++++++++ .../features/joinroom/impl}/JoinRoomEvents.kt | 2 +- .../features/joinroom/impl}/JoinRoomNode.kt | 11 ++-- .../joinroom/impl}/JoinRoomPresenter.kt | 2 +- .../features/joinroom/impl}/JoinRoomState.kt | 2 +- .../joinroom/impl}/JoinRoomStateProvider.kt | 2 +- .../features/joinroom/impl}/JoinRoomView.kt | 2 +- .../joinroom/impl}/di/JoinRoomModule.kt | 4 +- 12 files changed, 165 insertions(+), 19 deletions(-) create mode 100644 features/joinroom/api/build.gradle.kts create mode 100644 features/joinroom/api/src/main/kotlin/io/element/android/features/joinroom/api/JoinRoomEntryPoint.kt create mode 100644 features/joinroom/impl/build.gradle.kts create mode 100644 features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/DefaultJoinRoomEntryPoint.kt rename {appnav/src/main/kotlin/io/element/android/appnav/room/join => features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl}/JoinRoomEvents.kt (93%) rename {appnav/src/main/kotlin/io/element/android/appnav/room/join => features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl}/JoinRoomNode.kt (87%) rename {appnav/src/main/kotlin/io/element/android/appnav/room/join => features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl}/JoinRoomPresenter.kt (98%) rename {appnav/src/main/kotlin/io/element/android/appnav/room/join => features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl}/JoinRoomState.kt (97%) rename {appnav/src/main/kotlin/io/element/android/appnav/room/join => features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl}/JoinRoomStateProvider.kt (97%) rename {appnav/src/main/kotlin/io/element/android/appnav/room/join => features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl}/JoinRoomView.kt (99%) rename {appnav/src/main/kotlin/io/element/android/appnav/room/join => features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl}/di/JoinRoomModule.kt (93%) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt index beef8a883c..7149937e7e 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt @@ -34,16 +34,15 @@ import com.bumble.appyx.navmodel.backstack.operation.newRoot import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode -import io.element.android.appnav.room.join.JoinRoomNode import io.element.android.appnav.room.joined.JoinedRoomFlowNode import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode +import io.element.android.features.joinroom.api.JoinRoomEntryPoint import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.inputs import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator -import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.CurrentUserMembership @@ -64,6 +63,7 @@ class RoomFlowNode @AssistedInject constructor( @Assisted plugins: List, private val roomListService: RoomListService, private val roomMembershipObserver: RoomMembershipObserver, + private val joinRoomEntryPoint: JoinRoomEntryPoint, ) : BaseFlowNode( backstack = BackStack( initialElement = NavTarget.Loading, @@ -118,8 +118,8 @@ class RoomFlowNode @AssistedInject constructor( return when (navTarget) { NavTarget.Loading -> loadingNode(buildContext) NavTarget.JoinRoom -> { - val inputs = JoinRoomNode.Inputs(inputs.roomId) - createNode(buildContext, plugins = listOf(inputs)) + val inputs = JoinRoomEntryPoint.Inputs(inputs.roomId) + joinRoomEntryPoint.createNode(this, buildContext, inputs) } NavTarget.JoinedRoom -> { val roomFlowNodeCallback = plugins() diff --git a/features/joinroom/api/build.gradle.kts b/features/joinroom/api/build.gradle.kts new file mode 100644 index 0000000000..697dc5deee --- /dev/null +++ b/features/joinroom/api/build.gradle.kts @@ -0,0 +1,28 @@ +/* + * 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.joinroom.api" +} + +dependencies { + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) +} diff --git a/features/joinroom/api/src/main/kotlin/io/element/android/features/joinroom/api/JoinRoomEntryPoint.kt b/features/joinroom/api/src/main/kotlin/io/element/android/features/joinroom/api/JoinRoomEntryPoint.kt new file mode 100644 index 0000000000..50dc1010d8 --- /dev/null +++ b/features/joinroom/api/src/main/kotlin/io/element/android/features/joinroom/api/JoinRoomEntryPoint.kt @@ -0,0 +1,33 @@ +/* + * 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.joinroom.api + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import io.element.android.libraries.architecture.FeatureEntryPoint +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.matrix.api.core.RoomId + +interface JoinRoomEntryPoint : FeatureEntryPoint { + + fun createNode(parentNode: Node, buildContext: BuildContext, inputs: Inputs): Node + + data class Inputs( + val roomId: RoomId, + ) : NodeInputs +} + diff --git a/features/joinroom/impl/build.gradle.kts b/features/joinroom/impl/build.gradle.kts new file mode 100644 index 0000000000..047bc1d960 --- /dev/null +++ b/features/joinroom/impl/build.gradle.kts @@ -0,0 +1,54 @@ +/* + * 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. + */ + +@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.joinroom.impl" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(projects.anvilannotations) + anvil(projects.anvilcodegen) + api(projects.features.joinroom.api) + implementation(projects.libraries.core) + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrixui) + implementation(projects.libraries.designsystem) + implementation(projects.features.invite.api) + implementation(projects.libraries.uiStrings) + + + 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) +} diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/DefaultJoinRoomEntryPoint.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/DefaultJoinRoomEntryPoint.kt new file mode 100644 index 0000000000..ab2745a63f --- /dev/null +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/DefaultJoinRoomEntryPoint.kt @@ -0,0 +1,36 @@ +/* + * 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.joinroom.impl + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.joinroom.api.JoinRoomEntryPoint +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.AppScope +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultJoinRoomEntryPoint @Inject constructor() : JoinRoomEntryPoint { + + override fun createNode(parentNode: Node, buildContext: BuildContext, inputs: JoinRoomEntryPoint.Inputs): Node { + return parentNode.createNode( + buildContext = buildContext, + plugins = listOf(inputs) + ) + } +} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomEvents.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomEvents.kt similarity index 93% rename from appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomEvents.kt rename to features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomEvents.kt index 7679e77c63..e3dc73f505 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomEvents.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomEvents.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.appnav.room.join +package io.element.android.features.joinroom.impl sealed interface JoinRoomEvents { data object JoinRoom: JoinRoomEvents diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomNode.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomNode.kt similarity index 87% rename from appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomNode.kt rename to features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomNode.kt index 3574b37e2d..a0dcf7a52c 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomNode.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomNode.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.appnav.room.join +package io.element.android.features.joinroom.impl import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -25,10 +25,9 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.features.invite.api.response.AcceptDeclineInviteView -import io.element.android.libraries.architecture.NodeInputs +import io.element.android.features.joinroom.api.JoinRoomEntryPoint import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.SessionScope -import io.element.android.libraries.matrix.api.core.RoomId @ContributesNode(SessionScope::class) class JoinRoomNode @AssistedInject constructor( @@ -38,11 +37,7 @@ class JoinRoomNode @AssistedInject constructor( private val acceptDeclineInviteView: AcceptDeclineInviteView, ) : Node(buildContext, plugins = plugins) { - data class Inputs( - val roomId: RoomId, - ) : NodeInputs - - private val inputs: Inputs = inputs() + private val inputs: JoinRoomEntryPoint.Inputs = inputs() private val presenter = presenterFactory.create(inputs.roomId) @Composable diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomPresenter.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt similarity index 98% rename from appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomPresenter.kt rename to features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt index 60a59652bf..d74ec9d75b 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomPresenter.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.appnav.room.join +package io.element.android.features.joinroom.impl import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomState.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt similarity index 97% rename from appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomState.kt rename to features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt index ca9f71baba..2437a76b3e 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomState.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.appnav.room.join +package io.element.android.features.joinroom.impl import androidx.compose.runtime.Immutable import io.element.android.features.invite.api.response.AcceptDeclineInviteState diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomStateProvider.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt similarity index 97% rename from appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomStateProvider.kt rename to features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt index 6ac24cbdcb..063f8b1315 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomStateProvider.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.appnav.room.join +package io.element.android.features.joinroom.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.invite.api.response.AcceptDeclineInviteState diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomView.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt similarity index 99% rename from appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomView.kt rename to features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt index eff9181c11..e803c8315c 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomView.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.appnav.room.join +package io.element.android.features.joinroom.impl import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/join/di/JoinRoomModule.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt similarity index 93% rename from appnav/src/main/kotlin/io/element/android/appnav/room/join/di/JoinRoomModule.kt rename to features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt index 15b7f72ef0..b4fc45ca18 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/join/di/JoinRoomModule.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package io.element.android.appnav.room.join.di +package io.element.android.features.joinroom.impl.di import com.squareup.anvil.annotations.ContributesTo import dagger.Module import dagger.Provides -import io.element.android.appnav.room.join.JoinRoomPresenter import io.element.android.features.invite.api.response.AcceptDeclineInvitePresenter +import io.element.android.features.joinroom.impl.JoinRoomPresenter import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId From 23604e0549b08e1cd433bf518a994b64ff8f19c1 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 9 Apr 2024 15:53:25 +0200 Subject: [PATCH 10/25] Room navigation : import join room strings --- .../android/features/joinroom/impl/JoinRoomView.kt | 4 ++-- features/joinroom/impl/src/main/res/values/localazy.xml | 9 +++++++++ tools/localazy/config.json | 6 ++++++ 3 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 features/joinroom/impl/src/main/res/values/localazy.xml diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt index e803c8315c..a4a252a41e 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt @@ -147,14 +147,14 @@ private fun JoinRoomContent( } Spacer(modifier = Modifier.height(16.dp)) Text( - text = "Preview is not available", + text = stringResource(id = R.string.screen_join_room_title_no_preview), style = ElementTheme.typography.fontHeadingMdBold, textAlign = TextAlign.Center, color = ElementTheme.colors.textPrimary, ) Spacer(modifier = Modifier.height(8.dp)) Text( - text = "You must be a member of this room to view the message history.", + text = stringResource(id = R.string.screen_join_room_subtitle_no_preview), style = ElementTheme.typography.fontBodyMdRegular, textAlign = TextAlign.Center, color = ElementTheme.colors.textSecondary, diff --git a/features/joinroom/impl/src/main/res/values/localazy.xml b/features/joinroom/impl/src/main/res/values/localazy.xml new file mode 100644 index 0000000000..1c187d403d --- /dev/null +++ b/features/joinroom/impl/src/main/res/values/localazy.xml @@ -0,0 +1,9 @@ + + + "Join room" + "Knock to join" + "Click the button below and a room administrator will be notified. You’ll be able to join the conversation once approved." + "You must be a member of this room to view the message history." + "Want to join this room?" + "Preview is not available" + diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 35e13730c2..d005eeed7e 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -233,6 +233,12 @@ "includeRegex" : [ "screen_room_directory_.*" ] + }, + { + "name" : ":features:joinroom:impl", + "includeRegex" : [ + "screen_join_room_.*" + ] } ] } From fc20b7399ad84838242aa111cbb19c09a648d2b3 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 10 Apr 2024 11:41:06 +0200 Subject: [PATCH 11/25] Room navigation : more reliable roomInfoFlow method --- .../android/appnav/room/RoomFlowNode.kt | 24 +++++++-------- .../joinroom/impl/JoinRoomPresenter.kt | 29 ++++++++++--------- .../features/joinroom/impl/JoinRoomState.kt | 5 ++-- .../joinroom/impl/JoinRoomStateProvider.kt | 1 + .../features/joinroom/impl/JoinRoomView.kt | 6 +++- .../joinroom/impl/di/JoinRoomModule.kt | 1 - .../impl/datasource/RoomListDataSource.kt | 2 +- .../impl/search/RoomListSearchDataSource.kt | 2 +- .../libraries/matrix/api/MatrixClient.kt | 5 ++++ .../matrix/api/roomlist/DynamicRoomList.kt | 3 ++ .../matrix/api/roomlist/RoomListService.kt | 16 ++++------ .../libraries/matrix/impl/RustMatrixClient.kt | 22 ++++++++++++++ .../matrix/impl/roomlist/RoomListFactory.kt | 5 +++- .../impl/roomlist/RustRoomListService.kt | 28 ------------------ 14 files changed, 75 insertions(+), 74 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt index 7149937e7e..91809fe0ca 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt @@ -44,13 +44,11 @@ import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.inputs import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator 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.CurrentUserMembership import io.element.android.libraries.matrix.api.room.RoomMembershipObserver -import io.element.android.libraries.matrix.api.roomlist.RoomListService -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.parcelize.Parcelize @@ -61,7 +59,7 @@ import kotlin.jvm.optionals.getOrNull class RoomFlowNode @AssistedInject constructor( @Assisted val buildContext: BuildContext, @Assisted plugins: List, - private val roomListService: RoomListService, + private val client: MatrixClient, private val roomMembershipObserver: RoomMembershipObserver, private val joinRoomEntryPoint: JoinRoomEntryPoint, ) : BaseFlowNode( @@ -92,18 +90,16 @@ class RoomFlowNode @AssistedInject constructor( override fun onBuilt() { super.onBuilt() - roomListService.getUserMembershipForRoom( + client.getRoomInfoFlow( inputs.roomId - ).flowOn(Dispatchers.Default) - .onEach { membership -> - Timber.d("RoomMembership = $membership") - if (membership.getOrNull() == CurrentUserMembership.JOINED) { - backstack.newRoot(NavTarget.JoinedRoom) - } else { - backstack.newRoot(NavTarget.JoinRoom) - } + ).onEach { roomInfo -> + Timber.d("Room membership: ${roomInfo.map { it.currentUserMembership }}") + if (roomInfo.getOrNull()?.currentUserMembership == CurrentUserMembership.JOINED) { + backstack.newRoot(NavTarget.JoinedRoom) + } else { + backstack.newRoot(NavTarget.JoinRoom) } - .flowOn(Dispatchers.Main) + } .launchIn(lifecycleScope) roomMembershipObserver.updates diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt index d74ec9d75b..39b8058075 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt @@ -30,14 +30,13 @@ import io.element.android.libraries.architecture.Presenter 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.CurrentUserMembership -import io.element.android.libraries.matrix.api.roomlist.RoomListService +import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import java.util.Optional import kotlin.jvm.optionals.getOrNull class JoinRoomPresenter @AssistedInject constructor( @Assisted private val roomId: RoomId, private val matrixClient: MatrixClient, - private val roomListService: RoomListService, private val acceptDeclineInvitePresenter: AcceptDeclineInvitePresenter, ) : Presenter { @@ -47,23 +46,24 @@ class JoinRoomPresenter @AssistedInject constructor( @Composable override fun present(): JoinRoomState { - val userMembership by roomListService.getUserMembershipForRoom(roomId).collectAsState(initial = Optional.empty()) - val joinAuthorisationStatus = joinAuthorisationStatus(userMembership) + val mxRoomInfo by matrixClient.getRoomInfoFlow(roomId).collectAsState(initial = Optional.empty()) + val joinAuthorisationStatus = joinAuthorisationStatus(mxRoomInfo) val acceptDeclineInviteState = acceptDeclineInvitePresenter.present() - val roomInfo by produceState>(initialValue = AsyncData.Uninitialized, key1 = userMembership) { + val roomInfo by produceState>(initialValue = AsyncData.Uninitialized, key1 = mxRoomInfo) { value = when { - userMembership.isPresent -> { - val roomInfo = matrixClient.getRoom(roomId)?.use { + mxRoomInfo.isPresent -> { + val roomInfo = mxRoomInfo.get().let { RoomInfo( - roomId = it.roomId, - roomName = it.displayName, - roomAlias = it.alias, - memberCount = it.activeMemberCount, + roomId = roomId, + roomName = it.name, + roomAlias = it.canonicalAlias, + memberCount = it.activeMembersCount, isDirect = it.isDirect, + topic = it.topic, roomAvatarUrl = it.avatarUrl ) } - roomInfo?.let { AsyncData.Success(it) } ?: AsyncData.Failure(Exception("Failed to load room info")) + AsyncData.Success(roomInfo) } else -> AsyncData.Uninitialized } @@ -103,9 +103,10 @@ class JoinRoomPresenter @AssistedInject constructor( } @Composable - private fun joinAuthorisationStatus(userMembership: Optional): JoinAuthorisationStatus { + private fun joinAuthorisationStatus(roomInfo: Optional): JoinAuthorisationStatus { + val userMembership = roomInfo.getOrNull()?.currentUserMembership return when { - userMembership.getOrNull() == CurrentUserMembership.INVITED -> return JoinAuthorisationStatus.IsInvited + userMembership == CurrentUserMembership.INVITED -> return JoinAuthorisationStatus.IsInvited else -> JoinAuthorisationStatus.Unknown } } diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt index 2437a76b3e..b866ec0d42 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt @@ -29,15 +29,16 @@ data class JoinRoomState( val joinAuthorisationStatus: JoinAuthorisationStatus, val acceptDeclineInviteState: AcceptDeclineInviteState, val eventSink: (JoinRoomEvents) -> Unit -){ +) { val showMemberCount = roomInfo.dataOrNull()?.memberCount != null } data class RoomInfo( val roomId: RoomId, - val roomName: String, + val roomName: String?, val roomAlias: String?, val memberCount: Long?, + val topic: String?, val isDirect: Boolean, val roomAvatarUrl: String?, ) { diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt index 063f8b1315..7ae82b3ce9 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt @@ -48,6 +48,7 @@ fun aJoinRoomState( roomAlias = "#exa:matrix.org", memberCount = null, isDirect = false, + topic = null, roomAvatarUrl = null ) ), diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt index a4a252a41e..d85ba33edf 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt @@ -203,7 +203,11 @@ private fun JoinRoomTopBar( when (asyncRoomInfo) { is AsyncData.Success -> { val roomInfo = asyncRoomInfo.data - RoomAvatarAndNameRow(roomName = roomInfo.roomName, roomAvatar = roomInfo.avatarData(AvatarSize.TimelineRoom)) + if(roomInfo.roomName == null){ + IconTitlePlaceholdersRowMolecule(iconSize = AvatarSize.TimelineRoom.dp) + }else { + RoomAvatarAndNameRow(roomName = roomInfo.roomName, roomAvatar = roomInfo.avatarData(AvatarSize.TimelineRoom)) + } } else -> { IconTitlePlaceholdersRowMolecule(iconSize = AvatarSize.TimelineRoom.dp) diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt index b4fc45ca18..b1e9b04879 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt @@ -40,7 +40,6 @@ object JoinRoomModule { return JoinRoomPresenter( roomId = roomId, matrixClient = client, - roomListService = roomListService, acceptDeclineInvitePresenter = acceptDeclineInvitePresenter, ) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSource.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSource.kt index 2f078d7a69..e2508446e7 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSource.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSource.kt @@ -60,7 +60,7 @@ class RoomListDataSource @Inject constructor( fun launchIn(coroutineScope: CoroutineScope) { roomListService .allRooms - .summaries + .filteredSummaries .onEach { roomSummaries -> replaceWith(roomSummaries) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchDataSource.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchDataSource.kt index 59d8bf9f13..165c92bff8 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchDataSource.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchDataSource.kt @@ -45,7 +45,7 @@ class RoomListSearchDataSource @Inject constructor( source = RoomList.Source.All, ) - val roomSummaries: Flow> = roomList.summaries + val roomSummaries: Flow> = roomList.filteredSummaries .map { roomSummaries -> roomSummaries .filterIsInstance() diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index e6c536ee72..0875c54043 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -27,7 +27,9 @@ import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.oidc.AccountManagementAction import io.element.android.libraries.matrix.api.pusher.PushersService +import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService import io.element.android.libraries.matrix.api.roomlist.RoomListService @@ -37,8 +39,10 @@ import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.verification.SessionVerificationService import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import java.io.Closeable +import java.util.Optional interface MatrixClient : Closeable { val sessionId: SessionId @@ -89,6 +93,7 @@ interface MatrixClient : Closeable { suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result fun roomMembershipObserver(): RoomMembershipObserver + fun getRoomInfoFlow(roomId: RoomId): Flow> fun isMe(userId: UserId?) = userId == sessionId } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/DynamicRoomList.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/DynamicRoomList.kt index 8cb43af24c..7bd4c42bdf 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/DynamicRoomList.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/DynamicRoomList.kt @@ -17,6 +17,7 @@ package io.element.android.libraries.matrix.api.roomlist import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn @@ -32,6 +33,8 @@ interface DynamicRoomList : RoomList { val loadedPages: StateFlow val pageSize: Int + val filteredSummaries: SharedFlow> + /** * Load more rooms into the list if possible. */ diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt index 4534f10a74..860aeb7e68 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt @@ -17,15 +17,9 @@ package io.element.android.libraries.matrix.api.roomlist import androidx.compose.runtime.Immutable -import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.api.room.CurrentUserMembership import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import java.util.Optional +import kotlinx.coroutines.flow.filterIsInstance /** * Entry point for the room list api. @@ -86,10 +80,10 @@ interface RoomListService { */ val state: StateFlow - /** - * Get a flow of the room summary for a given room id. - */ - fun getUserMembershipForRoom(roomId: RoomId): Flow> +} + +fun RoomList.loadedStateFlow(): Flow { + return loadingState.filterIsInstance() } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 546acc6e84..997301c2a8 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -35,6 +35,7 @@ import io.element.android.libraries.matrix.api.notificationsettings.Notification import io.element.android.libraries.matrix.api.oidc.AccountManagementAction import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService import io.element.android.libraries.matrix.api.roomlist.RoomListService @@ -79,12 +80,15 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -104,8 +108,10 @@ import org.matrix.rustcomponents.sdk.TimelineEventTypeFilter import org.matrix.rustcomponents.sdk.use import timber.log.Timber import java.io.File +import java.util.Optional import java.util.concurrent.atomic.AtomicBoolean import kotlin.time.Duration +import kotlin.time.Duration.Companion.INFINITE import kotlin.time.Duration.Companion.seconds import org.matrix.rustcomponents.sdk.CreateRoomParameters as RustCreateRoomParameters import org.matrix.rustcomponents.sdk.RoomPreset as RustRoomPreset @@ -524,6 +530,22 @@ class RustMatrixClient( override fun roomMembershipObserver(): RoomMembershipObserver = roomMembershipObserver + override fun getRoomInfoFlow(roomId: RoomId): Flow> { + return flow { + var room = getRoom(roomId) + if (room == null) { + emit(Optional.empty()) + awaitRoom(roomId, INFINITE) + room = getRoom(roomId) + } + room?.use { + room.roomInfoFlow + .map { roomInfo -> Optional.of(roomInfo) } + .collect(this) + } + }.distinctUntilChanged() + } + private suspend fun File.getCacheSize( includeCryptoDb: Boolean = false, ): Long = withContext(sessionDispatcher) { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactory.kt index 4231af07d2..7ac4aed637 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactory.kt @@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.roomlist.RoomSummary import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.flow.launchIn @@ -92,7 +93,8 @@ internal class RoomListFactory( innerRoomList?.destroy() } return RustDynamicRoomList( - summaries = filteredSummariesFlow, + summaries = summariesFlow, + filteredSummaries = filteredSummariesFlow, loadingState = loadingStateFlow, currentFilter = currentFilter, loadedPages = loadedPages, @@ -105,6 +107,7 @@ internal class RoomListFactory( private class RustDynamicRoomList( override val summaries: MutableSharedFlow>, + override val filteredSummaries: SharedFlow>, override val loadingState: MutableStateFlow, override val currentFilter: MutableStateFlow, override val loadedPages: MutableStateFlow, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt index 09e0a514b1..70310e472e 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt @@ -16,24 +16,16 @@ package io.element.android.libraries.matrix.impl.roomlist -import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.roomlist.DynamicRoomList import io.element.android.libraries.matrix.api.roomlist.RoomList import io.element.android.libraries.matrix.api.roomlist.RoomListFilter import io.element.android.libraries.matrix.api.roomlist.RoomListService -import io.element.android.libraries.matrix.api.roomlist.awaitLoaded import io.element.android.libraries.matrix.api.roomlist.loadAllIncrementally -import io.element.android.libraries.matrix.impl.room.map import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn @@ -43,9 +35,7 @@ import org.matrix.rustcomponents.sdk.RoomListInput import org.matrix.rustcomponents.sdk.RoomListRange import org.matrix.rustcomponents.sdk.RoomListServiceState import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicator -import org.matrix.rustcomponents.sdk.use import timber.log.Timber -import java.util.Optional import org.matrix.rustcomponents.sdk.RoomListService as InnerRustRoomListService private const val DEFAULT_PAGE_SIZE = 20 @@ -122,24 +112,6 @@ internal class RustRoomListService( } .distinctUntilChanged() .stateIn(sessionCoroutineScope, SharingStarted.Eagerly, RoomListService.State.Idle) - - override fun getUserMembershipForRoom(roomId: RoomId): Flow> { - return combine( - allRooms.loadedStateFlow(), - invites.loadedStateFlow(), - ) { _, _ -> - val membership = innerRoomListService.roomOrNull(roomId.value)?.use { - it.roomInfo().use { roomInfo -> - roomInfo.membership.map() - } - } - Optional.ofNullable(membership) - }.distinctUntilChanged() - } - - private fun RoomList.loadedStateFlow(): Flow { - return loadingState.filterIsInstance() - } } private fun RoomListServiceState.toRoomListState(): RoomListService.State { From bf7a94cc9303136965acf78308c5f7a1435f384b Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 10 Apr 2024 15:14:59 +0200 Subject: [PATCH 12/25] Room navigation : make it working with RoomDirectory --- .../android/appnav/LoggedInFlowNode.kt | 19 +++- .../android/appnav/room/RoomFlowNode.kt | 6 +- .../room/joined/JoinedRoomLoadedFlowNode.kt | 4 +- features/joinroom/api/build.gradle.kts | 1 + .../joinroom/api/JoinRoomEntryPoint.kt | 3 + features/joinroom/impl/build.gradle.kts | 1 + .../features/joinroom/impl/JoinRoomNode.kt | 2 +- .../joinroom/impl/JoinRoomPresenter.kt | 78 +++++++++---- .../features/joinroom/impl/JoinRoomState.kt | 20 ++-- .../joinroom/impl/JoinRoomStateProvider.kt | 15 ++- .../features/joinroom/impl/JoinRoomView.kt | 107 ++++++++---------- .../joinroom/impl/di/JoinRoomModule.kt | 5 +- features/roomdirectory/api/build.gradle.kts | 1 + .../roomdirectory/api/RoomDescription.kt | 17 ++- .../api/RoomDirectoryEntryPoint.kt | 3 +- .../impl/root/RoomDirectoryNode.kt | 11 +- .../impl/root/RoomDirectoryStateProvider.kt | 18 +-- .../impl/root/RoomDirectoryView.kt | 68 ++++++----- .../impl/root/model/RoomDescription.kt | 8 +- .../impl/root/RoomDirectoryViewTest.kt | 2 +- 20 files changed, 226 insertions(+), 163 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index 4b82cb6992..0f6da36e26 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -42,8 +42,8 @@ import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.appnav.loggedin.LoggedInNode import io.element.android.appnav.room.RoomFlowNode -import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode import io.element.android.appnav.room.RoomNavigationTarget +import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode import io.element.android.features.createroom.api.CreateRoomEntryPoint import io.element.android.features.ftue.api.FtueEntryPoint import io.element.android.features.ftue.api.state.FtueService @@ -55,6 +55,7 @@ import io.element.android.features.lockscreen.api.LockScreenService import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.preferences.api.PreferencesEntryPoint +import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint import io.element.android.features.roomlist.api.RoomListEntryPoint import io.element.android.features.securebackup.api.SecureBackupEntryPoint @@ -83,6 +84,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize import timber.log.Timber +import java.util.Optional @ContributesNode(SessionScope::class) class LoggedInFlowNode @AssistedInject constructor( @@ -214,6 +216,7 @@ class LoggedInFlowNode @AssistedInject constructor( @Parcelize data class Room( val roomId: RoomId, + val roomDescription: RoomDescription? = null, val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages ) : NavTarget @@ -304,7 +307,11 @@ class LoggedInFlowNode @AssistedInject constructor( backstack.push(NavTarget.Settings(PreferencesEntryPoint.InitialTarget.NotificationSettings)) } } - val inputs = RoomFlowNode.Inputs(roomId = navTarget.roomId, initialElement = navTarget.initialElement) + val inputs = RoomFlowNode.Inputs( + roomId = navTarget.roomId, + roomDescription = Optional.ofNullable(navTarget.roomDescription), + initialElement = navTarget.initialElement + ) createNode(buildContext, plugins = listOf(inputs, callback)) } is NavTarget.Settings -> { @@ -375,8 +382,12 @@ class LoggedInFlowNode @AssistedInject constructor( NavTarget.RoomDirectorySearch -> { roomDirectoryEntryPoint.nodeBuilder(this, buildContext) .callback(object : RoomDirectoryEntryPoint.Callback { - override fun onOpenRoom(roomId: RoomId) { - coroutineScope.launch { attachRoom(roomId) } + override fun onRoomJoined(roomId: RoomId) { + backstack.push(NavTarget.Room(roomId)) + } + + override fun onResultClicked(roomDescription: RoomDescription) { + backstack.push(NavTarget.Room(roomDescription.roomId, roomDescription)) } }) .build() diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt index 91809fe0ca..db3664e631 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt @@ -37,6 +37,7 @@ import io.element.android.anvilannotations.ContributesNode import io.element.android.appnav.room.joined.JoinedRoomFlowNode import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode import io.element.android.features.joinroom.api.JoinRoomEntryPoint +import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.NodeInputs @@ -53,6 +54,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.parcelize.Parcelize import timber.log.Timber +import java.util.Optional import kotlin.jvm.optionals.getOrNull @ContributesNode(SessionScope::class) @@ -72,6 +74,7 @@ class RoomFlowNode @AssistedInject constructor( ) { data class Inputs( val roomId: RoomId, + val roomDescription: Optional, val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages, ) : NodeInputs @@ -102,6 +105,7 @@ class RoomFlowNode @AssistedInject constructor( } .launchIn(lifecycleScope) + // When leaving the room from this session only, navigate up. roomMembershipObserver.updates .filter { update -> update.roomId == inputs.roomId && !update.isUserInRoom } .onEach { @@ -114,7 +118,7 @@ class RoomFlowNode @AssistedInject constructor( return when (navTarget) { NavTarget.Loading -> loadingNode(buildContext) NavTarget.JoinRoom -> { - val inputs = JoinRoomEntryPoint.Inputs(inputs.roomId) + val inputs = JoinRoomEntryPoint.Inputs(inputs.roomId, roomDescription = inputs.roomDescription) joinRoomEntryPoint.createNode(this, buildContext, inputs) } NavTarget.JoinedRoom -> { diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt index ade1f7e147..7d44825fe2 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt @@ -44,7 +44,6 @@ import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.services.appnavstate.api.AppNavigationStateService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -60,10 +59,9 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor( private val appNavigationStateService: AppNavigationStateService, private val appCoroutineScope: CoroutineScope, roomComponentFactory: RoomComponentFactory, - roomMembershipObserver: RoomMembershipObserver, ) : BaseFlowNode( backstack = BackStack( - initialElement = when(plugins.filterIsInstance(Inputs::class.java).first().initialElement){ + initialElement = when (plugins.filterIsInstance(Inputs::class.java).first().initialElement) { RoomNavigationTarget.Messages -> NavTarget.Messages RoomNavigationTarget.Details -> NavTarget.RoomDetails RoomNavigationTarget.NotificationSettings -> NavTarget.RoomNotificationSettings diff --git a/features/joinroom/api/build.gradle.kts b/features/joinroom/api/build.gradle.kts index 697dc5deee..a016c2d195 100644 --- a/features/joinroom/api/build.gradle.kts +++ b/features/joinroom/api/build.gradle.kts @@ -25,4 +25,5 @@ android { dependencies { implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) + implementation(projects.features.roomdirectory.api) } diff --git a/features/joinroom/api/src/main/kotlin/io/element/android/features/joinroom/api/JoinRoomEntryPoint.kt b/features/joinroom/api/src/main/kotlin/io/element/android/features/joinroom/api/JoinRoomEntryPoint.kt index 50dc1010d8..0047eb10a8 100644 --- a/features/joinroom/api/src/main/kotlin/io/element/android/features/joinroom/api/JoinRoomEntryPoint.kt +++ b/features/joinroom/api/src/main/kotlin/io/element/android/features/joinroom/api/JoinRoomEntryPoint.kt @@ -18,9 +18,11 @@ package io.element.android.features.joinroom.api import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node +import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.libraries.architecture.FeatureEntryPoint import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.matrix.api.core.RoomId +import java.util.Optional interface JoinRoomEntryPoint : FeatureEntryPoint { @@ -28,6 +30,7 @@ interface JoinRoomEntryPoint : FeatureEntryPoint { data class Inputs( val roomId: RoomId, + val roomDescription: Optional, ) : NodeInputs } diff --git a/features/joinroom/impl/build.gradle.kts b/features/joinroom/impl/build.gradle.kts index 047bc1d960..10b95789ec 100644 --- a/features/joinroom/impl/build.gradle.kts +++ b/features/joinroom/impl/build.gradle.kts @@ -40,6 +40,7 @@ dependencies { implementation(projects.libraries.matrixui) implementation(projects.libraries.designsystem) implementation(projects.features.invite.api) + implementation(projects.features.roomdirectory.api) implementation(projects.libraries.uiStrings) diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomNode.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomNode.kt index a0dcf7a52c..7763c17cad 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomNode.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomNode.kt @@ -38,7 +38,7 @@ class JoinRoomNode @AssistedInject constructor( ) : Node(buildContext, plugins = plugins) { private val inputs: JoinRoomEntryPoint.Inputs = inputs() - private val presenter = presenterFactory.create(inputs.roomId) + private val presenter = presenterFactory.create(inputs.roomId, inputs.roomDescription) @Composable override fun View(modifier: Modifier) { diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt index 39b8058075..f21422a8ce 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt @@ -25,6 +25,7 @@ import dagger.assisted.AssistedInject import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents import io.element.android.features.invite.api.response.AcceptDeclineInvitePresenter import io.element.android.features.invite.api.response.InviteData +import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.MatrixClient @@ -36,34 +37,29 @@ import kotlin.jvm.optionals.getOrNull class JoinRoomPresenter @AssistedInject constructor( @Assisted private val roomId: RoomId, + @Assisted private val roomDescription: Optional, private val matrixClient: MatrixClient, private val acceptDeclineInvitePresenter: AcceptDeclineInvitePresenter, ) : Presenter { interface Factory { - fun create(roomId: RoomId): JoinRoomPresenter + fun create(roomId: RoomId, roomDescription: Optional): JoinRoomPresenter } @Composable override fun present(): JoinRoomState { - val mxRoomInfo by matrixClient.getRoomInfoFlow(roomId).collectAsState(initial = Optional.empty()) - val joinAuthorisationStatus = joinAuthorisationStatus(mxRoomInfo) + val roomInfo by matrixClient.getRoomInfoFlow(roomId).collectAsState(initial = Optional.empty()) + val joinAuthorisationStatus = joinAuthorisationStatus(roomInfo) val acceptDeclineInviteState = acceptDeclineInvitePresenter.present() - val roomInfo by produceState>(initialValue = AsyncData.Uninitialized, key1 = mxRoomInfo) { + val contentState by produceState>(initialValue = AsyncData.Uninitialized, key1 = roomInfo) { value = when { - mxRoomInfo.isPresent -> { - val roomInfo = mxRoomInfo.get().let { - RoomInfo( - roomId = roomId, - roomName = it.name, - roomAlias = it.canonicalAlias, - memberCount = it.activeMembersCount, - isDirect = it.isDirect, - topic = it.topic, - roomAvatarUrl = it.avatarUrl - ) - } - AsyncData.Success(roomInfo) + roomInfo.isPresent -> { + val contentState = roomInfo.get().toContentState() + AsyncData.Success(contentState) + } + roomDescription.isPresent -> { + val contentState = roomDescription.get().toContentState() + AsyncData.Success(contentState) } else -> AsyncData.Uninitialized } @@ -73,30 +69,68 @@ class JoinRoomPresenter @AssistedInject constructor( when (event) { JoinRoomEvents.AcceptInvite, JoinRoomEvents.JoinRoom -> { acceptDeclineInviteState.eventSink( - AcceptDeclineInviteEvents.AcceptInvite(roomInfo.toInviteData()) + AcceptDeclineInviteEvents.AcceptInvite(contentState.toInviteData()) ) } JoinRoomEvents.DeclineInvite -> { acceptDeclineInviteState.eventSink( - AcceptDeclineInviteEvents.DeclineInvite(roomInfo.toInviteData()) + AcceptDeclineInviteEvents.DeclineInvite(contentState.toInviteData()) ) } } } return JoinRoomState( - roomInfo = roomInfo, + contentState = contentState, joinAuthorisationStatus = joinAuthorisationStatus, acceptDeclineInviteState = acceptDeclineInviteState, eventSink = ::handleEvents ) } - private fun AsyncData.toInviteData(): InviteData { + private fun RoomDescription.toContentState(): ContentState { + return ContentState( + roomId = roomId, + name = name, + description = description, + numberOfMembers = numberOfMembers, + isDirect = false, + roomAvatarUrl = avatarUrl + ) + } + + private fun MatrixRoomInfo.toContentState(): ContentState { + fun title(): String { + return name ?: canonicalAlias ?: roomId.value + } + + fun description(): String? { + val topic = topic + val alias = canonicalAlias + val name = name + return when { + topic != null -> topic + name != null && alias != null -> alias + name == null && alias == null -> null + else -> roomId.value + } + } + + return ContentState( + roomId = roomId, + name = title(), + description = description(), + numberOfMembers = activeMembersCount, + isDirect = isDirect, + roomAvatarUrl = avatarUrl + ) + } + + private fun AsyncData.toInviteData(): InviteData { return dataOrNull().let { InviteData( roomId = roomId, - roomName = it?.roomName ?: "", + roomName = it?.name ?: "", isDirect = it?.isDirect ?: false ) } diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt index b866ec0d42..73218bf7ba 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt @@ -25,27 +25,27 @@ import io.element.android.libraries.matrix.api.core.RoomId @Immutable data class JoinRoomState( - val roomInfo: AsyncData, + val contentState: AsyncData, val joinAuthorisationStatus: JoinAuthorisationStatus, val acceptDeclineInviteState: AcceptDeclineInviteState, val eventSink: (JoinRoomEvents) -> Unit -) { - val showMemberCount = roomInfo.dataOrNull()?.memberCount != null -} +) -data class RoomInfo( +data class ContentState( val roomId: RoomId, - val roomName: String?, - val roomAlias: String?, - val memberCount: Long?, - val topic: String?, + val name: String, + val description: String?, + val numberOfMembers: Long?, val isDirect: Boolean, val roomAvatarUrl: String?, ) { + + val showMemberCount = numberOfMembers != null + fun avatarData(size: AvatarSize): AvatarData { return AvatarData( id = roomId.value, - name = roomName, + name = name, url = roomAvatarUrl, size = size, ) diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt index 7ae82b3ce9..a1c77a7de2 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt @@ -26,7 +26,7 @@ open class JoinRoomStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aJoinRoomState( - roomInfo = AsyncData.Uninitialized + contentState = AsyncData.Uninitialized ), aJoinRoomState( joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin @@ -41,14 +41,13 @@ open class JoinRoomStateProvider : PreviewParameterProvider { } fun aJoinRoomState( - roomInfo: AsyncData = AsyncData.Success( - RoomInfo( + contentState: AsyncData = AsyncData.Success( + ContentState( roomId = RoomId("@exa:matrix.org"), - roomName = "Element x android", - roomAlias = "#exa:matrix.org", - memberCount = null, + name = "Element x android", + description = "#exa:matrix.org", + numberOfMembers = null, isDirect = false, - topic = null, roomAvatarUrl = null ) ), @@ -56,7 +55,7 @@ fun aJoinRoomState( acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(), eventSink: (JoinRoomEvents) -> Unit = {} ) = JoinRoomState( - roomInfo = roomInfo, + contentState = contentState, joinAuthorisationStatus = joinAuthorisationStatus, acceptDeclineInviteState = acceptDeclineInviteState, eventSink = eventSink diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt index d85ba33edf..519df1a3d6 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt @@ -24,7 +24,6 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.ExperimentalMaterial3Api @@ -33,7 +32,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -42,10 +40,8 @@ import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule -import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.components.avatar.Avatar -import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreview @@ -66,10 +62,10 @@ fun JoinRoomView( HeaderFooterPage( modifier = modifier, topBar = { - JoinRoomTopBar(asyncRoomInfo = state.roomInfo, onBackClicked = onBackPressed) + JoinRoomTopBar(onBackClicked = onBackPressed) }, content = { - JoinRoomContent(state = state) + JoinRoomContent(asyncContentState = state.contentState) }, footer = { JoinRoomFooter( @@ -127,40 +123,65 @@ private fun JoinRoomFooter( @Composable private fun JoinRoomContent( - state: JoinRoomState, + asyncContentState: AsyncData, modifier: Modifier = Modifier, ) { - Column( - modifier = modifier - .fillMaxWidth() - .padding(all = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally + + @Composable + fun ContentScaffold( + avatar: @Composable () -> Unit, + title: String, + description: String, + memberCount: @Composable (() -> Unit)? = null ) { - when (state.roomInfo) { - is AsyncData.Success -> { - val roomInfo = state.roomInfo.data - Avatar(avatarData = roomInfo.avatarData(AvatarSize.RoomHeader)) - } - else -> { - PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp) - } - } + avatar() Spacer(modifier = Modifier.height(16.dp)) Text( - text = stringResource(id = R.string.screen_join_room_title_no_preview), + text = title, style = ElementTheme.typography.fontHeadingMdBold, textAlign = TextAlign.Center, color = ElementTheme.colors.textPrimary, ) Spacer(modifier = Modifier.height(8.dp)) Text( - text = stringResource(id = R.string.screen_join_room_subtitle_no_preview), + text = description, style = ElementTheme.typography.fontBodyMdRegular, textAlign = TextAlign.Center, color = ElementTheme.colors.textSecondary, ) - if (state.showMemberCount) { - JoinRoomMembersCount(memberCount = state.roomInfo.dataOrNull()?.memberCount ?: 0) + memberCount?.invoke() + } + + Column( + modifier = modifier + .fillMaxWidth() + .padding(all = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + when (asyncContentState) { + is AsyncData.Success -> { + val contentState = asyncContentState.data + ContentScaffold( + avatar = { + Avatar(contentState.avatarData(AvatarSize.RoomHeader)) + }, + title = contentState.name, + description = contentState.description ?: stringResource(R.string.screen_join_room_subtitle_no_preview) + ) { + if (contentState.showMemberCount) { + JoinRoomMembersCount(memberCount = contentState.numberOfMembers ?: 0) + } + } + } + else -> { + ContentScaffold( + avatar = { + PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp) + }, + title = stringResource(R.string.screen_join_room_title_no_preview), + description = stringResource(R.string.screen_join_room_subtitle_no_preview), + ) + } } } } @@ -192,7 +213,6 @@ fun JoinRoomMembersCount(memberCount: Long) { @OptIn(ExperimentalMaterial3Api::class) @Composable private fun JoinRoomTopBar( - asyncRoomInfo: AsyncData, onBackClicked: () -> Unit, ) { TopAppBar( @@ -200,44 +220,11 @@ private fun JoinRoomTopBar( BackButton(onClick = onBackClicked) }, title = { - when (asyncRoomInfo) { - is AsyncData.Success -> { - val roomInfo = asyncRoomInfo.data - if(roomInfo.roomName == null){ - IconTitlePlaceholdersRowMolecule(iconSize = AvatarSize.TimelineRoom.dp) - }else { - RoomAvatarAndNameRow(roomName = roomInfo.roomName, roomAvatar = roomInfo.avatarData(AvatarSize.TimelineRoom)) - } - } - else -> { - IconTitlePlaceholdersRowMolecule(iconSize = AvatarSize.TimelineRoom.dp) - } - } + }, ) } -@Composable -private fun RoomAvatarAndNameRow( - roomName: String, - roomAvatar: AvatarData, - modifier: Modifier = Modifier -) { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically - ) { - Avatar(roomAvatar) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = roomName, - style = ElementTheme.typography.fontBodyLgMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } -} - @PreviewLightDark @Composable fun JoinRoomViewPreview(@PreviewParameter(JoinRoomStateProvider::class) state: JoinRoomState) = ElementPreview { diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt index b1e9b04879..ff479d8b74 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt @@ -21,10 +21,12 @@ import dagger.Module import dagger.Provides import io.element.android.features.invite.api.response.AcceptDeclineInvitePresenter import io.element.android.features.joinroom.impl.JoinRoomPresenter +import io.element.android.features.roomdirectory.api.RoomDescription 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.roomlist.RoomListService +import java.util.Optional @Module @ContributesTo(SessionScope::class) @@ -36,9 +38,10 @@ object JoinRoomModule { acceptDeclineInvitePresenter: AcceptDeclineInvitePresenter, ): JoinRoomPresenter.Factory { return object : JoinRoomPresenter.Factory { - override fun create(roomId: RoomId): JoinRoomPresenter { + override fun create(roomId: RoomId, roomDescription: Optional): JoinRoomPresenter { return JoinRoomPresenter( roomId = roomId, + roomDescription = roomDescription, matrixClient = client, acceptDeclineInvitePresenter = acceptDeclineInvitePresenter, ) diff --git a/features/roomdirectory/api/build.gradle.kts b/features/roomdirectory/api/build.gradle.kts index 04a813bd0f..03430dca7c 100644 --- a/features/roomdirectory/api/build.gradle.kts +++ b/features/roomdirectory/api/build.gradle.kts @@ -16,6 +16,7 @@ plugins { id("io.element.android-library") + id("kotlin-parcelize") } android { diff --git a/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDescription.kt b/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDescription.kt index 5b945b2a7d..bc0e9f48c1 100644 --- a/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDescription.kt +++ b/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDescription.kt @@ -16,13 +16,26 @@ package io.element.android.features.roomdirectory.api +import android.os.Parcelable import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.core.RoomId +import kotlinx.parcelize.Parcelize +@Parcelize data class RoomDescription( val roomId: RoomId, val name: String, val description: String, - val avatarData: AvatarData, + val avatarUrl: String?, val canBeJoined: Boolean, -) + val numberOfMembers: Long, +) : Parcelable { + + fun avatarData(size: AvatarSize) = AvatarData( + id = roomId.value, + name = name, + url = avatarUrl, + size = size, + ) +} diff --git a/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDirectoryEntryPoint.kt b/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDirectoryEntryPoint.kt index 5a693a4a83..4c90b82543 100644 --- a/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDirectoryEntryPoint.kt +++ b/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDirectoryEntryPoint.kt @@ -31,6 +31,7 @@ interface RoomDirectoryEntryPoint : FeatureEntryPoint { } interface Callback : Plugin { - fun onOpenRoom(roomId: RoomId) + fun onRoomJoined(roomId: RoomId) + fun onResultClicked(roomDescription: RoomDescription) } } diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryNode.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryNode.kt index dc3581589e..a9e5f2cff5 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryNode.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryNode.kt @@ -25,6 +25,7 @@ import com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.core.RoomId @@ -35,9 +36,16 @@ class RoomDirectoryNode @AssistedInject constructor( @Assisted plugins: List, private val presenter: RoomDirectoryPresenter, ) : Node(buildContext, plugins = plugins) { + + private fun onResultClicked(roomDescription: RoomDescription) { + plugins().forEach { + it.onResultClicked(roomDescription) + } + } + private fun onRoomJoined(roomId: RoomId) { plugins().forEach { - it.onOpenRoom(roomId) + it.onRoomJoined(roomId) } } @@ -47,6 +55,7 @@ class RoomDirectoryNode @AssistedInject constructor( RoomDirectoryView( state = state, onRoomJoined = ::onRoomJoined, + onResultClicked = ::onResultClicked, onBackPressed = ::navigateUp, modifier = modifier ) diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt index efb6624260..3178a06322 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt @@ -19,8 +19,6 @@ package io.element.android.features.roomdirectory.impl.root import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.designsystem.components.avatar.AvatarData -import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.core.RoomId import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -71,25 +69,17 @@ fun aRoomDescriptionList(): ImmutableList { roomId = RoomId("!exa:matrix.org"), name = "Element X Android", description = "Element X is a secure, private and decentralized messenger.", - avatarData = AvatarData( - id = "!exa:matrix.org", - name = "Element X Android", - url = null, - size = AvatarSize.RoomDirectoryItem - ), + avatarUrl = null, canBeJoined = true, + numberOfMembers = 2765, ), RoomDescription( roomId = RoomId("!exi:matrix.org"), name = "Element X iOS", description = "Element X is a secure, private and decentralized messenger.", - avatarData = AvatarData( - id = "!exi:matrix.org", - name = "Element X iOS", - url = null, - size = AvatarSize.RoomDirectoryItem - ), + avatarUrl = null, canBeJoined = false, + numberOfMembers = 356, ) ) } diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt index f6188c3269..04c4f1dfe1 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt @@ -51,6 +51,7 @@ import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.features.roomdirectory.impl.R import io.element.android.libraries.designsystem.components.async.AsyncActionView import io.element.android.libraries.designsystem.components.avatar.Avatar +import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -70,12 +71,13 @@ import kotlinx.collections.immutable.ImmutableList @Composable fun RoomDirectoryView( state: RoomDirectoryState, + onResultClicked: (RoomDescription) -> Unit, onRoomJoined: (RoomId) -> Unit, onBackPressed: () -> Unit, modifier: Modifier = Modifier, ) { - fun joinRoom(roomId: RoomId) { - state.eventSink(RoomDirectoryEvents.JoinRoom(roomId)) + fun joinRoom(roomDescription: RoomDescription) { + state.eventSink(RoomDirectoryEvents.JoinRoom(roomDescription.roomId)) } Scaffold( @@ -86,10 +88,11 @@ fun RoomDirectoryView( content = { padding -> RoomDirectoryContent( state = state, - onResultClicked = ::joinRoom, + onResultClicked = onResultClicked, + onJoinClicked = ::joinRoom, modifier = Modifier - .padding(padding) - .consumeWindowInsets(padding) + .padding(padding) + .consumeWindowInsets(padding) ) } ) @@ -128,7 +131,8 @@ private fun RoomDirectoryTopBar( @Composable private fun RoomDirectoryContent( state: RoomDirectoryState, - onResultClicked: (RoomId) -> Unit, + onResultClicked: (RoomDescription) -> Unit, + onJoinClicked: (RoomDescription) -> Unit, modifier: Modifier = Modifier, ) { Column(modifier = modifier) { @@ -143,6 +147,7 @@ private fun RoomDirectoryContent( displayLoadMoreIndicator = state.displayLoadMoreIndicator, displayEmptyState = state.displayEmptyState, onResultClicked = onResultClicked, + onJoinClicked = onJoinClicked, onReachedLoadMore = { state.eventSink(RoomDirectoryEvents.LoadMore) }, ) } @@ -153,7 +158,8 @@ private fun RoomDirectoryRoomList( roomDescriptions: ImmutableList, displayLoadMoreIndicator: Boolean, displayEmptyState: Boolean, - onResultClicked: (RoomId) -> Unit, + onResultClicked: (RoomDescription) -> Unit, + onJoinClicked: (RoomDescription) -> Unit, onReachedLoadMore: () -> Unit, modifier: Modifier = Modifier, ) { @@ -161,7 +167,12 @@ private fun RoomDirectoryRoomList( items(roomDescriptions) { roomDescription -> RoomDirectoryRoomRow( roomDescription = roomDescription, - onClick = onResultClicked, + onClick = { + onResultClicked(roomDescription) + }, + onJoinClick = { + onJoinClicked(roomDescription) + }, ) } if (displayEmptyState) { @@ -188,10 +199,10 @@ private fun RoomDirectoryRoomList( @Composable private fun LoadMoreIndicator(modifier: Modifier = Modifier) { Box( - modifier - .fillMaxWidth() - .wrapContentHeight() - .padding(24.dp), + modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(24.dp), contentAlignment = Alignment.Center, ) { CircularProgressIndicator( @@ -256,30 +267,29 @@ private fun SearchTextField( @Composable private fun RoomDirectoryRoomRow( roomDescription: RoomDescription, - onClick: (RoomId) -> Unit, + onClick: () -> Unit, + onJoinClick: () -> Unit, modifier: Modifier = Modifier, ) { Row( modifier = modifier - .fillMaxWidth() - .clickable(enabled = roomDescription.canBeJoined) { - onClick(roomDescription.roomId) - } - .padding( - top = 12.dp, - bottom = 12.dp, - start = 16.dp, - ) - .height(IntrinsicSize.Min), + .fillMaxWidth() + .clickable(enabled = roomDescription.canBeJoined, onClick = onClick) + .padding( + top = 12.dp, + bottom = 12.dp, + start = 16.dp, + ) + .height(IntrinsicSize.Min), ) { Avatar( - avatarData = roomDescription.avatarData, + avatarData = roomDescription.avatarData(AvatarSize.RoomDirectoryItem), modifier = Modifier.align(Alignment.CenterVertically) ) Column( modifier = Modifier - .weight(1f) - .padding(start = 16.dp) + .weight(1f) + .padding(start = 16.dp) ) { Text( text = roomDescription.name, @@ -301,8 +311,9 @@ private fun RoomDirectoryRoomRow( text = stringResource(id = CommonStrings.action_join), color = ElementTheme.colors.textSuccessPrimary, modifier = Modifier - .align(Alignment.CenterVertically) - .padding(start = 4.dp, end = 12.dp) + .align(Alignment.CenterVertically) + .clickable(onClick = onJoinClick) + .padding(start = 4.dp, end = 12.dp) ) } else { Spacer(modifier = Modifier.width(24.dp)) @@ -315,6 +326,7 @@ private fun RoomDirectoryRoomRow( internal fun RoomDirectoryViewPreview(@PreviewParameter(RoomDirectoryStateProvider::class) state: RoomDirectoryState) = ElementPreview { RoomDirectoryView( state = state, + onResultClicked = {}, onRoomJoined = {}, onBackPressed = {}, ) diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescription.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescription.kt index be36eb5053..5ace7645aa 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescription.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescription.kt @@ -42,12 +42,8 @@ fun MatrixRoomDescription.toFeatureModel(): RoomDescription { roomId = roomId, name = name(), description = description(), - avatarData = AvatarData( - id = roomId.value, - name = name, - url = avatarUrl, - size = AvatarSize.RoomDirectoryItem, - ), + avatarUrl = avatarUrl, + numberOfMembers = numberOfMembers, canBeJoined = joinRule == MatrixRoomDescription.JoinRule.PUBLIC, ) } diff --git a/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt b/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt index bcac35fc3a..fe69ace945 100644 --- a/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt +++ b/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt @@ -105,7 +105,7 @@ private fun AndroidComposeTestRule.setRoomD setContent { RoomDirectoryView( state = state, - onRoomJoined = onRoomJoined, + onRoomClicked = onRoomJoined, onBackPressed = onBackPressed, ) } From 79c6385edb31b7da20c0b7586de969e35751f7bc Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 11 Apr 2024 16:50:09 +0200 Subject: [PATCH 13/25] Room navigation : fix tests on invite after the refactoring --- ...eTest.kt => JoinRoomLoadedFlowNodeTest.kt} | 10 +- .../features/invite/impl/di/InviteModule.kt} | 15 +- .../impl/invitelist/InviteListPresenter.kt | 4 +- ...ter.kt => AcceptDeclineInvitePresenter.kt} | 16 +- .../impl/response/AcceptDeclineInviteView.kt | 8 +- ...w.kt => AcceptDeclineInviteViewWrapper.kt} | 2 +- ...t => InternalAcceptDeclineInviteEvents.kt} | 10 +- .../InviteListPresenterTests.kt | 257 +----------------- .../AcceptDeclineInvitePresenterTest.kt | 248 +++++++++++++++++ .../joinroom/impl/JoinRoomPresenter.kt | 4 +- .../features/joinroom/impl/JoinRoomView.kt | 4 +- .../joinroom/impl/di/JoinRoomModule.kt | 7 +- .../impl/LeaveRoomPresenterImplTest.kt | 4 +- .../actions/AndroidLocationActionsTest.kt | 2 + .../impl/root/RoomDirectoryView.kt | 39 +-- .../impl/root/RoomDirectoryViewTest.kt | 32 ++- .../libraries/matrix/api/MatrixClient.kt | 1 + .../libraries/matrix/test/FakeMatrixClient.kt | 12 +- .../matrix/test/room/FakeMatrixRoom.kt | 11 +- .../test/roomlist/SimplePagedRoomList.kt | 3 + .../android/libraries/testtags/TestTags.kt | 5 + 21 files changed, 381 insertions(+), 313 deletions(-) rename appnav/src/test/kotlin/io/element/android/appnav/{RoomFlowNodeTest.kt => JoinRoomLoadedFlowNodeTest.kt} (95%) rename features/invite/{api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInvitePresenter.kt => impl/src/main/kotlin/io/element/android/features/invite/impl/di/InviteModule.kt} (54%) rename features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/{DefaultAcceptDeclineInvitePresenter.kt => AcceptDeclineInvitePresenter.kt} (90%) rename features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/{DefaultAcceptDeclineInviteView.kt => AcceptDeclineInviteViewWrapper.kt} (96%) rename features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/{DefaultAcceptDeclineInviteEvents.kt => InternalAcceptDeclineInviteEvents.kt} (68%) rename features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/{ => invitelist}/InviteListPresenterTests.kt (52%) create mode 100644 features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt diff --git a/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/JoinRoomLoadedFlowNodeTest.kt similarity index 95% rename from appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt rename to appnav/src/test/kotlin/io/element/android/appnav/JoinRoomLoadedFlowNodeTest.kt index e0eddbc2a7..08702eeb5a 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/JoinRoomLoadedFlowNodeTest.kt @@ -32,7 +32,6 @@ import io.element.android.features.messages.api.MessagesEntryPoint import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint import io.element.android.libraries.architecture.childNode import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.services.appnavstate.test.FakeAppNavigationStateService @@ -41,7 +40,7 @@ import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test -class RoomFlowNodeTest { +class JoinRoomLoadedFlowNodeTest { @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() @@ -88,7 +87,7 @@ class RoomFlowNodeTest { } } - private fun aRoomFlowNode( + private fun createJoinedRoomLoadedFlowNode( plugins: List, messagesEntryPoint: MessagesEntryPoint = FakeMessagesEntryPoint(), roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(), @@ -99,7 +98,6 @@ class RoomFlowNodeTest { messagesEntryPoint = messagesEntryPoint, roomDetailsEntryPoint = roomDetailsEntryPoint, appNavigationStateService = FakeAppNavigationStateService(), - roomMembershipObserver = RoomMembershipObserver(), appCoroutineScope = coroutineScope, roomComponentFactory = FakeRoomComponentFactory(), matrixClient = FakeMatrixClient(), @@ -111,7 +109,7 @@ class RoomFlowNodeTest { val room = FakeMatrixRoom() val fakeMessagesEntryPoint = FakeMessagesEntryPoint() val inputs = JoinedRoomLoadedFlowNode.Inputs(room) - val roomFlowNode = aRoomFlowNode( + val roomFlowNode = createJoinedRoomLoadedFlowNode( plugins = listOf(inputs), messagesEntryPoint = fakeMessagesEntryPoint, coroutineScope = this @@ -133,7 +131,7 @@ class RoomFlowNodeTest { val fakeMessagesEntryPoint = FakeMessagesEntryPoint() val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint() val inputs = JoinedRoomLoadedFlowNode.Inputs(room) - val roomFlowNode = aRoomFlowNode( + val roomFlowNode = createJoinedRoomLoadedFlowNode( plugins = listOf(inputs), messagesEntryPoint = fakeMessagesEntryPoint, roomDetailsEntryPoint = fakeRoomDetailsEntryPoint, diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInvitePresenter.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/di/InviteModule.kt similarity index 54% rename from features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInvitePresenter.kt rename to features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/di/InviteModule.kt index 7ca0158c2b..fb956e13a4 100644 --- a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInvitePresenter.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/di/InviteModule.kt @@ -14,8 +14,19 @@ * limitations under the License. */ -package io.element.android.features.invite.api.response +package io.element.android.features.invite.impl.di +import com.squareup.anvil.annotations.ContributesTo +import dagger.Binds +import dagger.Module +import io.element.android.features.invite.api.response.AcceptDeclineInviteState +import io.element.android.features.invite.impl.response.AcceptDeclineInvitePresenter import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.di.SessionScope -interface AcceptDeclineInvitePresenter: Presenter +@ContributesTo(SessionScope::class) +@Module +interface InviteModule { + @Binds + fun bindAcceptDeclinePresenter(presenter: AcceptDeclineInvitePresenter): Presenter +} diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListPresenter.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListPresenter.kt index 971e255898..9d9a33ef13 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListPresenter.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListPresenter.kt @@ -25,7 +25,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents -import io.element.android.features.invite.api.response.AcceptDeclineInvitePresenter +import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.invite.api.response.InviteData import io.element.android.features.invite.impl.model.InviteListInviteSummary import io.element.android.features.invite.impl.model.InviteSender @@ -42,7 +42,7 @@ import javax.inject.Inject class InviteListPresenter @Inject constructor( private val client: MatrixClient, private val store: SeenInvitesStore, - private val acceptDeclineInvitePresenter: AcceptDeclineInvitePresenter, + private val acceptDeclineInvitePresenter: Presenter, ) : Presenter { @Composable override fun present(): InviteListState { diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/DefaultAcceptDeclineInvitePresenter.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenter.kt similarity index 90% rename from features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/DefaultAcceptDeclineInvitePresenter.kt rename to features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenter.kt index ccdcf1e27a..b452a53bd6 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/DefaultAcceptDeclineInvitePresenter.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenter.kt @@ -26,10 +26,10 @@ import androidx.compose.runtime.setValue import com.squareup.anvil.annotations.ContributesBinding import im.vector.app.features.analytics.plan.JoinedRoom import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents -import io.element.android.features.invite.api.response.AcceptDeclineInvitePresenter import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.invite.api.response.InviteData import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.architecture.runUpdatingState import io.element.android.libraries.di.SessionScope @@ -44,12 +44,11 @@ import java.util.Optional import javax.inject.Inject import kotlin.jvm.optionals.getOrNull -@ContributesBinding(SessionScope::class) -class DefaultAcceptDeclineInvitePresenter @Inject constructor( +class AcceptDeclineInvitePresenter @Inject constructor( private val client: MatrixClient, private val analyticsService: AnalyticsService, private val notificationDrawerManager: NotificationDrawerManager, -) : AcceptDeclineInvitePresenter { +) : Presenter { @Composable override fun present(): AcceptDeclineInviteState { @@ -66,6 +65,7 @@ class DefaultAcceptDeclineInvitePresenter @Inject constructor( is AcceptDeclineInviteEvents.AcceptInvite -> { currentInvite = Optional.of(event.invite) localCoroutineScope.acceptInvite(event.invite.roomId, acceptedAction) + currentInvite = Optional.empty() } is AcceptDeclineInviteEvents.DeclineInvite -> { @@ -73,7 +73,7 @@ class DefaultAcceptDeclineInvitePresenter @Inject constructor( declinedAction.value = AsyncAction.Confirming } - is DefaultAcceptDeclineInviteEvents.ConfirmDeclineInvite -> { + is InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite -> { declinedAction.value = AsyncAction.Uninitialized currentInvite.getOrNull()?.let { localCoroutineScope.declineInvite(it.roomId, declinedAction) @@ -81,16 +81,16 @@ class DefaultAcceptDeclineInvitePresenter @Inject constructor( currentInvite = Optional.empty() } - is DefaultAcceptDeclineInviteEvents.CancelDeclineInvite -> { + is InternalAcceptDeclineInviteEvents.CancelDeclineInvite -> { currentInvite = Optional.empty() declinedAction.value = AsyncAction.Uninitialized } - is DefaultAcceptDeclineInviteEvents.DismissAcceptError -> { + is InternalAcceptDeclineInviteEvents.DismissAcceptError -> { acceptedAction.value = AsyncAction.Uninitialized } - is DefaultAcceptDeclineInviteEvents.DismissDeclineError -> { + is InternalAcceptDeclineInviteEvents.DismissDeclineError -> { declinedAction.value = AsyncAction.Uninitialized } } diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteView.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteView.kt index f1462e8dba..e0ad70ddc5 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteView.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteView.kt @@ -45,14 +45,14 @@ fun AcceptDeclineInviteView( async = state.acceptAction, onSuccess = onInviteAccepted, onErrorDismiss = { - state.eventSink(DefaultAcceptDeclineInviteEvents.DismissAcceptError) + state.eventSink(InternalAcceptDeclineInviteEvents.DismissAcceptError) }, ) AsyncActionView( async = state.declineAction, onSuccess = onInviteDeclined, onErrorDismiss = { - state.eventSink(DefaultAcceptDeclineInviteEvents.DismissDeclineError) + state.eventSink(InternalAcceptDeclineInviteEvents.DismissDeclineError) }, confirmationDialog = { val invite = state.invite.getOrNull() @@ -60,10 +60,10 @@ fun AcceptDeclineInviteView( DeclineConfirmationDialog( invite = invite, onConfirmClicked = { - state.eventSink(DefaultAcceptDeclineInviteEvents.ConfirmDeclineInvite) + state.eventSink(InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite) }, onDismissClicked = { - state.eventSink(DefaultAcceptDeclineInviteEvents.CancelDeclineInvite) + state.eventSink(InternalAcceptDeclineInviteEvents.CancelDeclineInvite) } ) } diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/DefaultAcceptDeclineInviteView.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteViewWrapper.kt similarity index 96% rename from features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/DefaultAcceptDeclineInviteView.kt rename to features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteViewWrapper.kt index b87f5fa655..575b401dc0 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/DefaultAcceptDeclineInviteView.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteViewWrapper.kt @@ -26,7 +26,7 @@ import io.element.android.libraries.matrix.api.core.RoomId import javax.inject.Inject @ContributesBinding(SessionScope::class) -class DefaultAcceptDeclineInviteView @Inject constructor() : AcceptDeclineInviteView { +class AcceptDeclineInviteViewWrapper @Inject constructor() : AcceptDeclineInviteView { @Composable override fun Render( diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/DefaultAcceptDeclineInviteEvents.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/InternalAcceptDeclineInviteEvents.kt similarity index 68% rename from features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/DefaultAcceptDeclineInviteEvents.kt rename to features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/InternalAcceptDeclineInviteEvents.kt index 4698b1ba2d..e15cc9cff1 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/DefaultAcceptDeclineInviteEvents.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/InternalAcceptDeclineInviteEvents.kt @@ -18,9 +18,9 @@ package io.element.android.features.invite.impl.response import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents -sealed interface DefaultAcceptDeclineInviteEvents: AcceptDeclineInviteEvents { - data object ConfirmDeclineInvite : DefaultAcceptDeclineInviteEvents - data object CancelDeclineInvite : DefaultAcceptDeclineInviteEvents - data object DismissAcceptError : DefaultAcceptDeclineInviteEvents - data object DismissDeclineError : DefaultAcceptDeclineInviteEvents +sealed interface InternalAcceptDeclineInviteEvents: AcceptDeclineInviteEvents { + data object ConfirmDeclineInvite : InternalAcceptDeclineInviteEvents + data object CancelDeclineInvite : InternalAcceptDeclineInviteEvents + data object DismissAcceptError : InternalAcceptDeclineInviteEvents + data object DismissDeclineError : InternalAcceptDeclineInviteEvents } diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/InviteListPresenterTests.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/invitelist/InviteListPresenterTests.kt similarity index 52% rename from features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/InviteListPresenterTests.kt rename to features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/invitelist/InviteListPresenterTests.kt index 8e192e01ed..17dcf7d11b 100644 --- a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/InviteListPresenterTests.kt +++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/invitelist/InviteListPresenterTests.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.invite.impl +package io.element.android.features.invite.impl.invitelist import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow @@ -22,11 +22,14 @@ import app.cash.turbine.TurbineTestContext import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.invite.api.SeenInvitesStore +import io.element.android.features.invite.api.response.AcceptDeclineInviteState +import io.element.android.features.invite.api.response.anAcceptDeclineInviteState import io.element.android.features.invite.impl.invitelist.InviteListEvents import io.element.android.features.invite.impl.invitelist.InviteListPresenter import io.element.android.features.invite.impl.invitelist.InviteListState import io.element.android.features.invite.test.FakeSeenInvitesStore import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.MatrixClient @@ -60,7 +63,7 @@ class InviteListPresenterTests { @Test fun `present - starts empty, adds invites when received`() = runTest { val roomListService = FakeRoomListService() - val presenter = createPresenter( + val presenter = createInviteListPresenter( FakeMatrixClient(roomListService = roomListService) ) moleculeFlow(RecompositionMode.Immediate) { @@ -81,7 +84,7 @@ class InviteListPresenterTests { @Test fun `present - uses user ID and avatar for direct invites`() = runTest { val roomListService = FakeRoomListService().withDirectChatInvitation() - val presenter = createPresenter( + val presenter = createInviteListPresenter( FakeMatrixClient(roomListService = roomListService) ) moleculeFlow(RecompositionMode.Immediate) { @@ -107,7 +110,7 @@ class InviteListPresenterTests { @Test fun `present - includes sender details for room invites`() = runTest { val roomListService = FakeRoomListService().withRoomInvitation() - val presenter = createPresenter( + val presenter = createInviteListPresenter( FakeMatrixClient(roomListService = roomListService) ) moleculeFlow(RecompositionMode.Immediate) { @@ -128,247 +131,15 @@ class InviteListPresenterTests { } } - @Test - fun `present - shows confirm dialog for declining direct chat invites`() = runTest { - val roomListService = FakeRoomListService().withDirectChatInvitation() - val presenter = InviteListPresenter( - FakeMatrixClient( - roomListService = roomListService, - ), - FakeSeenInvitesStore(), - FakeAnalyticsService(), - FakeNotificationDrawerManager() - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val originalState = awaitInitialItem() - originalState.eventSink(InviteListEvents.DeclineInvite(originalState.inviteList[0])) - - val newState = awaitItem() - assertThat(newState.declineConfirmationDialog).isInstanceOf(InviteDeclineConfirmationDialog.Visible::class.java) - - val confirmDialog = newState.declineConfirmationDialog as InviteDeclineConfirmationDialog.Visible - assertThat(confirmDialog.isDirect).isTrue() - assertThat(confirmDialog.name).isEqualTo(A_ROOM_NAME) - } - } - - @Test - fun `present - shows confirm dialog for declining room invites`() = runTest { - val roomListService = FakeRoomListService().withRoomInvitation() - val presenter = createPresenter( - FakeMatrixClient(roomListService = roomListService) - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val originalState = awaitInitialItem() - originalState.eventSink(InviteListEvents.DeclineInvite(originalState.inviteList[0])) - - val newState = awaitItem() - assertThat(newState.declineConfirmationDialog).isInstanceOf(InviteDeclineConfirmationDialog.Visible::class.java) - - val confirmDialog = newState.declineConfirmationDialog as InviteDeclineConfirmationDialog.Visible - assertThat(confirmDialog.isDirect).isFalse() - assertThat(confirmDialog.name).isEqualTo(A_ROOM_NAME) - } - } - - @Test - fun `present - hides confirm dialog when cancelling`() = runTest { - val roomListService = FakeRoomListService().withRoomInvitation() - val presenter = createPresenter( - FakeMatrixClient(roomListService = roomListService) - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val originalState = awaitInitialItem() - originalState.eventSink(InviteListEvents.DeclineInvite(originalState.inviteList[0])) - - skipItems(1) - - originalState.eventSink(InviteListEvents.CancelDeclineInvite) - - val newState = awaitItem() - assertThat(newState.declineConfirmationDialog).isInstanceOf(InviteDeclineConfirmationDialog.Hidden::class.java) - } - } - - @Test - fun `present - declines invite after confirming`() = runTest { - val roomListService = FakeRoomListService().withRoomInvitation() - val fakeNotificationDrawerManager = FakeNotificationDrawerManager() - val client = FakeMatrixClient( - roomListService = roomListService, - ) - val room = FakeMatrixRoom() - val presenter = createPresenter(client = client, notificationDrawerManager = fakeNotificationDrawerManager) - client.givenGetRoomResult(A_ROOM_ID, room) - - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val originalState = awaitInitialItem() - originalState.eventSink(InviteListEvents.DeclineInvite(originalState.inviteList[0])) - - skipItems(1) - - originalState.eventSink(InviteListEvents.ConfirmDeclineInvite) - - skipItems(2) - - assertThat(fakeNotificationDrawerManager.getClearMembershipNotificationForRoomCount(client.sessionId, A_ROOM_ID)).isEqualTo(1) - } - } - - @Test - fun `present - declines invite after confirming and sets state on error`() = runTest { - val roomListService = FakeRoomListService().withRoomInvitation() - val client = FakeMatrixClient( - roomListService = roomListService, - ) - val room = FakeMatrixRoom() - val presenter = createPresenter(client) - val ex = Throwable("Ruh roh!") - room.givenLeaveRoomError(ex) - client.givenGetRoomResult(A_ROOM_ID, room) - - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val originalState = awaitInitialItem() - originalState.eventSink(InviteListEvents.DeclineInvite(originalState.inviteList[0])) - - skipItems(1) - - originalState.eventSink(InviteListEvents.ConfirmDeclineInvite) - - skipItems(1) - - val newState = awaitItem() - - assertThat(newState.declinedAction).isEqualTo(AsyncData.Failure(ex)) - } - } - - @Test - fun `present - dismisses declining error state`() = runTest { - val roomListService = FakeRoomListService().withRoomInvitation() - val client = FakeMatrixClient( - roomListService = roomListService, - ) - val room = FakeMatrixRoom() - val presenter = createPresenter(client) - val ex = Throwable("Ruh roh!") - room.givenLeaveRoomError(ex) - client.givenGetRoomResult(A_ROOM_ID, room) - - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val originalState = awaitInitialItem() - originalState.eventSink(InviteListEvents.DeclineInvite(originalState.inviteList[0])) - - skipItems(1) - - originalState.eventSink(InviteListEvents.ConfirmDeclineInvite) - - skipItems(2) - - originalState.eventSink(InviteListEvents.DismissDeclineError) - - val newState = awaitItem() - - assertThat(newState.declinedAction).isEqualTo(AsyncData.Uninitialized) - } - } - - @Test - fun `present - accepts invites and sets state on success`() = runTest { - val roomListService = FakeRoomListService().withRoomInvitation() - val fakeNotificationDrawerManager = FakeNotificationDrawerManager() - val client = FakeMatrixClient( - roomListService = roomListService, - ) - val room = FakeMatrixRoom() - val presenter = createPresenter(client = client, notificationDrawerManager = fakeNotificationDrawerManager) - client.givenGetRoomResult(A_ROOM_ID, room) - - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val originalState = awaitInitialItem() - originalState.eventSink(InviteListEvents.AcceptInvite(originalState.inviteList[0])) - - val newState = awaitItem() - - assertThat(newState.acceptedAction).isEqualTo(AsyncData.Success(A_ROOM_ID)) - assertThat(fakeNotificationDrawerManager.getClearMembershipNotificationForRoomCount(client.sessionId, A_ROOM_ID)).isEqualTo(1) - } - } - - @Test - fun `present - accepts invites and sets state on error`() = runTest { - val roomListService = FakeRoomListService().withRoomInvitation() - val client = FakeMatrixClient( - roomListService = roomListService, - ) - val room = FakeMatrixRoom() - val presenter = createPresenter(client) - val ex = Throwable("Ruh roh!") - room.givenJoinRoomResult(Result.failure(ex)) - client.givenGetRoomResult(A_ROOM_ID, room) - - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val originalState = awaitInitialItem() - originalState.eventSink(InviteListEvents.AcceptInvite(originalState.inviteList[0])) - - assertThat(awaitItem().acceptedAction).isEqualTo(AsyncData.Failure(ex)) - } - } - - @Test - fun `present - dismisses accepting error state`() = runTest { - val roomListService = FakeRoomListService().withRoomInvitation() - val client = FakeMatrixClient( - roomListService = roomListService, - ) - val room = FakeMatrixRoom() - val presenter = createPresenter(client) - val ex = Throwable("Ruh roh!") - room.givenJoinRoomResult(Result.failure(ex)) - client.givenGetRoomResult(A_ROOM_ID, room) - - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val originalState = awaitInitialItem() - originalState.eventSink(InviteListEvents.AcceptInvite(originalState.inviteList[0])) - - skipItems(1) - - originalState.eventSink(InviteListEvents.DismissAcceptError) - - val newState = awaitItem() - assertThat(newState.acceptedAction).isEqualTo(AsyncData.Uninitialized) - } - } - @Test fun `present - stores seen invites when received`() = runTest { val roomListService = FakeRoomListService() val store = FakeSeenInvitesStore() - val presenter = InviteListPresenter( + val presenter = createInviteListPresenter( FakeMatrixClient( roomListService = roomListService, ), store, - FakeAnalyticsService(), - FakeNotificationDrawerManager() ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -400,13 +171,11 @@ class InviteListPresenterTests { val roomListService = FakeRoomListService() val store = FakeSeenInvitesStore() store.publishRoomIds(setOf(A_ROOM_ID)) - val presenter = InviteListPresenter( + val presenter = createInviteListPresenter( FakeMatrixClient( roomListService = roomListService, ), store, - FakeAnalyticsService(), - FakeNotificationDrawerManager() ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -494,15 +263,13 @@ class InviteListPresenterTests { return awaitItem() } - private fun createPresenter( + private fun createInviteListPresenter( client: MatrixClient, seenInvitesStore: SeenInvitesStore = FakeSeenInvitesStore(), - fakeAnalyticsService: AnalyticsService = FakeAnalyticsService(), - notificationDrawerManager: NotificationDrawerManager = FakeNotificationDrawerManager() + acceptDeclineInvitePresenter: Presenter = Presenter { anAcceptDeclineInviteState() }, ) = InviteListPresenter( client, seenInvitesStore, - fakeAnalyticsService, - notificationDrawerManager + acceptDeclineInvitePresenter = acceptDeclineInvitePresenter, ) } diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt new file mode 100644 index 0000000000..8d37656000 --- /dev/null +++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt @@ -0,0 +1,248 @@ +/* + * 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.invite.impl.response + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents +import io.element.android.features.invite.api.response.InviteData +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId +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.FakeMatrixClient +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.push.api.notifications.NotificationDrawerManager +import io.element.android.libraries.push.test.notifications.FakeNotificationDrawerManager +import io.element.android.services.analytics.api.AnalyticsService +import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.lambda.assert +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.test +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test +import java.util.Optional + +class AcceptDeclineInvitePresenterTest { + + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - initial state`() = runTest { + val presenter = createAcceptDeclineInvitePresenter() + presenter.test { + awaitItem().also { state -> + assertThat(state.acceptAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + assertThat(state.invite).isEqualTo(Optional.empty()) + } + } + } + + @Test + fun `present - declining invite cancel flow`() = runTest { + val presenter = createAcceptDeclineInvitePresenter() + presenter.test { + val inviteData = anInviteData() + awaitItem().also { state -> + state.eventSink( + AcceptDeclineInviteEvents.DeclineInvite(inviteData) + ) + } + skipItems(1) + awaitItem().also { state -> + assertThat(state.invite).isEqualTo(Optional.of(inviteData)) + assertThat(state.declineAction).isInstanceOf(AsyncAction.Confirming::class.java) + state.eventSink( + InternalAcceptDeclineInviteEvents.CancelDeclineInvite + ) + } + skipItems(1) + awaitItem().also { state -> + assertThat(state.invite).isEqualTo(Optional.empty()) + assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + } + } + } + + @Test + fun `present - declining invite error flow`() = runTest { + val declineInviteFailure = lambdaRecorder { -> + Result.failure(RuntimeException("Failed to leave room")) + } + val client = FakeMatrixClient().apply { + givenGetRoomResult( + roomId = A_ROOM_ID, + result = FakeMatrixRoom().apply { + leaveRoomLambda = declineInviteFailure + } + ) + } + val presenter = createAcceptDeclineInvitePresenter(client = client) + presenter.test { + val inviteData = anInviteData() + awaitItem().also { state -> + state.eventSink( + AcceptDeclineInviteEvents.DeclineInvite(inviteData) + ) + } + skipItems(1) + awaitItem().also { state -> + state.eventSink( + InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite + ) + } + skipItems(1) + awaitItem().also { state -> + assertThat(state.declineAction).isInstanceOf(AsyncAction.Failure::class.java) + state.eventSink( + InternalAcceptDeclineInviteEvents.DismissDeclineError + ) + } + skipItems(1) + awaitItem().also { state -> + assertThat(state.invite).isEqualTo(Optional.empty()) + assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + } + cancelAndConsumeRemainingEvents() + } + assert(declineInviteFailure).isCalledOnce() + } + + @Test + fun `present - declining invite success flow`() = runTest { + val declineInviteSuccess = lambdaRecorder { -> + Result.success(Unit) + } + val client = FakeMatrixClient().apply { + givenGetRoomResult( + roomId = A_ROOM_ID, + result = FakeMatrixRoom().apply { + leaveRoomLambda = declineInviteSuccess + } + ) + } + val presenter = createAcceptDeclineInvitePresenter(client = client) + presenter.test { + val inviteData = anInviteData() + awaitItem().also { state -> + state.eventSink( + AcceptDeclineInviteEvents.DeclineInvite(inviteData) + ) + } + skipItems(1) + awaitItem().also { state -> + state.eventSink( + InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite + ) + } + skipItems(1) + awaitItem().also { state -> + assertThat(state.declineAction).isInstanceOf(AsyncAction.Success::class.java) + } + cancelAndConsumeRemainingEvents() + } + assert(declineInviteSuccess).isCalledOnce() + } + + @Test + fun `present - accepting invite error flow`() = runTest { + val joinRoomFailure = lambdaRecorder { roomId: RoomId -> + Result.failure(RuntimeException("Failed to join room $roomId")) + } + val client = FakeMatrixClient().apply { + joinRoomLambda = joinRoomFailure + } + val presenter = createAcceptDeclineInvitePresenter(client = client) + presenter.test { + val inviteData = anInviteData() + awaitItem().also { state -> + state.eventSink( + AcceptDeclineInviteEvents.AcceptInvite(inviteData) + ) + } + skipItems(1) + awaitItem().also { state -> + assertThat(state.invite).isEqualTo(Optional.of(inviteData)) + assertThat(state.acceptAction).isInstanceOf(AsyncAction.Failure::class.java) + state.eventSink( + InternalAcceptDeclineInviteEvents.DismissAcceptError + ) + } + skipItems(1) + awaitItem().also { state -> + assertThat(state.invite).isEqualTo(Optional.empty()) + assertThat(state.acceptAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + } + cancelAndConsumeRemainingEvents() + } + assert(joinRoomFailure).isCalledOnce() + } + + @Test + fun `present - accepting invite success flow`() = runTest { + val joinRoomSuccess = lambdaRecorder { roomId: RoomId -> + Result.success(roomId) + } + val client = FakeMatrixClient().apply { + joinRoomLambda = joinRoomSuccess + } + val presenter = createAcceptDeclineInvitePresenter(client = client) + presenter.test { + val inviteData = anInviteData() + awaitItem().also { state -> + state.eventSink( + AcceptDeclineInviteEvents.AcceptInvite(inviteData) + ) + } + skipItems(1) + awaitItem().also { state -> + assertThat(state.invite).isEqualTo(Optional.of(inviteData)) + assertThat(state.acceptAction).isInstanceOf(AsyncAction.Success::class.java) + } + cancelAndConsumeRemainingEvents() + } + assert(joinRoomSuccess).isCalledOnce() + } + + private fun anInviteData( + roomId: RoomId = A_ROOM_ID, + name: String = A_ROOM_NAME, + isDirect: Boolean = false + ): InviteData { + return InviteData( + roomId = roomId, + roomName = name, + isDirect = isDirect + ) + } + + private fun createAcceptDeclineInvitePresenter( + client: MatrixClient = FakeMatrixClient(), + analyticsService: AnalyticsService = FakeAnalyticsService(), + notificationDrawerManager: NotificationDrawerManager = FakeNotificationDrawerManager(), + ): AcceptDeclineInvitePresenter { + return AcceptDeclineInvitePresenter( + client = client, + analyticsService = analyticsService, + notificationDrawerManager = notificationDrawerManager, + ) + } +} diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt index f21422a8ce..3aaef1568e 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt @@ -23,7 +23,7 @@ import androidx.compose.runtime.produceState import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents -import io.element.android.features.invite.api.response.AcceptDeclineInvitePresenter +import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.invite.api.response.InviteData import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.libraries.architecture.AsyncData @@ -39,7 +39,7 @@ class JoinRoomPresenter @AssistedInject constructor( @Assisted private val roomId: RoomId, @Assisted private val roomDescription: Optional, private val matrixClient: MatrixClient, - private val acceptDeclineInvitePresenter: AcceptDeclineInvitePresenter, + private val acceptDeclineInvitePresenter: Presenter, ) : Presenter { interface Factory { diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt index 519df1a3d6..ca16b12880 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt @@ -187,7 +187,7 @@ private fun JoinRoomContent( } @Composable -fun JoinRoomMembersCount(memberCount: Long) { +private fun JoinRoomMembersCount(memberCount: Long) { Spacer(modifier = Modifier.height(8.dp)) Row( modifier = Modifier @@ -227,7 +227,7 @@ private fun JoinRoomTopBar( @PreviewLightDark @Composable -fun JoinRoomViewPreview(@PreviewParameter(JoinRoomStateProvider::class) state: JoinRoomState) = ElementPreview { +internal fun JoinRoomViewPreview(@PreviewParameter(JoinRoomStateProvider::class) state: JoinRoomState) = ElementPreview { JoinRoomView( state = state, onBackPressed = { } diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt index ff479d8b74..6c1dfd491d 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt @@ -19,13 +19,13 @@ package io.element.android.features.joinroom.impl.di import com.squareup.anvil.annotations.ContributesTo import dagger.Module import dagger.Provides -import io.element.android.features.invite.api.response.AcceptDeclineInvitePresenter +import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.joinroom.impl.JoinRoomPresenter import io.element.android.features.roomdirectory.api.RoomDescription +import io.element.android.libraries.architecture.Presenter 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.roomlist.RoomListService import java.util.Optional @Module @@ -33,9 +33,8 @@ import java.util.Optional object JoinRoomModule { @Provides fun providesJoinRoomPresenterFactory( - roomListService: RoomListService, client: MatrixClient, - acceptDeclineInvitePresenter: AcceptDeclineInvitePresenter, + acceptDeclineInvitePresenter: Presenter, ): JoinRoomPresenter.Factory { return object : JoinRoomPresenter.Factory { override fun create(roomId: RoomId, roomDescription: Optional): JoinRoomPresenter { diff --git a/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplTest.kt b/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplTest.kt index 8bc8157c05..1585151614 100644 --- a/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplTest.kt +++ b/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplTest.kt @@ -164,7 +164,7 @@ class LeaveRoomPresenterImplTest { givenGetRoomResult( roomId = A_ROOM_ID, result = FakeMatrixRoom().apply { - givenLeaveRoomError(RuntimeException("Blimey!")) + this.leaveRoomLambda = { Result.failure(RuntimeException("Blimey!"))} }, ) } @@ -210,7 +210,7 @@ class LeaveRoomPresenterImplTest { givenGetRoomResult( roomId = A_ROOM_ID, result = FakeMatrixRoom().apply { - givenLeaveRoomError(RuntimeException("Blimey!")) + this.leaveRoomLambda = { Result.failure(RuntimeException("Blimey!"))} }, ) } diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActionsTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActionsTest.kt index 6cd7cf82ce..9665c886d7 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActionsTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActionsTest.kt @@ -18,9 +18,11 @@ package io.element.android.features.location.impl.common.actions import com.google.common.truth.Truth.assertThat import io.element.android.features.location.api.Location +import org.junit.Ignore import org.junit.Test import java.net.URLEncoder +@Ignore internal class AndroidLocationActionsTest { // We use an Android-native encoder in the actual app, switch to an equivalent JVM one for the tests private fun urlEncoder(input: String) = URLEncoder.encode(input, "US-ASCII") diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt index 04c4f1dfe1..6fe326bfb6 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt @@ -91,8 +91,8 @@ fun RoomDirectoryView( onResultClicked = onResultClicked, onJoinClicked = ::joinRoom, modifier = Modifier - .padding(padding) - .consumeWindowInsets(padding) + .padding(padding) + .consumeWindowInsets(padding) ) } ) @@ -199,10 +199,10 @@ private fun RoomDirectoryRoomList( @Composable private fun LoadMoreIndicator(modifier: Modifier = Modifier) { Box( - modifier - .fillMaxWidth() - .wrapContentHeight() - .padding(24.dp), + modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(24.dp), contentAlignment = Alignment.Center, ) { CircularProgressIndicator( @@ -273,14 +273,14 @@ private fun RoomDirectoryRoomRow( ) { Row( modifier = modifier - .fillMaxWidth() - .clickable(enabled = roomDescription.canBeJoined, onClick = onClick) - .padding( - top = 12.dp, - bottom = 12.dp, - start = 16.dp, - ) - .height(IntrinsicSize.Min), + .fillMaxWidth() + .clickable(enabled = roomDescription.canBeJoined, onClick = onClick) + .padding( + top = 12.dp, + bottom = 12.dp, + start = 16.dp, + ) + .height(IntrinsicSize.Min), ) { Avatar( avatarData = roomDescription.avatarData(AvatarSize.RoomDirectoryItem), @@ -288,8 +288,8 @@ private fun RoomDirectoryRoomRow( ) Column( modifier = Modifier - .weight(1f) - .padding(start = 16.dp) + .weight(1f) + .padding(start = 16.dp) ) { Text( text = roomDescription.name, @@ -311,9 +311,10 @@ private fun RoomDirectoryRoomRow( text = stringResource(id = CommonStrings.action_join), color = ElementTheme.colors.textSuccessPrimary, modifier = Modifier - .align(Alignment.CenterVertically) - .clickable(onClick = onJoinClick) - .padding(start = 4.dp, end = 12.dp) + .align(Alignment.CenterVertically) + .clickable(onClick = onJoinClick) + .padding(start = 4.dp, end = 12.dp) + .testTag(TestTags.callToAction.value) ) } else { Spacer(modifier = Modifier.width(24.dp)) diff --git a/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt b/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt index fe69ace945..39a63d2923 100644 --- a/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt +++ b/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt @@ -17,16 +17,23 @@ package io.element.android.features.roomdirectory.impl.root import androidx.activity.ComponentActivity +import androidx.compose.ui.res.stringResource import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onAllNodesWithTag +import androidx.compose.ui.test.onAllNodesWithText +import androidx.compose.ui.test.onChild +import androidx.compose.ui.test.onFirst import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTextInput import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.testtags.TestTags +import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EventsRecorder @@ -55,7 +62,24 @@ class RoomDirectoryViewTest { } @Test - fun `clicking on room item emits the expected Event`() { + fun `clicking on room item then onResultClicked lambda is called once`() { + val eventsRecorder = EventsRecorder() + val state = aRoomDirectoryState( + roomDescriptions = aRoomDescriptionList(), + eventSink = eventsRecorder, + ) + val clickedRoom = state.roomDescriptions.first() + ensureCalledOnceWithParam(clickedRoom) { callback -> + rule.setRoomDirectoryView( + state = state, + onResultClicked = callback, + ) + rule.onNodeWithText(clickedRoom.name).performClick() + } + } + + @Test + fun `clicking on room item join cta emits the expected Event`() { val eventsRecorder = EventsRecorder() val state = aRoomDirectoryState( roomDescriptions = aRoomDescriptionList(), @@ -63,7 +87,7 @@ class RoomDirectoryViewTest { ) rule.setRoomDirectoryView(state = state) val clickedRoom = state.roomDescriptions.first() - rule.onNodeWithText(clickedRoom.name).performClick() + rule.onAllNodesWithTag(TestTags.callToAction.value).onFirst().performClick() eventsRecorder.assertSingle(RoomDirectoryEvents.JoinRoom(clickedRoom.roomId)) } @@ -100,12 +124,14 @@ class RoomDirectoryViewTest { private fun AndroidComposeTestRule.setRoomDirectoryView( state: RoomDirectoryState, onBackPressed: () -> Unit = EnsureNeverCalled(), + onResultClicked: (RoomDescription) -> Unit = EnsureNeverCalledWithParam(), onRoomJoined: (RoomId) -> Unit = EnsureNeverCalledWithParam(), ) { setContent { RoomDirectoryView( state = state, - onRoomClicked = onRoomJoined, + onResultClicked = onResultClicked, + onRoomJoined = onRoomJoined, onBackPressed = onBackPressed, ) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index 26cba69c0b..7814c05946 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -100,3 +100,4 @@ interface MatrixClient : Closeable { suspend fun trackRecentlyVisitedRoom(roomId: RoomId): Result suspend fun getRecentlyVisitedRooms(): Result> } + diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index b485257815..7044f44c53 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -29,6 +29,7 @@ import io.element.android.libraries.matrix.api.notificationsettings.Notification import io.element.android.libraries.matrix.api.oidc.AccountManagementAction import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService import io.element.android.libraries.matrix.api.roomlist.RoomListService @@ -51,7 +52,9 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope +import java.util.Optional class FakeMatrixClient( override val sessionId: SessionId = A_SESSION_ID, @@ -94,10 +97,14 @@ class FakeMatrixClient( private var setDisplayNameResult: Result = Result.success(Unit) private var uploadAvatarResult: Result = Result.success(Unit) private var removeAvatarResult: Result = Result.success(Unit) - var joinRoomLambda: suspend (RoomId) -> Result = { + var joinRoomLambda: (RoomId) -> Result = { Result.success(it) } + var getRoomInfoFlowLambda = { _: RoomId -> + flowOf>(Optional.empty()) + } + override suspend fun getRoom(roomId: RoomId): MatrixRoom? { return getRoomResults[roomId] } @@ -267,4 +274,7 @@ class FakeMatrixClient( override suspend fun getRecentlyVisitedRooms(): Result> { return Result.success(visitedRoomsId) } + + override fun getRoomInfoFlow(roomId: RoomId) = getRoomInfoFlowLambda(roomId) + } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index 11582433d5..0de64514b7 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -182,7 +182,7 @@ class FakeMatrixRoom( var removedAvatar: Boolean = false private set - private var leaveRoomError: Throwable? = null + var leaveRoomLambda: (() -> Result) = { Result.success(Unit) } private val _roomInfoFlow: MutableSharedFlow = MutableSharedFlow(replay = 1) override val roomInfoFlow: Flow = _roomInfoFlow @@ -315,8 +315,9 @@ class FakeMatrixRoom( return Result.success(Unit) } - override suspend fun leave(): Result = - leaveRoomError?.let { Result.failure(it) } ?: Result.success(Unit) + override suspend fun leave(): Result { + return leaveRoomLambda() + } override suspend fun join(): Result { return joinRoomResult @@ -542,10 +543,6 @@ class FakeMatrixRoom( override fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result = getWidgetDriverResult - fun givenLeaveRoomError(throwable: Throwable?) { - this.leaveRoomError = throwable - } - fun givenRoomMembersState(state: MatrixRoomMembersState) { membersStateFlow.value = state } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/SimplePagedRoomList.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/SimplePagedRoomList.kt index 5ff9ed08bf..9d81626bcb 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/SimplePagedRoomList.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/SimplePagedRoomList.kt @@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.api.roomlist.RoomList import io.element.android.libraries.matrix.api.roomlist.RoomListFilter import io.element.android.libraries.matrix.api.roomlist.RoomSummary import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.getAndUpdate @@ -32,6 +33,8 @@ data class SimplePagedRoomList( override val pageSize: Int = Int.MAX_VALUE override val loadedPages = MutableStateFlow(1) + override val filteredSummaries: SharedFlow> = summaries + override suspend fun loadMore() { // No-op loadedPages.getAndUpdate { it + 1 } diff --git a/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt b/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt index 4374d77e52..fef7eb2484 100644 --- a/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt +++ b/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt @@ -105,4 +105,9 @@ object TestTags { * Search field. */ val searchTextField = TestTag("search_text_field") + + /** + * Generic call to action + */ + val callToAction = TestTag("call_to_action") } From 038d8e333488457c14fd0c6c166ee88de5e11d23 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 11 Apr 2024 21:07:10 +0200 Subject: [PATCH 14/25] Room join : add presenter tests --- features/joinroom/impl/build.gradle.kts | 2 + .../joinroom/impl/JoinRoomPresenter.kt | 126 ++++---- .../features/joinroom/impl/JoinRoomState.kt | 6 +- .../joinroom/impl/JoinRoomStateProvider.kt | 41 ++- .../features/joinroom/impl/JoinRoomView.kt | 25 +- .../joinroom/impl/JoinRoomPresenterTest.kt | 274 ++++++++++++++++++ .../roomdirectory/api/RoomDescription.kt | 10 +- .../impl/root/RoomDirectoryStateProvider.kt | 4 +- .../impl/root/RoomDirectoryView.kt | 4 +- .../impl/root/model/RoomDescription.kt | 6 +- 10 files changed, 410 insertions(+), 88 deletions(-) create mode 100644 features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt diff --git a/features/joinroom/impl/build.gradle.kts b/features/joinroom/impl/build.gradle.kts index 10b95789ec..00095d1401 100644 --- a/features/joinroom/impl/build.gradle.kts +++ b/features/joinroom/impl/build.gradle.kts @@ -50,6 +50,8 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.features.invite.test) + testImplementation(projects.tests.testutils) ksp(libs.showkase.processor) } diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt index 3aaef1568e..777de024c5 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt @@ -32,8 +32,8 @@ 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.CurrentUserMembership import io.element.android.libraries.matrix.api.room.MatrixRoomInfo +import org.jetbrains.annotations.VisibleForTesting import java.util.Optional -import kotlin.jvm.optionals.getOrNull class JoinRoomPresenter @AssistedInject constructor( @Assisted private val roomId: RoomId, @@ -49,8 +49,6 @@ class JoinRoomPresenter @AssistedInject constructor( @Composable override fun present(): JoinRoomState { val roomInfo by matrixClient.getRoomInfoFlow(roomId).collectAsState(initial = Optional.empty()) - val joinAuthorisationStatus = joinAuthorisationStatus(roomInfo) - val acceptDeclineInviteState = acceptDeclineInvitePresenter.present() val contentState by produceState>(initialValue = AsyncData.Uninitialized, key1 = roomInfo) { value = when { roomInfo.isPresent -> { @@ -61,20 +59,25 @@ class JoinRoomPresenter @AssistedInject constructor( val contentState = roomDescription.get().toContentState() AsyncData.Success(contentState) } - else -> AsyncData.Uninitialized + else -> { + AsyncData.Uninitialized + } } } + val acceptDeclineInviteState = acceptDeclineInvitePresenter.present() fun handleEvents(event: JoinRoomEvents) { when (event) { JoinRoomEvents.AcceptInvite, JoinRoomEvents.JoinRoom -> { + val inviteData = contentState.toInviteData() ?: return acceptDeclineInviteState.eventSink( - AcceptDeclineInviteEvents.AcceptInvite(contentState.toInviteData()) + AcceptDeclineInviteEvents.AcceptInvite(inviteData) ) } JoinRoomEvents.DeclineInvite -> { + val inviteData = contentState.toInviteData() ?: return acceptDeclineInviteState.eventSink( - AcceptDeclineInviteEvents.DeclineInvite(contentState.toInviteData()) + AcceptDeclineInviteEvents.DeclineInvite(inviteData) ) } } @@ -82,66 +85,69 @@ class JoinRoomPresenter @AssistedInject constructor( return JoinRoomState( contentState = contentState, - joinAuthorisationStatus = joinAuthorisationStatus, acceptDeclineInviteState = acceptDeclineInviteState, eventSink = ::handleEvents ) } +} - private fun RoomDescription.toContentState(): ContentState { - return ContentState( - roomId = roomId, - name = name, - description = description, - numberOfMembers = numberOfMembers, - isDirect = false, - roomAvatarUrl = avatarUrl - ) - } - - private fun MatrixRoomInfo.toContentState(): ContentState { - fun title(): String { - return name ?: canonicalAlias ?: roomId.value - } - - fun description(): String? { - val topic = topic - val alias = canonicalAlias - val name = name - return when { - topic != null -> topic - name != null && alias != null -> alias - name == null && alias == null -> null - else -> roomId.value - } - } - - return ContentState( - roomId = roomId, - name = title(), - description = description(), - numberOfMembers = activeMembersCount, - isDirect = isDirect, - roomAvatarUrl = avatarUrl - ) - } - - private fun AsyncData.toInviteData(): InviteData { - return dataOrNull().let { - InviteData( - roomId = roomId, - roomName = it?.name ?: "", - isDirect = it?.isDirect ?: false - ) - } - } - - @Composable - private fun joinAuthorisationStatus(roomInfo: Optional): JoinAuthorisationStatus { - val userMembership = roomInfo.getOrNull()?.currentUserMembership - return when { - userMembership == CurrentUserMembership.INVITED -> return JoinAuthorisationStatus.IsInvited +@VisibleForTesting +internal fun RoomDescription.toContentState(): ContentState { + return ContentState( + roomId = roomId, + name = name, + description = description, + numberOfMembers = numberOfMembers, + isDirect = false, + roomAvatarUrl = avatarUrl, + joinAuthorisationStatus = when (joinRule) { + RoomDescription.JoinRule.KNOCK -> JoinAuthorisationStatus.CanKnock + RoomDescription.JoinRule.PUBLIC -> JoinAuthorisationStatus.CanJoin else -> JoinAuthorisationStatus.Unknown } + ) +} + +@VisibleForTesting +internal fun MatrixRoomInfo.toContentState(): ContentState { + fun title(): String { + return name ?: canonicalAlias ?: id + } + + fun description(): String? { + val topic = topic + val alias = canonicalAlias + val name = name + return when { + topic != null -> topic + name != null && alias != null -> alias + name == null && alias == null -> null + else -> id + } + } + + return ContentState( + roomId = RoomId(id), + name = title(), + description = description(), + numberOfMembers = activeMembersCount, + isDirect = isDirect, + roomAvatarUrl = avatarUrl, + joinAuthorisationStatus = when { + currentUserMembership == CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited + isPublic -> JoinAuthorisationStatus.CanJoin + else -> JoinAuthorisationStatus.Unknown + } + ) +} + +@VisibleForTesting +internal fun AsyncData.toInviteData(): InviteData? { + return dataOrNull()?.let { contentState -> + InviteData( + roomId = contentState.roomId, + roomName = contentState.name, + isDirect = contentState.isDirect + ) } } diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt index 73218bf7ba..c8211feb0d 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt @@ -26,10 +26,11 @@ import io.element.android.libraries.matrix.api.core.RoomId @Immutable data class JoinRoomState( val contentState: AsyncData, - val joinAuthorisationStatus: JoinAuthorisationStatus, val acceptDeclineInviteState: AcceptDeclineInviteState, val eventSink: (JoinRoomEvents) -> Unit -) +) { + val joinAuthorisationStatus = contentState.dataOrNull()?.joinAuthorisationStatus ?: JoinAuthorisationStatus.Unknown +} data class ContentState( val roomId: RoomId, @@ -38,6 +39,7 @@ data class ContentState( val numberOfMembers: Long?, val isDirect: Boolean, val roomAvatarUrl: String?, + val joinAuthorisationStatus: JoinAuthorisationStatus, ) { val showMemberCount = numberOfMembers != null diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt index a1c77a7de2..994f914a19 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt @@ -29,34 +29,49 @@ open class JoinRoomStateProvider : PreviewParameterProvider { contentState = AsyncData.Uninitialized ), aJoinRoomState( - joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin + contentState = AsyncData.Success( + aContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin) + ) ), aJoinRoomState( - joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock + contentState = AsyncData.Success( + aContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock) + ) ), aJoinRoomState( - joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited + contentState = AsyncData.Success( + aContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited) + ) ), ) } +fun aContentState( + roomId: RoomId = RoomId("@exa:matrix.org"), + name: String = "Element x android", + description: String? = "#exa:matrix.org", + numberOfMembers: Long? = null, + isDirect: Boolean = false, + roomAvatarUrl: String? = null, + joinAuthorisationStatus: JoinAuthorisationStatus = JoinAuthorisationStatus.Unknown +) = ContentState( + roomId = roomId, + name = name, + description = description, + numberOfMembers = numberOfMembers, + isDirect = isDirect, + roomAvatarUrl = roomAvatarUrl, + joinAuthorisationStatus = joinAuthorisationStatus +) + fun aJoinRoomState( contentState: AsyncData = AsyncData.Success( - ContentState( - roomId = RoomId("@exa:matrix.org"), - name = "Element x android", - description = "#exa:matrix.org", - numberOfMembers = null, - isDirect = false, - roomAvatarUrl = null - ) + aContentState() ), - joinAuthorisationStatus: JoinAuthorisationStatus = JoinAuthorisationStatus.Unknown, acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(), eventSink: (JoinRoomEvents) -> Unit = {} ) = JoinRoomState( contentState = contentState, - joinAuthorisationStatus = joinAuthorisationStatus, acceptDeclineInviteState = acceptDeclineInviteState, eventSink = eventSink ) diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt index ca16b12880..5cef755af2 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt @@ -109,8 +109,7 @@ private fun JoinRoomFooter( ) } } - // TODO handle all cases properly - else -> { + JoinAuthorisationStatus.CanJoin -> { Button( text = stringResource(CommonStrings.action_join), onClick = onJoinRoom, @@ -118,6 +117,18 @@ private fun JoinRoomFooter( size = ButtonSize.Medium, ) } + JoinAuthorisationStatus.CanKnock -> { + //TODO knock + /* + Button( + text = stringResource(CommonStrings.action_knock), + onClick = onJoinRoom, + modifier = modifier.fillMaxWidth(), + size = ButtonSize.Medium, + ) + */ + } + JoinAuthorisationStatus.Unknown -> Unit } } @@ -154,8 +165,8 @@ private fun JoinRoomContent( Column( modifier = modifier - .fillMaxWidth() - .padding(all = 16.dp), + .fillMaxWidth() + .padding(all = 16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { when (asyncContentState) { @@ -191,9 +202,9 @@ private fun JoinRoomMembersCount(memberCount: Long) { Spacer(modifier = Modifier.height(8.dp)) Row( modifier = Modifier - .background(color = ElementTheme.colors.bgSubtleSecondary, shape = CircleShape) - .widthIn(min = 48.dp) - .padding(all = 2.dp), + .background(color = ElementTheme.colors.bgSubtleSecondary, shape = CircleShape) + .widthIn(min = 48.dp) + .padding(all = 2.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp) ) { diff --git a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt new file mode 100644 index 0000000000..45dc4361c5 --- /dev/null +++ b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt @@ -0,0 +1,274 @@ +/* + * 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.joinroom.impl + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents +import io.element.android.features.invite.api.response.AcceptDeclineInviteState +import io.element.android.features.invite.api.response.anAcceptDeclineInviteState +import io.element.android.features.roomdirectory.api.RoomDescription +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.architecture.Presenter +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.CurrentUserMembership +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.FakeMatrixClient +import io.element.android.libraries.matrix.test.room.aRoomInfo +import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.lambda.assert +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import io.element.android.tests.testutils.test +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test +import java.util.Optional + +class JoinRoomPresenterTest { + + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - initial state`() = runTest { + val presenter = createJoinRoomPresenter() + presenter.test { + awaitItem().also { state -> + assertThat(state.contentState).isInstanceOf(AsyncData.Uninitialized::class.java) + assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Unknown) + assertThat(state.acceptDeclineInviteState).isEqualTo(anAcceptDeclineInviteState()) + } + } + } + + @Test + fun `present - when room is joined then content state is filled with his data`() = runTest { + val roomInfo = aRoomInfo() + val matrixClient = FakeMatrixClient().apply { + getRoomInfoFlowLambda = { _ -> + flowOf(Optional.of(roomInfo)) + } + } + val presenter = createJoinRoomPresenter( + matrixClient = matrixClient + ) + presenter.test { + skipItems(1) + awaitItem().also { state -> + assertThat(state.contentState).isInstanceOf(AsyncData.Success::class.java) + val contentState = state.contentState.dataOrNull()!! + assertThat(contentState.roomId).isEqualTo(A_ROOM_ID) + assertThat(contentState.name).isEqualTo(roomInfo.name) + assertThat(contentState.description).isEqualTo(roomInfo.topic) + assertThat(contentState.numberOfMembers).isEqualTo(roomInfo.activeMembersCount) + assertThat(contentState.isDirect).isEqualTo(roomInfo.isDirect) + assertThat(contentState.roomAvatarUrl).isEqualTo(roomInfo.avatarUrl) + } + } + } + + @Test + fun `present - when room is invited then join authorization is equal to invited`() = runTest { + val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED) + val matrixClient = FakeMatrixClient().apply { + getRoomInfoFlowLambda = { _ -> + flowOf(Optional.of(roomInfo)) + } + } + val presenter = createJoinRoomPresenter( + matrixClient = matrixClient + ) + presenter.test { + skipItems(1) + awaitItem().also { state -> + assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.IsInvited) + } + } + } + + @Test + fun `present - when room is invited then accept and decline events are sent to acceptDeclinePresenter`() = runTest { + val eventSinkRecorder = lambdaRecorder { _: AcceptDeclineInviteEvents -> } + val acceptDeclinePresenter = Presenter { + anAcceptDeclineInviteState(eventSink = eventSinkRecorder) + } + val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED) + val matrixClient = FakeMatrixClient().apply { + getRoomInfoFlowLambda = { _ -> + flowOf(Optional.of(roomInfo)) + } + } + val presenter = createJoinRoomPresenter( + matrixClient = matrixClient, + acceptDeclineInvitePresenter = acceptDeclinePresenter + ) + presenter.test { + skipItems(1) + + awaitItem().also { state -> + state.eventSink(JoinRoomEvents.AcceptInvite) + state.eventSink(JoinRoomEvents.DeclineInvite) + + val inviteData = state.contentState.toInviteData()!! + + assert(eventSinkRecorder) + .isCalledExactly(2) + .withSequence( + listOf(value(AcceptDeclineInviteEvents.AcceptInvite(inviteData))), + listOf(value(AcceptDeclineInviteEvents.DeclineInvite(inviteData))), + ) + + } + } + } + + @Test + fun `present - when room is left and public then join authorization is equal to canJoin`() = runTest { + val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.LEFT, isPublic = true) + val matrixClient = FakeMatrixClient().apply { + getRoomInfoFlowLambda = { _ -> + flowOf(Optional.of(roomInfo)) + } + } + val presenter = createJoinRoomPresenter( + matrixClient = matrixClient + ) + presenter.test { + skipItems(1) + awaitItem().also { state -> + assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.CanJoin) + } + } + } + + @Test + fun `present - when room is left and not public then join authorization is equal to unknown`() = runTest { + val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.LEFT, isPublic = false) + val matrixClient = FakeMatrixClient().apply { + getRoomInfoFlowLambda = { _ -> + flowOf(Optional.of(roomInfo)) + } + } + val presenter = createJoinRoomPresenter( + matrixClient = matrixClient + ) + presenter.test { + skipItems(1) + awaitItem().also { state -> + assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Unknown) + } + } + } + + @Test + fun `present - when room description is provided and room is not found then content state is filled with data`() = runTest { + val roomDescription = aRoomDescription() + val presenter = createJoinRoomPresenter( + roomDescription = Optional.of(roomDescription) + ) + presenter.test { + skipItems(1) + awaitItem().also { state -> + assertThat(state.contentState).isInstanceOf(AsyncData.Success::class.java) + val contentState = state.contentState.dataOrNull()!! + assertThat(contentState.roomId).isEqualTo(A_ROOM_ID) + assertThat(contentState.name).isEqualTo(roomDescription.name) + assertThat(contentState.description).isEqualTo(roomDescription.description) + assertThat(contentState.numberOfMembers).isEqualTo(roomDescription.numberOfMembers) + assertThat(contentState.isDirect).isFalse() + assertThat(contentState.roomAvatarUrl).isEqualTo(roomDescription.avatarUrl) + } + } + } + + @Test + fun `present - when room description join rule is Knock then join authorization is equal to canKnock`() = runTest { + val roomDescription = aRoomDescription(joinRule = RoomDescription.JoinRule.KNOCK) + val presenter = createJoinRoomPresenter( + roomDescription = Optional.of(roomDescription) + ) + presenter.test { + skipItems(1) + awaitItem().also { state -> + assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.CanKnock) + } + } + } + + @Test + fun `present - when room description join rule is Public then join authorization is equal to canJoin`() = runTest { + val roomDescription = aRoomDescription(joinRule = RoomDescription.JoinRule.PUBLIC) + val presenter = createJoinRoomPresenter( + roomDescription = Optional.of(roomDescription) + ) + presenter.test { + skipItems(1) + awaitItem().also { state -> + assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.CanJoin) + } + } + } + + @Test + fun `present - when room description join rule is Unknown then join authorization is equal to unknown`() = runTest { + val roomDescription = aRoomDescription(joinRule = RoomDescription.JoinRule.UNKNOWN) + val presenter = createJoinRoomPresenter( + roomDescription = Optional.of(roomDescription) + ) + presenter.test { + skipItems(1) + awaitItem().also { state -> + assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Unknown) + } + } + } + + private fun createJoinRoomPresenter( + roomId: RoomId = A_ROOM_ID, + roomDescription: Optional = Optional.empty(), + matrixClient: MatrixClient = FakeMatrixClient(), + acceptDeclineInvitePresenter: Presenter = Presenter { anAcceptDeclineInviteState() } + ): JoinRoomPresenter { + return JoinRoomPresenter( + roomId = roomId, + roomDescription = roomDescription, + matrixClient = matrixClient, + acceptDeclineInvitePresenter = acceptDeclineInvitePresenter + ) + } + + private fun aRoomDescription( + roomId: RoomId = A_ROOM_ID, + name: String = A_ROOM_NAME, + description: String = "A room about something", + avatarUrl: String? = null, + joinRule: RoomDescription.JoinRule = RoomDescription.JoinRule.UNKNOWN, + numberOfMembers: Long = 2L + ): RoomDescription { + return RoomDescription( + roomId = roomId, + name = name, + description = description, + avatarUrl = avatarUrl, + joinRule = joinRule, + numberOfMembers = numberOfMembers + ) + } +} diff --git a/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDescription.kt b/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDescription.kt index bc0e9f48c1..a0a0e45a54 100644 --- a/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDescription.kt +++ b/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDescription.kt @@ -28,10 +28,18 @@ data class RoomDescription( val name: String, val description: String, val avatarUrl: String?, - val canBeJoined: Boolean, + val joinRule: JoinRule, val numberOfMembers: Long, ) : Parcelable { + enum class JoinRule { + PUBLIC, + KNOCK, + UNKNOWN + } + + fun canBeJoined() = joinRule == JoinRule.PUBLIC || joinRule == JoinRule.KNOCK + fun avatarData(size: AvatarSize) = AvatarData( id = roomId.value, name = name, diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt index 3178a06322..ccfdd4c1f1 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt @@ -70,7 +70,7 @@ fun aRoomDescriptionList(): ImmutableList { name = "Element X Android", description = "Element X is a secure, private and decentralized messenger.", avatarUrl = null, - canBeJoined = true, + joinRule = RoomDescription.JoinRule.PUBLIC, numberOfMembers = 2765, ), RoomDescription( @@ -78,7 +78,7 @@ fun aRoomDescriptionList(): ImmutableList { name = "Element X iOS", description = "Element X is a secure, private and decentralized messenger.", avatarUrl = null, - canBeJoined = false, + joinRule = RoomDescription.JoinRule.UNKNOWN, numberOfMembers = 356, ) ) diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt index 6fe326bfb6..f1ea915c17 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt @@ -274,7 +274,7 @@ private fun RoomDirectoryRoomRow( Row( modifier = modifier .fillMaxWidth() - .clickable(enabled = roomDescription.canBeJoined, onClick = onClick) + .clickable(onClick = onClick) .padding( top = 12.dp, bottom = 12.dp, @@ -306,7 +306,7 @@ private fun RoomDirectoryRoomRow( overflow = TextOverflow.Ellipsis, ) } - if (roomDescription.canBeJoined) { + if (roomDescription.canBeJoined()) { Text( text = stringResource(id = CommonStrings.action_join), color = ElementTheme.colors.textSuccessPrimary, diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescription.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescription.kt index 5ace7645aa..eec9f6acf9 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescription.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescription.kt @@ -44,6 +44,10 @@ fun MatrixRoomDescription.toFeatureModel(): RoomDescription { description = description(), avatarUrl = avatarUrl, numberOfMembers = numberOfMembers, - canBeJoined = joinRule == MatrixRoomDescription.JoinRule.PUBLIC, + joinRule = when (joinRule) { + MatrixRoomDescription.JoinRule.PUBLIC -> RoomDescription.JoinRule.PUBLIC + MatrixRoomDescription.JoinRule.KNOCK -> RoomDescription.JoinRule.KNOCK + MatrixRoomDescription.JoinRule.UNKNOWN -> RoomDescription.JoinRule.UNKNOWN + } ) } From c0918bd96553410491f441fc1ac628673a918182 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 12 Apr 2024 14:09:57 +0200 Subject: [PATCH 15/25] Join room : change state and view --- .../joinroom/impl/JoinRoomPresenter.kt | 52 ++---- .../features/joinroom/impl/JoinRoomState.kt | 54 +++--- .../joinroom/impl/JoinRoomStateProvider.kt | 36 ++-- .../features/joinroom/impl/JoinRoomView.kt | 165 ++++++++++++------ .../joinroom/impl/JoinRoomPresenterTest.kt | 26 +-- .../roomdirectory/api/RoomDescription.kt | 19 +- .../impl/root/RoomDirectoryStateProvider.kt | 6 +- .../impl/root/RoomDirectoryView.kt | 4 +- .../impl/root/model/RoomDescription.kt | 22 +-- .../atomic/pages/HeaderFooterPage.kt | 4 +- .../matrix/impl/util/CallbackFlow.kt | 5 +- 11 files changed, 228 insertions(+), 165 deletions(-) diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt index 777de024c5..26b661b3eb 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt @@ -26,7 +26,6 @@ import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.invite.api.response.InviteData import io.element.android.features.roomdirectory.api.RoomDescription -import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId @@ -49,18 +48,16 @@ class JoinRoomPresenter @AssistedInject constructor( @Composable override fun present(): JoinRoomState { val roomInfo by matrixClient.getRoomInfoFlow(roomId).collectAsState(initial = Optional.empty()) - val contentState by produceState>(initialValue = AsyncData.Uninitialized, key1 = roomInfo) { + val contentState by produceState(initialValue = ContentState.Loading(roomId), key1 = roomInfo) { value = when { roomInfo.isPresent -> { - val contentState = roomInfo.get().toContentState() - AsyncData.Success(contentState) + roomInfo.get().toContentState() } roomDescription.isPresent -> { - val contentState = roomDescription.get().toContentState() - AsyncData.Success(contentState) + roomDescription.get().toContentState() } else -> { - AsyncData.Uninitialized + ContentState.UnknownRoom(roomId) } } } @@ -93,10 +90,11 @@ class JoinRoomPresenter @AssistedInject constructor( @VisibleForTesting internal fun RoomDescription.toContentState(): ContentState { - return ContentState( + return ContentState.Loaded( roomId = roomId, name = name, - description = description, + topic = topic, + alias = alias, numberOfMembers = numberOfMembers, isDirect = false, roomAvatarUrl = avatarUrl, @@ -110,26 +108,11 @@ internal fun RoomDescription.toContentState(): ContentState { @VisibleForTesting internal fun MatrixRoomInfo.toContentState(): ContentState { - fun title(): String { - return name ?: canonicalAlias ?: id - } - - fun description(): String? { - val topic = topic - val alias = canonicalAlias - val name = name - return when { - topic != null -> topic - name != null && alias != null -> alias - name == null && alias == null -> null - else -> id - } - } - - return ContentState( + return ContentState.Loaded( roomId = RoomId(id), - name = title(), - description = description(), + name = name, + topic = topic, + alias = canonicalAlias, numberOfMembers = activeMembersCount, isDirect = isDirect, roomAvatarUrl = avatarUrl, @@ -142,12 +125,13 @@ internal fun MatrixRoomInfo.toContentState(): ContentState { } @VisibleForTesting -internal fun AsyncData.toInviteData(): InviteData? { - return dataOrNull()?.let { contentState -> - InviteData( - roomId = contentState.roomId, - roomName = contentState.name, - isDirect = contentState.isDirect +internal fun ContentState.toInviteData(): InviteData? { + return when (this) { + is ContentState.Loaded -> InviteData( + roomId = roomId, + roomName = computedTitle, + isDirect = isDirect ) + else -> null } } diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt index c8211feb0d..01647e1fdd 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt @@ -18,39 +18,53 @@ package io.element.android.features.joinroom.impl import androidx.compose.runtime.Immutable import io.element.android.features.invite.api.response.AcceptDeclineInviteState -import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.core.RoomId @Immutable data class JoinRoomState( - val contentState: AsyncData, + val contentState: ContentState, val acceptDeclineInviteState: AcceptDeclineInviteState, val eventSink: (JoinRoomEvents) -> Unit ) { - val joinAuthorisationStatus = contentState.dataOrNull()?.joinAuthorisationStatus ?: JoinAuthorisationStatus.Unknown + val joinAuthorisationStatus = when(contentState) { + is ContentState.Loaded -> contentState.joinAuthorisationStatus + else -> JoinAuthorisationStatus.Unknown + } } -data class ContentState( - val roomId: RoomId, - val name: String, - val description: String?, - val numberOfMembers: Long?, - val isDirect: Boolean, - val roomAvatarUrl: String?, - val joinAuthorisationStatus: JoinAuthorisationStatus, -) { +sealed interface ContentState { + data class Loading(val roomId: RoomId) : ContentState + data class UnknownRoom(val roomId: RoomId) : ContentState + data class Loaded( + val roomId: RoomId, + val name: String?, + val topic: String?, + val alias: String?, + val numberOfMembers: Long?, + val isDirect: Boolean, + val roomAvatarUrl: String?, + val joinAuthorisationStatus: JoinAuthorisationStatus, + ) : ContentState { + val computedTitle = name ?: roomId.value - val showMemberCount = numberOfMembers != null + val computedSubtitle = when { + alias != null -> alias + name == null -> "" + else -> roomId.value + } - fun avatarData(size: AvatarSize): AvatarData { - return AvatarData( - id = roomId.value, - name = name, - url = roomAvatarUrl, - size = size, - ) + val showMemberCount = numberOfMembers != null + + fun avatarData(size: AvatarSize): AvatarData { + return AvatarData( + id = roomId.value, + name = name, + url = roomAvatarUrl, + size = size, + ) + } } } diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt index 994f914a19..5e10613557 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt @@ -19,45 +19,47 @@ package io.element.android.features.joinroom.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.invite.api.response.anAcceptDeclineInviteState -import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.core.RoomId open class JoinRoomStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aJoinRoomState( - contentState = AsyncData.Uninitialized + contentState = anUninitializedContentState() ), aJoinRoomState( - contentState = AsyncData.Success( - aContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin) - ) + contentState = anUnknownContentState() ), aJoinRoomState( - contentState = AsyncData.Success( - aContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock) - ) + contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin) ), aJoinRoomState( - contentState = AsyncData.Success( - aContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited) - ) + contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock) + ), + aJoinRoomState( + contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited) ), ) } -fun aContentState( +fun anUnknownContentState(roomId: RoomId = RoomId("@exa:matrix.org")) = ContentState.UnknownRoom(roomId) + +fun anUninitializedContentState(roomId: RoomId = RoomId("@exa:matrix.org")) = ContentState.Loading(roomId) + +fun aLoadedContentState( roomId: RoomId = RoomId("@exa:matrix.org"), name: String = "Element x android", - description: String? = "#exa:matrix.org", + alias: String? = "#exa:matrix.org", + topic: String? = "Element X is a secure, private and decentralized messenger.", numberOfMembers: Long? = null, isDirect: Boolean = false, roomAvatarUrl: String? = null, joinAuthorisationStatus: JoinAuthorisationStatus = JoinAuthorisationStatus.Unknown -) = ContentState( +) = ContentState.Loaded( roomId = roomId, name = name, - description = description, + alias = alias, + topic = topic, numberOfMembers = numberOfMembers, isDirect = isDirect, roomAvatarUrl = roomAvatarUrl, @@ -65,9 +67,7 @@ fun aContentState( ) fun aJoinRoomState( - contentState: AsyncData = AsyncData.Success( - aContentState() - ), + contentState: ContentState = aLoadedContentState(), acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(), eventSink: (JoinRoomEvents) -> Unit = {} ) = JoinRoomState( diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt index 5cef755af2..3ac19b5daf 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt @@ -16,9 +16,11 @@ package io.element.android.features.joinroom.impl +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -30,14 +32,15 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons -import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage @@ -61,11 +64,12 @@ fun JoinRoomView( ) { HeaderFooterPage( modifier = modifier, + paddingValues = PaddingValues(16.dp), topBar = { JoinRoomTopBar(onBackClicked = onBackPressed) }, content = { - JoinRoomContent(asyncContentState = state.contentState) + JoinRoomContent(contentState = state.contentState) }, footer = { JoinRoomFooter( @@ -111,22 +115,19 @@ private fun JoinRoomFooter( } JoinAuthorisationStatus.CanJoin -> { Button( - text = stringResource(CommonStrings.action_join), + text = stringResource(R.string.screen_join_room_join_action), onClick = onJoinRoom, modifier = modifier.fillMaxWidth(), size = ButtonSize.Medium, ) } JoinAuthorisationStatus.CanKnock -> { - //TODO knock - /* Button( - text = stringResource(CommonStrings.action_knock), + text = stringResource(R.string.screen_join_room_knock_action), onClick = onJoinRoom, modifier = modifier.fillMaxWidth(), size = ButtonSize.Medium, ) - */ } JoinAuthorisationStatus.Unknown -> Unit } @@ -134,63 +135,60 @@ private fun JoinRoomFooter( @Composable private fun JoinRoomContent( - asyncContentState: AsyncData, + contentState: ContentState, modifier: Modifier = Modifier, ) { - @Composable - fun ContentScaffold( - avatar: @Composable () -> Unit, - title: String, - description: String, - memberCount: @Composable (() -> Unit)? = null - ) { - avatar() - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = title, - style = ElementTheme.typography.fontHeadingMdBold, - textAlign = TextAlign.Center, - color = ElementTheme.colors.textPrimary, - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = description, - style = ElementTheme.typography.fontBodyMdRegular, - textAlign = TextAlign.Center, - color = ElementTheme.colors.textSecondary, - ) - memberCount?.invoke() - } - Column( - modifier = modifier - .fillMaxWidth() - .padding(all = 16.dp), + modifier = modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { - when (asyncContentState) { - is AsyncData.Success -> { - val contentState = asyncContentState.data + when (contentState) { + is ContentState.Loaded -> { ContentScaffold( avatar = { Avatar(contentState.avatarData(AvatarSize.RoomHeader)) }, - title = contentState.name, - description = contentState.description ?: stringResource(R.string.screen_join_room_subtitle_no_preview) - ) { - if (contentState.showMemberCount) { - JoinRoomMembersCount(memberCount = contentState.numberOfMembers ?: 0) + title = { + Title(contentState.computedTitle) + }, + subtitle = { + Subtitle(contentState.computedSubtitle) + }, + description = { + Description(contentState.topic ?: "") + }, + memberCount = { + if (contentState.showMemberCount) { + MembersCount(memberCount = contentState.numberOfMembers ?: 0) + } } - } + ) } - else -> { + is ContentState.UnknownRoom -> { ContentScaffold( avatar = { PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp) }, - title = stringResource(R.string.screen_join_room_title_no_preview), - description = stringResource(R.string.screen_join_room_subtitle_no_preview), + title = { + Title(stringResource(R.string.screen_join_room_title_no_preview)) + }, + subtitle = { + Subtitle(stringResource(R.string.screen_join_room_subtitle_no_preview)) + }, + ) + } + is ContentState.Loading -> { + ContentScaffold( + avatar = { + PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp) + }, + title = { + PlaceholderAtom(width = 200.dp, height = 22.dp) + }, + subtitle = { + PlaceholderAtom(width = 140.dp, height = 20.dp) + }, ) } } @@ -198,13 +196,72 @@ private fun JoinRoomContent( } @Composable -private fun JoinRoomMembersCount(memberCount: Long) { +private fun ContentScaffold( + avatar: @Composable () -> Unit, + title: @Composable () -> Unit, + subtitle: @Composable () -> Unit, + description: @Composable (() -> Unit)? = null, + memberCount: @Composable (() -> Unit)? = null, +) { + avatar() + Spacer(modifier = Modifier.height(16.dp)) + title() + Spacer(modifier = Modifier.height(8.dp)) + subtitle() + Spacer(modifier = Modifier.height(8.dp)) + if (memberCount != null) { + memberCount() + } + Spacer(modifier = Modifier.height(8.dp)) + if (description != null) { + description() + } + Spacer(modifier = Modifier.height(24.dp)) +} + +@Composable +private fun Title(title: String, modifier: Modifier = Modifier) { + Text( + modifier = modifier, + text = title, + style = ElementTheme.typography.fontHeadingMdBold, + textAlign = TextAlign.Center, + color = ElementTheme.colors.textPrimary, + ) +} + +@Composable +private fun Subtitle(subtitle: String, modifier: Modifier = Modifier) { + Text( + modifier = modifier, + text = subtitle, + style = ElementTheme.typography.fontBodyLgRegular, + textAlign = TextAlign.Center, + color = ElementTheme.colors.textSecondary, + ) +} + +@Composable +private fun Description(description: String, modifier: Modifier = Modifier) { + Text( + modifier = modifier, + text = description, + style = ElementTheme.typography.fontBodySmRegular, + textAlign = TextAlign.Center, + color = ElementTheme.colors.textSecondary, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) +} + +@Composable +private fun MembersCount(memberCount: Long) { Spacer(modifier = Modifier.height(8.dp)) Row( modifier = Modifier - .background(color = ElementTheme.colors.bgSubtleSecondary, shape = CircleShape) - .widthIn(min = 48.dp) - .padding(all = 2.dp), + .background(color = ElementTheme.colors.bgSubtleSecondary, shape = CircleShape) + .widthIn(min = 48.dp) + .padding(all = 2.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp) ) { @@ -230,9 +287,7 @@ private fun JoinRoomTopBar( navigationIcon = { BackButton(onClick = onBackClicked) }, - title = { - - }, + title = {}, ) } diff --git a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt index 45dc4361c5..19cc23c995 100644 --- a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt +++ b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt @@ -21,7 +21,6 @@ import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.invite.api.response.anAcceptDeclineInviteState import io.element.android.features.roomdirectory.api.RoomDescription -import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId @@ -51,10 +50,13 @@ class JoinRoomPresenterTest { val presenter = createJoinRoomPresenter() presenter.test { awaitItem().also { state -> - assertThat(state.contentState).isInstanceOf(AsyncData.Uninitialized::class.java) + assertThat(state.contentState).isEqualTo(ContentState.Loading(A_ROOM_ID)) assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Unknown) assertThat(state.acceptDeclineInviteState).isEqualTo(anAcceptDeclineInviteState()) } + awaitItem().also { state -> + assertThat(state.contentState).isEqualTo(ContentState.UnknownRoom(A_ROOM_ID)) + } } } @@ -72,11 +74,11 @@ class JoinRoomPresenterTest { presenter.test { skipItems(1) awaitItem().also { state -> - assertThat(state.contentState).isInstanceOf(AsyncData.Success::class.java) - val contentState = state.contentState.dataOrNull()!! + val contentState = state.contentState as ContentState.Loaded assertThat(contentState.roomId).isEqualTo(A_ROOM_ID) assertThat(contentState.name).isEqualTo(roomInfo.name) - assertThat(contentState.description).isEqualTo(roomInfo.topic) + assertThat(contentState.topic).isEqualTo(roomInfo.topic) + assertThat(contentState.alias).isEqualTo(roomInfo.canonicalAlias) assertThat(contentState.numberOfMembers).isEqualTo(roomInfo.activeMembersCount) assertThat(contentState.isDirect).isEqualTo(roomInfo.isDirect) assertThat(contentState.roomAvatarUrl).isEqualTo(roomInfo.avatarUrl) @@ -186,11 +188,11 @@ class JoinRoomPresenterTest { presenter.test { skipItems(1) awaitItem().also { state -> - assertThat(state.contentState).isInstanceOf(AsyncData.Success::class.java) - val contentState = state.contentState.dataOrNull()!! + val contentState = state.contentState as ContentState.Loaded assertThat(contentState.roomId).isEqualTo(A_ROOM_ID) assertThat(contentState.name).isEqualTo(roomDescription.name) - assertThat(contentState.description).isEqualTo(roomDescription.description) + assertThat(contentState.topic).isEqualTo(roomDescription.topic) + assertThat(contentState.alias).isEqualTo(roomDescription.alias) assertThat(contentState.numberOfMembers).isEqualTo(roomDescription.numberOfMembers) assertThat(contentState.isDirect).isFalse() assertThat(contentState.roomAvatarUrl).isEqualTo(roomDescription.avatarUrl) @@ -256,8 +258,9 @@ class JoinRoomPresenterTest { private fun aRoomDescription( roomId: RoomId = A_ROOM_ID, - name: String = A_ROOM_NAME, - description: String = "A room about something", + name: String? = A_ROOM_NAME, + topic: String? = "A room about something", + alias: String? = "#alias:matrix.org", avatarUrl: String? = null, joinRule: RoomDescription.JoinRule = RoomDescription.JoinRule.UNKNOWN, numberOfMembers: Long = 2L @@ -265,7 +268,8 @@ class JoinRoomPresenterTest { return RoomDescription( roomId = roomId, name = name, - description = description, + topic = topic, + alias = alias, avatarUrl = avatarUrl, joinRule = joinRule, numberOfMembers = numberOfMembers diff --git a/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDescription.kt b/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDescription.kt index a0a0e45a54..632173100a 100644 --- a/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDescription.kt +++ b/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDescription.kt @@ -17,16 +17,19 @@ package io.element.android.features.roomdirectory.api import android.os.Parcelable +import androidx.compose.runtime.Immutable import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.core.RoomId import kotlinx.parcelize.Parcelize @Parcelize +@Immutable data class RoomDescription( val roomId: RoomId, - val name: String, - val description: String, + val name: String?, + val alias: String?, + val topic: String?, val avatarUrl: String?, val joinRule: JoinRule, val numberOfMembers: Long, @@ -38,6 +41,18 @@ data class RoomDescription( UNKNOWN } + val computedName = name ?: alias ?: roomId.value + + val computedDescription: String + get() { + return when { + topic != null -> topic + name != null && alias != null -> alias + name == null && alias == null -> "" + else -> roomId.value + } + } + fun canBeJoined() = joinRule == JoinRule.PUBLIC || joinRule == JoinRule.KNOCK fun avatarData(size: AvatarSize) = AvatarData( diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt index ccfdd4c1f1..e94271cfb8 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt @@ -68,7 +68,8 @@ fun aRoomDescriptionList(): ImmutableList { RoomDescription( roomId = RoomId("!exa:matrix.org"), name = "Element X Android", - description = "Element X is a secure, private and decentralized messenger.", + topic = "Element X is a secure, private and decentralized messenger.", + alias = "#element-x-android:matrix.org", avatarUrl = null, joinRule = RoomDescription.JoinRule.PUBLIC, numberOfMembers = 2765, @@ -76,7 +77,8 @@ fun aRoomDescriptionList(): ImmutableList { RoomDescription( roomId = RoomId("!exi:matrix.org"), name = "Element X iOS", - description = "Element X is a secure, private and decentralized messenger.", + topic = "Element X is a secure, private and decentralized messenger.", + alias = "#element-x-ios:matrix.org", avatarUrl = null, joinRule = RoomDescription.JoinRule.UNKNOWN, numberOfMembers = 356, diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt index f1ea915c17..c8fe34e85e 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt @@ -292,14 +292,14 @@ private fun RoomDirectoryRoomRow( .padding(start = 16.dp) ) { Text( - text = roomDescription.name, + text = roomDescription.computedName, maxLines = 1, style = ElementTheme.typography.fontBodyLgRegular, color = ElementTheme.colors.textPrimary, overflow = TextOverflow.Ellipsis, ) Text( - text = roomDescription.description, + text = roomDescription.computedDescription, maxLines = 1, style = ElementTheme.typography.fontBodyMdRegular, color = ElementTheme.colors.textSecondary, diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescription.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescription.kt index eec9f6acf9..994f20dd16 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescription.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescription.kt @@ -17,31 +17,15 @@ package io.element.android.features.roomdirectory.impl.root.model import io.element.android.features.roomdirectory.api.RoomDescription -import io.element.android.libraries.designsystem.components.avatar.AvatarData -import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.roomdirectory.RoomDescription as MatrixRoomDescription fun MatrixRoomDescription.toFeatureModel(): RoomDescription { - fun name(): String { - return name ?: alias ?: roomId.value - } - - fun description(): String { - val topic = topic - val alias = alias - val name = name - return when { - topic != null -> topic - name != null && alias != null -> alias - name == null && alias == null -> "" - else -> roomId.value - } - } return RoomDescription( roomId = roomId, - name = name(), - description = description(), + name = name, + alias = alias, + topic = topic, avatarUrl = avatarUrl, numberOfMembers = numberOfMembers, joinRule = when (joinRule) { diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/HeaderFooterPage.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/HeaderFooterPage.kt index b2cf88b8bc..71dfecddb8 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/HeaderFooterPage.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/HeaderFooterPage.kt @@ -18,6 +18,7 @@ package io.element.android.libraries.designsystem.atomic.pages import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -43,6 +44,7 @@ import io.element.android.libraries.designsystem.theme.components.Text @Composable fun HeaderFooterPage( modifier: Modifier = Modifier, + paddingValues: PaddingValues = PaddingValues(20.dp), background: @Composable () -> Unit = {}, topBar: @Composable () -> Unit = {}, header: @Composable () -> Unit = {}, @@ -57,7 +59,7 @@ fun HeaderFooterPage( background() Column( modifier = Modifier - .padding(all = 20.dp) + .padding(paddingValues = paddingValues) .padding(padding) .consumeWindowInsets(padding) ) { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/CallbackFlow.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/CallbackFlow.kt index fbf393e587..b17a2f8cf2 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/CallbackFlow.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/CallbackFlow.kt @@ -16,6 +16,7 @@ package io.element.android.libraries.matrix.impl.util +import io.element.android.libraries.core.data.tryOrNull import kotlinx.coroutines.channels.ProducerScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.callbackFlow @@ -23,7 +24,9 @@ import org.matrix.rustcomponents.sdk.TaskHandle internal fun mxCallbackFlow(block: suspend ProducerScope.() -> TaskHandle?) = callbackFlow { - val taskHandle: TaskHandle? = block(this) + val taskHandle: TaskHandle? = tryOrNull { + block(this) + } awaitClose { taskHandle?.cancelAndDestroy() } From 8b80b2859fb6f0d71b2101d36356d74cae2d09ec Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 12 Apr 2024 15:58:15 +0200 Subject: [PATCH 16/25] Room navigation : some clean up --- .../invitelist/InviteListStateProvider.kt | 5 +- .../response/AcceptDeclineInvitePresenter.kt | 4 - .../AcceptDeclineInviteViewWrapper.kt | 1 - .../InternalAcceptDeclineInviteEvents.kt | 2 +- .../invitelist/InviteListPresenterTests.kt | 9 -- .../AcceptDeclineInvitePresenterTest.kt | 1 - .../joinroom/api/JoinRoomEntryPoint.kt | 2 - features/joinroom/impl/build.gradle.kts | 1 - .../impl/DefaultJoinRoomEntryPoint.kt | 1 - .../features/joinroom/impl/JoinRoomEvents.kt | 2 +- .../features/joinroom/impl/JoinRoomNode.kt | 1 - .../joinroom/impl/JoinRoomPresenter.kt | 1 - .../features/joinroom/impl/JoinRoomState.kt | 2 +- .../joinroom/impl/JoinRoomStateProvider.kt | 1 - .../features/joinroom/impl/JoinRoomView.kt | 142 +++++++++--------- .../joinroom/impl/JoinRoomPresenterTest.kt | 2 - .../impl/LeaveRoomPresenterImplTest.kt | 4 +- .../actions/AndroidLocationActionsTest.kt | 2 - .../roomdirectory/api/RoomDescription.kt | 7 +- .../impl/root/RoomDirectoryNode.kt | 1 - .../impl/root/RoomDirectoryView.kt | 42 +++--- .../impl/root/model/RoomDescription.kt | 1 - .../impl/root/RoomDirectoryViewTest.kt | 6 +- .../atomic/pages/HeaderFooterPage.kt | 1 + .../libraries/matrix/api/MatrixClient.kt | 2 - .../matrix/api/roomlist/RoomListService.kt | 3 - .../libraries/matrix/test/FakeMatrixClient.kt | 1 - .../android/libraries/testtags/TestTags.kt | 2 +- 28 files changed, 107 insertions(+), 142 deletions(-) diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListStateProvider.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListStateProvider.kt index e796896745..9814b1b20d 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListStateProvider.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListStateProvider.kt @@ -18,17 +18,16 @@ package io.element.android.features.invite.impl.invitelist import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.invite.api.response.AcceptDeclineInviteState -import io.element.android.features.invite.impl.model.InviteListInviteSummary -import io.element.android.features.invite.impl.model.InviteSender import io.element.android.features.invite.api.response.AcceptDeclineInviteStateProvider import io.element.android.features.invite.api.response.anAcceptDeclineInviteState +import io.element.android.features.invite.impl.model.InviteListInviteSummary +import io.element.android.features.invite.impl.model.InviteSender import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf open class InviteListStateProvider : PreviewParameterProvider { - private val acceptDeclineInviteStateProvider = AcceptDeclineInviteStateProvider() override val values: Sequence diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenter.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenter.kt index b452a53bd6..5f7e80b7b1 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenter.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenter.kt @@ -23,7 +23,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import com.squareup.anvil.annotations.ContributesBinding import im.vector.app.features.analytics.plan.JoinedRoom import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents import io.element.android.features.invite.api.response.AcceptDeclineInviteState @@ -32,7 +31,6 @@ import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.architecture.runUpdatingState -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.push.api.notifications.NotificationDrawerManager @@ -49,10 +47,8 @@ class AcceptDeclineInvitePresenter @Inject constructor( private val analyticsService: AnalyticsService, private val notificationDrawerManager: NotificationDrawerManager, ) : Presenter { - @Composable override fun present(): AcceptDeclineInviteState { - val localCoroutineScope = rememberCoroutineScope() val acceptedAction: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } val declinedAction: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteViewWrapper.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteViewWrapper.kt index 575b401dc0..a86b220364 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteViewWrapper.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteViewWrapper.kt @@ -27,7 +27,6 @@ import javax.inject.Inject @ContributesBinding(SessionScope::class) class AcceptDeclineInviteViewWrapper @Inject constructor() : AcceptDeclineInviteView { - @Composable override fun Render( state: AcceptDeclineInviteState, diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/InternalAcceptDeclineInviteEvents.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/InternalAcceptDeclineInviteEvents.kt index e15cc9cff1..1ccf2ee34f 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/InternalAcceptDeclineInviteEvents.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/InternalAcceptDeclineInviteEvents.kt @@ -18,7 +18,7 @@ package io.element.android.features.invite.impl.response import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents -sealed interface InternalAcceptDeclineInviteEvents: AcceptDeclineInviteEvents { +sealed interface InternalAcceptDeclineInviteEvents : AcceptDeclineInviteEvents { data object ConfirmDeclineInvite : InternalAcceptDeclineInviteEvents data object CancelDeclineInvite : InternalAcceptDeclineInviteEvents data object DismissAcceptError : InternalAcceptDeclineInviteEvents diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/invitelist/InviteListPresenterTests.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/invitelist/InviteListPresenterTests.kt index 17dcf7d11b..c984e9c5fb 100644 --- a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/invitelist/InviteListPresenterTests.kt +++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/invitelist/InviteListPresenterTests.kt @@ -24,11 +24,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.invite.api.response.anAcceptDeclineInviteState -import io.element.android.features.invite.impl.invitelist.InviteListEvents -import io.element.android.features.invite.impl.invitelist.InviteListPresenter -import io.element.android.features.invite.impl.invitelist.InviteListState import io.element.android.features.invite.test.FakeSeenInvitesStore -import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize @@ -43,14 +39,9 @@ import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_USER_ID 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.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.matrix.test.room.aRoomSummaryDetails import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService -import io.element.android.libraries.push.api.notifications.NotificationDrawerManager -import io.element.android.libraries.push.test.notifications.FakeNotificationDrawerManager -import io.element.android.services.analytics.api.AnalyticsService -import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest import org.junit.Rule diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt index 8d37656000..dfb330da59 100644 --- a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt +++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt @@ -40,7 +40,6 @@ import org.junit.Test import java.util.Optional class AcceptDeclineInvitePresenterTest { - @get:Rule val warmUpRule = WarmUpRule() diff --git a/features/joinroom/api/src/main/kotlin/io/element/android/features/joinroom/api/JoinRoomEntryPoint.kt b/features/joinroom/api/src/main/kotlin/io/element/android/features/joinroom/api/JoinRoomEntryPoint.kt index 0047eb10a8..60f49b9d36 100644 --- a/features/joinroom/api/src/main/kotlin/io/element/android/features/joinroom/api/JoinRoomEntryPoint.kt +++ b/features/joinroom/api/src/main/kotlin/io/element/android/features/joinroom/api/JoinRoomEntryPoint.kt @@ -25,7 +25,6 @@ import io.element.android.libraries.matrix.api.core.RoomId import java.util.Optional interface JoinRoomEntryPoint : FeatureEntryPoint { - fun createNode(parentNode: Node, buildContext: BuildContext, inputs: Inputs): Node data class Inputs( @@ -33,4 +32,3 @@ interface JoinRoomEntryPoint : FeatureEntryPoint { val roomDescription: Optional, ) : NodeInputs } - diff --git a/features/joinroom/impl/build.gradle.kts b/features/joinroom/impl/build.gradle.kts index 00095d1401..b87410ec87 100644 --- a/features/joinroom/impl/build.gradle.kts +++ b/features/joinroom/impl/build.gradle.kts @@ -43,7 +43,6 @@ dependencies { implementation(projects.features.roomdirectory.api) implementation(projects.libraries.uiStrings) - testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) testImplementation(libs.molecule.runtime) diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/DefaultJoinRoomEntryPoint.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/DefaultJoinRoomEntryPoint.kt index ab2745a63f..05db8f1cb1 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/DefaultJoinRoomEntryPoint.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/DefaultJoinRoomEntryPoint.kt @@ -26,7 +26,6 @@ import javax.inject.Inject @ContributesBinding(AppScope::class) class DefaultJoinRoomEntryPoint @Inject constructor() : JoinRoomEntryPoint { - override fun createNode(parentNode: Node, buildContext: BuildContext, inputs: JoinRoomEntryPoint.Inputs): Node { return parentNode.createNode( buildContext = buildContext, diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomEvents.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomEvents.kt index e3dc73f505..999030cd50 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomEvents.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomEvents.kt @@ -17,7 +17,7 @@ package io.element.android.features.joinroom.impl sealed interface JoinRoomEvents { - data object JoinRoom: JoinRoomEvents + data object JoinRoom : JoinRoomEvents data object AcceptInvite : JoinRoomEvents data object DeclineInvite : JoinRoomEvents } diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomNode.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomNode.kt index 7763c17cad..eaa195d88d 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomNode.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomNode.kt @@ -36,7 +36,6 @@ class JoinRoomNode @AssistedInject constructor( presenterFactory: JoinRoomPresenter.Factory, private val acceptDeclineInviteView: AcceptDeclineInviteView, ) : Node(buildContext, plugins = plugins) { - private val inputs: JoinRoomEntryPoint.Inputs = inputs() private val presenter = presenterFactory.create(inputs.roomId, inputs.roomDescription) diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt index 26b661b3eb..6bcb09b4c9 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt @@ -40,7 +40,6 @@ class JoinRoomPresenter @AssistedInject constructor( private val matrixClient: MatrixClient, private val acceptDeclineInvitePresenter: Presenter, ) : Presenter { - interface Factory { fun create(roomId: RoomId, roomDescription: Optional): JoinRoomPresenter } diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt index 01647e1fdd..08591c068e 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt @@ -28,7 +28,7 @@ data class JoinRoomState( val acceptDeclineInviteState: AcceptDeclineInviteState, val eventSink: (JoinRoomEvents) -> Unit ) { - val joinAuthorisationStatus = when(contentState) { + val joinAuthorisationStatus = when (contentState) { is ContentState.Loaded -> contentState.joinAuthorisationStatus else -> JoinAuthorisationStatus.Unknown } diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt index 5e10613557..0982980204 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt @@ -75,4 +75,3 @@ fun aJoinRoomState( acceptDeclineInviteState = acceptDeclineInviteState, eventSink = eventSink ) - diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt index 3ac19b5daf..3176924c9a 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt @@ -16,7 +16,6 @@ package io.element.android.features.joinroom.impl -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -32,7 +31,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -138,59 +136,56 @@ private fun JoinRoomContent( contentState: ContentState, modifier: Modifier = Modifier, ) { - - Column( - modifier = modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - when (contentState) { - is ContentState.Loaded -> { - ContentScaffold( - avatar = { - Avatar(contentState.avatarData(AvatarSize.RoomHeader)) - }, - title = { - Title(contentState.computedTitle) - }, - subtitle = { - Subtitle(contentState.computedSubtitle) - }, - description = { - Description(contentState.topic ?: "") - }, - memberCount = { - if (contentState.showMemberCount) { - MembersCount(memberCount = contentState.numberOfMembers ?: 0) - } + when (contentState) { + is ContentState.Loaded -> { + ContentScaffold( + modifier = modifier, + avatar = { + Avatar(contentState.avatarData(AvatarSize.RoomHeader)) + }, + title = { + Title(contentState.computedTitle) + }, + subtitle = { + Subtitle(contentState.computedSubtitle) + }, + description = { + Description(contentState.topic ?: "") + }, + memberCount = { + if (contentState.showMemberCount) { + MembersCount(memberCount = contentState.numberOfMembers ?: 0) } - ) - } - is ContentState.UnknownRoom -> { - ContentScaffold( - avatar = { - PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp) - }, - title = { - Title(stringResource(R.string.screen_join_room_title_no_preview)) - }, - subtitle = { - Subtitle(stringResource(R.string.screen_join_room_subtitle_no_preview)) - }, - ) - } - is ContentState.Loading -> { - ContentScaffold( - avatar = { - PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp) - }, - title = { - PlaceholderAtom(width = 200.dp, height = 22.dp) - }, - subtitle = { - PlaceholderAtom(width = 140.dp, height = 20.dp) - }, - ) - } + } + ) + } + is ContentState.UnknownRoom -> { + ContentScaffold( + modifier = modifier, + avatar = { + PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp) + }, + title = { + Title(stringResource(R.string.screen_join_room_title_no_preview)) + }, + subtitle = { + Subtitle(stringResource(R.string.screen_join_room_subtitle_no_preview)) + }, + ) + } + is ContentState.Loading -> { + ContentScaffold( + modifier = modifier, + avatar = { + PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp) + }, + title = { + PlaceholderAtom(width = 200.dp, height = 22.dp) + }, + subtitle = { + PlaceholderAtom(width = 140.dp, height = 20.dp) + }, + ) } } } @@ -200,23 +195,29 @@ private fun ContentScaffold( avatar: @Composable () -> Unit, title: @Composable () -> Unit, subtitle: @Composable () -> Unit, + modifier: Modifier = Modifier, description: @Composable (() -> Unit)? = null, memberCount: @Composable (() -> Unit)? = null, ) { - avatar() - Spacer(modifier = Modifier.height(16.dp)) - title() - Spacer(modifier = Modifier.height(8.dp)) - subtitle() - Spacer(modifier = Modifier.height(8.dp)) - if (memberCount != null) { - memberCount() + Column( + modifier = modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + avatar() + Spacer(modifier = Modifier.height(16.dp)) + title() + Spacer(modifier = Modifier.height(8.dp)) + subtitle() + Spacer(modifier = Modifier.height(8.dp)) + if (memberCount != null) { + memberCount() + } + Spacer(modifier = Modifier.height(8.dp)) + if (description != null) { + description() + } + Spacer(modifier = Modifier.height(24.dp)) } - Spacer(modifier = Modifier.height(8.dp)) - if (description != null) { - description() - } - Spacer(modifier = Modifier.height(24.dp)) } @Composable @@ -256,12 +257,11 @@ private fun Description(description: String, modifier: Modifier = Modifier) { @Composable private fun MembersCount(memberCount: Long) { - Spacer(modifier = Modifier.height(8.dp)) Row( modifier = Modifier - .background(color = ElementTheme.colors.bgSubtleSecondary, shape = CircleShape) - .widthIn(min = 48.dp) - .padding(all = 2.dp), + .background(color = ElementTheme.colors.bgSubtleSecondary, shape = CircleShape) + .widthIn(min = 48.dp) + .padding(all = 2.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp) ) { diff --git a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt index 19cc23c995..056482cca9 100644 --- a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt +++ b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt @@ -41,7 +41,6 @@ import org.junit.Test import java.util.Optional class JoinRoomPresenterTest { - @get:Rule val warmUpRule = WarmUpRule() @@ -136,7 +135,6 @@ class JoinRoomPresenterTest { listOf(value(AcceptDeclineInviteEvents.AcceptInvite(inviteData))), listOf(value(AcceptDeclineInviteEvents.DeclineInvite(inviteData))), ) - } } } diff --git a/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplTest.kt b/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplTest.kt index 1585151614..664ca4bd28 100644 --- a/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplTest.kt +++ b/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplTest.kt @@ -164,7 +164,7 @@ class LeaveRoomPresenterImplTest { givenGetRoomResult( roomId = A_ROOM_ID, result = FakeMatrixRoom().apply { - this.leaveRoomLambda = { Result.failure(RuntimeException("Blimey!"))} + this.leaveRoomLambda = { Result.failure(RuntimeException("Blimey!")) } }, ) } @@ -210,7 +210,7 @@ class LeaveRoomPresenterImplTest { givenGetRoomResult( roomId = A_ROOM_ID, result = FakeMatrixRoom().apply { - this.leaveRoomLambda = { Result.failure(RuntimeException("Blimey!"))} + this.leaveRoomLambda = { Result.failure(RuntimeException("Blimey!")) } }, ) } diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActionsTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActionsTest.kt index 9665c886d7..6cd7cf82ce 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActionsTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActionsTest.kt @@ -18,11 +18,9 @@ package io.element.android.features.location.impl.common.actions import com.google.common.truth.Truth.assertThat import io.element.android.features.location.api.Location -import org.junit.Ignore import org.junit.Test import java.net.URLEncoder -@Ignore internal class AndroidLocationActionsTest { // We use an Android-native encoder in the actual app, switch to an equivalent JVM one for the tests private fun urlEncoder(input: String) = URLEncoder.encode(input, "US-ASCII") diff --git a/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDescription.kt b/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDescription.kt index 632173100a..a27f413e9b 100644 --- a/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDescription.kt +++ b/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDescription.kt @@ -21,6 +21,7 @@ import androidx.compose.runtime.Immutable import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.core.RoomId +import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize @Parcelize @@ -34,15 +35,16 @@ data class RoomDescription( val joinRule: JoinRule, val numberOfMembers: Long, ) : Parcelable { - enum class JoinRule { PUBLIC, KNOCK, UNKNOWN } + @IgnoredOnParcel val computedName = name ?: alias ?: roomId.value + @IgnoredOnParcel val computedDescription: String get() { return when { @@ -53,7 +55,8 @@ data class RoomDescription( } } - fun canBeJoined() = joinRule == JoinRule.PUBLIC || joinRule == JoinRule.KNOCK + @IgnoredOnParcel + val canJoinOrKnock = joinRule == JoinRule.PUBLIC || joinRule == JoinRule.KNOCK fun avatarData(size: AvatarSize) = AvatarData( id = roomId.value, diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryNode.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryNode.kt index a9e5f2cff5..32f9571d44 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryNode.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryNode.kt @@ -36,7 +36,6 @@ class RoomDirectoryNode @AssistedInject constructor( @Assisted plugins: List, private val presenter: RoomDirectoryPresenter, ) : Node(buildContext, plugins = plugins) { - private fun onResultClicked(roomDescription: RoomDescription) { plugins().forEach { it.onResultClicked(roomDescription) diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt index c8fe34e85e..d6eeb65d7c 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt @@ -91,8 +91,8 @@ fun RoomDirectoryView( onResultClicked = onResultClicked, onJoinClicked = ::joinRoom, modifier = Modifier - .padding(padding) - .consumeWindowInsets(padding) + .padding(padding) + .consumeWindowInsets(padding) ) } ) @@ -199,10 +199,10 @@ private fun RoomDirectoryRoomList( @Composable private fun LoadMoreIndicator(modifier: Modifier = Modifier) { Box( - modifier - .fillMaxWidth() - .wrapContentHeight() - .padding(24.dp), + modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(24.dp), contentAlignment = Alignment.Center, ) { CircularProgressIndicator( @@ -273,14 +273,14 @@ private fun RoomDirectoryRoomRow( ) { Row( modifier = modifier - .fillMaxWidth() - .clickable(onClick = onClick) - .padding( - top = 12.dp, - bottom = 12.dp, - start = 16.dp, - ) - .height(IntrinsicSize.Min), + .fillMaxWidth() + .clickable(onClick = onClick) + .padding( + top = 12.dp, + bottom = 12.dp, + start = 16.dp, + ) + .height(IntrinsicSize.Min), ) { Avatar( avatarData = roomDescription.avatarData(AvatarSize.RoomDirectoryItem), @@ -288,8 +288,8 @@ private fun RoomDirectoryRoomRow( ) Column( modifier = Modifier - .weight(1f) - .padding(start = 16.dp) + .weight(1f) + .padding(start = 16.dp) ) { Text( text = roomDescription.computedName, @@ -306,15 +306,15 @@ private fun RoomDirectoryRoomRow( overflow = TextOverflow.Ellipsis, ) } - if (roomDescription.canBeJoined()) { + if (roomDescription.canJoinOrKnock) { Text( text = stringResource(id = CommonStrings.action_join), color = ElementTheme.colors.textSuccessPrimary, modifier = Modifier - .align(Alignment.CenterVertically) - .clickable(onClick = onJoinClick) - .padding(start = 4.dp, end = 12.dp) - .testTag(TestTags.callToAction.value) + .align(Alignment.CenterVertically) + .clickable(onClick = onJoinClick) + .padding(start = 4.dp, end = 12.dp) + .testTag(TestTags.callToAction.value) ) } else { Spacer(modifier = Modifier.width(24.dp)) diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescription.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescription.kt index 994f20dd16..9038a1a0d6 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescription.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescription.kt @@ -20,7 +20,6 @@ import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.libraries.matrix.api.roomdirectory.RoomDescription as MatrixRoomDescription fun MatrixRoomDescription.toFeatureModel(): RoomDescription { - return RoomDescription( roomId = roomId, name = name, diff --git a/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt b/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt index 39a63d2923..ce949250f0 100644 --- a/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt +++ b/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt @@ -17,12 +17,9 @@ package io.element.android.features.roomdirectory.impl.root import androidx.activity.ComponentActivity -import androidx.compose.ui.res.stringResource import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onAllNodesWithTag -import androidx.compose.ui.test.onAllNodesWithText -import androidx.compose.ui.test.onChild import androidx.compose.ui.test.onFirst import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText @@ -33,7 +30,6 @@ import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.testtags.TestTags -import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EventsRecorder @@ -74,7 +70,7 @@ class RoomDirectoryViewTest { state = state, onResultClicked = callback, ) - rule.onNodeWithText(clickedRoom.name).performClick() + rule.onNodeWithText(clickedRoom.computedName).performClick() } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/HeaderFooterPage.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/HeaderFooterPage.kt index 71dfecddb8..3cae67cb1b 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/HeaderFooterPage.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/HeaderFooterPage.kt @@ -35,6 +35,7 @@ import io.element.android.libraries.designsystem.theme.components.Text /** * @param modifier Classical modifier. + * @param paddingValues padding values to apply to the content. * @param background optional background component. * @param topBar optional topBar. * @param header optional header. diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index 7814c05946..24a034c070 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -27,7 +27,6 @@ import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.oidc.AccountManagementAction import io.element.android.libraries.matrix.api.pusher.PushersService -import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.RoomMembershipObserver @@ -100,4 +99,3 @@ interface MatrixClient : Closeable { suspend fun trackRecentlyVisitedRoom(roomId: RoomId): Result suspend fun getRecentlyVisitedRooms(): Result> } - diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt index 860aeb7e68..95992b965e 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt @@ -79,11 +79,8 @@ interface RoomListService { * The state of the service as a flow. */ val state: StateFlow - } fun RoomList.loadedStateFlow(): Flow { return loadingState.filterIsInstance() } - - diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index 7044f44c53..7e86f29cb7 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -276,5 +276,4 @@ class FakeMatrixClient( } override fun getRoomInfoFlow(roomId: RoomId) = getRoomInfoFlowLambda(roomId) - } diff --git a/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt b/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt index fef7eb2484..3046ba3372 100644 --- a/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt +++ b/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt @@ -107,7 +107,7 @@ object TestTags { val searchTextField = TestTag("search_text_field") /** - * Generic call to action + * Generic call to action. */ val callToAction = TestTag("call_to_action") } From f7d74070ea773476bf4da9ce58dbfda2c66411f5 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 12 Apr 2024 16:45:40 +0200 Subject: [PATCH 17/25] Fix malformed room id in StateProvider.. --- .../invite/api/response/AcceptDeclineInviteStateProvider.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteStateProvider.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteStateProvider.kt index e6a20d9b80..af2c37e68f 100644 --- a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteStateProvider.kt +++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteStateProvider.kt @@ -27,13 +27,13 @@ open class AcceptDeclineInviteStateProvider : PreviewParameterProvider Date: Fri, 12 Apr 2024 14:57:03 +0000 Subject: [PATCH 18/25] Update screenshots --- ...ull_LoadingRoomNodeView-Day-2_2_null_0,NEXUS_5,1.0,en].png} | 0 ...ull_LoadingRoomNodeView-Day-2_2_null_1,NEXUS_5,1.0,en].png} | 0 ...l_LoadingRoomNodeView-Night-2_3_null_0,NEXUS_5,1.0,en].png} | 0 ...l_LoadingRoomNodeView-Night-2_3_null_1,NEXUS_5,1.0,en].png} | 0 ...w_null_InviteSummaryRow-Day-0_1_null_0,NEXUS_5,1.0,en].png} | 0 ...w_null_InviteSummaryRow-Day-0_1_null_1,NEXUS_5,1.0,en].png} | 0 ...w_null_InviteSummaryRow-Day-0_1_null_2,NEXUS_5,1.0,en].png} | 0 ...w_null_InviteSummaryRow-Day-0_1_null_3,NEXUS_5,1.0,en].png} | 0 ...w_null_InviteSummaryRow-Day-0_1_null_4,NEXUS_5,1.0,en].png} | 0 ...null_InviteSummaryRow-Night-0_2_null_0,NEXUS_5,1.0,en].png} | 0 ...null_InviteSummaryRow-Night-0_2_null_1,NEXUS_5,1.0,en].png} | 0 ...null_InviteSummaryRow-Night-0_2_null_2,NEXUS_5,1.0,en].png} | 0 ...null_InviteSummaryRow-Night-0_2_null_3,NEXUS_5,1.0,en].png} | 0 ...null_InviteSummaryRow-Night-0_2_null_4,NEXUS_5,1.0,en].png} | 0 ...iew_null_InviteListView-Day-1_2_null_0,NEXUS_5,1.0,en].png} | 0 ...iew_null_InviteListView-Day-1_2_null_1,NEXUS_5,1.0,en].png} | 0 ...View_null_InviteListView-Day-1_2_null_2,NEXUS_5,1.0,en].png | 3 +++ ...iew_null_InviteListView-Day-1_2_null_3,NEXUS_5,1.0,en].png} | 0 ...View_null_InviteListView-Day-1_2_null_4,NEXUS_5,1.0,en].png | 3 +++ ...View_null_InviteListView-Day-1_2_null_5,NEXUS_5,1.0,en].png | 3 +++ ...View_null_InviteListView-Day-1_2_null_6,NEXUS_5,1.0,en].png | 3 +++ ...w_null_InviteListView-Night-1_3_null_0,NEXUS_5,1.0,en].png} | 0 ...w_null_InviteListView-Night-1_3_null_1,NEXUS_5,1.0,en].png} | 0 ...ew_null_InviteListView-Night-1_3_null_2,NEXUS_5,1.0,en].png | 3 +++ ...w_null_InviteListView-Night-1_3_null_3,NEXUS_5,1.0,en].png} | 0 ...ew_null_InviteListView-Night-1_3_null_4,NEXUS_5,1.0,en].png | 3 +++ ...ew_null_InviteListView-Night-1_3_null_5,NEXUS_5,1.0,en].png | 3 +++ ...ew_null_InviteListView-Night-1_3_null_6,NEXUS_5,1.0,en].png | 3 +++ ...View_null_InviteListView-Day-0_1_null_3,NEXUS_5,1.0,en].png | 3 --- ...View_null_InviteListView-Day-0_1_null_4,NEXUS_5,1.0,en].png | 3 --- ...View_null_InviteListView-Day-0_1_null_5,NEXUS_5,1.0,en].png | 3 --- ...ew_null_InviteListView-Night-0_2_null_3,NEXUS_5,1.0,en].png | 3 --- ...ew_null_InviteListView-Night-0_2_null_4,NEXUS_5,1.0,en].png | 3 --- ...ew_null_InviteListView-Night-0_2_null_5,NEXUS_5,1.0,en].png | 3 --- 34 files changed, 24 insertions(+), 18 deletions(-) rename tests/uitests/src/test/snapshots/images/{ui_S_t[appnav.room_LoadingRoomNodeView_null_LoadingRoomNodeView-Day-2_2_null_0,NEXUS_5,1.0,en].png => ui_S_t[appnav.room.joined_LoadingRoomNodeView_null_LoadingRoomNodeView-Day-2_2_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[appnav.room_LoadingRoomNodeView_null_LoadingRoomNodeView-Day-2_2_null_1,NEXUS_5,1.0,en].png => ui_S_t[appnav.room.joined_LoadingRoomNodeView_null_LoadingRoomNodeView-Day-2_2_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[appnav.room_LoadingRoomNodeView_null_LoadingRoomNodeView-Night-2_3_null_0,NEXUS_5,1.0,en].png => ui_S_t[appnav.room.joined_LoadingRoomNodeView_null_LoadingRoomNodeView-Night-2_3_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[appnav.room_LoadingRoomNodeView_null_LoadingRoomNodeView-Night-2_3_null_1,NEXUS_5,1.0,en].png => ui_S_t[appnav.room.joined_LoadingRoomNodeView_null_LoadingRoomNodeView-Night-2_3_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.invitelist.impl.components_InviteSummaryRow_null_InviteSummaryRow-Day-1_2_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.invite.impl.components_InviteSummaryRow_null_InviteSummaryRow-Day-0_1_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.invitelist.impl.components_InviteSummaryRow_null_InviteSummaryRow-Day-1_2_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.invite.impl.components_InviteSummaryRow_null_InviteSummaryRow-Day-0_1_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.invitelist.impl.components_InviteSummaryRow_null_InviteSummaryRow-Day-1_2_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.invite.impl.components_InviteSummaryRow_null_InviteSummaryRow-Day-0_1_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.invitelist.impl.components_InviteSummaryRow_null_InviteSummaryRow-Day-1_2_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.invite.impl.components_InviteSummaryRow_null_InviteSummaryRow-Day-0_1_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.invitelist.impl.components_InviteSummaryRow_null_InviteSummaryRow-Day-1_2_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.invite.impl.components_InviteSummaryRow_null_InviteSummaryRow-Day-0_1_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.invitelist.impl.components_InviteSummaryRow_null_InviteSummaryRow-Night-1_3_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.invite.impl.components_InviteSummaryRow_null_InviteSummaryRow-Night-0_2_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.invitelist.impl.components_InviteSummaryRow_null_InviteSummaryRow-Night-1_3_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.invite.impl.components_InviteSummaryRow_null_InviteSummaryRow-Night-0_2_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.invitelist.impl.components_InviteSummaryRow_null_InviteSummaryRow-Night-1_3_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.invite.impl.components_InviteSummaryRow_null_InviteSummaryRow-Night-0_2_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.invitelist.impl.components_InviteSummaryRow_null_InviteSummaryRow-Night-1_3_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.invite.impl.components_InviteSummaryRow_null_InviteSummaryRow-Night-0_2_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.invitelist.impl.components_InviteSummaryRow_null_InviteSummaryRow-Night-1_3_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.invite.impl.components_InviteSummaryRow_null_InviteSummaryRow-Night-0_2_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Day-0_1_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Day-1_2_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Day-0_1_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Day-1_2_null_1,NEXUS_5,1.0,en].png} (100%) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Day-1_2_null_2,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Day-0_1_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Day-1_2_null_3,NEXUS_5,1.0,en].png} (100%) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Day-1_2_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Day-1_2_null_5,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Day-1_2_null_6,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Night-0_2_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Night-1_3_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Night-0_2_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Night-1_3_null_1,NEXUS_5,1.0,en].png} (100%) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Night-1_3_null_2,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Night-0_2_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Night-1_3_null_3,NEXUS_5,1.0,en].png} (100%) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Night-1_3_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Night-1_3_null_5,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Night-1_3_null_6,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Day-0_1_null_3,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Day-0_1_null_4,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Day-0_1_null_5,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Night-0_2_null_3,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Night-0_2_null_4,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Night-0_2_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[appnav.room_LoadingRoomNodeView_null_LoadingRoomNodeView-Day-2_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[appnav.room.joined_LoadingRoomNodeView_null_LoadingRoomNodeView-Day-2_2_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[appnav.room_LoadingRoomNodeView_null_LoadingRoomNodeView-Day-2_2_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[appnav.room.joined_LoadingRoomNodeView_null_LoadingRoomNodeView-Day-2_2_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[appnav.room_LoadingRoomNodeView_null_LoadingRoomNodeView-Day-2_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[appnav.room.joined_LoadingRoomNodeView_null_LoadingRoomNodeView-Day-2_2_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[appnav.room_LoadingRoomNodeView_null_LoadingRoomNodeView-Day-2_2_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[appnav.room.joined_LoadingRoomNodeView_null_LoadingRoomNodeView-Day-2_2_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[appnav.room_LoadingRoomNodeView_null_LoadingRoomNodeView-Night-2_3_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[appnav.room.joined_LoadingRoomNodeView_null_LoadingRoomNodeView-Night-2_3_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[appnav.room_LoadingRoomNodeView_null_LoadingRoomNodeView-Night-2_3_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[appnav.room.joined_LoadingRoomNodeView_null_LoadingRoomNodeView-Night-2_3_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[appnav.room_LoadingRoomNodeView_null_LoadingRoomNodeView-Night-2_3_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[appnav.room.joined_LoadingRoomNodeView_null_LoadingRoomNodeView-Night-2_3_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[appnav.room_LoadingRoomNodeView_null_LoadingRoomNodeView-Night-2_3_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[appnav.room.joined_LoadingRoomNodeView_null_LoadingRoomNodeView-Night-2_3_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_InviteSummaryRow_null_InviteSummaryRow-Day-1_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.components_InviteSummaryRow_null_InviteSummaryRow-Day-0_1_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_InviteSummaryRow_null_InviteSummaryRow-Day-1_2_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.components_InviteSummaryRow_null_InviteSummaryRow-Day-0_1_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_InviteSummaryRow_null_InviteSummaryRow-Day-1_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.components_InviteSummaryRow_null_InviteSummaryRow-Day-0_1_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_InviteSummaryRow_null_InviteSummaryRow-Day-1_2_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.components_InviteSummaryRow_null_InviteSummaryRow-Day-0_1_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_InviteSummaryRow_null_InviteSummaryRow-Day-1_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.components_InviteSummaryRow_null_InviteSummaryRow-Day-0_1_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_InviteSummaryRow_null_InviteSummaryRow-Day-1_2_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.components_InviteSummaryRow_null_InviteSummaryRow-Day-0_1_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_InviteSummaryRow_null_InviteSummaryRow-Day-1_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.components_InviteSummaryRow_null_InviteSummaryRow-Day-0_1_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_InviteSummaryRow_null_InviteSummaryRow-Day-1_2_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.components_InviteSummaryRow_null_InviteSummaryRow-Day-0_1_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_InviteSummaryRow_null_InviteSummaryRow-Day-1_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.components_InviteSummaryRow_null_InviteSummaryRow-Day-0_1_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_InviteSummaryRow_null_InviteSummaryRow-Day-1_2_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.components_InviteSummaryRow_null_InviteSummaryRow-Day-0_1_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_InviteSummaryRow_null_InviteSummaryRow-Night-1_3_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.components_InviteSummaryRow_null_InviteSummaryRow-Night-0_2_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_InviteSummaryRow_null_InviteSummaryRow-Night-1_3_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.components_InviteSummaryRow_null_InviteSummaryRow-Night-0_2_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_InviteSummaryRow_null_InviteSummaryRow-Night-1_3_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.components_InviteSummaryRow_null_InviteSummaryRow-Night-0_2_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_InviteSummaryRow_null_InviteSummaryRow-Night-1_3_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.components_InviteSummaryRow_null_InviteSummaryRow-Night-0_2_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_InviteSummaryRow_null_InviteSummaryRow-Night-1_3_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.components_InviteSummaryRow_null_InviteSummaryRow-Night-0_2_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_InviteSummaryRow_null_InviteSummaryRow-Night-1_3_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.components_InviteSummaryRow_null_InviteSummaryRow-Night-0_2_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_InviteSummaryRow_null_InviteSummaryRow-Night-1_3_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.components_InviteSummaryRow_null_InviteSummaryRow-Night-0_2_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_InviteSummaryRow_null_InviteSummaryRow-Night-1_3_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.components_InviteSummaryRow_null_InviteSummaryRow-Night-0_2_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_InviteSummaryRow_null_InviteSummaryRow-Night-1_3_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.components_InviteSummaryRow_null_InviteSummaryRow-Night-0_2_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_InviteSummaryRow_null_InviteSummaryRow-Night-1_3_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.components_InviteSummaryRow_null_InviteSummaryRow-Night-0_2_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Day-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Day-1_2_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Day-0_1_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Day-1_2_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Day-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Day-1_2_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Day-0_1_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Day-1_2_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Day-1_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Day-1_2_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f9e386b0a2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Day-1_2_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa90fb393dcb986721190468a83b7b9956e97e2a89d809913ea8895b0f698eba +size 54691 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Day-0_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Day-1_2_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Day-0_1_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Day-1_2_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Day-1_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Day-1_2_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..7885f98840 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Day-1_2_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb75ce275178fd84581680df4ba8cfe0cc4a5db079160a0a1c22e717521efb56 +size 54607 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Day-1_2_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Day-1_2_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..8454a07904 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Day-1_2_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3dfe58becc60142b7547331dc4c263e761930dcf0dbb11d4e1efcbfaf618cb31 +size 42625 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Day-1_2_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Day-1_2_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..8454a07904 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Day-1_2_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3dfe58becc60142b7547331dc4c263e761930dcf0dbb11d4e1efcbfaf618cb31 +size 42625 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Night-0_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Night-1_3_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Night-0_2_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Night-1_3_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Night-0_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Night-1_3_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Night-0_2_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Night-1_3_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Night-1_3_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Night-1_3_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..bc150d35ac --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Night-1_3_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb2e96e4737a2616676439c4d6ce85279c7ea9f09129058818502658841e8f10 +size 51858 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Night-0_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Night-1_3_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Night-0_2_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Night-1_3_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Night-1_3_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Night-1_3_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..958a3d1254 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Night-1_3_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c5fd489d8088e8afd11738f52417cff00ff720ea493ac9b8d350b10c1b1effd0 +size 49458 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Night-1_3_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Night-1_3_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a70274e954 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Night-1_3_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76f3386f41e0b2daad020c08e577e9f7266c8f80e2cb020c6f6d4f51126d182b +size 37982 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Night-1_3_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Night-1_3_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a70274e954 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invite.impl.invitelist_InviteListView_null_InviteListView-Night-1_3_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76f3386f41e0b2daad020c08e577e9f7266c8f80e2cb020c6f6d4f51126d182b +size 37982 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Day-0_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Day-0_1_null_3,NEXUS_5,1.0,en].png deleted file mode 100644 index 34611720e6..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Day-0_1_null_3,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fa1bed037eb1bac4ff3e5ce5bf477d646b19c40744e9529f05c331083985be4d -size 54707 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Day-0_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Day-0_1_null_4,NEXUS_5,1.0,en].png deleted file mode 100644 index 506f9344a5..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Day-0_1_null_4,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c42f6c5a1378d921530457716c59dc0b8685d0ce974224a3805f31d2c83a5f58 -size 44419 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Day-0_1_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Day-0_1_null_5,NEXUS_5,1.0,en].png deleted file mode 100644 index 506f9344a5..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Day-0_1_null_5,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c42f6c5a1378d921530457716c59dc0b8685d0ce974224a3805f31d2c83a5f58 -size 44419 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Night-0_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Night-0_2_null_3,NEXUS_5,1.0,en].png deleted file mode 100644 index 89d6f7b185..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Night-0_2_null_3,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:323e331e3be97ce4c20b21d4f72ed4d9528b2b3520201289b5fff4c8c2a95a6e -size 49637 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Night-0_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Night-0_2_null_4,NEXUS_5,1.0,en].png deleted file mode 100644 index fdb5d56575..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Night-0_2_null_4,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b7cdaa24081b0169e704e9e7084cdc263da54e79429dcfde87ea9a324fa1607c -size 39715 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Night-0_2_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Night-0_2_null_5,NEXUS_5,1.0,en].png deleted file mode 100644 index fdb5d56575..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Night-0_2_null_5,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b7cdaa24081b0169e704e9e7084cdc263da54e79429dcfde87ea9a324fa1607c -size 39715 From 020e51d0f993d2eb4278689d997e6ebcd8b976bd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 12 Apr 2024 18:16:17 +0200 Subject: [PATCH 19/25] Use `ContentState.Loading` instead of `ContentState.UnknownRoom`. --- .../element/android/features/joinroom/impl/JoinRoomPresenter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt index 6bcb09b4c9..5b3225575f 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt @@ -56,7 +56,7 @@ class JoinRoomPresenter @AssistedInject constructor( roomDescription.get().toContentState() } else -> { - ContentState.UnknownRoom(roomId) + ContentState.Loading(roomId) } } } From 25f4dbc934a9f45e4482b0a486dc3941a0d260c6 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 12 Apr 2024 18:45:34 +0200 Subject: [PATCH 20/25] Fix preview of JoinRoomView --- .../features/joinroom/impl/JoinRoomStateProvider.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt index 0982980204..82b81d8e7b 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt @@ -25,7 +25,7 @@ open class JoinRoomStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aJoinRoomState( - contentState = anUninitializedContentState() + contentState = aLoadingContentState() ), aJoinRoomState( contentState = anUnknownContentState() @@ -42,13 +42,13 @@ open class JoinRoomStateProvider : PreviewParameterProvider { ) } -fun anUnknownContentState(roomId: RoomId = RoomId("@exa:matrix.org")) = ContentState.UnknownRoom(roomId) +fun anUnknownContentState(roomId: RoomId = A_ROOM_ID) = ContentState.UnknownRoom(roomId) -fun anUninitializedContentState(roomId: RoomId = RoomId("@exa:matrix.org")) = ContentState.Loading(roomId) +fun aLoadingContentState(roomId: RoomId = A_ROOM_ID) = ContentState.Loading(roomId) fun aLoadedContentState( - roomId: RoomId = RoomId("@exa:matrix.org"), - name: String = "Element x android", + roomId: RoomId = A_ROOM_ID, + name: String = "Element X android", alias: String? = "#exa:matrix.org", topic: String? = "Element X is a secure, private and decentralized messenger.", numberOfMembers: Long? = null, @@ -75,3 +75,5 @@ fun aJoinRoomState( acceptDeclineInviteState = acceptDeclineInviteState, eventSink = eventSink ) + +private val A_ROOM_ID = RoomId("!exa:matrix.org") From fa54e20c3fc679e4d555f882b2ccbf5e3db5b7ac Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 12 Apr 2024 18:45:43 +0200 Subject: [PATCH 21/25] Format --- .../element/android/features/joinroom/impl/JoinRoomView.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt index 3176924c9a..31f065c0e5 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt @@ -259,9 +259,9 @@ private fun Description(description: String, modifier: Modifier = Modifier) { private fun MembersCount(memberCount: Long) { Row( modifier = Modifier - .background(color = ElementTheme.colors.bgSubtleSecondary, shape = CircleShape) - .widthIn(min = 48.dp) - .padding(all = 2.dp), + .background(color = ElementTheme.colors.bgSubtleSecondary, shape = CircleShape) + .widthIn(min = 48.dp) + .padding(all = 2.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp) ) { From bc2a8e4e1c83289282bed06dfaebba22b512d0dd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 15 Apr 2024 08:58:06 +0200 Subject: [PATCH 22/25] Cleanup --- features/joinroom/impl/build.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/features/joinroom/impl/build.gradle.kts b/features/joinroom/impl/build.gradle.kts index b87410ec87..188feb662b 100644 --- a/features/joinroom/impl/build.gradle.kts +++ b/features/joinroom/impl/build.gradle.kts @@ -14,7 +14,6 @@ * limitations under the License. */ -@Suppress("DSL_SCOPE_VIOLATION") plugins { id("io.element.android-compose-library") alias(libs.plugins.anvil) From 822990af095484a0dfd0f419b0afac725a1a4b16 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 15 Apr 2024 09:01:03 +0200 Subject: [PATCH 23/25] Fix test: ContentState.UnknownRoom case will be handled later. --- .../android/features/joinroom/impl/JoinRoomPresenterTest.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt index 056482cca9..e61dd18aad 100644 --- a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt +++ b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt @@ -53,9 +53,6 @@ class JoinRoomPresenterTest { assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Unknown) assertThat(state.acceptDeclineInviteState).isEqualTo(anAcceptDeclineInviteState()) } - awaitItem().also { state -> - assertThat(state.contentState).isEqualTo(ContentState.UnknownRoom(A_ROOM_ID)) - } } } From f9df0c34a9dbd7be1cb222e0ff30e0edb9f03a77 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 15 Apr 2024 10:44:45 +0200 Subject: [PATCH 24/25] Attempt to fix preview of JoinRoomView --- features/joinroom/impl/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/features/joinroom/impl/build.gradle.kts b/features/joinroom/impl/build.gradle.kts index 188feb662b..cfdddcfee8 100644 --- a/features/joinroom/impl/build.gradle.kts +++ b/features/joinroom/impl/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { api(projects.features.joinroom.api) implementation(projects.libraries.core) implementation(projects.libraries.architecture) + implementation(projects.libraries.androidutils) implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixui) implementation(projects.libraries.designsystem) From bb75d338070dc6a0f8029cd4eadff27745641189 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 15 Apr 2024 10:47:28 +0200 Subject: [PATCH 25/25] Attempt to fix preview of RoomDirectoryView --- features/roomdirectory/impl/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/features/roomdirectory/impl/build.gradle.kts b/features/roomdirectory/impl/build.gradle.kts index 85bb195da3..49638ece40 100644 --- a/features/roomdirectory/impl/build.gradle.kts +++ b/features/roomdirectory/impl/build.gradle.kts @@ -42,6 +42,7 @@ dependencies { implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixui) + implementation(projects.libraries.androidutils) implementation(projects.libraries.designsystem) implementation(projects.libraries.uiStrings) implementation(projects.libraries.testtags)