Merge branch 'develop' into renovate/lifecycle
This commit is contained in:
commit
c0c83a5376
314 changed files with 2281 additions and 1021 deletions
2
.github/workflows/maestro.yml
vendored
2
.github/workflows/maestro.yml
vendored
|
|
@ -79,7 +79,7 @@ jobs:
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: elementx-apk-maestro
|
name: elementx-apk-maestro
|
||||||
- uses: mobile-dev-inc/action-maestro-cloud@v1.9.2
|
- uses: mobile-dev-inc/action-maestro-cloud@v1.9.4
|
||||||
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
|
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
|
||||||
with:
|
with:
|
||||||
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
|
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
# Element X Android
|
# Element X Android
|
||||||
|
|
||||||
Element X Android is a [Matrix](https://matrix.org/) Android Client provided by [element.io](https://element.io/). This app is currently in a pre-alpha release stage with only basic functionalities.
|
Element X Android is a [Matrix](https://matrix.org/) Android Client provided by [element.io](https://element.io/).
|
||||||
|
|
||||||
The application is a total rewrite of [Element-Android](https://github.com/element-hq/element-android) using the [Matrix Rust SDK](https://github.com/matrix-org/matrix-rust-sdk) underneath and targeting devices running Android 7+. The UI layer is written using [Jetpack Compose](https://developer.android.com/jetpack/compose), and the navigation is managed using [Appyx](https://github.com/bumble-tech/appyx).
|
The application is a total rewrite of [Element-Android](https://github.com/element-hq/element-android) using the [Matrix Rust SDK](https://github.com/matrix-org/matrix-rust-sdk) underneath and targeting devices running Android 7+. The UI layer is written using [Jetpack Compose](https://developer.android.com/jetpack/compose), and the navigation is managed using [Appyx](https://github.com/bumble-tech/appyx).
|
||||||
|
|
||||||
|
|
@ -71,7 +71,7 @@ We're doing this as a way to share code between platforms and while we've seen p
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
This project is in work in progress. The app does not cover yet all functionalities we expect. The list of supported features can be found in [this issue](https://github.com/element-hq/element-x-android/issues/911).
|
This project is in an early rollout and migration phase.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,9 +48,9 @@ android {
|
||||||
} else {
|
} else {
|
||||||
"io.element.android.x"
|
"io.element.android.x"
|
||||||
}
|
}
|
||||||
targetSdk = Versions.targetSdk
|
targetSdk = Versions.TARGET_SDK
|
||||||
versionCode = Versions.versionCode
|
versionCode = Versions.VERSION_CODE
|
||||||
versionName = Versions.versionName
|
versionName = Versions.VERSION_NAME
|
||||||
|
|
||||||
// Keep abiFilter for the universalApk
|
// Keep abiFilter for the universalApk
|
||||||
ndk {
|
ndk {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.call.api
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Value for the local current call.
|
||||||
|
*/
|
||||||
|
sealed interface CurrentCall {
|
||||||
|
data object None : CurrentCall
|
||||||
|
|
||||||
|
data class RoomCall(
|
||||||
|
val roomId: RoomId,
|
||||||
|
) : CurrentCall
|
||||||
|
|
||||||
|
data class ExternalUrl(
|
||||||
|
val url: String,
|
||||||
|
) : CurrentCall
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.call.api
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
interface CurrentCallService {
|
||||||
|
/**
|
||||||
|
* The current call state flow, which will be updated when the active call changes.
|
||||||
|
* This value reflect the local state of the call. It is not updated if the user answers
|
||||||
|
* a call from another session.
|
||||||
|
*/
|
||||||
|
val currentCall: StateFlow<CurrentCall>
|
||||||
|
}
|
||||||
|
|
@ -183,6 +183,7 @@ private fun WebView.setup(
|
||||||
allowFileAccess = true
|
allowFileAccess = true
|
||||||
domStorageEnabled = true
|
domStorageEnabled = true
|
||||||
mediaPlaybackRequiresUserGesture = false
|
mediaPlaybackRequiresUserGesture = false
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
databaseEnabled = true
|
databaseEnabled = true
|
||||||
loadsImagesAutomatically = true
|
loadsImagesAutomatically = true
|
||||||
userAgentString = userAgent
|
userAgentString = userAgent
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import androidx.core.app.NotificationManagerCompat
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.appconfig.ElementCallConfig
|
import io.element.android.appconfig.ElementCallConfig
|
||||||
import io.element.android.features.call.api.CallType
|
import io.element.android.features.call.api.CallType
|
||||||
|
import io.element.android.features.call.api.CurrentCall
|
||||||
import io.element.android.features.call.impl.notifications.CallNotificationData
|
import io.element.android.features.call.impl.notifications.CallNotificationData
|
||||||
import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator
|
import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
|
|
@ -82,6 +83,7 @@ class DefaultActiveCallManager @Inject constructor(
|
||||||
private val ringingCallNotificationCreator: RingingCallNotificationCreator,
|
private val ringingCallNotificationCreator: RingingCallNotificationCreator,
|
||||||
private val notificationManagerCompat: NotificationManagerCompat,
|
private val notificationManagerCompat: NotificationManagerCompat,
|
||||||
private val matrixClientProvider: MatrixClientProvider,
|
private val matrixClientProvider: MatrixClientProvider,
|
||||||
|
private val defaultCurrentCallService: DefaultCurrentCallService,
|
||||||
) : ActiveCallManager {
|
) : ActiveCallManager {
|
||||||
private var timedOutCallJob: Job? = null
|
private var timedOutCallJob: Job? = null
|
||||||
|
|
||||||
|
|
@ -89,6 +91,7 @@ class DefaultActiveCallManager @Inject constructor(
|
||||||
|
|
||||||
init {
|
init {
|
||||||
observeRingingCall()
|
observeRingingCall()
|
||||||
|
observeCurrentCall()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun registerIncomingCall(notificationData: CallNotificationData) {
|
override fun registerIncomingCall(notificationData: CallNotificationData) {
|
||||||
|
|
@ -209,6 +212,28 @@ class DefaultActiveCallManager @Inject constructor(
|
||||||
}
|
}
|
||||||
.launchIn(coroutineScope)
|
.launchIn(coroutineScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun observeCurrentCall() {
|
||||||
|
activeCall
|
||||||
|
.onEach { value ->
|
||||||
|
if (value == null) {
|
||||||
|
defaultCurrentCallService.onCallEnded()
|
||||||
|
} else {
|
||||||
|
when (value.callState) {
|
||||||
|
is CallState.Ringing -> {
|
||||||
|
// Nothing to do
|
||||||
|
}
|
||||||
|
is CallState.InCall -> {
|
||||||
|
when (val callType = value.callType) {
|
||||||
|
is CallType.ExternalUrl -> defaultCurrentCallService.onCallStarted(CurrentCall.ExternalUrl(callType.url))
|
||||||
|
is CallType.RoomCall -> defaultCurrentCallService.onCallStarted(CurrentCall.RoomCall(callType.roomId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.launchIn(coroutineScope)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.call.impl.utils
|
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.features.call.api.CurrentCall
|
||||||
|
import io.element.android.features.call.api.CurrentCallService
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import io.element.android.libraries.di.SingleIn
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@SingleIn(AppScope::class)
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class DefaultCurrentCallService @Inject constructor() : CurrentCallService {
|
||||||
|
override val currentCall = MutableStateFlow<CurrentCall>(CurrentCall.None)
|
||||||
|
|
||||||
|
fun onCallStarted(call: CurrentCall) {
|
||||||
|
currentCall.value = call
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onCallEnded() {
|
||||||
|
currentCall.value = CurrentCall.None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,7 @@ import io.element.android.features.call.impl.notifications.RingingCallNotificati
|
||||||
import io.element.android.features.call.impl.utils.ActiveCall
|
import io.element.android.features.call.impl.utils.ActiveCall
|
||||||
import io.element.android.features.call.impl.utils.CallState
|
import io.element.android.features.call.impl.utils.CallState
|
||||||
import io.element.android.features.call.impl.utils.DefaultActiveCallManager
|
import io.element.android.features.call.impl.utils.DefaultActiveCallManager
|
||||||
|
import io.element.android.features.call.impl.utils.DefaultCurrentCallService
|
||||||
import io.element.android.features.call.test.aCallNotificationData
|
import io.element.android.features.call.test.aCallNotificationData
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
@ -299,5 +300,6 @@ class DefaultActiveCallManagerTest {
|
||||||
),
|
),
|
||||||
notificationManagerCompat = notificationManagerCompat,
|
notificationManagerCompat = notificationManagerCompat,
|
||||||
matrixClientProvider = matrixClientProvider,
|
matrixClientProvider = matrixClientProvider,
|
||||||
|
defaultCurrentCallService = DefaultCurrentCallService(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.call.test
|
||||||
|
|
||||||
|
import io.element.android.features.call.api.CurrentCall
|
||||||
|
import io.element.android.features.call.api.CurrentCallService
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
||||||
|
class FakeCurrentCallService(
|
||||||
|
override val currentCall: MutableStateFlow<CurrentCall> = MutableStateFlow(CurrentCall.None),
|
||||||
|
) : CurrentCallService
|
||||||
|
|
@ -40,6 +40,7 @@ dependencies {
|
||||||
implementation(projects.libraries.usersearch.impl)
|
implementation(projects.libraries.usersearch.impl)
|
||||||
implementation(projects.services.analytics.api)
|
implementation(projects.services.analytics.api)
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
|
implementation(projects.libraries.featureflag.api)
|
||||||
api(projects.features.createroom.api)
|
api(projects.features.createroom.api)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
|
|
@ -56,6 +57,7 @@ dependencies {
|
||||||
testImplementation(projects.libraries.permissions.test)
|
testImplementation(projects.libraries.permissions.test)
|
||||||
testImplementation(projects.libraries.usersearch.test)
|
testImplementation(projects.libraries.usersearch.test)
|
||||||
testImplementation(projects.features.createroom.test)
|
testImplementation(projects.features.createroom.test)
|
||||||
|
testImplementation(projects.libraries.featureflag.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
package io.element.android.features.createroom.impl
|
package io.element.android.features.createroom.impl
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import io.element.android.features.createroom.impl.configureroom.RoomPrivacy
|
import io.element.android.features.createroom.impl.configureroom.RoomVisibilityState
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
@ -18,5 +18,7 @@ data class CreateRoomConfig(
|
||||||
val topic: String? = null,
|
val topic: String? = null,
|
||||||
val avatarUri: Uri? = null,
|
val avatarUri: Uri? = null,
|
||||||
val invites: ImmutableList<MatrixUser> = persistentListOf(),
|
val invites: ImmutableList<MatrixUser> = persistentListOf(),
|
||||||
val privacy: RoomPrivacy = RoomPrivacy.Private,
|
val roomVisibility: RoomVisibilityState = RoomVisibilityState.Private,
|
||||||
)
|
) {
|
||||||
|
val isValid = roomName.isNullOrEmpty().not() && roomVisibility.isValid()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,12 @@
|
||||||
package io.element.android.features.createroom.impl
|
package io.element.android.features.createroom.impl
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import io.element.android.features.createroom.impl.configureroom.RoomPrivacy
|
import io.element.android.features.createroom.impl.configureroom.RoomAccess
|
||||||
|
import io.element.android.features.createroom.impl.configureroom.RoomAccessItem
|
||||||
|
import io.element.android.features.createroom.impl.configureroom.RoomAddress
|
||||||
|
import io.element.android.features.createroom.impl.configureroom.RoomAddressErrorState
|
||||||
|
import io.element.android.features.createroom.impl.configureroom.RoomVisibilityItem
|
||||||
|
import io.element.android.features.createroom.impl.configureroom.RoomVisibilityState
|
||||||
import io.element.android.features.createroom.impl.di.CreateRoomScope
|
import io.element.android.features.createroom.impl.di.CreateRoomScope
|
||||||
import io.element.android.features.createroom.impl.userlist.UserListDataStore
|
import io.element.android.features.createroom.impl.userlist.UserListDataStore
|
||||||
import io.element.android.libraries.androidutils.file.safeDelete
|
import io.element.android.libraries.androidutils.file.safeDelete
|
||||||
|
|
@ -17,6 +22,7 @@ import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.getAndUpdate
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -31,28 +37,89 @@ class CreateRoomDataStore @Inject constructor(
|
||||||
field = value
|
field = value
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCreateRoomConfig(): Flow<CreateRoomConfig> = combine(
|
val createRoomConfigWithInvites: Flow<CreateRoomConfig> = combine(
|
||||||
selectedUserListDataStore.selectedUsers(),
|
selectedUserListDataStore.selectedUsers(),
|
||||||
createRoomConfigFlow,
|
createRoomConfigFlow,
|
||||||
) { selectedUsers, config ->
|
) { selectedUsers, config ->
|
||||||
config.copy(invites = selectedUsers.toImmutableList())
|
config.copy(invites = selectedUsers.toImmutableList())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setRoomName(roomName: String?) {
|
fun setRoomName(roomName: String) {
|
||||||
createRoomConfigFlow.tryEmit(createRoomConfigFlow.value.copy(roomName = roomName?.takeIf { it.isNotEmpty() }))
|
createRoomConfigFlow.getAndUpdate { config ->
|
||||||
|
/*
|
||||||
|
val newVisibility = when (config.roomVisibility) {
|
||||||
|
is RoomVisibilityState.Public -> {
|
||||||
|
val roomAddress = config.roomVisibility.roomAddress
|
||||||
|
if (roomAddress is RoomAddress.AutoFilled || roomName.isEmpty()) {
|
||||||
|
config.roomVisibility.copy(
|
||||||
|
roomAddress = RoomAddress.AutoFilled(roomName),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
config.roomVisibility
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> config.roomVisibility
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
config.copy(
|
||||||
|
roomName = roomName.takeIf { it.isNotEmpty() },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setTopic(topic: String?) {
|
fun setTopic(topic: String) {
|
||||||
createRoomConfigFlow.tryEmit(createRoomConfigFlow.value.copy(topic = topic?.takeIf { it.isNotEmpty() }))
|
createRoomConfigFlow.getAndUpdate { config ->
|
||||||
|
config.copy(topic = topic.takeIf { it.isNotEmpty() })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setAvatarUri(uri: Uri?, cached: Boolean = false) {
|
fun setAvatarUri(uri: Uri?, cached: Boolean = false) {
|
||||||
cachedAvatarUri = uri.takeIf { cached }
|
cachedAvatarUri = uri.takeIf { cached }
|
||||||
createRoomConfigFlow.tryEmit(createRoomConfigFlow.value.copy(avatarUri = uri))
|
createRoomConfigFlow.getAndUpdate { config ->
|
||||||
|
config.copy(avatarUri = uri)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setPrivacy(privacy: RoomPrivacy) {
|
fun setRoomVisibility(visibility: RoomVisibilityItem) {
|
||||||
createRoomConfigFlow.tryEmit(createRoomConfigFlow.value.copy(privacy = privacy))
|
createRoomConfigFlow.getAndUpdate { config ->
|
||||||
|
config.copy(
|
||||||
|
roomVisibility = when (visibility) {
|
||||||
|
RoomVisibilityItem.Private -> RoomVisibilityState.Private
|
||||||
|
RoomVisibilityItem.Public -> RoomVisibilityState.Public(
|
||||||
|
roomAddress = RoomAddress.AutoFilled(config.roomName.orEmpty()),
|
||||||
|
roomAddressErrorState = RoomAddressErrorState.None,
|
||||||
|
roomAccess = RoomAccess.Anyone,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setRoomAddress(address: String) {
|
||||||
|
createRoomConfigFlow.getAndUpdate { config ->
|
||||||
|
config.copy(
|
||||||
|
roomVisibility = when (config.roomVisibility) {
|
||||||
|
is RoomVisibilityState.Public -> config.roomVisibility.copy(roomAddress = RoomAddress.Edited(address))
|
||||||
|
else -> config.roomVisibility
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setRoomAccess(access: RoomAccessItem) {
|
||||||
|
createRoomConfigFlow.getAndUpdate { config ->
|
||||||
|
config.copy(
|
||||||
|
roomVisibility = when (config.roomVisibility) {
|
||||||
|
is RoomVisibilityState.Public -> {
|
||||||
|
when (access) {
|
||||||
|
RoomAccessItem.Anyone -> config.roomVisibility.copy(roomAccess = RoomAccess.Anyone)
|
||||||
|
RoomAccessItem.AskToJoin -> config.roomVisibility.copy(roomAccess = RoomAccess.Knocking)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> config.roomVisibility
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearCachedData() {
|
fun clearCachedData() {
|
||||||
|
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* Please see LICENSE in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.selection.selectable
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.semantics.Role
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import io.element.android.compound.theme.ElementTheme
|
|
||||||
import io.element.android.features.createroom.impl.configureroom.RoomPrivacyItem
|
|
||||||
import io.element.android.features.createroom.impl.configureroom.roomPrivacyItems
|
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.RadioButton
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun RoomPrivacyOption(
|
|
||||||
roomPrivacyItem: RoomPrivacyItem,
|
|
||||||
onOptionClick: (RoomPrivacyItem) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
isSelected: Boolean = false,
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.selectable(
|
|
||||||
selected = isSelected,
|
|
||||||
onClick = { onOptionClick(roomPrivacyItem) },
|
|
||||||
role = Role.RadioButton,
|
|
||||||
)
|
|
||||||
.padding(8.dp),
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
modifier = Modifier.padding(horizontal = 8.dp),
|
|
||||||
resourceId = roomPrivacyItem.icon,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.secondary,
|
|
||||||
)
|
|
||||||
|
|
||||||
Column(
|
|
||||||
Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = roomPrivacyItem.title,
|
|
||||||
style = ElementTheme.typography.fontBodyLgRegular,
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
Spacer(Modifier.size(3.dp))
|
|
||||||
Text(
|
|
||||||
text = roomPrivacyItem.description,
|
|
||||||
style = ElementTheme.typography.fontBodySmRegular,
|
|
||||||
color = MaterialTheme.colorScheme.tertiary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
RadioButton(
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.CenterVertically)
|
|
||||||
.size(48.dp),
|
|
||||||
selected = isSelected,
|
|
||||||
// null recommended for accessibility with screenreaders
|
|
||||||
onClick = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreviewsDayNight
|
|
||||||
@Composable
|
|
||||||
internal fun RoomPrivacyOptionPreview() = ElementPreview {
|
|
||||||
val aRoomPrivacyItem = roomPrivacyItems().first()
|
|
||||||
Column {
|
|
||||||
RoomPrivacyOption(
|
|
||||||
roomPrivacyItem = aRoomPrivacyItem,
|
|
||||||
onOptionClick = {},
|
|
||||||
isSelected = true,
|
|
||||||
)
|
|
||||||
RoomPrivacyOption(
|
|
||||||
roomPrivacyItem = aRoomPrivacyItem,
|
|
||||||
onOptionClick = {},
|
|
||||||
isSelected = false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -7,16 +7,17 @@
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl.configureroom
|
package io.element.android.features.createroom.impl.configureroom
|
||||||
|
|
||||||
import io.element.android.features.createroom.impl.CreateRoomConfig
|
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
||||||
|
|
||||||
sealed interface ConfigureRoomEvents {
|
sealed interface ConfigureRoomEvents {
|
||||||
data class RoomNameChanged(val name: String) : ConfigureRoomEvents
|
data class RoomNameChanged(val name: String) : ConfigureRoomEvents
|
||||||
data class TopicChanged(val topic: String) : ConfigureRoomEvents
|
data class TopicChanged(val topic: String) : ConfigureRoomEvents
|
||||||
data class RoomPrivacyChanged(val privacy: RoomPrivacy) : ConfigureRoomEvents
|
data class RoomVisibilityChanged(val visibilityItem: RoomVisibilityItem) : ConfigureRoomEvents
|
||||||
data class RemoveFromSelection(val matrixUser: MatrixUser) : ConfigureRoomEvents
|
data class RoomAccessChanged(val roomAccess: RoomAccessItem) : ConfigureRoomEvents
|
||||||
data class CreateRoom(val config: CreateRoomConfig) : ConfigureRoomEvents
|
data class RoomAddressChanged(val roomAddress: String) : ConfigureRoomEvents
|
||||||
|
data class RemoveUserFromSelection(val matrixUser: MatrixUser) : ConfigureRoomEvents
|
||||||
|
data object CreateRoom : ConfigureRoomEvents
|
||||||
data class HandleAvatarAction(val action: AvatarAction) : ConfigureRoomEvents
|
data class HandleAvatarAction(val action: AvatarAction) : ConfigureRoomEvents
|
||||||
data object CancelCreateRoom : ConfigureRoomEvents
|
data object CancelCreateRoom : ConfigureRoomEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
|
import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
|
||||||
|
|
@ -38,6 +40,7 @@ import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ConfigureRoomPresenter @Inject constructor(
|
class ConfigureRoomPresenter @Inject constructor(
|
||||||
|
|
@ -47,6 +50,7 @@ class ConfigureRoomPresenter @Inject constructor(
|
||||||
private val mediaPreProcessor: MediaPreProcessor,
|
private val mediaPreProcessor: MediaPreProcessor,
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
||||||
|
private val featureFlagService: FeatureFlagService,
|
||||||
) : Presenter<ConfigureRoomState> {
|
) : Presenter<ConfigureRoomState> {
|
||||||
private val cameraPermissionPresenter: PermissionsPresenter = permissionsPresenterFactory.create(android.Manifest.permission.CAMERA)
|
private val cameraPermissionPresenter: PermissionsPresenter = permissionsPresenterFactory.create(android.Manifest.permission.CAMERA)
|
||||||
private var pendingPermissionRequest = false
|
private var pendingPermissionRequest = false
|
||||||
|
|
@ -54,7 +58,9 @@ class ConfigureRoomPresenter @Inject constructor(
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): ConfigureRoomState {
|
override fun present(): ConfigureRoomState {
|
||||||
val cameraPermissionState = cameraPermissionPresenter.present()
|
val cameraPermissionState = cameraPermissionPresenter.present()
|
||||||
val createRoomConfig = dataStore.getCreateRoomConfig().collectAsState(CreateRoomConfig())
|
val createRoomConfig = dataStore.createRoomConfigWithInvites.collectAsState(CreateRoomConfig())
|
||||||
|
val homeserverName = remember { matrixClient.userIdServerName() }
|
||||||
|
val isKnockFeatureEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock).collectAsState(initial = false)
|
||||||
|
|
||||||
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(
|
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(
|
||||||
onResult = { uri -> if (uri != null) dataStore.setAvatarUri(uri = uri, cached = true) },
|
onResult = { uri -> if (uri != null) dataStore.setAvatarUri(uri = uri, cached = true) },
|
||||||
|
|
@ -92,9 +98,11 @@ class ConfigureRoomPresenter @Inject constructor(
|
||||||
when (event) {
|
when (event) {
|
||||||
is ConfigureRoomEvents.RoomNameChanged -> dataStore.setRoomName(event.name)
|
is ConfigureRoomEvents.RoomNameChanged -> dataStore.setRoomName(event.name)
|
||||||
is ConfigureRoomEvents.TopicChanged -> dataStore.setTopic(event.topic)
|
is ConfigureRoomEvents.TopicChanged -> dataStore.setTopic(event.topic)
|
||||||
is ConfigureRoomEvents.RoomPrivacyChanged -> dataStore.setPrivacy(event.privacy)
|
is ConfigureRoomEvents.RoomVisibilityChanged -> dataStore.setRoomVisibility(event.visibilityItem)
|
||||||
is ConfigureRoomEvents.RemoveFromSelection -> dataStore.selectedUserListDataStore.removeUserFromSelection(event.matrixUser)
|
is ConfigureRoomEvents.RemoveUserFromSelection -> dataStore.selectedUserListDataStore.removeUserFromSelection(event.matrixUser)
|
||||||
is ConfigureRoomEvents.CreateRoom -> createRoom(event.config)
|
is ConfigureRoomEvents.RoomAccessChanged -> dataStore.setRoomAccess(event.roomAccess)
|
||||||
|
is ConfigureRoomEvents.RoomAddressChanged -> dataStore.setRoomAddress(event.roomAddress)
|
||||||
|
is ConfigureRoomEvents.CreateRoom -> createRoom(createRoomConfig.value)
|
||||||
is ConfigureRoomEvents.HandleAvatarAction -> {
|
is ConfigureRoomEvents.HandleAvatarAction -> {
|
||||||
when (event.action) {
|
when (event.action) {
|
||||||
AvatarAction.ChoosePhoto -> galleryImagePicker.launch()
|
AvatarAction.ChoosePhoto -> galleryImagePicker.launch()
|
||||||
|
|
@ -113,10 +121,12 @@ class ConfigureRoomPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
return ConfigureRoomState(
|
return ConfigureRoomState(
|
||||||
|
isKnockFeatureEnabled = isKnockFeatureEnabled,
|
||||||
config = createRoomConfig.value,
|
config = createRoomConfig.value,
|
||||||
avatarActions = avatarActions,
|
avatarActions = avatarActions,
|
||||||
createRoomAction = createRoomAction.value,
|
createRoomAction = createRoomAction.value,
|
||||||
cameraPermissionState = cameraPermissionState,
|
cameraPermissionState = cameraPermissionState,
|
||||||
|
homeserverName = homeserverName,
|
||||||
eventSink = ::handleEvents,
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -127,21 +137,40 @@ class ConfigureRoomPresenter @Inject constructor(
|
||||||
) = launch {
|
) = launch {
|
||||||
suspend {
|
suspend {
|
||||||
val avatarUrl = config.avatarUri?.let { uploadAvatar(it) }
|
val avatarUrl = config.avatarUri?.let { uploadAvatar(it) }
|
||||||
val params = CreateRoomParameters(
|
val params = if (config.roomVisibility is RoomVisibilityState.Public) {
|
||||||
name = config.roomName,
|
CreateRoomParameters(
|
||||||
topic = config.topic,
|
name = config.roomName,
|
||||||
isEncrypted = config.privacy == RoomPrivacy.Private,
|
topic = config.topic,
|
||||||
isDirect = false,
|
isEncrypted = false,
|
||||||
visibility = if (config.privacy == RoomPrivacy.Public) RoomVisibility.PUBLIC else RoomVisibility.PRIVATE,
|
isDirect = false,
|
||||||
preset = if (config.privacy == RoomPrivacy.Public) RoomPreset.PUBLIC_CHAT else RoomPreset.PRIVATE_CHAT,
|
visibility = RoomVisibility.PUBLIC,
|
||||||
invite = config.invites.map { it.userId },
|
joinRuleOverride = config.roomVisibility.roomAccess.toJoinRule(),
|
||||||
avatar = avatarUrl,
|
preset = RoomPreset.PUBLIC_CHAT,
|
||||||
)
|
invite = config.invites.map { it.userId },
|
||||||
matrixClient.createRoom(params).getOrThrow()
|
avatar = avatarUrl,
|
||||||
.also {
|
canonicalAlias = config.roomVisibility.roomAddress()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
CreateRoomParameters(
|
||||||
|
name = config.roomName,
|
||||||
|
topic = config.topic,
|
||||||
|
isEncrypted = config.roomVisibility is RoomVisibilityState.Private,
|
||||||
|
isDirect = false,
|
||||||
|
visibility = RoomVisibility.PRIVATE,
|
||||||
|
preset = RoomPreset.PRIVATE_CHAT,
|
||||||
|
invite = config.invites.map { it.userId },
|
||||||
|
avatar = avatarUrl,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
matrixClient.createRoom(params)
|
||||||
|
.onFailure { failure ->
|
||||||
|
Timber.e(failure, "Failed to create room")
|
||||||
|
}
|
||||||
|
.onSuccess {
|
||||||
dataStore.clearCachedData()
|
dataStore.clearCachedData()
|
||||||
analyticsService.capture(CreatedRoom(isDM = false))
|
analyticsService.capture(CreatedRoom(isDM = false))
|
||||||
}
|
}
|
||||||
|
.getOrThrow()
|
||||||
}.runCatchingUpdatingState(createRoomAction)
|
}.runCatchingUpdatingState(createRoomAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,11 @@ import io.element.android.libraries.permissions.api.PermissionsState
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class ConfigureRoomState(
|
data class ConfigureRoomState(
|
||||||
|
val isKnockFeatureEnabled: Boolean,
|
||||||
val config: CreateRoomConfig,
|
val config: CreateRoomConfig,
|
||||||
val avatarActions: ImmutableList<AvatarAction>,
|
val avatarActions: ImmutableList<AvatarAction>,
|
||||||
val createRoomAction: AsyncAction<RoomId>,
|
val createRoomAction: AsyncAction<RoomId>,
|
||||||
val cameraPermissionState: PermissionsState,
|
val cameraPermissionState: PermissionsState,
|
||||||
|
val homeserverName: String,
|
||||||
val eventSink: (ConfigureRoomEvents) -> Unit
|
val eventSink: (ConfigureRoomEvents) -> Unit
|
||||||
) {
|
)
|
||||||
val isCreateButtonEnabled: Boolean = config.roomName.isNullOrEmpty().not()
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -10,30 +10,59 @@ package io.element.android.features.createroom.impl.configureroom
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.createroom.impl.CreateRoomConfig
|
import io.element.android.features.createroom.impl.CreateRoomConfig
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||||
|
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
||||||
|
import io.element.android.libraries.permissions.api.PermissionsState
|
||||||
import io.element.android.libraries.permissions.api.aPermissionsState
|
import io.element.android.libraries.permissions.api.aPermissionsState
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomState> {
|
open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomState> {
|
||||||
override val values: Sequence<ConfigureRoomState>
|
override val values: Sequence<ConfigureRoomState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aConfigureRoomState(),
|
aConfigureRoomState(),
|
||||||
aConfigureRoomState().copy(
|
aConfigureRoomState(
|
||||||
|
isKnockFeatureEnabled = false,
|
||||||
config = CreateRoomConfig(
|
config = CreateRoomConfig(
|
||||||
roomName = "Room 101",
|
roomName = "Room 101",
|
||||||
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
||||||
invites = aMatrixUserList().toImmutableList(),
|
invites = aMatrixUserList().toImmutableList(),
|
||||||
privacy = RoomPrivacy.Public,
|
roomVisibility = RoomVisibilityState.Public(
|
||||||
|
roomAddress = RoomAddress.AutoFilled("Room 101"),
|
||||||
|
roomAccess = RoomAccess.Knocking,
|
||||||
|
roomAddressErrorState = RoomAddressErrorState.None,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
aConfigureRoomState(
|
||||||
|
config = CreateRoomConfig(
|
||||||
|
roomName = "Room 101",
|
||||||
|
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
||||||
|
invites = aMatrixUserList().toImmutableList(),
|
||||||
|
roomVisibility = RoomVisibilityState.Public(
|
||||||
|
roomAddress = RoomAddress.AutoFilled("Room 101"),
|
||||||
|
roomAccess = RoomAccess.Knocking,
|
||||||
|
roomAddressErrorState = RoomAddressErrorState.None,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aConfigureRoomState() = ConfigureRoomState(
|
fun aConfigureRoomState(
|
||||||
config = CreateRoomConfig(),
|
config: CreateRoomConfig = CreateRoomConfig(),
|
||||||
avatarActions = persistentListOf(),
|
isKnockFeatureEnabled: Boolean = true,
|
||||||
createRoomAction = AsyncAction.Uninitialized,
|
avatarActions: List<AvatarAction> = emptyList(),
|
||||||
cameraPermissionState = aPermissionsState(showDialog = false),
|
createRoomAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
|
||||||
eventSink = { },
|
cameraPermissionState: PermissionsState = aPermissionsState(showDialog = false),
|
||||||
|
homeserverName: String = "matrix.org",
|
||||||
|
eventSink: (ConfigureRoomEvents) -> Unit = { },
|
||||||
|
) = ConfigureRoomState(
|
||||||
|
config = config,
|
||||||
|
isKnockFeatureEnabled = isKnockFeatureEnabled,
|
||||||
|
avatarActions = avatarActions.toImmutableList(),
|
||||||
|
createRoomAction = createRoomAction,
|
||||||
|
cameraPermissionState = cameraPermissionState,
|
||||||
|
homeserverName = homeserverName,
|
||||||
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,11 @@ import android.net.Uri
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
|
@ -21,6 +23,7 @@ import androidx.compose.foundation.selection.selectableGroup
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
|
@ -33,18 +36,22 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.features.createroom.impl.R
|
import io.element.android.features.createroom.impl.R
|
||||||
import io.element.android.features.createroom.impl.components.RoomPrivacyOption
|
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom
|
||||||
|
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtomSize
|
||||||
import io.element.android.libraries.designsystem.components.LabelledTextField
|
import io.element.android.libraries.designsystem.components.LabelledTextField
|
||||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||||
import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults
|
import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
|
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||||
import io.element.android.libraries.designsystem.modifiers.clearFocusOnTap
|
import io.element.android.libraries.designsystem.modifiers.clearFocusOnTap
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.TextField
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.ui.components.AvatarActionBottomSheet
|
import io.element.android.libraries.matrix.ui.components.AvatarActionBottomSheet
|
||||||
|
|
@ -72,11 +79,11 @@ fun ConfigureRoomView(
|
||||||
modifier = modifier.clearFocusOnTap(focusManager),
|
modifier = modifier.clearFocusOnTap(focusManager),
|
||||||
topBar = {
|
topBar = {
|
||||||
ConfigureRoomToolbar(
|
ConfigureRoomToolbar(
|
||||||
isNextActionEnabled = state.isCreateButtonEnabled,
|
isNextActionEnabled = state.config.isValid,
|
||||||
onBackClick = onBackClick,
|
onBackClick = onBackClick,
|
||||||
onNextClick = {
|
onNextClick = {
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
state.eventSink(ConfigureRoomEvents.CreateRoom(state.config))
|
state.eventSink(ConfigureRoomEvents.CreateRoom)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -103,23 +110,42 @@ fun ConfigureRoomView(
|
||||||
)
|
)
|
||||||
if (state.config.invites.isNotEmpty()) {
|
if (state.config.invites.isNotEmpty()) {
|
||||||
SelectedUsersRowList(
|
SelectedUsersRowList(
|
||||||
modifier = Modifier.padding(bottom = 16.dp),
|
|
||||||
contentPadding = PaddingValues(horizontal = 24.dp),
|
contentPadding = PaddingValues(horizontal = 24.dp),
|
||||||
selectedUsers = state.config.invites,
|
selectedUsers = state.config.invites,
|
||||||
onUserRemove = {
|
onUserRemove = {
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
state.eventSink(ConfigureRoomEvents.RemoveFromSelection(it))
|
state.eventSink(ConfigureRoomEvents.RemoveUserFromSelection(it))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
RoomPrivacyOptions(
|
RoomVisibilityOptions(
|
||||||
modifier = Modifier.padding(bottom = 40.dp),
|
selected = when (state.config.roomVisibility) {
|
||||||
selected = state.config.privacy,
|
is RoomVisibilityState.Private -> RoomVisibilityItem.Private
|
||||||
|
is RoomVisibilityState.Public -> RoomVisibilityItem.Public
|
||||||
|
},
|
||||||
onOptionClick = {
|
onOptionClick = {
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
state.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(it.privacy))
|
state.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(it))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
if (state.config.roomVisibility is RoomVisibilityState.Public && state.isKnockFeatureEnabled) {
|
||||||
|
RoomAccessOptions(
|
||||||
|
selected = when (state.config.roomVisibility.roomAccess) {
|
||||||
|
RoomAccess.Anyone -> RoomAccessItem.Anyone
|
||||||
|
RoomAccess.Knocking -> RoomAccessItem.AskToJoin
|
||||||
|
},
|
||||||
|
onOptionClick = {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
state.eventSink(ConfigureRoomEvents.RoomAccessChanged(it))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
RoomAddressField(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
address = state.config.roomVisibility.roomAddress,
|
||||||
|
homeserverName = state.homeserverName,
|
||||||
|
onAddressChange = { state.eventSink(ConfigureRoomEvents.RoomAddressChanged(it)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -139,7 +165,7 @@ fun ConfigureRoomView(
|
||||||
},
|
},
|
||||||
onSuccess = { onCreateRoomSuccess(it) },
|
onSuccess = { onCreateRoomSuccess(it) },
|
||||||
errorMessage = { stringResource(R.string.screen_create_room_error_creating_room) },
|
errorMessage = { stringResource(R.string.screen_create_room_error_creating_room) },
|
||||||
onRetry = { state.eventSink(ConfigureRoomEvents.CreateRoom(state.config)) },
|
onRetry = { state.eventSink(ConfigureRoomEvents.CreateRoom) },
|
||||||
onErrorDismiss = { state.eventSink(ConfigureRoomEvents.CancelCreateRoom) },
|
onErrorDismiss = { state.eventSink(ConfigureRoomEvents.CancelCreateRoom) },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -221,23 +247,123 @@ private fun RoomTopic(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RoomPrivacyOptions(
|
private fun ConfigureRoomOptions(
|
||||||
selected: RoomPrivacy?,
|
title: String,
|
||||||
onOptionClick: (RoomPrivacyItem) -> Unit,
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable ColumnScope.() -> Unit,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier.selectableGroup()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = ElementTheme.typography.fontBodyLgMedium,
|
||||||
|
color = ElementTheme.colors.textPrimary,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
)
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RoomVisibilityOptions(
|
||||||
|
selected: RoomVisibilityItem,
|
||||||
|
onOptionClick: (RoomVisibilityItem) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val items = roomPrivacyItems()
|
ConfigureRoomOptions(
|
||||||
Column(modifier = modifier.selectableGroup()) {
|
title = stringResource(R.string.screen_create_room_room_visibility_section_title),
|
||||||
items.forEach { item ->
|
modifier = modifier,
|
||||||
RoomPrivacyOption(
|
) {
|
||||||
roomPrivacyItem = item,
|
RoomVisibilityItem.entries.forEach { item ->
|
||||||
isSelected = selected == item.privacy,
|
val isSelected = item == selected
|
||||||
onOptionClick = onOptionClick,
|
ListItem(
|
||||||
|
leadingContent = ListItemContent.Custom {
|
||||||
|
RoundedIconAtom(
|
||||||
|
size = RoundedIconAtomSize.Big,
|
||||||
|
resourceId = item.icon,
|
||||||
|
tint = if (isSelected) ElementTheme.colors.iconPrimary else ElementTheme.colors.iconSecondary,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
headlineContent = { Text(text = stringResource(item.title)) },
|
||||||
|
supportingContent = { Text(text = stringResource(item.description)) },
|
||||||
|
trailingContent = ListItemContent.RadioButton(selected = isSelected),
|
||||||
|
onClick = { onOptionClick(item) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RoomAccessOptions(
|
||||||
|
selected: RoomAccessItem,
|
||||||
|
onOptionClick: (RoomAccessItem) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
ConfigureRoomOptions(
|
||||||
|
title = stringResource(R.string.screen_create_room_room_access_section_header),
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
RoomAccessItem.entries.forEach { item ->
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text(text = stringResource(item.title)) },
|
||||||
|
supportingContent = { Text(text = stringResource(item.description)) },
|
||||||
|
trailingContent = ListItemContent.RadioButton(selected = item == selected),
|
||||||
|
onClick = { onOptionClick(item) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RoomAddressField(
|
||||||
|
address: RoomAddress,
|
||||||
|
homeserverName: String,
|
||||||
|
onAddressChange: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
text = stringResource(R.string.screen_create_room_room_address_section_title),
|
||||||
|
)
|
||||||
|
|
||||||
|
TextField(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = address.value,
|
||||||
|
leadingIcon = {
|
||||||
|
Text(
|
||||||
|
text = "#",
|
||||||
|
style = ElementTheme.typography.fontBodyLgMedium,
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
Text(
|
||||||
|
text = homeserverName,
|
||||||
|
style = ElementTheme.typography.fontBodyLgMedium,
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
modifier = Modifier.padding(end = 16.dp)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
supportingText = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.screen_create_room_room_address_section_footer),
|
||||||
|
style = ElementTheme.typography.fontBodySmRegular,
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onValueChange = onAddressChange,
|
||||||
|
singleLine = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ConfigureRoomViewPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) = ElementPreview {
|
internal fun ConfigureRoomViewPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) = ElementPreview {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.createroom.impl.configureroom
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.createroom.JoinRuleOverride
|
||||||
|
|
||||||
|
enum class RoomAccess {
|
||||||
|
Anyone,
|
||||||
|
Knocking
|
||||||
|
}
|
||||||
|
|
||||||
|
fun RoomAccess.toJoinRule(): JoinRuleOverride {
|
||||||
|
return when (this) {
|
||||||
|
RoomAccess.Anyone -> JoinRuleOverride.None
|
||||||
|
RoomAccess.Knocking -> JoinRuleOverride.Knock
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.createroom.impl.configureroom
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import io.element.android.features.createroom.impl.R
|
||||||
|
|
||||||
|
enum class RoomAccessItem(
|
||||||
|
@StringRes val title: Int,
|
||||||
|
@StringRes val description: Int
|
||||||
|
) {
|
||||||
|
Anyone(
|
||||||
|
title = R.string.screen_create_room_room_access_section_anyone_option_title,
|
||||||
|
description = R.string.screen_create_room_room_access_section_anyone_option_description,
|
||||||
|
),
|
||||||
|
AskToJoin(
|
||||||
|
title = R.string.screen_create_room_room_access_section_knocking_option_title,
|
||||||
|
description = R.string.screen_create_room_room_access_section_knocking_option_description,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.createroom.impl.configureroom
|
||||||
|
|
||||||
|
sealed class RoomAddress(open val value: String) {
|
||||||
|
data class AutoFilled(override val value: String) : RoomAddress(value)
|
||||||
|
data class Edited(override val value: String) : RoomAddress(value)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.createroom.impl.configureroom
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the error state of a room address.
|
||||||
|
*/
|
||||||
|
sealed interface RoomAddressErrorState {
|
||||||
|
data object InvalidCharacters : RoomAddressErrorState
|
||||||
|
data object AlreadyExists : RoomAddressErrorState
|
||||||
|
data object None : RoomAddressErrorState
|
||||||
|
}
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* Please see LICENSE in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl.configureroom
|
|
||||||
|
|
||||||
enum class RoomPrivacy {
|
|
||||||
Private,
|
|
||||||
Public,
|
|
||||||
}
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* Please see LICENSE in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl.configureroom
|
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import io.element.android.features.createroom.impl.R
|
|
||||||
import io.element.android.libraries.designsystem.icons.CompoundDrawables
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
|
||||||
|
|
||||||
data class RoomPrivacyItem(
|
|
||||||
val privacy: RoomPrivacy,
|
|
||||||
@DrawableRes val icon: Int,
|
|
||||||
val title: String,
|
|
||||||
val description: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun roomPrivacyItems(): ImmutableList<RoomPrivacyItem> {
|
|
||||||
return RoomPrivacy.entries
|
|
||||||
.map {
|
|
||||||
when (it) {
|
|
||||||
RoomPrivacy.Private -> RoomPrivacyItem(
|
|
||||||
privacy = it,
|
|
||||||
icon = CompoundDrawables.ic_compound_lock_solid,
|
|
||||||
title = stringResource(R.string.screen_create_room_private_option_title),
|
|
||||||
description = stringResource(R.string.screen_create_room_private_option_description),
|
|
||||||
)
|
|
||||||
RoomPrivacy.Public -> RoomPrivacyItem(
|
|
||||||
privacy = it,
|
|
||||||
icon = CompoundDrawables.ic_compound_public,
|
|
||||||
title = stringResource(R.string.screen_create_room_public_option_title),
|
|
||||||
description = stringResource(R.string.screen_create_room_public_option_description),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.toImmutableList()
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023, 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.createroom.impl.configureroom
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import io.element.android.features.createroom.impl.R
|
||||||
|
import io.element.android.libraries.designsystem.icons.CompoundDrawables
|
||||||
|
|
||||||
|
enum class RoomVisibilityItem(
|
||||||
|
@DrawableRes val icon: Int,
|
||||||
|
@StringRes val title: Int,
|
||||||
|
@StringRes val description: Int
|
||||||
|
) {
|
||||||
|
Private(
|
||||||
|
icon = CompoundDrawables.ic_compound_lock,
|
||||||
|
title = R.string.screen_create_room_private_option_title,
|
||||||
|
description = R.string.screen_create_room_private_option_description,
|
||||||
|
),
|
||||||
|
Public(
|
||||||
|
icon = CompoundDrawables.ic_compound_public,
|
||||||
|
title = R.string.screen_create_room_public_option_title,
|
||||||
|
description = R.string.screen_create_room_public_option_description,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023, 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.createroom.impl.configureroom
|
||||||
|
|
||||||
|
import java.util.Optional
|
||||||
|
|
||||||
|
sealed interface RoomVisibilityState {
|
||||||
|
data object Private : RoomVisibilityState
|
||||||
|
|
||||||
|
data class Public(
|
||||||
|
val roomAddress: RoomAddress,
|
||||||
|
val roomAddressErrorState: RoomAddressErrorState,
|
||||||
|
val roomAccess: RoomAccess,
|
||||||
|
) : RoomVisibilityState
|
||||||
|
|
||||||
|
fun roomAddress(): Optional<String> {
|
||||||
|
return when (this) {
|
||||||
|
is Private -> Optional.empty()
|
||||||
|
is Public -> Optional.of(roomAddress.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isValid(): Boolean {
|
||||||
|
return when (this) {
|
||||||
|
is Private -> true
|
||||||
|
is Public -> roomAddressErrorState is RoomAddressErrorState.None && roomAddress.value.isNotEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,9 @@
|
||||||
<string name="screen_create_room_private_option_title">"Прыватны пакой (толькі па запрашэнні)"</string>
|
<string name="screen_create_room_private_option_title">"Прыватны пакой (толькі па запрашэнні)"</string>
|
||||||
<string name="screen_create_room_public_option_description">"Паведамленні не зашыфраваны, і кожны можа іх прачытаць. Вы можаце ўключыць шыфраванне пазней."</string>
|
<string name="screen_create_room_public_option_description">"Паведамленні не зашыфраваны, і кожны можа іх прачытаць. Вы можаце ўключыць шыфраванне пазней."</string>
|
||||||
<string name="screen_create_room_public_option_title">"Публічны пакой (для ўсіх)"</string>
|
<string name="screen_create_room_public_option_title">"Публічны пакой (для ўсіх)"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_anyone_option_title">"Хто заўгодна"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_header">"Доступ у пакой"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_option_title">"Папрасіце далучыцца"</string>
|
||||||
<string name="screen_create_room_room_name_label">"Назва пакоя"</string>
|
<string name="screen_create_room_room_name_label">"Назва пакоя"</string>
|
||||||
<string name="screen_create_room_title">"Стварыце пакой"</string>
|
<string name="screen_create_room_title">"Стварыце пакой"</string>
|
||||||
<string name="screen_create_room_topic_label">"Тэма (неабавязкова)"</string>
|
<string name="screen_create_room_topic_label">"Тэма (неабавязкова)"</string>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,15 @@
|
||||||
<string name="screen_create_room_public_option_description">"Tuto místnost může najít kdokoli.
|
<string name="screen_create_room_public_option_description">"Tuto místnost může najít kdokoli.
|
||||||
To můžete kdykoli změnit v nastavení místnosti."</string>
|
To můžete kdykoli změnit v nastavení místnosti."</string>
|
||||||
<string name="screen_create_room_public_option_title">"Veřejná místnost"</string>
|
<string name="screen_create_room_public_option_title">"Veřejná místnost"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_anyone_option_description">"Do této místnosti může vstoupit kdokoli"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_anyone_option_title">"Kdokoliv"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_header">"Přístup do místnosti"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_option_description">"Kdokoli může požádat o vstup do místnosti, ale správce nebo moderátor bude muset žádost přijmout"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_option_title">"Požádat o připojení"</string>
|
||||||
|
<string name="screen_create_room_room_address_section_footer">"Aby byla tato místnost viditelná v adresáři veřejných místností, budete potřebovat adresu místnosti."</string>
|
||||||
|
<string name="screen_create_room_room_address_section_title">"Adresa místnosti"</string>
|
||||||
<string name="screen_create_room_room_name_label">"Název místnosti"</string>
|
<string name="screen_create_room_room_name_label">"Název místnosti"</string>
|
||||||
|
<string name="screen_create_room_room_visibility_section_title">"Viditelnost místnosti"</string>
|
||||||
<string name="screen_create_room_title">"Vytvořit místnost"</string>
|
<string name="screen_create_room_title">"Vytvořit místnost"</string>
|
||||||
<string name="screen_create_room_topic_label">"Téma (nepovinné)"</string>
|
<string name="screen_create_room_topic_label">"Téma (nepovinné)"</string>
|
||||||
<string name="screen_start_chat_error_starting_chat">"Při pokusu o zahájení chatu došlo k chybě"</string>
|
<string name="screen_start_chat_error_starting_chat">"Při pokusu o zahájení chatu došlo k chybě"</string>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,11 @@
|
||||||
<string name="screen_create_room_public_option_description">"Ο καθένας μπορεί να βρει αυτό το δωμάτιο.
|
<string name="screen_create_room_public_option_description">"Ο καθένας μπορεί να βρει αυτό το δωμάτιο.
|
||||||
Μπορείς να το αλλάξεις ανά πάσα στιγμή στις ρυθμίσεις δωματίου."</string>
|
Μπορείς να το αλλάξεις ανά πάσα στιγμή στις ρυθμίσεις δωματίου."</string>
|
||||||
<string name="screen_create_room_public_option_title">"Δημόσιο δωμάτιο"</string>
|
<string name="screen_create_room_public_option_title">"Δημόσιο δωμάτιο"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_anyone_option_description">"Οποιοσδήποτε μπορεί να συμμετάσχει σε αυτό το δωμάτιο"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_anyone_option_title">"Οποιοσδήποτε"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_header">"Πρόσβαση Δωματίου"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_option_description">"Οποιοσδήποτε μπορεί να ζητήσει να συμμετάσχει στο δωμάτιο, αλλά ένας διαχειριστής ή συντονιστής θα πρέπει να αποδεχθεί το αίτημα"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_option_title">"Αίτημα συμμετοχής"</string>
|
||||||
<string name="screen_create_room_room_name_label">"Όνομα δωματίου"</string>
|
<string name="screen_create_room_room_name_label">"Όνομα δωματίου"</string>
|
||||||
<string name="screen_create_room_title">"Δημιούργησε ένα δωμάτιο"</string>
|
<string name="screen_create_room_title">"Δημιούργησε ένα δωμάτιο"</string>
|
||||||
<string name="screen_create_room_topic_label">"Θέμα (προαιρετικό)"</string>
|
<string name="screen_create_room_topic_label">"Θέμα (προαιρετικό)"</string>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,15 @@
|
||||||
<string name="screen_create_room_public_option_description">"Kõik saavad seda jututuba leida.
|
<string name="screen_create_room_public_option_description">"Kõik saavad seda jututuba leida.
|
||||||
Sa võid seda jututoa seadistustest alati muuta."</string>
|
Sa võid seda jututoa seadistustest alati muuta."</string>
|
||||||
<string name="screen_create_room_public_option_title">"Avalik jututuba"</string>
|
<string name="screen_create_room_public_option_title">"Avalik jututuba"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_anyone_option_description">"Kõik võivad selle jututoaga liituda"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_anyone_option_title">"Kõik"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_header">"Ligipääs jututoale"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_option_description">"Kõik võivad paluda selle jututoaga liitumist, kuid peakasutaja või moderaator peavad selle kinnitama"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_option_title">"Küsi võimalust liitumiseks"</string>
|
||||||
|
<string name="screen_create_room_room_address_section_footer">"Selleks, et see jututuba oleks nähtav jututubade avalikus kataloogis, sa vajad jututoa aadressi."</string>
|
||||||
|
<string name="screen_create_room_room_address_section_title">"Jututoa aadress"</string>
|
||||||
<string name="screen_create_room_room_name_label">"Jututoa nimi"</string>
|
<string name="screen_create_room_room_name_label">"Jututoa nimi"</string>
|
||||||
|
<string name="screen_create_room_room_visibility_section_title">"Jututoa nähtavus"</string>
|
||||||
<string name="screen_create_room_title">"Loo jututuba"</string>
|
<string name="screen_create_room_title">"Loo jututuba"</string>
|
||||||
<string name="screen_create_room_topic_label">"Teema (kui soovid lisada)"</string>
|
<string name="screen_create_room_topic_label">"Teema (kui soovid lisada)"</string>
|
||||||
<string name="screen_start_chat_error_starting_chat">"Vestluse alustamisel tekkis viga"</string>
|
<string name="screen_start_chat_error_starting_chat">"Vestluse alustamisel tekkis viga"</string>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,15 @@
|
||||||
<string name="screen_create_room_public_option_description">"N’importe qui peut trouver ce salon.
|
<string name="screen_create_room_public_option_description">"N’importe qui peut trouver ce salon.
|
||||||
Vous pouvez modifier cela à tout moment dans les paramètres du salon."</string>
|
Vous pouvez modifier cela à tout moment dans les paramètres du salon."</string>
|
||||||
<string name="screen_create_room_public_option_title">"Salon public"</string>
|
<string name="screen_create_room_public_option_title">"Salon public"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_anyone_option_description">"Tout le monde peut rejoindre ce salon"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_anyone_option_title">"Tout le monde"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_header">"Accès au salon"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_option_description">"Tout le monde peut demander à rejoindre le salon, mais un administrateur ou un modérateur devra accepter la demande"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_option_title">"Demander à rejoindre"</string>
|
||||||
|
<string name="screen_create_room_room_address_section_footer">"Pour que ce salon soit visible dans le répertoire des salons publics, vous aurez besoin d’une adresse de salon."</string>
|
||||||
|
<string name="screen_create_room_room_address_section_title">"Adresse du salon"</string>
|
||||||
<string name="screen_create_room_room_name_label">"Nom du salon"</string>
|
<string name="screen_create_room_room_name_label">"Nom du salon"</string>
|
||||||
|
<string name="screen_create_room_room_visibility_section_title">"Visibilité du salon"</string>
|
||||||
<string name="screen_create_room_title">"Créer un salon"</string>
|
<string name="screen_create_room_title">"Créer un salon"</string>
|
||||||
<string name="screen_create_room_topic_label">"Sujet (facultatif)"</string>
|
<string name="screen_create_room_topic_label">"Sujet (facultatif)"</string>
|
||||||
<string name="screen_start_chat_error_starting_chat">"Une erreur s’est produite lors de la tentative de création de la discussion"</string>
|
<string name="screen_start_chat_error_starting_chat">"Une erreur s’est produite lors de la tentative de création de la discussion"</string>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,15 @@
|
||||||
<string name="screen_create_room_public_option_description">"Bárki megtalálhatja ezt a szobát.
|
<string name="screen_create_room_public_option_description">"Bárki megtalálhatja ezt a szobát.
|
||||||
Ezt bármikor módosíthatja a szobabeállításokban."</string>
|
Ezt bármikor módosíthatja a szobabeállításokban."</string>
|
||||||
<string name="screen_create_room_public_option_title">"Nyilvános szoba"</string>
|
<string name="screen_create_room_public_option_title">"Nyilvános szoba"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_anyone_option_description">"Bárki csatlakozhat ehhez a szobához"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_anyone_option_title">"Bárki"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_header">"Szobahozzáférés"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_option_description">"Bárki kérheti, hogy csatlakozzon a szobához, de egy adminisztrátornak vagy moderátornak el kell fogadnia a kérést"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_option_title">"Csatlakozás kérése"</string>
|
||||||
|
<string name="screen_create_room_room_address_section_footer">"Ahhoz, hogy ez a szoba látható legyen a nyilvános szobák címtárában, meg kell adnia a szoba címét."</string>
|
||||||
|
<string name="screen_create_room_room_address_section_title">"Szoba címe"</string>
|
||||||
<string name="screen_create_room_room_name_label">"Szoba neve"</string>
|
<string name="screen_create_room_room_name_label">"Szoba neve"</string>
|
||||||
|
<string name="screen_create_room_room_visibility_section_title">"Szoba láthatósága"</string>
|
||||||
<string name="screen_create_room_title">"Szoba létrehozása"</string>
|
<string name="screen_create_room_title">"Szoba létrehozása"</string>
|
||||||
<string name="screen_create_room_topic_label">"Téma (nem kötelező)"</string>
|
<string name="screen_create_room_topic_label">"Téma (nem kötelező)"</string>
|
||||||
<string name="screen_start_chat_error_starting_chat">"Hiba történt a csevegés indításakor"</string>
|
<string name="screen_start_chat_error_starting_chat">"Hiba történt a csevegés indításakor"</string>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,15 @@
|
||||||
<string name="screen_create_room_public_option_description">"Siapa pun dapat mencari ruangan ini.
|
<string name="screen_create_room_public_option_description">"Siapa pun dapat mencari ruangan ini.
|
||||||
Anda dapat mengubah ini kapan pun dalam pengaturan ruangan."</string>
|
Anda dapat mengubah ini kapan pun dalam pengaturan ruangan."</string>
|
||||||
<string name="screen_create_room_public_option_title">"Ruangan publik"</string>
|
<string name="screen_create_room_public_option_title">"Ruangan publik"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_anyone_option_description">"Siapa pun dapat bergabung dengan ruangan ini"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_anyone_option_title">"Siapa pun"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_header">"Akses Ruangan"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_option_description">"Siapa pun dapat meminta untuk bergabung dengan ruangan tetapi administrator atau moderator harus menerima permintaan tersebut"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_option_title">"Minta untuk bergabung"</string>
|
||||||
|
<string name="screen_create_room_room_address_section_footer">"Supaya ruangan ini terlihat di direktori ruangan publik, Anda memerlukan alamat ruangan."</string>
|
||||||
|
<string name="screen_create_room_room_address_section_title">"Alamat ruangan"</string>
|
||||||
<string name="screen_create_room_room_name_label">"Nama ruangan"</string>
|
<string name="screen_create_room_room_name_label">"Nama ruangan"</string>
|
||||||
|
<string name="screen_create_room_room_visibility_section_title">"Keterlihatan ruangan"</string>
|
||||||
<string name="screen_create_room_title">"Buat ruangan"</string>
|
<string name="screen_create_room_title">"Buat ruangan"</string>
|
||||||
<string name="screen_create_room_topic_label">"Topik (opsional)"</string>
|
<string name="screen_create_room_topic_label">"Topik (opsional)"</string>
|
||||||
<string name="screen_start_chat_error_starting_chat">"Terjadi kesalahan saat mencoba memulai obrolan"</string>
|
<string name="screen_start_chat_error_starting_chat">"Terjadi kesalahan saat mencoba memulai obrolan"</string>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,11 @@
|
||||||
<string name="screen_create_room_public_option_description">"Iedereen kan deze kamer vinden.
|
<string name="screen_create_room_public_option_description">"Iedereen kan deze kamer vinden.
|
||||||
Je kunt dit op elk gewenst moment wijzigen in de kamerinstellingen."</string>
|
Je kunt dit op elk gewenst moment wijzigen in de kamerinstellingen."</string>
|
||||||
<string name="screen_create_room_public_option_title">"Openbare kamer"</string>
|
<string name="screen_create_room_public_option_title">"Openbare kamer"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_anyone_option_description">"Iedereen kan toetreden tot deze kamer"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_anyone_option_title">"Iedereen"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_header">"Toegang tot de kamer"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_option_description">"Iedereen kan vragen om toe te treden tot de kamer, maar een beheerder of moderator moet het verzoek accepteren"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_option_title">"Vraag om toe te treden"</string>
|
||||||
<string name="screen_create_room_room_name_label">"Naam van de kamer"</string>
|
<string name="screen_create_room_room_name_label">"Naam van de kamer"</string>
|
||||||
<string name="screen_create_room_title">"Creëer een kamer"</string>
|
<string name="screen_create_room_title">"Creëer een kamer"</string>
|
||||||
<string name="screen_create_room_topic_label">"Onderwerp (optioneel)"</string>
|
<string name="screen_create_room_topic_label">"Onderwerp (optioneel)"</string>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,15 @@
|
||||||
<string name="screen_create_room_public_option_description">"Każdy może znaleźć ten pokój.
|
<string name="screen_create_room_public_option_description">"Każdy może znaleźć ten pokój.
|
||||||
Możesz to zmienić w ustawieniach pokoju."</string>
|
Możesz to zmienić w ustawieniach pokoju."</string>
|
||||||
<string name="screen_create_room_public_option_title">"Pokój publiczny"</string>
|
<string name="screen_create_room_public_option_title">"Pokój publiczny"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_anyone_option_description">"Każdy może dołączyć do tego pokoju"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_anyone_option_title">"Wszyscy"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_header">"Dostęp do pokoju"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_option_description">"Każdy może poprosić o dołączenie do pokoju, ale administrator lub moderator będzie musiał zatwierdzić prośbę"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_option_title">"Poproś o dołączenie"</string>
|
||||||
|
<string name="screen_create_room_room_address_section_footer">"Aby ten pokój był widoczny w katalogu pomieszczeń publicznych, będziesz potrzebował adres pokoju."</string>
|
||||||
|
<string name="screen_create_room_room_address_section_title">"Adres pokoju"</string>
|
||||||
<string name="screen_create_room_room_name_label">"Nazwa pokoju"</string>
|
<string name="screen_create_room_room_name_label">"Nazwa pokoju"</string>
|
||||||
|
<string name="screen_create_room_room_visibility_section_title">"Widoczność pomieszczenia"</string>
|
||||||
<string name="screen_create_room_title">"Utwórz pokój"</string>
|
<string name="screen_create_room_title">"Utwórz pokój"</string>
|
||||||
<string name="screen_create_room_topic_label">"Temat (opcjonalnie)"</string>
|
<string name="screen_create_room_topic_label">"Temat (opcjonalnie)"</string>
|
||||||
<string name="screen_start_chat_error_starting_chat">"Wystąpił błąd podczas próby rozpoczęcia czatu"</string>
|
<string name="screen_start_chat_error_starting_chat">"Wystąpił błąd podczas próby rozpoczęcia czatu"</string>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,15 @@
|
||||||
<string name="screen_create_room_public_option_description">"Qualquer um pode encontrar esta sala.
|
<string name="screen_create_room_public_option_description">"Qualquer um pode encontrar esta sala.
|
||||||
Pode alterar esta opção nas definições da sala."</string>
|
Pode alterar esta opção nas definições da sala."</string>
|
||||||
<string name="screen_create_room_public_option_title">"Sala pública"</string>
|
<string name="screen_create_room_public_option_title">"Sala pública"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_anyone_option_description">"Qualquer pessoa pode entrar nesta sala"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_anyone_option_title">"Qualquer pessoa"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_header">"Acesso à sala"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_option_description">"Qualquer pessoa pode pedir para entrar na sala, mas um administrador ou um moderador terá de aceitar o pedido"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_option_title">"Pedir para participar"</string>
|
||||||
|
<string name="screen_create_room_room_address_section_footer">"Para que esta sala seja visível no diretório público de salas, precisas de um endereço de sala."</string>
|
||||||
|
<string name="screen_create_room_room_address_section_title">"Endereço da sala"</string>
|
||||||
<string name="screen_create_room_room_name_label">"Nome da sala"</string>
|
<string name="screen_create_room_room_name_label">"Nome da sala"</string>
|
||||||
|
<string name="screen_create_room_room_visibility_section_title">"Visibilidade da sala"</string>
|
||||||
<string name="screen_create_room_title">"Criar uma sala"</string>
|
<string name="screen_create_room_title">"Criar uma sala"</string>
|
||||||
<string name="screen_create_room_topic_label">"Descrição (opcional)"</string>
|
<string name="screen_create_room_topic_label">"Descrição (opcional)"</string>
|
||||||
<string name="screen_start_chat_error_starting_chat">"Ocorreu um erro ao tentar iniciar uma conversa"</string>
|
<string name="screen_start_chat_error_starting_chat">"Ocorreu um erro ao tentar iniciar uma conversa"</string>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,15 @@
|
||||||
<string name="screen_create_room_public_option_description">"Любой желающий может найти эту комнату.
|
<string name="screen_create_room_public_option_description">"Любой желающий может найти эту комнату.
|
||||||
Вы можете изменить это в любое время в настройках комнаты."</string>
|
Вы можете изменить это в любое время в настройках комнаты."</string>
|
||||||
<string name="screen_create_room_public_option_title">"Общедоступная комната"</string>
|
<string name="screen_create_room_public_option_title">"Общедоступная комната"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_anyone_option_description">"Любой желающий может присоединиться к этой комнате"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_anyone_option_title">"Любой"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_header">"Доступ в комнату"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_option_description">"Любой желающий может подать заявку на присоединение к комнате, но администратор или модератор должен будет принять запрос."</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_option_title">"Попросить присоединиться"</string>
|
||||||
|
<string name="screen_create_room_room_address_section_footer">"Чтобы эта комната была видна в каталоге общедоступных, вам необходим ее адрес"</string>
|
||||||
|
<string name="screen_create_room_room_address_section_title">"Адрес комнаты"</string>
|
||||||
<string name="screen_create_room_room_name_label">"Название комнаты"</string>
|
<string name="screen_create_room_room_name_label">"Название комнаты"</string>
|
||||||
|
<string name="screen_create_room_room_visibility_section_title">"Видимость комнаты"</string>
|
||||||
<string name="screen_create_room_title">"Создать комнату"</string>
|
<string name="screen_create_room_title">"Создать комнату"</string>
|
||||||
<string name="screen_create_room_topic_label">"Тема (необязательно)"</string>
|
<string name="screen_create_room_topic_label">"Тема (необязательно)"</string>
|
||||||
<string name="screen_start_chat_error_starting_chat">"Произошла ошибка при запуске чата"</string>
|
<string name="screen_start_chat_error_starting_chat">"Произошла ошибка при запуске чата"</string>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,15 @@
|
||||||
<string name="screen_create_room_public_option_description">"Túto miestnosť môže nájsť ktokoľvek.
|
<string name="screen_create_room_public_option_description">"Túto miestnosť môže nájsť ktokoľvek.
|
||||||
Môžete to kedykoľvek zmeniť v nastaveniach miestnosti."</string>
|
Môžete to kedykoľvek zmeniť v nastaveniach miestnosti."</string>
|
||||||
<string name="screen_create_room_public_option_title">"Verejná miestnosť"</string>
|
<string name="screen_create_room_public_option_title">"Verejná miestnosť"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_anyone_option_description">"Do tejto miestnosti sa môže pripojiť ktokoľvek"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_anyone_option_title">"Ktokoľvek"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_header">"Prístup do miestnosti"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_option_description">"Ktokoľvek môže požiadať o pripojenie sa k miestnosti, ale administrátor alebo moderátor bude musieť žiadosť schváliť"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_option_title">"Požiadať o pripojenie"</string>
|
||||||
|
<string name="screen_create_room_room_address_section_footer">"Aby bola táto miestnosť viditeľná v adresári verejných miestností, budete potrebovať adresu miestnosti."</string>
|
||||||
|
<string name="screen_create_room_room_address_section_title">"Adresa miestnosti"</string>
|
||||||
<string name="screen_create_room_room_name_label">"Názov miestnosti"</string>
|
<string name="screen_create_room_room_name_label">"Názov miestnosti"</string>
|
||||||
|
<string name="screen_create_room_room_visibility_section_title">"Viditeľnosť miestnosti"</string>
|
||||||
<string name="screen_create_room_title">"Vytvoriť miestnosť"</string>
|
<string name="screen_create_room_title">"Vytvoriť miestnosť"</string>
|
||||||
<string name="screen_create_room_topic_label">"Téma (voliteľné)"</string>
|
<string name="screen_create_room_topic_label">"Téma (voliteľné)"</string>
|
||||||
<string name="screen_start_chat_error_starting_chat">"Pri pokuse o spustenie konverzácie sa vyskytla chyba"</string>
|
<string name="screen_start_chat_error_starting_chat">"Pri pokuse o spustenie konverzácie sa vyskytla chyba"</string>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,15 @@
|
||||||
<string name="screen_create_room_public_option_description">"Anyone can find this room.
|
<string name="screen_create_room_public_option_description">"Anyone can find this room.
|
||||||
You can change this anytime in room settings."</string>
|
You can change this anytime in room settings."</string>
|
||||||
<string name="screen_create_room_public_option_title">"Public room"</string>
|
<string name="screen_create_room_public_option_title">"Public room"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_anyone_option_description">"Anyone can join this room"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_anyone_option_title">"Anyone"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_header">"Room Access"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_option_description">"Anyone can ask to join the room but an administrator or a moderator will have to accept the request"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_option_title">"Ask to join"</string>
|
||||||
|
<string name="screen_create_room_room_address_section_footer">"In order for this room to be visible in the public room directory, you will need a room address."</string>
|
||||||
|
<string name="screen_create_room_room_address_section_title">"Room address"</string>
|
||||||
<string name="screen_create_room_room_name_label">"Room name"</string>
|
<string name="screen_create_room_room_name_label">"Room name"</string>
|
||||||
|
<string name="screen_create_room_room_visibility_section_title">"Room visibility"</string>
|
||||||
<string name="screen_create_room_title">"Create a room"</string>
|
<string name="screen_create_room_title">"Create a room"</string>
|
||||||
<string name="screen_create_room_topic_label">"Topic (optional)"</string>
|
<string name="screen_create_room_topic_label">"Topic (optional)"</string>
|
||||||
<string name="screen_start_chat_error_starting_chat">"An error occurred when trying to start a chat"</string>
|
<string name="screen_start_chat_error_starting_chat">"An error occurred when trying to start a chat"</string>
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,16 @@
|
||||||
package io.element.android.features.createroom.impl.configureroom
|
package io.element.android.features.createroom.impl.configureroom
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import app.cash.molecule.RecompositionMode
|
import app.cash.turbine.TurbineTestContext
|
||||||
import app.cash.molecule.moleculeFlow
|
|
||||||
import app.cash.turbine.test
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import im.vector.app.features.analytics.plan.CreatedRoom
|
import im.vector.app.features.analytics.plan.CreatedRoom
|
||||||
import io.element.android.features.createroom.impl.CreateRoomConfig
|
import io.element.android.features.createroom.impl.CreateRoomConfig
|
||||||
import io.element.android.features.createroom.impl.CreateRoomDataStore
|
import io.element.android.features.createroom.impl.CreateRoomDataStore
|
||||||
import io.element.android.features.createroom.impl.userlist.UserListDataStore
|
import io.element.android.features.createroom.impl.userlist.UserListDataStore
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||||
import io.element.android.libraries.matrix.test.A_MESSAGE
|
import io.element.android.libraries.matrix.test.A_MESSAGE
|
||||||
|
|
@ -25,13 +26,18 @@ import io.element.android.libraries.matrix.test.A_THROWABLE
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
||||||
|
import io.element.android.libraries.mediapickers.api.PickerProvider
|
||||||
import io.element.android.libraries.mediapickers.test.FakePickerProvider
|
import io.element.android.libraries.mediapickers.test.FakePickerProvider
|
||||||
|
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
||||||
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
||||||
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||||
|
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||||
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
|
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
|
||||||
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
|
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
|
||||||
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
import io.element.android.tests.testutils.test
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.mockkStatic
|
import io.mockk.mockkStatic
|
||||||
|
|
@ -56,33 +62,8 @@ class ConfigureRoomPresenterTest {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val warmUpRule = WarmUpRule()
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
||||||
private lateinit var presenter: ConfigureRoomPresenter
|
|
||||||
private lateinit var userListDataStore: UserListDataStore
|
|
||||||
private lateinit var createRoomDataStore: CreateRoomDataStore
|
|
||||||
private lateinit var fakeMatrixClient: FakeMatrixClient
|
|
||||||
private lateinit var fakePickerProvider: FakePickerProvider
|
|
||||||
private lateinit var fakeMediaPreProcessor: FakeMediaPreProcessor
|
|
||||||
private lateinit var fakeAnalyticsService: FakeAnalyticsService
|
|
||||||
private lateinit var fakePermissionsPresenter: FakePermissionsPresenter
|
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
fakeMatrixClient = FakeMatrixClient()
|
|
||||||
userListDataStore = UserListDataStore()
|
|
||||||
createRoomDataStore = CreateRoomDataStore(userListDataStore)
|
|
||||||
fakePickerProvider = FakePickerProvider()
|
|
||||||
fakeMediaPreProcessor = FakeMediaPreProcessor()
|
|
||||||
fakeAnalyticsService = FakeAnalyticsService()
|
|
||||||
fakePermissionsPresenter = FakePermissionsPresenter()
|
|
||||||
presenter = ConfigureRoomPresenter(
|
|
||||||
dataStore = createRoomDataStore,
|
|
||||||
matrixClient = fakeMatrixClient,
|
|
||||||
mediaPickerProvider = fakePickerProvider,
|
|
||||||
mediaPreProcessor = fakeMediaPreProcessor,
|
|
||||||
analyticsService = fakeAnalyticsService,
|
|
||||||
permissionsPresenterFactory = FakePermissionsPresenterFactory(fakePermissionsPresenter),
|
|
||||||
)
|
|
||||||
|
|
||||||
mockkStatic(File::readBytes)
|
mockkStatic(File::readBytes)
|
||||||
every { any<File>().readBytes() } returns byteArrayOf()
|
every { any<File>().readBytes() } returns byteArrayOf()
|
||||||
}
|
}
|
||||||
|
|
@ -94,50 +75,56 @@ class ConfigureRoomPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
val presenter = createConfigureRoomPresenter()
|
||||||
presenter.present()
|
presenter.test {
|
||||||
}.test {
|
val initialState = initialState()
|
||||||
val initialState = awaitItem()
|
|
||||||
assertThat(initialState.config).isEqualTo(CreateRoomConfig())
|
assertThat(initialState.config).isEqualTo(CreateRoomConfig())
|
||||||
assertThat(initialState.config.roomName).isNull()
|
assertThat(initialState.config.roomName).isNull()
|
||||||
assertThat(initialState.config.topic).isNull()
|
assertThat(initialState.config.topic).isNull()
|
||||||
assertThat(initialState.config.invites).isEmpty()
|
assertThat(initialState.config.invites).isEmpty()
|
||||||
assertThat(initialState.config.avatarUri).isNull()
|
assertThat(initialState.config.avatarUri).isNull()
|
||||||
assertThat(initialState.config.privacy).isEqualTo(RoomPrivacy.Private)
|
assertThat(initialState.config.roomVisibility).isEqualTo(RoomVisibilityState.Private)
|
||||||
|
assertThat(initialState.createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||||
|
assertThat(initialState.homeserverName).isEqualTo("matrix.org")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - create room button is enabled only if the required fields are completed`() = runTest {
|
fun `present - create room button is enabled only if the required fields are completed`() = runTest {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
val presenter = createConfigureRoomPresenter()
|
||||||
presenter.present()
|
presenter.test {
|
||||||
}.test {
|
val initialState = initialState()
|
||||||
val initialState = awaitItem()
|
|
||||||
var config = initialState.config
|
var config = initialState.config
|
||||||
assertThat(initialState.isCreateButtonEnabled).isFalse()
|
assertThat(initialState.config.isValid).isFalse()
|
||||||
|
|
||||||
// Room name not empty
|
// Room name not empty
|
||||||
initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME))
|
initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME))
|
||||||
var newState: ConfigureRoomState = awaitItem()
|
var newState: ConfigureRoomState = awaitItem()
|
||||||
config = config.copy(roomName = A_ROOM_NAME)
|
config = config.copy(roomName = A_ROOM_NAME)
|
||||||
assertThat(newState.config).isEqualTo(config)
|
assertThat(newState.config).isEqualTo(config)
|
||||||
assertThat(newState.isCreateButtonEnabled).isTrue()
|
assertThat(newState.config.isValid).isTrue()
|
||||||
|
|
||||||
// Clear room name
|
// Clear room name
|
||||||
newState.eventSink(ConfigureRoomEvents.RoomNameChanged(""))
|
newState.eventSink(ConfigureRoomEvents.RoomNameChanged(""))
|
||||||
newState = awaitItem()
|
newState = awaitItem()
|
||||||
config = config.copy(roomName = null)
|
config = config.copy(roomName = null)
|
||||||
assertThat(newState.config).isEqualTo(config)
|
assertThat(newState.config).isEqualTo(config)
|
||||||
assertThat(newState.isCreateButtonEnabled).isFalse()
|
assertThat(newState.config.isValid).isFalse()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - state is updated when fields are changed`() = runTest {
|
fun `present - state is updated when fields are changed`() = runTest {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
val userListDataStore = UserListDataStore()
|
||||||
presenter.present()
|
val pickerProvider = FakePickerProvider()
|
||||||
}.test {
|
val permissionsPresenter = FakePermissionsPresenter()
|
||||||
val initialState = awaitItem()
|
val presenter = createConfigureRoomPresenter(
|
||||||
|
createRoomDataStore = CreateRoomDataStore(userListDataStore),
|
||||||
|
pickerProvider = pickerProvider,
|
||||||
|
permissionsPresenter = permissionsPresenter,
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
val initialState = initialState()
|
||||||
var expectedConfig = CreateRoomConfig()
|
var expectedConfig = CreateRoomConfig()
|
||||||
assertThat(initialState.config).isEqualTo(expectedConfig)
|
assertThat(initialState.config).isEqualTo(expectedConfig)
|
||||||
|
|
||||||
|
|
@ -165,22 +152,22 @@ class ConfigureRoomPresenterTest {
|
||||||
|
|
||||||
// Room avatar
|
// Room avatar
|
||||||
// Pick avatar
|
// Pick avatar
|
||||||
fakePickerProvider.givenResult(null)
|
pickerProvider.givenResult(null)
|
||||||
// From gallery
|
// From gallery
|
||||||
val uriFromGallery = Uri.parse(AN_URI_FROM_GALLERY)
|
val uriFromGallery = Uri.parse(AN_URI_FROM_GALLERY)
|
||||||
fakePickerProvider.givenResult(uriFromGallery)
|
pickerProvider.givenResult(uriFromGallery)
|
||||||
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
|
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
|
||||||
newState = awaitItem()
|
newState = awaitItem()
|
||||||
expectedConfig = expectedConfig.copy(avatarUri = uriFromGallery)
|
expectedConfig = expectedConfig.copy(avatarUri = uriFromGallery)
|
||||||
assertThat(newState.config).isEqualTo(expectedConfig)
|
assertThat(newState.config).isEqualTo(expectedConfig)
|
||||||
// From camera
|
// From camera
|
||||||
val uriFromCamera = Uri.parse(AN_URI_FROM_CAMERA)
|
val uriFromCamera = Uri.parse(AN_URI_FROM_CAMERA)
|
||||||
fakePickerProvider.givenResult(uriFromCamera)
|
pickerProvider.givenResult(uriFromCamera)
|
||||||
assertThat(newState.cameraPermissionState.permissionGranted).isFalse()
|
assertThat(newState.cameraPermissionState.permissionGranted).isFalse()
|
||||||
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto))
|
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto))
|
||||||
newState = awaitItem()
|
newState = awaitItem()
|
||||||
assertThat(newState.cameraPermissionState.showDialog).isTrue()
|
assertThat(newState.cameraPermissionState.showDialog).isTrue()
|
||||||
fakePermissionsPresenter.setPermissionGranted()
|
permissionsPresenter.setPermissionGranted()
|
||||||
newState = awaitItem()
|
newState = awaitItem()
|
||||||
assertThat(newState.cameraPermissionState.permissionGranted).isTrue()
|
assertThat(newState.cameraPermissionState.permissionGranted).isTrue()
|
||||||
newState = awaitItem()
|
newState = awaitItem()
|
||||||
|
|
@ -188,7 +175,7 @@ class ConfigureRoomPresenterTest {
|
||||||
assertThat(newState.config).isEqualTo(expectedConfig)
|
assertThat(newState.config).isEqualTo(expectedConfig)
|
||||||
// Do it again, no permission is requested
|
// Do it again, no permission is requested
|
||||||
val uriFromCamera2 = Uri.parse(AN_URI_FROM_CAMERA_2)
|
val uriFromCamera2 = Uri.parse(AN_URI_FROM_CAMERA_2)
|
||||||
fakePickerProvider.givenResult(uriFromCamera2)
|
pickerProvider.givenResult(uriFromCamera2)
|
||||||
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto))
|
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto))
|
||||||
newState = awaitItem()
|
newState = awaitItem()
|
||||||
expectedConfig = expectedConfig.copy(avatarUri = uriFromCamera2)
|
expectedConfig = expectedConfig.copy(avatarUri = uriFromCamera2)
|
||||||
|
|
@ -200,13 +187,19 @@ class ConfigureRoomPresenterTest {
|
||||||
assertThat(newState.config).isEqualTo(expectedConfig)
|
assertThat(newState.config).isEqualTo(expectedConfig)
|
||||||
|
|
||||||
// Room privacy
|
// Room privacy
|
||||||
newState.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(RoomPrivacy.Public))
|
newState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public))
|
||||||
newState = awaitItem()
|
newState = awaitItem()
|
||||||
expectedConfig = expectedConfig.copy(privacy = RoomPrivacy.Public)
|
expectedConfig = expectedConfig.copy(
|
||||||
|
roomVisibility = RoomVisibilityState.Public(
|
||||||
|
roomAddress = RoomAddress.AutoFilled(expectedConfig.roomName ?: ""),
|
||||||
|
roomAddressErrorState = RoomAddressErrorState.None,
|
||||||
|
roomAccess = RoomAccess.Anyone,
|
||||||
|
)
|
||||||
|
)
|
||||||
assertThat(newState.config).isEqualTo(expectedConfig)
|
assertThat(newState.config).isEqualTo(expectedConfig)
|
||||||
|
|
||||||
// Remove user
|
// Remove user
|
||||||
newState.eventSink(ConfigureRoomEvents.RemoveFromSelection(selectedUser1))
|
newState.eventSink(ConfigureRoomEvents.RemoveUserFromSelection(selectedUser1))
|
||||||
newState = awaitItem()
|
newState = awaitItem()
|
||||||
expectedConfig = expectedConfig.copy(invites = expectedConfig.invites.minus(selectedUser1).toImmutableList())
|
expectedConfig = expectedConfig.copy(invites = expectedConfig.invites.minus(selectedUser1).toImmutableList())
|
||||||
assertThat(newState.config).isEqualTo(expectedConfig)
|
assertThat(newState.config).isEqualTo(expectedConfig)
|
||||||
|
|
@ -215,15 +208,17 @@ class ConfigureRoomPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - trigger create room action`() = runTest {
|
fun `present - trigger create room action`() = runTest {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
val matrixClient = createMatrixClient()
|
||||||
presenter.present()
|
val presenter = createConfigureRoomPresenter(
|
||||||
}.test {
|
matrixClient = matrixClient
|
||||||
val initialState = awaitItem()
|
)
|
||||||
|
presenter.test {
|
||||||
|
val initialState = initialState()
|
||||||
val createRoomResult = Result.success(RoomId("!createRoomResult:domain"))
|
val createRoomResult = Result.success(RoomId("!createRoomResult:domain"))
|
||||||
|
|
||||||
fakeMatrixClient.givenCreateRoomResult(createRoomResult)
|
matrixClient.givenCreateRoomResult(createRoomResult)
|
||||||
|
|
||||||
initialState.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config))
|
initialState.eventSink(ConfigureRoomEvents.CreateRoom)
|
||||||
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
|
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
val stateAfterCreateRoom = awaitItem()
|
val stateAfterCreateRoom = awaitItem()
|
||||||
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Success::class.java)
|
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Success::class.java)
|
||||||
|
|
@ -233,18 +228,22 @@ class ConfigureRoomPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - record analytics when creating room`() = runTest {
|
fun `present - record analytics when creating room`() = runTest {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
val matrixClient = createMatrixClient()
|
||||||
presenter.present()
|
val analyticsService = FakeAnalyticsService()
|
||||||
}.test {
|
val presenter = createConfigureRoomPresenter(
|
||||||
val initialState = awaitItem()
|
matrixClient = matrixClient,
|
||||||
|
analyticsService = analyticsService
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
val initialState = initialState()
|
||||||
val createRoomResult = Result.success(RoomId("!createRoomResult:domain"))
|
val createRoomResult = Result.success(RoomId("!createRoomResult:domain"))
|
||||||
|
|
||||||
fakeMatrixClient.givenCreateRoomResult(createRoomResult)
|
matrixClient.givenCreateRoomResult(createRoomResult)
|
||||||
|
|
||||||
initialState.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config))
|
initialState.eventSink(ConfigureRoomEvents.CreateRoom)
|
||||||
skipItems(2)
|
skipItems(2)
|
||||||
|
|
||||||
val analyticsEvent = fakeAnalyticsService.capturedEvents.filterIsInstance<CreatedRoom>().firstOrNull()
|
val analyticsEvent = analyticsService.capturedEvents.filterIsInstance<CreatedRoom>().firstOrNull()
|
||||||
assertThat(analyticsEvent).isNotNull()
|
assertThat(analyticsEvent).isNotNull()
|
||||||
assertThat(analyticsEvent?.isDM).isFalse()
|
assertThat(analyticsEvent?.isDM).isFalse()
|
||||||
}
|
}
|
||||||
|
|
@ -252,23 +251,31 @@ class ConfigureRoomPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - trigger create room with upload error and retry`() = runTest {
|
fun `present - trigger create room with upload error and retry`() = runTest {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
val matrixClient = createMatrixClient()
|
||||||
presenter.present()
|
val analyticsService = FakeAnalyticsService()
|
||||||
}.test {
|
val mediaPreProcessor = FakeMediaPreProcessor()
|
||||||
skipItems(1)
|
val createRoomDataStore = CreateRoomDataStore(UserListDataStore())
|
||||||
|
val presenter = createConfigureRoomPresenter(
|
||||||
|
createRoomDataStore = createRoomDataStore,
|
||||||
|
mediaPreProcessor = mediaPreProcessor,
|
||||||
|
matrixClient = matrixClient,
|
||||||
|
analyticsService = analyticsService
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
val initialState = initialState()
|
||||||
createRoomDataStore.setAvatarUri(Uri.parse(AN_URI_FROM_GALLERY))
|
createRoomDataStore.setAvatarUri(Uri.parse(AN_URI_FROM_GALLERY))
|
||||||
fakeMediaPreProcessor.givenResult(Result.success(MediaUploadInfo.Image(mockk(), mockk(), mockk())))
|
skipItems(1)
|
||||||
fakeMatrixClient.givenUploadMediaResult(Result.failure(A_THROWABLE))
|
mediaPreProcessor.givenResult(Result.success(MediaUploadInfo.Image(mockk(), mockk(), mockk())))
|
||||||
|
matrixClient.givenUploadMediaResult(Result.failure(A_THROWABLE))
|
||||||
|
|
||||||
val initialState = awaitItem()
|
initialState.eventSink(ConfigureRoomEvents.CreateRoom)
|
||||||
initialState.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config))
|
|
||||||
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
|
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
val stateAfterCreateRoom = awaitItem()
|
val stateAfterCreateRoom = awaitItem()
|
||||||
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java)
|
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||||
assertThat(fakeAnalyticsService.capturedEvents.filterIsInstance<CreatedRoom>()).isEmpty()
|
assertThat(analyticsService.capturedEvents.filterIsInstance<CreatedRoom>()).isEmpty()
|
||||||
|
|
||||||
fakeMatrixClient.givenUploadMediaResult(Result.success(AN_AVATAR_URL))
|
matrixClient.givenUploadMediaResult(Result.success(AN_AVATAR_URL))
|
||||||
stateAfterCreateRoom.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config))
|
stateAfterCreateRoom.eventSink(ConfigureRoomEvents.CreateRoom)
|
||||||
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||||
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
|
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Success::class.java)
|
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Success::class.java)
|
||||||
|
|
@ -277,23 +284,25 @@ class ConfigureRoomPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - trigger retry and cancel actions`() = runTest {
|
fun `present - trigger retry and cancel actions`() = runTest {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
val fakeMatrixClient = createMatrixClient()
|
||||||
presenter.present()
|
val presenter = createConfigureRoomPresenter(
|
||||||
}.test {
|
matrixClient = fakeMatrixClient
|
||||||
val initialState = awaitItem()
|
)
|
||||||
|
presenter.test {
|
||||||
|
val initialState = initialState()
|
||||||
val createRoomResult = Result.failure<RoomId>(A_THROWABLE)
|
val createRoomResult = Result.failure<RoomId>(A_THROWABLE)
|
||||||
|
|
||||||
fakeMatrixClient.givenCreateRoomResult(createRoomResult)
|
fakeMatrixClient.givenCreateRoomResult(createRoomResult)
|
||||||
|
|
||||||
// Create
|
// Create
|
||||||
initialState.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config))
|
initialState.eventSink(ConfigureRoomEvents.CreateRoom)
|
||||||
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
|
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
val stateAfterCreateRoom = awaitItem()
|
val stateAfterCreateRoom = awaitItem()
|
||||||
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java)
|
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||||
assertThat((stateAfterCreateRoom.createRoomAction as? AsyncAction.Failure)?.error).isEqualTo(createRoomResult.exceptionOrNull())
|
assertThat((stateAfterCreateRoom.createRoomAction as? AsyncAction.Failure)?.error).isEqualTo(createRoomResult.exceptionOrNull())
|
||||||
|
|
||||||
// Retry
|
// Retry
|
||||||
stateAfterCreateRoom.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config))
|
stateAfterCreateRoom.eventSink(ConfigureRoomEvents.CreateRoom)
|
||||||
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||||
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
|
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
val stateAfterRetry = awaitItem()
|
val stateAfterRetry = awaitItem()
|
||||||
|
|
@ -305,4 +314,33 @@ class ConfigureRoomPresenterTest {
|
||||||
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun TurbineTestContext<ConfigureRoomState>.initialState(): ConfigureRoomState {
|
||||||
|
skipItems(1)
|
||||||
|
return awaitItem()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createMatrixClient() = FakeMatrixClient(
|
||||||
|
userIdServerNameLambda = { "matrix.org" },
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun createConfigureRoomPresenter(
|
||||||
|
createRoomDataStore: CreateRoomDataStore = CreateRoomDataStore(UserListDataStore()),
|
||||||
|
matrixClient: MatrixClient = createMatrixClient(),
|
||||||
|
pickerProvider: PickerProvider = FakePickerProvider(),
|
||||||
|
mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
|
||||||
|
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
||||||
|
permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(),
|
||||||
|
isKnockFeatureEnabled: Boolean = true,
|
||||||
|
) = ConfigureRoomPresenter(
|
||||||
|
dataStore = createRoomDataStore,
|
||||||
|
matrixClient = matrixClient,
|
||||||
|
mediaPickerProvider = pickerProvider,
|
||||||
|
mediaPreProcessor = mediaPreProcessor,
|
||||||
|
analyticsService = analyticsService,
|
||||||
|
permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter),
|
||||||
|
featureFlagService = FakeFeatureFlagService(
|
||||||
|
mapOf(FeatureFlags.Knock.key to isKnockFeatureEnabled)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ dependencies {
|
||||||
implementation(projects.features.call.api)
|
implementation(projects.features.call.api)
|
||||||
implementation(projects.features.location.api)
|
implementation(projects.features.location.api)
|
||||||
implementation(projects.features.poll.api)
|
implementation(projects.features.poll.api)
|
||||||
|
implementation(projects.features.roomcall.api)
|
||||||
implementation(projects.libraries.androidutils)
|
implementation(projects.libraries.androidutils)
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ import io.element.android.features.messages.impl.timeline.protection.TimelinePro
|
||||||
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState
|
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState
|
||||||
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||||
|
import io.element.android.features.roomcall.api.RoomCallState
|
||||||
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
|
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
|
@ -75,7 +76,6 @@ import io.element.android.libraries.matrix.api.room.powerlevels.canSendMessage
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
||||||
import io.element.android.libraries.matrix.ui.messages.reply.map
|
import io.element.android.libraries.matrix.ui.messages.reply.map
|
||||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
import io.element.android.libraries.matrix.ui.room.canCall
|
|
||||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
|
|
@ -98,6 +98,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
private val reactionSummaryPresenter: Presenter<ReactionSummaryState>,
|
private val reactionSummaryPresenter: Presenter<ReactionSummaryState>,
|
||||||
private val readReceiptBottomSheetPresenter: Presenter<ReadReceiptBottomSheetState>,
|
private val readReceiptBottomSheetPresenter: Presenter<ReadReceiptBottomSheetState>,
|
||||||
private val pinnedMessagesBannerPresenter: Presenter<PinnedMessagesBannerState>,
|
private val pinnedMessagesBannerPresenter: Presenter<PinnedMessagesBannerState>,
|
||||||
|
private val roomCallStatePresenter: Presenter<RoomCallState>,
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
private val snackbarDispatcher: SnackbarDispatcher,
|
private val snackbarDispatcher: SnackbarDispatcher,
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
|
|
@ -133,6 +134,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
val reactionSummaryState = reactionSummaryPresenter.present()
|
val reactionSummaryState = reactionSummaryPresenter.present()
|
||||||
val readReceiptBottomSheetState = readReceiptBottomSheetPresenter.present()
|
val readReceiptBottomSheetState = readReceiptBottomSheetPresenter.present()
|
||||||
val pinnedMessagesBannerState = pinnedMessagesBannerPresenter.present()
|
val pinnedMessagesBannerState = pinnedMessagesBannerPresenter.present()
|
||||||
|
val roomCallState = roomCallStatePresenter.present()
|
||||||
|
|
||||||
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
||||||
|
|
||||||
|
|
@ -152,8 +154,6 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
val canJoinCall by room.canCall(updateKey = syncUpdateFlow.value)
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
// Remove the unread flag on entering but don't send read receipts
|
// Remove the unread flag on entering but don't send read receipts
|
||||||
// as those will be handled by the timeline.
|
// as those will be handled by the timeline.
|
||||||
|
|
@ -204,12 +204,6 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val callState = when {
|
|
||||||
!canJoinCall -> RoomCallState.DISABLED
|
|
||||||
roomInfo?.hasRoomCall == true -> RoomCallState.ONGOING
|
|
||||||
else -> RoomCallState.ENABLED
|
|
||||||
}
|
|
||||||
|
|
||||||
return MessagesState(
|
return MessagesState(
|
||||||
roomId = room.roomId,
|
roomId = room.roomId,
|
||||||
roomName = roomName,
|
roomName = roomName,
|
||||||
|
|
@ -232,7 +226,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
enableTextFormatting = MessageComposerConfig.ENABLE_RICH_TEXT_EDITING,
|
enableTextFormatting = MessageComposerConfig.ENABLE_RICH_TEXT_EDITING,
|
||||||
enableVoiceMessages = enableVoiceMessages,
|
enableVoiceMessages = enableVoiceMessages,
|
||||||
appName = buildMeta.applicationName,
|
appName = buildMeta.applicationName,
|
||||||
callState = callState,
|
roomCallState = roomCallState,
|
||||||
pinnedMessagesBannerState = pinnedMessagesBannerState,
|
pinnedMessagesBannerState = pinnedMessagesBannerState,
|
||||||
eventSink = { handleEvents(it) }
|
eventSink = { handleEvents(it) }
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import io.element.android.features.messages.impl.timeline.components.reactionsum
|
||||||
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState
|
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState
|
||||||
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
|
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
|
||||||
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState
|
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState
|
||||||
|
import io.element.android.features.roomcall.api.RoomCallState
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||||
|
|
@ -46,14 +47,8 @@ data class MessagesState(
|
||||||
val showReinvitePrompt: Boolean,
|
val showReinvitePrompt: Boolean,
|
||||||
val enableTextFormatting: Boolean,
|
val enableTextFormatting: Boolean,
|
||||||
val enableVoiceMessages: Boolean,
|
val enableVoiceMessages: Boolean,
|
||||||
val callState: RoomCallState,
|
val roomCallState: RoomCallState,
|
||||||
val appName: String,
|
val appName: String,
|
||||||
val pinnedMessagesBannerState: PinnedMessagesBannerState,
|
val pinnedMessagesBannerState: PinnedMessagesBannerState,
|
||||||
val eventSink: (MessagesEvents) -> Unit
|
val eventSink: (MessagesEvents) -> Unit
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class RoomCallState {
|
|
||||||
ENABLED,
|
|
||||||
ONGOING,
|
|
||||||
DISABLED
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,9 @@ import io.element.android.features.messages.impl.timeline.protection.aTimelinePr
|
||||||
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState
|
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState
|
||||||
import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessageComposerState
|
import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessageComposerState
|
||||||
import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessagePreviewState
|
import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessagePreviewState
|
||||||
|
import io.element.android.features.roomcall.api.RoomCallState
|
||||||
|
import io.element.android.features.roomcall.api.aStandByCallState
|
||||||
|
import io.element.android.features.roomcall.api.anOngoingCallState
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
|
@ -70,7 +73,7 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
aMessagesState(
|
aMessagesState(
|
||||||
callState = RoomCallState.ONGOING,
|
roomCallState = anOngoingCallState(),
|
||||||
),
|
),
|
||||||
aMessagesState(
|
aMessagesState(
|
||||||
enableVoiceMessages = true,
|
enableVoiceMessages = true,
|
||||||
|
|
@ -80,7 +83,7 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
aMessagesState(
|
aMessagesState(
|
||||||
callState = RoomCallState.DISABLED,
|
roomCallState = aStandByCallState(canStartCall = false),
|
||||||
),
|
),
|
||||||
aMessagesState(
|
aMessagesState(
|
||||||
pinnedMessagesBannerState = aLoadedPinnedMessagesBannerState(
|
pinnedMessagesBannerState = aLoadedPinnedMessagesBannerState(
|
||||||
|
|
@ -115,7 +118,7 @@ fun aMessagesState(
|
||||||
hasNetworkConnection: Boolean = true,
|
hasNetworkConnection: Boolean = true,
|
||||||
showReinvitePrompt: Boolean = false,
|
showReinvitePrompt: Boolean = false,
|
||||||
enableVoiceMessages: Boolean = true,
|
enableVoiceMessages: Boolean = true,
|
||||||
callState: RoomCallState = RoomCallState.ENABLED,
|
roomCallState: RoomCallState = aStandByCallState(),
|
||||||
pinnedMessagesBannerState: PinnedMessagesBannerState = aLoadedPinnedMessagesBannerState(),
|
pinnedMessagesBannerState: PinnedMessagesBannerState = aLoadedPinnedMessagesBannerState(),
|
||||||
eventSink: (MessagesEvents) -> Unit = {},
|
eventSink: (MessagesEvents) -> Unit = {},
|
||||||
) = MessagesState(
|
) = MessagesState(
|
||||||
|
|
@ -139,7 +142,7 @@ fun aMessagesState(
|
||||||
showReinvitePrompt = showReinvitePrompt,
|
showReinvitePrompt = showReinvitePrompt,
|
||||||
enableTextFormatting = true,
|
enableTextFormatting = true,
|
||||||
enableVoiceMessages = enableVoiceMessages,
|
enableVoiceMessages = enableVoiceMessages,
|
||||||
callState = callState,
|
roomCallState = roomCallState,
|
||||||
appName = "Element",
|
appName = "Element",
|
||||||
pinnedMessagesBannerState = pinnedMessagesBannerState,
|
pinnedMessagesBannerState = pinnedMessagesBannerState,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,6 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
|
||||||
import io.element.android.features.messages.impl.actionlist.ActionListEvents
|
import io.element.android.features.messages.impl.actionlist.ActionListEvents
|
||||||
import io.element.android.features.messages.impl.actionlist.ActionListView
|
import io.element.android.features.messages.impl.actionlist.ActionListView
|
||||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||||
|
|
@ -69,7 +68,7 @@ import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBan
|
||||||
import io.element.android.features.messages.impl.timeline.FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS
|
import io.element.android.features.messages.impl.timeline.FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineView
|
import io.element.android.features.messages.impl.timeline.TimelineView
|
||||||
import io.element.android.features.messages.impl.timeline.components.JoinCallMenuItem
|
import io.element.android.features.messages.impl.timeline.components.CallMenuItem
|
||||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionBottomSheet
|
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionBottomSheet
|
||||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvents
|
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvents
|
||||||
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryEvents
|
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryEvents
|
||||||
|
|
@ -81,6 +80,7 @@ import io.element.android.features.messages.impl.voicemessages.composer.VoiceMes
|
||||||
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessagePermissionRationaleDialog
|
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessagePermissionRationaleDialog
|
||||||
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageSendingFailedDialog
|
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageSendingFailedDialog
|
||||||
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
|
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
|
||||||
|
import io.element.android.features.roomcall.api.RoomCallState
|
||||||
import io.element.android.libraries.androidutils.ui.hideKeyboard
|
import io.element.android.libraries.androidutils.ui.hideKeyboard
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule
|
||||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||||
|
|
@ -93,8 +93,6 @@ import io.element.android.libraries.designsystem.components.dialogs.Confirmation
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.BottomSheetDragHandle
|
import io.element.android.libraries.designsystem.theme.components.BottomSheetDragHandle
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
|
@ -190,7 +188,7 @@ fun MessagesView(
|
||||||
roomName = state.roomName.dataOrNull(),
|
roomName = state.roomName.dataOrNull(),
|
||||||
roomAvatar = state.roomAvatar.dataOrNull(),
|
roomAvatar = state.roomAvatar.dataOrNull(),
|
||||||
heroes = state.heroes,
|
heroes = state.heroes,
|
||||||
callState = state.callState,
|
roomCallState = state.roomCallState,
|
||||||
onBackClick = {
|
onBackClick = {
|
||||||
// Since the textfield is now based on an Android view, this is no longer done automatically.
|
// Since the textfield is now based on an Android view, this is no longer done automatically.
|
||||||
// We need to hide the keyboard when navigating out of this screen.
|
// We need to hide the keyboard when navigating out of this screen.
|
||||||
|
|
@ -479,7 +477,7 @@ private fun MessagesViewTopBar(
|
||||||
roomName: String?,
|
roomName: String?,
|
||||||
roomAvatar: AvatarData?,
|
roomAvatar: AvatarData?,
|
||||||
heroes: ImmutableList<AvatarData>,
|
heroes: ImmutableList<AvatarData>,
|
||||||
callState: RoomCallState,
|
roomCallState: RoomCallState,
|
||||||
onRoomDetailsClick: () -> Unit,
|
onRoomDetailsClick: () -> Unit,
|
||||||
onJoinCallClick: () -> Unit,
|
onJoinCallClick: () -> Unit,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
|
|
@ -509,9 +507,8 @@ private fun MessagesViewTopBar(
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
CallMenuItem(
|
CallMenuItem(
|
||||||
isCallOngoing = callState == RoomCallState.ONGOING,
|
roomCallState = roomCallState,
|
||||||
onClick = onJoinCallClick,
|
onJoinCallClick = onJoinCallClick,
|
||||||
enabled = callState != RoomCallState.DISABLED
|
|
||||||
)
|
)
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
},
|
},
|
||||||
|
|
@ -519,24 +516,6 @@ private fun MessagesViewTopBar(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun CallMenuItem(
|
|
||||||
isCallOngoing: Boolean,
|
|
||||||
enabled: Boolean = true,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
) {
|
|
||||||
if (isCallOngoing) {
|
|
||||||
JoinCallMenuItem(onJoinCallClick = onClick)
|
|
||||||
} else {
|
|
||||||
IconButton(onClick = onClick, enabled = enabled) {
|
|
||||||
Icon(
|
|
||||||
imageVector = CompoundIcons.VideoCallSolid(),
|
|
||||||
contentDescription = stringResource(CommonStrings.a11y_start_call),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RoomAvatarAndNameRow(
|
private fun RoomAvatarAndNameRow(
|
||||||
roomName: String,
|
roomName: String,
|
||||||
|
|
|
||||||
|
|
@ -9,16 +9,21 @@ package io.element.android.features.messages.impl.attachments.preview
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.features.messages.impl.attachments.Attachment
|
import io.element.android.features.messages.impl.attachments.Attachment
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
||||||
|
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
||||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||||
|
import io.element.android.libraries.textcomposer.model.TextEditorState
|
||||||
|
import io.element.android.libraries.textcomposer.model.rememberMarkdownTextEditorState
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
|
@ -30,6 +35,7 @@ import kotlin.coroutines.coroutineContext
|
||||||
class AttachmentsPreviewPresenter @AssistedInject constructor(
|
class AttachmentsPreviewPresenter @AssistedInject constructor(
|
||||||
@Assisted private val attachment: Attachment,
|
@Assisted private val attachment: Attachment,
|
||||||
private val mediaSender: MediaSender,
|
private val mediaSender: MediaSender,
|
||||||
|
private val permalinkBuilder: PermalinkBuilder,
|
||||||
) : Presenter<AttachmentsPreviewState> {
|
) : Presenter<AttachmentsPreviewState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
|
@ -44,11 +50,24 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
||||||
mutableStateOf<SendActionState>(SendActionState.Idle)
|
mutableStateOf<SendActionState>(SendActionState.Idle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val markdownTextEditorState = rememberMarkdownTextEditorState(initialText = null, initialFocus = false)
|
||||||
|
val textEditorState by rememberUpdatedState(
|
||||||
|
TextEditorState.Markdown(markdownTextEditorState)
|
||||||
|
)
|
||||||
|
|
||||||
val ongoingSendAttachmentJob = remember { mutableStateOf<Job?>(null) }
|
val ongoingSendAttachmentJob = remember { mutableStateOf<Job?>(null) }
|
||||||
|
|
||||||
fun handleEvents(attachmentsPreviewEvents: AttachmentsPreviewEvents) {
|
fun handleEvents(attachmentsPreviewEvents: AttachmentsPreviewEvents) {
|
||||||
when (attachmentsPreviewEvents) {
|
when (attachmentsPreviewEvents) {
|
||||||
AttachmentsPreviewEvents.SendAttachment -> ongoingSendAttachmentJob.value = coroutineScope.sendAttachment(attachment, sendActionState)
|
is AttachmentsPreviewEvents.SendAttachment -> {
|
||||||
|
val caption = markdownTextEditorState.getMessageMarkdown(permalinkBuilder)
|
||||||
|
.takeIf { it.isNotEmpty() }
|
||||||
|
ongoingSendAttachmentJob.value = coroutineScope.sendAttachment(
|
||||||
|
attachment = attachment,
|
||||||
|
caption = caption,
|
||||||
|
sendActionState = sendActionState,
|
||||||
|
)
|
||||||
|
}
|
||||||
AttachmentsPreviewEvents.ClearSendState -> {
|
AttachmentsPreviewEvents.ClearSendState -> {
|
||||||
ongoingSendAttachmentJob.value?.let {
|
ongoingSendAttachmentJob.value?.let {
|
||||||
it.cancel()
|
it.cancel()
|
||||||
|
|
@ -62,18 +81,21 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
||||||
return AttachmentsPreviewState(
|
return AttachmentsPreviewState(
|
||||||
attachment = attachment,
|
attachment = attachment,
|
||||||
sendActionState = sendActionState.value,
|
sendActionState = sendActionState.value,
|
||||||
|
textEditorState = textEditorState,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.sendAttachment(
|
private fun CoroutineScope.sendAttachment(
|
||||||
attachment: Attachment,
|
attachment: Attachment,
|
||||||
|
caption: String?,
|
||||||
sendActionState: MutableState<SendActionState>,
|
sendActionState: MutableState<SendActionState>,
|
||||||
) = launch {
|
) = launch {
|
||||||
when (attachment) {
|
when (attachment) {
|
||||||
is Attachment.Media -> {
|
is Attachment.Media -> {
|
||||||
sendMedia(
|
sendMedia(
|
||||||
mediaAttachment = attachment,
|
mediaAttachment = attachment,
|
||||||
|
caption = caption,
|
||||||
sendActionState = sendActionState,
|
sendActionState = sendActionState,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -82,6 +104,7 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
private suspend fun sendMedia(
|
private suspend fun sendMedia(
|
||||||
mediaAttachment: Attachment.Media,
|
mediaAttachment: Attachment.Media,
|
||||||
|
caption: String?,
|
||||||
sendActionState: MutableState<SendActionState>,
|
sendActionState: MutableState<SendActionState>,
|
||||||
) = runCatching {
|
) = runCatching {
|
||||||
val context = coroutineContext
|
val context = coroutineContext
|
||||||
|
|
@ -96,6 +119,7 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
||||||
mediaSender.sendMedia(
|
mediaSender.sendMedia(
|
||||||
uri = mediaAttachment.localMedia.uri,
|
uri = mediaAttachment.localMedia.uri,
|
||||||
mimeType = mediaAttachment.localMedia.info.mimeType,
|
mimeType = mediaAttachment.localMedia.info.mimeType,
|
||||||
|
caption = caption,
|
||||||
progressCallback = progressCallback
|
progressCallback = progressCallback
|
||||||
).getOrThrow()
|
).getOrThrow()
|
||||||
}.fold(
|
}.fold(
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,21 @@ package io.element.android.features.messages.impl.attachments.preview
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import io.element.android.features.messages.impl.attachments.Attachment
|
import io.element.android.features.messages.impl.attachments.Attachment
|
||||||
|
import io.element.android.libraries.core.bool.orFalse
|
||||||
|
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeImage
|
||||||
|
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo
|
||||||
|
import io.element.android.libraries.textcomposer.model.TextEditorState
|
||||||
|
|
||||||
data class AttachmentsPreviewState(
|
data class AttachmentsPreviewState(
|
||||||
val attachment: Attachment,
|
val attachment: Attachment,
|
||||||
val sendActionState: SendActionState,
|
val sendActionState: SendActionState,
|
||||||
|
val textEditorState: TextEditorState,
|
||||||
val eventSink: (AttachmentsPreviewEvents) -> Unit
|
val eventSink: (AttachmentsPreviewEvents) -> Unit
|
||||||
)
|
) {
|
||||||
|
val allowCaption: Boolean = (attachment as? Attachment.Media)?.localMedia?.info?.mimeType?.let {
|
||||||
|
it.isMimeTypeImage() || it.isMimeTypeVideo()
|
||||||
|
}.orFalse()
|
||||||
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
sealed interface SendActionState {
|
sealed interface SendActionState {
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,19 @@ import androidx.core.net.toUri
|
||||||
import io.element.android.features.messages.impl.attachments.Attachment
|
import io.element.android.features.messages.impl.attachments.Attachment
|
||||||
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
||||||
import io.element.android.libraries.mediaviewer.api.local.MediaInfo
|
import io.element.android.libraries.mediaviewer.api.local.MediaInfo
|
||||||
|
import io.element.android.libraries.mediaviewer.api.local.aVideoMediaInfo
|
||||||
import io.element.android.libraries.mediaviewer.api.local.anApkMediaInfo
|
import io.element.android.libraries.mediaviewer.api.local.anApkMediaInfo
|
||||||
|
import io.element.android.libraries.mediaviewer.api.local.anAudioMediaInfo
|
||||||
import io.element.android.libraries.mediaviewer.api.local.anImageMediaInfo
|
import io.element.android.libraries.mediaviewer.api.local.anImageMediaInfo
|
||||||
|
import io.element.android.libraries.textcomposer.model.TextEditorState
|
||||||
|
import io.element.android.libraries.textcomposer.model.aTextEditorStateMarkdown
|
||||||
|
|
||||||
open class AttachmentsPreviewStateProvider : PreviewParameterProvider<AttachmentsPreviewState> {
|
open class AttachmentsPreviewStateProvider : PreviewParameterProvider<AttachmentsPreviewState> {
|
||||||
override val values: Sequence<AttachmentsPreviewState>
|
override val values: Sequence<AttachmentsPreviewState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
anAttachmentsPreviewState(),
|
anAttachmentsPreviewState(),
|
||||||
|
anAttachmentsPreviewState(mediaInfo = aVideoMediaInfo()),
|
||||||
|
anAttachmentsPreviewState(mediaInfo = anAudioMediaInfo()),
|
||||||
anAttachmentsPreviewState(mediaInfo = anApkMediaInfo()),
|
anAttachmentsPreviewState(mediaInfo = anApkMediaInfo()),
|
||||||
anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Uploading(0.5f)),
|
anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Uploading(0.5f)),
|
||||||
anAttachmentsPreviewState(sendActionState = SendActionState.Failure(RuntimeException("error"))),
|
anAttachmentsPreviewState(sendActionState = SendActionState.Failure(RuntimeException("error"))),
|
||||||
|
|
@ -27,11 +33,13 @@ open class AttachmentsPreviewStateProvider : PreviewParameterProvider<Attachment
|
||||||
|
|
||||||
fun anAttachmentsPreviewState(
|
fun anAttachmentsPreviewState(
|
||||||
mediaInfo: MediaInfo = anImageMediaInfo(),
|
mediaInfo: MediaInfo = anImageMediaInfo(),
|
||||||
sendActionState: SendActionState = SendActionState.Idle
|
textEditorState: TextEditorState = aTextEditorStateMarkdown(),
|
||||||
|
sendActionState: SendActionState = SendActionState.Idle,
|
||||||
) = AttachmentsPreviewState(
|
) = AttachmentsPreviewState(
|
||||||
attachment = Attachment.Media(
|
attachment = Attachment.Media(
|
||||||
localMedia = LocalMedia("file://path".toUri(), mediaInfo),
|
localMedia = LocalMedia("file://path".toUri(), mediaInfo),
|
||||||
),
|
),
|
||||||
sendActionState = sendActionState,
|
sendActionState = sendActionState,
|
||||||
|
textEditorState = textEditorState,
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -9,37 +9,44 @@ package io.element.android.features.messages.impl.attachments.preview
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.defaultMinSize
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
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.features.messages.impl.attachments.Attachment
|
import io.element.android.features.messages.impl.attachments.Attachment
|
||||||
import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError
|
import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule
|
|
||||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||||
import io.element.android.libraries.designsystem.components.ProgressDialogType
|
import io.element.android.libraries.designsystem.components.ProgressDialogType
|
||||||
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
|
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
import io.element.android.libraries.mediaviewer.api.local.LocalMediaView
|
import io.element.android.libraries.mediaviewer.api.local.LocalMediaView
|
||||||
import io.element.android.libraries.mediaviewer.api.local.rememberLocalMediaViewState
|
import io.element.android.libraries.mediaviewer.api.local.rememberLocalMediaViewState
|
||||||
|
import io.element.android.libraries.textcomposer.TextComposer
|
||||||
|
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||||
|
import io.element.android.libraries.textcomposer.model.VoiceMessageState
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import io.element.android.wysiwyg.display.TextDisplay
|
||||||
import me.saket.telephoto.zoomable.ZoomSpec
|
import me.saket.telephoto.zoomable.ZoomSpec
|
||||||
import me.saket.telephoto.zoomable.rememberZoomableState
|
import me.saket.telephoto.zoomable.rememberZoomableState
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AttachmentsPreviewView(
|
fun AttachmentsPreviewView(
|
||||||
state: AttachmentsPreviewState,
|
state: AttachmentsPreviewState,
|
||||||
|
|
@ -61,11 +68,23 @@ fun AttachmentsPreviewView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(modifier) {
|
Scaffold(
|
||||||
|
modifier = modifier,
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
navigationIcon = {
|
||||||
|
BackButton(
|
||||||
|
imageVector = CompoundIcons.Close(),
|
||||||
|
onClick = onDismiss,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
title = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
AttachmentPreviewContent(
|
AttachmentPreviewContent(
|
||||||
attachment = state.attachment,
|
state = state,
|
||||||
onSendClick = ::postSendAttachment,
|
onSendClick = ::postSendAttachment,
|
||||||
onDismiss = onDismiss
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
AttachmentSendStateView(
|
AttachmentSendStateView(
|
||||||
|
|
@ -106,21 +125,19 @@ private fun AttachmentSendStateView(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun AttachmentPreviewContent(
|
private fun AttachmentPreviewContent(
|
||||||
attachment: Attachment,
|
state: AttachmentsPreviewState,
|
||||||
onSendClick: () -> Unit,
|
onSendClick: () -> Unit,
|
||||||
onDismiss: () -> Unit,
|
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.navigationBarsPadding(),
|
.navigationBarsPadding(),
|
||||||
contentAlignment = Alignment.BottomCenter
|
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
when (attachment) {
|
when (val attachment = state.attachment) {
|
||||||
is Attachment.Media -> {
|
is Attachment.Media -> {
|
||||||
val localMediaViewState = rememberLocalMediaViewState(
|
val localMediaViewState = rememberLocalMediaViewState(
|
||||||
zoomableState = rememberZoomableState(
|
zoomableState = rememberZoomableState(
|
||||||
|
|
@ -137,27 +154,46 @@ private fun AttachmentPreviewContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AttachmentsPreviewBottomActions(
|
AttachmentsPreviewBottomActions(
|
||||||
onCancelClick = onDismiss,
|
state = state,
|
||||||
onSendClick = onSendClick,
|
onSendClick = onSendClick,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(Color.Black.copy(alpha = 0.7f))
|
.background(ElementTheme.colors.bgCanvasDefault)
|
||||||
.padding(horizontal = 24.dp)
|
.height(IntrinsicSize.Min)
|
||||||
.defaultMinSize(minHeight = 80.dp)
|
.align(Alignment.BottomCenter)
|
||||||
|
.imePadding(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun AttachmentsPreviewBottomActions(
|
private fun AttachmentsPreviewBottomActions(
|
||||||
onCancelClick: () -> Unit,
|
state: AttachmentsPreviewState,
|
||||||
onSendClick: () -> Unit,
|
onSendClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
ButtonRowMolecule(modifier = modifier) {
|
TextComposer(
|
||||||
TextButton(stringResource(id = CommonStrings.action_cancel), onClick = onCancelClick)
|
modifier = modifier,
|
||||||
TextButton(stringResource(id = CommonStrings.action_send), onClick = onSendClick)
|
state = state.textEditorState,
|
||||||
}
|
voiceMessageState = VoiceMessageState.Idle,
|
||||||
|
composerMode = MessageComposerMode.Attachment(state.allowCaption),
|
||||||
|
onRequestFocus = {},
|
||||||
|
onSendMessage = onSendClick,
|
||||||
|
showTextFormatting = false,
|
||||||
|
onResetComposerMode = {},
|
||||||
|
onAddAttachment = {},
|
||||||
|
onDismissTextFormatting = {},
|
||||||
|
enableVoiceMessages = false,
|
||||||
|
onVoiceRecorderEvent = {},
|
||||||
|
onVoicePlayerEvent = {},
|
||||||
|
onSendVoiceMessage = {},
|
||||||
|
onDeleteVoiceMessage = {},
|
||||||
|
onReceiveSuggestion = {},
|
||||||
|
resolveMentionDisplay = { _, _ -> TextDisplay.Plain },
|
||||||
|
onError = {},
|
||||||
|
onTyping = {},
|
||||||
|
onSelectRichContent = {},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only preview in dark, dark theme is forced on the Node.
|
// Only preview in dark, dark theme is forced on the Node.
|
||||||
|
|
|
||||||
|
|
@ -436,6 +436,7 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
// Reset composer right away
|
// Reset composer right away
|
||||||
resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = capturedMode is MessageComposerMode.Edit)
|
resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = capturedMode is MessageComposerMode.Edit)
|
||||||
when (capturedMode) {
|
when (capturedMode) {
|
||||||
|
is MessageComposerMode.Attachment,
|
||||||
is MessageComposerMode.Normal -> room.sendMessage(
|
is MessageComposerMode.Normal -> room.sendMessage(
|
||||||
body = message.markdown,
|
body = message.markdown,
|
||||||
htmlBody = message.html,
|
htmlBody = message.html,
|
||||||
|
|
@ -605,6 +606,7 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
): ComposerDraft? {
|
): ComposerDraft? {
|
||||||
val message = currentComposerMessage(markdownTextEditorState, richTextEditorState, withMentions = false)
|
val message = currentComposerMessage(markdownTextEditorState, richTextEditorState, withMentions = false)
|
||||||
val draftType = when (val mode = messageComposerContext.composerMode) {
|
val draftType = when (val mode = messageComposerContext.composerMode) {
|
||||||
|
is MessageComposerMode.Attachment,
|
||||||
is MessageComposerMode.Normal -> ComposerDraftType.NewMessage
|
is MessageComposerMode.Normal -> ComposerDraftType.NewMessage
|
||||||
is MessageComposerMode.Edit -> {
|
is MessageComposerMode.Edit -> {
|
||||||
mode.eventOrTransactionId.eventId?.let { eventId -> ComposerDraftType.Edit(eventId) }
|
mode.eventOrTransactionId.eventId?.let { eventId -> ComposerDraftType.Edit(eventId) }
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import io.element.android.features.messages.impl.timeline.factories.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
|
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
|
||||||
import io.element.android.features.messages.impl.typing.TypingNotificationState
|
import io.element.android.features.messages.impl.typing.TypingNotificationState
|
||||||
|
import io.element.android.features.roomcall.api.aStandByCallState
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
|
|
@ -89,7 +90,8 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
|
||||||
// We don't need to compute those values
|
// We don't need to compute those values
|
||||||
userHasPermissionToSendMessage = false,
|
userHasPermissionToSendMessage = false,
|
||||||
userHasPermissionToSendReaction = false,
|
userHasPermissionToSendReaction = false,
|
||||||
isCallOngoing = false,
|
// We do not care about the call state here.
|
||||||
|
roomCallState = aStandByCallState(),
|
||||||
// don't compute this value or the pin icon will be shown
|
// don't compute this value or the pin icon will be shown
|
||||||
pinnedEventIds = emptyList(),
|
pinnedEventIds = emptyList(),
|
||||||
typingNotificationState = TypingNotificationState(
|
typingNotificationState = TypingNotificationState(
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,8 @@ import io.element.android.features.messages.impl.typing.TypingNotificationState
|
||||||
import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager
|
import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager
|
||||||
import io.element.android.features.poll.api.actions.EndPollAction
|
import io.element.android.features.poll.api.actions.EndPollAction
|
||||||
import io.element.android.features.poll.api.actions.SendPollResponseAction
|
import io.element.android.features.poll.api.actions.SendPollResponseAction
|
||||||
|
import io.element.android.features.roomcall.api.RoomCallState
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.bool.orFalse
|
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||||
|
|
@ -73,6 +73,7 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
private val timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer(),
|
private val timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer(),
|
||||||
private val resolveVerifiedUserSendFailurePresenter: Presenter<ResolveVerifiedUserSendFailureState>,
|
private val resolveVerifiedUserSendFailurePresenter: Presenter<ResolveVerifiedUserSendFailureState>,
|
||||||
private val typingNotificationPresenter: Presenter<TypingNotificationState>,
|
private val typingNotificationPresenter: Presenter<TypingNotificationState>,
|
||||||
|
private val roomCallStatePresenter: Presenter<RoomCallState>,
|
||||||
) : Presenter<TimelineState> {
|
) : Presenter<TimelineState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
|
@ -229,14 +230,15 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
val typingNotificationState = typingNotificationPresenter.present()
|
val typingNotificationState = typingNotificationPresenter.present()
|
||||||
val timelineRoomInfo by remember(typingNotificationState) {
|
val roomCallState = roomCallStatePresenter.present()
|
||||||
|
val timelineRoomInfo by remember(typingNotificationState, roomCallState) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
TimelineRoomInfo(
|
TimelineRoomInfo(
|
||||||
name = room.displayName,
|
name = room.displayName,
|
||||||
isDm = room.isDm,
|
isDm = room.isDm,
|
||||||
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
|
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
|
||||||
userHasPermissionToSendReaction = userHasPermissionToSendReaction,
|
userHasPermissionToSendReaction = userHasPermissionToSendReaction,
|
||||||
isCallOngoing = roomInfo?.hasRoomCall.orFalse(),
|
roomCallState = roomCallState,
|
||||||
pinnedEventIds = roomInfo?.pinnedEventIds.orEmpty(),
|
pinnedEventIds = roomInfo?.pinnedEventIds.orEmpty(),
|
||||||
typingNotificationState = typingNotificationState,
|
typingNotificationState = typingNotificationState,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import io.element.android.features.messages.impl.crypto.sendfailure.resolve.Reso
|
||||||
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.typing.TypingNotificationState
|
import io.element.android.features.messages.impl.typing.TypingNotificationState
|
||||||
|
import io.element.android.features.roomcall.api.RoomCallState
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
||||||
|
|
@ -73,7 +74,7 @@ data class TimelineRoomInfo(
|
||||||
val name: String?,
|
val name: String?,
|
||||||
val userHasPermissionToSendMessage: Boolean,
|
val userHasPermissionToSendMessage: Boolean,
|
||||||
val userHasPermissionToSendReaction: Boolean,
|
val userHasPermissionToSendReaction: Boolean,
|
||||||
val isCallOngoing: Boolean,
|
val roomCallState: RoomCallState,
|
||||||
val pinnedEventIds: List<EventId>,
|
val pinnedEventIds: List<EventId>,
|
||||||
val typingNotificationState: TypingNotificationState,
|
val typingNotificationState: TypingNotificationState,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI
|
||||||
import io.element.android.features.messages.impl.timeline.model.virtual.aTimelineItemDaySeparatorModel
|
import io.element.android.features.messages.impl.timeline.model.virtual.aTimelineItemDaySeparatorModel
|
||||||
import io.element.android.features.messages.impl.typing.TypingNotificationState
|
import io.element.android.features.messages.impl.typing.TypingNotificationState
|
||||||
import io.element.android.features.messages.impl.typing.aTypingNotificationState
|
import io.element.android.features.messages.impl.typing.aTypingNotificationState
|
||||||
|
import io.element.android.features.roomcall.api.aStandByCallState
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
|
@ -249,7 +250,7 @@ internal fun aTimelineRoomInfo(
|
||||||
name = name,
|
name = name,
|
||||||
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
|
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
|
||||||
userHasPermissionToSendReaction = true,
|
userHasPermissionToSendReaction = true,
|
||||||
isCallOngoing = false,
|
roomCallState = aStandByCallState(),
|
||||||
pinnedEventIds = pinnedEventIds,
|
pinnedEventIds = pinnedEventIds,
|
||||||
typingNotificationState = typingNotificationState,
|
typingNotificationState = typingNotificationState,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.timeline.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
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.features.roomcall.api.RoomCallState
|
||||||
|
import io.element.android.features.roomcall.api.RoomCallStateProvider
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun CallMenuItem(
|
||||||
|
roomCallState: RoomCallState,
|
||||||
|
onJoinCallClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
when (roomCallState) {
|
||||||
|
is RoomCallState.StandBy -> {
|
||||||
|
StandByCallMenuItem(
|
||||||
|
roomCallState = roomCallState,
|
||||||
|
onJoinCallClick = onJoinCallClick,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is RoomCallState.OnGoing -> {
|
||||||
|
OnGoingCallMenuItem(
|
||||||
|
roomCallState = roomCallState,
|
||||||
|
onJoinCallClick = onJoinCallClick,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun StandByCallMenuItem(
|
||||||
|
roomCallState: RoomCallState.StandBy,
|
||||||
|
onJoinCallClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
modifier = modifier,
|
||||||
|
onClick = onJoinCallClick,
|
||||||
|
enabled = roomCallState.canStartCall,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = CompoundIcons.VideoCallSolid(),
|
||||||
|
contentDescription = stringResource(CommonStrings.a11y_start_call),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun OnGoingCallMenuItem(
|
||||||
|
roomCallState: RoomCallState.OnGoing,
|
||||||
|
onJoinCallClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
if (!roomCallState.isUserLocallyInTheCall) {
|
||||||
|
Button(
|
||||||
|
onClick = onJoinCallClick,
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
contentColor = ElementTheme.colors.bgCanvasDefault,
|
||||||
|
containerColor = ElementTheme.colors.iconAccentTertiary
|
||||||
|
),
|
||||||
|
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp),
|
||||||
|
modifier = modifier.heightIn(min = 36.dp),
|
||||||
|
enabled = roomCallState.canJoinCall,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
imageVector = CompoundIcons.VideoCallSolid(),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(CommonStrings.action_join),
|
||||||
|
style = ElementTheme.typography.fontBodyMdMedium
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Else user is already in the call, hide the button.
|
||||||
|
Box(modifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun CallMenuItemPreview(
|
||||||
|
@PreviewParameter(RoomCallStateProvider::class) roomCallState: RoomCallState
|
||||||
|
) = ElementPreview {
|
||||||
|
CallMenuItem(
|
||||||
|
roomCallState = roomCallState,
|
||||||
|
onJoinCallClick = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* Please see LICENSE in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.timeline.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.heightIn
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.ButtonDefaults
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
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.theme.components.Icon
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
internal fun JoinCallMenuItem(
|
|
||||||
onJoinCallClick: () -> Unit,
|
|
||||||
) {
|
|
||||||
Button(
|
|
||||||
onClick = onJoinCallClick,
|
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
contentColor = ElementTheme.colors.bgCanvasDefault,
|
|
||||||
containerColor = ElementTheme.colors.iconAccentTertiary
|
|
||||||
),
|
|
||||||
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp),
|
|
||||||
modifier = Modifier.heightIn(min = 36.dp),
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
modifier = Modifier.size(20.dp),
|
|
||||||
imageVector = CompoundIcons.VideoCallSolid(),
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
Spacer(Modifier.width(8.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(CommonStrings.action_join),
|
|
||||||
style = ElementTheme.typography.fontBodyMdMedium
|
|
||||||
)
|
|
||||||
Spacer(Modifier.width(8.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -31,6 +31,8 @@ import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent
|
||||||
|
import io.element.android.features.roomcall.api.RoomCallState
|
||||||
|
import io.element.android.features.roomcall.api.RoomCallStateProvider
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
|
@ -41,7 +43,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TimelineItemCallNotifyView(
|
internal fun TimelineItemCallNotifyView(
|
||||||
event: TimelineItem.Event,
|
event: TimelineItem.Event,
|
||||||
isCallOngoing: Boolean,
|
roomCallState: RoomCallState,
|
||||||
onLongClick: (TimelineItem.Event) -> Unit,
|
onLongClick: (TimelineItem.Event) -> Unit,
|
||||||
onJoinCallClick: () -> Unit,
|
onJoinCallClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
|
|
@ -82,8 +84,11 @@ internal fun TimelineItemCallNotifyView(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isCallOngoing) {
|
if (roomCallState is RoomCallState.OnGoing) {
|
||||||
JoinCallMenuItem(onJoinCallClick)
|
CallMenuItem(
|
||||||
|
roomCallState = roomCallState,
|
||||||
|
onJoinCallClick = onJoinCallClick,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
Text(
|
Text(
|
||||||
text = event.sentTime,
|
text = event.sentTime,
|
||||||
|
|
@ -101,18 +106,14 @@ internal fun TimelineItemCallNotifyView(
|
||||||
internal fun TimelineItemCallNotifyViewPreview() {
|
internal fun TimelineItemCallNotifyViewPreview() {
|
||||||
ElementPreview {
|
ElementPreview {
|
||||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
TimelineItemCallNotifyView(
|
RoomCallStateProvider().values.forEach { roomCallState ->
|
||||||
event = aTimelineItemEvent(content = TimelineItemCallNotifyContent()),
|
TimelineItemCallNotifyView(
|
||||||
isCallOngoing = true,
|
event = aTimelineItemEvent(content = TimelineItemCallNotifyContent()),
|
||||||
onLongClick = {},
|
roomCallState = roomCallState,
|
||||||
onJoinCallClick = {},
|
onLongClick = {},
|
||||||
)
|
onJoinCallClick = {},
|
||||||
TimelineItemCallNotifyView(
|
)
|
||||||
event = aTimelineItemEvent(content = TimelineItemCallNotifyContent()),
|
}
|
||||||
isCallOngoing = false,
|
|
||||||
onLongClick = {},
|
|
||||||
onJoinCallClick = {},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ internal fun TimelineItemRow(
|
||||||
TimelineItemCallNotifyView(
|
TimelineItemCallNotifyView(
|
||||||
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp),
|
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp),
|
||||||
event = timelineItem,
|
event = timelineItem,
|
||||||
isCallOngoing = timelineRoomInfo.isCallOngoing,
|
roomCallState = timelineRoomInfo.roomCallState,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
onJoinCallClick = onJoinCallClick,
|
onJoinCallClick = onJoinCallClick,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -70,14 +70,14 @@ fun TimelineItemVideoView(
|
||||||
onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit,
|
onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val description = stringResource(CommonStrings.common_image)
|
val description = stringResource(CommonStrings.common_video)
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier.semantics { contentDescription = description }
|
modifier = modifier.semantics { contentDescription = description }
|
||||||
) {
|
) {
|
||||||
val containerModifier = if (content.showCaption) {
|
val containerModifier = if (content.showCaption) {
|
||||||
Modifier
|
Modifier
|
||||||
.padding(top = 6.dp)
|
.padding(top = 6.dp)
|
||||||
.clip(RoundedCornerShape(6.dp))
|
.clip(RoundedCornerShape(6.dp))
|
||||||
} else {
|
} else {
|
||||||
Modifier
|
Modifier
|
||||||
}
|
}
|
||||||
|
|
@ -93,8 +93,8 @@ fun TimelineItemVideoView(
|
||||||
var isLoaded by remember { mutableStateOf(false) }
|
var isLoaded by remember { mutableStateOf(false) }
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.then(if (isLoaded) Modifier.background(Color.White) else Modifier),
|
.then(if (isLoaded) Modifier.background(Color.White) else Modifier),
|
||||||
model = MediaRequestData(
|
model = MediaRequestData(
|
||||||
source = content.thumbnailSource,
|
source = content.thumbnailSource,
|
||||||
kind = MediaRequestData.Kind.File(
|
kind = MediaRequestData.Kind.File(
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ import io.element.android.features.messages.test.timeline.FakeHtmlConverterProvi
|
||||||
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
||||||
import io.element.android.features.poll.api.actions.EndPollAction
|
import io.element.android.features.poll.api.actions.EndPollAction
|
||||||
import io.element.android.features.poll.test.actions.FakeEndPollAction
|
import io.element.android.features.poll.test.actions.FakeEndPollAction
|
||||||
|
import io.element.android.features.roomcall.api.aStandByCallState
|
||||||
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
|
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
|
@ -139,27 +140,6 @@ class MessagesPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - call is disabled if user cannot join it even if there is an ongoing call`() = runTest {
|
|
||||||
val room = FakeMatrixRoom(
|
|
||||||
canUserJoinCallResult = { Result.success(false) },
|
|
||||||
canUserSendMessageResult = { _, _ -> Result.success(true) },
|
|
||||||
canRedactOwnResult = { Result.success(true) },
|
|
||||||
canRedactOtherResult = { Result.success(true) },
|
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
|
||||||
canUserPinUnpinResult = { Result.success(true) },
|
|
||||||
).apply {
|
|
||||||
givenRoomInfo(aRoomInfo(hasRoomCall = true))
|
|
||||||
}
|
|
||||||
val presenter = createMessagesPresenter(matrixRoom = room)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = consumeItemsUntilTimeout().last()
|
|
||||||
assertThat(initialState.callState).isEqualTo(RoomCallState.DISABLED)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - handle toggling a reaction`() = runTest {
|
fun `present - handle toggling a reaction`() = runTest {
|
||||||
val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
||||||
|
|
@ -1030,6 +1010,7 @@ class MessagesPresenterTest {
|
||||||
readReceiptBottomSheetPresenter = { aReadReceiptBottomSheetState() },
|
readReceiptBottomSheetPresenter = { aReadReceiptBottomSheetState() },
|
||||||
identityChangeStatePresenter = { anIdentityChangeState() },
|
identityChangeStatePresenter = { anIdentityChangeState() },
|
||||||
pinnedMessagesBannerPresenter = { aLoadedPinnedMessagesBannerState() },
|
pinnedMessagesBannerPresenter = { aLoadedPinnedMessagesBannerState() },
|
||||||
|
roomCallStatePresenter = { aStandByCallState() },
|
||||||
networkMonitor = FakeNetworkMonitor(),
|
networkMonitor = FakeNetworkMonitor(),
|
||||||
snackbarDispatcher = SnackbarDispatcher(),
|
snackbarDispatcher = SnackbarDispatcher(),
|
||||||
navigator = navigator,
|
navigator = navigator,
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,13 @@ import io.element.android.features.messages.impl.attachments.preview.SendActionS
|
||||||
import io.element.android.features.messages.impl.fixtures.aMediaAttachment
|
import io.element.android.features.messages.impl.fixtures.aMediaAttachment
|
||||||
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
||||||
import io.element.android.libraries.matrix.api.media.FileInfo
|
import io.element.android.libraries.matrix.api.media.FileInfo
|
||||||
|
import io.element.android.libraries.matrix.api.media.ImageInfo
|
||||||
|
import io.element.android.libraries.matrix.api.media.VideoInfo
|
||||||
|
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.test.A_CAPTION
|
||||||
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
|
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
|
||||||
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
||||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||||
|
|
@ -30,19 +35,23 @@ import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
||||||
import io.element.android.libraries.mediaviewer.test.viewer.aLocalMedia
|
import io.element.android.libraries.mediaviewer.test.viewer.aLocalMedia
|
||||||
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
import io.element.android.tests.testutils.lambda.any
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
import io.element.android.tests.testutils.lambda.value
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
class AttachmentsPreviewPresenterTest {
|
class AttachmentsPreviewPresenterTest {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val warmUpRule = WarmUpRule()
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
||||||
private val mediaPreProcessor = FakeMediaPreProcessor()
|
|
||||||
private val mockMediaUrl: Uri = mockk("localMediaUri")
|
private val mockMediaUrl: Uri = mockk("localMediaUri")
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -75,6 +84,80 @@ class AttachmentsPreviewPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - send image with caption success scenario`() = runTest {
|
||||||
|
val sendImageResult =
|
||||||
|
lambdaRecorder<File, File?, ImageInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _, _ ->
|
||||||
|
Result.success(FakeMediaUploadHandler())
|
||||||
|
}
|
||||||
|
val mediaPreProcessor = FakeMediaPreProcessor().apply {
|
||||||
|
givenImageResult()
|
||||||
|
}
|
||||||
|
val room = FakeMatrixRoom(
|
||||||
|
sendImageResult = sendImageResult,
|
||||||
|
)
|
||||||
|
val presenter = createAttachmentsPreviewPresenter(
|
||||||
|
room = room,
|
||||||
|
mediaPreProcessor = mediaPreProcessor,
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
initialState.textEditorState.setMarkdown(A_CAPTION)
|
||||||
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||||
|
val successState = awaitItem()
|
||||||
|
assertThat(successState.sendActionState).isEqualTo(SendActionState.Done)
|
||||||
|
sendImageResult.assertions().isCalledOnce().with(
|
||||||
|
any(),
|
||||||
|
any(),
|
||||||
|
any(),
|
||||||
|
value(A_CAPTION),
|
||||||
|
any(),
|
||||||
|
any(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - send video with caption success scenario`() = runTest {
|
||||||
|
val sendVideoResult =
|
||||||
|
lambdaRecorder<File, File?, VideoInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _, _ ->
|
||||||
|
Result.success(FakeMediaUploadHandler())
|
||||||
|
}
|
||||||
|
val mediaPreProcessor = FakeMediaPreProcessor().apply {
|
||||||
|
givenVideoResult()
|
||||||
|
}
|
||||||
|
val room = FakeMatrixRoom(
|
||||||
|
sendVideoResult = sendVideoResult,
|
||||||
|
)
|
||||||
|
val presenter = createAttachmentsPreviewPresenter(
|
||||||
|
room = room,
|
||||||
|
mediaPreProcessor = mediaPreProcessor,
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
initialState.textEditorState.setMarkdown(A_CAPTION)
|
||||||
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||||
|
val successState = awaitItem()
|
||||||
|
assertThat(successState.sendActionState).isEqualTo(SendActionState.Done)
|
||||||
|
sendVideoResult.assertions().isCalledOnce().with(
|
||||||
|
any(),
|
||||||
|
any(),
|
||||||
|
any(),
|
||||||
|
value(A_CAPTION),
|
||||||
|
any(),
|
||||||
|
any(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - send media failure scenario`() = runTest {
|
fun `present - send media failure scenario`() = runTest {
|
||||||
val failure = MediaPreProcessor.Failure(null)
|
val failure = MediaPreProcessor.Failure(null)
|
||||||
|
|
@ -121,11 +204,14 @@ class AttachmentsPreviewPresenterTest {
|
||||||
localMedia: LocalMedia = aLocalMedia(
|
localMedia: LocalMedia = aLocalMedia(
|
||||||
uri = mockMediaUrl,
|
uri = mockMediaUrl,
|
||||||
),
|
),
|
||||||
room: MatrixRoom = FakeMatrixRoom()
|
room: MatrixRoom = FakeMatrixRoom(),
|
||||||
|
permalinkBuilder: PermalinkBuilder = FakePermalinkBuilder(),
|
||||||
|
mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
|
||||||
): AttachmentsPreviewPresenter {
|
): AttachmentsPreviewPresenter {
|
||||||
return AttachmentsPreviewPresenter(
|
return AttachmentsPreviewPresenter(
|
||||||
attachment = aMediaAttachment(localMedia),
|
attachment = aMediaAttachment(localMedia),
|
||||||
mediaSender = MediaSender(mediaPreProcessor, room, InMemorySessionPreferencesStore())
|
mediaSender = MediaSender(mediaPreProcessor, room, InMemorySessionPreferencesStore()),
|
||||||
|
permalinkBuilder = permalinkBuilder,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import io.element.android.features.poll.api.actions.EndPollAction
|
||||||
import io.element.android.features.poll.api.actions.SendPollResponseAction
|
import io.element.android.features.poll.api.actions.SendPollResponseAction
|
||||||
import io.element.android.features.poll.test.actions.FakeEndPollAction
|
import io.element.android.features.poll.test.actions.FakeEndPollAction
|
||||||
import io.element.android.features.poll.test.actions.FakeSendPollResponseAction
|
import io.element.android.features.poll.test.actions.FakeSendPollResponseAction
|
||||||
|
import io.element.android.features.roomcall.api.aStandByCallState
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||||
|
|
@ -685,5 +686,6 @@ internal fun TestScope.createTimelinePresenter(
|
||||||
timelineController = TimelineController(room),
|
timelineController = TimelineController(room),
|
||||||
resolveVerifiedUserSendFailurePresenter = { aResolveVerifiedUserSendFailureState() },
|
resolveVerifiedUserSendFailurePresenter = { aResolveVerifiedUserSendFailureState() },
|
||||||
typingNotificationPresenter = { aTypingNotificationState() },
|
typingNotificationPresenter = { aTypingNotificationState() },
|
||||||
|
roomCallStatePresenter = { aStandByCallState() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.jvm.optionals.getOrElse
|
||||||
|
|
||||||
class RoomAliasResolverPresenter @AssistedInject constructor(
|
class RoomAliasResolverPresenter @AssistedInject constructor(
|
||||||
@Assisted private val roomAlias: RoomAlias,
|
@Assisted private val roomAlias: RoomAlias,
|
||||||
|
|
@ -57,7 +58,9 @@ class RoomAliasResolverPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
private fun CoroutineScope.resolveAlias(resolveState: MutableState<AsyncData<ResolvedRoomAlias>>) = launch {
|
private fun CoroutineScope.resolveAlias(resolveState: MutableState<AsyncData<ResolvedRoomAlias>>) = launch {
|
||||||
suspend {
|
suspend {
|
||||||
matrixClient.resolveRoomAlias(roomAlias).getOrThrow()
|
matrixClient.resolveRoomAlias(roomAlias)
|
||||||
|
.getOrThrow()
|
||||||
|
.getOrElse { error("Failed to resolve room alias $roomAlias") }
|
||||||
}.runCatchingUpdatingState(resolveState)
|
}.runCatchingUpdatingState(resolveState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import io.element.android.tests.testutils.WarmUpRule
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import java.util.Optional
|
||||||
|
|
||||||
class RoomAliasResolverPresenterTest {
|
class RoomAliasResolverPresenterTest {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
|
|
@ -42,7 +43,7 @@ class RoomAliasResolverPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - resolve alias to roomId`() = runTest {
|
fun `present - resolve alias to roomId`() = runTest {
|
||||||
val result = aResolvedRoomAlias()
|
val result = Optional.of(aResolvedRoomAlias())
|
||||||
val client = FakeMatrixClient(
|
val client = FakeMatrixClient(
|
||||||
resolveRoomAliasResult = { Result.success(result) }
|
resolveRoomAliasResult = { Result.success(result) }
|
||||||
)
|
)
|
||||||
|
|
@ -54,7 +55,7 @@ class RoomAliasResolverPresenterTest {
|
||||||
assertThat(awaitItem().resolveState.isLoading()).isTrue()
|
assertThat(awaitItem().resolveState.isLoading()).isTrue()
|
||||||
val resultState = awaitItem()
|
val resultState = awaitItem()
|
||||||
assertThat(resultState.roomAlias).isEqualTo(A_ROOM_ALIAS)
|
assertThat(resultState.roomAlias).isEqualTo(A_ROOM_ALIAS)
|
||||||
assertThat(resultState.resolveState.dataOrNull()).isEqualTo(result)
|
assertThat(resultState.resolveState.dataOrNull()).isEqualTo(result.get())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
20
features/roomcall/api/build.gradle.kts
Normal file
20
features/roomcall/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("io.element.android-library")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "io.element.android.features.roomcall.api"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(projects.libraries.architecture)
|
||||||
|
implementation(projects.libraries.matrix.api)
|
||||||
|
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.roomcall.api
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import io.element.android.features.roomcall.api.RoomCallState.OnGoing
|
||||||
|
import io.element.android.features.roomcall.api.RoomCallState.StandBy
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
sealed interface RoomCallState {
|
||||||
|
data class StandBy(
|
||||||
|
val canStartCall: Boolean,
|
||||||
|
) : RoomCallState
|
||||||
|
|
||||||
|
data class OnGoing(
|
||||||
|
val canJoinCall: Boolean,
|
||||||
|
val isUserInTheCall: Boolean,
|
||||||
|
val isUserLocallyInTheCall: Boolean,
|
||||||
|
) : RoomCallState
|
||||||
|
}
|
||||||
|
|
||||||
|
fun RoomCallState.hasPermissionToJoin() = when (this) {
|
||||||
|
is StandBy -> canStartCall
|
||||||
|
is OnGoing -> canJoinCall
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.roomcall.api
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
|
||||||
|
open class RoomCallStateProvider : PreviewParameterProvider<RoomCallState> {
|
||||||
|
override val values: Sequence<RoomCallState> = sequenceOf(
|
||||||
|
aStandByCallState(),
|
||||||
|
aStandByCallState(canStartCall = false),
|
||||||
|
anOngoingCallState(),
|
||||||
|
anOngoingCallState(canJoinCall = false),
|
||||||
|
anOngoingCallState(canJoinCall = true, isUserInTheCall = true),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun anOngoingCallState(
|
||||||
|
canJoinCall: Boolean = true,
|
||||||
|
isUserInTheCall: Boolean = false,
|
||||||
|
isUserLocallyInTheCall: Boolean = isUserInTheCall,
|
||||||
|
) = RoomCallState.OnGoing(
|
||||||
|
canJoinCall = canJoinCall,
|
||||||
|
isUserInTheCall = isUserInTheCall,
|
||||||
|
isUserLocallyInTheCall = isUserLocallyInTheCall,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun aStandByCallState(
|
||||||
|
canStartCall: Boolean = true,
|
||||||
|
) = RoomCallState.StandBy(
|
||||||
|
canStartCall = canStartCall,
|
||||||
|
)
|
||||||
38
features/roomcall/impl/build.gradle.kts
Normal file
38
features/roomcall/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import extension.setupAnvil
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("io.element.android-compose-library")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "io.element.android.features.roomcall.impl"
|
||||||
|
}
|
||||||
|
|
||||||
|
setupAnvil()
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
api(projects.features.roomcall.api)
|
||||||
|
implementation(libs.kotlinx.collections.immutable)
|
||||||
|
implementation(projects.features.call.api)
|
||||||
|
implementation(projects.libraries.architecture)
|
||||||
|
implementation(projects.libraries.matrix.api)
|
||||||
|
implementation(projects.libraries.matrixui)
|
||||||
|
|
||||||
|
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.call.test)
|
||||||
|
testImplementation(projects.tests.testutils)
|
||||||
|
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||||
|
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.roomcall.impl
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import io.element.android.features.call.api.CurrentCall
|
||||||
|
import io.element.android.features.call.api.CurrentCallService
|
||||||
|
import io.element.android.features.roomcall.api.RoomCallState
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.ui.room.canCall
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class RoomCallStatePresenter @Inject constructor(
|
||||||
|
private val room: MatrixRoom,
|
||||||
|
private val currentCallService: CurrentCallService,
|
||||||
|
) : Presenter<RoomCallState> {
|
||||||
|
@Composable
|
||||||
|
override fun present(): RoomCallState {
|
||||||
|
val roomInfo by room.roomInfoFlow.collectAsState(null)
|
||||||
|
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
||||||
|
val canJoinCall by room.canCall(updateKey = syncUpdateFlow.value)
|
||||||
|
val isUserInTheCall by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
room.sessionId in roomInfo?.activeRoomCallParticipants.orEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val currentCall by currentCallService.currentCall.collectAsState()
|
||||||
|
val isUserLocallyInTheCall by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
(currentCall as? CurrentCall.RoomCall)?.roomId == room.roomId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val callState = when {
|
||||||
|
roomInfo?.hasRoomCall == true -> RoomCallState.OnGoing(
|
||||||
|
canJoinCall = canJoinCall,
|
||||||
|
isUserInTheCall = isUserInTheCall,
|
||||||
|
isUserLocallyInTheCall = isUserLocallyInTheCall,
|
||||||
|
)
|
||||||
|
else -> RoomCallState.StandBy(canStartCall = canJoinCall)
|
||||||
|
}
|
||||||
|
return callState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.roomcall.impl.di
|
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesTo
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import io.element.android.features.roomcall.api.RoomCallState
|
||||||
|
import io.element.android.features.roomcall.impl.RoomCallStatePresenter
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.di.RoomScope
|
||||||
|
|
||||||
|
@ContributesTo(RoomScope::class)
|
||||||
|
@Module
|
||||||
|
interface RoomCallModule {
|
||||||
|
@Binds
|
||||||
|
fun bindRoomCallStatePresenter(presenter: RoomCallStatePresenter): Presenter<RoomCallState>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,201 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.roomcall.impl
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.call.api.CurrentCall
|
||||||
|
import io.element.android.features.call.api.CurrentCallService
|
||||||
|
import io.element.android.features.call.test.FakeCurrentCallService
|
||||||
|
import io.element.android.features.roomcall.api.RoomCallState
|
||||||
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||||
|
import io.element.android.tests.testutils.test
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class RoomCallStatePresenterTest {
|
||||||
|
@Test
|
||||||
|
fun `present - initial state`() = runTest {
|
||||||
|
val room = FakeMatrixRoom(
|
||||||
|
canUserJoinCallResult = { Result.success(false) },
|
||||||
|
)
|
||||||
|
val presenter = createRoomCallStatePresenter(matrixRoom = room)
|
||||||
|
presenter.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState).isEqualTo(
|
||||||
|
RoomCallState.StandBy(
|
||||||
|
canStartCall = false,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - initial state - user can join call`() = runTest {
|
||||||
|
val room = FakeMatrixRoom(
|
||||||
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
|
)
|
||||||
|
val presenter = createRoomCallStatePresenter(matrixRoom = room)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(1)
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState).isEqualTo(
|
||||||
|
RoomCallState.StandBy(
|
||||||
|
canStartCall = true,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - call is disabled if user cannot join it even if there is an ongoing call`() = runTest {
|
||||||
|
val room = FakeMatrixRoom(
|
||||||
|
canUserJoinCallResult = { Result.success(false) },
|
||||||
|
).apply {
|
||||||
|
givenRoomInfo(aRoomInfo(hasRoomCall = true))
|
||||||
|
}
|
||||||
|
val presenter = createRoomCallStatePresenter(matrixRoom = room)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(1)
|
||||||
|
assertThat(awaitItem()).isEqualTo(
|
||||||
|
RoomCallState.OnGoing(
|
||||||
|
canJoinCall = false,
|
||||||
|
isUserInTheCall = false,
|
||||||
|
isUserLocallyInTheCall = false,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - user has joined the call on another session`() = runTest {
|
||||||
|
val room = FakeMatrixRoom(
|
||||||
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
|
).apply {
|
||||||
|
givenRoomInfo(
|
||||||
|
aRoomInfo(
|
||||||
|
hasRoomCall = true,
|
||||||
|
activeRoomCallParticipants = listOf(sessionId),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val presenter = createRoomCallStatePresenter(matrixRoom = room)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(1)
|
||||||
|
assertThat(awaitItem()).isEqualTo(
|
||||||
|
RoomCallState.OnGoing(
|
||||||
|
canJoinCall = true,
|
||||||
|
isUserInTheCall = true,
|
||||||
|
isUserLocallyInTheCall = false,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - user has joined the call locally`() = runTest {
|
||||||
|
val room = FakeMatrixRoom(
|
||||||
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
|
).apply {
|
||||||
|
givenRoomInfo(
|
||||||
|
aRoomInfo(
|
||||||
|
hasRoomCall = true,
|
||||||
|
activeRoomCallParticipants = listOf(sessionId),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val presenter = createRoomCallStatePresenter(
|
||||||
|
matrixRoom = room,
|
||||||
|
currentCallService = FakeCurrentCallService(MutableStateFlow(CurrentCall.RoomCall(room.roomId))),
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(1)
|
||||||
|
assertThat(awaitItem()).isEqualTo(
|
||||||
|
RoomCallState.OnGoing(
|
||||||
|
canJoinCall = true,
|
||||||
|
isUserInTheCall = true,
|
||||||
|
isUserLocallyInTheCall = true,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - user leaves the call`() = runTest {
|
||||||
|
val room = FakeMatrixRoom(
|
||||||
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
|
).apply {
|
||||||
|
givenRoomInfo(
|
||||||
|
aRoomInfo(
|
||||||
|
hasRoomCall = true,
|
||||||
|
activeRoomCallParticipants = listOf(sessionId),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val currentCall = MutableStateFlow<CurrentCall>(CurrentCall.RoomCall(room.roomId))
|
||||||
|
val currentCallService = FakeCurrentCallService(currentCall = currentCall)
|
||||||
|
val presenter = createRoomCallStatePresenter(
|
||||||
|
matrixRoom = room,
|
||||||
|
currentCallService = currentCallService
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(1)
|
||||||
|
assertThat(awaitItem()).isEqualTo(
|
||||||
|
RoomCallState.OnGoing(
|
||||||
|
canJoinCall = true,
|
||||||
|
isUserInTheCall = true,
|
||||||
|
isUserLocallyInTheCall = true,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
currentCall.value = CurrentCall.None
|
||||||
|
assertThat(awaitItem()).isEqualTo(
|
||||||
|
RoomCallState.OnGoing(
|
||||||
|
canJoinCall = true,
|
||||||
|
isUserInTheCall = true,
|
||||||
|
isUserLocallyInTheCall = false,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
room.givenRoomInfo(
|
||||||
|
aRoomInfo(
|
||||||
|
hasRoomCall = true,
|
||||||
|
activeRoomCallParticipants = emptyList(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assertThat(awaitItem()).isEqualTo(
|
||||||
|
RoomCallState.OnGoing(
|
||||||
|
canJoinCall = true,
|
||||||
|
isUserInTheCall = false,
|
||||||
|
isUserLocallyInTheCall = false,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
room.givenRoomInfo(
|
||||||
|
aRoomInfo(
|
||||||
|
hasRoomCall = false,
|
||||||
|
activeRoomCallParticipants = emptyList(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assertThat(awaitItem()).isEqualTo(
|
||||||
|
RoomCallState.StandBy(
|
||||||
|
canStartCall = true,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createRoomCallStatePresenter(
|
||||||
|
matrixRoom: MatrixRoom,
|
||||||
|
currentCallService: CurrentCallService = FakeCurrentCallService(),
|
||||||
|
): RoomCallStatePresenter {
|
||||||
|
return RoomCallStatePresenter(
|
||||||
|
room = matrixRoom,
|
||||||
|
currentCallService = currentCallService,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -49,6 +49,7 @@ dependencies {
|
||||||
implementation(projects.services.analytics.compose)
|
implementation(projects.services.analytics.compose)
|
||||||
implementation(projects.features.poll.api)
|
implementation(projects.features.poll.api)
|
||||||
implementation(projects.features.messages.api)
|
implementation(projects.features.messages.api)
|
||||||
|
implementation(projects.features.roomcall.api)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
testImplementation(libs.coroutines.test)
|
testImplementation(libs.coroutines.test)
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import im.vector.app.features.analytics.plan.Interaction
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||||
import io.element.android.features.messages.api.pinned.IsPinnedMessagesFeatureEnabled
|
import io.element.android.features.messages.api.pinned.IsPinnedMessagesFeatureEnabled
|
||||||
|
import io.element.android.features.roomcall.api.RoomCallState
|
||||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.bool.orFalse
|
import io.element.android.libraries.core.bool.orFalse
|
||||||
|
|
@ -37,7 +38,6 @@ import io.element.android.libraries.matrix.api.room.isDm
|
||||||
import io.element.android.libraries.matrix.api.room.powerlevels.canInvite
|
import io.element.android.libraries.matrix.api.room.powerlevels.canInvite
|
||||||
import io.element.android.libraries.matrix.api.room.powerlevels.canSendState
|
import io.element.android.libraries.matrix.api.room.powerlevels.canSendState
|
||||||
import io.element.android.libraries.matrix.api.room.roomNotificationSettings
|
import io.element.android.libraries.matrix.api.room.roomNotificationSettings
|
||||||
import io.element.android.libraries.matrix.ui.room.canCall
|
|
||||||
import io.element.android.libraries.matrix.ui.room.getCurrentRoomMember
|
import io.element.android.libraries.matrix.ui.room.getCurrentRoomMember
|
||||||
import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
|
import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
|
||||||
import io.element.android.libraries.matrix.ui.room.isOwnUserAdmin
|
import io.element.android.libraries.matrix.ui.room.isOwnUserAdmin
|
||||||
|
|
@ -57,6 +57,7 @@ class RoomDetailsPresenter @Inject constructor(
|
||||||
private val notificationSettingsService: NotificationSettingsService,
|
private val notificationSettingsService: NotificationSettingsService,
|
||||||
private val roomMembersDetailsPresenterFactory: RoomMemberDetailsPresenter.Factory,
|
private val roomMembersDetailsPresenterFactory: RoomMemberDetailsPresenter.Factory,
|
||||||
private val leaveRoomPresenter: Presenter<LeaveRoomState>,
|
private val leaveRoomPresenter: Presenter<LeaveRoomState>,
|
||||||
|
private val roomCallStatePresenter: Presenter<RoomCallState>,
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
private val isPinnedMessagesFeatureEnabled: IsPinnedMessagesFeatureEnabled,
|
private val isPinnedMessagesFeatureEnabled: IsPinnedMessagesFeatureEnabled,
|
||||||
|
|
@ -87,18 +88,16 @@ class RoomDetailsPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val syncUpdateTimestamp by room.syncUpdateFlow.collectAsState()
|
|
||||||
|
|
||||||
val membersState by room.membersStateFlow.collectAsState()
|
val membersState by room.membersStateFlow.collectAsState()
|
||||||
val canInvite by getCanInvite(membersState)
|
val canInvite by getCanInvite(membersState)
|
||||||
val canEditName by getCanSendState(membersState, StateEventType.ROOM_NAME)
|
val canEditName by getCanSendState(membersState, StateEventType.ROOM_NAME)
|
||||||
val canEditAvatar by getCanSendState(membersState, StateEventType.ROOM_AVATAR)
|
val canEditAvatar by getCanSendState(membersState, StateEventType.ROOM_AVATAR)
|
||||||
val canEditTopic by getCanSendState(membersState, StateEventType.ROOM_TOPIC)
|
val canEditTopic by getCanSendState(membersState, StateEventType.ROOM_TOPIC)
|
||||||
val canJoinCall by room.canCall(updateKey = syncUpdateTimestamp)
|
|
||||||
val dmMember by room.getDirectRoomMember(membersState)
|
val dmMember by room.getDirectRoomMember(membersState)
|
||||||
val currentMember by room.getCurrentRoomMember(membersState)
|
val currentMember by room.getCurrentRoomMember(membersState)
|
||||||
val roomMemberDetailsPresenter = roomMemberDetailsPresenter(dmMember)
|
val roomMemberDetailsPresenter = roomMemberDetailsPresenter(dmMember)
|
||||||
val roomType by getRoomType(dmMember, currentMember)
|
val roomType by getRoomType(dmMember, currentMember)
|
||||||
|
val roomCallState = roomCallStatePresenter.present()
|
||||||
|
|
||||||
val topicState = remember(canEditTopic, roomTopic, roomType) {
|
val topicState = remember(canEditTopic, roomTopic, roomType) {
|
||||||
val topic = roomTopic
|
val topic = roomTopic
|
||||||
|
|
@ -143,7 +142,7 @@ class RoomDetailsPresenter @Inject constructor(
|
||||||
canInvite = canInvite,
|
canInvite = canInvite,
|
||||||
canEdit = (canEditAvatar || canEditName || canEditTopic) && roomType == RoomDetailsType.Room,
|
canEdit = (canEditAvatar || canEditName || canEditTopic) && roomType == RoomDetailsType.Room,
|
||||||
canShowNotificationSettings = canShowNotificationSettings.value,
|
canShowNotificationSettings = canShowNotificationSettings.value,
|
||||||
canCall = canJoinCall,
|
roomCallState = roomCallState,
|
||||||
roomType = roomType,
|
roomType = roomType,
|
||||||
roomMemberDetailsState = roomMemberDetailsState,
|
roomMemberDetailsState = roomMemberDetailsState,
|
||||||
leaveRoomState = leaveRoomState,
|
leaveRoomState = leaveRoomState,
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ package io.element.android.features.roomdetails.impl
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||||
|
import io.element.android.features.roomcall.api.RoomCallState
|
||||||
import io.element.android.features.userprofile.api.UserProfileState
|
import io.element.android.features.userprofile.api.UserProfileState
|
||||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
@ -31,7 +32,7 @@ data class RoomDetailsState(
|
||||||
val canEdit: Boolean,
|
val canEdit: Boolean,
|
||||||
val canInvite: Boolean,
|
val canInvite: Boolean,
|
||||||
val canShowNotificationSettings: Boolean,
|
val canShowNotificationSettings: Boolean,
|
||||||
val canCall: Boolean,
|
val roomCallState: RoomCallState,
|
||||||
val leaveRoomState: LeaveRoomState,
|
val leaveRoomState: LeaveRoomState,
|
||||||
val roomNotificationSettings: RoomNotificationSettings?,
|
val roomNotificationSettings: RoomNotificationSettings?,
|
||||||
val isFavorite: Boolean,
|
val isFavorite: Boolean,
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ package io.element.android.features.roomdetails.impl
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||||
import io.element.android.features.leaveroom.api.aLeaveRoomState
|
import io.element.android.features.leaveroom.api.aLeaveRoomState
|
||||||
|
import io.element.android.features.roomcall.api.RoomCallState
|
||||||
|
import io.element.android.features.roomcall.api.aStandByCallState
|
||||||
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
||||||
import io.element.android.features.userprofile.api.UserProfileState
|
import io.element.android.features.userprofile.api.UserProfileState
|
||||||
import io.element.android.features.userprofile.shared.aUserProfileState
|
import io.element.android.features.userprofile.shared.aUserProfileState
|
||||||
|
|
@ -42,7 +44,7 @@ open class RoomDetailsStateProvider : PreviewParameterProvider<RoomDetailsState>
|
||||||
// Also test the roomNotificationSettings ALL_MESSAGES in the same screenshot. Icon 'Mute' should be displayed
|
// Also test the roomNotificationSettings ALL_MESSAGES in the same screenshot. Icon 'Mute' should be displayed
|
||||||
roomNotificationSettings = aRoomNotificationSettings(mode = RoomNotificationMode.ALL_MESSAGES, isDefault = true)
|
roomNotificationSettings = aRoomNotificationSettings(mode = RoomNotificationMode.ALL_MESSAGES, isDefault = true)
|
||||||
),
|
),
|
||||||
aRoomDetailsState(canCall = false, canInvite = false),
|
aRoomDetailsState(roomCallState = aStandByCallState(false), canInvite = false),
|
||||||
aRoomDetailsState(isPublic = false),
|
aRoomDetailsState(isPublic = false),
|
||||||
aRoomDetailsState(heroes = aMatrixUserList()),
|
aRoomDetailsState(heroes = aMatrixUserList()),
|
||||||
aRoomDetailsState(pinnedMessagesCount = 3),
|
aRoomDetailsState(pinnedMessagesCount = 3),
|
||||||
|
|
@ -89,7 +91,7 @@ fun aRoomDetailsState(
|
||||||
canInvite: Boolean = false,
|
canInvite: Boolean = false,
|
||||||
canEdit: Boolean = false,
|
canEdit: Boolean = false,
|
||||||
canShowNotificationSettings: Boolean = true,
|
canShowNotificationSettings: Boolean = true,
|
||||||
canCall: Boolean = true,
|
roomCallState: RoomCallState = aStandByCallState(),
|
||||||
roomType: RoomDetailsType = RoomDetailsType.Room,
|
roomType: RoomDetailsType = RoomDetailsType.Room,
|
||||||
roomMemberDetailsState: UserProfileState? = null,
|
roomMemberDetailsState: UserProfileState? = null,
|
||||||
leaveRoomState: LeaveRoomState = aLeaveRoomState(),
|
leaveRoomState: LeaveRoomState = aLeaveRoomState(),
|
||||||
|
|
@ -112,7 +114,7 @@ fun aRoomDetailsState(
|
||||||
canInvite = canInvite,
|
canInvite = canInvite,
|
||||||
canEdit = canEdit,
|
canEdit = canEdit,
|
||||||
canShowNotificationSettings = canShowNotificationSettings,
|
canShowNotificationSettings = canShowNotificationSettings,
|
||||||
canCall = canCall,
|
roomCallState = roomCallState,
|
||||||
roomType = roomType,
|
roomType = roomType,
|
||||||
roomMemberDetailsState = roomMemberDetailsState,
|
roomMemberDetailsState = roomMemberDetailsState,
|
||||||
leaveRoomState = leaveRoomState,
|
leaveRoomState = leaveRoomState,
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ import im.vector.app.features.analytics.plan.Interaction
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomView
|
import io.element.android.features.leaveroom.api.LeaveRoomView
|
||||||
|
import io.element.android.features.roomcall.api.hasPermissionToJoin
|
||||||
import io.element.android.features.userprofile.shared.blockuser.BlockUserDialogs
|
import io.element.android.features.userprofile.shared.blockuser.BlockUserDialogs
|
||||||
import io.element.android.features.userprofile.shared.blockuser.BlockUserSection
|
import io.element.android.features.userprofile.shared.blockuser.BlockUserSection
|
||||||
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
|
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
|
||||||
|
|
@ -299,7 +300,8 @@ private fun MainActionsSection(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (state.canCall) {
|
if (state.roomCallState.hasPermissionToJoin()) {
|
||||||
|
// TODO Improve the view depending on all the cases here?
|
||||||
MainActionButton(
|
MainActionButton(
|
||||||
title = stringResource(CommonStrings.action_call),
|
title = stringResource(CommonStrings.action_call),
|
||||||
imageVector = CompoundIcons.VideoCall(),
|
imageVector = CompoundIcons.VideoCall(),
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import im.vector.app.features.analytics.plan.Interaction
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||||
import io.element.android.features.leaveroom.api.aLeaveRoomState
|
import io.element.android.features.leaveroom.api.aLeaveRoomState
|
||||||
|
import io.element.android.features.roomcall.api.aStandByCallState
|
||||||
import io.element.android.features.roomdetails.aMatrixRoom
|
import io.element.android.features.roomdetails.aMatrixRoom
|
||||||
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
||||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
||||||
|
|
@ -98,6 +99,7 @@ class RoomDetailsPresenterTest {
|
||||||
notificationSettingsService = matrixClient.notificationSettingsService(),
|
notificationSettingsService = matrixClient.notificationSettingsService(),
|
||||||
roomMembersDetailsPresenterFactory = roomMemberDetailsPresenterFactory,
|
roomMembersDetailsPresenterFactory = roomMemberDetailsPresenterFactory,
|
||||||
leaveRoomPresenter = { leaveRoomState },
|
leaveRoomPresenter = { leaveRoomState },
|
||||||
|
roomCallStatePresenter = { aStandByCallState() },
|
||||||
dispatchers = dispatchers,
|
dispatchers = dispatchers,
|
||||||
isPinnedMessagesFeatureEnabled = { isPinnedMessagesFeatureEnabled },
|
isPinnedMessagesFeatureEnabled = { isPinnedMessagesFeatureEnabled },
|
||||||
analyticsService = analyticsService,
|
analyticsService = analyticsService,
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private const val SEARCH_BATCH_SIZE = 20
|
||||||
|
|
||||||
class RoomDirectoryPresenter @Inject constructor(
|
class RoomDirectoryPresenter @Inject constructor(
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
private val roomDirectoryService: RoomDirectoryService,
|
private val roomDirectoryService: RoomDirectoryService,
|
||||||
|
|
@ -51,7 +53,7 @@ class RoomDirectoryPresenter @Inject constructor(
|
||||||
loadingMore = false
|
loadingMore = false
|
||||||
// debounce search query
|
// debounce search query
|
||||||
delay(300)
|
delay(300)
|
||||||
roomDirectoryList.filter(searchQuery, 20)
|
roomDirectoryList.filter(filter = searchQuery, batchSize = SEARCH_BATCH_SIZE, viaServerName = null)
|
||||||
}
|
}
|
||||||
LaunchedEffect(loadingMore) {
|
LaunchedEffect(loadingMore) {
|
||||||
if (loadingMore) {
|
if (loadingMore) {
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ import org.junit.Test
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - emit search event`() = runTest {
|
fun `present - emit search event`() = runTest {
|
||||||
val filterLambda = lambdaRecorder { _: String?, _: Int ->
|
val filterLambda = lambdaRecorder { _: String?, _: Int, _: String? ->
|
||||||
Result.success(Unit)
|
Result.success(Unit)
|
||||||
}
|
}
|
||||||
val roomDirectoryList = FakeRoomDirectoryList(filterLambda = filterLambda)
|
val roomDirectoryList = FakeRoomDirectoryList(filterLambda = filterLambda)
|
||||||
|
|
@ -99,7 +99,7 @@ import org.junit.Test
|
||||||
}
|
}
|
||||||
assert(filterLambda)
|
assert(filterLambda)
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
.with(value("test"), any())
|
.with(value("test"), any(), value(null))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ class DefaultShareService @Inject constructor(
|
||||||
PackageManager.GET_ACTIVITIES or PackageManager.MATCH_DISABLED_COMPONENTS
|
PackageManager.GET_ACTIVITIES or PackageManager.MATCH_DISABLED_COMPONENTS
|
||||||
)
|
)
|
||||||
.activities
|
.activities
|
||||||
.firstOrNull { it.name.endsWith(".ShareActivity") }
|
?.firstOrNull { it.name.endsWith(".ShareActivity") }
|
||||||
?.let { shareActivityInfo ->
|
?.let { shareActivityInfo ->
|
||||||
ComponentName(
|
ComponentName(
|
||||||
shareActivityInfo.packageName,
|
shareActivityInfo.packageName,
|
||||||
|
|
|
||||||
|
|
@ -80,14 +80,14 @@ fun IncomingVerificationView(
|
||||||
@Composable
|
@Composable
|
||||||
private fun IncomingVerificationHeader(step: Step) {
|
private fun IncomingVerificationHeader(step: Step) {
|
||||||
val iconStyle = when (step) {
|
val iconStyle = when (step) {
|
||||||
Step.Canceled,
|
Step.Canceled -> BigIcon.Style.AlertSolid
|
||||||
is Step.Initial -> BigIcon.Style.Default(CompoundIcons.LockSolid())
|
is Step.Initial -> BigIcon.Style.Default(CompoundIcons.LockSolid())
|
||||||
is Step.Verifying -> BigIcon.Style.Default(CompoundIcons.Reaction())
|
is Step.Verifying -> BigIcon.Style.Default(CompoundIcons.Reaction())
|
||||||
Step.Completed -> BigIcon.Style.SuccessSolid
|
Step.Completed -> BigIcon.Style.SuccessSolid
|
||||||
Step.Failure -> BigIcon.Style.AlertSolid
|
Step.Failure -> BigIcon.Style.AlertSolid
|
||||||
}
|
}
|
||||||
val titleTextId = when (step) {
|
val titleTextId = when (step) {
|
||||||
Step.Canceled -> CommonStrings.common_verification_cancelled
|
Step.Canceled -> R.string.screen_session_verification_request_failure_title
|
||||||
is Step.Initial -> R.string.screen_session_verification_request_title
|
is Step.Initial -> R.string.screen_session_verification_request_title
|
||||||
is Step.Verifying -> when (step.data) {
|
is Step.Verifying -> when (step.data) {
|
||||||
is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_title
|
is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_title
|
||||||
|
|
@ -97,7 +97,7 @@ private fun IncomingVerificationHeader(step: Step) {
|
||||||
Step.Failure -> R.string.screen_session_verification_request_failure_title
|
Step.Failure -> R.string.screen_session_verification_request_failure_title
|
||||||
}
|
}
|
||||||
val subtitleTextId = when (step) {
|
val subtitleTextId = when (step) {
|
||||||
Step.Canceled -> R.string.screen_session_verification_cancelled_subtitle
|
Step.Canceled -> R.string.screen_session_verification_request_failure_subtitle
|
||||||
is Step.Initial -> R.string.screen_session_verification_request_subtitle
|
is Step.Initial -> R.string.screen_session_verification_request_subtitle
|
||||||
is Step.Verifying -> when (step.data) {
|
is Step.Verifying -> when (step.data) {
|
||||||
is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_subtitle
|
is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_subtitle
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,7 @@ class VerifySelfSessionPresenter @AssistedInject constructor(
|
||||||
fun handleEvents(event: VerifySelfSessionViewEvents) {
|
fun handleEvents(event: VerifySelfSessionViewEvents) {
|
||||||
Timber.d("Verification user action: ${event::class.simpleName}")
|
Timber.d("Verification user action: ${event::class.simpleName}")
|
||||||
when (event) {
|
when (event) {
|
||||||
|
VerifySelfSessionViewEvents.UseAnotherDevice -> stateAndDispatch.dispatchAction(StateMachineEvent.UseAnotherDevice)
|
||||||
VerifySelfSessionViewEvents.RequestVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.RequestVerification)
|
VerifySelfSessionViewEvents.RequestVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.RequestVerification)
|
||||||
VerifySelfSessionViewEvents.StartSasVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.StartSasVerification)
|
VerifySelfSessionViewEvents.StartSasVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.StartSasVerification)
|
||||||
VerifySelfSessionViewEvents.ConfirmVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.AcceptChallenge)
|
VerifySelfSessionViewEvents.ConfirmVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.AcceptChallenge)
|
||||||
|
|
@ -134,6 +135,9 @@ class VerifySelfSessionPresenter @AssistedInject constructor(
|
||||||
isLastDevice = encryptionService.isLastDevice.value
|
isLastDevice = encryptionService.isLastDevice.value
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
VerifySelfSessionStateMachine.State.UseAnotherDevice -> {
|
||||||
|
VerifySelfSessionState.Step.UseAnotherDevice
|
||||||
|
}
|
||||||
StateMachineState.RequestingVerification,
|
StateMachineState.RequestingVerification,
|
||||||
StateMachineState.StartingSasVerification,
|
StateMachineState.StartingSasVerification,
|
||||||
StateMachineState.SasVerificationStarted,
|
StateMachineState.SasVerificationStarted,
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ data class VerifySelfSessionState(
|
||||||
|
|
||||||
// FIXME canEnterRecoveryKey value is never read.
|
// FIXME canEnterRecoveryKey value is never read.
|
||||||
data class Initial(val canEnterRecoveryKey: Boolean, val isLastDevice: Boolean = false) : Step
|
data class Initial(val canEnterRecoveryKey: Boolean, val isLastDevice: Boolean = false) : Step
|
||||||
|
data object UseAnotherDevice : Step
|
||||||
data object Canceled : Step
|
data object Canceled : Step
|
||||||
data object AwaitingOtherDeviceResponse : Step
|
data object AwaitingOtherDeviceResponse : Step
|
||||||
data object Ready : Step
|
data object Ready : Step
|
||||||
|
|
|
||||||
|
|
@ -38,12 +38,14 @@ class VerifySelfSessionStateMachine @Inject constructor(
|
||||||
init {
|
init {
|
||||||
spec {
|
spec {
|
||||||
inState<State.Initial> {
|
inState<State.Initial> {
|
||||||
|
on { _: Event.UseAnotherDevice, state ->
|
||||||
|
state.override { State.UseAnotherDevice.andLogStateChange() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inState<State.UseAnotherDevice> {
|
||||||
on { _: Event.RequestVerification, state ->
|
on { _: Event.RequestVerification, state ->
|
||||||
state.override { State.RequestingVerification.andLogStateChange() }
|
state.override { State.RequestingVerification.andLogStateChange() }
|
||||||
}
|
}
|
||||||
on { _: Event.StartSasVerification, state ->
|
|
||||||
state.override { State.StartingSasVerification.andLogStateChange() }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
inState<State.RequestingVerification> {
|
inState<State.RequestingVerification> {
|
||||||
onEnterEffect {
|
onEnterEffect {
|
||||||
|
|
@ -64,9 +66,6 @@ class VerifySelfSessionStateMachine @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
inState<State.Canceled> {
|
inState<State.Canceled> {
|
||||||
on { _: Event.RequestVerification, state ->
|
|
||||||
state.override { State.RequestingVerification.andLogStateChange() }
|
|
||||||
}
|
|
||||||
on { _: Event.Reset, state ->
|
on { _: Event.Reset, state ->
|
||||||
state.override { State.Initial.andLogStateChange() }
|
state.override { State.Initial.andLogStateChange() }
|
||||||
}
|
}
|
||||||
|
|
@ -119,6 +118,7 @@ class VerifySelfSessionStateMachine @Inject constructor(
|
||||||
on { _: Event.Cancel, state: MachineState<State> ->
|
on { _: Event.Cancel, state: MachineState<State> ->
|
||||||
when (state.snapshot) {
|
when (state.snapshot) {
|
||||||
State.Initial, State.Completed, State.Canceled -> state.noChange()
|
State.Initial, State.Completed, State.Canceled -> state.noChange()
|
||||||
|
State.UseAnotherDevice -> state.override { State.Initial.andLogStateChange() }
|
||||||
// For some reason `cancelVerification` is not calling its delegate `didCancel` method so we don't pass from
|
// For some reason `cancelVerification` is not calling its delegate `didCancel` method so we don't pass from
|
||||||
// `Canceling` state to `Canceled` automatically anymore
|
// `Canceling` state to `Canceled` automatically anymore
|
||||||
else -> {
|
else -> {
|
||||||
|
|
@ -144,6 +144,9 @@ class VerifySelfSessionStateMachine @Inject constructor(
|
||||||
/** The initial state, before verification started. */
|
/** The initial state, before verification started. */
|
||||||
data object Initial : State
|
data object Initial : State
|
||||||
|
|
||||||
|
/** Let the user know that they need to get ready on their other session. */
|
||||||
|
data object UseAnotherDevice : State
|
||||||
|
|
||||||
/** Waiting for verification acceptance. */
|
/** Waiting for verification acceptance. */
|
||||||
data object RequestingVerification : State
|
data object RequestingVerification : State
|
||||||
|
|
||||||
|
|
@ -175,6 +178,9 @@ class VerifySelfSessionStateMachine @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface Event {
|
sealed interface Event {
|
||||||
|
/** User wants to use another session. */
|
||||||
|
data object UseAnotherDevice : Event
|
||||||
|
|
||||||
/** Request verification. */
|
/** Request verification. */
|
||||||
data object RequestVerification : Event
|
data object RequestVerification : Event
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,9 @@ open class VerifySelfSessionStateProvider : PreviewParameterProvider<VerifySelfS
|
||||||
aVerifySelfSessionState(
|
aVerifySelfSessionState(
|
||||||
step = Step.Skipped
|
step = Step.Skipped
|
||||||
),
|
),
|
||||||
|
aVerifySelfSessionState(
|
||||||
|
step = Step.UseAnotherDevice
|
||||||
|
),
|
||||||
// Add other state here
|
// Add other state here
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,9 @@ fun VerifySelfSessionView(
|
||||||
fun cancelOrResetFlow() {
|
fun cancelOrResetFlow() {
|
||||||
when (step) {
|
when (step) {
|
||||||
is Step.Canceled -> state.eventSink(VerifySelfSessionViewEvents.Reset)
|
is Step.Canceled -> state.eventSink(VerifySelfSessionViewEvents.Reset)
|
||||||
is Step.AwaitingOtherDeviceResponse, Step.Ready -> state.eventSink(VerifySelfSessionViewEvents.Cancel)
|
is Step.AwaitingOtherDeviceResponse,
|
||||||
|
Step.UseAnotherDevice,
|
||||||
|
Step.Ready -> state.eventSink(VerifySelfSessionViewEvents.Cancel)
|
||||||
is Step.Verifying -> {
|
is Step.Verifying -> {
|
||||||
if (!step.state.isLoading()) {
|
if (!step.state.isLoading()) {
|
||||||
state.eventSink(VerifySelfSessionViewEvents.DeclineVerification)
|
state.eventSink(VerifySelfSessionViewEvents.DeclineVerification)
|
||||||
|
|
@ -159,7 +161,9 @@ fun VerifySelfSessionView(
|
||||||
private fun VerifySelfSessionHeader(step: Step) {
|
private fun VerifySelfSessionHeader(step: Step) {
|
||||||
val iconStyle = when (step) {
|
val iconStyle = when (step) {
|
||||||
Step.Loading -> error("Should not happen")
|
Step.Loading -> error("Should not happen")
|
||||||
is Step.Initial, Step.AwaitingOtherDeviceResponse -> BigIcon.Style.Default(CompoundIcons.LockSolid())
|
is Step.Initial -> BigIcon.Style.Default(CompoundIcons.LockSolid())
|
||||||
|
Step.UseAnotherDevice -> BigIcon.Style.Default(CompoundIcons.Devices())
|
||||||
|
Step.AwaitingOtherDeviceResponse -> BigIcon.Style.Default(CompoundIcons.Devices())
|
||||||
Step.Canceled -> BigIcon.Style.AlertSolid
|
Step.Canceled -> BigIcon.Style.AlertSolid
|
||||||
Step.Ready, is Step.Verifying -> BigIcon.Style.Default(CompoundIcons.Reaction())
|
Step.Ready, is Step.Verifying -> BigIcon.Style.Default(CompoundIcons.Reaction())
|
||||||
Step.Completed -> BigIcon.Style.SuccessSolid
|
Step.Completed -> BigIcon.Style.SuccessSolid
|
||||||
|
|
@ -167,8 +171,10 @@ private fun VerifySelfSessionHeader(step: Step) {
|
||||||
}
|
}
|
||||||
val titleTextId = when (step) {
|
val titleTextId = when (step) {
|
||||||
Step.Loading -> error("Should not happen")
|
Step.Loading -> error("Should not happen")
|
||||||
is Step.Initial, Step.AwaitingOtherDeviceResponse -> R.string.screen_identity_confirmation_title
|
is Step.Initial -> R.string.screen_identity_confirmation_title
|
||||||
Step.Canceled -> CommonStrings.common_verification_cancelled
|
Step.UseAnotherDevice -> R.string.screen_session_verification_use_another_device_title
|
||||||
|
Step.AwaitingOtherDeviceResponse -> R.string.screen_session_verification_waiting_another_device_title
|
||||||
|
Step.Canceled -> CommonStrings.common_verification_failed
|
||||||
Step.Ready -> R.string.screen_session_verification_compare_emojis_title
|
Step.Ready -> R.string.screen_session_verification_compare_emojis_title
|
||||||
Step.Completed -> R.string.screen_identity_confirmed_title
|
Step.Completed -> R.string.screen_identity_confirmed_title
|
||||||
is Step.Verifying -> when (step.data) {
|
is Step.Verifying -> when (step.data) {
|
||||||
|
|
@ -179,8 +185,10 @@ private fun VerifySelfSessionHeader(step: Step) {
|
||||||
}
|
}
|
||||||
val subtitleTextId = when (step) {
|
val subtitleTextId = when (step) {
|
||||||
Step.Loading -> error("Should not happen")
|
Step.Loading -> error("Should not happen")
|
||||||
is Step.Initial, Step.AwaitingOtherDeviceResponse -> R.string.screen_identity_confirmation_subtitle
|
is Step.Initial -> R.string.screen_identity_confirmation_subtitle
|
||||||
Step.Canceled -> R.string.screen_session_verification_cancelled_subtitle
|
Step.UseAnotherDevice -> R.string.screen_session_verification_use_another_device_subtitle
|
||||||
|
Step.AwaitingOtherDeviceResponse -> R.string.screen_session_verification_waiting_another_device_subtitle
|
||||||
|
Step.Canceled -> R.string.screen_session_verification_failed_subtitle
|
||||||
Step.Ready -> R.string.screen_session_verification_ready_subtitle
|
Step.Ready -> R.string.screen_session_verification_ready_subtitle
|
||||||
Step.Completed -> R.string.screen_identity_confirmed_subtitle
|
Step.Completed -> R.string.screen_identity_confirmed_subtitle
|
||||||
is Step.Verifying -> when (step.data) {
|
is Step.Verifying -> when (step.data) {
|
||||||
|
|
@ -248,25 +256,18 @@ private fun VerifySelfSessionBottomMenu(
|
||||||
Step.Loading -> error("Should not happen")
|
Step.Loading -> error("Should not happen")
|
||||||
is Step.Initial -> {
|
is Step.Initial -> {
|
||||||
VerificationBottomMenu {
|
VerificationBottomMenu {
|
||||||
if (verificationViewState.isLastDevice) {
|
if (verificationViewState.isLastDevice.not()) {
|
||||||
Button(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
text = stringResource(R.string.screen_session_verification_enter_recovery_key),
|
|
||||||
onClick = onEnterRecoveryKey,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Button(
|
Button(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
text = stringResource(R.string.screen_identity_use_another_device),
|
text = stringResource(R.string.screen_identity_use_another_device),
|
||||||
onClick = { eventSink(VerifySelfSessionViewEvents.RequestVerification) },
|
onClick = { eventSink(VerifySelfSessionViewEvents.UseAnotherDevice) },
|
||||||
)
|
|
||||||
Button(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
text = stringResource(R.string.screen_session_verification_enter_recovery_key),
|
|
||||||
onClick = onEnterRecoveryKey,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// This option should always be displayed
|
Button(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
text = stringResource(R.string.screen_session_verification_enter_recovery_key),
|
||||||
|
onClick = onEnterRecoveryKey,
|
||||||
|
)
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
text = stringResource(R.string.screen_identity_confirmation_cannot_confirm),
|
text = stringResource(R.string.screen_identity_confirmation_cannot_confirm),
|
||||||
|
|
@ -274,18 +275,26 @@ private fun VerifySelfSessionBottomMenu(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is Step.UseAnotherDevice -> {
|
||||||
|
VerificationBottomMenu {
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
text = stringResource(CommonStrings.action_start_verification),
|
||||||
|
onClick = { eventSink(VerifySelfSessionViewEvents.RequestVerification) },
|
||||||
|
)
|
||||||
|
// Placeholder so the 1st button keeps its vertical position
|
||||||
|
Spacer(modifier = Modifier.height(40.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
is Step.Canceled -> {
|
is Step.Canceled -> {
|
||||||
VerificationBottomMenu {
|
VerificationBottomMenu {
|
||||||
Button(
|
Button(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
text = stringResource(R.string.screen_session_verification_positive_button_canceled),
|
text = stringResource(CommonStrings.action_done),
|
||||||
onClick = { eventSink(VerifySelfSessionViewEvents.RequestVerification) },
|
|
||||||
)
|
|
||||||
TextButton(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
text = stringResource(CommonStrings.action_cancel),
|
|
||||||
onClick = onCancelClick,
|
onClick = onCancelClick,
|
||||||
)
|
)
|
||||||
|
// Placeholder so the 1st button keeps its vertical position
|
||||||
|
Spacer(modifier = Modifier.height(40.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is Step.Ready -> {
|
is Step.Ready -> {
|
||||||
|
|
@ -309,6 +318,7 @@ private fun VerifySelfSessionBottomMenu(
|
||||||
text = stringResource(R.string.screen_identity_waiting_on_other_device),
|
text = stringResource(R.string.screen_identity_waiting_on_other_device),
|
||||||
onClick = {},
|
onClick = {},
|
||||||
showProgress = true,
|
showProgress = true,
|
||||||
|
enabled = false,
|
||||||
)
|
)
|
||||||
// Placeholder so the 1st button keeps its vertical position
|
// Placeholder so the 1st button keeps its vertical position
|
||||||
Spacer(modifier = Modifier.height(40.dp))
|
Spacer(modifier = Modifier.height(40.dp))
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
package io.element.android.features.verifysession.impl.outgoing
|
package io.element.android.features.verifysession.impl.outgoing
|
||||||
|
|
||||||
sealed interface VerifySelfSessionViewEvents {
|
sealed interface VerifySelfSessionViewEvents {
|
||||||
|
data object UseAnotherDevice : VerifySelfSessionViewEvents
|
||||||
data object RequestVerification : VerifySelfSessionViewEvents
|
data object RequestVerification : VerifySelfSessionViewEvents
|
||||||
data object StartSasVerification : VerifySelfSessionViewEvents
|
data object StartSasVerification : VerifySelfSessionViewEvents
|
||||||
data object ConfirmVerification : VerifySelfSessionViewEvents
|
data object ConfirmVerification : VerifySelfSessionViewEvents
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,10 @@
|
||||||
<string name="screen_session_verification_request_title">"Verification requested"</string>
|
<string name="screen_session_verification_request_title">"Verification requested"</string>
|
||||||
<string name="screen_session_verification_they_dont_match">"They don’t match"</string>
|
<string name="screen_session_verification_they_dont_match">"They don’t match"</string>
|
||||||
<string name="screen_session_verification_they_match">"They match"</string>
|
<string name="screen_session_verification_they_match">"They match"</string>
|
||||||
|
<string name="screen_session_verification_use_another_device_subtitle">"Make sure you have the app open in the other device before starting verification from here."</string>
|
||||||
|
<string name="screen_session_verification_use_another_device_title">"Open the app on another verified device"</string>
|
||||||
|
<string name="screen_session_verification_waiting_another_device_subtitle">"You should see a popup on the other device. Start the verification from there now."</string>
|
||||||
|
<string name="screen_session_verification_waiting_another_device_title">"Start verification on the other device"</string>
|
||||||
<string name="screen_session_verification_waiting_to_accept_subtitle">"Accept the request to start the verification process in your other session to continue."</string>
|
<string name="screen_session_verification_waiting_to_accept_subtitle">"Accept the request to start the verification process in your other session to continue."</string>
|
||||||
<string name="screen_session_verification_waiting_to_accept_title">"Waiting to accept request"</string>
|
<string name="screen_session_verification_waiting_to_accept_title">"Waiting to accept request"</string>
|
||||||
<string name="screen_signout_in_progress_dialog_content">"Signing out…"</string>
|
<string name="screen_signout_in_progress_dialog_content">"Signing out…"</string>
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,7 @@
|
||||||
|
|
||||||
package io.element.android.features.verifysession.impl.outgoing
|
package io.element.android.features.verifysession.impl.outgoing
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionMode
|
|
||||||
import app.cash.molecule.moleculeFlow
|
|
||||||
import app.cash.turbine.ReceiveTurbine
|
import app.cash.turbine.ReceiveTurbine
|
||||||
import app.cash.turbine.test
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.logout.api.LogoutUseCase
|
import io.element.android.features.logout.api.LogoutUseCase
|
||||||
import io.element.android.features.logout.test.FakeLogoutUseCase
|
import io.element.android.features.logout.test.FakeLogoutUseCase
|
||||||
|
|
@ -33,6 +30,7 @@ import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
import io.element.android.tests.testutils.lambda.value
|
||||||
|
import io.element.android.tests.testutils.test
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
|
@ -48,9 +46,7 @@ class VerifySelfSessionPresenterTest {
|
||||||
val presenter = createVerifySelfSessionPresenter(
|
val presenter = createVerifySelfSessionPresenter(
|
||||||
service = unverifiedSessionService(),
|
service = unverifiedSessionService(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
awaitItem().run {
|
awaitItem().run {
|
||||||
assertThat(step).isEqualTo(Step.Initial(false))
|
assertThat(step).isEqualTo(Step.Initial(false))
|
||||||
assertThat(displaySkipButton).isTrue()
|
assertThat(displaySkipButton).isTrue()
|
||||||
|
|
@ -65,9 +61,7 @@ class VerifySelfSessionPresenterTest {
|
||||||
service = unverifiedSessionService(),
|
service = unverifiedSessionService(),
|
||||||
buildMeta = buildMeta,
|
buildMeta = buildMeta,
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
assertThat(awaitItem().displaySkipButton).isFalse()
|
assertThat(awaitItem().displaySkipButton).isFalse()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -83,9 +77,7 @@ class VerifySelfSessionPresenterTest {
|
||||||
emitRecoveryState(RecoveryState.INCOMPLETE)
|
emitRecoveryState(RecoveryState.INCOMPLETE)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
assertThat(awaitItem().step).isEqualTo(Step.Initial(true))
|
assertThat(awaitItem().step).isEqualTo(Step.Initial(true))
|
||||||
resetLambda.assertions().isCalledOnce().with(value(true))
|
resetLambda.assertions().isCalledOnce().with(value(true))
|
||||||
}
|
}
|
||||||
|
|
@ -100,9 +92,7 @@ class VerifySelfSessionPresenterTest {
|
||||||
emitRecoveryState(RecoveryState.INCOMPLETE)
|
emitRecoveryState(RecoveryState.INCOMPLETE)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
assertThat(awaitItem().step).isEqualTo(Step.Initial(canEnterRecoveryKey = true, isLastDevice = true))
|
assertThat(awaitItem().step).isEqualTo(Step.Initial(canEnterRecoveryKey = true, isLastDevice = true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -114,43 +104,17 @@ class VerifySelfSessionPresenterTest {
|
||||||
startVerificationLambda = { },
|
startVerificationLambda = { },
|
||||||
)
|
)
|
||||||
val presenter = createVerifySelfSessionPresenter(service)
|
val presenter = createVerifySelfSessionPresenter(service)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
requestVerificationAndAwaitVerifyingState(service)
|
requestVerificationAndAwaitVerifyingState(service)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - Handles startSasVerification`() = runTest {
|
|
||||||
val service = unverifiedSessionService(
|
|
||||||
startVerificationLambda = { },
|
|
||||||
)
|
|
||||||
val presenter = createVerifySelfSessionPresenter(service)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
assertThat(initialState.step).isEqualTo(Step.Initial(false))
|
|
||||||
initialState.eventSink(VerifySelfSessionViewEvents.StartSasVerification)
|
|
||||||
// Await for other device response:
|
|
||||||
assertThat(awaitItem().step).isEqualTo(Step.AwaitingOtherDeviceResponse)
|
|
||||||
service.emitVerificationFlowState(VerificationFlowState.DidStartSasVerification)
|
|
||||||
// ChallengeReceived:
|
|
||||||
service.emitVerificationFlowState(VerificationFlowState.DidReceiveVerificationData(SessionVerificationData.Emojis(emptyList())))
|
|
||||||
val verifyingState = awaitItem()
|
|
||||||
assertThat(verifyingState.step).isInstanceOf(Step.Verifying::class.java)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - Cancellation on initial state does nothing`() = runTest {
|
fun `present - Cancellation on initial state does nothing`() = runTest {
|
||||||
val presenter = createVerifySelfSessionPresenter(
|
val presenter = createVerifySelfSessionPresenter(
|
||||||
service = unverifiedSessionService(),
|
service = unverifiedSessionService(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.step).isEqualTo(Step.Initial(false))
|
assertThat(initialState.step).isEqualTo(Step.Initial(false))
|
||||||
val eventSink = initialState.eventSink
|
val eventSink = initialState.eventSink
|
||||||
|
|
@ -167,9 +131,7 @@ class VerifySelfSessionPresenterTest {
|
||||||
approveVerificationLambda = { },
|
approveVerificationLambda = { },
|
||||||
)
|
)
|
||||||
val presenter = createVerifySelfSessionPresenter(service)
|
val presenter = createVerifySelfSessionPresenter(service)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val state = requestVerificationAndAwaitVerifyingState(service)
|
val state = requestVerificationAndAwaitVerifyingState(service)
|
||||||
state.eventSink(VerifySelfSessionViewEvents.ConfirmVerification)
|
state.eventSink(VerifySelfSessionViewEvents.ConfirmVerification)
|
||||||
// Cancelling
|
// Cancelling
|
||||||
|
|
@ -186,9 +148,8 @@ class VerifySelfSessionPresenterTest {
|
||||||
requestVerificationLambda = { },
|
requestVerificationLambda = { },
|
||||||
)
|
)
|
||||||
val presenter = createVerifySelfSessionPresenter(service)
|
val presenter = createVerifySelfSessionPresenter(service)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
awaitItem().eventSink(VerifySelfSessionViewEvents.UseAnotherDevice)
|
||||||
}.test {
|
|
||||||
awaitItem().eventSink(VerifySelfSessionViewEvents.RequestVerification)
|
awaitItem().eventSink(VerifySelfSessionViewEvents.RequestVerification)
|
||||||
service.emitVerificationFlowState(VerificationFlowState.DidFail)
|
service.emitVerificationFlowState(VerificationFlowState.DidFail)
|
||||||
assertThat(awaitItem().step).isInstanceOf(Step.AwaitingOtherDeviceResponse::class.java)
|
assertThat(awaitItem().step).isInstanceOf(Step.AwaitingOtherDeviceResponse::class.java)
|
||||||
|
|
@ -204,9 +165,7 @@ class VerifySelfSessionPresenterTest {
|
||||||
cancelVerificationLambda = { },
|
cancelVerificationLambda = { },
|
||||||
)
|
)
|
||||||
val presenter = createVerifySelfSessionPresenter(service)
|
val presenter = createVerifySelfSessionPresenter(service)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val state = requestVerificationAndAwaitVerifyingState(service)
|
val state = requestVerificationAndAwaitVerifyingState(service)
|
||||||
state.eventSink(VerifySelfSessionViewEvents.Cancel)
|
state.eventSink(VerifySelfSessionViewEvents.Cancel)
|
||||||
assertThat(awaitItem().step).isEqualTo(Step.Canceled)
|
assertThat(awaitItem().step).isEqualTo(Step.Canceled)
|
||||||
|
|
@ -220,35 +179,13 @@ class VerifySelfSessionPresenterTest {
|
||||||
startVerificationLambda = { },
|
startVerificationLambda = { },
|
||||||
)
|
)
|
||||||
val presenter = createVerifySelfSessionPresenter(service)
|
val presenter = createVerifySelfSessionPresenter(service)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
requestVerificationAndAwaitVerifyingState(service)
|
requestVerificationAndAwaitVerifyingState(service)
|
||||||
service.emitVerificationFlowState(VerificationFlowState.DidReceiveVerificationData(SessionVerificationData.Emojis(emptyList())))
|
service.emitVerificationFlowState(VerificationFlowState.DidReceiveVerificationData(SessionVerificationData.Emojis(emptyList())))
|
||||||
ensureAllEventsConsumed()
|
ensureAllEventsConsumed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - Restart after cancellation returns to requesting verification`() = runTest {
|
|
||||||
val service = unverifiedSessionService(
|
|
||||||
requestVerificationLambda = { },
|
|
||||||
startVerificationLambda = { },
|
|
||||||
)
|
|
||||||
val presenter = createVerifySelfSessionPresenter(service)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val state = requestVerificationAndAwaitVerifyingState(service)
|
|
||||||
service.emitVerificationFlowState(VerificationFlowState.DidCancel)
|
|
||||||
assertThat(awaitItem().step).isEqualTo(Step.Canceled)
|
|
||||||
state.eventSink(VerifySelfSessionViewEvents.RequestVerification)
|
|
||||||
// Went back to requesting verification
|
|
||||||
assertThat(awaitItem().step).isEqualTo(Step.AwaitingOtherDeviceResponse)
|
|
||||||
cancelAndIgnoreRemainingEvents()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - Go back after cancellation returns to initial state`() = runTest {
|
fun `present - Go back after cancellation returns to initial state`() = runTest {
|
||||||
val service = unverifiedSessionService(
|
val service = unverifiedSessionService(
|
||||||
|
|
@ -256,9 +193,7 @@ class VerifySelfSessionPresenterTest {
|
||||||
startVerificationLambda = { },
|
startVerificationLambda = { },
|
||||||
)
|
)
|
||||||
val presenter = createVerifySelfSessionPresenter(service)
|
val presenter = createVerifySelfSessionPresenter(service)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val state = requestVerificationAndAwaitVerifyingState(service)
|
val state = requestVerificationAndAwaitVerifyingState(service)
|
||||||
service.emitVerificationFlowState(VerificationFlowState.DidCancel)
|
service.emitVerificationFlowState(VerificationFlowState.DidCancel)
|
||||||
assertThat(awaitItem().step).isEqualTo(Step.Canceled)
|
assertThat(awaitItem().step).isEqualTo(Step.Canceled)
|
||||||
|
|
@ -280,9 +215,7 @@ class VerifySelfSessionPresenterTest {
|
||||||
approveVerificationLambda = { },
|
approveVerificationLambda = { },
|
||||||
)
|
)
|
||||||
val presenter = createVerifySelfSessionPresenter(service)
|
val presenter = createVerifySelfSessionPresenter(service)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val state = requestVerificationAndAwaitVerifyingState(
|
val state = requestVerificationAndAwaitVerifyingState(
|
||||||
service,
|
service,
|
||||||
SessionVerificationData.Emojis(emojis)
|
SessionVerificationData.Emojis(emojis)
|
||||||
|
|
@ -307,9 +240,7 @@ class VerifySelfSessionPresenterTest {
|
||||||
declineVerificationLambda = { },
|
declineVerificationLambda = { },
|
||||||
)
|
)
|
||||||
val presenter = createVerifySelfSessionPresenter(service)
|
val presenter = createVerifySelfSessionPresenter(service)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val state = requestVerificationAndAwaitVerifyingState(service)
|
val state = requestVerificationAndAwaitVerifyingState(service)
|
||||||
state.eventSink(VerifySelfSessionViewEvents.DeclineVerification)
|
state.eventSink(VerifySelfSessionViewEvents.DeclineVerification)
|
||||||
assertThat(awaitItem().step).isEqualTo(
|
assertThat(awaitItem().step).isEqualTo(
|
||||||
|
|
@ -330,9 +261,7 @@ class VerifySelfSessionPresenterTest {
|
||||||
startVerificationLambda = { },
|
startVerificationLambda = { },
|
||||||
)
|
)
|
||||||
val presenter = createVerifySelfSessionPresenter(service)
|
val presenter = createVerifySelfSessionPresenter(service)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val state = requestVerificationAndAwaitVerifyingState(service)
|
val state = requestVerificationAndAwaitVerifyingState(service)
|
||||||
state.eventSink(VerifySelfSessionViewEvents.SkipVerification)
|
state.eventSink(VerifySelfSessionViewEvents.SkipVerification)
|
||||||
assertThat(awaitItem().step).isEqualTo(Step.Skipped)
|
assertThat(awaitItem().step).isEqualTo(Step.Skipped)
|
||||||
|
|
@ -352,9 +281,7 @@ class VerifySelfSessionPresenterTest {
|
||||||
service = service,
|
service = service,
|
||||||
showDeviceVerifiedScreen = true,
|
showDeviceVerifiedScreen = true,
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
assertThat(awaitItem().step).isEqualTo(Step.Completed)
|
assertThat(awaitItem().step).isEqualTo(Step.Completed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -372,9 +299,7 @@ class VerifySelfSessionPresenterTest {
|
||||||
service = service,
|
service = service,
|
||||||
showDeviceVerifiedScreen = false,
|
showDeviceVerifiedScreen = false,
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
assertThat(awaitItem().step).isEqualTo(Step.Skipped)
|
assertThat(awaitItem().step).isEqualTo(Step.Skipped)
|
||||||
}
|
}
|
||||||
|
|
@ -394,9 +319,7 @@ class VerifySelfSessionPresenterTest {
|
||||||
service,
|
service,
|
||||||
logoutUseCase = FakeLogoutUseCase(signOutLambda)
|
logoutUseCase = FakeLogoutUseCase(signOutLambda)
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
val initialItem = awaitItem()
|
val initialItem = awaitItem()
|
||||||
initialItem.eventSink(VerifySelfSessionViewEvents.SignOut)
|
initialItem.eventSink(VerifySelfSessionViewEvents.SignOut)
|
||||||
|
|
@ -414,6 +337,9 @@ class VerifySelfSessionPresenterTest {
|
||||||
): VerifySelfSessionState {
|
): VerifySelfSessionState {
|
||||||
var state = awaitItem()
|
var state = awaitItem()
|
||||||
assertThat(state.step).isEqualTo(Step.Initial(false))
|
assertThat(state.step).isEqualTo(Step.Initial(false))
|
||||||
|
state.eventSink(VerifySelfSessionViewEvents.UseAnotherDevice)
|
||||||
|
state = awaitItem()
|
||||||
|
assertThat(state.step).isEqualTo(Step.UseAnotherDevice)
|
||||||
state.eventSink(VerifySelfSessionViewEvents.RequestVerification)
|
state.eventSink(VerifySelfSessionViewEvents.RequestVerification)
|
||||||
// Await for other device response:
|
// Await for other device response:
|
||||||
fakeService.emitVerificationFlowState(VerificationFlowState.DidAcceptVerificationRequest)
|
fakeService.emitVerificationFlowState(VerificationFlowState.DidAcceptVerificationRequest)
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ core = "1.13.1"
|
||||||
# due to the DefaultMigrationStore not behaving as expected.
|
# due to the DefaultMigrationStore not behaving as expected.
|
||||||
# Stick to 1.0.0 for now, and ensure that this scenario cannot be reproduced when upgrading the version.
|
# Stick to 1.0.0 for now, and ensure that this scenario cannot be reproduced when upgrading the version.
|
||||||
datastore = "1.0.0"
|
datastore = "1.0.0"
|
||||||
constraintlayout = "2.1.4"
|
constraintlayout = "2.2.0"
|
||||||
constraintlayout_compose = "1.1.0"
|
constraintlayout_compose = "1.1.0"
|
||||||
lifecycle = "2.8.7"
|
lifecycle = "2.8.7"
|
||||||
activity = "1.9.3"
|
activity = "1.9.3"
|
||||||
|
|
@ -173,7 +173,7 @@ jsoup = "org.jsoup:jsoup:1.18.1"
|
||||||
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
|
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
|
||||||
molecule-runtime = "app.cash.molecule:molecule-runtime:2.0.0"
|
molecule-runtime = "app.cash.molecule:molecule-runtime:2.0.0"
|
||||||
timber = "com.jakewharton.timber:timber:5.0.1"
|
timber = "com.jakewharton.timber:timber:5.0.1"
|
||||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.58"
|
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.60"
|
||||||
matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
|
matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
|
||||||
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" }
|
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" }
|
||||||
sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }
|
sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }
|
||||||
|
|
@ -182,7 +182,7 @@ sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions",
|
||||||
sqlcipher = "net.zetetic:android-database-sqlcipher:4.5.4"
|
sqlcipher = "net.zetetic:android-database-sqlcipher:4.5.4"
|
||||||
sqlite = "androidx.sqlite:sqlite-ktx:2.4.0"
|
sqlite = "androidx.sqlite:sqlite-ktx:2.4.0"
|
||||||
unifiedpush = "com.github.UnifiedPush:android-connector:2.4.0"
|
unifiedpush = "com.github.UnifiedPush:android-connector:2.4.0"
|
||||||
otaliastudios_transcoder = "com.otaliastudios:transcoder:0.11.1"
|
otaliastudios_transcoder = "com.otaliastudios:transcoder:0.11.2"
|
||||||
vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0"
|
vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0"
|
||||||
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
|
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
|
||||||
telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "telephoto" }
|
telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "telephoto" }
|
||||||
|
|
@ -237,7 +237,7 @@ ktlint = "org.jlleitschuh.gradle.ktlint:12.1.1"
|
||||||
dependencygraph = "com.savvasdalkitsis.module-dependency-graph:0.12"
|
dependencygraph = "com.savvasdalkitsis.module-dependency-graph:0.12"
|
||||||
dependencycheck = "org.owasp.dependencycheck:10.0.4"
|
dependencycheck = "org.owasp.dependencycheck:10.0.4"
|
||||||
dependencyanalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyAnalysis" }
|
dependencyanalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyAnalysis" }
|
||||||
paparazzi = "app.cash.paparazzi:1.3.4"
|
paparazzi = "app.cash.paparazzi:1.3.5"
|
||||||
sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }
|
sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }
|
||||||
firebaseAppDistribution = { id = "com.google.firebase.appdistribution", version.ref = "firebaseAppDistribution" }
|
firebaseAppDistribution = { id = "com.google.firebase.appdistribution", version.ref = "firebaseAppDistribution" }
|
||||||
knit = { id = "org.jetbrains.kotlinx.knit", version = "0.5.0" }
|
knit = { id = "org.jetbrains.kotlinx.knit", version = "0.5.0" }
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,9 @@ class DefaultPinnedMessagesBannerFormatter @Inject constructor(
|
||||||
messageType.bestDescription.prefixWith(CommonStrings.common_audio)
|
messageType.bestDescription.prefixWith(CommonStrings.common_audio)
|
||||||
}
|
}
|
||||||
is VoiceMessageType -> {
|
is VoiceMessageType -> {
|
||||||
messageType.bestDescription.prefixWith(CommonStrings.common_voice_message)
|
// In this case, do not use bestDescription, because the filename is useless, only use the caption if available.
|
||||||
|
messageType.caption?.prefixWith(sp.getString(CommonStrings.common_voice_message))
|
||||||
|
?: sp.getString(CommonStrings.common_voice_message)
|
||||||
}
|
}
|
||||||
is OtherMessageType -> {
|
is OtherMessageType -> {
|
||||||
messageType.body
|
messageType.body
|
||||||
|
|
|
||||||
|
|
@ -110,25 +110,27 @@ class DefaultRoomLastMessageFormatter @Inject constructor(
|
||||||
messageType.toPlainText(permalinkParser)
|
messageType.toPlainText(permalinkParser)
|
||||||
}
|
}
|
||||||
is VideoMessageType -> {
|
is VideoMessageType -> {
|
||||||
sp.getString(CommonStrings.common_video)
|
messageType.bestDescription.prefixWith(sp.getString(CommonStrings.common_video))
|
||||||
}
|
}
|
||||||
is ImageMessageType -> {
|
is ImageMessageType -> {
|
||||||
sp.getString(CommonStrings.common_image)
|
messageType.bestDescription.prefixWith(sp.getString(CommonStrings.common_image))
|
||||||
}
|
}
|
||||||
is StickerMessageType -> {
|
is StickerMessageType -> {
|
||||||
sp.getString(CommonStrings.common_sticker)
|
messageType.bestDescription.prefixWith(sp.getString(CommonStrings.common_sticker))
|
||||||
}
|
}
|
||||||
is LocationMessageType -> {
|
is LocationMessageType -> {
|
||||||
sp.getString(CommonStrings.common_shared_location)
|
sp.getString(CommonStrings.common_shared_location)
|
||||||
}
|
}
|
||||||
is FileMessageType -> {
|
is FileMessageType -> {
|
||||||
sp.getString(CommonStrings.common_file)
|
messageType.bestDescription.prefixWith(sp.getString(CommonStrings.common_file))
|
||||||
}
|
}
|
||||||
is AudioMessageType -> {
|
is AudioMessageType -> {
|
||||||
sp.getString(CommonStrings.common_audio)
|
messageType.bestDescription.prefixWith(sp.getString(CommonStrings.common_audio))
|
||||||
}
|
}
|
||||||
is VoiceMessageType -> {
|
is VoiceMessageType -> {
|
||||||
sp.getString(CommonStrings.common_voice_message)
|
// In this case, do not use bestDescription, because the filename is useless, only use the caption if available.
|
||||||
|
messageType.caption?.prefixWith(sp.getString(CommonStrings.common_voice_message))
|
||||||
|
?: sp.getString(CommonStrings.common_voice_message)
|
||||||
}
|
}
|
||||||
is OtherMessageType -> {
|
is OtherMessageType -> {
|
||||||
messageType.body
|
messageType.body
|
||||||
|
|
@ -140,7 +142,7 @@ class DefaultRoomLastMessageFormatter @Inject constructor(
|
||||||
return message.prefixIfNeeded(senderDisambiguatedDisplayName, isDmRoom, isOutgoing)
|
return message.prefixIfNeeded(senderDisambiguatedDisplayName, isDmRoom, isOutgoing)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String.prefixIfNeeded(
|
private fun CharSequence.prefixIfNeeded(
|
||||||
senderDisambiguatedDisplayName: String,
|
senderDisambiguatedDisplayName: String,
|
||||||
isDmRoom: Boolean,
|
isDmRoom: Boolean,
|
||||||
isOutgoing: Boolean,
|
isOutgoing: Boolean,
|
||||||
|
|
|
||||||
|
|
@ -159,11 +159,11 @@ class DefaultPinnedMessagesBannerFormatterTest {
|
||||||
val expectedResult = when (type) {
|
val expectedResult = when (type) {
|
||||||
is VideoMessageType,
|
is VideoMessageType,
|
||||||
is AudioMessageType,
|
is AudioMessageType,
|
||||||
is VoiceMessageType,
|
|
||||||
is ImageMessageType,
|
is ImageMessageType,
|
||||||
is StickerMessageType,
|
is StickerMessageType,
|
||||||
is FileMessageType,
|
is FileMessageType,
|
||||||
is LocationMessageType -> AnnotatedString::class.java
|
is LocationMessageType -> AnnotatedString::class.java
|
||||||
|
is VoiceMessageType,
|
||||||
is EmoteMessageType,
|
is EmoteMessageType,
|
||||||
is TextMessageType,
|
is TextMessageType,
|
||||||
is NoticeMessageType,
|
is NoticeMessageType,
|
||||||
|
|
@ -176,7 +176,7 @@ class DefaultPinnedMessagesBannerFormatterTest {
|
||||||
val expectedResult = when (type) {
|
val expectedResult = when (type) {
|
||||||
is VideoMessageType -> "Video: Shared body"
|
is VideoMessageType -> "Video: Shared body"
|
||||||
is AudioMessageType -> "Audio: Shared body"
|
is AudioMessageType -> "Audio: Shared body"
|
||||||
is VoiceMessageType -> "Voice message: Shared body"
|
is VoiceMessageType -> "Voice message"
|
||||||
is ImageMessageType -> "Image: Shared body"
|
is ImageMessageType -> "Image: Shared body"
|
||||||
is StickerMessageType -> "Sticker: Shared body"
|
is StickerMessageType -> "Sticker: Shared body"
|
||||||
is FileMessageType -> "File: Shared body"
|
is FileMessageType -> "File: Shared body"
|
||||||
|
|
|
||||||
|
|
@ -208,32 +208,51 @@ class DefaultRoomLastMessageFormatterTest {
|
||||||
|
|
||||||
// Verify results of DM mode
|
// Verify results of DM mode
|
||||||
for ((type, result) in resultsInDm) {
|
for ((type, result) in resultsInDm) {
|
||||||
|
val string = result.toString()
|
||||||
val expectedResult = when (type) {
|
val expectedResult = when (type) {
|
||||||
is VideoMessageType -> "Video"
|
is VideoMessageType -> "Video: Shared body"
|
||||||
is AudioMessageType -> "Audio"
|
is AudioMessageType -> "Audio: Shared body"
|
||||||
is VoiceMessageType -> "Voice message"
|
is VoiceMessageType -> "Voice message"
|
||||||
is ImageMessageType -> "Image"
|
is ImageMessageType -> "Image: Shared body"
|
||||||
is StickerMessageType -> "Sticker"
|
is StickerMessageType -> "Sticker: Shared body"
|
||||||
is FileMessageType -> "File"
|
is FileMessageType -> "File: Shared body"
|
||||||
is LocationMessageType -> "Shared location"
|
is LocationMessageType -> "Shared location"
|
||||||
is EmoteMessageType -> "* $senderName ${type.body}"
|
is EmoteMessageType -> "* $senderName ${type.body}"
|
||||||
is TextMessageType,
|
is TextMessageType,
|
||||||
is NoticeMessageType,
|
is NoticeMessageType,
|
||||||
is OtherMessageType -> body
|
is OtherMessageType -> body
|
||||||
}
|
}
|
||||||
assertWithMessage("$type was not properly handled for DM").that(result).isEqualTo(expectedResult)
|
val shouldCreateAnnotatedString = when (type) {
|
||||||
|
is VideoMessageType -> true
|
||||||
|
is AudioMessageType -> true
|
||||||
|
is VoiceMessageType -> false
|
||||||
|
is ImageMessageType -> true
|
||||||
|
is StickerMessageType -> true
|
||||||
|
is FileMessageType -> true
|
||||||
|
is LocationMessageType -> false
|
||||||
|
is EmoteMessageType -> false
|
||||||
|
is TextMessageType -> false
|
||||||
|
is NoticeMessageType -> false
|
||||||
|
is OtherMessageType -> false
|
||||||
|
}
|
||||||
|
if (shouldCreateAnnotatedString) {
|
||||||
|
assertWithMessage("$type doesn't produce an AnnotatedString")
|
||||||
|
.that(result)
|
||||||
|
.isInstanceOf(AnnotatedString::class.java)
|
||||||
|
}
|
||||||
|
assertWithMessage("$type was not properly handled for DM").that(string).isEqualTo(expectedResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify results of Room mode
|
// Verify results of Room mode
|
||||||
for ((type, result) in resultsInRoom) {
|
for ((type, result) in resultsInRoom) {
|
||||||
val string = result.toString()
|
val string = result.toString()
|
||||||
val expectedResult = when (type) {
|
val expectedResult = when (type) {
|
||||||
is VideoMessageType -> "$expectedPrefix: Video"
|
is VideoMessageType -> "$expectedPrefix: Video: Shared body"
|
||||||
is AudioMessageType -> "$expectedPrefix: Audio"
|
is AudioMessageType -> "$expectedPrefix: Audio: Shared body"
|
||||||
is VoiceMessageType -> "$expectedPrefix: Voice message"
|
is VoiceMessageType -> "$expectedPrefix: Voice message"
|
||||||
is ImageMessageType -> "$expectedPrefix: Image"
|
is ImageMessageType -> "$expectedPrefix: Image: Shared body"
|
||||||
is StickerMessageType -> "$expectedPrefix: Sticker"
|
is StickerMessageType -> "$expectedPrefix: Sticker: Shared body"
|
||||||
is FileMessageType -> "$expectedPrefix: File"
|
is FileMessageType -> "$expectedPrefix: File: Shared body"
|
||||||
is LocationMessageType -> "$expectedPrefix: Shared location"
|
is LocationMessageType -> "$expectedPrefix: Shared location"
|
||||||
is TextMessageType,
|
is TextMessageType,
|
||||||
is NoticeMessageType,
|
is NoticeMessageType,
|
||||||
|
|
@ -249,7 +268,8 @@ class DefaultRoomLastMessageFormatterTest {
|
||||||
is FileMessageType -> true
|
is FileMessageType -> true
|
||||||
is LocationMessageType -> false
|
is LocationMessageType -> false
|
||||||
is EmoteMessageType -> false
|
is EmoteMessageType -> false
|
||||||
is TextMessageType, is NoticeMessageType -> true
|
is TextMessageType -> true
|
||||||
|
is NoticeMessageType -> true
|
||||||
is OtherMessageType -> true
|
is OtherMessageType -> true
|
||||||
}
|
}
|
||||||
if (shouldCreateAnnotatedString) {
|
if (shouldCreateAnnotatedString) {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ package io.element.android.libraries.featureflag.api
|
||||||
|
|
||||||
import io.element.android.appconfig.OnBoardingConfig
|
import io.element.android.appconfig.OnBoardingConfig
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
|
import io.element.android.libraries.core.meta.BuildType
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To enable or disable a FeatureFlags, change the `defaultValue` value.
|
* To enable or disable a FeatureFlags, change the `defaultValue` value.
|
||||||
|
|
@ -124,5 +125,19 @@ enum class FeatureFlags(
|
||||||
" You'll have to stop and re-open the app manually for that setting to take effect.",
|
" You'll have to stop and re-open the app manually for that setting to take effect.",
|
||||||
defaultValue = { false },
|
defaultValue = { false },
|
||||||
isFinished = false,
|
isFinished = false,
|
||||||
)
|
),
|
||||||
|
Knock(
|
||||||
|
key = "feature.knock",
|
||||||
|
title = "Ask to join",
|
||||||
|
description = "Allow creating rooms which users can request access to.",
|
||||||
|
defaultValue = { false },
|
||||||
|
isFinished = false,
|
||||||
|
),
|
||||||
|
MediaUploadOnSendQueue(
|
||||||
|
key = "feature.media_upload_through_send_queue",
|
||||||
|
title = "Media upload through send queue",
|
||||||
|
description = "Experimental support for treating media uploads as regular events, with an improved retry and cancellation implementation.",
|
||||||
|
defaultValue = { buildMeta -> buildMeta.buildType != BuildType.RELEASE },
|
||||||
|
isFinished = false,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,14 @@ interface MatrixClient : Closeable {
|
||||||
|
|
||||||
suspend fun trackRecentlyVisitedRoom(roomId: RoomId): Result<Unit>
|
suspend fun trackRecentlyVisitedRoom(roomId: RoomId): Result<Unit>
|
||||||
suspend fun getRecentlyVisitedRooms(): Result<List<RoomId>>
|
suspend fun getRecentlyVisitedRooms(): Result<List<RoomId>>
|
||||||
suspend fun resolveRoomAlias(roomAlias: RoomAlias): Result<ResolvedRoomAlias>
|
|
||||||
|
/**
|
||||||
|
* Resolves the given room alias to a roomID (and a list of servers), if possible.
|
||||||
|
* @param roomAlias the room alias to resolve
|
||||||
|
* @return the resolved room alias if any, an empty result if not found,or an error if the resolution failed.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
suspend fun resolveRoomAlias(roomAlias: RoomAlias): Result<Optional<ResolvedRoomAlias>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enables or disables the sending queue, according to the given parameter.
|
* Enables or disables the sending queue, according to the given parameter.
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
package io.element.android.libraries.matrix.api.createroom
|
package io.element.android.libraries.matrix.api.createroom
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import java.util.Optional
|
||||||
|
|
||||||
data class CreateRoomParameters(
|
data class CreateRoomParameters(
|
||||||
val name: String?,
|
val name: String?,
|
||||||
|
|
@ -18,4 +19,6 @@ data class CreateRoomParameters(
|
||||||
val preset: RoomPreset,
|
val preset: RoomPreset,
|
||||||
val invite: List<UserId>? = null,
|
val invite: List<UserId>? = null,
|
||||||
val avatar: String? = null,
|
val avatar: String? = null,
|
||||||
|
val joinRuleOverride: JoinRuleOverride = JoinRuleOverride.None,
|
||||||
|
val canonicalAlias: Optional<String> = Optional.empty(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.matrix.api.createroom
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rules to override the default room join rules.
|
||||||
|
*/
|
||||||
|
sealed interface JoinRuleOverride {
|
||||||
|
data object Knock : JoinRuleOverride
|
||||||
|
data object None : JoinRuleOverride
|
||||||
|
}
|
||||||
|
|
@ -132,8 +132,8 @@ interface MatrixRoom : Closeable {
|
||||||
file: File,
|
file: File,
|
||||||
thumbnailFile: File?,
|
thumbnailFile: File?,
|
||||||
imageInfo: ImageInfo,
|
imageInfo: ImageInfo,
|
||||||
body: String?,
|
caption: String?,
|
||||||
formattedBody: String?,
|
formattedCaption: String?,
|
||||||
progressCallback: ProgressCallback?
|
progressCallback: ProgressCallback?
|
||||||
): Result<MediaUploadHandler>
|
): Result<MediaUploadHandler>
|
||||||
|
|
||||||
|
|
@ -141,8 +141,8 @@ interface MatrixRoom : Closeable {
|
||||||
file: File,
|
file: File,
|
||||||
thumbnailFile: File?,
|
thumbnailFile: File?,
|
||||||
videoInfo: VideoInfo,
|
videoInfo: VideoInfo,
|
||||||
body: String?,
|
caption: String?,
|
||||||
formattedBody: String?,
|
formattedCaption: String?,
|
||||||
progressCallback: ProgressCallback?
|
progressCallback: ProgressCallback?
|
||||||
): Result<MediaUploadHandler>
|
): Result<MediaUploadHandler>
|
||||||
|
|
||||||
|
|
|
||||||
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