Merge pull request #2695 from element-hq/feature/fga/room_navigation
Room navigation
This commit is contained in:
commit
1dcdc713a4
133 changed files with 2824 additions and 934 deletions
|
|
@ -42,18 +42,20 @@ import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.appnav.loggedin.LoggedInNode
|
import io.element.android.appnav.loggedin.LoggedInNode
|
||||||
import io.element.android.appnav.room.RoomFlowNode
|
import io.element.android.appnav.room.RoomFlowNode
|
||||||
import io.element.android.appnav.room.RoomLoadedFlowNode
|
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.createroom.api.CreateRoomEntryPoint
|
||||||
import io.element.android.features.ftue.api.FtueEntryPoint
|
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.FtueService
|
||||||
import io.element.android.features.ftue.api.state.FtueState
|
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.LockScreenEntryPoint
|
||||||
import io.element.android.features.lockscreen.api.LockScreenLockState
|
import io.element.android.features.lockscreen.api.LockScreenLockState
|
||||||
import io.element.android.features.lockscreen.api.LockScreenService
|
import io.element.android.features.lockscreen.api.LockScreenService
|
||||||
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||||
import io.element.android.features.preferences.api.PreferencesEntryPoint
|
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.roomdirectory.api.RoomDirectoryEntryPoint
|
||||||
import io.element.android.features.roomlist.api.RoomListEntryPoint
|
import io.element.android.features.roomlist.api.RoomListEntryPoint
|
||||||
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
|
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
|
||||||
|
|
@ -82,6 +84,7 @@ import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.util.Optional
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
class LoggedInFlowNode @AssistedInject constructor(
|
class LoggedInFlowNode @AssistedInject constructor(
|
||||||
|
|
@ -213,7 +216,8 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Room(
|
data class Room(
|
||||||
val roomId: RoomId,
|
val roomId: RoomId,
|
||||||
val initialElement: RoomLoadedFlowNode.NavTarget = RoomLoadedFlowNode.NavTarget.Messages
|
val roomDescription: RoomDescription? = null,
|
||||||
|
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages
|
||||||
) : NavTarget
|
) : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
|
@ -273,7 +277,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRoomSettingsClicked(roomId: RoomId) {
|
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() {
|
override fun onReportBugClicked() {
|
||||||
|
|
@ -290,7 +294,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
is NavTarget.Room -> {
|
is NavTarget.Room -> {
|
||||||
val callback = object : RoomLoadedFlowNode.Callback {
|
val callback = object : JoinedRoomLoadedFlowNode.Callback {
|
||||||
override fun onOpenRoom(roomId: RoomId) {
|
override fun onOpenRoom(roomId: RoomId) {
|
||||||
backstack.push(NavTarget.Room(roomId))
|
backstack.push(NavTarget.Room(roomId))
|
||||||
}
|
}
|
||||||
|
|
@ -303,7 +307,11 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
backstack.push(NavTarget.Settings(PreferencesEntryPoint.InitialTarget.NotificationSettings))
|
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<RoomFlowNode>(buildContext, plugins = listOf(inputs, callback))
|
createNode<RoomFlowNode>(buildContext, plugins = listOf(inputs, callback))
|
||||||
}
|
}
|
||||||
is NavTarget.Settings -> {
|
is NavTarget.Settings -> {
|
||||||
|
|
@ -317,7 +325,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenRoomNotificationSettings(roomId: RoomId) {
|
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)
|
val inputs = PreferencesEntryPoint.Params(navTarget.initialElement)
|
||||||
|
|
@ -349,6 +357,10 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
backstack.pop()
|
backstack.pop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onInviteClicked(roomId: RoomId) {
|
||||||
|
backstack.push(NavTarget.Room(roomId))
|
||||||
|
}
|
||||||
|
|
||||||
override fun onInviteAccepted(roomId: RoomId) {
|
override fun onInviteAccepted(roomId: RoomId) {
|
||||||
backstack.push(NavTarget.Room(roomId))
|
backstack.push(NavTarget.Room(roomId))
|
||||||
}
|
}
|
||||||
|
|
@ -370,8 +382,12 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
NavTarget.RoomDirectorySearch -> {
|
NavTarget.RoomDirectorySearch -> {
|
||||||
roomDirectoryEntryPoint.nodeBuilder(this, buildContext)
|
roomDirectoryEntryPoint.nodeBuilder(this, buildContext)
|
||||||
.callback(object : RoomDirectoryEntryPoint.Callback {
|
.callback(object : RoomDirectoryEntryPoint.Callback {
|
||||||
override fun onOpenRoom(roomId: RoomId) {
|
override fun onRoomJoined(roomId: RoomId) {
|
||||||
coroutineScope.launch { attachRoom(roomId) }
|
backstack.push(NavTarget.Room(roomId))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResultClicked(roomDescription: RoomDescription) {
|
||||||
|
backstack.push(NavTarget.Room(roomDescription.roomId, roomDescription))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.build()
|
.build()
|
||||||
|
|
|
||||||
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -14,15 +14,13 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
|
|
||||||
package io.element.android.appnav.room
|
package io.element.android.appnav.room
|
||||||
|
|
||||||
import android.os.Parcelable
|
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.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
|
@ -36,102 +34,109 @@ import com.bumble.appyx.navmodel.backstack.operation.newRoot
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
import io.element.android.appnav.room.joined.JoinedRoomFlowNode
|
||||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
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.BackstackView
|
||||||
import io.element.android.libraries.architecture.BaseFlowNode
|
import io.element.android.libraries.architecture.BaseFlowNode
|
||||||
import io.element.android.libraries.architecture.NodeInputs
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.architecture.inputs
|
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.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.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 kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.Optional
|
||||||
|
import kotlin.jvm.optionals.getOrNull
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
class RoomFlowNode @AssistedInject constructor(
|
class RoomFlowNode @AssistedInject constructor(
|
||||||
@Assisted val buildContext: BuildContext,
|
@Assisted val buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
loadingRoomStateFlowFactory: LoadingRoomStateFlowFactory,
|
private val client: MatrixClient,
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val roomMembershipObserver: RoomMembershipObserver,
|
||||||
) :
|
private val joinRoomEntryPoint: JoinRoomEntryPoint,
|
||||||
BaseFlowNode<RoomFlowNode.NavTarget>(
|
) : BaseFlowNode<RoomFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = NavTarget.Loading,
|
initialElement = NavTarget.Loading,
|
||||||
savedStateMap = buildContext.savedStateMap,
|
savedStateMap = buildContext.savedStateMap,
|
||||||
),
|
),
|
||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
plugins = plugins
|
plugins = plugins
|
||||||
) {
|
) {
|
||||||
data class Inputs(
|
data class Inputs(
|
||||||
val roomId: RoomId,
|
val roomId: RoomId,
|
||||||
val initialElement: RoomLoadedFlowNode.NavTarget = RoomLoadedFlowNode.NavTarget.Messages,
|
val roomDescription: Optional<RoomDescription>,
|
||||||
|
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages,
|
||||||
) : NodeInputs
|
) : NodeInputs
|
||||||
|
|
||||||
private val inputs: Inputs = inputs()
|
private val inputs: Inputs = inputs()
|
||||||
private val loadingRoomStateStateFlow = loadingRoomStateFlowFactory.create(lifecycleScope, inputs.roomId)
|
|
||||||
|
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object Loading : NavTarget
|
data object Loading : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object Loaded : NavTarget
|
data object JoinRoom : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data object JoinedRoom : NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
super.onBuilt()
|
super.onBuilt()
|
||||||
loadingRoomStateStateFlow
|
client.getRoomInfoFlow(
|
||||||
.map {
|
inputs.roomId
|
||||||
it is LoadingRoomState.Loaded
|
).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)
|
||||||
}
|
}
|
||||||
.distinctUntilChanged()
|
}
|
||||||
.onEach { isLoaded ->
|
.launchIn(lifecycleScope)
|
||||||
if (isLoaded) {
|
|
||||||
backstack.newRoot(NavTarget.Loaded)
|
// When leaving the room from this session only, navigate up.
|
||||||
} else {
|
roomMembershipObserver.updates
|
||||||
backstack.newRoot(NavTarget.Loading)
|
.filter { update -> update.roomId == inputs.roomId && !update.isUserInRoom }
|
||||||
}
|
.onEach {
|
||||||
|
navigateUp()
|
||||||
}
|
}
|
||||||
.launchIn(lifecycleScope)
|
.launchIn(lifecycleScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
return when (navTarget) {
|
return when (navTarget) {
|
||||||
NavTarget.Loaded -> {
|
NavTarget.Loading -> loadingNode(buildContext)
|
||||||
val roomFlowNodeCallback = plugins<RoomLoadedFlowNode.Callback>()
|
NavTarget.JoinRoom -> {
|
||||||
val awaitRoomState = loadingRoomStateStateFlow.value
|
val inputs = JoinRoomEntryPoint.Inputs(inputs.roomId, roomDescription = inputs.roomDescription)
|
||||||
if (awaitRoomState is LoadingRoomState.Loaded) {
|
joinRoomEntryPoint.createNode(this, buildContext, inputs)
|
||||||
val inputs = RoomLoadedFlowNode.Inputs(awaitRoomState.room, initialElement = inputs.initialElement)
|
|
||||||
createNode<RoomLoadedFlowNode>(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback)
|
|
||||||
} else {
|
|
||||||
loadingNode(buildContext, this::navigateUp)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
NavTarget.Loading -> {
|
NavTarget.JoinedRoom -> {
|
||||||
loadingNode(buildContext, this::navigateUp)
|
val roomFlowNodeCallback = plugins<JoinedRoomLoadedFlowNode.Callback>()
|
||||||
|
val inputs = JoinedRoomFlowNode.Inputs(inputs.roomId, initialElement = inputs.initialElement)
|
||||||
|
createNode<JoinedRoomFlowNode>(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadingNode(buildContext: BuildContext, onBackClicked: () -> Unit) = node(buildContext) { modifier ->
|
private fun loadingNode(buildContext: BuildContext) = node(buildContext) {
|
||||||
val loadingRoomState by loadingRoomStateStateFlow.collectAsState()
|
Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
val networkStatus by networkMonitor.connectivity.collectAsState()
|
CircularProgressIndicator()
|
||||||
LoadingRoomNodeView(
|
}
|
||||||
state = loadingRoomState,
|
|
||||||
hasNetworkConnection = networkStatus == NetworkStatus.Online,
|
|
||||||
modifier = modifier,
|
|
||||||
onBackClicked = onBackClicked
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
BackstackView(
|
BackstackView(transitionHandler = JumpToEndTransitionHandler())
|
||||||
transitionHandler = JumpToEndTransitionHandler(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
|
|
@ -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<Plugin>,
|
||||||
|
loadingRoomStateFlowFactory: LoadingRoomStateFlowFactory,
|
||||||
|
private val networkMonitor: NetworkMonitor,
|
||||||
|
) :
|
||||||
|
BaseFlowNode<JoinedRoomFlowNode.NavTarget>(
|
||||||
|
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<JoinedRoomLoadedFlowNode.Callback>()
|
||||||
|
val awaitRoomState = loadingRoomStateStateFlow.value
|
||||||
|
if (awaitRoomState is LoadingRoomState.Loaded) {
|
||||||
|
val inputs = JoinedRoomLoadedFlowNode.Inputs(awaitRoomState.room, initialElement = inputs.initialElement)
|
||||||
|
createNode<JoinedRoomLoadedFlowNode>(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(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.appnav.room
|
package io.element.android.appnav.room.joined
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -32,6 +32,7 @@ import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.appnav.di.RoomComponentFactory
|
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.messages.api.MessagesEntryPoint
|
||||||
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
||||||
import io.element.android.libraries.architecture.BackstackView
|
import io.element.android.libraries.architecture.BackstackView
|
||||||
|
|
@ -44,18 +45,14 @@ import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
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.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
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 io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
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.coroutines.launch
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
class RoomLoadedFlowNode @AssistedInject constructor(
|
class JoinedRoomLoadedFlowNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
private val messagesEntryPoint: MessagesEntryPoint,
|
private val messagesEntryPoint: MessagesEntryPoint,
|
||||||
|
|
@ -64,10 +61,13 @@ class RoomLoadedFlowNode @AssistedInject constructor(
|
||||||
private val appCoroutineScope: CoroutineScope,
|
private val appCoroutineScope: CoroutineScope,
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
roomComponentFactory: RoomComponentFactory,
|
roomComponentFactory: RoomComponentFactory,
|
||||||
roomMembershipObserver: RoomMembershipObserver,
|
) : BaseFlowNode<JoinedRoomLoadedFlowNode.NavTarget>(
|
||||||
) : BaseFlowNode<RoomLoadedFlowNode.NavTarget>(
|
|
||||||
backstack = BackStack(
|
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,
|
savedStateMap = buildContext.savedStateMap,
|
||||||
),
|
),
|
||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
|
|
@ -81,7 +81,7 @@ class RoomLoadedFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
data class Inputs(
|
data class Inputs(
|
||||||
val room: MatrixRoom,
|
val room: MatrixRoom,
|
||||||
val initialElement: NavTarget = NavTarget.Messages,
|
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages,
|
||||||
) : NodeInputs
|
) : NodeInputs
|
||||||
|
|
||||||
private val inputs: Inputs = inputs()
|
private val inputs: Inputs = inputs()
|
||||||
|
|
@ -111,13 +111,6 @@ class RoomLoadedFlowNode @AssistedInject constructor(
|
||||||
appNavigationStateService.onLeavingRoom(id)
|
appNavigationStateService.onLeavingRoom(id)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
roomMembershipObserver.updates
|
|
||||||
.filter { update -> update.roomId == inputs.room.roomId && !update.isUserInRoom }
|
|
||||||
.onEach {
|
|
||||||
navigateUp()
|
|
||||||
}
|
|
||||||
.launchIn(lifecycleScope)
|
|
||||||
inputs<Inputs>()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun trackVisitedRoom() = lifecycleScope.launch {
|
private fun trackVisitedRoom() = lifecycleScope.launch {
|
||||||
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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 androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
|
@ -27,12 +27,11 @@ import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
|
||||||
import com.bumble.appyx.testing.unit.common.helper.parentNodeTestHelper
|
import com.bumble.appyx.testing.unit.common.helper.parentNodeTestHelper
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.appnav.di.RoomComponentFactory
|
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.messages.api.MessagesEntryPoint
|
||||||
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
||||||
import io.element.android.libraries.architecture.childNode
|
import io.element.android.libraries.architecture.childNode
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
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.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
|
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
|
||||||
|
|
@ -41,7 +40,7 @@ import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class RoomFlowNodeTest {
|
class JoinRoomLoadedFlowNodeTest {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||||
|
|
||||||
|
|
@ -88,18 +87,17 @@ class RoomFlowNodeTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun aRoomFlowNode(
|
private fun createJoinedRoomLoadedFlowNode(
|
||||||
plugins: List<Plugin>,
|
plugins: List<Plugin>,
|
||||||
messagesEntryPoint: MessagesEntryPoint = FakeMessagesEntryPoint(),
|
messagesEntryPoint: MessagesEntryPoint = FakeMessagesEntryPoint(),
|
||||||
roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(),
|
roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(),
|
||||||
coroutineScope: CoroutineScope,
|
coroutineScope: CoroutineScope,
|
||||||
) = RoomLoadedFlowNode(
|
) = JoinedRoomLoadedFlowNode(
|
||||||
buildContext = BuildContext.root(savedStateMap = null),
|
buildContext = BuildContext.root(savedStateMap = null),
|
||||||
plugins = plugins,
|
plugins = plugins,
|
||||||
messagesEntryPoint = messagesEntryPoint,
|
messagesEntryPoint = messagesEntryPoint,
|
||||||
roomDetailsEntryPoint = roomDetailsEntryPoint,
|
roomDetailsEntryPoint = roomDetailsEntryPoint,
|
||||||
appNavigationStateService = FakeAppNavigationStateService(),
|
appNavigationStateService = FakeAppNavigationStateService(),
|
||||||
roomMembershipObserver = RoomMembershipObserver(),
|
|
||||||
appCoroutineScope = coroutineScope,
|
appCoroutineScope = coroutineScope,
|
||||||
roomComponentFactory = FakeRoomComponentFactory(),
|
roomComponentFactory = FakeRoomComponentFactory(),
|
||||||
matrixClient = FakeMatrixClient(),
|
matrixClient = FakeMatrixClient(),
|
||||||
|
|
@ -110,8 +108,8 @@ class RoomFlowNodeTest {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
val room = FakeMatrixRoom()
|
val room = FakeMatrixRoom()
|
||||||
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
||||||
val inputs = RoomLoadedFlowNode.Inputs(room)
|
val inputs = JoinedRoomLoadedFlowNode.Inputs(room)
|
||||||
val roomFlowNode = aRoomFlowNode(
|
val roomFlowNode = createJoinedRoomLoadedFlowNode(
|
||||||
plugins = listOf(inputs),
|
plugins = listOf(inputs),
|
||||||
messagesEntryPoint = fakeMessagesEntryPoint,
|
messagesEntryPoint = fakeMessagesEntryPoint,
|
||||||
coroutineScope = this
|
coroutineScope = this
|
||||||
|
|
@ -120,9 +118,9 @@ class RoomFlowNodeTest {
|
||||||
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
|
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
assertThat(roomFlowNode.backstack.activeElement).isEqualTo(RoomLoadedFlowNode.NavTarget.Messages)
|
assertThat(roomFlowNode.backstack.activeElement).isEqualTo(JoinedRoomLoadedFlowNode.NavTarget.Messages)
|
||||||
roomFlowNodeTestHelper.assertChildHasLifecycle(RoomLoadedFlowNode.NavTarget.Messages, Lifecycle.State.CREATED)
|
roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.Messages, Lifecycle.State.CREATED)
|
||||||
val messagesNode = roomFlowNode.childNode(RoomLoadedFlowNode.NavTarget.Messages)!!
|
val messagesNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.Messages)!!
|
||||||
assertThat(messagesNode.id).isEqualTo(fakeMessagesEntryPoint.nodeId)
|
assertThat(messagesNode.id).isEqualTo(fakeMessagesEntryPoint.nodeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -132,8 +130,8 @@ class RoomFlowNodeTest {
|
||||||
val room = FakeMatrixRoom()
|
val room = FakeMatrixRoom()
|
||||||
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
||||||
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
|
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
|
||||||
val inputs = RoomLoadedFlowNode.Inputs(room)
|
val inputs = JoinedRoomLoadedFlowNode.Inputs(room)
|
||||||
val roomFlowNode = aRoomFlowNode(
|
val roomFlowNode = createJoinedRoomLoadedFlowNode(
|
||||||
plugins = listOf(inputs),
|
plugins = listOf(inputs),
|
||||||
messagesEntryPoint = fakeMessagesEntryPoint,
|
messagesEntryPoint = fakeMessagesEntryPoint,
|
||||||
roomDetailsEntryPoint = fakeRoomDetailsEntryPoint,
|
roomDetailsEntryPoint = fakeRoomDetailsEntryPoint,
|
||||||
|
|
@ -143,8 +141,8 @@ class RoomFlowNodeTest {
|
||||||
// WHEN
|
// WHEN
|
||||||
fakeMessagesEntryPoint.callback?.onRoomDetailsClicked()
|
fakeMessagesEntryPoint.callback?.onRoomDetailsClicked()
|
||||||
// THEN
|
// THEN
|
||||||
roomFlowNodeTestHelper.assertChildHasLifecycle(RoomLoadedFlowNode.NavTarget.RoomDetails, Lifecycle.State.CREATED)
|
roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.RoomDetails, Lifecycle.State.CREATED)
|
||||||
val roomDetailsNode = roomFlowNode.childNode(RoomLoadedFlowNode.NavTarget.RoomDetails)!!
|
val roomDetailsNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.RoomDetails)!!
|
||||||
assertThat(roomDetailsNode.id).isEqualTo(fakeRoomDetailsEntryPoint.nodeId)
|
assertThat(roomDetailsNode.id).isEqualTo(fakeRoomDetailsEntryPoint.nodeId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -18,6 +18,8 @@ package io.element.android.appnav.room
|
||||||
|
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.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.api.roomlist.RoomList
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,11 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("io.element.android-library")
|
id("io.element.android-compose-library")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.features.invitelist.api"
|
namespace = "io.element.android.features.invite.api"
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
|
|
@ -32,7 +32,7 @@ interface InviteListEntryPoint : FeatureEntryPoint {
|
||||||
|
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun onBackClicked()
|
fun onBackClicked()
|
||||||
|
fun onInviteClicked(roomId: RoomId)
|
||||||
fun onInviteAccepted(roomId: RoomId)
|
fun onInviteAccepted(roomId: RoomId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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 io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
interface AcceptDeclineInviteEvents {
|
||||||
|
data class AcceptInvite(val invite: InviteData) : AcceptDeclineInviteEvents
|
||||||
|
data class DeclineInvite(val invite: InviteData) : AcceptDeclineInviteEvents
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invite.api.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<InviteData>,
|
||||||
|
val acceptAction: AsyncAction<RoomId>,
|
||||||
|
val declineAction: AsyncAction<RoomId>,
|
||||||
|
val eventSink: (AcceptDeclineInviteEvents) -> Unit,
|
||||||
|
)
|
||||||
|
|
@ -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.api.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<AcceptDeclineInviteState> {
|
||||||
|
override val values: Sequence<AcceptDeclineInviteState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
anAcceptDeclineInviteState(),
|
||||||
|
anAcceptDeclineInviteState(
|
||||||
|
invite = Optional.of(
|
||||||
|
InviteData(RoomId("!room:matrix.org"), isDirect = true, roomName = "Alice"),
|
||||||
|
),
|
||||||
|
declineAction = AsyncAction.Confirming,
|
||||||
|
),
|
||||||
|
anAcceptDeclineInviteState(
|
||||||
|
invite = Optional.of(
|
||||||
|
InviteData(RoomId("!room:matrix.org"), isDirect = false, roomName = "Some room"),
|
||||||
|
),
|
||||||
|
declineAction = AsyncAction.Confirming,
|
||||||
|
),
|
||||||
|
anAcceptDeclineInviteState(
|
||||||
|
acceptAction = AsyncAction.Failure(Throwable("Whoops")),
|
||||||
|
),
|
||||||
|
anAcceptDeclineInviteState(
|
||||||
|
declineAction = AsyncAction.Failure(Throwable("Whoops")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun anAcceptDeclineInviteState(
|
||||||
|
invite: Optional<InviteData> = Optional.empty(),
|
||||||
|
acceptAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
|
||||||
|
declineAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
|
||||||
|
eventSink: (AcceptDeclineInviteEvents) -> Unit = {}
|
||||||
|
) = AcceptDeclineInviteState(
|
||||||
|
invite = invite,
|
||||||
|
acceptAction = acceptAction,
|
||||||
|
declineAction = declineAction,
|
||||||
|
eventSink = eventSink,
|
||||||
|
)
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
@ -22,7 +22,7 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.features.invitelist.impl"
|
namespace = "io.element.android.features.invite.impl"
|
||||||
}
|
}
|
||||||
|
|
||||||
anvil {
|
anvil {
|
||||||
|
|
@ -32,7 +32,7 @@ anvil {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.anvilannotations)
|
implementation(projects.anvilannotations)
|
||||||
anvil(projects.anvilcodegen)
|
anvil(projects.anvilcodegen)
|
||||||
api(projects.features.invitelist.api)
|
api(projects.features.invite.api)
|
||||||
implementation(libs.androidx.datastore.preferences)
|
implementation(libs.androidx.datastore.preferences)
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
|
|
@ -50,7 +50,7 @@ dependencies {
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.libraries.push.test)
|
testImplementation(projects.libraries.push.test)
|
||||||
testImplementation(projects.features.invitelist.test)
|
testImplementation(projects.features.invite.test)
|
||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
|
|
||||||
|
|
@ -14,13 +14,14 @@
|
||||||
* limitations under the License.
|
* 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.modality.BuildContext
|
||||||
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.Plugin
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
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.features.invite.impl.invitelist.InviteListNode
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.invitelist.impl
|
package io.element.android.features.invite.impl
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.datastore.core.DataStore
|
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.core.stringSetPreferencesKey
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
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.ApplicationContext
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
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.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.features.invitelist.impl.R
|
import io.element.android.features.invite.impl.R
|
||||||
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
|
import io.element.android.features.invite.impl.model.InviteListInviteSummary
|
||||||
import io.element.android.features.invitelist.impl.model.InviteListInviteSummaryProvider
|
import io.element.android.features.invite.impl.model.InviteListInviteSummaryProvider
|
||||||
import io.element.android.features.invitelist.impl.model.InviteSender
|
import io.element.android.features.invite.impl.model.InviteSender
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom
|
import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* 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.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
|
||||||
|
|
||||||
|
@ContributesTo(SessionScope::class)
|
||||||
|
@Module
|
||||||
|
interface InviteModule {
|
||||||
|
@Binds
|
||||||
|
fun bindAcceptDeclinePresenter(presenter: AcceptDeclineInvitePresenter): Presenter<AcceptDeclineInviteState>
|
||||||
|
}
|
||||||
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -14,17 +14,11 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.invitelist.impl
|
package io.element.android.features.invite.impl.invitelist
|
||||||
|
|
||||||
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
|
import io.element.android.features.invite.impl.model.InviteListInviteSummary
|
||||||
|
|
||||||
sealed interface InviteListEvents {
|
sealed interface InviteListEvents {
|
||||||
data class AcceptInvite(val invite: InviteListInviteSummary) : InviteListEvents
|
data class AcceptInvite(val invite: InviteListInviteSummary) : InviteListEvents
|
||||||
data class DeclineInvite(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
|
|
||||||
}
|
}
|
||||||
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.invitelist.impl
|
package io.element.android.features.invite.impl.invitelist
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
@ -25,7 +25,7 @@ import com.bumble.appyx.core.plugin.plugins
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
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.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
|
|
@ -43,6 +43,10 @@ class InviteListNode @AssistedInject constructor(
|
||||||
plugins<InviteListEntryPoint.Callback>().forEach { it.onInviteAccepted(roomId) }
|
plugins<InviteListEntryPoint.Callback>().forEach { it.onInviteAccepted(roomId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onInviteClicked(roomId: RoomId) {
|
||||||
|
plugins<InviteListEntryPoint.Callback>().forEach { it.onInviteClicked(roomId) }
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
|
|
@ -50,6 +54,8 @@ class InviteListNode @AssistedInject constructor(
|
||||||
state = state,
|
state = state,
|
||||||
onBackClicked = ::onBackClicked,
|
onBackClicked = ::onBackClicked,
|
||||||
onInviteAccepted = ::onInviteAccepted,
|
onInviteAccepted = ::onInviteAccepted,
|
||||||
|
onInviteDeclined = {},
|
||||||
|
onInviteClicked = ::onInviteClicked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -14,43 +14,35 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.invitelist.impl
|
package io.element.android.features.invite.impl.invitelist
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
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.invitelist.api.SeenInvitesStore
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
||||||
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||||
import io.element.android.features.invitelist.impl.model.InviteSender
|
import io.element.android.features.invite.api.response.InviteData
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.features.invite.impl.model.InviteListInviteSummary
|
||||||
|
import io.element.android.features.invite.impl.model.InviteSender
|
||||||
import io.element.android.libraries.architecture.Presenter
|
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.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
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.collections.immutable.toPersistentList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class InviteListPresenter @Inject constructor(
|
class InviteListPresenter @Inject constructor(
|
||||||
private val client: MatrixClient,
|
private val client: MatrixClient,
|
||||||
private val store: SeenInvitesStore,
|
private val store: SeenInvitesStore,
|
||||||
private val analyticsService: AnalyticsService,
|
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
||||||
private val notificationDrawerManager: NotificationDrawerManager,
|
|
||||||
) : Presenter<InviteListState> {
|
) : Presenter<InviteListState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): InviteListState {
|
override fun present(): InviteListState {
|
||||||
|
|
@ -75,40 +67,20 @@ class InviteListPresenter @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
val acceptDeclineInviteState = acceptDeclineInvitePresenter.present()
|
||||||
val acceptedAction: MutableState<AsyncData<RoomId>> = remember { mutableStateOf(AsyncData.Uninitialized) }
|
|
||||||
val declinedAction: MutableState<AsyncData<Unit>> = remember { mutableStateOf(AsyncData.Uninitialized) }
|
|
||||||
val decliningInvite: MutableState<InviteListInviteSummary?> = remember { mutableStateOf(null) }
|
|
||||||
|
|
||||||
fun handleEvent(event: InviteListEvents) {
|
fun handleEvent(event: InviteListEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is InviteListEvents.AcceptInvite -> {
|
is InviteListEvents.AcceptInvite -> {
|
||||||
acceptedAction.value = AsyncData.Uninitialized
|
acceptDeclineInviteState.eventSink(
|
||||||
localCoroutineScope.acceptInvite(event.invite.roomId, acceptedAction)
|
AcceptDeclineInviteEvents.AcceptInvite(event.invite.toInviteData())
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is InviteListEvents.DeclineInvite -> {
|
is InviteListEvents.DeclineInvite -> {
|
||||||
decliningInvite.value = event.invite
|
acceptDeclineInviteState.eventSink(
|
||||||
}
|
AcceptDeclineInviteEvents.DeclineInvite(event.invite.toInviteData())
|
||||||
|
)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -124,38 +96,11 @@ class InviteListPresenter @Inject constructor(
|
||||||
|
|
||||||
return InviteListState(
|
return InviteListState(
|
||||||
inviteList = inviteList,
|
inviteList = inviteList,
|
||||||
declineConfirmationDialog = decliningInvite.value?.let {
|
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||||
InviteDeclineConfirmationDialog.Visible(
|
|
||||||
isDirect = it.isDirect,
|
|
||||||
name = it.roomName,
|
|
||||||
)
|
|
||||||
} ?: InviteDeclineConfirmationDialog.Hidden,
|
|
||||||
acceptedAction = acceptedAction.value,
|
|
||||||
declinedAction = declinedAction.value,
|
|
||||||
eventSink = ::handleEvent
|
eventSink = ::handleEvent
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.acceptInvite(roomId: RoomId, acceptedAction: MutableState<AsyncData<RoomId>>) = 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<AsyncData<Unit>>) = 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 {
|
private fun RoomSummary.Filled.toInviteSummary(seen: Boolean) = details.run {
|
||||||
val i = inviter
|
val i = inviter
|
||||||
val avatarData = if (isDirect && i != null) {
|
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,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -14,24 +14,16 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.invitelist.impl
|
package io.element.android.features.invite.impl.invitelist
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.features.invite.impl.model.InviteListInviteSummary
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class InviteListState(
|
data class InviteListState(
|
||||||
val inviteList: ImmutableList<InviteListInviteSummary>,
|
val inviteList: ImmutableList<InviteListInviteSummary>,
|
||||||
val declineConfirmationDialog: InviteDeclineConfirmationDialog,
|
val acceptDeclineInviteState: AcceptDeclineInviteState,
|
||||||
val acceptedAction: AsyncData<RoomId>,
|
|
||||||
val declinedAction: AsyncData<Unit>,
|
|
||||||
val eventSink: (InviteListEvents) -> Unit
|
val eventSink: (InviteListEvents) -> Unit
|
||||||
)
|
)
|
||||||
|
|
||||||
sealed interface InviteDeclineConfirmationDialog {
|
|
||||||
data object Hidden : InviteDeclineConfirmationDialog
|
|
||||||
data class Visible(val isDirect: Boolean, val name: String) : InviteDeclineConfirmationDialog
|
|
||||||
}
|
|
||||||
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -14,35 +14,39 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.invitelist.impl
|
package io.element.android.features.invite.impl.invitelist
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||||
import io.element.android.features.invitelist.impl.model.InviteSender
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteStateProvider
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
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.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
open class InviteListStateProvider : PreviewParameterProvider<InviteListState> {
|
open class InviteListStateProvider : PreviewParameterProvider<InviteListState> {
|
||||||
|
private val acceptDeclineInviteStateProvider = AcceptDeclineInviteStateProvider()
|
||||||
|
|
||||||
override val values: Sequence<InviteListState>
|
override val values: Sequence<InviteListState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aInviteListState(),
|
anInviteListState(),
|
||||||
aInviteListState().copy(inviteList = persistentListOf()),
|
anInviteListState(inviteList = persistentListOf()),
|
||||||
aInviteListState().copy(declineConfirmationDialog = InviteDeclineConfirmationDialog.Visible(true, "Alice")),
|
) + acceptDeclineInviteStateProvider.values.map { acceptDeclineInviteState ->
|
||||||
aInviteListState().copy(declineConfirmationDialog = InviteDeclineConfirmationDialog.Visible(false, "Some Room")),
|
anInviteListState(acceptDeclineInviteState = acceptDeclineInviteState)
|
||||||
aInviteListState().copy(acceptedAction = AsyncData.Failure(Throwable("Whoops"))),
|
}
|
||||||
aInviteListState().copy(declinedAction = AsyncData.Failure(Throwable("Whoops"))),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun aInviteListState() = InviteListState(
|
internal fun anInviteListState(
|
||||||
inviteList = aInviteListInviteSummaryList(),
|
inviteList: ImmutableList<InviteListInviteSummary> = aInviteListInviteSummaryList(),
|
||||||
declineConfirmationDialog = InviteDeclineConfirmationDialog.Hidden,
|
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
|
||||||
acceptedAction = AsyncData.Uninitialized,
|
eventSink: (InviteListEvents) -> Unit = {}
|
||||||
declinedAction = AsyncData.Uninitialized,
|
) = InviteListState(
|
||||||
eventSink = {},
|
inviteList = inviteList,
|
||||||
|
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||||
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
||||||
internal fun aInviteListInviteSummaryList(): ImmutableList<InviteListInviteSummary> {
|
internal fun aInviteListInviteSummaryList(): ImmutableList<InviteListInviteSummary> {
|
||||||
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -14,8 +14,9 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.invitelist.impl
|
package io.element.android.features.invite.impl.invitelist
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
|
|
@ -27,20 +28,16 @@ import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
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.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.features.invitelist.impl.components.InviteSummaryRow
|
import io.element.android.features.invite.impl.R
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.features.invite.impl.components.InviteSummaryRow
|
||||||
|
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.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.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
||||||
|
|
@ -56,61 +53,21 @@ fun InviteListView(
|
||||||
state: InviteListState,
|
state: InviteListState,
|
||||||
onBackClicked: () -> Unit,
|
onBackClicked: () -> Unit,
|
||||||
onInviteAccepted: (RoomId) -> Unit,
|
onInviteAccepted: (RoomId) -> Unit,
|
||||||
|
onInviteDeclined: (RoomId) -> Unit,
|
||||||
|
onInviteClicked: (RoomId) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
if (state.acceptedAction is AsyncData.Success) {
|
|
||||||
val latestOnInviteAccepted by rememberUpdatedState(onInviteAccepted)
|
|
||||||
LaunchedEffect(state.acceptedAction) {
|
|
||||||
latestOnInviteAccepted(state.acceptedAction.data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
InviteListContent(
|
InviteListContent(
|
||||||
state = state,
|
state = state,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
onInviteClicked = onInviteClicked,
|
||||||
onBackClicked = onBackClicked,
|
onBackClicked = onBackClicked,
|
||||||
)
|
)
|
||||||
|
AcceptDeclineInviteView(
|
||||||
if (state.declineConfirmationDialog is InviteDeclineConfirmationDialog.Visible) {
|
state = state.acceptDeclineInviteState,
|
||||||
val contentResource = if (state.declineConfirmationDialog.isDirect) {
|
onInviteAccepted = onInviteAccepted,
|
||||||
R.string.screen_invites_decline_direct_chat_message
|
onInviteDeclined = onInviteDeclined,
|
||||||
} 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) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
|
@ -118,6 +75,7 @@ fun InviteListView(
|
||||||
private fun InviteListContent(
|
private fun InviteListContent(
|
||||||
state: InviteListState,
|
state: InviteListState,
|
||||||
onBackClicked: () -> Unit,
|
onBackClicked: () -> Unit,
|
||||||
|
onInviteClicked: (RoomId) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
|
@ -138,8 +96,8 @@ private fun InviteListContent(
|
||||||
content = { padding ->
|
content = { padding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.consumeWindowInsets(padding)
|
.consumeWindowInsets(padding)
|
||||||
) {
|
) {
|
||||||
if (state.inviteList.isEmpty()) {
|
if (state.inviteList.isEmpty()) {
|
||||||
Spacer(Modifier.size(80.dp))
|
Spacer(Modifier.size(80.dp))
|
||||||
|
|
@ -158,6 +116,9 @@ private fun InviteListContent(
|
||||||
items = state.inviteList,
|
items = state.inviteList,
|
||||||
) { index, invite ->
|
) { index, invite ->
|
||||||
InviteSummaryRow(
|
InviteSummaryRow(
|
||||||
|
modifier = Modifier.clickable(
|
||||||
|
onClick = { onInviteClicked(invite.roomId) }
|
||||||
|
),
|
||||||
invite = invite,
|
invite = invite,
|
||||||
onAcceptClicked = { state.eventSink(InviteListEvents.AcceptInvite(invite)) },
|
onAcceptClicked = { state.eventSink(InviteListEvents.AcceptInvite(invite)) },
|
||||||
onDeclineClicked = { state.eventSink(InviteListEvents.DeclineInvite(invite)) },
|
onDeclineClicked = { state.eventSink(InviteListEvents.DeclineInvite(invite)) },
|
||||||
|
|
@ -181,5 +142,7 @@ internal fun InviteListViewPreview(@PreviewParameter(InviteListStateProvider::cl
|
||||||
state = state,
|
state = state,
|
||||||
onBackClicked = {},
|
onBackClicked = {},
|
||||||
onInviteAccepted = {},
|
onInviteAccepted = {},
|
||||||
|
onInviteDeclined = {},
|
||||||
|
onInviteClicked = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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 androidx.compose.runtime.Immutable
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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 androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
/*
|
||||||
|
* 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.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.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.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<AcceptDeclineInviteState> {
|
||||||
|
@Composable
|
||||||
|
override fun present(): AcceptDeclineInviteState {
|
||||||
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
|
val acceptedAction: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||||
|
val declinedAction: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||||
|
var currentInvite by remember {
|
||||||
|
mutableStateOf<Optional<InviteData>>(Optional.empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleEvents(event: AcceptDeclineInviteEvents) {
|
||||||
|
when (event) {
|
||||||
|
is AcceptDeclineInviteEvents.AcceptInvite -> {
|
||||||
|
currentInvite = Optional.of(event.invite)
|
||||||
|
localCoroutineScope.acceptInvite(event.invite.roomId, acceptedAction)
|
||||||
|
currentInvite = Optional.empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
is AcceptDeclineInviteEvents.DeclineInvite -> {
|
||||||
|
currentInvite = Optional.of(event.invite)
|
||||||
|
declinedAction.value = AsyncAction.Confirming
|
||||||
|
}
|
||||||
|
|
||||||
|
is InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite -> {
|
||||||
|
declinedAction.value = AsyncAction.Uninitialized
|
||||||
|
currentInvite.getOrNull()?.let {
|
||||||
|
localCoroutineScope.declineInvite(it.roomId, declinedAction)
|
||||||
|
}
|
||||||
|
currentInvite = Optional.empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
is InternalAcceptDeclineInviteEvents.CancelDeclineInvite -> {
|
||||||
|
currentInvite = Optional.empty()
|
||||||
|
declinedAction.value = AsyncAction.Uninitialized
|
||||||
|
}
|
||||||
|
|
||||||
|
is InternalAcceptDeclineInviteEvents.DismissAcceptError -> {
|
||||||
|
acceptedAction.value = AsyncAction.Uninitialized
|
||||||
|
}
|
||||||
|
|
||||||
|
is InternalAcceptDeclineInviteEvents.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<AsyncAction<RoomId>>) = launch {
|
||||||
|
acceptedAction.runUpdatingState {
|
||||||
|
client.joinRoom(roomId).onSuccess {
|
||||||
|
notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId, doRender = true)
|
||||||
|
client.getRoom(roomId)?.use { room ->
|
||||||
|
analyticsService.capture(room.toAnalyticsJoinedRoom(JoinedRoom.Trigger.Invite))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.declineInvite(roomId: RoomId, declinedAction: MutableState<AsyncAction<RoomId>>) = launch {
|
||||||
|
suspend {
|
||||||
|
client.getRoom(roomId)?.use {
|
||||||
|
it.leave().getOrThrow()
|
||||||
|
notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId, doRender = true)
|
||||||
|
}
|
||||||
|
roomId
|
||||||
|
}.runCatchingUpdatingState(declinedAction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
/*
|
||||||
|
* 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.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
|
||||||
|
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(InternalAcceptDeclineInviteEvents.DismissAcceptError)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
AsyncActionView(
|
||||||
|
async = state.declineAction,
|
||||||
|
onSuccess = onInviteDeclined,
|
||||||
|
onErrorDismiss = {
|
||||||
|
state.eventSink(InternalAcceptDeclineInviteEvents.DismissDeclineError)
|
||||||
|
},
|
||||||
|
confirmationDialog = {
|
||||||
|
val invite = state.invite.getOrNull()
|
||||||
|
if (invite != null) {
|
||||||
|
DeclineConfirmationDialog(
|
||||||
|
invite = invite,
|
||||||
|
onConfirmClicked = {
|
||||||
|
state.eventSink(InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite)
|
||||||
|
},
|
||||||
|
onDismissClicked = {
|
||||||
|
state.eventSink(InternalAcceptDeclineInviteEvents.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 = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* 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.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
|
||||||
|
|
||||||
|
@ContributesBinding(SessionScope::class)
|
||||||
|
class AcceptDeclineInviteViewWrapper @Inject constructor() : AcceptDeclineInviteView {
|
||||||
|
@Composable
|
||||||
|
override fun Render(
|
||||||
|
state: AcceptDeclineInviteState,
|
||||||
|
onInviteAccepted: (RoomId) -> Unit,
|
||||||
|
onInviteDeclined: (RoomId) -> Unit,
|
||||||
|
modifier: Modifier,
|
||||||
|
) {
|
||||||
|
AcceptDeclineInviteView(
|
||||||
|
state = state,
|
||||||
|
onInviteAccepted = onInviteAccepted,
|
||||||
|
onInviteDeclined = onInviteDeclined,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 InternalAcceptDeclineInviteEvents : AcceptDeclineInviteEvents {
|
||||||
|
data object ConfirmDeclineInvite : InternalAcceptDeclineInviteEvents
|
||||||
|
data object CancelDeclineInvite : InternalAcceptDeclineInviteEvents
|
||||||
|
data object DismissAcceptError : InternalAcceptDeclineInviteEvents
|
||||||
|
data object DismissDeclineError : InternalAcceptDeclineInviteEvents
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,266 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invite.impl.invitelist
|
||||||
|
|
||||||
|
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.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.test.FakeSeenInvitesStore
|
||||||
|
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
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||||
|
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||||
|
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.aRoomMember
|
||||||
|
import io.element.android.libraries.matrix.test.room.aRoomSummaryDetails
|
||||||
|
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class InviteListPresenterTests {
|
||||||
|
@get:Rule
|
||||||
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - starts empty, adds invites when received`() = runTest {
|
||||||
|
val roomListService = FakeRoomListService()
|
||||||
|
val presenter = createInviteListPresenter(
|
||||||
|
FakeMatrixClient(roomListService = roomListService)
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.inviteList).isEmpty()
|
||||||
|
|
||||||
|
roomListService.postInviteRooms(listOf(aRoomSummary()))
|
||||||
|
|
||||||
|
val withInviteState = awaitItem()
|
||||||
|
assertThat(withInviteState.inviteList.size).isEqualTo(1)
|
||||||
|
assertThat(withInviteState.inviteList[0].roomId).isEqualTo(A_ROOM_ID)
|
||||||
|
assertThat(withInviteState.inviteList[0].roomName).isEqualTo(A_ROOM_NAME)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - uses user ID and avatar for direct invites`() = runTest {
|
||||||
|
val roomListService = FakeRoomListService().withDirectChatInvitation()
|
||||||
|
val presenter = createInviteListPresenter(
|
||||||
|
FakeMatrixClient(roomListService = roomListService)
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val withInviteState = awaitInitialItem()
|
||||||
|
assertThat(withInviteState.inviteList.size).isEqualTo(1)
|
||||||
|
assertThat(withInviteState.inviteList[0].roomId).isEqualTo(A_ROOM_ID)
|
||||||
|
assertThat(withInviteState.inviteList[0].roomAlias).isEqualTo(A_USER_ID.value)
|
||||||
|
assertThat(withInviteState.inviteList[0].roomName).isEqualTo(A_ROOM_NAME)
|
||||||
|
assertThat(withInviteState.inviteList[0].roomAvatarData).isEqualTo(
|
||||||
|
AvatarData(
|
||||||
|
id = A_USER_ID.value,
|
||||||
|
name = A_USER_NAME,
|
||||||
|
url = AN_AVATAR_URL,
|
||||||
|
size = AvatarSize.RoomInviteItem,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assertThat(withInviteState.inviteList[0].sender).isNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - includes sender details for room invites`() = runTest {
|
||||||
|
val roomListService = FakeRoomListService().withRoomInvitation()
|
||||||
|
val presenter = createInviteListPresenter(
|
||||||
|
FakeMatrixClient(roomListService = roomListService)
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val withInviteState = awaitInitialItem()
|
||||||
|
assertThat(withInviteState.inviteList.size).isEqualTo(1)
|
||||||
|
assertThat(withInviteState.inviteList[0].sender?.displayName).isEqualTo(A_USER_NAME)
|
||||||
|
assertThat(withInviteState.inviteList[0].sender?.userId).isEqualTo(A_USER_ID)
|
||||||
|
assertThat(withInviteState.inviteList[0].sender?.avatarData).isEqualTo(
|
||||||
|
AvatarData(
|
||||||
|
id = A_USER_ID.value,
|
||||||
|
name = A_USER_NAME,
|
||||||
|
url = AN_AVATAR_URL,
|
||||||
|
size = AvatarSize.InviteSender,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - stores seen invites when received`() = runTest {
|
||||||
|
val roomListService = FakeRoomListService()
|
||||||
|
val store = FakeSeenInvitesStore()
|
||||||
|
val presenter = createInviteListPresenter(
|
||||||
|
FakeMatrixClient(
|
||||||
|
roomListService = roomListService,
|
||||||
|
),
|
||||||
|
store,
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
awaitItem()
|
||||||
|
|
||||||
|
// When one invite is received, that ID is saved
|
||||||
|
roomListService.postInviteRooms(listOf(aRoomSummary()))
|
||||||
|
|
||||||
|
awaitItem()
|
||||||
|
assertThat(store.getProvidedRoomIds()).isEqualTo(setOf(A_ROOM_ID))
|
||||||
|
|
||||||
|
// When a second is added, both are saved
|
||||||
|
roomListService.postInviteRooms(listOf(aRoomSummary(), aRoomSummary(A_ROOM_ID_2)))
|
||||||
|
|
||||||
|
awaitItem()
|
||||||
|
assertThat(store.getProvidedRoomIds()).isEqualTo(setOf(A_ROOM_ID, A_ROOM_ID_2))
|
||||||
|
|
||||||
|
// When they're both dismissed, an empty set is saved
|
||||||
|
roomListService.postInviteRooms(listOf())
|
||||||
|
|
||||||
|
awaitItem()
|
||||||
|
assertThat(store.getProvidedRoomIds()).isEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - marks invite as new if they're unseen`() = runTest {
|
||||||
|
val roomListService = FakeRoomListService()
|
||||||
|
val store = FakeSeenInvitesStore()
|
||||||
|
store.publishRoomIds(setOf(A_ROOM_ID))
|
||||||
|
val presenter = createInviteListPresenter(
|
||||||
|
FakeMatrixClient(
|
||||||
|
roomListService = roomListService,
|
||||||
|
),
|
||||||
|
store,
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
awaitItem()
|
||||||
|
|
||||||
|
roomListService.postInviteRooms(listOf(aRoomSummary(), aRoomSummary(A_ROOM_ID_2)))
|
||||||
|
skipItems(1)
|
||||||
|
|
||||||
|
val withInviteState = awaitItem()
|
||||||
|
assertThat(withInviteState.inviteList.size).isEqualTo(2)
|
||||||
|
assertThat(withInviteState.inviteList[0].roomId).isEqualTo(A_ROOM_ID)
|
||||||
|
assertThat(withInviteState.inviteList[0].isNew).isFalse()
|
||||||
|
assertThat(withInviteState.inviteList[1].roomId).isEqualTo(A_ROOM_ID_2)
|
||||||
|
assertThat(withInviteState.inviteList[1].isNew).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun FakeRoomListService.withRoomInvitation(): FakeRoomListService {
|
||||||
|
postInviteRooms(
|
||||||
|
listOf(
|
||||||
|
RoomSummary.Filled(
|
||||||
|
aRoomSummaryDetails(
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
name = A_ROOM_NAME,
|
||||||
|
avatarUrl = null,
|
||||||
|
isDirect = false,
|
||||||
|
lastMessage = null,
|
||||||
|
inviter = aRoomMember(
|
||||||
|
userId = A_USER_ID,
|
||||||
|
displayName = A_USER_NAME,
|
||||||
|
avatarUrl = AN_AVATAR_URL,
|
||||||
|
membership = RoomMembershipState.JOIN,
|
||||||
|
isNameAmbiguous = false,
|
||||||
|
powerLevel = 0,
|
||||||
|
normalizedPowerLevel = 0,
|
||||||
|
isIgnored = false,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun FakeRoomListService.withDirectChatInvitation(): FakeRoomListService {
|
||||||
|
postInviteRooms(
|
||||||
|
listOf(
|
||||||
|
RoomSummary.Filled(
|
||||||
|
aRoomSummaryDetails(
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
name = A_ROOM_NAME,
|
||||||
|
avatarUrl = null,
|
||||||
|
isDirect = true,
|
||||||
|
lastMessage = null,
|
||||||
|
inviter = aRoomMember(
|
||||||
|
userId = A_USER_ID,
|
||||||
|
displayName = A_USER_NAME,
|
||||||
|
avatarUrl = AN_AVATAR_URL,
|
||||||
|
membership = RoomMembershipState.JOIN,
|
||||||
|
isNameAmbiguous = false,
|
||||||
|
powerLevel = 0,
|
||||||
|
normalizedPowerLevel = 0,
|
||||||
|
isIgnored = false,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun aRoomSummary(id: RoomId = A_ROOM_ID) = RoomSummary.Filled(
|
||||||
|
aRoomSummaryDetails(
|
||||||
|
roomId = id,
|
||||||
|
name = A_ROOM_NAME,
|
||||||
|
avatarUrl = null,
|
||||||
|
isDirect = false,
|
||||||
|
lastMessage = null,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
private suspend fun TurbineTestContext<InviteListState>.awaitInitialItem(): InviteListState {
|
||||||
|
skipItems(1)
|
||||||
|
return awaitItem()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createInviteListPresenter(
|
||||||
|
client: MatrixClient,
|
||||||
|
seenInvitesStore: SeenInvitesStore = FakeSeenInvitesStore(),
|
||||||
|
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
|
||||||
|
) = InviteListPresenter(
|
||||||
|
client,
|
||||||
|
seenInvitesStore,
|
||||||
|
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,247 @@
|
||||||
|
/*
|
||||||
|
* 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<InviteData>())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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<InviteData>())
|
||||||
|
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - declining invite error flow`() = runTest {
|
||||||
|
val declineInviteFailure = lambdaRecorder { ->
|
||||||
|
Result.failure<Unit>(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<InviteData>())
|
||||||
|
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<RoomId>(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<InviteData>())
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,11 +19,11 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.features.invitelist.test"
|
namespace = "io.element.android.features.invite.test"
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.coroutines.core)
|
implementation(libs.coroutines.core)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
api(projects.features.invitelist.api)
|
api(projects.features.invite.api)
|
||||||
}
|
}
|
||||||
|
|
@ -14,9 +14,9 @@
|
||||||
* limitations under the License.
|
* 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 io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
@ -1,505 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.invitelist.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.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.MatrixClient
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
|
||||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
|
||||||
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
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class InviteListPresenterTests {
|
|
||||||
@get:Rule
|
|
||||||
val warmUpRule = WarmUpRule()
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - starts empty, adds invites when received`() = runTest {
|
|
||||||
val roomListService = FakeRoomListService()
|
|
||||||
val presenter = createPresenter(
|
|
||||||
FakeMatrixClient(roomListService = roomListService)
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
assertThat(initialState.inviteList).isEmpty()
|
|
||||||
|
|
||||||
roomListService.postInviteRooms(listOf(aRoomSummary()))
|
|
||||||
|
|
||||||
val withInviteState = awaitItem()
|
|
||||||
assertThat(withInviteState.inviteList.size).isEqualTo(1)
|
|
||||||
assertThat(withInviteState.inviteList[0].roomId).isEqualTo(A_ROOM_ID)
|
|
||||||
assertThat(withInviteState.inviteList[0].roomName).isEqualTo(A_ROOM_NAME)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - uses user ID and avatar for direct invites`() = runTest {
|
|
||||||
val roomListService = FakeRoomListService().withDirectChatInvitation()
|
|
||||||
val presenter = createPresenter(
|
|
||||||
FakeMatrixClient(roomListService = roomListService)
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val withInviteState = awaitInitialItem()
|
|
||||||
assertThat(withInviteState.inviteList.size).isEqualTo(1)
|
|
||||||
assertThat(withInviteState.inviteList[0].roomId).isEqualTo(A_ROOM_ID)
|
|
||||||
assertThat(withInviteState.inviteList[0].roomAlias).isEqualTo(A_USER_ID.value)
|
|
||||||
assertThat(withInviteState.inviteList[0].roomName).isEqualTo(A_ROOM_NAME)
|
|
||||||
assertThat(withInviteState.inviteList[0].roomAvatarData).isEqualTo(
|
|
||||||
AvatarData(
|
|
||||||
id = A_USER_ID.value,
|
|
||||||
name = A_USER_NAME,
|
|
||||||
url = AN_AVATAR_URL,
|
|
||||||
size = AvatarSize.RoomInviteItem,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
assertThat(withInviteState.inviteList[0].sender).isNull()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - includes sender details for room invites`() = runTest {
|
|
||||||
val roomListService = FakeRoomListService().withRoomInvitation()
|
|
||||||
val presenter = createPresenter(
|
|
||||||
FakeMatrixClient(roomListService = roomListService)
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val withInviteState = awaitInitialItem()
|
|
||||||
assertThat(withInviteState.inviteList.size).isEqualTo(1)
|
|
||||||
assertThat(withInviteState.inviteList[0].sender?.displayName).isEqualTo(A_USER_NAME)
|
|
||||||
assertThat(withInviteState.inviteList[0].sender?.userId).isEqualTo(A_USER_ID)
|
|
||||||
assertThat(withInviteState.inviteList[0].sender?.avatarData).isEqualTo(
|
|
||||||
AvatarData(
|
|
||||||
id = A_USER_ID.value,
|
|
||||||
name = A_USER_NAME,
|
|
||||||
url = AN_AVATAR_URL,
|
|
||||||
size = AvatarSize.InviteSender,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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<Unit>(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<RoomId>(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(
|
|
||||||
FakeMatrixClient(
|
|
||||||
roomListService = roomListService,
|
|
||||||
),
|
|
||||||
store,
|
|
||||||
FakeAnalyticsService(),
|
|
||||||
FakeNotificationDrawerManager()
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
awaitItem()
|
|
||||||
|
|
||||||
// When one invite is received, that ID is saved
|
|
||||||
roomListService.postInviteRooms(listOf(aRoomSummary()))
|
|
||||||
|
|
||||||
awaitItem()
|
|
||||||
assertThat(store.getProvidedRoomIds()).isEqualTo(setOf(A_ROOM_ID))
|
|
||||||
|
|
||||||
// When a second is added, both are saved
|
|
||||||
roomListService.postInviteRooms(listOf(aRoomSummary(), aRoomSummary(A_ROOM_ID_2)))
|
|
||||||
|
|
||||||
awaitItem()
|
|
||||||
assertThat(store.getProvidedRoomIds()).isEqualTo(setOf(A_ROOM_ID, A_ROOM_ID_2))
|
|
||||||
|
|
||||||
// When they're both dismissed, an empty set is saved
|
|
||||||
roomListService.postInviteRooms(listOf())
|
|
||||||
|
|
||||||
awaitItem()
|
|
||||||
assertThat(store.getProvidedRoomIds()).isEmpty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - marks invite as new if they're unseen`() = runTest {
|
|
||||||
val roomListService = FakeRoomListService()
|
|
||||||
val store = FakeSeenInvitesStore()
|
|
||||||
store.publishRoomIds(setOf(A_ROOM_ID))
|
|
||||||
val presenter = InviteListPresenter(
|
|
||||||
FakeMatrixClient(
|
|
||||||
roomListService = roomListService,
|
|
||||||
),
|
|
||||||
store,
|
|
||||||
FakeAnalyticsService(),
|
|
||||||
FakeNotificationDrawerManager()
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
awaitItem()
|
|
||||||
|
|
||||||
roomListService.postInviteRooms(listOf(aRoomSummary(), aRoomSummary(A_ROOM_ID_2)))
|
|
||||||
skipItems(1)
|
|
||||||
|
|
||||||
val withInviteState = awaitItem()
|
|
||||||
assertThat(withInviteState.inviteList.size).isEqualTo(2)
|
|
||||||
assertThat(withInviteState.inviteList[0].roomId).isEqualTo(A_ROOM_ID)
|
|
||||||
assertThat(withInviteState.inviteList[0].isNew).isFalse()
|
|
||||||
assertThat(withInviteState.inviteList[1].roomId).isEqualTo(A_ROOM_ID_2)
|
|
||||||
assertThat(withInviteState.inviteList[1].isNew).isTrue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun FakeRoomListService.withRoomInvitation(): FakeRoomListService {
|
|
||||||
postInviteRooms(
|
|
||||||
listOf(
|
|
||||||
RoomSummary.Filled(
|
|
||||||
aRoomSummaryDetails(
|
|
||||||
roomId = A_ROOM_ID,
|
|
||||||
name = A_ROOM_NAME,
|
|
||||||
avatarUrl = null,
|
|
||||||
isDirect = false,
|
|
||||||
lastMessage = null,
|
|
||||||
inviter = aRoomMember(
|
|
||||||
userId = A_USER_ID,
|
|
||||||
displayName = A_USER_NAME,
|
|
||||||
avatarUrl = AN_AVATAR_URL,
|
|
||||||
membership = RoomMembershipState.JOIN,
|
|
||||||
isNameAmbiguous = false,
|
|
||||||
powerLevel = 0,
|
|
||||||
normalizedPowerLevel = 0,
|
|
||||||
isIgnored = false,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun FakeRoomListService.withDirectChatInvitation(): FakeRoomListService {
|
|
||||||
postInviteRooms(
|
|
||||||
listOf(
|
|
||||||
RoomSummary.Filled(
|
|
||||||
aRoomSummaryDetails(
|
|
||||||
roomId = A_ROOM_ID,
|
|
||||||
name = A_ROOM_NAME,
|
|
||||||
avatarUrl = null,
|
|
||||||
isDirect = true,
|
|
||||||
lastMessage = null,
|
|
||||||
inviter = aRoomMember(
|
|
||||||
userId = A_USER_ID,
|
|
||||||
displayName = A_USER_NAME,
|
|
||||||
avatarUrl = AN_AVATAR_URL,
|
|
||||||
membership = RoomMembershipState.JOIN,
|
|
||||||
isNameAmbiguous = false,
|
|
||||||
powerLevel = 0,
|
|
||||||
normalizedPowerLevel = 0,
|
|
||||||
isIgnored = false,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun aRoomSummary(id: RoomId = A_ROOM_ID) = RoomSummary.Filled(
|
|
||||||
aRoomSummaryDetails(
|
|
||||||
roomId = id,
|
|
||||||
name = A_ROOM_NAME,
|
|
||||||
avatarUrl = null,
|
|
||||||
isDirect = false,
|
|
||||||
lastMessage = null,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
private suspend fun TurbineTestContext<InviteListState>.awaitInitialItem(): InviteListState {
|
|
||||||
skipItems(1)
|
|
||||||
return awaitItem()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createPresenter(
|
|
||||||
client: MatrixClient,
|
|
||||||
seenInvitesStore: SeenInvitesStore = FakeSeenInvitesStore(),
|
|
||||||
fakeAnalyticsService: AnalyticsService = FakeAnalyticsService(),
|
|
||||||
notificationDrawerManager: NotificationDrawerManager = FakeNotificationDrawerManager()
|
|
||||||
) = InviteListPresenter(
|
|
||||||
client,
|
|
||||||
seenInvitesStore,
|
|
||||||
fakeAnalyticsService,
|
|
||||||
notificationDrawerManager
|
|
||||||
)
|
|
||||||
}
|
|
||||||
29
features/joinroom/api/build.gradle.kts
Normal file
29
features/joinroom/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* 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)
|
||||||
|
implementation(projects.features.roomdirectory.api)
|
||||||
|
}
|
||||||
|
|
@ -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.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 {
|
||||||
|
fun createNode(parentNode: Node, buildContext: BuildContext, inputs: Inputs): Node
|
||||||
|
|
||||||
|
data class Inputs(
|
||||||
|
val roomId: RoomId,
|
||||||
|
val roomDescription: Optional<RoomDescription>,
|
||||||
|
) : NodeInputs
|
||||||
|
}
|
||||||
56
features/joinroom/impl/build.gradle.kts
Normal file
56
features/joinroom/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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.androidutils)
|
||||||
|
implementation(projects.libraries.matrix.api)
|
||||||
|
implementation(projects.libraries.matrixui)
|
||||||
|
implementation(projects.libraries.designsystem)
|
||||||
|
implementation(projects.features.invite.api)
|
||||||
|
implementation(projects.features.roomdirectory.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)
|
||||||
|
testImplementation(projects.features.invite.test)
|
||||||
|
testImplementation(projects.tests.testutils)
|
||||||
|
|
||||||
|
ksp(libs.showkase.processor)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* 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<JoinRoomNode>(
|
||||||
|
buildContext = buildContext,
|
||||||
|
plugins = listOf(inputs)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.features.joinroom.impl
|
||||||
|
|
||||||
|
sealed interface JoinRoomEvents {
|
||||||
|
data object JoinRoom : JoinRoomEvents
|
||||||
|
data object AcceptInvite : JoinRoomEvents
|
||||||
|
data object DeclineInvite : JoinRoomEvents
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* 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 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.features.invite.api.response.AcceptDeclineInviteView
|
||||||
|
import io.element.android.features.joinroom.api.JoinRoomEntryPoint
|
||||||
|
import io.element.android.libraries.architecture.inputs
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
|
||||||
|
@ContributesNode(SessionScope::class)
|
||||||
|
class JoinRoomNode @AssistedInject constructor(
|
||||||
|
@Assisted buildContext: BuildContext,
|
||||||
|
@Assisted plugins: List<Plugin>,
|
||||||
|
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)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier) {
|
||||||
|
val state = presenter.present()
|
||||||
|
JoinRoomView(
|
||||||
|
state = state,
|
||||||
|
onBackPressed = ::navigateUp,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
acceptDeclineInviteView.Render(
|
||||||
|
state = state.acceptDeclineInviteState,
|
||||||
|
onInviteAccepted = {},
|
||||||
|
onInviteDeclined = { navigateUp() },
|
||||||
|
modifier = Modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
/*
|
||||||
|
* 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 androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
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.AcceptDeclineInviteState
|
||||||
|
import io.element.android.features.invite.api.response.InviteData
|
||||||
|
import io.element.android.features.roomdirectory.api.RoomDescription
|
||||||
|
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.room.MatrixRoomInfo
|
||||||
|
import org.jetbrains.annotations.VisibleForTesting
|
||||||
|
import java.util.Optional
|
||||||
|
|
||||||
|
class JoinRoomPresenter @AssistedInject constructor(
|
||||||
|
@Assisted private val roomId: RoomId,
|
||||||
|
@Assisted private val roomDescription: Optional<RoomDescription>,
|
||||||
|
private val matrixClient: MatrixClient,
|
||||||
|
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
||||||
|
) : Presenter<JoinRoomState> {
|
||||||
|
interface Factory {
|
||||||
|
fun create(roomId: RoomId, roomDescription: Optional<RoomDescription>): JoinRoomPresenter
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun present(): JoinRoomState {
|
||||||
|
val roomInfo by matrixClient.getRoomInfoFlow(roomId).collectAsState(initial = Optional.empty())
|
||||||
|
val contentState by produceState<ContentState>(initialValue = ContentState.Loading(roomId), key1 = roomInfo) {
|
||||||
|
value = when {
|
||||||
|
roomInfo.isPresent -> {
|
||||||
|
roomInfo.get().toContentState()
|
||||||
|
}
|
||||||
|
roomDescription.isPresent -> {
|
||||||
|
roomDescription.get().toContentState()
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
ContentState.Loading(roomId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val acceptDeclineInviteState = acceptDeclineInvitePresenter.present()
|
||||||
|
|
||||||
|
fun handleEvents(event: JoinRoomEvents) {
|
||||||
|
when (event) {
|
||||||
|
JoinRoomEvents.AcceptInvite, JoinRoomEvents.JoinRoom -> {
|
||||||
|
val inviteData = contentState.toInviteData() ?: return
|
||||||
|
acceptDeclineInviteState.eventSink(
|
||||||
|
AcceptDeclineInviteEvents.AcceptInvite(inviteData)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
JoinRoomEvents.DeclineInvite -> {
|
||||||
|
val inviteData = contentState.toInviteData() ?: return
|
||||||
|
acceptDeclineInviteState.eventSink(
|
||||||
|
AcceptDeclineInviteEvents.DeclineInvite(inviteData)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JoinRoomState(
|
||||||
|
contentState = contentState,
|
||||||
|
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||||
|
eventSink = ::handleEvents
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun RoomDescription.toContentState(): ContentState {
|
||||||
|
return ContentState.Loaded(
|
||||||
|
roomId = roomId,
|
||||||
|
name = name,
|
||||||
|
topic = topic,
|
||||||
|
alias = alias,
|
||||||
|
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 {
|
||||||
|
return ContentState.Loaded(
|
||||||
|
roomId = RoomId(id),
|
||||||
|
name = name,
|
||||||
|
topic = topic,
|
||||||
|
alias = canonicalAlias,
|
||||||
|
numberOfMembers = activeMembersCount,
|
||||||
|
isDirect = isDirect,
|
||||||
|
roomAvatarUrl = avatarUrl,
|
||||||
|
joinAuthorisationStatus = when {
|
||||||
|
currentUserMembership == CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited
|
||||||
|
isPublic -> JoinAuthorisationStatus.CanJoin
|
||||||
|
else -> JoinAuthorisationStatus.Unknown
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun ContentState.toInviteData(): InviteData? {
|
||||||
|
return when (this) {
|
||||||
|
is ContentState.Loaded -> InviteData(
|
||||||
|
roomId = roomId,
|
||||||
|
roomName = computedTitle,
|
||||||
|
isDirect = isDirect
|
||||||
|
)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
/*
|
||||||
|
* 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 androidx.compose.runtime.Immutable
|
||||||
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||||
|
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: ContentState,
|
||||||
|
val acceptDeclineInviteState: AcceptDeclineInviteState,
|
||||||
|
val eventSink: (JoinRoomEvents) -> Unit
|
||||||
|
) {
|
||||||
|
val joinAuthorisationStatus = when (contentState) {
|
||||||
|
is ContentState.Loaded -> contentState.joinAuthorisationStatus
|
||||||
|
else -> JoinAuthorisationStatus.Unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 computedSubtitle = when {
|
||||||
|
alias != null -> alias
|
||||||
|
name == null -> ""
|
||||||
|
else -> roomId.value
|
||||||
|
}
|
||||||
|
|
||||||
|
val showMemberCount = numberOfMembers != null
|
||||||
|
|
||||||
|
fun avatarData(size: AvatarSize): AvatarData {
|
||||||
|
return AvatarData(
|
||||||
|
id = roomId.value,
|
||||||
|
name = name,
|
||||||
|
url = roomAvatarUrl,
|
||||||
|
size = size,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class JoinAuthorisationStatus {
|
||||||
|
IsInvited,
|
||||||
|
CanKnock,
|
||||||
|
CanJoin,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
/*
|
||||||
|
* 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 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.matrix.api.core.RoomId
|
||||||
|
|
||||||
|
open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
|
||||||
|
override val values: Sequence<JoinRoomState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
aJoinRoomState(
|
||||||
|
contentState = aLoadingContentState()
|
||||||
|
),
|
||||||
|
aJoinRoomState(
|
||||||
|
contentState = anUnknownContentState()
|
||||||
|
),
|
||||||
|
aJoinRoomState(
|
||||||
|
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin)
|
||||||
|
),
|
||||||
|
aJoinRoomState(
|
||||||
|
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock)
|
||||||
|
),
|
||||||
|
aJoinRoomState(
|
||||||
|
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun anUnknownContentState(roomId: RoomId = A_ROOM_ID) = ContentState.UnknownRoom(roomId)
|
||||||
|
|
||||||
|
fun aLoadingContentState(roomId: RoomId = A_ROOM_ID) = ContentState.Loading(roomId)
|
||||||
|
|
||||||
|
fun aLoadedContentState(
|
||||||
|
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,
|
||||||
|
isDirect: Boolean = false,
|
||||||
|
roomAvatarUrl: String? = null,
|
||||||
|
joinAuthorisationStatus: JoinAuthorisationStatus = JoinAuthorisationStatus.Unknown
|
||||||
|
) = ContentState.Loaded(
|
||||||
|
roomId = roomId,
|
||||||
|
name = name,
|
||||||
|
alias = alias,
|
||||||
|
topic = topic,
|
||||||
|
numberOfMembers = numberOfMembers,
|
||||||
|
isDirect = isDirect,
|
||||||
|
roomAvatarUrl = roomAvatarUrl,
|
||||||
|
joinAuthorisationStatus = joinAuthorisationStatus
|
||||||
|
)
|
||||||
|
|
||||||
|
fun aJoinRoomState(
|
||||||
|
contentState: ContentState = aLoadedContentState(),
|
||||||
|
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
|
||||||
|
eventSink: (JoinRoomEvents) -> Unit = {}
|
||||||
|
) = JoinRoomState(
|
||||||
|
contentState = contentState,
|
||||||
|
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||||
|
eventSink = eventSink
|
||||||
|
)
|
||||||
|
|
||||||
|
private val A_ROOM_ID = RoomId("!exa:matrix.org")
|
||||||
|
|
@ -0,0 +1,301 @@
|
||||||
|
/*
|
||||||
|
* 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 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
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
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
|
||||||
|
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.compound.tokens.generated.CompoundIcons
|
||||||
|
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
|
||||||
|
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.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
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun JoinRoomView(
|
||||||
|
state: JoinRoomState,
|
||||||
|
onBackPressed: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
HeaderFooterPage(
|
||||||
|
modifier = modifier,
|
||||||
|
paddingValues = PaddingValues(16.dp),
|
||||||
|
topBar = {
|
||||||
|
JoinRoomTopBar(onBackClicked = onBackPressed)
|
||||||
|
},
|
||||||
|
content = {
|
||||||
|
JoinRoomContent(contentState = state.contentState)
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JoinAuthorisationStatus.CanJoin -> {
|
||||||
|
Button(
|
||||||
|
text = stringResource(R.string.screen_join_room_join_action),
|
||||||
|
onClick = onJoinRoom,
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
size = ButtonSize.Medium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
JoinAuthorisationStatus.CanKnock -> {
|
||||||
|
Button(
|
||||||
|
text = stringResource(R.string.screen_join_room_knock_action),
|
||||||
|
onClick = onJoinRoom,
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
size = ButtonSize.Medium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
JoinAuthorisationStatus.Unknown -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun JoinRoomContent(
|
||||||
|
contentState: ContentState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
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(
|
||||||
|
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)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ContentScaffold(
|
||||||
|
avatar: @Composable () -> Unit,
|
||||||
|
title: @Composable () -> Unit,
|
||||||
|
subtitle: @Composable () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
description: @Composable (() -> Unit)? = null,
|
||||||
|
memberCount: @Composable (() -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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) {
|
||||||
|
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(
|
||||||
|
onBackClicked: () -> Unit,
|
||||||
|
) {
|
||||||
|
TopAppBar(
|
||||||
|
navigationIcon = {
|
||||||
|
BackButton(onClick = onBackClicked)
|
||||||
|
},
|
||||||
|
title = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewLightDark
|
||||||
|
@Composable
|
||||||
|
internal fun JoinRoomViewPreview(@PreviewParameter(JoinRoomStateProvider::class) state: JoinRoomState) = ElementPreview {
|
||||||
|
JoinRoomView(
|
||||||
|
state = state,
|
||||||
|
onBackPressed = { }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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.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 java.util.Optional
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@ContributesTo(SessionScope::class)
|
||||||
|
object JoinRoomModule {
|
||||||
|
@Provides
|
||||||
|
fun providesJoinRoomPresenterFactory(
|
||||||
|
client: MatrixClient,
|
||||||
|
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
||||||
|
): JoinRoomPresenter.Factory {
|
||||||
|
return object : JoinRoomPresenter.Factory {
|
||||||
|
override fun create(roomId: RoomId, roomDescription: Optional<RoomDescription>): JoinRoomPresenter {
|
||||||
|
return JoinRoomPresenter(
|
||||||
|
roomId = roomId,
|
||||||
|
roomDescription = roomDescription,
|
||||||
|
matrixClient = client,
|
||||||
|
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
features/joinroom/impl/src/main/res/values/localazy.xml
Normal file
9
features/joinroom/impl/src/main/res/values/localazy.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_join_room_join_action">"Join room"</string>
|
||||||
|
<string name="screen_join_room_knock_action">"Knock to join"</string>
|
||||||
|
<string name="screen_join_room_subtitle_knock">"Click the button below and a room administrator will be notified. You’ll be able to join the conversation once approved."</string>
|
||||||
|
<string name="screen_join_room_subtitle_no_preview">"You must be a member of this room to view the message history."</string>
|
||||||
|
<string name="screen_join_room_title_knock">"Want to join this room?"</string>
|
||||||
|
<string name="screen_join_room_title_no_preview">"Preview is not available"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,273 @@
|
||||||
|
/*
|
||||||
|
* 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.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).isEqualTo(ContentState.Loading(A_ROOM_ID))
|
||||||
|
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 ->
|
||||||
|
val contentState = state.contentState as ContentState.Loaded
|
||||||
|
assertThat(contentState.roomId).isEqualTo(A_ROOM_ID)
|
||||||
|
assertThat(contentState.name).isEqualTo(roomInfo.name)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 ->
|
||||||
|
val contentState = state.contentState as ContentState.Loaded
|
||||||
|
assertThat(contentState.roomId).isEqualTo(A_ROOM_ID)
|
||||||
|
assertThat(contentState.name).isEqualTo(roomDescription.name)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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<RoomDescription> = Optional.empty(),
|
||||||
|
matrixClient: MatrixClient = FakeMatrixClient(),
|
||||||
|
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = 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,
|
||||||
|
topic: String? = "A room about something",
|
||||||
|
alias: String? = "#alias:matrix.org",
|
||||||
|
avatarUrl: String? = null,
|
||||||
|
joinRule: RoomDescription.JoinRule = RoomDescription.JoinRule.UNKNOWN,
|
||||||
|
numberOfMembers: Long = 2L
|
||||||
|
): RoomDescription {
|
||||||
|
return RoomDescription(
|
||||||
|
roomId = roomId,
|
||||||
|
name = name,
|
||||||
|
topic = topic,
|
||||||
|
alias = alias,
|
||||||
|
avatarUrl = avatarUrl,
|
||||||
|
joinRule = joinRule,
|
||||||
|
numberOfMembers = numberOfMembers
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -164,7 +164,7 @@ class LeaveRoomPresenterImplTest {
|
||||||
givenGetRoomResult(
|
givenGetRoomResult(
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
result = FakeMatrixRoom().apply {
|
result = FakeMatrixRoom().apply {
|
||||||
givenLeaveRoomError(RuntimeException("Blimey!"))
|
this.leaveRoomLambda = { Result.failure(RuntimeException("Blimey!")) }
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -210,7 +210,7 @@ class LeaveRoomPresenterImplTest {
|
||||||
givenGetRoomResult(
|
givenGetRoomResult(
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
result = FakeMatrixRoom().apply {
|
result = FakeMatrixRoom().apply {
|
||||||
givenLeaveRoomError(RuntimeException("Blimey!"))
|
this.leaveRoomLambda = { Result.failure(RuntimeException("Blimey!")) }
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("io.element.android-library")
|
id("io.element.android-library")
|
||||||
|
id("kotlin-parcelize")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,52 @@
|
||||||
|
|
||||||
package io.element.android.features.roomdirectory.api
|
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.AvatarData
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
@Immutable
|
||||||
data class RoomDescription(
|
data class RoomDescription(
|
||||||
val roomId: RoomId,
|
val roomId: RoomId,
|
||||||
val name: String,
|
val name: String?,
|
||||||
val description: String,
|
val alias: String?,
|
||||||
val avatarData: AvatarData,
|
val topic: String?,
|
||||||
val canBeJoined: Boolean,
|
val avatarUrl: String?,
|
||||||
)
|
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 {
|
||||||
|
topic != null -> topic
|
||||||
|
name != null && alias != null -> alias
|
||||||
|
name == null && alias == null -> ""
|
||||||
|
else -> roomId.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IgnoredOnParcel
|
||||||
|
val canJoinOrKnock = joinRule == JoinRule.PUBLIC || joinRule == JoinRule.KNOCK
|
||||||
|
|
||||||
|
fun avatarData(size: AvatarSize) = AvatarData(
|
||||||
|
id = roomId.value,
|
||||||
|
name = name,
|
||||||
|
url = avatarUrl,
|
||||||
|
size = size,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ interface RoomDirectoryEntryPoint : FeatureEntryPoint {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun onOpenRoom(roomId: RoomId)
|
fun onRoomJoined(roomId: RoomId)
|
||||||
|
fun onResultClicked(roomDescription: RoomDescription)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ dependencies {
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.libraries.matrixui)
|
implementation(projects.libraries.matrixui)
|
||||||
|
implementation(projects.libraries.androidutils)
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.libraries.testtags)
|
implementation(projects.libraries.testtags)
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import com.bumble.appyx.core.plugin.plugins
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
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.features.roomdirectory.api.RoomDirectoryEntryPoint
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
@ -35,9 +36,15 @@ class RoomDirectoryNode @AssistedInject constructor(
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
private val presenter: RoomDirectoryPresenter,
|
private val presenter: RoomDirectoryPresenter,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
private fun onResultClicked(roomDescription: RoomDescription) {
|
||||||
|
plugins<RoomDirectoryEntryPoint.Callback>().forEach {
|
||||||
|
it.onResultClicked(roomDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun onRoomJoined(roomId: RoomId) {
|
private fun onRoomJoined(roomId: RoomId) {
|
||||||
plugins<RoomDirectoryEntryPoint.Callback>().forEach {
|
plugins<RoomDirectoryEntryPoint.Callback>().forEach {
|
||||||
it.onOpenRoom(roomId)
|
it.onRoomJoined(roomId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,6 +54,7 @@ class RoomDirectoryNode @AssistedInject constructor(
|
||||||
RoomDirectoryView(
|
RoomDirectoryView(
|
||||||
state = state,
|
state = state,
|
||||||
onRoomJoined = ::onRoomJoined,
|
onRoomJoined = ::onRoomJoined,
|
||||||
|
onResultClicked = ::onResultClicked,
|
||||||
onBackPressed = ::navigateUp,
|
onBackPressed = ::navigateUp,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,6 @@ package io.element.android.features.roomdirectory.impl.root
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.roomdirectory.api.RoomDescription
|
import io.element.android.features.roomdirectory.api.RoomDescription
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
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 io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
@ -70,26 +68,20 @@ fun aRoomDescriptionList(): ImmutableList<RoomDescription> {
|
||||||
RoomDescription(
|
RoomDescription(
|
||||||
roomId = RoomId("!exa:matrix.org"),
|
roomId = RoomId("!exa:matrix.org"),
|
||||||
name = "Element X Android",
|
name = "Element X Android",
|
||||||
description = "Element X is a secure, private and decentralized messenger.",
|
topic = "Element X is a secure, private and decentralized messenger.",
|
||||||
avatarData = AvatarData(
|
alias = "#element-x-android:matrix.org",
|
||||||
id = "!exa:matrix.org",
|
avatarUrl = null,
|
||||||
name = "Element X Android",
|
joinRule = RoomDescription.JoinRule.PUBLIC,
|
||||||
url = null,
|
numberOfMembers = 2765,
|
||||||
size = AvatarSize.RoomDirectoryItem
|
|
||||||
),
|
|
||||||
canBeJoined = true,
|
|
||||||
),
|
),
|
||||||
RoomDescription(
|
RoomDescription(
|
||||||
roomId = RoomId("!exi:matrix.org"),
|
roomId = RoomId("!exi:matrix.org"),
|
||||||
name = "Element X iOS",
|
name = "Element X iOS",
|
||||||
description = "Element X is a secure, private and decentralized messenger.",
|
topic = "Element X is a secure, private and decentralized messenger.",
|
||||||
avatarData = AvatarData(
|
alias = "#element-x-ios:matrix.org",
|
||||||
id = "!exi:matrix.org",
|
avatarUrl = null,
|
||||||
name = "Element X iOS",
|
joinRule = RoomDescription.JoinRule.UNKNOWN,
|
||||||
url = null,
|
numberOfMembers = 356,
|
||||||
size = AvatarSize.RoomDirectoryItem
|
|
||||||
),
|
|
||||||
canBeJoined = false,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ import io.element.android.features.roomdirectory.api.RoomDescription
|
||||||
import io.element.android.features.roomdirectory.impl.R
|
import io.element.android.features.roomdirectory.impl.R
|
||||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
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.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.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
|
@ -70,12 +71,13 @@ import kotlinx.collections.immutable.ImmutableList
|
||||||
@Composable
|
@Composable
|
||||||
fun RoomDirectoryView(
|
fun RoomDirectoryView(
|
||||||
state: RoomDirectoryState,
|
state: RoomDirectoryState,
|
||||||
|
onResultClicked: (RoomDescription) -> Unit,
|
||||||
onRoomJoined: (RoomId) -> Unit,
|
onRoomJoined: (RoomId) -> Unit,
|
||||||
onBackPressed: () -> Unit,
|
onBackPressed: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
fun joinRoom(roomId: RoomId) {
|
fun joinRoom(roomDescription: RoomDescription) {
|
||||||
state.eventSink(RoomDirectoryEvents.JoinRoom(roomId))
|
state.eventSink(RoomDirectoryEvents.JoinRoom(roomDescription.roomId))
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
|
@ -86,10 +88,11 @@ fun RoomDirectoryView(
|
||||||
content = { padding ->
|
content = { padding ->
|
||||||
RoomDirectoryContent(
|
RoomDirectoryContent(
|
||||||
state = state,
|
state = state,
|
||||||
onResultClicked = ::joinRoom,
|
onResultClicked = onResultClicked,
|
||||||
|
onJoinClicked = ::joinRoom,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.consumeWindowInsets(padding)
|
.consumeWindowInsets(padding)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -128,7 +131,8 @@ private fun RoomDirectoryTopBar(
|
||||||
@Composable
|
@Composable
|
||||||
private fun RoomDirectoryContent(
|
private fun RoomDirectoryContent(
|
||||||
state: RoomDirectoryState,
|
state: RoomDirectoryState,
|
||||||
onResultClicked: (RoomId) -> Unit,
|
onResultClicked: (RoomDescription) -> Unit,
|
||||||
|
onJoinClicked: (RoomDescription) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
|
|
@ -143,6 +147,7 @@ private fun RoomDirectoryContent(
|
||||||
displayLoadMoreIndicator = state.displayLoadMoreIndicator,
|
displayLoadMoreIndicator = state.displayLoadMoreIndicator,
|
||||||
displayEmptyState = state.displayEmptyState,
|
displayEmptyState = state.displayEmptyState,
|
||||||
onResultClicked = onResultClicked,
|
onResultClicked = onResultClicked,
|
||||||
|
onJoinClicked = onJoinClicked,
|
||||||
onReachedLoadMore = { state.eventSink(RoomDirectoryEvents.LoadMore) },
|
onReachedLoadMore = { state.eventSink(RoomDirectoryEvents.LoadMore) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -153,7 +158,8 @@ private fun RoomDirectoryRoomList(
|
||||||
roomDescriptions: ImmutableList<RoomDescription>,
|
roomDescriptions: ImmutableList<RoomDescription>,
|
||||||
displayLoadMoreIndicator: Boolean,
|
displayLoadMoreIndicator: Boolean,
|
||||||
displayEmptyState: Boolean,
|
displayEmptyState: Boolean,
|
||||||
onResultClicked: (RoomId) -> Unit,
|
onResultClicked: (RoomDescription) -> Unit,
|
||||||
|
onJoinClicked: (RoomDescription) -> Unit,
|
||||||
onReachedLoadMore: () -> Unit,
|
onReachedLoadMore: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|
@ -161,7 +167,12 @@ private fun RoomDirectoryRoomList(
|
||||||
items(roomDescriptions) { roomDescription ->
|
items(roomDescriptions) { roomDescription ->
|
||||||
RoomDirectoryRoomRow(
|
RoomDirectoryRoomRow(
|
||||||
roomDescription = roomDescription,
|
roomDescription = roomDescription,
|
||||||
onClick = onResultClicked,
|
onClick = {
|
||||||
|
onResultClicked(roomDescription)
|
||||||
|
},
|
||||||
|
onJoinClick = {
|
||||||
|
onJoinClicked(roomDescription)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (displayEmptyState) {
|
if (displayEmptyState) {
|
||||||
|
|
@ -188,10 +199,10 @@ private fun RoomDirectoryRoomList(
|
||||||
@Composable
|
@Composable
|
||||||
private fun LoadMoreIndicator(modifier: Modifier = Modifier) {
|
private fun LoadMoreIndicator(modifier: Modifier = Modifier) {
|
||||||
Box(
|
Box(
|
||||||
modifier
|
modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.wrapContentHeight()
|
.wrapContentHeight()
|
||||||
.padding(24.dp),
|
.padding(24.dp),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
|
|
@ -256,53 +267,54 @@ private fun SearchTextField(
|
||||||
@Composable
|
@Composable
|
||||||
private fun RoomDirectoryRoomRow(
|
private fun RoomDirectoryRoomRow(
|
||||||
roomDescription: RoomDescription,
|
roomDescription: RoomDescription,
|
||||||
onClick: (RoomId) -> Unit,
|
onClick: () -> Unit,
|
||||||
|
onJoinClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(enabled = roomDescription.canBeJoined) {
|
.clickable(onClick = onClick)
|
||||||
onClick(roomDescription.roomId)
|
.padding(
|
||||||
}
|
top = 12.dp,
|
||||||
.padding(
|
bottom = 12.dp,
|
||||||
top = 12.dp,
|
start = 16.dp,
|
||||||
bottom = 12.dp,
|
)
|
||||||
start = 16.dp,
|
.height(IntrinsicSize.Min),
|
||||||
)
|
|
||||||
.height(IntrinsicSize.Min),
|
|
||||||
) {
|
) {
|
||||||
Avatar(
|
Avatar(
|
||||||
avatarData = roomDescription.avatarData,
|
avatarData = roomDescription.avatarData(AvatarSize.RoomDirectoryItem),
|
||||||
modifier = Modifier.align(Alignment.CenterVertically)
|
modifier = Modifier.align(Alignment.CenterVertically)
|
||||||
)
|
)
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.padding(start = 16.dp)
|
.padding(start = 16.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = roomDescription.name,
|
text = roomDescription.computedName,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
style = ElementTheme.typography.fontBodyLgRegular,
|
style = ElementTheme.typography.fontBodyLgRegular,
|
||||||
color = ElementTheme.colors.textPrimary,
|
color = ElementTheme.colors.textPrimary,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = roomDescription.description,
|
text = roomDescription.computedDescription,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
style = ElementTheme.typography.fontBodyMdRegular,
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
color = ElementTheme.colors.textSecondary,
|
color = ElementTheme.colors.textSecondary,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (roomDescription.canBeJoined) {
|
if (roomDescription.canJoinOrKnock) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = CommonStrings.action_join),
|
text = stringResource(id = CommonStrings.action_join),
|
||||||
color = ElementTheme.colors.textSuccessPrimary,
|
color = ElementTheme.colors.textSuccessPrimary,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.CenterVertically)
|
.align(Alignment.CenterVertically)
|
||||||
.padding(start = 4.dp, end = 12.dp)
|
.clickable(onClick = onJoinClick)
|
||||||
|
.padding(start = 4.dp, end = 12.dp)
|
||||||
|
.testTag(TestTags.callToAction.value)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Spacer(modifier = Modifier.width(24.dp))
|
Spacer(modifier = Modifier.width(24.dp))
|
||||||
|
|
@ -315,6 +327,7 @@ private fun RoomDirectoryRoomRow(
|
||||||
internal fun RoomDirectoryViewPreview(@PreviewParameter(RoomDirectoryStateProvider::class) state: RoomDirectoryState) = ElementPreview {
|
internal fun RoomDirectoryViewPreview(@PreviewParameter(RoomDirectoryStateProvider::class) state: RoomDirectoryState) = ElementPreview {
|
||||||
RoomDirectoryView(
|
RoomDirectoryView(
|
||||||
state = state,
|
state = state,
|
||||||
|
onResultClicked = {},
|
||||||
onRoomJoined = {},
|
onRoomJoined = {},
|
||||||
onBackPressed = {},
|
onBackPressed = {},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -17,37 +17,20 @@
|
||||||
package io.element.android.features.roomdirectory.impl.root.model
|
package io.element.android.features.roomdirectory.impl.root.model
|
||||||
|
|
||||||
import io.element.android.features.roomdirectory.api.RoomDescription
|
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
|
import io.element.android.libraries.matrix.api.roomdirectory.RoomDescription as MatrixRoomDescription
|
||||||
|
|
||||||
fun MatrixRoomDescription.toFeatureModel(): RoomDescription {
|
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(
|
return RoomDescription(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
name = name(),
|
name = name,
|
||||||
description = description(),
|
alias = alias,
|
||||||
avatarData = AvatarData(
|
topic = topic,
|
||||||
id = roomId.value,
|
avatarUrl = avatarUrl,
|
||||||
name = name,
|
numberOfMembers = numberOfMembers,
|
||||||
url = avatarUrl,
|
joinRule = when (joinRule) {
|
||||||
size = AvatarSize.RoomDirectoryItem,
|
MatrixRoomDescription.JoinRule.PUBLIC -> RoomDescription.JoinRule.PUBLIC
|
||||||
),
|
MatrixRoomDescription.JoinRule.KNOCK -> RoomDescription.JoinRule.KNOCK
|
||||||
canBeJoined = joinRule == MatrixRoomDescription.JoinRule.PUBLIC,
|
MatrixRoomDescription.JoinRule.UNKNOWN -> RoomDescription.JoinRule.UNKNOWN
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,14 @@ package io.element.android.features.roomdirectory.impl.root
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.compose.ui.test.onAllNodesWithTag
|
||||||
|
import androidx.compose.ui.test.onFirst
|
||||||
import androidx.compose.ui.test.onNodeWithTag
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
import androidx.compose.ui.test.onNodeWithText
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
import androidx.compose.ui.test.performTextInput
|
import androidx.compose.ui.test.performTextInput
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
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.architecture.AsyncAction
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.testtags.TestTags
|
import io.element.android.libraries.testtags.TestTags
|
||||||
|
|
@ -55,7 +58,24 @@ class RoomDirectoryViewTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on room item emits the expected Event`() {
|
fun `clicking on room item then onResultClicked lambda is called once`() {
|
||||||
|
val eventsRecorder = EventsRecorder<RoomDirectoryEvents>()
|
||||||
|
val state = aRoomDirectoryState(
|
||||||
|
roomDescriptions = aRoomDescriptionList(),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
)
|
||||||
|
val clickedRoom = state.roomDescriptions.first()
|
||||||
|
ensureCalledOnceWithParam(clickedRoom) { callback ->
|
||||||
|
rule.setRoomDirectoryView(
|
||||||
|
state = state,
|
||||||
|
onResultClicked = callback,
|
||||||
|
)
|
||||||
|
rule.onNodeWithText(clickedRoom.computedName).performClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on room item join cta emits the expected Event`() {
|
||||||
val eventsRecorder = EventsRecorder<RoomDirectoryEvents>()
|
val eventsRecorder = EventsRecorder<RoomDirectoryEvents>()
|
||||||
val state = aRoomDirectoryState(
|
val state = aRoomDirectoryState(
|
||||||
roomDescriptions = aRoomDescriptionList(),
|
roomDescriptions = aRoomDescriptionList(),
|
||||||
|
|
@ -63,7 +83,7 @@ class RoomDirectoryViewTest {
|
||||||
)
|
)
|
||||||
rule.setRoomDirectoryView(state = state)
|
rule.setRoomDirectoryView(state = state)
|
||||||
val clickedRoom = state.roomDescriptions.first()
|
val clickedRoom = state.roomDescriptions.first()
|
||||||
rule.onNodeWithText(clickedRoom.name).performClick()
|
rule.onAllNodesWithTag(TestTags.callToAction.value).onFirst().performClick()
|
||||||
eventsRecorder.assertSingle(RoomDirectoryEvents.JoinRoom(clickedRoom.roomId))
|
eventsRecorder.assertSingle(RoomDirectoryEvents.JoinRoom(clickedRoom.roomId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,11 +120,13 @@ class RoomDirectoryViewTest {
|
||||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomDirectoryView(
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomDirectoryView(
|
||||||
state: RoomDirectoryState,
|
state: RoomDirectoryState,
|
||||||
onBackPressed: () -> Unit = EnsureNeverCalled(),
|
onBackPressed: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onResultClicked: (RoomDescription) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
onRoomJoined: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
|
onRoomJoined: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
) {
|
) {
|
||||||
setContent {
|
setContent {
|
||||||
RoomDirectoryView(
|
RoomDirectoryView(
|
||||||
state = state,
|
state = state,
|
||||||
|
onResultClicked = onResultClicked,
|
||||||
onRoomJoined = onRoomJoined,
|
onRoomJoined = onRoomJoined,
|
||||||
onBackPressed = onBackPressed,
|
onBackPressed = onBackPressed,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ dependencies {
|
||||||
implementation(projects.libraries.indicator.api)
|
implementation(projects.libraries.indicator.api)
|
||||||
implementation(projects.libraries.deeplink)
|
implementation(projects.libraries.deeplink)
|
||||||
implementation(projects.libraries.preferences.api)
|
implementation(projects.libraries.preferences.api)
|
||||||
implementation(projects.features.invitelist.api)
|
implementation(projects.features.invite.api)
|
||||||
implementation(projects.features.networkmonitor.api)
|
implementation(projects.features.networkmonitor.api)
|
||||||
implementation(projects.features.leaveroom.api)
|
implementation(projects.features.leaveroom.api)
|
||||||
implementation(projects.services.analytics.api)
|
implementation(projects.services.analytics.api)
|
||||||
|
|
@ -75,7 +75,7 @@ dependencies {
|
||||||
testImplementation(projects.libraries.indicator.impl)
|
testImplementation(projects.libraries.indicator.impl)
|
||||||
testImplementation(projects.libraries.permissions.noop)
|
testImplementation(projects.libraries.permissions.noop)
|
||||||
testImplementation(projects.libraries.preferences.test)
|
testImplementation(projects.libraries.preferences.test)
|
||||||
testImplementation(projects.features.invitelist.test)
|
testImplementation(projects.features.invite.test)
|
||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(projects.features.networkmonitor.test)
|
testImplementation(projects.features.networkmonitor.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
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.features.roomlist.impl.InvitesState
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ class RoomListDataSource @Inject constructor(
|
||||||
fun launchIn(coroutineScope: CoroutineScope) {
|
fun launchIn(coroutineScope: CoroutineScope) {
|
||||||
roomListService
|
roomListService
|
||||||
.allRooms
|
.allRooms
|
||||||
.summaries
|
.filteredSummaries
|
||||||
.onEach { roomSummaries ->
|
.onEach { roomSummaries ->
|
||||||
replaceWith(roomSummaries)
|
replaceWith(roomSummaries)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ class RoomListSearchDataSource @Inject constructor(
|
||||||
source = RoomList.Source.All,
|
source = RoomList.Source.All,
|
||||||
)
|
)
|
||||||
|
|
||||||
val roomSummaries: Flow<PersistentList<RoomListRoomSummary>> = roomList.summaries
|
val roomSummaries: Flow<PersistentList<RoomListRoomSummary>> = roomList.filteredSummaries
|
||||||
.map { roomSummaries ->
|
.map { roomSummaries ->
|
||||||
roomSummaries
|
roomSummaries
|
||||||
.filterIsInstance<RoomSummary.Filled>()
|
.filterIsInstance<RoomSummary.Filled>()
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.invitelist.test.FakeSeenInvitesStore
|
import io.element.android.features.invite.test.FakeSeenInvitesStore
|
||||||
import io.element.android.features.roomlist.impl.InvitesState
|
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
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
|
@ -29,11 +30,14 @@ import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
@Composable
|
@Composable
|
||||||
fun ButtonRowMolecule(
|
fun ButtonRowMolecule(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
horizontalArrangement: Arrangement.Horizontal = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment: Alignment.Vertical = Alignment.Top,
|
||||||
content: @Composable RowScope.() -> Unit
|
content: @Composable RowScope.() -> Unit
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = horizontalArrangement,
|
||||||
|
verticalAlignment = verticalAlignment,
|
||||||
) {
|
) {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package io.element.android.libraries.designsystem.atomic.pages
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
|
@ -34,6 +35,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param modifier Classical modifier.
|
* @param modifier Classical modifier.
|
||||||
|
* @param paddingValues padding values to apply to the content.
|
||||||
* @param background optional background component.
|
* @param background optional background component.
|
||||||
* @param topBar optional topBar.
|
* @param topBar optional topBar.
|
||||||
* @param header optional header.
|
* @param header optional header.
|
||||||
|
|
@ -43,6 +45,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
@Composable
|
@Composable
|
||||||
fun HeaderFooterPage(
|
fun HeaderFooterPage(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
paddingValues: PaddingValues = PaddingValues(20.dp),
|
||||||
background: @Composable () -> Unit = {},
|
background: @Composable () -> Unit = {},
|
||||||
topBar: @Composable () -> Unit = {},
|
topBar: @Composable () -> Unit = {},
|
||||||
header: @Composable () -> Unit = {},
|
header: @Composable () -> Unit = {},
|
||||||
|
|
@ -57,7 +60,7 @@ fun HeaderFooterPage(
|
||||||
background()
|
background()
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(all = 20.dp)
|
.padding(paddingValues = paddingValues)
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.consumeWindowInsets(padding)
|
.consumeWindowInsets(padding)
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,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.oidc.AccountManagementAction
|
||||||
import io.element.android.libraries.matrix.api.pusher.PushersService
|
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.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.room.RoomMembershipObserver
|
||||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
|
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
|
|
@ -37,8 +38,10 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
|
import java.util.Optional
|
||||||
|
|
||||||
interface MatrixClient : Closeable {
|
interface MatrixClient : Closeable {
|
||||||
val sessionId: SessionId
|
val sessionId: SessionId
|
||||||
|
|
@ -89,6 +92,7 @@ interface MatrixClient : Closeable {
|
||||||
suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result<String?>
|
suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result<String?>
|
||||||
suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result<String>
|
suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result<String>
|
||||||
fun roomMembershipObserver(): RoomMembershipObserver
|
fun roomMembershipObserver(): RoomMembershipObserver
|
||||||
|
fun getRoomInfoFlow(roomId: RoomId): Flow<Optional<MatrixRoomInfo>>
|
||||||
|
|
||||||
fun isMe(userId: UserId?) = userId == sessionId
|
fun isMe(userId: UserId?) = userId == sessionId
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,10 +28,10 @@ class RoomMembershipObserver {
|
||||||
val change: MembershipChange,
|
val change: MembershipChange,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val _updates = MutableSharedFlow<RoomMembershipUpdate>(replay = 1)
|
private val _updates = MutableSharedFlow<RoomMembershipUpdate>(extraBufferCapacity = 10)
|
||||||
val updates = _updates.asSharedFlow()
|
val updates = _updates.asSharedFlow()
|
||||||
|
|
||||||
fun notifyUserLeftRoom(roomId: RoomId) {
|
suspend fun notifyUserLeftRoom(roomId: RoomId) {
|
||||||
_updates.tryEmit(RoomMembershipUpdate(roomId, false, MembershipChange.LEFT))
|
_updates.emit(RoomMembershipUpdate(roomId, false, MembershipChange.LEFT))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package io.element.android.libraries.matrix.api.roomlist
|
package io.element.android.libraries.matrix.api.roomlist
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
|
@ -32,6 +33,8 @@ interface DynamicRoomList : RoomList {
|
||||||
val loadedPages: StateFlow<Int>
|
val loadedPages: StateFlow<Int>
|
||||||
val pageSize: Int
|
val pageSize: Int
|
||||||
|
|
||||||
|
val filteredSummaries: SharedFlow<List<RoomSummary>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load more rooms into the list if possible.
|
* Load more rooms into the list if possible.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,9 @@
|
||||||
package io.element.android.libraries.matrix.api.roomlist
|
package io.element.android.libraries.matrix.api.roomlist
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.filterIsInstance
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entry point for the room list api.
|
* Entry point for the room list api.
|
||||||
|
|
@ -78,3 +80,7 @@ interface RoomListService {
|
||||||
*/
|
*/
|
||||||
val state: StateFlow<State>
|
val state: StateFlow<State>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun RoomList.loadedStateFlow(): Flow<RoomList.LoadingState.Loaded> {
|
||||||
|
return loadingState.filterIsInstance()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.oidc.AccountManagementAction
|
||||||
import io.element.android.libraries.matrix.api.pusher.PushersService
|
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.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.room.RoomMembershipObserver
|
||||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
|
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
|
|
@ -79,12 +80,15 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.TimeoutCancellationException
|
import kotlinx.coroutines.TimeoutCancellationException
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.buffer
|
import kotlinx.coroutines.flow.buffer
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -104,8 +108,10 @@ import org.matrix.rustcomponents.sdk.TimelineEventTypeFilter
|
||||||
import org.matrix.rustcomponents.sdk.use
|
import org.matrix.rustcomponents.sdk.use
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.Optional
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.INFINITE
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
import org.matrix.rustcomponents.sdk.CreateRoomParameters as RustCreateRoomParameters
|
import org.matrix.rustcomponents.sdk.CreateRoomParameters as RustCreateRoomParameters
|
||||||
import org.matrix.rustcomponents.sdk.RoomPreset as RustRoomPreset
|
import org.matrix.rustcomponents.sdk.RoomPreset as RustRoomPreset
|
||||||
|
|
@ -540,6 +546,22 @@ class RustMatrixClient(
|
||||||
|
|
||||||
override fun roomMembershipObserver(): RoomMembershipObserver = roomMembershipObserver
|
override fun roomMembershipObserver(): RoomMembershipObserver = roomMembershipObserver
|
||||||
|
|
||||||
|
override fun getRoomInfoFlow(roomId: RoomId): Flow<Optional<MatrixRoomInfo>> {
|
||||||
|
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(
|
private suspend fun File.getCacheSize(
|
||||||
includeCryptoDb: Boolean = false,
|
includeCryptoDb: Boolean = false,
|
||||||
): Long = withContext(sessionDispatcher) {
|
): Long = withContext(sessionDispatcher) {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.getAndUpdate
|
import kotlinx.coroutines.flow.getAndUpdate
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
|
@ -92,7 +93,8 @@ internal class RoomListFactory(
|
||||||
innerRoomList?.destroy()
|
innerRoomList?.destroy()
|
||||||
}
|
}
|
||||||
return RustDynamicRoomList(
|
return RustDynamicRoomList(
|
||||||
summaries = filteredSummariesFlow,
|
summaries = summariesFlow,
|
||||||
|
filteredSummaries = filteredSummariesFlow,
|
||||||
loadingState = loadingStateFlow,
|
loadingState = loadingStateFlow,
|
||||||
currentFilter = currentFilter,
|
currentFilter = currentFilter,
|
||||||
loadedPages = loadedPages,
|
loadedPages = loadedPages,
|
||||||
|
|
@ -105,6 +107,7 @@ internal class RoomListFactory(
|
||||||
|
|
||||||
private class RustDynamicRoomList(
|
private class RustDynamicRoomList(
|
||||||
override val summaries: MutableSharedFlow<List<RoomSummary>>,
|
override val summaries: MutableSharedFlow<List<RoomSummary>>,
|
||||||
|
override val filteredSummaries: SharedFlow<List<RoomSummary>>,
|
||||||
override val loadingState: MutableStateFlow<RoomList.LoadingState>,
|
override val loadingState: MutableStateFlow<RoomList.LoadingState>,
|
||||||
override val currentFilter: MutableStateFlow<RoomListFilter>,
|
override val currentFilter: MutableStateFlow<RoomListFilter>,
|
||||||
override val loadedPages: MutableStateFlow<Int>,
|
override val loadedPages: MutableStateFlow<Int>,
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.impl.util
|
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.ProducerScope
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
|
|
@ -23,7 +24,9 @@ import org.matrix.rustcomponents.sdk.TaskHandle
|
||||||
|
|
||||||
internal fun <T> mxCallbackFlow(block: suspend ProducerScope<T>.() -> TaskHandle?) =
|
internal fun <T> mxCallbackFlow(block: suspend ProducerScope<T>.() -> TaskHandle?) =
|
||||||
callbackFlow {
|
callbackFlow {
|
||||||
val taskHandle: TaskHandle? = block(this)
|
val taskHandle: TaskHandle? = tryOrNull {
|
||||||
|
block(this)
|
||||||
|
}
|
||||||
awaitClose {
|
awaitClose {
|
||||||
taskHandle?.cancelAndDestroy()
|
taskHandle?.cancelAndDestroy()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.oidc.AccountManagementAction
|
||||||
import io.element.android.libraries.matrix.api.pusher.PushersService
|
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.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.room.RoomMembershipObserver
|
||||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
|
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
|
|
@ -51,7 +52,9 @@ import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import java.util.Optional
|
||||||
|
|
||||||
class FakeMatrixClient(
|
class FakeMatrixClient(
|
||||||
override val sessionId: SessionId = A_SESSION_ID,
|
override val sessionId: SessionId = A_SESSION_ID,
|
||||||
|
|
@ -94,10 +97,14 @@ class FakeMatrixClient(
|
||||||
private var setDisplayNameResult: Result<Unit> = Result.success(Unit)
|
private var setDisplayNameResult: Result<Unit> = Result.success(Unit)
|
||||||
private var uploadAvatarResult: Result<Unit> = Result.success(Unit)
|
private var uploadAvatarResult: Result<Unit> = Result.success(Unit)
|
||||||
private var removeAvatarResult: Result<Unit> = Result.success(Unit)
|
private var removeAvatarResult: Result<Unit> = Result.success(Unit)
|
||||||
var joinRoomLambda: suspend (RoomId) -> Result<RoomId> = {
|
var joinRoomLambda: (RoomId) -> Result<RoomId> = {
|
||||||
Result.success(it)
|
Result.success(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var getRoomInfoFlowLambda = { _: RoomId ->
|
||||||
|
flowOf<Optional<MatrixRoomInfo>>(Optional.empty())
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun getRoom(roomId: RoomId): MatrixRoom? {
|
override suspend fun getRoom(roomId: RoomId): MatrixRoom? {
|
||||||
return getRoomResults[roomId]
|
return getRoomResults[roomId]
|
||||||
}
|
}
|
||||||
|
|
@ -267,4 +274,6 @@ class FakeMatrixClient(
|
||||||
override suspend fun getRecentlyVisitedRooms(): Result<List<RoomId>> {
|
override suspend fun getRecentlyVisitedRooms(): Result<List<RoomId>> {
|
||||||
return Result.success(visitedRoomsId)
|
return Result.success(visitedRoomsId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getRoomInfoFlow(roomId: RoomId) = getRoomInfoFlowLambda(roomId)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -182,7 +182,7 @@ class FakeMatrixRoom(
|
||||||
var removedAvatar: Boolean = false
|
var removedAvatar: Boolean = false
|
||||||
private set
|
private set
|
||||||
|
|
||||||
private var leaveRoomError: Throwable? = null
|
var leaveRoomLambda: (() -> Result<Unit>) = { Result.success(Unit) }
|
||||||
|
|
||||||
private val _roomInfoFlow: MutableSharedFlow<MatrixRoomInfo> = MutableSharedFlow(replay = 1)
|
private val _roomInfoFlow: MutableSharedFlow<MatrixRoomInfo> = MutableSharedFlow(replay = 1)
|
||||||
override val roomInfoFlow: Flow<MatrixRoomInfo> = _roomInfoFlow
|
override val roomInfoFlow: Flow<MatrixRoomInfo> = _roomInfoFlow
|
||||||
|
|
@ -315,8 +315,9 @@ class FakeMatrixRoom(
|
||||||
return Result.success(Unit)
|
return Result.success(Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun leave(): Result<Unit> =
|
override suspend fun leave(): Result<Unit> {
|
||||||
leaveRoomError?.let { Result.failure(it) } ?: Result.success(Unit)
|
return leaveRoomLambda()
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun join(): Result<Unit> {
|
override suspend fun join(): Result<Unit> {
|
||||||
return joinRoomResult
|
return joinRoomResult
|
||||||
|
|
@ -542,10 +543,6 @@ class FakeMatrixRoom(
|
||||||
|
|
||||||
override fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result<MatrixWidgetDriver> = getWidgetDriverResult
|
override fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result<MatrixWidgetDriver> = getWidgetDriverResult
|
||||||
|
|
||||||
fun givenLeaveRoomError(throwable: Throwable?) {
|
|
||||||
this.leaveRoomError = throwable
|
|
||||||
}
|
|
||||||
|
|
||||||
fun givenRoomMembersState(state: MatrixRoomMembersState) {
|
fun givenRoomMembersState(state: MatrixRoomMembersState) {
|
||||||
membersStateFlow.value = state
|
membersStateFlow.value = state
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.RoomListFilter
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.getAndUpdate
|
import kotlinx.coroutines.flow.getAndUpdate
|
||||||
|
|
||||||
|
|
@ -32,6 +33,8 @@ data class SimplePagedRoomList(
|
||||||
override val pageSize: Int = Int.MAX_VALUE
|
override val pageSize: Int = Int.MAX_VALUE
|
||||||
override val loadedPages = MutableStateFlow(1)
|
override val loadedPages = MutableStateFlow(1)
|
||||||
|
|
||||||
|
override val filteredSummaries: SharedFlow<List<RoomSummary>> = summaries
|
||||||
|
|
||||||
override suspend fun loadMore() {
|
override suspend fun loadMore() {
|
||||||
// No-op
|
// No-op
|
||||||
loadedPages.getAndUpdate { it + 1 }
|
loadedPages.getAndUpdate { it + 1 }
|
||||||
|
|
|
||||||
|
|
@ -105,4 +105,9 @@ object TestTags {
|
||||||
* Search field.
|
* Search field.
|
||||||
*/
|
*/
|
||||||
val searchTextField = TestTag("search_text_field")
|
val searchTextField = TestTag("search_text_field")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic call to action.
|
||||||
|
*/
|
||||||
|
val callToAction = TestTag("call_to_action")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ dependencies {
|
||||||
implementation(projects.libraries.eventformatter.impl)
|
implementation(projects.libraries.eventformatter.impl)
|
||||||
implementation(projects.libraries.preferences.impl)
|
implementation(projects.libraries.preferences.impl)
|
||||||
implementation(projects.libraries.indicator.impl)
|
implementation(projects.libraries.indicator.impl)
|
||||||
implementation(projects.features.invitelist.impl)
|
implementation(projects.features.invite.impl)
|
||||||
implementation(projects.features.roomlist.impl)
|
implementation(projects.features.roomlist.impl)
|
||||||
implementation(projects.features.leaveroom.impl)
|
implementation(projects.features.leaveroom.impl)
|
||||||
implementation(projects.features.login.impl)
|
implementation(projects.features.login.impl)
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import android.content.Context
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.ui.Modifier
|
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.leaveroom.impl.LeaveRoomPresenterImpl
|
||||||
import io.element.android.features.networkmonitor.impl.NetworkMonitorImpl
|
import io.element.android.features.networkmonitor.impl.NetworkMonitorImpl
|
||||||
import io.element.android.features.roomlist.impl.RoomListPresenter
|
import io.element.android.features.roomlist.impl.RoomListPresenter
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue