Merge branch 'release/0.4.10' into main
This commit is contained in:
commit
0f08e8053a
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.
|
||||
steps:
|
||||
- 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)
|
||||
========================================
|
||||
|
||||
|
|
|
|||
|
|
@ -42,18 +42,20 @@ import dagger.assisted.AssistedInject
|
|||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.appnav.loggedin.LoggedInNode
|
||||
import io.element.android.appnav.room.RoomFlowNode
|
||||
import io.element.android.appnav.room.RoomLoadedFlowNode
|
||||
import io.element.android.appnav.room.RoomNavigationTarget
|
||||
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
|
||||
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
||||
import io.element.android.features.ftue.api.FtueEntryPoint
|
||||
import io.element.android.features.ftue.api.state.FtueService
|
||||
import io.element.android.features.ftue.api.state.FtueState
|
||||
import io.element.android.features.invitelist.api.InviteListEntryPoint
|
||||
import io.element.android.features.invite.api.InviteListEntryPoint
|
||||
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
||||
import io.element.android.features.lockscreen.api.LockScreenLockState
|
||||
import io.element.android.features.lockscreen.api.LockScreenService
|
||||
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||
import io.element.android.features.preferences.api.PreferencesEntryPoint
|
||||
import io.element.android.features.roomdirectory.api.RoomDescription
|
||||
import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint
|
||||
import io.element.android.features.roomlist.api.RoomListEntryPoint
|
||||
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
|
||||
|
|
@ -82,6 +84,7 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import timber.log.Timber
|
||||
import java.util.Optional
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
class LoggedInFlowNode @AssistedInject constructor(
|
||||
|
|
@ -213,7 +216,8 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
@Parcelize
|
||||
data class Room(
|
||||
val roomId: RoomId,
|
||||
val initialElement: RoomLoadedFlowNode.NavTarget = RoomLoadedFlowNode.NavTarget.Messages
|
||||
val roomDescription: RoomDescription? = null,
|
||||
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages
|
||||
) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
|
|
@ -273,7 +277,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
}
|
||||
|
||||
override fun onRoomSettingsClicked(roomId: RoomId) {
|
||||
backstack.push(NavTarget.Room(roomId, initialElement = RoomLoadedFlowNode.NavTarget.RoomDetails))
|
||||
backstack.push(NavTarget.Room(roomId, initialElement = RoomNavigationTarget.Details))
|
||||
}
|
||||
|
||||
override fun onReportBugClicked() {
|
||||
|
|
@ -290,7 +294,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
.build()
|
||||
}
|
||||
is NavTarget.Room -> {
|
||||
val callback = object : RoomLoadedFlowNode.Callback {
|
||||
val callback = object : JoinedRoomLoadedFlowNode.Callback {
|
||||
override fun onOpenRoom(roomId: RoomId) {
|
||||
backstack.push(NavTarget.Room(roomId))
|
||||
}
|
||||
|
|
@ -303,7 +307,11 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
backstack.push(NavTarget.Settings(PreferencesEntryPoint.InitialTarget.NotificationSettings))
|
||||
}
|
||||
}
|
||||
val inputs = RoomFlowNode.Inputs(roomId = navTarget.roomId, initialElement = navTarget.initialElement)
|
||||
val inputs = RoomFlowNode.Inputs(
|
||||
roomId = navTarget.roomId,
|
||||
roomDescription = Optional.ofNullable(navTarget.roomDescription),
|
||||
initialElement = navTarget.initialElement
|
||||
)
|
||||
createNode<RoomFlowNode>(buildContext, plugins = listOf(inputs, callback))
|
||||
}
|
||||
is NavTarget.Settings -> {
|
||||
|
|
@ -317,7 +325,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
}
|
||||
|
||||
override fun onOpenRoomNotificationSettings(roomId: RoomId) {
|
||||
backstack.push(NavTarget.Room(roomId, initialElement = RoomLoadedFlowNode.NavTarget.RoomNotificationSettings))
|
||||
backstack.push(NavTarget.Room(roomId, initialElement = RoomNavigationTarget.NotificationSettings))
|
||||
}
|
||||
}
|
||||
val inputs = PreferencesEntryPoint.Params(navTarget.initialElement)
|
||||
|
|
@ -349,6 +357,10 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
backstack.pop()
|
||||
}
|
||||
|
||||
override fun onInviteClicked(roomId: RoomId) {
|
||||
backstack.push(NavTarget.Room(roomId))
|
||||
}
|
||||
|
||||
override fun onInviteAccepted(roomId: RoomId) {
|
||||
backstack.push(NavTarget.Room(roomId))
|
||||
}
|
||||
|
|
@ -370,8 +382,12 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
NavTarget.RoomDirectorySearch -> {
|
||||
roomDirectoryEntryPoint.nodeBuilder(this, buildContext)
|
||||
.callback(object : RoomDirectoryEntryPoint.Callback {
|
||||
override fun onOpenRoom(roomId: RoomId) {
|
||||
coroutineScope.launch { attachRoom(roomId) }
|
||||
override fun onRoomJoined(roomId: RoomId) {
|
||||
backstack.push(NavTarget.Room(roomId))
|
||||
}
|
||||
|
||||
override fun onResultClicked(roomDescription: RoomDescription) {
|
||||
backstack.push(NavTarget.Room(roomDescription.roomId, roomDescription))
|
||||
}
|
||||
})
|
||||
.build()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
|
@ -14,15 +14,13 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
package io.element.android.appnav.room
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
|
|
@ -36,102 +34,109 @@ 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.features.networkmonitor.api.NetworkMonitor
|
||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||
import io.element.android.appnav.room.joined.JoinedRoomFlowNode
|
||||
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
|
||||
import io.element.android.features.joinroom.api.JoinRoomEntryPoint
|
||||
import io.element.android.features.roomdirectory.api.RoomDescription
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import 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.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import timber.log.Timber
|
||||
import java.util.Optional
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
class RoomFlowNode @AssistedInject constructor(
|
||||
@Assisted val buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
loadingRoomStateFlowFactory: LoadingRoomStateFlowFactory,
|
||||
private val networkMonitor: NetworkMonitor,
|
||||
) :
|
||||
BaseFlowNode<RoomFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Loading,
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
plugins = plugins
|
||||
) {
|
||||
private val client: MatrixClient,
|
||||
private val roomMembershipObserver: RoomMembershipObserver,
|
||||
private val joinRoomEntryPoint: JoinRoomEntryPoint,
|
||||
) : BaseFlowNode<RoomFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Loading,
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
plugins = plugins
|
||||
) {
|
||||
data class Inputs(
|
||||
val roomId: RoomId,
|
||||
val initialElement: RoomLoadedFlowNode.NavTarget = RoomLoadedFlowNode.NavTarget.Messages,
|
||||
val roomDescription: Optional<RoomDescription>,
|
||||
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages,
|
||||
) : NodeInputs
|
||||
|
||||
private val inputs: Inputs = inputs()
|
||||
private val loadingRoomStateStateFlow = loadingRoomStateFlowFactory.create(lifecycleScope, inputs.roomId)
|
||||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
data object Loading : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object Loaded : NavTarget
|
||||
data object JoinRoom : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object JoinedRoom : NavTarget
|
||||
}
|
||||
|
||||
override fun onBuilt() {
|
||||
super.onBuilt()
|
||||
loadingRoomStateStateFlow
|
||||
.map {
|
||||
it is LoadingRoomState.Loaded
|
||||
client.getRoomInfoFlow(
|
||||
inputs.roomId
|
||||
).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 ->
|
||||
if (isLoaded) {
|
||||
backstack.newRoot(NavTarget.Loaded)
|
||||
} else {
|
||||
backstack.newRoot(NavTarget.Loading)
|
||||
}
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
|
||||
// When leaving the room from this session only, navigate up.
|
||||
roomMembershipObserver.updates
|
||||
.filter { update -> update.roomId == inputs.roomId && !update.isUserInRoom }
|
||||
.onEach {
|
||||
navigateUp()
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
NavTarget.Loaded -> {
|
||||
val roomFlowNodeCallback = plugins<RoomLoadedFlowNode.Callback>()
|
||||
val awaitRoomState = loadingRoomStateStateFlow.value
|
||||
if (awaitRoomState is LoadingRoomState.Loaded) {
|
||||
val inputs = RoomLoadedFlowNode.Inputs(awaitRoomState.room, initialElement = inputs.initialElement)
|
||||
createNode<RoomLoadedFlowNode>(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback)
|
||||
} else {
|
||||
loadingNode(buildContext, this::navigateUp)
|
||||
}
|
||||
NavTarget.Loading -> loadingNode(buildContext)
|
||||
NavTarget.JoinRoom -> {
|
||||
val inputs = JoinRoomEntryPoint.Inputs(inputs.roomId, roomDescription = inputs.roomDescription)
|
||||
joinRoomEntryPoint.createNode(this, buildContext, inputs)
|
||||
}
|
||||
NavTarget.Loading -> {
|
||||
loadingNode(buildContext, this::navigateUp)
|
||||
NavTarget.JoinedRoom -> {
|
||||
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 ->
|
||||
val loadingRoomState by loadingRoomStateStateFlow.collectAsState()
|
||||
val networkStatus by networkMonitor.connectivity.collectAsState()
|
||||
LoadingRoomNodeView(
|
||||
state = loadingRoomState,
|
||||
hasNetworkConnection = networkStatus == NetworkStatus.Online,
|
||||
modifier = modifier,
|
||||
onBackClicked = onBackClicked
|
||||
)
|
||||
private fun loadingNode(buildContext: BuildContext) = node(buildContext) {
|
||||
Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
BackstackView(
|
||||
transitionHandler = JumpToEndTransitionHandler(),
|
||||
)
|
||||
BackstackView(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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.appnav.room
|
||||
package io.element.android.appnav.room.joined
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -32,6 +32,7 @@ import dagger.assisted.Assisted
|
|||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.appnav.di.RoomComponentFactory
|
||||
import io.element.android.appnav.room.RoomNavigationTarget
|
||||
import io.element.android.features.messages.api.MessagesEntryPoint
|
||||
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
|
|
@ -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.UserId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import timber.log.Timber
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
class RoomLoadedFlowNode @AssistedInject constructor(
|
||||
class JoinedRoomLoadedFlowNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val messagesEntryPoint: MessagesEntryPoint,
|
||||
|
|
@ -64,10 +61,13 @@ class RoomLoadedFlowNode @AssistedInject constructor(
|
|||
private val appCoroutineScope: CoroutineScope,
|
||||
private val matrixClient: MatrixClient,
|
||||
roomComponentFactory: RoomComponentFactory,
|
||||
roomMembershipObserver: RoomMembershipObserver,
|
||||
) : BaseFlowNode<RoomLoadedFlowNode.NavTarget>(
|
||||
) : BaseFlowNode<JoinedRoomLoadedFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = plugins.filterIsInstance(Inputs::class.java).first().initialElement,
|
||||
initialElement = when (plugins.filterIsInstance(Inputs::class.java).first().initialElement) {
|
||||
RoomNavigationTarget.Messages -> NavTarget.Messages
|
||||
RoomNavigationTarget.Details -> NavTarget.RoomDetails
|
||||
RoomNavigationTarget.NotificationSettings -> NavTarget.RoomNotificationSettings
|
||||
},
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
|
|
@ -81,7 +81,7 @@ class RoomLoadedFlowNode @AssistedInject constructor(
|
|||
|
||||
data class Inputs(
|
||||
val room: MatrixRoom,
|
||||
val initialElement: NavTarget = NavTarget.Messages,
|
||||
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages,
|
||||
) : NodeInputs
|
||||
|
||||
private val inputs: Inputs = inputs()
|
||||
|
|
@ -111,13 +111,6 @@ class RoomLoadedFlowNode @AssistedInject constructor(
|
|||
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 {
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.appnav.room
|
||||
package io.element.android.appnav.room.joined
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.appnav.room
|
||||
package io.element.android.appnav.room.joined
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
|
@ -27,12 +27,11 @@ import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
|
|||
import com.bumble.appyx.testing.unit.common.helper.parentNodeTestHelper
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.appnav.di.RoomComponentFactory
|
||||
import io.element.android.appnav.room.RoomLoadedFlowNode
|
||||
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
|
||||
import io.element.android.features.messages.api.MessagesEntryPoint
|
||||
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
||||
import io.element.android.libraries.architecture.childNode
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
|
||||
|
|
@ -41,7 +40,7 @@ import kotlinx.coroutines.test.runTest
|
|||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class RoomFlowNodeTest {
|
||||
class JoinRoomLoadedFlowNodeTest {
|
||||
@get:Rule
|
||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
|
|
@ -88,18 +87,17 @@ class RoomFlowNodeTest {
|
|||
}
|
||||
}
|
||||
|
||||
private fun aRoomFlowNode(
|
||||
private fun createJoinedRoomLoadedFlowNode(
|
||||
plugins: List<Plugin>,
|
||||
messagesEntryPoint: MessagesEntryPoint = FakeMessagesEntryPoint(),
|
||||
roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(),
|
||||
coroutineScope: CoroutineScope,
|
||||
) = RoomLoadedFlowNode(
|
||||
) = JoinedRoomLoadedFlowNode(
|
||||
buildContext = BuildContext.root(savedStateMap = null),
|
||||
plugins = plugins,
|
||||
messagesEntryPoint = messagesEntryPoint,
|
||||
roomDetailsEntryPoint = roomDetailsEntryPoint,
|
||||
appNavigationStateService = FakeAppNavigationStateService(),
|
||||
roomMembershipObserver = RoomMembershipObserver(),
|
||||
appCoroutineScope = coroutineScope,
|
||||
roomComponentFactory = FakeRoomComponentFactory(),
|
||||
matrixClient = FakeMatrixClient(),
|
||||
|
|
@ -110,8 +108,8 @@ class RoomFlowNodeTest {
|
|||
// GIVEN
|
||||
val room = FakeMatrixRoom()
|
||||
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
||||
val inputs = RoomLoadedFlowNode.Inputs(room)
|
||||
val roomFlowNode = aRoomFlowNode(
|
||||
val inputs = JoinedRoomLoadedFlowNode.Inputs(room)
|
||||
val roomFlowNode = createJoinedRoomLoadedFlowNode(
|
||||
plugins = listOf(inputs),
|
||||
messagesEntryPoint = fakeMessagesEntryPoint,
|
||||
coroutineScope = this
|
||||
|
|
@ -120,9 +118,9 @@ class RoomFlowNodeTest {
|
|||
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
|
||||
|
||||
// THEN
|
||||
assertThat(roomFlowNode.backstack.activeElement).isEqualTo(RoomLoadedFlowNode.NavTarget.Messages)
|
||||
roomFlowNodeTestHelper.assertChildHasLifecycle(RoomLoadedFlowNode.NavTarget.Messages, Lifecycle.State.CREATED)
|
||||
val messagesNode = roomFlowNode.childNode(RoomLoadedFlowNode.NavTarget.Messages)!!
|
||||
assertThat(roomFlowNode.backstack.activeElement).isEqualTo(JoinedRoomLoadedFlowNode.NavTarget.Messages)
|
||||
roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.Messages, Lifecycle.State.CREATED)
|
||||
val messagesNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.Messages)!!
|
||||
assertThat(messagesNode.id).isEqualTo(fakeMessagesEntryPoint.nodeId)
|
||||
}
|
||||
|
||||
|
|
@ -132,8 +130,8 @@ class RoomFlowNodeTest {
|
|||
val room = FakeMatrixRoom()
|
||||
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
||||
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
|
||||
val inputs = RoomLoadedFlowNode.Inputs(room)
|
||||
val roomFlowNode = aRoomFlowNode(
|
||||
val inputs = JoinedRoomLoadedFlowNode.Inputs(room)
|
||||
val roomFlowNode = createJoinedRoomLoadedFlowNode(
|
||||
plugins = listOf(inputs),
|
||||
messagesEntryPoint = fakeMessagesEntryPoint,
|
||||
roomDetailsEntryPoint = fakeRoomDetailsEntryPoint,
|
||||
|
|
@ -143,8 +141,8 @@ class RoomFlowNodeTest {
|
|||
// WHEN
|
||||
fakeMessagesEntryPoint.callback?.onRoomDetailsClicked()
|
||||
// THEN
|
||||
roomFlowNodeTestHelper.assertChildHasLifecycle(RoomLoadedFlowNode.NavTarget.RoomDetails, Lifecycle.State.CREATED)
|
||||
val roomDetailsNode = roomFlowNode.childNode(RoomLoadedFlowNode.NavTarget.RoomDetails)!!
|
||||
roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.RoomDetails, Lifecycle.State.CREATED)
|
||||
val roomDetailsNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.RoomDetails)!!
|
||||
assertThat(roomDetailsNode.id).isEqualTo(fakeRoomDetailsEntryPoint.nodeId)
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,8 @@ package io.element.android.appnav.room
|
|||
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.appnav.room.joined.LoadingRoomState
|
||||
import io.element.android.appnav.room.joined.LoadingRoomStateFlowFactory
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomList
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
|
|
|
|||
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"?>
|
||||
<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_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_private_option_description">"Сообщения в этой комнате зашифрованы. Отключить шифрование позже будет невозможно."</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_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 або скарыстайцеся мабільнымі дадзенымі замест 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_2">"Націсніце на свой аватар"</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_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_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_2">"Гісторыя паведамленняў для зашыфраваных пакояў пакуль недаступна."</string>
|
||||
<string name="screen_welcome_bullet_3">"Мы будзем рады пачуць вашае меркаванне, паведаміце нам аб гэтым праз старонку налад."</string>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_notification_optin_subtitle">"Можете да промените настройките си по-късно."</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_title">"Добре дошли в %1$s!"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,34 @@
|
|||
<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_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_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>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,13 @@
|
|||
<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_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_2">"Klick auf deinen Avatar"</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_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_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_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>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
<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_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_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>
|
||||
|
|
|
|||
|
|
@ -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_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_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_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>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,29 @@
|
|||
<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_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_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>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
<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_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_2">"Klik pada avatar Anda"</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_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_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_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>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
<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_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_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>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
<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_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_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>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,13 @@
|
|||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_notification_optin_subtitle">"Вы можете изменить настройки позже."</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_2">"Нажмите на свое изображение"</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_action">"\"Показать 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_2">"История сообщений для зашифрованных комнат в этом обновлении будет недоступна."</string>
|
||||
<string name="screen_welcome_bullet_3">"Мы будем рады услышать ваше мнение, сообщите нам об этом через страницу настроек."</string>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,15 @@
|
|||
<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_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_2">"Kliknite na svoj obrázok"</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_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_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_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>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_notification_optin_subtitle">"Ви можете змінити свої налаштування пізніше."</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_2">"Історія повідомлень для зашифрованих кімнат ще недоступна."</string>
|
||||
<string name="screen_welcome_bullet_3">"Ми хотіли б почути вас, розкажіть нам ваші враження та ідеї щодо застосунку на сторінці налаштувань."</string>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_2">"在這次的更新,您無法查看聊天室內被加密的歷史訊息。"</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_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_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_2">"Click on your avatar"</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_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_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_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_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_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>
|
||||
|
|
|
|||
|
|
@ -15,11 +15,11 @@
|
|||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
id("io.element.android-compose-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.invitelist.api"
|
||||
namespace = "io.element.android.features.invite.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.invitelist.api
|
||||
package io.element.android.features.invite.api
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
|
|
@ -32,7 +32,7 @@ interface InviteListEntryPoint : FeatureEntryPoint {
|
|||
|
||||
interface Callback : Plugin {
|
||||
fun onBackClicked()
|
||||
|
||||
fun onInviteClicked(roomId: RoomId)
|
||||
fun onInviteAccepted(roomId: RoomId)
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.invitelist.api
|
||||
package io.element.android.features.invite.api
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
|
@ -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 {
|
||||
namespace = "io.element.android.features.invitelist.impl"
|
||||
namespace = "io.element.android.features.invite.impl"
|
||||
}
|
||||
|
||||
anvil {
|
||||
|
|
@ -32,7 +32,7 @@ anvil {
|
|||
dependencies {
|
||||
implementation(projects.anvilannotations)
|
||||
anvil(projects.anvilcodegen)
|
||||
api(projects.features.invitelist.api)
|
||||
api(projects.features.invite.api)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.architecture)
|
||||
|
|
@ -50,7 +50,7 @@ dependencies {
|
|||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.push.test)
|
||||
testImplementation(projects.features.invitelist.test)
|
||||
testImplementation(projects.features.invite.test)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
|
||||
|
|
@ -14,13 +14,14 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.invitelist.impl
|
||||
package io.element.android.features.invite.impl
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.invitelist.api.InviteListEntryPoint
|
||||
import io.element.android.features.invite.api.InviteListEntryPoint
|
||||
import io.element.android.features.invite.impl.invitelist.InviteListNode
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import javax.inject.Inject
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.invitelist.impl
|
||||
package io.element.android.features.invite.impl
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
|
|
@ -23,7 +23,7 @@ import androidx.datastore.preferences.core.edit
|
|||
import androidx.datastore.preferences.core.stringSetPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.invitelist.api.SeenInvitesStore
|
||||
import io.element.android.features.invite.api.SeenInvitesStore
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.invitelist.impl.components
|
||||
package io.element.android.features.invite.impl.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
|
|
@ -40,10 +40,10 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.invitelist.impl.R
|
||||
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
|
||||
import io.element.android.features.invitelist.impl.model.InviteListInviteSummaryProvider
|
||||
import io.element.android.features.invitelist.impl.model.InviteSender
|
||||
import io.element.android.features.invite.impl.R
|
||||
import io.element.android.features.invite.impl.model.InviteListInviteSummary
|
||||
import io.element.android.features.invite.impl.model.InviteListInviteSummaryProvider
|
||||
import io.element.android.features.invite.impl.model.InviteSender
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
|
|
@ -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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
|
@ -14,17 +14,11 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.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 {
|
||||
data class AcceptInvite(val invite: InviteListInviteSummary) : InviteListEvents
|
||||
data class DeclineInvite(val invite: InviteListInviteSummary) : InviteListEvents
|
||||
|
||||
data object ConfirmDeclineInvite : InviteListEvents
|
||||
data object CancelDeclineInvite : InviteListEvents
|
||||
|
||||
data object DismissAcceptError : InviteListEvents
|
||||
data object DismissDeclineError : InviteListEvents
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.invitelist.impl
|
||||
package io.element.android.features.invite.impl.invitelist
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -25,7 +25,7 @@ import com.bumble.appyx.core.plugin.plugins
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.invitelist.api.InviteListEntryPoint
|
||||
import io.element.android.features.invite.api.InviteListEntryPoint
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
|
|
@ -43,6 +43,10 @@ class InviteListNode @AssistedInject constructor(
|
|||
plugins<InviteListEntryPoint.Callback>().forEach { it.onInviteAccepted(roomId) }
|
||||
}
|
||||
|
||||
private fun onInviteClicked(roomId: RoomId) {
|
||||
plugins<InviteListEntryPoint.Callback>().forEach { it.onInviteClicked(roomId) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
|
|
@ -50,6 +54,8 @@ class InviteListNode @AssistedInject constructor(
|
|||
state = state,
|
||||
onBackClicked = ::onBackClicked,
|
||||
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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
|
@ -14,43 +14,35 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.invitelist.impl
|
||||
package io.element.android.features.invite.impl.invitelist
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import im.vector.app.features.analytics.plan.JoinedRoom
|
||||
import io.element.android.features.invitelist.api.SeenInvitesStore
|
||||
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
|
||||
import io.element.android.features.invitelist.impl.model.InviteSender
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.features.invite.api.SeenInvitesStore
|
||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||
import io.element.android.features.invite.api.response.InviteData
|
||||
import io.element.android.features.invite.impl.model.InviteListInviteSummary
|
||||
import io.element.android.features.invite.impl.model.InviteSender
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analytics.api.extensions.toAnalyticsJoinedRoom
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class InviteListPresenter @Inject constructor(
|
||||
private val client: MatrixClient,
|
||||
private val store: SeenInvitesStore,
|
||||
private val analyticsService: AnalyticsService,
|
||||
private val notificationDrawerManager: NotificationDrawerManager,
|
||||
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
||||
) : Presenter<InviteListState> {
|
||||
@Composable
|
||||
override fun present(): InviteListState {
|
||||
|
|
@ -75,40 +67,20 @@ class InviteListPresenter @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
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) }
|
||||
val acceptDeclineInviteState = acceptDeclineInvitePresenter.present()
|
||||
|
||||
fun handleEvent(event: InviteListEvents) {
|
||||
when (event) {
|
||||
is InviteListEvents.AcceptInvite -> {
|
||||
acceptedAction.value = AsyncData.Uninitialized
|
||||
localCoroutineScope.acceptInvite(event.invite.roomId, acceptedAction)
|
||||
acceptDeclineInviteState.eventSink(
|
||||
AcceptDeclineInviteEvents.AcceptInvite(event.invite.toInviteData())
|
||||
)
|
||||
}
|
||||
|
||||
is InviteListEvents.DeclineInvite -> {
|
||||
decliningInvite.value = event.invite
|
||||
}
|
||||
|
||||
is InviteListEvents.ConfirmDeclineInvite -> {
|
||||
declinedAction.value = AsyncData.Uninitialized
|
||||
decliningInvite.value?.let {
|
||||
localCoroutineScope.declineInvite(it.roomId, declinedAction)
|
||||
}
|
||||
decliningInvite.value = null
|
||||
}
|
||||
|
||||
is InviteListEvents.CancelDeclineInvite -> {
|
||||
decliningInvite.value = null
|
||||
}
|
||||
|
||||
is InviteListEvents.DismissAcceptError -> {
|
||||
acceptedAction.value = AsyncData.Uninitialized
|
||||
}
|
||||
|
||||
is InviteListEvents.DismissDeclineError -> {
|
||||
declinedAction.value = AsyncData.Uninitialized
|
||||
acceptDeclineInviteState.eventSink(
|
||||
AcceptDeclineInviteEvents.DeclineInvite(event.invite.toInviteData())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -124,38 +96,11 @@ class InviteListPresenter @Inject constructor(
|
|||
|
||||
return InviteListState(
|
||||
inviteList = inviteList,
|
||||
declineConfirmationDialog = decliningInvite.value?.let {
|
||||
InviteDeclineConfirmationDialog.Visible(
|
||||
isDirect = it.isDirect,
|
||||
name = it.roomName,
|
||||
)
|
||||
} ?: InviteDeclineConfirmationDialog.Hidden,
|
||||
acceptedAction = acceptedAction.value,
|
||||
declinedAction = declinedAction.value,
|
||||
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||
eventSink = ::handleEvent
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.acceptInvite(roomId: RoomId, acceptedAction: MutableState<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 {
|
||||
val i = inviter
|
||||
val avatarData = if (isDirect && i != null) {
|
||||
|
|
@ -203,4 +148,10 @@ class InviteListPresenter @Inject constructor(
|
|||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun InviteListInviteSummary.toInviteData() = InviteData(
|
||||
roomId = roomId,
|
||||
roomName = roomName,
|
||||
isDirect = isDirect,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
|
@ -14,24 +14,16 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.invitelist.impl
|
||||
package io.element.android.features.invite.impl.invitelist
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||
import io.element.android.features.invite.impl.model.InviteListInviteSummary
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
data class InviteListState(
|
||||
val inviteList: ImmutableList<InviteListInviteSummary>,
|
||||
val declineConfirmationDialog: InviteDeclineConfirmationDialog,
|
||||
val acceptedAction: AsyncData<RoomId>,
|
||||
val declinedAction: AsyncData<Unit>,
|
||||
val acceptDeclineInviteState: AcceptDeclineInviteState,
|
||||
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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
|
@ -14,35 +14,39 @@
|
|||
* 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 io.element.android.features.invitelist.impl.model.InviteListInviteSummary
|
||||
import io.element.android.features.invitelist.impl.model.InviteSender
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
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.anAcceptDeclineInviteState
|
||||
import io.element.android.features.invite.impl.model.InviteListInviteSummary
|
||||
import io.element.android.features.invite.impl.model.InviteSender
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
open class InviteListStateProvider : PreviewParameterProvider<InviteListState> {
|
||||
private val acceptDeclineInviteStateProvider = AcceptDeclineInviteStateProvider()
|
||||
|
||||
override val values: Sequence<InviteListState>
|
||||
get() = sequenceOf(
|
||||
aInviteListState(),
|
||||
aInviteListState().copy(inviteList = persistentListOf()),
|
||||
aInviteListState().copy(declineConfirmationDialog = InviteDeclineConfirmationDialog.Visible(true, "Alice")),
|
||||
aInviteListState().copy(declineConfirmationDialog = InviteDeclineConfirmationDialog.Visible(false, "Some Room")),
|
||||
aInviteListState().copy(acceptedAction = AsyncData.Failure(Throwable("Whoops"))),
|
||||
aInviteListState().copy(declinedAction = AsyncData.Failure(Throwable("Whoops"))),
|
||||
)
|
||||
anInviteListState(),
|
||||
anInviteListState(inviteList = persistentListOf()),
|
||||
) + acceptDeclineInviteStateProvider.values.map { acceptDeclineInviteState ->
|
||||
anInviteListState(acceptDeclineInviteState = acceptDeclineInviteState)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun aInviteListState() = InviteListState(
|
||||
inviteList = aInviteListInviteSummaryList(),
|
||||
declineConfirmationDialog = InviteDeclineConfirmationDialog.Hidden,
|
||||
acceptedAction = AsyncData.Uninitialized,
|
||||
declinedAction = AsyncData.Uninitialized,
|
||||
eventSink = {},
|
||||
internal fun anInviteListState(
|
||||
inviteList: ImmutableList<InviteListInviteSummary> = aInviteListInviteSummaryList(),
|
||||
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
|
||||
eventSink: (InviteListEvents) -> Unit = {}
|
||||
) = InviteListState(
|
||||
inviteList = inviteList,
|
||||
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
|
@ -14,8 +14,9 @@
|
|||
* 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.Spacer
|
||||
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.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.invitelist.impl.components.InviteSummaryRow
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.features.invite.impl.R
|
||||
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.dialogs.ConfirmationDialog
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
||||
|
|
@ -56,61 +53,21 @@ fun InviteListView(
|
|||
state: InviteListState,
|
||||
onBackClicked: () -> Unit,
|
||||
onInviteAccepted: (RoomId) -> Unit,
|
||||
onInviteDeclined: (RoomId) -> Unit,
|
||||
onInviteClicked: (RoomId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (state.acceptedAction is AsyncData.Success) {
|
||||
val latestOnInviteAccepted by rememberUpdatedState(onInviteAccepted)
|
||||
LaunchedEffect(state.acceptedAction) {
|
||||
latestOnInviteAccepted(state.acceptedAction.data)
|
||||
}
|
||||
}
|
||||
|
||||
InviteListContent(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onInviteClicked = onInviteClicked,
|
||||
onBackClicked = onBackClicked,
|
||||
)
|
||||
|
||||
if (state.declineConfirmationDialog is InviteDeclineConfirmationDialog.Visible) {
|
||||
val contentResource = if (state.declineConfirmationDialog.isDirect) {
|
||||
R.string.screen_invites_decline_direct_chat_message
|
||||
} else {
|
||||
R.string.screen_invites_decline_chat_message
|
||||
}
|
||||
|
||||
val titleResource = if (state.declineConfirmationDialog.isDirect) {
|
||||
R.string.screen_invites_decline_direct_chat_title
|
||||
} else {
|
||||
R.string.screen_invites_decline_chat_title
|
||||
}
|
||||
|
||||
ConfirmationDialog(
|
||||
content = stringResource(contentResource, state.declineConfirmationDialog.name),
|
||||
title = stringResource(titleResource),
|
||||
submitText = stringResource(CommonStrings.action_decline),
|
||||
cancelText = stringResource(CommonStrings.action_cancel),
|
||||
onSubmitClicked = { state.eventSink(InviteListEvents.ConfirmDeclineInvite) },
|
||||
onDismiss = { state.eventSink(InviteListEvents.CancelDeclineInvite) }
|
||||
)
|
||||
}
|
||||
|
||||
if (state.acceptedAction is AsyncData.Failure) {
|
||||
ErrorDialog(
|
||||
content = stringResource(CommonStrings.error_unknown),
|
||||
title = stringResource(CommonStrings.common_error),
|
||||
submitText = stringResource(CommonStrings.action_ok),
|
||||
onDismiss = { state.eventSink(InviteListEvents.DismissAcceptError) }
|
||||
)
|
||||
}
|
||||
|
||||
if (state.declinedAction is AsyncData.Failure) {
|
||||
ErrorDialog(
|
||||
content = stringResource(CommonStrings.error_unknown),
|
||||
title = stringResource(CommonStrings.common_error),
|
||||
submitText = stringResource(CommonStrings.action_ok),
|
||||
onDismiss = { state.eventSink(InviteListEvents.DismissDeclineError) }
|
||||
)
|
||||
}
|
||||
AcceptDeclineInviteView(
|
||||
state = state.acceptDeclineInviteState,
|
||||
onInviteAccepted = onInviteAccepted,
|
||||
onInviteDeclined = onInviteDeclined,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
|
|
@ -118,6 +75,7 @@ fun InviteListView(
|
|||
private fun InviteListContent(
|
||||
state: InviteListState,
|
||||
onBackClicked: () -> Unit,
|
||||
onInviteClicked: (RoomId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Scaffold(
|
||||
|
|
@ -138,8 +96,8 @@ private fun InviteListContent(
|
|||
content = { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding)
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding)
|
||||
) {
|
||||
if (state.inviteList.isEmpty()) {
|
||||
Spacer(Modifier.size(80.dp))
|
||||
|
|
@ -158,6 +116,9 @@ private fun InviteListContent(
|
|||
items = state.inviteList,
|
||||
) { index, invite ->
|
||||
InviteSummaryRow(
|
||||
modifier = Modifier.clickable(
|
||||
onClick = { onInviteClicked(invite.roomId) }
|
||||
),
|
||||
invite = invite,
|
||||
onAcceptClicked = { state.eventSink(InviteListEvents.AcceptInvite(invite)) },
|
||||
onDeclineClicked = { state.eventSink(InviteListEvents.DeclineInvite(invite)) },
|
||||
|
|
@ -181,5 +142,7 @@ internal fun InviteListViewPreview(@PreviewParameter(InviteListStateProvider::cl
|
|||
state = state,
|
||||
onBackClicked = {},
|
||||
onInviteAccepted = {},
|
||||
onInviteDeclined = {},
|
||||
onInviteClicked = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.invitelist.impl.model
|
||||
package io.element.android.features.invite.impl.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.invitelist.impl.model
|
||||
package io.element.android.features.invite.impl.model
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
|
@ -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 {
|
||||
namespace = "io.element.android.features.invitelist.test"
|
||||
namespace = "io.element.android.features.invite.test"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.coroutines.core)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
api(projects.features.invitelist.api)
|
||||
api(projects.features.invite.api)
|
||||
}
|
||||
|
|
@ -14,9 +14,9 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.invitelist.test
|
||||
package io.element.android.features.invite.test
|
||||
|
||||
import io.element.android.features.invitelist.api.SeenInvitesStore
|
||||
import io.element.android.features.invite.api.SeenInvitesStore
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
|
@ -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(
|
||||
roomId = A_ROOM_ID,
|
||||
result = FakeMatrixRoom().apply {
|
||||
givenLeaveRoomError(RuntimeException("Blimey!"))
|
||||
this.leaveRoomLambda = { Result.failure(RuntimeException("Blimey!")) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -210,7 +210,7 @@ class LeaveRoomPresenterImplTest {
|
|||
givenGetRoomResult(
|
||||
roomId = A_ROOM_ID,
|
||||
result = FakeMatrixRoom().apply {
|
||||
givenLeaveRoomError(RuntimeException("Blimey!"))
|
||||
this.leaveRoomLambda = { Result.failure(RuntimeException("Blimey!")) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
<string name="screen_app_lock_setup_pin_blacklisted_dialog_title">"Выберите другой PIN-код"</string>
|
||||
<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_title">"PIN-коды не совпадают"</string>
|
||||
<string name="screen_app_lock_signout_alert_message">"Чтобы продолжить, вам необходимо повторно войти в систему и создать новый PIN-код"</string>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
package io.element.android.features.messages.impl
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -108,13 +107,19 @@ class MessagesNode @AssistedInject constructor(
|
|||
context: Context,
|
||||
url: String,
|
||||
) {
|
||||
when (val permalink = permalinkParser.parse(Uri.parse(url))) {
|
||||
when (val permalink = permalinkParser.parse(url)) {
|
||||
is PermalinkData.UserLink -> {
|
||||
callback?.onUserDataClicked(UserId(permalink.userId))
|
||||
callback?.onUserDataClicked(permalink.userId)
|
||||
}
|
||||
is PermalinkData.RoomLink -> {
|
||||
// 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.RoomEmailInviteLink -> {
|
||||
context.openUrlInExternalApp(url)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
package io.element.android.features.messages.impl.messagecomposer
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.messages.impl.mentions.MentionSuggestion
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
|
@ -49,7 +48,6 @@ fun aMessageComposerState(
|
|||
richTextEditorState = richTextEditorState,
|
||||
permalinkParser = object : PermalinkParser {
|
||||
override fun parse(uriString: String): PermalinkData = TODO()
|
||||
override fun parse(uri: Uri): PermalinkData = TODO()
|
||||
},
|
||||
isFullScreen = isFullScreen,
|
||||
mode = mode,
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@ class MessagesPresenterTest {
|
|||
val clipboardHelper = FakeClipboardHelper()
|
||||
val event = aMessageEvent()
|
||||
val matrixRoom = FakeMatrixRoom(
|
||||
permalinkResult = { Result.success("a link") },
|
||||
eventPermalinkResult = { Result.success("a link") },
|
||||
)
|
||||
val presenter = createMessagesPresenter(
|
||||
clipboardHelper = clipboardHelper,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import android.content.Context
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.bumble.appyx.core.lifecycle.subscribe
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
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.RoomMember
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import io.element.android.libraries.androidutils.R as AndroidUtilsR
|
||||
|
||||
|
|
@ -83,34 +86,35 @@ class RoomDetailsNode @AssistedInject constructor(
|
|||
callbacks.forEach { it.openPollHistory() }
|
||||
}
|
||||
|
||||
private fun onShareRoom(context: Context) {
|
||||
val alias = room.alias ?: room.alternativeAliases.firstOrNull()
|
||||
val permalinkResult = alias?.let { permalinkBuilder.permalinkForRoomAlias(it) }
|
||||
?: permalinkBuilder.permalinkForRoomId(room.roomId)
|
||||
permalinkResult.onSuccess { permalink ->
|
||||
context.startSharePlainTextIntent(
|
||||
activityResultLauncher = null,
|
||||
chooserTitle = context.getString(R.string.screen_room_details_share_room_title),
|
||||
text = permalink,
|
||||
noActivityFoundMessage = context.getString(AndroidUtilsR.string.error_no_compatible_app_found)
|
||||
)
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
}
|
||||
private fun CoroutineScope.onShareRoom(context: Context) = launch {
|
||||
room.getPermalink()
|
||||
.onSuccess { permalink ->
|
||||
context.startSharePlainTextIntent(
|
||||
activityResultLauncher = null,
|
||||
chooserTitle = context.getString(R.string.screen_room_details_share_room_title),
|
||||
text = permalink,
|
||||
noActivityFoundMessage = context.getString(AndroidUtilsR.string.error_no_compatible_app_found)
|
||||
)
|
||||
}
|
||||
.onFailure {
|
||||
Timber.e(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onShareMember(context: Context, member: RoomMember) {
|
||||
val permalinkResult = permalinkBuilder.permalinkForUser(member.userId)
|
||||
permalinkResult.onSuccess { permalink ->
|
||||
context.startSharePlainTextIntent(
|
||||
activityResultLauncher = null,
|
||||
chooserTitle = context.getString(R.string.screen_room_details_share_room_title),
|
||||
text = permalink,
|
||||
noActivityFoundMessage = context.getString(AndroidUtilsR.string.error_no_compatible_app_found)
|
||||
)
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
}
|
||||
permalinkResult
|
||||
.onSuccess { permalink ->
|
||||
context.startSharePlainTextIntent(
|
||||
activityResultLauncher = null,
|
||||
chooserTitle = context.getString(R.string.screen_room_details_share_room_title),
|
||||
text = permalink,
|
||||
noActivityFoundMessage = context.getString(AndroidUtilsR.string.error_no_compatible_app_found)
|
||||
)
|
||||
}
|
||||
.onFailure {
|
||||
Timber.e(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEditRoomDetails() {
|
||||
|
|
@ -131,7 +135,7 @@ class RoomDetailsNode @AssistedInject constructor(
|
|||
val state = presenter.present()
|
||||
|
||||
fun onShareRoom() {
|
||||
this.onShareRoom(context)
|
||||
lifecycleScope.onShareRoom(context)
|
||||
}
|
||||
|
||||
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