Merge branch 'release/0.4.10' into main
This commit is contained in:
commit
60447690b5
339 changed files with 4338 additions and 2243 deletions
|
|
@ -12,4 +12,4 @@ jobs:
|
||||||
# No concurrency required, this is a prerequisite to other actions and should run every time.
|
# No concurrency required, this is a prerequisite to other actions and should run every time.
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: gradle/wrapper-validation-action@v2
|
- uses: gradle/wrapper-validation-action@v3
|
||||||
|
|
|
||||||
17
CHANGES.md
17
CHANGES.md
|
|
@ -1,3 +1,20 @@
|
||||||
|
Changes in Element X v0.4.10 (2024-04-17)
|
||||||
|
=========================================
|
||||||
|
|
||||||
|
Matrix Rust SDK 0.2.14
|
||||||
|
|
||||||
|
Features ✨
|
||||||
|
----------
|
||||||
|
- Rework room navigation to handle unknown room and prepare work on permalink. ([#2695](https://github.com/element-hq/element-x-android/issues/2695))
|
||||||
|
|
||||||
|
Other changes
|
||||||
|
-------------
|
||||||
|
- Encrypt new session data with a passphrase ([#2703](https://github.com/element-hq/element-x-android/issues/2703))
|
||||||
|
- Use sdk API to build permalinks ([#2708](https://github.com/element-hq/element-x-android/issues/2708))
|
||||||
|
- Parse permalink using parseMatrixEntityFrom from the SDK ([#2709](https://github.com/element-hq/element-x-android/issues/2709))
|
||||||
|
- Fix compile for forks that use the `noop` analytics module ([#2698](https://github.com/element-hq/element-x-android/issues/2698))
|
||||||
|
|
||||||
|
|
||||||
Changes in Element X v0.4.9 (2024-04-12)
|
Changes in Element X v0.4.9 (2024-04-12)
|
||||||
========================================
|
========================================
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
2
fastlane/metadata/android/en-US/changelogs/40004100.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40004100.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
Main changes in this version: Prepare navigation with permalink.
|
||||||
|
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_create_room_action_create_room">"Новая комната"</string>
|
<string name="screen_create_room_action_create_room">"Новая комната"</string>
|
||||||
<string name="screen_create_room_add_people_title">"Пригласить друзей"</string>
|
<string name="screen_create_room_add_people_title">"Пригласить в комнату"</string>
|
||||||
<string name="screen_create_room_error_creating_room">"Произошла ошибка при создании комнаты"</string>
|
<string name="screen_create_room_error_creating_room">"Произошла ошибка при создании комнаты"</string>
|
||||||
<string name="screen_create_room_private_option_description">"Сообщения в этой комнате зашифрованы. Отключить шифрование позже будет невозможно."</string>
|
<string name="screen_create_room_private_option_description">"Сообщения в этой комнате зашифрованы. Отключить шифрование позже будет невозможно."</string>
|
||||||
<string name="screen_create_room_private_option_title">"Приватная комната (только по приглашению)"</string>
|
<string name="screen_create_room_private_option_title">"Приватная комната (только по приглашению)"</string>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,14 @@
|
||||||
<string name="screen_notification_optin_subtitle">"Вы можаце змяніць налады пазней."</string>
|
<string name="screen_notification_optin_subtitle">"Вы можаце змяніць налады пазней."</string>
|
||||||
<string name="screen_notification_optin_title">"Дазвольце апавяшчэнні і ніколі не прапускайце іх"</string>
|
<string name="screen_notification_optin_title">"Дазвольце апавяшчэнні і ніколі не прапускайце іх"</string>
|
||||||
<string name="screen_qr_code_login_connecting_subtitle">"Устанаўленне злучэння"</string>
|
<string name="screen_qr_code_login_connecting_subtitle">"Устанаўленне злучэння"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_description">"Не атрымалася ўсталяваць бяспечнае злучэнне з новай прыладай. Існуючыя прылады па-ранейшаму ў бяспецы, і вам не трэба турбавацца пра іх."</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Што зараз?"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Паспрабуйце зноў увайсці ў сістэму з дапамогай QR-кода, калі гэта была сеткавая праблема"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Калі вы сутыкнуліся з той жа праблемай, паспрабуйце іншую сетку Wi-Fi або скарыстайцеся мабільнымі дадзенымі замест Wi-Fi."</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Калі гэта не дапамагло, увайдзіце ўручную"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"Злучэнне небяспечнае"</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_subtitle">"Вам будзе прапанавана ўвесці дзве лічбы, паказаныя ніжэй."</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_title">"Увядзіце нумар на прыладзе"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_1">"Адкрыйце %1$s на настольнай прыладзе"</string>
|
<string name="screen_qr_code_login_initial_state_item_1">"Адкрыйце %1$s на настольнай прыладзе"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_2">"Націсніце на свой аватар"</string>
|
<string name="screen_qr_code_login_initial_state_item_2">"Націсніце на свой аватар"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3">"Выберыце %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_3">"Выберыце %1$s"</string>
|
||||||
|
|
@ -13,7 +21,15 @@
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Выкарыстоўвайце QR-код, паказаны на іншай прыладзе."</string>
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Выкарыстоўвайце QR-код, паказаны на іншай прыладзе."</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Паўтарыць спробу"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Паўтарыць спробу"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Няправільны QR-код"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Няправільны QR-код"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_button">"Перайсці ў налады камеры"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_state_description">"Каб працягнуць, вам неабходна дазволіць Element выкарыстоўваць камеру вашай прылады."</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_state_title">"Дазвольце доступ да камеры для сканавання QR-кода"</string>
|
||||||
<string name="screen_qr_code_login_scanning_state_title">"Сканаваць QR-код"</string>
|
<string name="screen_qr_code_login_scanning_state_title">"Сканаваць QR-код"</string>
|
||||||
|
<string name="screen_qr_code_login_start_over_button">"Пачаць спачатку"</string>
|
||||||
|
<string name="screen_qr_code_login_unknown_error_description">"Адбылася нечаканая памылка. Калі ласка, паспрабуйце яшчэ раз."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_loading">"У чаканні іншай прылады"</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_subtitle">"Ваш правайдэр уліковага запісу можа запытаць наступны код для праверкі ўваходу."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_title">"Ваш код спраўджання"</string>
|
||||||
<string name="screen_welcome_bullet_1">"Званкі, апытанні, пошук і многае іншае будзе дададзена пазней у гэтым годзе."</string>
|
<string name="screen_welcome_bullet_1">"Званкі, апытанні, пошук і многае іншае будзе дададзена пазней у гэтым годзе."</string>
|
||||||
<string name="screen_welcome_bullet_2">"Гісторыя паведамленняў для зашыфраваных пакояў пакуль недаступна."</string>
|
<string name="screen_welcome_bullet_2">"Гісторыя паведамленняў для зашыфраваных пакояў пакуль недаступна."</string>
|
||||||
<string name="screen_welcome_bullet_3">"Мы будзем рады пачуць вашае меркаванне, паведаміце нам аб гэтым праз старонку налад."</string>
|
<string name="screen_welcome_bullet_3">"Мы будзем рады пачуць вашае меркаванне, паведаміце нам аб гэтым праз старонку налад."</string>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_notification_optin_subtitle">"Можете да промените настройките си по-късно."</string>
|
<string name="screen_notification_optin_subtitle">"Можете да промените настройките си по-късно."</string>
|
||||||
<string name="screen_notification_optin_title">"Разрешете известията и никога не пропускайте съобщение"</string>
|
<string name="screen_notification_optin_title">"Разрешете известията и никога не пропускайте съобщение"</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Повторен опит"</string>
|
||||||
<string name="screen_welcome_bullet_2">"Хронологията на съобщенията за шифровани стаи все още не е налична."</string>
|
<string name="screen_welcome_bullet_2">"Хронологията на съобщенията за шифровани стаи все още не е налична."</string>
|
||||||
<string name="screen_welcome_title">"Добре дошли в %1$s!"</string>
|
<string name="screen_welcome_title">"Добре дошли в %1$s!"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,34 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_notification_optin_subtitle">"Nastavení můžete později změnit."</string>
|
<string name="screen_notification_optin_subtitle">"Nastavení můžete později změnit."</string>
|
||||||
<string name="screen_notification_optin_title">"Povolte oznámení a nezmeškejte žádnou zprávu"</string>
|
<string name="screen_notification_optin_title">"Povolte oznámení a nezmeškejte žádnou zprávu"</string>
|
||||||
|
<string name="screen_qr_code_login_connecting_subtitle">"Navazování spojení"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_description">"K novému zařízení se nepodařilo navázat bezpečné připojení. Vaše stávající zařízení jsou stále v bezpečí a nemusíte se o ně obávat."</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Co teď?"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Zkuste se znovu přihlásit pomocí QR kódu v případě, že se jednalo o problém se sítí"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Pokud narazíte na stejný problém, zkuste jinou síť wifi nebo použijte mobilní data místo wifi"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Pokud to nefunguje, přihlaste se ručně"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"Připojení není zabezpečené"</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_subtitle">"Budete požádáni o zadání dvou níže uvedených číslic."</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_title">"Zadejte číslo na svém zařízení"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_1">"Otevřete %1$s na stolním počítači"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_2">"Klikněte na svůj avatar"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_3">"Vybrat %1$s"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Připojit nové zařízení\""</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_4">"Vybrat %1$s"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_4_action">"\"Zobrazit QR kód\""</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_title">"Otevřete %1$s na jiném zařízení pro získání QR kódu"</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Použijte QR kód zobrazený na druhém zařízení."</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Zkusit znovu"</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Špatný QR kód"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_button">"Přejděte na nastavení fotoaparátu"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_state_description">"Abyste mohli pokračovat, musíte aplikaci Element udělit povolení k použití kamery vašeho zařízení."</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_state_title">"Povolte přístup k fotoaparátu a naskenujte QR kód"</string>
|
||||||
|
<string name="screen_qr_code_login_scanning_state_title">"Naskenujte QR kód"</string>
|
||||||
|
<string name="screen_qr_code_login_start_over_button">"Začít znovu"</string>
|
||||||
|
<string name="screen_qr_code_login_unknown_error_description">"Vyskytla se neočekávaná chyba. Prosím zkuste to znovu."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_loading">"Čekání na vaše další zařízení"</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_subtitle">"Váš poskytovatel účtu může požádat o následující kód pro ověření přihlášení."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_title">"Váš ověřovací kód"</string>
|
||||||
<string name="screen_welcome_bullet_1">"Hovory, hlasování, vyhledávání a další budou přidány koncem tohoto roku."</string>
|
<string name="screen_welcome_bullet_1">"Hovory, hlasování, vyhledávání a další budou přidány koncem tohoto roku."</string>
|
||||||
<string name="screen_welcome_bullet_2">"Historie zpráv šifrovaných místností nebude v této aktualizaci k dispozici."</string>
|
<string name="screen_welcome_bullet_2">"Historie zpráv šifrovaných místností nebude v této aktualizaci k dispozici."</string>
|
||||||
<string name="screen_welcome_bullet_3">"Rádi bychom se od vás dozvěděli, co si o tom myslíte, dejte nám vědět prostřednictvím stránky s nastavením."</string>
|
<string name="screen_welcome_bullet_3">"Rádi bychom se od vás dozvěděli, co si o tom myslíte, dejte nám vědět prostřednictvím stránky s nastavením."</string>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,13 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_notification_optin_subtitle">"Du kannst deine Einstellungen später ändern."</string>
|
<string name="screen_notification_optin_subtitle">"Du kannst deine Einstellungen später ändern."</string>
|
||||||
<string name="screen_notification_optin_title">"Erlaube Benachrichtigungen und verpasse keine Nachricht"</string>
|
<string name="screen_notification_optin_title">"Erlaube Benachrichtigungen und verpasse keine Nachricht"</string>
|
||||||
|
<string name="screen_qr_code_login_connecting_subtitle">"Verbindung aufbauen"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_description">"Es konnte keine sichere Verbindung zu dem neuen Gerät hergestellt werden."</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Und jetzt?"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Versuche, dich erneut mit einem QR-Code anzumelden, falls dies ein Netzwerkproblem war."</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Wenn das Problem bestehen bleibt, versuche es mit einem anderen WLAN-Netzwerk oder verwende deine mobilen Daten statt WLAN."</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Wenn das nicht funktioniert, melde dich manuell an"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"Die Verbindung ist nicht sicher"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_1">"%1$s auf einem Desktop-Gerät öffnen"</string>
|
<string name="screen_qr_code_login_initial_state_item_1">"%1$s auf einem Desktop-Gerät öffnen"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_2">"Klick auf deinen Avatar"</string>
|
<string name="screen_qr_code_login_initial_state_item_2">"Klick auf deinen Avatar"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3">"Wähle %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_3">"Wähle %1$s"</string>
|
||||||
|
|
@ -9,6 +16,15 @@
|
||||||
<string name="screen_qr_code_login_initial_state_item_4">"Wähle %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_4">"Wähle %1$s"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4_action">"\"QR-Code anzeigen\""</string>
|
<string name="screen_qr_code_login_initial_state_item_4_action">"\"QR-Code anzeigen\""</string>
|
||||||
<string name="screen_qr_code_login_initial_state_title">"Öffne %1$s auf einem anderen Gerät, um den QR-Code zu erhalten"</string>
|
<string name="screen_qr_code_login_initial_state_title">"Öffne %1$s auf einem anderen Gerät, um den QR-Code zu erhalten"</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Verwende den QR-Code, der auf dem anderen Gerät angezeigt wird."</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Erneut versuchen"</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Falscher QR-Code"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_button">"Gehe zu den Kameraeinstellungen"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_state_description">"Du musst Element die Erlaubnis erteilen, die Kamera deines Geräts zu verwenden, um fortzufahren."</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_state_title">"Erlaube Zugriff auf die Kamera zum Scannen des QR-Codes"</string>
|
||||||
|
<string name="screen_qr_code_login_scanning_state_title">"QR-Code scannen"</string>
|
||||||
|
<string name="screen_qr_code_login_start_over_button">"Neu beginnen"</string>
|
||||||
|
<string name="screen_qr_code_login_unknown_error_description">"Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es erneut."</string>
|
||||||
<string name="screen_welcome_bullet_1">"Anrufe, Umfragen, Suchfunktionen und mehr werden im Laufe des Jahres hinzugefügt."</string>
|
<string name="screen_welcome_bullet_1">"Anrufe, Umfragen, Suchfunktionen und mehr werden im Laufe des Jahres hinzugefügt."</string>
|
||||||
<string name="screen_welcome_bullet_2">"Der Nachrichtenverlauf für verschlüsselte Räume wird in diesem Update nicht verfügbar sein."</string>
|
<string name="screen_welcome_bullet_2">"Der Nachrichtenverlauf für verschlüsselte Räume wird in diesem Update nicht verfügbar sein."</string>
|
||||||
<string name="screen_welcome_bullet_3">"Wir würden uns freuen, von dir zu hören. Teile uns deine Meinung über die Einstellungsseite mit."</string>
|
<string name="screen_welcome_bullet_3">"Wir würden uns freuen, von dir zu hören. Teile uns deine Meinung über die Einstellungsseite mit."</string>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_notification_optin_subtitle">"Puedes cambiar la configuración más tarde."</string>
|
<string name="screen_notification_optin_subtitle">"Puedes cambiar la configuración más tarde."</string>
|
||||||
<string name="screen_notification_optin_title">"Activa las notificaciones y nunca te pierdas un mensaje"</string>
|
<string name="screen_notification_optin_title">"Activa las notificaciones y nunca te pierdas un mensaje"</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Inténtalo de nuevo"</string>
|
||||||
<string name="screen_welcome_bullet_1">"Las llamadas, las encuestas, la búsqueda y más se agregarán más adelante este año."</string>
|
<string name="screen_welcome_bullet_1">"Las llamadas, las encuestas, la búsqueda y más se agregarán más adelante este año."</string>
|
||||||
<string name="screen_welcome_bullet_2">"El historial de mensajes de las salas cifradas aún no está disponible."</string>
|
<string name="screen_welcome_bullet_2">"El historial de mensajes de las salas cifradas aún no está disponible."</string>
|
||||||
<string name="screen_welcome_bullet_3">"Nos encantaría saber de ti, haznos saber lo que piensas a través de la página de configuración."</string>
|
<string name="screen_welcome_bullet_3">"Nos encantaría saber de ti, haznos saber lo que piensas a través de la página de configuración."</string>
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
<string name="screen_qr_code_login_initial_state_item_4_action">"“Afficher le QR code”"</string>
|
<string name="screen_qr_code_login_initial_state_item_4_action">"“Afficher le QR code”"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_title">"Ouvrez %1$s sur un autre appareil pour obtenir le QR code"</string>
|
<string name="screen_qr_code_login_initial_state_title">"Ouvrez %1$s sur un autre appareil pour obtenir le QR code"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Scannez le QR code affiché sur l’autre appareil."</string>
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Scannez le QR code affiché sur l’autre appareil."</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Réessayer"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Essayer à nouveau"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"QR code erroné"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"QR code erroné"</string>
|
||||||
<string name="screen_qr_code_login_scanning_state_title">"Scannez le QR code"</string>
|
<string name="screen_qr_code_login_scanning_state_title">"Scannez le QR code"</string>
|
||||||
<string name="screen_welcome_bullet_1">"Les appels, les sondages, les recherches et plus encore seront ajoutés plus tard cette année."</string>
|
<string name="screen_welcome_bullet_1">"Les appels, les sondages, les recherches et plus encore seront ajoutés plus tard cette année."</string>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,29 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_notification_optin_subtitle">"A beállításokat később is módosíthatja."</string>
|
<string name="screen_notification_optin_subtitle">"A beállításokat később is módosíthatja."</string>
|
||||||
<string name="screen_notification_optin_title">"Értesítések engedélyezése, hogy soha ne maradjon le egyetlen üzenetről sem"</string>
|
<string name="screen_notification_optin_title">"Értesítések engedélyezése, hogy soha ne maradjon le egyetlen üzenetről sem"</string>
|
||||||
|
<string name="screen_qr_code_login_connecting_subtitle">"Kapcsolat létesítése"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_description">"Nem sikerült biztonságos kapcsolatot létesíteni az új eszközzel. A meglévő eszközei továbbra is biztonságban vannak, és nem kell aggódnia miattuk."</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Most mi lesz?"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Próbáljon meg újra bejelentkezni egy QR-kóddal, ha ez hálózati probléma volt."</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Ha ugyanezzel a problémával találkozik, próbálkozzon másik Wi-Fi-hálózattal, vagy a Wi-Fi helyett használja a mobil-adatkapcsolatát"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Ha ez nem működik, jelentkezzen be kézileg"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"A kapcsolat nem biztonságos"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_1">"Nyissa meg az %1$set egy asztali eszközön"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_2">"Kattintson a profilképére"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_3">"Válassza ezt: %1$s"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_3_action">"„Új eszköz összekapcsolása”"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_4">"Válassza ezt: %1$s"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_4_action">"„QR-kód megjelenítése”"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_title">"Nyissa meg az %1$set egy másik eszközön a QR-kód lekéréséhez."</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Használja a másik eszközön látható QR-kódot."</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Próbálja újra"</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Hibás QR-kód"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_button">"Ugrás a kamerabeállításokhoz"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_state_description">"A folytatáshoz engedélyeznie kell, hogy az Element használhassa az eszköz kameráját."</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_state_title">"Engedélyezze a kamera elérését a QR-kód beolvasásához"</string>
|
||||||
|
<string name="screen_qr_code_login_scanning_state_title">"Olvassa be a QR-kódot"</string>
|
||||||
|
<string name="screen_qr_code_login_start_over_button">"Újrakezdés"</string>
|
||||||
|
<string name="screen_qr_code_login_unknown_error_description">"Váratlan hiba történt. Próbálja meg újra."</string>
|
||||||
<string name="screen_welcome_bullet_1">"A hívások, szavazások, keresések és egyebek az év további részében kerülnek hozzáadásra."</string>
|
<string name="screen_welcome_bullet_1">"A hívások, szavazások, keresések és egyebek az év további részében kerülnek hozzáadásra."</string>
|
||||||
<string name="screen_welcome_bullet_2">"A titkosított szobák üzenetelőzményei nem lesznek elérhetők ebben a frissítésben."</string>
|
<string name="screen_welcome_bullet_2">"A titkosított szobák üzenetelőzményei nem lesznek elérhetők ebben a frissítésben."</string>
|
||||||
<string name="screen_welcome_bullet_3">"Szeretnénk hallani a véleményét, ossza meg velünk a beállítások oldalon."</string>
|
<string name="screen_welcome_bullet_3">"Szeretnénk hallani a véleményét, ossza meg velünk a beállítások oldalon."</string>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_notification_optin_subtitle">"Anda dapat mengubah pengaturan Anda nanti."</string>
|
<string name="screen_notification_optin_subtitle">"Anda dapat mengubah pengaturan Anda nanti."</string>
|
||||||
<string name="screen_notification_optin_title">"Izinkan pemberitahuan dan jangan pernah melewatkan pesan"</string>
|
<string name="screen_notification_optin_title">"Izinkan pemberitahuan dan jangan pernah melewatkan pesan"</string>
|
||||||
|
<string name="screen_qr_code_login_connecting_subtitle">"Membuat koneksi"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_1">"Buka %1$s di perangkat desktop"</string>
|
<string name="screen_qr_code_login_initial_state_item_1">"Buka %1$s di perangkat desktop"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_2">"Klik pada avatar Anda"</string>
|
<string name="screen_qr_code_login_initial_state_item_2">"Klik pada avatar Anda"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3">"Pilih %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_3">"Pilih %1$s"</string>
|
||||||
|
|
@ -9,6 +10,10 @@
|
||||||
<string name="screen_qr_code_login_initial_state_item_4">"Pilih %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_4">"Pilih %1$s"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4_action">"“Tampilkan kode QR”"</string>
|
<string name="screen_qr_code_login_initial_state_item_4_action">"“Tampilkan kode QR”"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_title">"Buka %1$s di perangkat lain untuk mendapatkan kode QR"</string>
|
<string name="screen_qr_code_login_initial_state_title">"Buka %1$s di perangkat lain untuk mendapatkan kode QR"</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Gunakan kode QR yang ditampilkan di perangkat lain."</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Coba lagi"</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Kode QR salah"</string>
|
||||||
|
<string name="screen_qr_code_login_scanning_state_title">"Pindai kode QR"</string>
|
||||||
<string name="screen_welcome_bullet_1">"Panggilan, pemungutan suara, pencarian, dan lainnya akan ditambahkan di tahun ini."</string>
|
<string name="screen_welcome_bullet_1">"Panggilan, pemungutan suara, pencarian, dan lainnya akan ditambahkan di tahun ini."</string>
|
||||||
<string name="screen_welcome_bullet_2">"Riwayat pesan untuk ruangan terenkripsi tidak akan tersedia dalam pembaruan ini."</string>
|
<string name="screen_welcome_bullet_2">"Riwayat pesan untuk ruangan terenkripsi tidak akan tersedia dalam pembaruan ini."</string>
|
||||||
<string name="screen_welcome_bullet_3">"Kami ingin mendengar dari Anda, beri tahu kami pendapat Anda melalui halaman pengaturan."</string>
|
<string name="screen_welcome_bullet_3">"Kami ingin mendengar dari Anda, beri tahu kami pendapat Anda melalui halaman pengaturan."</string>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_notification_optin_subtitle">"Potrai modificare le tue impostazioni in seguito."</string>
|
<string name="screen_notification_optin_subtitle">"Potrai modificare le tue impostazioni in seguito."</string>
|
||||||
<string name="screen_notification_optin_title">"Consenti le notifiche e non perdere mai un messaggio"</string>
|
<string name="screen_notification_optin_title">"Consenti le notifiche e non perdere mai un messaggio"</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Riprova"</string>
|
||||||
<string name="screen_welcome_bullet_1">"Chiamate, sondaggi, ricerche e altro ancora saranno aggiunti nel corso dell\'anno."</string>
|
<string name="screen_welcome_bullet_1">"Chiamate, sondaggi, ricerche e altro ancora saranno aggiunti nel corso dell\'anno."</string>
|
||||||
<string name="screen_welcome_bullet_2">"La cronologia dei messaggi per le stanze crittografate non è ancora disponibile."</string>
|
<string name="screen_welcome_bullet_2">"La cronologia dei messaggi per le stanze crittografate non è ancora disponibile."</string>
|
||||||
<string name="screen_welcome_bullet_3">"Ci piacerebbe sentire il tuo parere, facci sapere cosa ne pensi tramite la pagina delle impostazioni."</string>
|
<string name="screen_welcome_bullet_3">"Ci piacerebbe sentire il tuo parere, facci sapere cosa ne pensi tramite la pagina delle impostazioni."</string>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_notification_optin_subtitle">"Puteți modifica setările mai târziu."</string>
|
<string name="screen_notification_optin_subtitle">"Puteți modifica setările mai târziu."</string>
|
||||||
<string name="screen_notification_optin_title">"Permiteți notificările și nu pierdeți niciodată un mesaj"</string>
|
<string name="screen_notification_optin_title">"Permiteți notificările și nu pierdeți niciodată un mesaj"</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Încercați din nou"</string>
|
||||||
<string name="screen_welcome_bullet_1">"Apelurile, sondajele, căutare și multe altele vor fi adăugate în cursul acestui an."</string>
|
<string name="screen_welcome_bullet_1">"Apelurile, sondajele, căutare și multe altele vor fi adăugate în cursul acestui an."</string>
|
||||||
<string name="screen_welcome_bullet_2">"Istoricul mesajelor pentru camerele criptate nu va fi disponibil în această actualizare."</string>
|
<string name="screen_welcome_bullet_2">"Istoricul mesajelor pentru camerele criptate nu va fi disponibil în această actualizare."</string>
|
||||||
<string name="screen_welcome_bullet_3">"Ne-ar plăcea să auzim de la dumneavoastră, spuneți-ne ce părere aveți prin intermediul paginii de setări."</string>
|
<string name="screen_welcome_bullet_3">"Ne-ar plăcea să auzim de la dumneavoastră, spuneți-ne ce părere aveți prin intermediul paginii de setări."</string>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,13 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_notification_optin_subtitle">"Вы можете изменить настройки позже."</string>
|
<string name="screen_notification_optin_subtitle">"Вы можете изменить настройки позже."</string>
|
||||||
<string name="screen_notification_optin_title">"Разрешите уведомления и никогда не пропустите сообщение"</string>
|
<string name="screen_notification_optin_title">"Разрешите уведомления и никогда не пропустите сообщение"</string>
|
||||||
|
<string name="screen_qr_code_login_connecting_subtitle">"Установление соединения"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_description">"Не удалось установить безопасное соединение с новым устройством. Существующие устройства по-прежнему в безопасности, и вам не нужно беспокоиться о них."</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Что теперь?"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Попробуйте снова войти в систему с помощью QR-кода, если это была сетевая проблема"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Если вы столкнулись с той же проблемой, попробуйте сменить точку доступа Wi-Fi или используйте мобильные данные"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Если это не помогло, войдите вручную"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"Соединение не защищено"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_1">"Откройте %1$s на настольном устройстве"</string>
|
<string name="screen_qr_code_login_initial_state_item_1">"Откройте %1$s на настольном устройстве"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_2">"Нажмите на свое изображение"</string>
|
<string name="screen_qr_code_login_initial_state_item_2">"Нажмите на свое изображение"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3">"Выбрать %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_3">"Выбрать %1$s"</string>
|
||||||
|
|
@ -9,6 +16,15 @@
|
||||||
<string name="screen_qr_code_login_initial_state_item_4">"Выбрать %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_4">"Выбрать %1$s"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4_action">"\"Показать QR-код\""</string>
|
<string name="screen_qr_code_login_initial_state_item_4_action">"\"Показать QR-код\""</string>
|
||||||
<string name="screen_qr_code_login_initial_state_title">"Откройте %1$s на другом устройстве, чтобы получить QR-код"</string>
|
<string name="screen_qr_code_login_initial_state_title">"Откройте %1$s на другом устройстве, чтобы получить QR-код"</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Используйте QR-код, показанный на другом устройстве."</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Повторить попытку"</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Неверный QR-код"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_button">"Перейдите в настройки камеры"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_state_description">"Чтобы продолжить, вам необходимо разрешить Element использовать камеру вашего устройства."</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_state_title">"Разрешите доступ к камере для сканирования QR-кода"</string>
|
||||||
|
<string name="screen_qr_code_login_scanning_state_title">"Сканировать QR-код"</string>
|
||||||
|
<string name="screen_qr_code_login_start_over_button">"Начать заново"</string>
|
||||||
|
<string name="screen_qr_code_login_unknown_error_description">"Произошла непредвиденная ошибка. Пожалуйста, попробуйте еще раз."</string>
|
||||||
<string name="screen_welcome_bullet_1">"Звонки, опросы, поиск и многое другое будут добавлены позже в этом году."</string>
|
<string name="screen_welcome_bullet_1">"Звонки, опросы, поиск и многое другое будут добавлены позже в этом году."</string>
|
||||||
<string name="screen_welcome_bullet_2">"История сообщений для зашифрованных комнат в этом обновлении будет недоступна."</string>
|
<string name="screen_welcome_bullet_2">"История сообщений для зашифрованных комнат в этом обновлении будет недоступна."</string>
|
||||||
<string name="screen_welcome_bullet_3">"Мы будем рады услышать ваше мнение, сообщите нам об этом через страницу настроек."</string>
|
<string name="screen_welcome_bullet_3">"Мы будем рады услышать ваше мнение, сообщите нам об этом через страницу настроек."</string>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,15 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_notification_optin_subtitle">"Svoje nastavenia môžete neskôr zmeniť."</string>
|
<string name="screen_notification_optin_subtitle">"Svoje nastavenia môžete neskôr zmeniť."</string>
|
||||||
<string name="screen_notification_optin_title">"Povoľte oznámenia a nikdy nezmeškajte žiadnu správu"</string>
|
<string name="screen_notification_optin_title">"Povoľte oznámenia a nikdy nezmeškajte žiadnu správu"</string>
|
||||||
|
<string name="screen_qr_code_login_connecting_subtitle">"Nadväzovanie spojenia"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_description">"K novému zariadeniu sa nepodarilo vytvoriť bezpečné pripojenie. Vaše existujúce zariadenia sú stále v bezpečí a nemusíte sa o ne obávať."</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Čo teraz?"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Skúste sa znova prihlásiť pomocou QR kódu v prípade, že ide o problém so sieťou"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Ak narazíte na rovnaký problém, vyskúšajte inú sieť Wi-Fi alebo namiesto siete Wi-Fi použite mobilné dáta"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Ak to nefunguje, prihláste sa manuálne"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"Pripojenie nie je bezpečené"</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_subtitle">"Budete vyzvaní na zadanie dvoch číslic uvedených nižšie."</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_title">"Zadajte číslo na svojom zariadení"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_1">"Otvorte %1$s na stolnom zariadení"</string>
|
<string name="screen_qr_code_login_initial_state_item_1">"Otvorte %1$s na stolnom zariadení"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_2">"Kliknite na svoj obrázok"</string>
|
<string name="screen_qr_code_login_initial_state_item_2">"Kliknite na svoj obrázok"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3">"Vyberte %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_3">"Vyberte %1$s"</string>
|
||||||
|
|
@ -9,6 +18,18 @@
|
||||||
<string name="screen_qr_code_login_initial_state_item_4">"Vyberte %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_4">"Vyberte %1$s"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4_action">"„Zobraziť QR kód“"</string>
|
<string name="screen_qr_code_login_initial_state_item_4_action">"„Zobraziť QR kód“"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_title">"Ak chcete získať QR kód, otvorte %1$s na inom zariadení"</string>
|
<string name="screen_qr_code_login_initial_state_title">"Ak chcete získať QR kód, otvorte %1$s na inom zariadení"</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Použite QR kód zobrazený na druhom zariadení."</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Skúste to znova"</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Nesprávny QR kód"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_button">"Prejsť na nastavenia fotoaparátu"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_state_description">"Ak chcete pokračovať, musíte udeliť povolenie aplikácii Element používať fotoaparát vášho zariadenia."</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_state_title">"Povoľte prístup k fotoaparátu na naskenovanie QR kódu"</string>
|
||||||
|
<string name="screen_qr_code_login_scanning_state_title">"Naskenovať QR kód"</string>
|
||||||
|
<string name="screen_qr_code_login_start_over_button">"Začať odznova"</string>
|
||||||
|
<string name="screen_qr_code_login_unknown_error_description">"Vyskytla sa neočakávaná chyba. Prosím, skúste to znova."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_loading">"Čaká sa na vaše druhé zariadenie"</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_subtitle">"Váš poskytovateľ účtu môže požiadať o nasledujúci kód na overenie prihlásenia."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_title">"Váš overovací kód"</string>
|
||||||
<string name="screen_welcome_bullet_1">"Hovory, ankety, vyhľadávanie a ďalšie funkcie pribudnú neskôr v tomto roku."</string>
|
<string name="screen_welcome_bullet_1">"Hovory, ankety, vyhľadávanie a ďalšie funkcie pribudnú neskôr v tomto roku."</string>
|
||||||
<string name="screen_welcome_bullet_2">"História správ pre zašifrované miestnosti nebude v tejto aktualizácii k dispozícii."</string>
|
<string name="screen_welcome_bullet_2">"História správ pre zašifrované miestnosti nebude v tejto aktualizácii k dispozícii."</string>
|
||||||
<string name="screen_welcome_bullet_3">"Radi by sme od vás počuli, dajte nám vedieť, čo si myslíte, prostredníctvom stránky nastavení."</string>
|
<string name="screen_welcome_bullet_3">"Radi by sme od vás počuli, dajte nám vedieť, čo si myslíte, prostredníctvom stránky nastavení."</string>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_notification_optin_subtitle">"Ви можете змінити свої налаштування пізніше."</string>
|
<string name="screen_notification_optin_subtitle">"Ви можете змінити свої налаштування пізніше."</string>
|
||||||
<string name="screen_notification_optin_title">"Дозволити сповіщення і ніколи не пропускати повідомлення"</string>
|
<string name="screen_notification_optin_title">"Дозволити сповіщення і ніколи не пропускати повідомлення"</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Спробуйте ще раз"</string>
|
||||||
<string name="screen_welcome_bullet_1">"Дзвінки, опитування, пошук тощо будуть додані пізніше цього року."</string>
|
<string name="screen_welcome_bullet_1">"Дзвінки, опитування, пошук тощо будуть додані пізніше цього року."</string>
|
||||||
<string name="screen_welcome_bullet_2">"Історія повідомлень для зашифрованих кімнат ще недоступна."</string>
|
<string name="screen_welcome_bullet_2">"Історія повідомлень для зашифрованих кімнат ще недоступна."</string>
|
||||||
<string name="screen_welcome_bullet_3">"Ми хотіли б почути вас, розкажіть нам ваші враження та ідеї щодо застосунку на сторінці налаштувань."</string>
|
<string name="screen_welcome_bullet_3">"Ми хотіли б почути вас, розкажіть нам ваші враження та ідеї щодо застосунку на сторінці налаштувань."</string>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"再試一次"</string>
|
||||||
<string name="screen_welcome_bullet_1">"通話、投票、搜尋等更多功能將在今年登場。"</string>
|
<string name="screen_welcome_bullet_1">"通話、投票、搜尋等更多功能將在今年登場。"</string>
|
||||||
<string name="screen_welcome_bullet_2">"在這次的更新,您無法查看聊天室內被加密的歷史訊息。"</string>
|
<string name="screen_welcome_bullet_2">"在這次的更新,您無法查看聊天室內被加密的歷史訊息。"</string>
|
||||||
<string name="screen_welcome_bullet_3">"我們很樂意聽取您的意見,請到設定頁面告訴我們您的想法。"</string>
|
<string name="screen_welcome_bullet_3">"我們很樂意聽取您的意見,請到設定頁面告訴我們您的想法。"</string>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,14 @@
|
||||||
<string name="screen_notification_optin_subtitle">"You can change your settings later."</string>
|
<string name="screen_notification_optin_subtitle">"You can change your settings later."</string>
|
||||||
<string name="screen_notification_optin_title">"Allow notifications and never miss a message"</string>
|
<string name="screen_notification_optin_title">"Allow notifications and never miss a message"</string>
|
||||||
<string name="screen_qr_code_login_connecting_subtitle">"Establishing connection"</string>
|
<string name="screen_qr_code_login_connecting_subtitle">"Establishing connection"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_description">"A secure connection could not be made to the new device. Your existing devices are still safe and you don\'t need to worry about them."</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"What now?"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Try signing in again with a QR code in case this was a network problem"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"If you encounter the same problem, try a different wifi network or use your mobile data instead of wifi"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"If that doesn’t work, sign in manually"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"Connection not secure"</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_subtitle">"You’ll be asked to enter the two digits shown below."</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_title">"Enter number on your device"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_1">"Open %1$s on a desktop device"</string>
|
<string name="screen_qr_code_login_initial_state_item_1">"Open %1$s on a desktop device"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_2">"Click on your avatar"</string>
|
<string name="screen_qr_code_login_initial_state_item_2">"Click on your avatar"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3">"Select %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_3">"Select %1$s"</string>
|
||||||
|
|
@ -11,9 +19,17 @@
|
||||||
<string name="screen_qr_code_login_initial_state_item_4_action">"“Show QR code”"</string>
|
<string name="screen_qr_code_login_initial_state_item_4_action">"“Show QR code”"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_title">"Open %1$s on another device to get the QR code"</string>
|
<string name="screen_qr_code_login_initial_state_title">"Open %1$s on another device to get the QR code"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Use the QR code shown on the other device."</string>
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Use the QR code shown on the other device."</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Try Again"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Try again"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Wrong QR code"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Wrong QR code"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_button">"Go to camera settings"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_state_description">"You need to give permission for Element to use your device’s camera in order to continue."</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_state_title">"Allow camera access to scan the QR code"</string>
|
||||||
<string name="screen_qr_code_login_scanning_state_title">"Scan the QR code"</string>
|
<string name="screen_qr_code_login_scanning_state_title">"Scan the QR code"</string>
|
||||||
|
<string name="screen_qr_code_login_start_over_button">"Start over"</string>
|
||||||
|
<string name="screen_qr_code_login_unknown_error_description">"An unexpected error occurred. Please try again."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_loading">"Waiting for your other device"</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_subtitle">"Your account provider may ask for the following code to verify the sign in."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_title">"Your verification code"</string>
|
||||||
<string name="screen_welcome_bullet_1">"Calls, polls, search and more will be added later this year."</string>
|
<string name="screen_welcome_bullet_1">"Calls, polls, search and more will be added later this year."</string>
|
||||||
<string name="screen_welcome_bullet_2">"Message history for encrypted rooms isn’t available yet."</string>
|
<string name="screen_welcome_bullet_2">"Message history for encrypted rooms isn’t available yet."</string>
|
||||||
<string name="screen_welcome_bullet_3">"We’d love to hear from you, let us know what you think via the settings page."</string>
|
<string name="screen_welcome_bullet_3">"We’d love to hear from you, let us know what you think via the settings page."</string>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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">"Далучыцца да пакоя"</string>
|
||||||
|
<string name="screen_join_room_knock_action">"Націсніце, каб далучыцца"</string>
|
||||||
|
<string name="screen_join_room_subtitle_knock">"Націсніце кнопку ніжэй, і адміністратар пакоя атрымае апавяшчэнне. Вы зможаце далучыцца да размовы пасля зацвярджэння."</string>
|
||||||
|
<string name="screen_join_room_subtitle_no_preview">"Вы павінны быць удзельнікам гэтага пакоя каб прагледзець гісторыю паведамленняў."</string>
|
||||||
|
<string name="screen_join_room_title_knock">"Вы хочаце далучыцца да гэтага пакоя?"</string>
|
||||||
|
<string name="screen_join_room_title_no_preview">"Папярэдні прагляд недаступны"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -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">"Připojit se do místnosti"</string>
|
||||||
|
<string name="screen_join_room_knock_action">"Zaklepejte a připojte se"</string>
|
||||||
|
<string name="screen_join_room_subtitle_knock">"Klikněte na tlačítko níže a správce místnosti bude informován. Po schválení se budete moci připojit ke konverzaci."</string>
|
||||||
|
<string name="screen_join_room_subtitle_no_preview">"Pro zobrazení historie zpráv musíte být členem této místnosti."</string>
|
||||||
|
<string name="screen_join_room_title_knock">"Chcete se připojit k této místnosti?"</string>
|
||||||
|
<string name="screen_join_room_title_no_preview">"Náhled není k dispozici"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -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">"Raum beitreten"</string>
|
||||||
|
<string name="screen_join_room_knock_action">"Anklopfen"</string>
|
||||||
|
<string name="screen_join_room_subtitle_knock">"Klopfe an um einen Raumadministrator zu benachrichtigen. Nach der Freigabe kannst du dich an der Unterhaltung beteiligen."</string>
|
||||||
|
<string name="screen_join_room_subtitle_no_preview">"Du musst Mitglied in diesem Raum sein, um den Nachrichtenverlauf zu sehen."</string>
|
||||||
|
<string name="screen_join_room_title_knock">"Willst du diesem Raum beitreten?"</string>
|
||||||
|
<string name="screen_join_room_title_no_preview">"Vorschau nicht verfügbar"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -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">"Rejoindre"</string>
|
||||||
|
<string name="screen_join_room_knock_action">"Demander à joindre"</string>
|
||||||
|
<string name="screen_join_room_subtitle_knock">"Cliquez ci-dessous et un administrateur sera prévenu. Une fois votre demande approuvée, pour pourrez rejoindre la discussion."</string>
|
||||||
|
<string name="screen_join_room_subtitle_no_preview">"Vous devez être un membre du salon pour pouvoir lire l’historique des messages."</string>
|
||||||
|
<string name="screen_join_room_title_knock">"Vous souhaitez rejoindre ce salon?"</string>
|
||||||
|
<string name="screen_join_room_title_no_preview">"La prévisualisation n’est pas disponible"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -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">"Csatlakozás a szobához"</string>
|
||||||
|
<string name="screen_join_room_knock_action">"Kopogtasson a csatlakozáshoz"</string>
|
||||||
|
<string name="screen_join_room_subtitle_knock">"Kattintson az alábbi gombra, és a szoba adminisztrátora értesítést kap. A jóváhagyást követően csatlakozhat a beszélgetéshez."</string>
|
||||||
|
<string name="screen_join_room_subtitle_no_preview">"Az üzenetelőzmények megtekintéséhez a szoba tagjának kell lennie."</string>
|
||||||
|
<string name="screen_join_room_title_knock">"Csatlakozna ehhez a szobához?"</string>
|
||||||
|
<string name="screen_join_room_title_no_preview">"Az előnézet nem érhető el"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -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">"Присоединиться к комнате"</string>
|
||||||
|
<string name="screen_join_room_knock_action">"Постучите, чтобы присоединиться"</string>
|
||||||
|
<string name="screen_join_room_subtitle_knock">"Нажмите кнопку ниже и администратор комнаты получит уведомление. После одобрения вы сможете присоединиться к обсуждению."</string>
|
||||||
|
<string name="screen_join_room_subtitle_no_preview">"Вы должны быть участником этой комнаты, чтобы просмотреть историю сообщений."</string>
|
||||||
|
<string name="screen_join_room_title_knock">"Хотите присоединиться к этой комнате?"</string>
|
||||||
|
<string name="screen_join_room_title_no_preview">"Предварительный просмотр недоступен"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -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">"Pripojiť sa do miestnosti"</string>
|
||||||
|
<string name="screen_join_room_knock_action">"Zaklopaním sa pripojíte"</string>
|
||||||
|
<string name="screen_join_room_subtitle_knock">"Kliknite na tlačidlo nižšie a správca miestnosti bude informovaný. Po schválení sa budete môcť pripojiť ku konverzácii."</string>
|
||||||
|
<string name="screen_join_room_subtitle_no_preview">"Ak chcete zobraziť históriu správ, musíte byť členom tejto miestnosti."</string>
|
||||||
|
<string name="screen_join_room_title_knock">"Chcete sa pripojiť do tejto miestnosti?"</string>
|
||||||
|
<string name="screen_join_room_title_no_preview">"Náhľad nie je k dispozícii"</string>
|
||||||
|
</resources>
|
||||||
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!")) }
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
<string name="screen_app_lock_setup_pin_blacklisted_dialog_title">"Выберите другой PIN-код"</string>
|
<string name="screen_app_lock_setup_pin_blacklisted_dialog_title">"Выберите другой PIN-код"</string>
|
||||||
<string name="screen_app_lock_setup_pin_context">"Заблокируйте %1$s, чтобы повысить безопасность ваших чатов.
|
<string name="screen_app_lock_setup_pin_context">"Заблокируйте %1$s, чтобы повысить безопасность ваших чатов.
|
||||||
|
|
||||||
Выберите что-нибудь незабываемое. Если вы забудете этот PIN-код, вы выйдете из приложения."</string>
|
Введите что-нибудь незабываемое. Если вы забудете этот PIN-код, вы выйдете из приложения."</string>
|
||||||
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Повторите PIN-код"</string>
|
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Повторите PIN-код"</string>
|
||||||
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN-коды не совпадают"</string>
|
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN-коды не совпадают"</string>
|
||||||
<string name="screen_app_lock_signout_alert_message">"Чтобы продолжить, вам необходимо повторно войти в систему и создать новый PIN-код"</string>
|
<string name="screen_app_lock_signout_alert_message">"Чтобы продолжить, вам необходимо повторно войти в систему и создать новый PIN-код"</string>
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
package io.element.android.features.messages.impl
|
package io.element.android.features.messages.impl
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
@ -108,13 +107,19 @@ class MessagesNode @AssistedInject constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
url: String,
|
url: String,
|
||||||
) {
|
) {
|
||||||
when (val permalink = permalinkParser.parse(Uri.parse(url))) {
|
when (val permalink = permalinkParser.parse(url)) {
|
||||||
is PermalinkData.UserLink -> {
|
is PermalinkData.UserLink -> {
|
||||||
callback?.onUserDataClicked(UserId(permalink.userId))
|
callback?.onUserDataClicked(permalink.userId)
|
||||||
}
|
}
|
||||||
is PermalinkData.RoomLink -> {
|
is PermalinkData.RoomLink -> {
|
||||||
// TODO Implement room link handling
|
// TODO Implement room link handling
|
||||||
}
|
}
|
||||||
|
is PermalinkData.EventIdAliasLink -> {
|
||||||
|
// TODO Implement room and Event link handling
|
||||||
|
}
|
||||||
|
is PermalinkData.EventIdLink -> {
|
||||||
|
// TODO Implement room and Event link handling
|
||||||
|
}
|
||||||
is PermalinkData.FallbackLink,
|
is PermalinkData.FallbackLink,
|
||||||
is PermalinkData.RoomEmailInviteLink -> {
|
is PermalinkData.RoomEmailInviteLink -> {
|
||||||
context.openUrlInExternalApp(url)
|
context.openUrlInExternalApp(url)
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.messagecomposer
|
package io.element.android.features.messages.impl.messagecomposer
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.messages.impl.mentions.MentionSuggestion
|
import io.element.android.features.messages.impl.mentions.MentionSuggestion
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
@ -49,7 +48,6 @@ fun aMessageComposerState(
|
||||||
richTextEditorState = richTextEditorState,
|
richTextEditorState = richTextEditorState,
|
||||||
permalinkParser = object : PermalinkParser {
|
permalinkParser = object : PermalinkParser {
|
||||||
override fun parse(uriString: String): PermalinkData = TODO()
|
override fun parse(uriString: String): PermalinkData = TODO()
|
||||||
override fun parse(uri: Uri): PermalinkData = TODO()
|
|
||||||
},
|
},
|
||||||
isFullScreen = isFullScreen,
|
isFullScreen = isFullScreen,
|
||||||
mode = mode,
|
mode = mode,
|
||||||
|
|
|
||||||
|
|
@ -236,7 +236,7 @@ class MessagesPresenterTest {
|
||||||
val clipboardHelper = FakeClipboardHelper()
|
val clipboardHelper = FakeClipboardHelper()
|
||||||
val event = aMessageEvent()
|
val event = aMessageEvent()
|
||||||
val matrixRoom = FakeMatrixRoom(
|
val matrixRoom = FakeMatrixRoom(
|
||||||
permalinkResult = { Result.success("a link") },
|
eventPermalinkResult = { Result.success("a link") },
|
||||||
)
|
)
|
||||||
val presenter = createMessagesPresenter(
|
val presenter = createMessagesPresenter(
|
||||||
clipboardHelper = clipboardHelper,
|
clipboardHelper = clipboardHelper,
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import android.content.Context
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.bumble.appyx.core.lifecycle.subscribe
|
import com.bumble.appyx.core.lifecycle.subscribe
|
||||||
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
|
||||||
|
|
@ -35,6 +36,8 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
||||||
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.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import io.element.android.libraries.androidutils.R as AndroidUtilsR
|
import io.element.android.libraries.androidutils.R as AndroidUtilsR
|
||||||
|
|
||||||
|
|
@ -83,34 +86,35 @@ class RoomDetailsNode @AssistedInject constructor(
|
||||||
callbacks.forEach { it.openPollHistory() }
|
callbacks.forEach { it.openPollHistory() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onShareRoom(context: Context) {
|
private fun CoroutineScope.onShareRoom(context: Context) = launch {
|
||||||
val alias = room.alias ?: room.alternativeAliases.firstOrNull()
|
room.getPermalink()
|
||||||
val permalinkResult = alias?.let { permalinkBuilder.permalinkForRoomAlias(it) }
|
.onSuccess { permalink ->
|
||||||
?: permalinkBuilder.permalinkForRoomId(room.roomId)
|
context.startSharePlainTextIntent(
|
||||||
permalinkResult.onSuccess { permalink ->
|
activityResultLauncher = null,
|
||||||
context.startSharePlainTextIntent(
|
chooserTitle = context.getString(R.string.screen_room_details_share_room_title),
|
||||||
activityResultLauncher = null,
|
text = permalink,
|
||||||
chooserTitle = context.getString(R.string.screen_room_details_share_room_title),
|
noActivityFoundMessage = context.getString(AndroidUtilsR.string.error_no_compatible_app_found)
|
||||||
text = permalink,
|
)
|
||||||
noActivityFoundMessage = context.getString(AndroidUtilsR.string.error_no_compatible_app_found)
|
}
|
||||||
)
|
.onFailure {
|
||||||
}.onFailure {
|
Timber.e(it)
|
||||||
Timber.e(it)
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onShareMember(context: Context, member: RoomMember) {
|
private fun onShareMember(context: Context, member: RoomMember) {
|
||||||
val permalinkResult = permalinkBuilder.permalinkForUser(member.userId)
|
val permalinkResult = permalinkBuilder.permalinkForUser(member.userId)
|
||||||
permalinkResult.onSuccess { permalink ->
|
permalinkResult
|
||||||
context.startSharePlainTextIntent(
|
.onSuccess { permalink ->
|
||||||
activityResultLauncher = null,
|
context.startSharePlainTextIntent(
|
||||||
chooserTitle = context.getString(R.string.screen_room_details_share_room_title),
|
activityResultLauncher = null,
|
||||||
text = permalink,
|
chooserTitle = context.getString(R.string.screen_room_details_share_room_title),
|
||||||
noActivityFoundMessage = context.getString(AndroidUtilsR.string.error_no_compatible_app_found)
|
text = permalink,
|
||||||
)
|
noActivityFoundMessage = context.getString(AndroidUtilsR.string.error_no_compatible_app_found)
|
||||||
}.onFailure {
|
)
|
||||||
Timber.e(it)
|
}
|
||||||
}
|
.onFailure {
|
||||||
|
Timber.e(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onEditRoomDetails() {
|
private fun onEditRoomDetails() {
|
||||||
|
|
@ -131,7 +135,7 @@ class RoomDetailsNode @AssistedInject constructor(
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
|
|
||||||
fun onShareRoom() {
|
fun onShareRoom() {
|
||||||
this.onShareRoom(context)
|
lifecycleScope.onShareRoom(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onShareMember(roomMember: RoomMember) {
|
fun onShareMember(roomMember: RoomMember) {
|
||||||
|
|
|
||||||
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