Merge branch 'release/26.01.2' into main
This commit is contained in:
commit
790f5cd6b7
532 changed files with 6311 additions and 3920 deletions
2
.github/workflows/sync-localazy.yml
vendored
2
.github/workflows/sync-localazy.yml
vendored
|
|
@ -36,7 +36,7 @@ jobs:
|
|||
./tools/localazy/importSupportedLocalesFromLocalazy.py
|
||||
./tools/test/generateAllScreenshots.py
|
||||
- name: Create Pull Request for Strings
|
||||
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
||||
with:
|
||||
token: ${{ secrets.DANGER_GITHUB_API_TOKEN }}
|
||||
commit-message: Sync Strings from Localazy
|
||||
|
|
|
|||
2
.github/workflows/sync-sas-strings.yml
vendored
2
.github/workflows/sync-sas-strings.yml
vendored
|
|
@ -23,7 +23,7 @@ jobs:
|
|||
- name: Run SAS String script
|
||||
run: ./tools/sas/import_sas_strings.py
|
||||
- name: Create Pull Request for SAS Strings
|
||||
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
||||
with:
|
||||
commit-message: Sync SAS Strings
|
||||
title: Sync SAS Strings
|
||||
|
|
|
|||
23
CHANGES.md
23
CHANGES.md
|
|
@ -1,3 +1,26 @@
|
|||
Changes in Element X v26.01.1
|
||||
=============================
|
||||
|
||||
<!-- Release notes generated using configuration in .github/release.yml at v26.01.1 -->
|
||||
|
||||
## What's Changed
|
||||
### 🐛 Bugfixes
|
||||
* Ensure that log files are not too big. by @bmarty in https://github.com/element-hq/element-x-android/pull/6003
|
||||
* Make the number view scrollable by @bmarty in https://github.com/element-hq/element-x-android/pull/6017
|
||||
* Ensure that room with long names are rendered correctly in the room list. by @bmarty in https://github.com/element-hq/element-x-android/pull/6019
|
||||
* Create `AppMigration09` to remove the cached `well-known` config from the SDK by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6026
|
||||
### 🚧 In development 🚧
|
||||
* [POC] Signin with Element Classic by @bmarty in https://github.com/element-hq/element-x-android/pull/6013
|
||||
* Space : manage rooms by @ganfra in https://github.com/element-hq/element-x-android/pull/6022
|
||||
### Dependency upgrades
|
||||
* fix(deps): update dependency androidx.compose:compose-bom to v2026 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6010
|
||||
* fix(deps): update dependency io.sentry:sentry-android to v8.30.0 - autoclosed by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6014
|
||||
* fix(deps): update dependency com.google.firebase:firebase-bom to v34.8.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6018
|
||||
* Upgrade androidx.biometric:biometric-ktx to 1.4.0-alpha02 by @bmarty in https://github.com/element-hq/element-x-android/pull/6020
|
||||
|
||||
|
||||
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.01.0...v26.01.1
|
||||
|
||||
Changes in Element X v26.01.0
|
||||
=============================
|
||||
|
||||
|
|
|
|||
|
|
@ -132,8 +132,23 @@ android {
|
|||
optimization {
|
||||
enable = true
|
||||
keepRules {
|
||||
files.add(File(projectDir, "proguard-rules.pro"))
|
||||
files.add(File(projectDir, "common-proguard-rules.pro"))
|
||||
files.add(getDefaultProguardFile("proguard-android-optimize.txt"))
|
||||
|
||||
// Depending on whether the app flavor is enterprise or not we want to use different proguard rules.
|
||||
val flavorProguardFile = if (isEnterpriseBuild) {
|
||||
// Custom rules for enterprise builds
|
||||
File(projectDir, "enterprise-proguard-rules.pro")
|
||||
} else {
|
||||
// These default rules prevent the OSS app from being obfuscated
|
||||
File(projectDir, "default-proguard-rules.pro")
|
||||
}
|
||||
|
||||
if (flavorProguardFile.exists()) {
|
||||
files.add(flavorProguardFile)
|
||||
} else {
|
||||
logger.warn("Proguard file ${flavorProguardFile.absolutePath} does not exist")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@
|
|||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Increase optimizations passes to 3
|
||||
-optimizationpasses 3
|
||||
|
||||
# JNA
|
||||
-dontwarn java.awt.*
|
||||
-keep class com.sun.jna.** { *; }
|
||||
|
|
@ -41,7 +44,6 @@
|
|||
static int windowAttachCount(android.view.View);
|
||||
}
|
||||
|
||||
|
||||
# Keep LogSessionId class and related classes (https://github.com/androidx/media/issues/2535)
|
||||
-keep class android.media.metrics.LogSessionId { *; }
|
||||
-keep class android.media.metrics.** { *; }
|
||||
|
|
@ -66,10 +68,9 @@
|
|||
-dontwarn androidx.window.sidecar.SidecarWindowLayoutInfo
|
||||
|
||||
# Also needed after AGP 8.13.1 upgrade, it seems like proguard is now more aggressive on removing unused code
|
||||
-keep,allowshrinking class org.matrix.rustcomponents.sdk.** { *;}
|
||||
-keep,allowshrinking class uniffi.** { *;}
|
||||
-keep,allowshrinking class io.element.android.x.di.** { *; }
|
||||
-keepclasseswithmembernames,allowoptimization,allowshrinking class io.element.android.** { *; }
|
||||
-keep,allowoptimization,allowshrinking class org.matrix.rustcomponents.sdk.** { *;}
|
||||
-keep,allowoptimization,allowshrinking class uniffi.** { *;}
|
||||
-keep,allowoptimization,allowshrinking class io.element.android.x.di.** { *; }
|
||||
|
||||
# Keep Metro classes
|
||||
-keep,allowshrinking class dev.zacsweers.metro.** { *; }
|
||||
-keep,allowoptimization,allowshrinking class dev.zacsweers.metro.** { *; }
|
||||
2
app/default-proguard-rules.pro
Normal file
2
app/default-proguard-rules.pro
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# Don't obfuscate anything for non-enterprise builds
|
||||
-dontobfuscate
|
||||
|
|
@ -30,4 +30,9 @@ object RageshakeConfig {
|
|||
* The maximum size of a single log file.
|
||||
*/
|
||||
const val MAX_LOG_CONTENT_SIZE = 100 * 1024 * 1024L
|
||||
|
||||
/**
|
||||
* The maximum number of log lines a rageshake can contain.
|
||||
*/
|
||||
const val MAX_LOG_LINES_SIZE = 1_000_000
|
||||
}
|
||||
|
|
|
|||
|
|
@ -484,7 +484,10 @@ class LoggedInFlowNode(
|
|||
backstack.replace(NavTarget.Room(roomIdOrAlias = RoomIdOrAlias.Id(roomId), serverNames = emptyList()))
|
||||
}
|
||||
}
|
||||
createRoomEntryPoint.createNode(isSpace = true, parentNode = this, buildContext = buildContext, callback = callback)
|
||||
createRoomEntryPoint
|
||||
.builder(parentNode = this, buildContext = buildContext, callback = callback)
|
||||
.setIsSpace(true)
|
||||
.build()
|
||||
}
|
||||
is NavTarget.SecureBackup -> {
|
||||
secureBackupEntryPoint.createNode(
|
||||
|
|
|
|||
|
|
@ -214,6 +214,10 @@ class RoomFlowNode(
|
|||
)
|
||||
}
|
||||
is NavTarget.JoinRoom -> {
|
||||
// Clear analytics transactions for opening a joined room, since we're display a non-joined one
|
||||
analyticsService.removeLongRunningTransaction(LoadJoinedRoomFlow)
|
||||
analyticsService.removeLongRunningTransaction(OpenRoom)
|
||||
|
||||
val inputs = JoinRoomEntryPoint.Inputs(
|
||||
roomId = navTarget.roomId,
|
||||
roomIdOrAlias = inputs.roomIdOrAlias,
|
||||
|
|
|
|||
|
|
@ -41,6 +41,10 @@ import io.element.android.libraries.matrix.api.core.ThreadId
|
|||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.ui.room.LoadingRoomState
|
||||
import io.element.android.libraries.matrix.ui.room.LoadingRoomStateFlowFactory
|
||||
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.LoadJoinedRoomFlow
|
||||
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.NotificationToMessage
|
||||
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.OpenRoom
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
|
@ -53,6 +57,7 @@ class JoinedRoomFlowNode(
|
|||
@Assisted val buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
loadingRoomStateFlowFactory: LoadingRoomStateFlowFactory,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) :
|
||||
BaseFlowNode<JoinedRoomFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
|
|
@ -81,6 +86,11 @@ class JoinedRoomFlowNode(
|
|||
|
||||
override fun onBuilt() {
|
||||
super.onBuilt()
|
||||
|
||||
val parentTransaction = analyticsService.getLongRunningTransaction(NotificationToMessage)
|
||||
val openRoomTransaction = analyticsService.startLongRunningTransaction(OpenRoom, parentTransaction)
|
||||
analyticsService.startLongRunningTransaction(LoadJoinedRoomFlow, openRoomTransaction)
|
||||
|
||||
loadingRoomStateStateFlow
|
||||
.map {
|
||||
it is LoadingRoomState.Loaded
|
||||
|
|
|
|||
|
|
@ -48,8 +48,6 @@ import io.element.android.libraries.matrix.api.core.UserId
|
|||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.LoadJoinedRoomFlow
|
||||
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.LoadMessagesUi
|
||||
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.OpenRoom
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analytics.api.finishLongRunningTransaction
|
||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||
|
|
@ -106,8 +104,6 @@ class JoinedRoomLoadedFlowNode(
|
|||
init {
|
||||
lifecycle.subscribe(
|
||||
onCreate = {
|
||||
val parent = analyticsService.getLongRunningTransaction(OpenRoom)
|
||||
analyticsService.startLongRunningTransaction(LoadMessagesUi, parent)
|
||||
Timber.v("OnCreate => ${inputs.room.roomId}")
|
||||
appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId)
|
||||
activeRoomsHolder.addRoom(inputs.room)
|
||||
|
|
|
|||
|
|
@ -15,9 +15,9 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import io.element.android.features.rageshake.api.crash.CrashDetectionEvents
|
||||
import io.element.android.features.rageshake.api.crash.CrashDetectionEvent
|
||||
import io.element.android.features.rageshake.api.crash.CrashDetectionView
|
||||
import io.element.android.features.rageshake.api.detection.RageshakeDetectionEvents
|
||||
import io.element.android.features.rageshake.api.detection.RageshakeDetectionEvent
|
||||
import io.element.android.features.rageshake.api.detection.RageshakeDetectionView
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
|
|
@ -39,8 +39,8 @@ fun RootView(
|
|||
children()
|
||||
|
||||
fun onOpenBugReport() {
|
||||
state.crashDetectionState.eventSink(CrashDetectionEvents.ResetAppHasCrashed)
|
||||
state.rageshakeDetectionState.eventSink(RageshakeDetectionEvents.Dismiss)
|
||||
state.crashDetectionState.eventSink(CrashDetectionEvent.ResetAppHasCrashed)
|
||||
state.rageshakeDetectionState.eventSink(RageshakeDetectionEvent.Dismiss)
|
||||
onOpenBugReport.invoke()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ allprojects {
|
|||
config.from(files("$rootDir/tools/detekt/detekt.yml"))
|
||||
}
|
||||
dependencies {
|
||||
detektPlugins("io.nlopez.compose.rules:detekt:0.5.3")
|
||||
detektPlugins("io.nlopez.compose.rules:detekt:0.5.6")
|
||||
detektPlugins(project(":tests:detekt-rules"))
|
||||
}
|
||||
|
||||
|
|
|
|||
2
fastlane/metadata/android/en-US/changelogs/202601020.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/202601020.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
Main changes in this version: bug fixes and improvements.
|
||||
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_space_announcement_item1">"Просмотр пространств, которые вы создали или к которым присоединились"</string>
|
||||
<string name="screen_space_announcement_item1">"Просмотреть пространства, которые вы создали или к которым присоединились"</string>
|
||||
<string name="screen_space_announcement_item2">"Принимать или отклонять приглашения в пространства"</string>
|
||||
<string name="screen_space_announcement_item3">"Откройте для себя все комнаты, к которым вы можете присоединиться в своих пространствах."</string>
|
||||
<string name="screen_space_announcement_item4">"Присоединиться к публичному пространству"</string>
|
||||
<string name="screen_space_announcement_item3">"Найти все комнаты, к которым можно присоединиться в ваших пространствах"</string>
|
||||
<string name="screen_space_announcement_item4">"Присоединяться к публичным пространствам"</string>
|
||||
<string name="screen_space_announcement_item5">"Покинуть все пространства, к которым вы присоединились"</string>
|
||||
<string name="screen_space_announcement_notice">"Работа с пространствами скоро станет доступна"</string>
|
||||
<string name="screen_space_announcement_subtitle">"Добро пожаловать в бета-версию Spaces! В этой первой версии вы сможете:"</string>
|
||||
<string name="screen_space_announcement_subtitle">"Добро пожаловать в бета-версию пространств! В этой первой версии вы сможете:"</string>
|
||||
<string name="screen_space_announcement_title">"Представляем пространства"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -15,12 +15,13 @@ import io.element.android.libraries.architecture.FeatureEntryPoint
|
|||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
interface CreateRoomEntryPoint : FeatureEntryPoint {
|
||||
fun createNode(
|
||||
isSpace: Boolean,
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
callback: Callback,
|
||||
): Node
|
||||
interface Builder {
|
||||
fun setIsSpace(isSpace: Boolean): Builder
|
||||
fun setParentSpace(parentSpaceId: RoomId): Builder
|
||||
fun build(): Node
|
||||
}
|
||||
|
||||
fun builder(parentNode: Node, buildContext: BuildContext, callback: Callback): Builder
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun onRoomCreated(roomId: RoomId)
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ dependencies {
|
|||
implementation(projects.libraries.mediapickers.api)
|
||||
implementation(projects.libraries.mediaupload.api)
|
||||
implementation(projects.libraries.permissions.api)
|
||||
implementation(projects.libraries.previewutils)
|
||||
implementation(projects.libraries.usersearch.impl)
|
||||
implementation(projects.services.analytics.api)
|
||||
implementation(libs.coil.compose)
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class CreateRoomFlowNode(
|
|||
@Assisted plugins: List<Plugin>,
|
||||
) : BaseFlowNode<CreateRoomFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.ConfigureRoom(isSpace = plugins.filterIsInstance<Inputs>().first().isSpace),
|
||||
initialElement = initialElementFromInputs(plugins.filterIsInstance<Inputs>().first()),
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
|
|
@ -46,7 +46,8 @@ class CreateRoomFlowNode(
|
|||
) {
|
||||
@Parcelize
|
||||
data class Inputs(
|
||||
val isSpace: Boolean
|
||||
val isSpace: Boolean,
|
||||
val parentSpaceId: RoomId?,
|
||||
) : NodeInputs, Parcelable
|
||||
|
||||
private val callback: CreateRoomEntryPoint.Callback = callback()
|
||||
|
|
@ -54,7 +55,7 @@ class CreateRoomFlowNode(
|
|||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
is NavTarget.ConfigureRoom -> {
|
||||
val inputs = ConfigureRoomNode.Inputs(isSpace = navTarget.isSpace)
|
||||
val inputs = ConfigureRoomNode.Inputs(isSpace = navTarget.isSpace, parentSpaceId = navTarget.parentSpaceId)
|
||||
val callback = object : ConfigureRoomNode.Callback {
|
||||
override fun onCreateRoomSuccess(roomId: RoomId) {
|
||||
backstack.replace(NavTarget.AddPeople(roomId))
|
||||
|
|
@ -81,9 +82,14 @@ class CreateRoomFlowNode(
|
|||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
data class ConfigureRoom(val isSpace: Boolean) : NavTarget
|
||||
data class ConfigureRoom(val isSpace: Boolean, val parentSpaceId: RoomId?) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class AddPeople(val roomId: RoomId) : NavTarget
|
||||
}
|
||||
}
|
||||
|
||||
private fun initialElementFromInputs(inputs: CreateRoomFlowNode.Inputs) = CreateRoomFlowNode.NavTarget.ConfigureRoom(
|
||||
isSpace = inputs.isSpace,
|
||||
parentSpaceId = inputs.parentSpaceId,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -14,16 +14,35 @@ import dev.zacsweers.metro.ContributesBinding
|
|||
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class DefaultCreateRoomEntryPoint : CreateRoomEntryPoint {
|
||||
override fun createNode(
|
||||
isSpace: Boolean,
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
callback: CreateRoomEntryPoint.Callback,
|
||||
): Node {
|
||||
val inputs = CreateRoomFlowNode.Inputs(isSpace)
|
||||
return parentNode.createNode<CreateRoomFlowNode>(buildContext, listOf(inputs, callback))
|
||||
class Builder(
|
||||
private val parentNode: Node,
|
||||
private val buildContext: BuildContext,
|
||||
private val callback: CreateRoomEntryPoint.Callback,
|
||||
) : CreateRoomEntryPoint.Builder {
|
||||
private var isSpace = false
|
||||
private var parentSpaceId: RoomId? = null
|
||||
|
||||
override fun setIsSpace(isSpace: Boolean): Builder {
|
||||
this.isSpace = isSpace
|
||||
return this
|
||||
}
|
||||
|
||||
override fun setParentSpace(parentSpaceId: RoomId): Builder {
|
||||
this.parentSpaceId = parentSpaceId
|
||||
return this
|
||||
}
|
||||
|
||||
override fun build(): Node {
|
||||
val inputs = CreateRoomFlowNode.Inputs(isSpace = isSpace, parentSpaceId = parentSpaceId)
|
||||
return parentNode.createNode<CreateRoomFlowNode>(buildContext, listOf(inputs, callback))
|
||||
}
|
||||
}
|
||||
|
||||
override fun builder(parentNode: Node, buildContext: BuildContext, callback: CreateRoomEntryPoint.Callback): CreateRoomEntryPoint.Builder {
|
||||
return Builder(parentNode, buildContext, callback)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,15 +8,16 @@
|
|||
|
||||
package io.element.android.features.createroom.impl.configureroom
|
||||
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
||||
|
||||
sealed interface ConfigureRoomEvents {
|
||||
data class RoomNameChanged(val name: String) : ConfigureRoomEvents
|
||||
data class TopicChanged(val topic: String) : ConfigureRoomEvents
|
||||
data class RoomVisibilityChanged(val visibilityItem: RoomVisibilityItem) : ConfigureRoomEvents
|
||||
data class RoomAccessChanged(val roomAccess: RoomAccessItem) : ConfigureRoomEvents
|
||||
data class JoinRuleChanged(val joinRuleItem: JoinRuleItem) : ConfigureRoomEvents
|
||||
data class RoomAddressChanged(val roomAddress: String) : ConfigureRoomEvents
|
||||
data object CreateRoom : ConfigureRoomEvents
|
||||
data class HandleAvatarAction(val action: AvatarAction) : ConfigureRoomEvents
|
||||
data class SetParentSpace(val space: SpaceRoom?) : ConfigureRoomEvents
|
||||
data object CancelCreateRoom : ConfigureRoomEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,11 +42,12 @@ class ConfigureRoomNode(
|
|||
@Parcelize
|
||||
data class Inputs(
|
||||
val isSpace: Boolean,
|
||||
val parentSpaceId: RoomId?,
|
||||
) : NodeInputs, Parcelable
|
||||
|
||||
private val inputs = inputs<Inputs>()
|
||||
|
||||
private val presenter = presenterFactory.create(inputs.isSpace)
|
||||
private val presenter = presenterFactory.create(inputs.isSpace, inputs.parentSpaceId)
|
||||
|
||||
init {
|
||||
lifecycle.subscribe(
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.core.net.toUri
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
|
|
@ -35,7 +36,9 @@ import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
|
|||
import io.element.android.libraries.matrix.api.createroom.RoomPreset
|
||||
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
|
||||
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
||||
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity
|
||||
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidityEffect
|
||||
|
|
@ -45,15 +48,22 @@ import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
|||
import io.element.android.libraries.permissions.api.PermissionsEvent
|
||||
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import timber.log.Timber
|
||||
import kotlin.jvm.optionals.getOrDefault
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@AssistedInject
|
||||
class ConfigureRoomPresenter(
|
||||
@Assisted private val isSpace: Boolean,
|
||||
@Assisted private val initialParentSpaceId: RoomId?,
|
||||
private val dataStore: CreateRoomConfigStore,
|
||||
private val matrixClient: MatrixClient,
|
||||
private val mediaPickerProvider: PickerProvider,
|
||||
|
|
@ -66,7 +76,7 @@ class ConfigureRoomPresenter(
|
|||
) : Presenter<ConfigureRoomState> {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(isSpace: Boolean): ConfigureRoomPresenter
|
||||
fun create(isSpace: Boolean, parentSpaceId: RoomId?): ConfigureRoomPresenter
|
||||
}
|
||||
|
||||
private val cameraPermissionPresenter: PermissionsPresenter = permissionsPresenterFactory.create(android.Manifest.permission.CAMERA)
|
||||
|
|
@ -78,6 +88,7 @@ class ConfigureRoomPresenter(
|
|||
|
||||
@Composable
|
||||
override fun present(): ConfigureRoomState {
|
||||
val canAddRoomToSpace by featureFlagService.isFeatureEnabledFlow(FeatureFlags.CreateSpaces).collectAsState(false)
|
||||
val cameraPermissionState = cameraPermissionPresenter.present()
|
||||
val createRoomConfig by dataStore.getCreateRoomConfigFlow().collectAsState()
|
||||
val homeserverName = remember { matrixClient.userIdServerName() }
|
||||
|
|
@ -105,6 +116,18 @@ class ConfigureRoomPresenter(
|
|||
}
|
||||
}
|
||||
|
||||
var spaces by remember { mutableStateOf<ImmutableList<SpaceRoom>>(persistentListOf()) }
|
||||
LaunchedEffect(canAddRoomToSpace) {
|
||||
spaces = if (canAddRoomToSpace) {
|
||||
matrixClient.spaceService.editableSpaces().getOrElse { emptyList() }.toImmutableList()
|
||||
} else {
|
||||
persistentListOf()
|
||||
}
|
||||
|
||||
val parentSpace = spaces.find { it.roomId == initialParentSpaceId }
|
||||
parentSpace?.let { dataStore.setParentSpace(it) }
|
||||
}
|
||||
|
||||
LaunchedEffect(cameraPermissionState.permissionGranted) {
|
||||
if (cameraPermissionState.permissionGranted && pendingPermissionRequest) {
|
||||
pendingPermissionRequest = false
|
||||
|
|
@ -115,7 +138,7 @@ class ConfigureRoomPresenter(
|
|||
RoomAddressValidityEffect(
|
||||
client = matrixClient,
|
||||
roomAliasHelper = roomAliasHelper,
|
||||
newRoomAddress = createRoomConfig.roomVisibility.roomAddress().getOrDefault(""),
|
||||
newRoomAddress = createRoomConfig.visibilityState.roomAddress().getOrDefault(""),
|
||||
knownRoomAddress = null,
|
||||
) { newRoomAddressValidity ->
|
||||
roomAddressValidity.value = newRoomAddressValidity
|
||||
|
|
@ -124,6 +147,27 @@ class ConfigureRoomPresenter(
|
|||
val localCoroutineScope = rememberCoroutineScope()
|
||||
val createRoomAction: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||
|
||||
// Calculate available join rules based:
|
||||
// 1. If we are creating a space.
|
||||
// 2. If it has a parent space.
|
||||
// 3. If knocking is enabled.
|
||||
val parentSpace = createRoomConfig.parentSpace
|
||||
val availableJoinRules = remember(createRoomConfig.parentSpace, isSpace, isKnockFeatureEnabled) {
|
||||
when {
|
||||
isSpace && parentSpace != null -> TODO("Adding a space to a parent space is not supported yet! How did you get here?")
|
||||
parentSpace == null || parentSpace.joinRule == JoinRule.Public -> listOfNotNull(
|
||||
JoinRuleItem.PublicVisibility.Public,
|
||||
JoinRuleItem.PublicVisibility.AskToJoin.takeIf { !isSpace && isKnockFeatureEnabled },
|
||||
JoinRuleItem.Private,
|
||||
).toImmutableList()
|
||||
else -> listOfNotNull(
|
||||
JoinRuleItem.PublicVisibility.Restricted(parentSpace.roomId),
|
||||
JoinRuleItem.PublicVisibility.AskToJoinRestricted(parentSpace.roomId).takeIf { !isSpace && isKnockFeatureEnabled },
|
||||
JoinRuleItem.Private,
|
||||
).toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
fun createRoom(config: CreateRoomConfig) {
|
||||
createRoomAction.value = AsyncAction.Uninitialized
|
||||
localCoroutineScope.createRoom(config, createRoomAction)
|
||||
|
|
@ -133,8 +177,7 @@ class ConfigureRoomPresenter(
|
|||
when (event) {
|
||||
is ConfigureRoomEvents.RoomNameChanged -> dataStore.setRoomName(event.name)
|
||||
is ConfigureRoomEvents.TopicChanged -> dataStore.setTopic(event.topic)
|
||||
is ConfigureRoomEvents.RoomVisibilityChanged -> dataStore.setRoomVisibility(event.visibilityItem)
|
||||
is ConfigureRoomEvents.RoomAccessChanged -> dataStore.setRoomAccess(event.roomAccess)
|
||||
is ConfigureRoomEvents.JoinRuleChanged -> dataStore.setJoinRule(event.joinRuleItem)
|
||||
is ConfigureRoomEvents.RoomAddressChanged -> dataStore.setRoomAddress(event.roomAddress)
|
||||
is ConfigureRoomEvents.CreateRoom -> createRoom(createRoomConfig)
|
||||
is ConfigureRoomEvents.HandleAvatarAction -> {
|
||||
|
|
@ -149,19 +192,24 @@ class ConfigureRoomPresenter(
|
|||
AvatarAction.Remove -> dataStore.setAvatarUri(uri = null)
|
||||
}
|
||||
}
|
||||
|
||||
ConfigureRoomEvents.CancelCreateRoom -> createRoomAction.value = AsyncAction.Uninitialized
|
||||
is ConfigureRoomEvents.SetParentSpace -> {
|
||||
dataStore.setParentSpace(event.space)
|
||||
}
|
||||
ConfigureRoomEvents.CancelCreateRoom -> {
|
||||
createRoomAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ConfigureRoomState(
|
||||
isKnockFeatureEnabled = isKnockFeatureEnabled,
|
||||
config = createRoomConfig,
|
||||
avatarActions = avatarActions,
|
||||
createRoomAction = createRoomAction.value,
|
||||
cameraPermissionState = cameraPermissionState,
|
||||
homeserverName = homeserverName,
|
||||
roomAddressValidity = roomAddressValidity.value,
|
||||
availableJoinRules = availableJoinRules,
|
||||
spaces = spaces,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
|
|
@ -172,25 +220,27 @@ class ConfigureRoomPresenter(
|
|||
) = launch {
|
||||
suspend {
|
||||
val avatarUrl = config.avatarUri?.let { uploadAvatar(it.toUri()) }
|
||||
val params = if (config.roomVisibility is RoomVisibilityState.Public) {
|
||||
val params = if (config.visibilityState is RoomVisibilityState.Public) {
|
||||
CreateRoomParameters(
|
||||
name = config.roomName,
|
||||
topic = config.topic,
|
||||
isEncrypted = false,
|
||||
isDirect = false,
|
||||
visibility = RoomVisibility.Public,
|
||||
joinRuleOverride = config.roomVisibility.roomAccess.toJoinRule(),
|
||||
joinRuleOverride = config.visibilityState.joinRuleItem.toJoinRule()
|
||||
// No need to specify the public join rule override, since the preset is already PUBLIC_CHAT
|
||||
.takeIf { it != JoinRule.Public },
|
||||
preset = RoomPreset.PUBLIC_CHAT,
|
||||
invite = config.invites.map { it.userId },
|
||||
avatar = avatarUrl,
|
||||
roomAliasName = config.roomVisibility.roomAddress(),
|
||||
roomAliasName = config.visibilityState.roomAddress(),
|
||||
isSpace = isSpace,
|
||||
)
|
||||
} else {
|
||||
CreateRoomParameters(
|
||||
name = config.roomName,
|
||||
topic = config.topic,
|
||||
isEncrypted = config.roomVisibility is RoomVisibilityState.Private,
|
||||
isEncrypted = config.visibilityState is RoomVisibilityState.Private,
|
||||
isDirect = false,
|
||||
visibility = RoomVisibility.Private,
|
||||
historyVisibilityOverride = RoomHistoryVisibility.Invited,
|
||||
|
|
@ -200,7 +250,7 @@ class ConfigureRoomPresenter(
|
|||
isSpace = isSpace,
|
||||
)
|
||||
}
|
||||
matrixClient.createRoom(params)
|
||||
val roomId = matrixClient.createRoom(params)
|
||||
.onFailure { failure ->
|
||||
Timber.e(failure, "Failed to create room")
|
||||
}
|
||||
|
|
@ -209,7 +259,22 @@ class ConfigureRoomPresenter(
|
|||
analyticsService.capture(CreatedRoom(isDM = false))
|
||||
}
|
||||
.getOrThrow()
|
||||
|
||||
// Add the newly created room to the parent space too
|
||||
if (config.parentSpace != null) {
|
||||
Timber.d("Adding room $roomId to parent space ${config.parentSpace.roomId}")
|
||||
// Wait until we receive the power level info for the room, as it's needed to check if it can be added to a space
|
||||
// TODO create some SDK function that does this instead?
|
||||
withTimeoutOrNull(30.seconds) {
|
||||
matrixClient.getRoomInfoFlow(roomId).first { it.getOrNull()?.roomPowerLevels != null }
|
||||
} ?: error("Did not receive created room power levels for room $roomId, needed for adding it to a space")
|
||||
|
||||
matrixClient.spaceService.addChildToSpace(spaceId = config.parentSpace.roomId, childId = roomId).getOrThrow()
|
||||
}
|
||||
|
||||
roomId
|
||||
}.runCatchingUpdatingState(createRoomAction)
|
||||
.onFailure { Timber.e(it, "Could not create room or add it to parent space ${config.parentSpace?.roomId}") }
|
||||
}
|
||||
|
||||
private suspend fun uploadAvatar(avatarUri: Uri): String {
|
||||
|
|
|
|||
|
|
@ -10,21 +10,23 @@ package io.element.android.features.createroom.impl.configureroom
|
|||
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
||||
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity
|
||||
import io.element.android.libraries.permissions.api.PermissionsState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class ConfigureRoomState(
|
||||
val isKnockFeatureEnabled: Boolean,
|
||||
val config: CreateRoomConfig,
|
||||
val avatarActions: ImmutableList<AvatarAction>,
|
||||
val createRoomAction: AsyncAction<RoomId>,
|
||||
val cameraPermissionState: PermissionsState,
|
||||
val roomAddressValidity: RoomAddressValidity,
|
||||
val homeserverName: String,
|
||||
val availableJoinRules: ImmutableList<JoinRuleItem>,
|
||||
val spaces: ImmutableList<SpaceRoom>,
|
||||
val eventSink: (ConfigureRoomEvents) -> Unit
|
||||
) {
|
||||
val isValid: Boolean = config.roomName?.isNotEmpty() == true &&
|
||||
(config.roomVisibility is RoomVisibilityState.Private || roomAddressValidity == RoomAddressValidity.Valid)
|
||||
(config.visibilityState is RoomVisibilityState.Private || roomAddressValidity == RoomAddressValidity.Valid)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,15 @@ package io.element.android.features.createroom.impl.configureroom
|
|||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
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.spaces.SpaceRoom
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
||||
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity
|
||||
import io.element.android.libraries.permissions.api.PermissionsState
|
||||
import io.element.android.libraries.permissions.api.aPermissionsState
|
||||
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomState> {
|
||||
|
|
@ -28,9 +31,9 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
|
|||
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(
|
||||
visibilityState = RoomVisibilityState.Public(
|
||||
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
||||
roomAccess = RoomAccess.Knocking,
|
||||
joinRuleItem = JoinRuleItem.PublicVisibility.AskToJoin,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -39,9 +42,9 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
|
|||
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(
|
||||
visibilityState = RoomVisibilityState.Public(
|
||||
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
||||
roomAccess = RoomAccess.Knocking,
|
||||
joinRuleItem = JoinRuleItem.PublicVisibility.AskToJoin,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -49,9 +52,9 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
|
|||
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",
|
||||
roomVisibility = RoomVisibilityState.Public(
|
||||
visibilityState = RoomVisibilityState.Public(
|
||||
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
||||
roomAccess = RoomAccess.Knocking,
|
||||
joinRuleItem = JoinRuleItem.PublicVisibility.AskToJoin,
|
||||
),
|
||||
),
|
||||
roomAddressValidity = RoomAddressValidity.NotAvailable,
|
||||
|
|
@ -60,9 +63,9 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
|
|||
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",
|
||||
roomVisibility = RoomVisibilityState.Public(
|
||||
visibilityState = RoomVisibilityState.Public(
|
||||
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
||||
roomAccess = RoomAccess.Knocking,
|
||||
joinRuleItem = JoinRuleItem.PublicVisibility.AskToJoin,
|
||||
),
|
||||
),
|
||||
roomAddressValidity = RoomAddressValidity.InvalidSymbols,
|
||||
|
|
@ -71,9 +74,9 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
|
|||
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",
|
||||
roomVisibility = RoomVisibilityState.Public(
|
||||
visibilityState = RoomVisibilityState.Public(
|
||||
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
||||
roomAccess = RoomAccess.Knocking,
|
||||
joinRuleItem = JoinRuleItem.PublicVisibility.AskToJoin,
|
||||
),
|
||||
),
|
||||
roomAddressValidity = RoomAddressValidity.Valid,
|
||||
|
|
@ -83,13 +86,41 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
|
|||
isSpace = true,
|
||||
roomName = "Space 101",
|
||||
topic = "Space topic for this space when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
||||
roomVisibility = RoomVisibilityState.Public(
|
||||
visibilityState = RoomVisibilityState.Public(
|
||||
roomAddress = RoomAddress.AutoFilled("Space-101"),
|
||||
roomAccess = RoomAccess.Anyone,
|
||||
joinRuleItem = JoinRuleItem.PublicVisibility.Public,
|
||||
),
|
||||
),
|
||||
roomAddressValidity = RoomAddressValidity.Valid,
|
||||
),
|
||||
aConfigureRoomState(
|
||||
config = CreateRoomConfig(
|
||||
isSpace = false,
|
||||
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",
|
||||
parentSpace = null,
|
||||
visibilityState = RoomVisibilityState.Public(
|
||||
roomAddress = RoomAddress.AutoFilled("Space-101"),
|
||||
joinRuleItem = JoinRuleItem.PublicVisibility.Restricted(aSpaceRoom().roomId),
|
||||
),
|
||||
),
|
||||
spaces = listOf(aSpaceRoom()),
|
||||
roomAddressValidity = RoomAddressValidity.Valid,
|
||||
),
|
||||
aConfigureRoomState(
|
||||
config = CreateRoomConfig(
|
||||
isSpace = false,
|
||||
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",
|
||||
parentSpace = aSpaceRoom(canonicalAlias = RoomAlias("#a-space-room:example.org")),
|
||||
visibilityState = RoomVisibilityState.Public(
|
||||
roomAddress = RoomAddress.AutoFilled("Space-101"),
|
||||
joinRuleItem = JoinRuleItem.PublicVisibility.Restricted(aSpaceRoom().roomId),
|
||||
),
|
||||
),
|
||||
spaces = listOf(aSpaceRoom()),
|
||||
roomAddressValidity = RoomAddressValidity.Valid,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -101,14 +132,29 @@ fun aConfigureRoomState(
|
|||
cameraPermissionState: PermissionsState = aPermissionsState(showDialog = false),
|
||||
homeserverName: String = "matrix.org",
|
||||
roomAddressValidity: RoomAddressValidity = RoomAddressValidity.Valid,
|
||||
availableVisibilityOptions: List<JoinRuleItem> = if (config.parentSpace != null) {
|
||||
listOfNotNull(
|
||||
JoinRuleItem.PublicVisibility.Restricted(config.parentSpace.roomId),
|
||||
JoinRuleItem.PublicVisibility.AskToJoinRestricted(config.parentSpace.roomId).takeIf { isKnockFeatureEnabled },
|
||||
JoinRuleItem.Private,
|
||||
)
|
||||
} else {
|
||||
listOfNotNull(
|
||||
JoinRuleItem.PublicVisibility.Public,
|
||||
JoinRuleItem.PublicVisibility.AskToJoin.takeIf { isKnockFeatureEnabled },
|
||||
JoinRuleItem.Private,
|
||||
)
|
||||
},
|
||||
spaces: List<SpaceRoom> = emptyList(),
|
||||
eventSink: (ConfigureRoomEvents) -> Unit = { },
|
||||
) = ConfigureRoomState(
|
||||
config = config,
|
||||
isKnockFeatureEnabled = isKnockFeatureEnabled,
|
||||
avatarActions = avatarActions.toImmutableList(),
|
||||
createRoomAction = createRoomAction,
|
||||
cameraPermissionState = cameraPermissionState,
|
||||
homeserverName = homeserverName,
|
||||
roomAddressValidity = roomAddressValidity,
|
||||
availableJoinRules = availableVisibilityOptions.toImmutableList(),
|
||||
spaces = spaces.toImmutableList(),
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import androidx.compose.ui.text.input.KeyboardCapitalization
|
|||
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.createroom.impl.R
|
||||
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom
|
||||
|
|
@ -46,7 +47,6 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
|||
import io.element.android.libraries.designsystem.components.avatar.AvatarType
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||
import io.element.android.libraries.designsystem.icons.CompoundDrawables
|
||||
import io.element.android.libraries.designsystem.modifiers.clearFocusOnTap
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
|
|
@ -59,12 +59,14 @@ 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.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import io.element.android.libraries.matrix.ui.components.AvatarActionBottomSheet
|
||||
import io.element.android.libraries.matrix.ui.components.AvatarPickerState
|
||||
import io.element.android.libraries.matrix.ui.components.AvatarPickerView
|
||||
import io.element.android.libraries.matrix.ui.room.address.RoomAddressField
|
||||
import io.element.android.libraries.permissions.api.PermissionsView
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
@Composable
|
||||
|
|
@ -119,27 +121,30 @@ fun ConfigureRoomView(
|
|||
onTopicChange = { state.eventSink(ConfigureRoomEvents.TopicChanged(it)) },
|
||||
)
|
||||
|
||||
RoomVisibilityAndAccessOptions(
|
||||
selected = when (state.config.roomVisibility) {
|
||||
is RoomVisibilityState.Private -> RoomVisibilityItem.Private
|
||||
is RoomVisibilityState.Public -> when (state.config.roomVisibility.roomAccess) {
|
||||
RoomAccess.Knocking -> RoomVisibilityItem.AskToJoin
|
||||
RoomAccess.Anyone -> RoomVisibilityItem.Public
|
||||
}
|
||||
},
|
||||
isKnockingEnabled = state.isKnockFeatureEnabled,
|
||||
if (!state.config.isSpace && state.spaces.isNotEmpty()) {
|
||||
SelectParentSpaceOptions(
|
||||
spaces = state.spaces,
|
||||
selectedSpace = state.config.parentSpace,
|
||||
onSelectSpace = { state.eventSink(ConfigureRoomEvents.SetParentSpace(it)) },
|
||||
)
|
||||
}
|
||||
|
||||
RoomJoinRuleOptions(
|
||||
options = state.availableJoinRules,
|
||||
selected = state.config.visibilityState.joinRuleItem,
|
||||
parentSpace = state.config.parentSpace,
|
||||
onOptionClick = {
|
||||
focusManager.clearFocus()
|
||||
state.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(it))
|
||||
state.eventSink(ConfigureRoomEvents.JoinRuleChanged(it))
|
||||
},
|
||||
)
|
||||
|
||||
if (state.config.roomVisibility !is RoomVisibilityState.Private) {
|
||||
if (state.config.visibilityState !is RoomVisibilityState.Private) {
|
||||
Column {
|
||||
ListSectionHeader(title = stringResource(R.string.screen_create_room_room_address_section_title))
|
||||
RoomAddressField(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
address = state.config.roomVisibility.roomAddress().getOrNull().orEmpty(),
|
||||
address = state.config.visibilityState.roomAddress().getOrNull().orEmpty(),
|
||||
homeserverName = state.homeserverName,
|
||||
addressValidity = state.roomAddressValidity,
|
||||
onAddressChange = { state.eventSink(ConfigureRoomEvents.RoomAddressChanged(it)) },
|
||||
|
|
@ -264,7 +269,7 @@ private fun RoomTopic(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun ConfigureRoomOptions(
|
||||
internal fun ConfigureRoomOptions(
|
||||
title: String,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
|
|
@ -278,30 +283,29 @@ private fun ConfigureRoomOptions(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun RoomVisibilityAndAccessOptions(
|
||||
selected: RoomVisibilityItem,
|
||||
isKnockingEnabled: Boolean,
|
||||
onOptionClick: (RoomVisibilityItem) -> Unit,
|
||||
private fun RoomJoinRuleOptions(
|
||||
options: ImmutableList<JoinRuleItem>,
|
||||
selected: JoinRuleItem,
|
||||
onOptionClick: (JoinRuleItem) -> Unit,
|
||||
parentSpace: SpaceRoom?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ConfigureRoomOptions(
|
||||
title = stringResource(R.string.screen_create_room_room_access_section_title),
|
||||
modifier = modifier,
|
||||
) {
|
||||
RoomVisibilityItem.entries.forEach { item ->
|
||||
if (item == RoomVisibilityItem.AskToJoin && !isKnockingEnabled) {
|
||||
return@forEach
|
||||
}
|
||||
|
||||
options.forEach { item ->
|
||||
val isSelected = item == selected
|
||||
ListItem(
|
||||
leadingContent = ListItemContent.Custom {
|
||||
RoundedIconAtom(
|
||||
size = RoundedIconAtomSize.Big,
|
||||
resourceId = when (item) {
|
||||
RoomVisibilityItem.Public -> CompoundDrawables.ic_compound_public
|
||||
RoomVisibilityItem.AskToJoin -> CompoundDrawables.ic_compound_user_add
|
||||
RoomVisibilityItem.Private -> CompoundDrawables.ic_compound_lock
|
||||
imageVector = when (item) {
|
||||
JoinRuleItem.PublicVisibility.Public -> CompoundIcons.Public()
|
||||
is JoinRuleItem.PublicVisibility.Restricted -> CompoundIcons.Space()
|
||||
JoinRuleItem.PublicVisibility.AskToJoin,
|
||||
is JoinRuleItem.PublicVisibility.AskToJoinRestricted -> CompoundIcons.UserAdd()
|
||||
JoinRuleItem.Private -> CompoundIcons.Lock()
|
||||
},
|
||||
tint = if (isSelected) ElementTheme.colors.iconPrimary else ElementTheme.colors.iconSecondary,
|
||||
backgroundTint = Color.Transparent,
|
||||
|
|
@ -309,18 +313,29 @@ private fun RoomVisibilityAndAccessOptions(
|
|||
},
|
||||
headlineContent = {
|
||||
val title = when (item) {
|
||||
RoomVisibilityItem.Public -> stringResource(R.string.screen_create_room_public_option_title)
|
||||
RoomVisibilityItem.AskToJoin -> stringResource(R.string.screen_create_room_room_access_section_knocking_option_title)
|
||||
RoomVisibilityItem.Private -> stringResource(R.string.screen_create_room_private_option_title)
|
||||
JoinRuleItem.PublicVisibility.Public -> stringResource(R.string.screen_create_room_room_access_section_public_option_title)
|
||||
is JoinRuleItem.PublicVisibility.Restricted -> stringResource(R.string.screen_create_room_room_access_section_restricted_option_title)
|
||||
JoinRuleItem.PublicVisibility.AskToJoin -> stringResource(R.string.screen_create_room_room_access_section_knocking_option_title)
|
||||
is JoinRuleItem.PublicVisibility.AskToJoinRestricted -> stringResource(
|
||||
R.string.screen_create_room_room_access_section_knocking_restricted_option_title
|
||||
)
|
||||
JoinRuleItem.Private -> stringResource(R.string.screen_create_room_room_access_section_private_option_title)
|
||||
}
|
||||
Text(text = title)
|
||||
},
|
||||
supportingContent = {
|
||||
// TODO handle description of items in a certain space/org
|
||||
val description = when (item) {
|
||||
RoomVisibilityItem.Public -> stringResource(R.string.screen_create_room_public_option_short_description)
|
||||
RoomVisibilityItem.AskToJoin -> stringResource(R.string.screen_create_room_room_access_section_knocking_option_description)
|
||||
RoomVisibilityItem.Private -> stringResource(R.string.screen_create_room_private_option_description)
|
||||
JoinRuleItem.PublicVisibility.Public -> stringResource(R.string.screen_create_room_room_access_section_public_option_description)
|
||||
is JoinRuleItem.PublicVisibility.Restricted -> stringResource(
|
||||
R.string.screen_create_room_room_access_section_restricted_option_description,
|
||||
parentSpace?.displayName.orEmpty()
|
||||
)
|
||||
JoinRuleItem.PublicVisibility.AskToJoin -> stringResource(R.string.screen_create_room_room_access_section_knocking_option_description)
|
||||
is JoinRuleItem.PublicVisibility.AskToJoinRestricted -> stringResource(
|
||||
R.string.screen_create_room_room_access_section_knocking_restricted_option_description,
|
||||
parentSpace?.displayName.orEmpty()
|
||||
)
|
||||
JoinRuleItem.Private -> stringResource(R.string.screen_create_room_room_access_section_private_option_description)
|
||||
}
|
||||
Text(text = description)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
package io.element.android.features.createroom.impl.configureroom
|
||||
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -18,5 +19,6 @@ data class CreateRoomConfig(
|
|||
val topic: String? = null,
|
||||
val avatarUri: String? = null,
|
||||
val invites: ImmutableList<MatrixUser> = persistentListOf(),
|
||||
val roomVisibility: RoomVisibilityState = RoomVisibilityState.Private,
|
||||
val visibilityState: RoomVisibilityState = RoomVisibilityState.Private(),
|
||||
val parentSpace: SpaceRoom? = null,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import android.net.Uri
|
|||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.androidutils.file.safeDelete
|
||||
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.getAndUpdate
|
||||
|
|
@ -33,23 +34,20 @@ class CreateRoomConfigStore(
|
|||
|
||||
fun setRoomName(roomName: String) {
|
||||
createRoomConfigFlow.getAndUpdate { config ->
|
||||
val newVisibility = when (config.roomVisibility) {
|
||||
is RoomVisibilityState.Public -> {
|
||||
val roomAddress = config.roomVisibility.roomAddress
|
||||
if (roomAddress is RoomAddress.AutoFilled || roomName.isEmpty()) {
|
||||
val roomAliasName = roomAliasHelper.roomAliasNameFromRoomDisplayName(roomName)
|
||||
config.roomVisibility.copy(
|
||||
roomAddress = RoomAddress.AutoFilled(roomAliasName),
|
||||
)
|
||||
} else {
|
||||
config.roomVisibility
|
||||
}
|
||||
val roomAccessWithNewAddress = if (config.visibilityState is RoomVisibilityState.Public) {
|
||||
val roomAddress = config.visibilityState.roomAddress
|
||||
if (roomAddress is RoomAddress.AutoFilled || roomName.isEmpty()) {
|
||||
val roomAliasName = roomAliasHelper.roomAliasNameFromRoomDisplayName(roomName)
|
||||
config.visibilityState.copy(roomAddress = RoomAddress.AutoFilled(roomAliasName))
|
||||
} else {
|
||||
config.visibilityState
|
||||
}
|
||||
else -> config.roomVisibility
|
||||
} else {
|
||||
config.visibilityState
|
||||
}
|
||||
config.copy(
|
||||
roomName = roomName.takeIf { it.isNotEmpty() },
|
||||
roomVisibility = newVisibility,
|
||||
visibilityState = roomAccessWithNewAddress,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -67,16 +65,19 @@ class CreateRoomConfigStore(
|
|||
}
|
||||
}
|
||||
|
||||
fun setRoomVisibility(visibility: RoomVisibilityItem) {
|
||||
/**
|
||||
* Sets both the room visibility and its access based on the provided join rule.
|
||||
*/
|
||||
fun setJoinRule(joinRule: JoinRuleItem) {
|
||||
createRoomConfigFlow.getAndUpdate { config ->
|
||||
config.copy(
|
||||
roomVisibility = when (visibility) {
|
||||
RoomVisibilityItem.Private -> RoomVisibilityState.Private
|
||||
RoomVisibilityItem.Public, RoomVisibilityItem.AskToJoin -> {
|
||||
visibilityState = when (joinRule) {
|
||||
JoinRuleItem.Private -> RoomVisibilityState.Private()
|
||||
is JoinRuleItem.PublicVisibility -> {
|
||||
val roomAliasName = roomAliasHelper.roomAliasNameFromRoomDisplayName(config.roomName.orEmpty())
|
||||
RoomVisibilityState.Public(
|
||||
roomAddress = RoomAddress.AutoFilled(roomAliasName),
|
||||
roomAccess = if (visibility == RoomVisibilityItem.AskToJoin) RoomAccess.Knocking else RoomAccess.Anyone,
|
||||
joinRuleItem = joinRule,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -87,28 +88,12 @@ class CreateRoomConfigStore(
|
|||
fun setRoomAddress(address: String) {
|
||||
createRoomConfigFlow.getAndUpdate { config ->
|
||||
config.copy(
|
||||
roomVisibility = when (config.roomVisibility) {
|
||||
visibilityState = when (config.visibilityState) {
|
||||
is RoomVisibilityState.Public -> {
|
||||
val sanitizedAddress = address.lowercase()
|
||||
config.roomVisibility.copy(roomAddress = RoomAddress.Edited(sanitizedAddress))
|
||||
config.visibilityState.copy(roomAddress = RoomAddress.Edited(sanitizedAddress))
|
||||
}
|
||||
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
|
||||
else -> config.visibilityState
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -120,6 +105,15 @@ class CreateRoomConfigStore(
|
|||
}
|
||||
}
|
||||
|
||||
fun setParentSpace(parentSpace: SpaceRoom?) {
|
||||
createRoomConfigFlow.getAndUpdate { config ->
|
||||
config.copy(
|
||||
parentSpace = parentSpace,
|
||||
visibilityState = RoomVisibilityState.Private(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearCachedData() {
|
||||
cachedAvatarUri = null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.createroom.impl.configureroom
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.join.AllowRule
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
/**
|
||||
* Join rule items to display in UI.
|
||||
*/
|
||||
@Immutable
|
||||
sealed interface JoinRuleItem {
|
||||
data object Private : JoinRuleItem
|
||||
|
||||
/**
|
||||
* Those join rule items that represent public visibility of the room/space.
|
||||
*/
|
||||
@Immutable
|
||||
sealed interface PublicVisibility : JoinRuleItem {
|
||||
data object Public : PublicVisibility
|
||||
data object AskToJoin : PublicVisibility
|
||||
data class Restricted(val parentSpaceId: RoomId) : PublicVisibility
|
||||
data class AskToJoinRestricted(val parentSpaceId: RoomId) : PublicVisibility
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a [JoinRuleItem] option into a [JoinRule].
|
||||
*/
|
||||
fun toJoinRule(): JoinRule = when (this) {
|
||||
Private -> JoinRule.Private
|
||||
PublicVisibility.Public -> JoinRule.Public
|
||||
PublicVisibility.AskToJoin -> JoinRule.Knock
|
||||
is PublicVisibility.Restricted -> JoinRule.Restricted(persistentListOf(AllowRule.RoomMembership(parentSpaceId)))
|
||||
is PublicVisibility.AskToJoinRestricted -> JoinRule.KnockRestricted(persistentListOf(AllowRule.RoomMembership(parentSpaceId)))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.createroom.impl.configureroom
|
||||
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
|
||||
enum class RoomAccess {
|
||||
Anyone,
|
||||
Knocking
|
||||
}
|
||||
|
||||
fun RoomAccess.toJoinRule(): JoinRule? {
|
||||
return when (this) {
|
||||
RoomAccess.Anyone -> null
|
||||
RoomAccess.Knocking -> JoinRule.Knock
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.createroom.impl.configureroom
|
||||
|
||||
enum class RoomAccessItem {
|
||||
Anyone,
|
||||
AskToJoin,
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.createroom.impl.configureroom
|
||||
|
||||
enum class RoomVisibilityItem {
|
||||
Public,
|
||||
AskToJoin,
|
||||
Private
|
||||
}
|
||||
|
|
@ -11,11 +11,12 @@ package io.element.android.features.createroom.impl.configureroom
|
|||
import java.util.Optional
|
||||
|
||||
sealed interface RoomVisibilityState {
|
||||
data object Private : RoomVisibilityState
|
||||
val joinRuleItem: JoinRuleItem
|
||||
data class Private(override val joinRuleItem: JoinRuleItem.Private = JoinRuleItem.Private) : RoomVisibilityState
|
||||
|
||||
data class Public(
|
||||
val roomAddress: RoomAddress,
|
||||
val roomAccess: RoomAccess,
|
||||
override val joinRuleItem: JoinRuleItem.PublicVisibility,
|
||||
) : RoomVisibilityState
|
||||
|
||||
fun roomAddress(): Optional<String> {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,209 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.createroom.impl.configureroom
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.createroom.impl.R
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarType
|
||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.IconSource
|
||||
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||
import io.element.android.libraries.designsystem.theme.components.ListSectionHeader
|
||||
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.hide
|
||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun SelectParentSpaceOptions(
|
||||
spaces: ImmutableList<SpaceRoom>,
|
||||
selectedSpace: SpaceRoom?,
|
||||
onSelectSpace: (SpaceRoom?) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var displaySelectSpaceBottomSheet by remember { mutableStateOf(false) }
|
||||
ConfigureRoomOptions(
|
||||
title = stringResource(CommonStrings.common_space),
|
||||
modifier = modifier
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = selectedSpace?.displayName
|
||||
?: stringResource(R.string.screen_create_room_space_selection_no_space_title),
|
||||
maxLines = 1
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(
|
||||
text = if (selectedSpace != null) {
|
||||
selectedSpace.canonicalAlias?.value.orEmpty()
|
||||
} else {
|
||||
stringResource(R.string.screen_create_room_space_selection_no_space_description)
|
||||
},
|
||||
maxLines = 1
|
||||
)
|
||||
},
|
||||
leadingContent = if (selectedSpace == null) {
|
||||
ListItemContent.Icon(IconSource.Vector(CompoundIcons.Home()))
|
||||
} else {
|
||||
ListItemContent.Custom({
|
||||
val avatarData = AvatarData(
|
||||
id = selectedSpace.roomId.value,
|
||||
name = selectedSpace.displayName,
|
||||
url = selectedSpace.avatarUrl,
|
||||
size = AvatarSize.SelectParentSpace,
|
||||
)
|
||||
Avatar(avatarData = avatarData, avatarType = AvatarType.Space())
|
||||
})
|
||||
},
|
||||
onClick = { displaySelectSpaceBottomSheet = true }
|
||||
)
|
||||
|
||||
if (displaySelectSpaceBottomSheet) {
|
||||
val sheetState = rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded = true,
|
||||
confirmValueChange = { true },
|
||||
)
|
||||
ModalBottomSheet(
|
||||
sheetState = sheetState,
|
||||
onDismissRequest = {
|
||||
sheetState.hide(coroutineScope) {
|
||||
displaySelectSpaceBottomSheet = false
|
||||
}
|
||||
}
|
||||
) {
|
||||
SelectParentSpaceBottomSheet(
|
||||
spaces = spaces,
|
||||
selectedSpace = selectedSpace,
|
||||
) {
|
||||
sheetState.hide(coroutineScope) {
|
||||
displaySelectSpaceBottomSheet = false
|
||||
}
|
||||
onSelectSpace(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.SelectParentSpaceBottomSheet(
|
||||
spaces: ImmutableList<SpaceRoom>,
|
||||
selectedSpace: SpaceRoom?,
|
||||
onSelectSpace: (SpaceRoom?) -> Unit,
|
||||
) {
|
||||
ListSectionHeader(
|
||||
title = stringResource(R.string.screen_create_room_space_selection_sheet_title),
|
||||
hasDivider = false
|
||||
)
|
||||
LazyColumn(modifier = Modifier.fillMaxWidth()) {
|
||||
item {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
stringResource(R.string.screen_create_room_space_selection_no_space_title),
|
||||
maxLines = 1
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(
|
||||
stringResource(R.string.screen_create_room_space_selection_no_space_description),
|
||||
maxLines = 1
|
||||
)
|
||||
},
|
||||
leadingContent = ListItemContent.Icon(
|
||||
IconSource.Vector(CompoundIcons.Home())
|
||||
),
|
||||
trailingContent = ListItemContent.RadioButton(
|
||||
selected = selectedSpace == null
|
||||
),
|
||||
onClick = { onSelectSpace(null) },
|
||||
)
|
||||
}
|
||||
for (space in spaces) {
|
||||
item {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
space.displayName,
|
||||
maxLines = 1
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(
|
||||
space.canonicalAlias?.value.orEmpty(),
|
||||
maxLines = 1
|
||||
)
|
||||
},
|
||||
leadingContent = ListItemContent.Custom({
|
||||
val avatarData =
|
||||
AvatarData(
|
||||
id = space.roomId.value,
|
||||
name = space.displayName,
|
||||
url = space.avatarUrl,
|
||||
size = AvatarSize.SelectParentSpace,
|
||||
)
|
||||
Avatar(
|
||||
avatarData = avatarData,
|
||||
avatarType = AvatarType.Space()
|
||||
)
|
||||
}),
|
||||
trailingContent = ListItemContent.RadioButton(
|
||||
selected = selectedSpace == space
|
||||
),
|
||||
onClick = { onSelectSpace(space) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun SelectParentSpaceBottomSheetPreview() =
|
||||
ElementPreview {
|
||||
Column {
|
||||
SelectParentSpaceBottomSheet(
|
||||
spaces = persistentListOf(
|
||||
aSpaceRoom(
|
||||
canonicalAlias = RoomAlias(
|
||||
"#a-room-alias:example.org"
|
||||
)
|
||||
)
|
||||
),
|
||||
selectedSpace = null,
|
||||
) {}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,15 +3,30 @@
|
|||
<string name="screen_create_room_action_create_room">"Nyt rum"</string>
|
||||
<string name="screen_create_room_add_people_title">"Invitér andre"</string>
|
||||
<string name="screen_create_room_error_creating_room">"Der opstod en fejl ved oprettelsen af rummet"</string>
|
||||
<string name="screen_create_room_private_option_description">"Kun inviterede personer kan få adgang til dette rum. Alle meddelelser er ende-til-ende krypteret."</string>
|
||||
<string name="screen_create_room_error_creating_space">"Gruppen kunne ikke oprettes på grund af en ukendt fejl. Prøv igen senere."</string>
|
||||
<string name="screen_create_room_name_placeholder">"Tilføj navn…"</string>
|
||||
<string name="screen_create_room_new_room_title">"Nyt rum"</string>
|
||||
<string name="screen_create_room_new_space_title">"Ny gruppe"</string>
|
||||
<string name="screen_create_room_private_option_description">"Kun inviterede personer kan deltage."</string>
|
||||
<string name="screen_create_room_private_option_title">"Privat"</string>
|
||||
<string name="screen_create_room_public_option_description">"Alle kan finde dette rum.
|
||||
Du kan ændre dette når som helst i rummets indstillinger."</string>
|
||||
<string name="screen_create_room_public_option_short_description">"Alle kan deltage."</string>
|
||||
<string name="screen_create_room_public_option_title">"Offentlig"</string>
|
||||
<string name="screen_create_room_room_access_section_knocking_option_description">"Alle kan bede om at deltage i rummet, men en administrator eller en moderator skal acceptere anmodningen"</string>
|
||||
<string name="screen_create_room_room_access_section_knocking_option_title">"Spørg om at deltage"</string>
|
||||
<string name="screen_create_room_room_access_section_knocking_option_title">"Tillad at man kan anmode om deltagelse"</string>
|
||||
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"Enhver i %1$s kan deltage, men alle andre skal anmode om adgang."</string>
|
||||
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"Anmod om at deltage"</string>
|
||||
<string name="screen_create_room_room_access_section_private_option_description">"Kun inviterede brugere kan deltage."</string>
|
||||
<string name="screen_create_room_room_access_section_private_option_title">"Privat"</string>
|
||||
<string name="screen_create_room_room_access_section_public_option_description">"Alle kan deltage i dette rum"</string>
|
||||
<string name="screen_create_room_room_access_section_public_option_title">"Enhver"</string>
|
||||
<string name="screen_create_room_room_address_section_footer">"Hvis dette rum skal være synligt i det offentlige register, skal du bruge en rum-adresse."</string>
|
||||
<string name="screen_create_room_room_address_section_title">"Rummets adresse"</string>
|
||||
<string name="screen_create_room_room_access_section_public_option_title">"Offentlig"</string>
|
||||
<string name="screen_create_room_room_access_section_restricted_option_description">"Alle i %1$s kan deltage."</string>
|
||||
<string name="screen_create_room_room_access_section_restricted_option_title">"Standard"</string>
|
||||
<string name="screen_create_room_room_access_section_title">"Hvem har adgang"</string>
|
||||
<string name="screen_create_room_room_address_section_footer">"Hvis dette rum skal være synligt i det offentlige register, skal du bruge en adresse."</string>
|
||||
<string name="screen_create_room_room_address_section_title">"Adresse"</string>
|
||||
<string name="screen_create_room_room_visibility_section_title">"Rummets synlighed"</string>
|
||||
<string name="screen_create_room_topic_label">"Emne (valgfrit)"</string>
|
||||
<string name="screen_create_room_topic_placeholder">"Tilføj beskrivelse…"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -15,14 +15,20 @@ Du kannst dies jederzeit in den Einstellungen des Chats ändern."</string>
|
|||
<string name="screen_create_room_public_option_title">"Öffentlicher Chatroom"</string>
|
||||
<string name="screen_create_room_room_access_section_knocking_option_description">"Jeder kann den Beitritt zum Chat erbitten, aber ein Admin oder Moderator muss die Anfrage akzeptieren."</string>
|
||||
<string name="screen_create_room_room_access_section_knocking_option_title">"Anfrage zum Beitritt zulassen"</string>
|
||||
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"Jeder in %1$s kann beitreten, aber alle anderen müssen den Beitritt anfragen."</string>
|
||||
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"Beitritt anfragen"</string>
|
||||
<string name="screen_create_room_room_access_section_private_option_description">"Nur eingeladene Personen können beitreten."</string>
|
||||
<string name="screen_create_room_room_access_section_private_option_title">"Privat"</string>
|
||||
<string name="screen_create_room_room_access_section_public_option_description">"Jeder darf diesem Chat beitreten."</string>
|
||||
<string name="screen_create_room_room_access_section_public_option_title">"Jeder"</string>
|
||||
<string name="screen_create_room_room_access_section_restricted_option_description">"Jeder in %1$s kann beitreten."</string>
|
||||
<string name="screen_create_room_room_access_section_restricted_option_title">"Standard"</string>
|
||||
<string name="screen_create_room_room_access_section_title">"Wer hat Zugang"</string>
|
||||
<string name="screen_create_room_room_address_section_footer">"Du benötigst eine Adresse, um diesen Chat im öffentlichen Verzeichnis sichtbar zu machen."</string>
|
||||
<string name="screen_create_room_room_address_section_title">"Adresse"</string>
|
||||
<string name="screen_create_room_room_visibility_section_title">" Sichtbarkeit des Chats"</string>
|
||||
<string name="screen_create_room_space_selection_no_space_description">"(kein Space)"</string>
|
||||
<string name="screen_create_room_space_selection_sheet_title">"Space hinzufügen"</string>
|
||||
<string name="screen_create_room_topic_label">"Thema (optional)"</string>
|
||||
<string name="screen_create_room_topic_placeholder">"Beschreibung hinzufügen…"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -15,14 +15,21 @@ 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_room_access_section_knocking_option_description">"Tout le monde peut demander à joindre, mais un administrateur ou un modérateur devra accepter la demande"</string>
|
||||
<string name="screen_create_room_room_access_section_knocking_option_title">"Autoriser la demande à joindre"</string>
|
||||
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"Tout membre de %1$s peut joindre, mais les autres doivent demander un accès."</string>
|
||||
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"Demander à joindre"</string>
|
||||
<string name="screen_create_room_room_access_section_private_option_description">"Seules les personnes invitées peuvent joindre."</string>
|
||||
<string name="screen_create_room_room_access_section_private_option_title">"Privé"</string>
|
||||
<string name="screen_create_room_room_access_section_public_option_description">"Tout le monde peut joindre"</string>
|
||||
<string name="screen_create_room_room_access_section_public_option_title">"Tout le monde"</string>
|
||||
<string name="screen_create_room_room_access_section_restricted_option_description">"Toute membre de %1$s peut joindre le salon."</string>
|
||||
<string name="screen_create_room_room_access_section_restricted_option_title">"Standard"</string>
|
||||
<string name="screen_create_room_room_access_section_title">"Qui a accès"</string>
|
||||
<string name="screen_create_room_room_address_section_footer">"Vous aurez besoin d’une adresse pour qu’il soit visible dans le répertoire public."</string>
|
||||
<string name="screen_create_room_room_address_section_title">"Adresse"</string>
|
||||
<string name="screen_create_room_room_visibility_section_title">"Visibilité du salon"</string>
|
||||
<string name="screen_create_room_space_selection_no_space_description">"(pas d’espace)"</string>
|
||||
<string name="screen_create_room_space_selection_no_space_title">"Accueil"</string>
|
||||
<string name="screen_create_room_space_selection_sheet_title">"Ajouter à l’espace"</string>
|
||||
<string name="screen_create_room_topic_label">"Sujet (facultatif)"</string>
|
||||
<string name="screen_create_room_topic_placeholder">"Ajouter une description…"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -3,16 +3,33 @@
|
|||
<string name="screen_create_room_action_create_room">"Új szoba"</string>
|
||||
<string name="screen_create_room_add_people_title">"Ismerősök meghívása"</string>
|
||||
<string name="screen_create_room_error_creating_room">"Hiba történt a szoba létrehozásakor"</string>
|
||||
<string name="screen_create_room_private_option_description">"Csak a meghívottak léphetnek be ebbe a szobába. Az összes üzenet végpontok közti titkosítással van védve."</string>
|
||||
<string name="screen_create_room_error_creating_space">"A teret ismeretlen hiba miatt nem sikerült létrehozni. Próbálja újra később."</string>
|
||||
<string name="screen_create_room_name_placeholder">"Név hozzáadása…"</string>
|
||||
<string name="screen_create_room_new_room_title">"Új szoba"</string>
|
||||
<string name="screen_create_room_new_space_title">"Új tér"</string>
|
||||
<string name="screen_create_room_private_option_description">"Csak a meghívottak léphetnek be."</string>
|
||||
<string name="screen_create_room_private_option_title">"Privát"</string>
|
||||
<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>
|
||||
<string name="screen_create_room_public_option_short_description">"Bárki csatlakozhat."</string>
|
||||
<string name="screen_create_room_public_option_title">"Nyilvános szoba"</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_description">"Bárki kérheti, hogy csatlakozhasson 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_access_section_public_option_description">"Bárki csatlakozhat ehhez a szobához"</string>
|
||||
<string name="screen_create_room_room_access_section_public_option_title">"Bárki"</string>
|
||||
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"Bárki csatlakozhat innen: %1$s, de mindenki másnak hozzáférést kell kérnie."</string>
|
||||
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"Csatlakozás kérése"</string>
|
||||
<string name="screen_create_room_room_access_section_private_option_description">"Csak a meghívottak léphetnek be."</string>
|
||||
<string name="screen_create_room_room_access_section_private_option_title">"Privát"</string>
|
||||
<string name="screen_create_room_room_access_section_public_option_description">"Bárki csatlakozhat."</string>
|
||||
<string name="screen_create_room_room_access_section_public_option_title">"Nyilvános"</string>
|
||||
<string name="screen_create_room_room_access_section_restricted_option_description">"Bárki csatlakozhat innen: %1$s."</string>
|
||||
<string name="screen_create_room_room_access_section_restricted_option_title">"Szokásos"</string>
|
||||
<string name="screen_create_room_room_access_section_title">"Hozzáférésre jogosultak"</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_address_section_title">"Cím"</string>
|
||||
<string name="screen_create_room_room_visibility_section_title">"Szoba láthatósága"</string>
|
||||
<string name="screen_create_room_space_selection_no_space_description">"(nincs tér)"</string>
|
||||
<string name="screen_create_room_space_selection_no_space_title">"Kezdőlap"</string>
|
||||
<string name="screen_create_room_space_selection_sheet_title">"Hozzáadás a térhez"</string>
|
||||
<string name="screen_create_room_topic_label">"Téma (nem kötelező)"</string>
|
||||
<string name="screen_create_room_topic_placeholder">"Leírás hozzáadása…"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -3,16 +3,17 @@
|
|||
<string name="screen_create_room_action_create_room">"Nytt rom"</string>
|
||||
<string name="screen_create_room_add_people_title">"Inviter folk"</string>
|
||||
<string name="screen_create_room_error_creating_room">"Det oppsto en feil under opprettelsen av rommet"</string>
|
||||
<string name="screen_create_room_private_option_description">"Bare inviterte personer har tilgang til dette rommet. Alle meldinger er ende-til-ende-kryptert."</string>
|
||||
<string name="screen_create_room_private_option_description">"Bare inviterte personer kan bli med."</string>
|
||||
<string name="screen_create_room_private_option_title">"Privat"</string>
|
||||
<string name="screen_create_room_public_option_description">"Alle kan finne dette rommet.
|
||||
Du kan endre dette når som helst i rominnstillingene."</string>
|
||||
<string name="screen_create_room_public_option_title">"Offentlig rom"</string>
|
||||
<string name="screen_create_room_room_access_section_knocking_option_description">"Alle kan be om å få bli med i rommet, men en administrator eller moderator må godta forespørselen"</string>
|
||||
<string name="screen_create_room_room_access_section_knocking_option_description">"Alle kan be om å få bli med, men en administrator eller moderator må godta forespørselen."</string>
|
||||
<string name="screen_create_room_room_access_section_knocking_option_title">"Be om å bli med"</string>
|
||||
<string name="screen_create_room_room_access_section_public_option_description">"Alle kan bli med i dette rommet"</string>
|
||||
<string name="screen_create_room_room_access_section_public_option_description">"Alle kan bli med."</string>
|
||||
<string name="screen_create_room_room_access_section_public_option_title">"Alle"</string>
|
||||
<string name="screen_create_room_room_address_section_footer">"For at dette rommet skal være synlig i den offentlige romkatalogen, trenger du en romadresse."</string>
|
||||
<string name="screen_create_room_room_address_section_title">"Romadresse"</string>
|
||||
<string name="screen_create_room_room_address_section_footer">"Du trenger en adresse for å gjøre den synlig i den offentlige katalogen."</string>
|
||||
<string name="screen_create_room_room_address_section_title">"Adresse"</string>
|
||||
<string name="screen_create_room_room_visibility_section_title">"Romsynlighet"</string>
|
||||
<string name="screen_create_room_topic_label">"Emne (valgfritt)"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
<string name="screen_create_room_add_people_title">"Convidar pessoas"</string>
|
||||
<string name="screen_create_room_error_creating_room">"Ocorreu um erro ao criar a sala"</string>
|
||||
<string name="screen_create_room_error_creating_space">"O espaço não pôde ser criado por conta de um erro desconhecido. Tente novamente mais tarde."</string>
|
||||
<string name="screen_create_room_name_placeholder">"Adicione o nome…"</string>
|
||||
<string name="screen_create_room_new_room_title">"Nova sala"</string>
|
||||
<string name="screen_create_room_new_space_title">"Novo espaço"</string>
|
||||
<string name="screen_create_room_private_option_description">"Apenas pessoas convidadas podem entrar."</string>
|
||||
<string name="screen_create_room_private_option_title">"Privada"</string>
|
||||
|
|
@ -12,12 +14,22 @@ Você pode mudar isso a qualquer momento nas configurações da sala."</string>
|
|||
<string name="screen_create_room_public_option_short_description">"Qualquer um pode entrar."</string>
|
||||
<string name="screen_create_room_public_option_title">"Publica"</string>
|
||||
<string name="screen_create_room_room_access_section_knocking_option_description">"Qualquer um pode pedir para entrar, mas um administrador ou moderador deve aceitar a solicitação"</string>
|
||||
<string name="screen_create_room_room_access_section_knocking_option_title">"Pedir para entrar"</string>
|
||||
<string name="screen_create_room_room_access_section_knocking_option_title">"Permitir pedir para entrar"</string>
|
||||
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"Qualquer um em %1$s pode entrar, mas todos os outros devem pedir acesso."</string>
|
||||
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"Pedir para entrar"</string>
|
||||
<string name="screen_create_room_room_access_section_private_option_description">"Apenas pessoas convidadas podem entrar."</string>
|
||||
<string name="screen_create_room_room_access_section_private_option_title">"Privada"</string>
|
||||
<string name="screen_create_room_room_access_section_public_option_description">"Qualquer um pode entrar."</string>
|
||||
<string name="screen_create_room_room_access_section_public_option_title">"Qualquer pessoa"</string>
|
||||
<string name="screen_create_room_room_access_section_restricted_option_description">"Qualquer um em %1$s pode participar."</string>
|
||||
<string name="screen_create_room_room_access_section_restricted_option_title">"Normal"</string>
|
||||
<string name="screen_create_room_room_access_section_title">"Quem tem acesso"</string>
|
||||
<string name="screen_create_room_room_address_section_footer">"Para que esta sala fique visível no diretório público de salas, você precisará 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_address_section_footer">"Você precisará de um endereço para torná-la visível no diretório público."</string>
|
||||
<string name="screen_create_room_room_address_section_title">"Endereço"</string>
|
||||
<string name="screen_create_room_room_visibility_section_title">"Visibilidade da sala"</string>
|
||||
<string name="screen_create_room_space_selection_no_space_description">"(sem espaço)"</string>
|
||||
<string name="screen_create_room_space_selection_no_space_title">"Início"</string>
|
||||
<string name="screen_create_room_space_selection_sheet_title">"Adicionar ao espaço"</string>
|
||||
<string name="screen_create_room_topic_label">"Tópico (opcional)"</string>
|
||||
<string name="screen_create_room_topic_placeholder">"Adicione a descrição…"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -3,16 +3,17 @@
|
|||
<string name="screen_create_room_action_create_room">"Nova sala"</string>
|
||||
<string name="screen_create_room_add_people_title">"Convidar pessoas"</string>
|
||||
<string name="screen_create_room_error_creating_room">"Ocorreu um erro ao criar a sala"</string>
|
||||
<string name="screen_create_room_private_option_description">"Apenas as pessoas convidadas podem aceder a esta sala. Todas as mensagens são cifradas ponta-a-ponta."</string>
|
||||
<string name="screen_create_room_private_option_description">"Apenas as pessoas convidadas podem entrar."</string>
|
||||
<string name="screen_create_room_private_option_title">"Privada"</string>
|
||||
<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>
|
||||
<string name="screen_create_room_public_option_title">"Sala pública"</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_description">"Qualquer pessoa pode pedir para entrar, 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_access_section_public_option_description">"Qualquer pessoa pode entrar nesta sala"</string>
|
||||
<string name="screen_create_room_room_access_section_public_option_title">"Qualquer pessoa"</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_access_section_public_option_description">"Qualquer pessoa pode entrar."</string>
|
||||
<string name="screen_create_room_room_access_section_public_option_title">"Pública"</string>
|
||||
<string name="screen_create_room_room_address_section_footer">"Para que esta sala seja visível no diretório público, precisa de ter um endereço."</string>
|
||||
<string name="screen_create_room_room_address_section_title">"Endereço"</string>
|
||||
<string name="screen_create_room_room_visibility_section_title">"Visibilidade da sala"</string>
|
||||
<string name="screen_create_room_topic_label">"Descrição (opcional)"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -3,16 +3,26 @@
|
|||
<string name="screen_create_room_action_create_room">"Создать новую комнату"</string>
|
||||
<string name="screen_create_room_add_people_title">"Пригласить в комнату"</string>
|
||||
<string name="screen_create_room_error_creating_room">"Произошла ошибка при создании комнаты"</string>
|
||||
<string name="screen_create_room_private_option_description">"Доступ в эту комнату имеют только приглашенные пользователи. Все сообщения защищены сквозным шифрованием."</string>
|
||||
<string name="screen_create_room_error_creating_space">"Не удалось создать это пространство из-за неизвестной ошибки. Попробуйте позже."</string>
|
||||
<string name="screen_create_room_name_placeholder">"Добавьте имя…"</string>
|
||||
<string name="screen_create_room_new_room_title">"Новая комната"</string>
|
||||
<string name="screen_create_room_new_space_title">"Новое пространство"</string>
|
||||
<string name="screen_create_room_private_option_description">"Присоединиться могут только приглашенные."</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_short_description">"Присоединиться может любой."</string>
|
||||
<string name="screen_create_room_public_option_title">"Общедоступная комната"</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_access_section_public_option_description">"Любой желающий может присоединиться к этой комнате"</string>
|
||||
<string name="screen_create_room_room_access_section_public_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_access_section_knocking_option_title">"Разрешить запрос на присоединение"</string>
|
||||
<string name="screen_create_room_room_access_section_private_option_description">"Присоединиться могут только приглашенные."</string>
|
||||
<string name="screen_create_room_room_access_section_private_option_title">"Частный"</string>
|
||||
<string name="screen_create_room_room_access_section_public_option_description">"Присоединиться может любой желающий."</string>
|
||||
<string name="screen_create_room_room_access_section_public_option_title">"Публичный"</string>
|
||||
<string name="screen_create_room_room_access_section_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_visibility_section_title">"Видимость комнаты"</string>
|
||||
<string name="screen_create_room_topic_label">"Тема (необязательно)"</string>
|
||||
<string name="screen_create_room_topic_placeholder">"Добавить описание…"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -3,16 +3,33 @@
|
|||
<string name="screen_create_room_action_create_room">"Nová miestnosť"</string>
|
||||
<string name="screen_create_room_add_people_title">"Pozvať ľudí"</string>
|
||||
<string name="screen_create_room_error_creating_room">"Pri vytváraní miestnosti došlo k chybe"</string>
|
||||
<string name="screen_create_room_private_option_description">"Do tejto miestnosti majú prístup iba pozvaní ľudia. Všetky správy sú end-to-end šifrované."</string>
|
||||
<string name="screen_create_room_error_creating_space">"Priestor sa nepodarilo vytvoriť z dôvodu neznámej chyby. Skúste to znova neskôr."</string>
|
||||
<string name="screen_create_room_name_placeholder">"Pridať názov…"</string>
|
||||
<string name="screen_create_room_new_room_title">"Nová miestnosť"</string>
|
||||
<string name="screen_create_room_new_space_title">"Nový priestor"</string>
|
||||
<string name="screen_create_room_private_option_description">"Pripojiť sa môžu iba pozvaní ľudia."</string>
|
||||
<string name="screen_create_room_private_option_title">"Súkromná"</string>
|
||||
<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>
|
||||
<string name="screen_create_room_public_option_short_description">"Pripojiť sa môže ktokoľvek."</string>
|
||||
<string name="screen_create_room_public_option_title">"Verejná miestnosť"</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_access_section_public_option_description">"Do tejto miestnosti sa môže pripojiť ktokoľvek"</string>
|
||||
<string name="screen_create_room_room_access_section_knocking_option_description">"O pripojenie sa môže požiadať ktokoľvek, ale žiadosť musí schváliť správca alebo moderátor."</string>
|
||||
<string name="screen_create_room_room_access_section_knocking_option_title">"Povoliť požiadať o vstup"</string>
|
||||
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"Ktokoľvek v %1$s sa môžu pripojiť, ale všetci ostatní musia požiadať o prístup."</string>
|
||||
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"Požiadať o pripojenie"</string>
|
||||
<string name="screen_create_room_room_access_section_private_option_description">"Pripojiť sa môžu iba pozvaní ľudia."</string>
|
||||
<string name="screen_create_room_room_access_section_private_option_title">"Súkromná"</string>
|
||||
<string name="screen_create_room_room_access_section_public_option_description">"Pripojiť sa môže ktokoľvek."</string>
|
||||
<string name="screen_create_room_room_access_section_public_option_title">"Ktokoľvek"</string>
|
||||
<string name="screen_create_room_room_access_section_restricted_option_description">"Ktokoľvek v %1$s sa môže pripojiť."</string>
|
||||
<string name="screen_create_room_room_access_section_restricted_option_title">"Štandardná"</string>
|
||||
<string name="screen_create_room_room_access_section_title">"Kto má prístup"</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_address_section_title">"Adresa"</string>
|
||||
<string name="screen_create_room_room_visibility_section_title">"Viditeľnosť miestnosti"</string>
|
||||
<string name="screen_create_room_space_selection_no_space_description">"(žiadny priestor)"</string>
|
||||
<string name="screen_create_room_space_selection_no_space_title">"Domov"</string>
|
||||
<string name="screen_create_room_space_selection_sheet_title">"Pridať do priestoru"</string>
|
||||
<string name="screen_create_room_topic_label">"Téma (voliteľné)"</string>
|
||||
<string name="screen_create_room_topic_placeholder">"Pridať popis…"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
<string name="screen_create_room_name_placeholder">"Add name…"</string>
|
||||
<string name="screen_create_room_new_room_title">"New room"</string>
|
||||
<string name="screen_create_room_new_space_title">"New space"</string>
|
||||
<string name="screen_create_room_parent_space_home_description">"(no space)"</string>
|
||||
<string name="screen_create_room_parent_space_home_title">"Home"</string>
|
||||
<string name="screen_create_room_private_option_description">"Only people invited can join."</string>
|
||||
<string name="screen_create_room_private_option_title">"Private"</string>
|
||||
<string name="screen_create_room_public_option_description">"Anyone can find this room.
|
||||
|
|
@ -15,14 +17,21 @@ You can change this anytime in room settings."</string>
|
|||
<string name="screen_create_room_public_option_title">"Public"</string>
|
||||
<string name="screen_create_room_room_access_section_knocking_option_description">"Anyone can ask to join but an administrator or a moderator must accept the request."</string>
|
||||
<string name="screen_create_room_room_access_section_knocking_option_title">"Allow ask to join"</string>
|
||||
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"Anyone in %1$s can join but everyone else must request access."</string>
|
||||
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"Ask to join"</string>
|
||||
<string name="screen_create_room_room_access_section_private_option_description">"Only people invited can join."</string>
|
||||
<string name="screen_create_room_room_access_section_private_option_title">"Private"</string>
|
||||
<string name="screen_create_room_room_access_section_public_option_description">"Anyone can join."</string>
|
||||
<string name="screen_create_room_room_access_section_public_option_title">"Public"</string>
|
||||
<string name="screen_create_room_room_access_section_restricted_option_description">"Anyone in %1$s can join."</string>
|
||||
<string name="screen_create_room_room_access_section_restricted_option_title">"Standard"</string>
|
||||
<string name="screen_create_room_room_access_section_title">"Who has access"</string>
|
||||
<string name="screen_create_room_room_address_section_footer">"You’ll need an address in order to make it visible in the public directory."</string>
|
||||
<string name="screen_create_room_room_address_section_title">"Address"</string>
|
||||
<string name="screen_create_room_room_visibility_section_title">"Room visibility"</string>
|
||||
<string name="screen_create_room_space_selection_no_space_description">"(no space)"</string>
|
||||
<string name="screen_create_room_space_selection_no_space_title">"Home"</string>
|
||||
<string name="screen_create_room_space_selection_sheet_title">"Add to space"</string>
|
||||
<string name="screen_create_room_topic_label">"Topic (optional)"</string>
|
||||
<string name="screen_create_room_topic_placeholder">"Add description…"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.startchat.impl.configureroom
|
||||
package io.element.android.features.createroom.impl
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
|
|
@ -18,24 +17,28 @@ import io.element.android.features.createroom.impl.configureroom.ConfigureRoomPr
|
|||
import io.element.android.features.createroom.impl.configureroom.ConfigureRoomState
|
||||
import io.element.android.features.createroom.impl.configureroom.CreateRoomConfig
|
||||
import io.element.android.features.createroom.impl.configureroom.CreateRoomConfigStore
|
||||
import io.element.android.features.createroom.impl.configureroom.RoomAccess
|
||||
import io.element.android.features.createroom.impl.configureroom.JoinRuleItem
|
||||
import io.element.android.features.createroom.impl.configureroom.RoomAddress
|
||||
import io.element.android.features.createroom.impl.configureroom.RoomVisibilityItem
|
||||
import io.element.android.features.createroom.impl.configureroom.RoomVisibilityState
|
||||
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.room.RoomInfo
|
||||
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
||||
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevels
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues
|
||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.element.android.libraries.matrix.test.A_MESSAGE
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||
import io.element.android.libraries.matrix.test.room.alias.FakeRoomAliasHelper
|
||||
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
|
||||
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
||||
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity
|
||||
import io.element.android.libraries.mediapickers.api.PickerProvider
|
||||
|
|
@ -47,12 +50,17 @@ 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.FakePermissionsPresenterFactory
|
||||
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.test
|
||||
import io.mockk.mockk
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
|
|
@ -81,7 +89,7 @@ class ConfigureRoomPresenterTest {
|
|||
assertThat(initialState.config.topic).isNull()
|
||||
assertThat(initialState.config.invites).isEmpty()
|
||||
assertThat(initialState.config.avatarUri).isNull()
|
||||
assertThat(initialState.config.roomVisibility).isEqualTo(RoomVisibilityState.Private)
|
||||
assertThat(initialState.config.visibilityState).isEqualTo(RoomVisibilityState.Private())
|
||||
assertThat(initialState.createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||
assertThat(initialState.homeserverName).isEqualTo("matrix.org")
|
||||
}
|
||||
|
|
@ -174,12 +182,12 @@ class ConfigureRoomPresenterTest {
|
|||
assertThat(newState.config).isEqualTo(expectedConfig)
|
||||
|
||||
// Room privacy
|
||||
newState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public))
|
||||
newState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
|
||||
newState = awaitItem()
|
||||
expectedConfig = expectedConfig.copy(
|
||||
roomVisibility = RoomVisibilityState.Public(
|
||||
visibilityState = RoomVisibilityState.Public(
|
||||
roomAddress = RoomAddress.AutoFilled(roomAliasHelper.roomAliasNameFromRoomDisplayName(expectedConfig.roomName ?: "")),
|
||||
roomAccess = RoomAccess.Anyone,
|
||||
joinRuleItem = JoinRuleItem.PublicVisibility.Public,
|
||||
)
|
||||
)
|
||||
assertThat(newState.config).isEqualTo(expectedConfig)
|
||||
|
|
@ -206,6 +214,109 @@ class ConfigureRoomPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - when creating a room in a space if the room doesn't receive the power levels value it can't be added to the space`() = runTest {
|
||||
val addChildToSpaceResult = lambdaRecorder<RoomId, RoomId, Result<Unit>> { _, _ -> Result.success(Unit) }
|
||||
val spaceService = FakeSpaceService(
|
||||
addChildToSpaceResult = addChildToSpaceResult,
|
||||
)
|
||||
val roomInfoFlow = MutableStateFlow<Optional<RoomInfo>>(Optional.empty())
|
||||
val getRoomInfoFlowLambda = lambdaRecorder<RoomId, Flow<Optional<RoomInfo>>> { roomInfoFlow }
|
||||
val matrixClient = createMatrixClient(spaceService = spaceService).apply {
|
||||
this.getRoomInfoFlowLambda = getRoomInfoFlowLambda
|
||||
}
|
||||
val presenter = createConfigureRoomPresenter(
|
||||
matrixClient = matrixClient
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = initialState()
|
||||
val createRoomResult = Result.success(RoomId("!createRoomResult:domain"))
|
||||
|
||||
matrixClient.givenCreateRoomResult(createRoomResult)
|
||||
|
||||
val parentSpace = aSpaceRoom()
|
||||
initialState.eventSink(ConfigureRoomEvents.SetParentSpace(parentSpace))
|
||||
assertThat(awaitItem().config.parentSpace).isEqualTo(parentSpace)
|
||||
|
||||
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.AskToJoin))
|
||||
assertThat(awaitItem().config.visibilityState.joinRuleItem).isEqualTo(JoinRuleItem.PublicVisibility.AskToJoin)
|
||||
|
||||
initialState.eventSink(ConfigureRoomEvents.CreateRoom)
|
||||
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
val stateAfterCreateRoom = awaitItem()
|
||||
|
||||
// getRoomInfoFlow is called, but it contains no updates
|
||||
getRoomInfoFlowLambda.assertions().isCalledOnce()
|
||||
// So adding the child room to the parent space is never done
|
||||
addChildToSpaceResult.assertions().isNeverCalled()
|
||||
|
||||
// And the operation fails
|
||||
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - creating a room and adding it into a parent space works when all the data is available`() = runTest {
|
||||
val addChildToSpaceResult = lambdaRecorder<RoomId, RoomId, Result<Unit>> { _, _ -> Result.success(Unit) }
|
||||
val spaceService = FakeSpaceService(
|
||||
addChildToSpaceResult = addChildToSpaceResult,
|
||||
)
|
||||
val roomInfoFlow = MutableStateFlow<Optional<RoomInfo>>(Optional.empty())
|
||||
val getRoomInfoFlowLambda = lambdaRecorder<RoomId, Flow<Optional<RoomInfo>>> { roomInfoFlow }
|
||||
val matrixClient = createMatrixClient(spaceService = spaceService).apply {
|
||||
this.getRoomInfoFlowLambda = getRoomInfoFlowLambda
|
||||
}
|
||||
val presenter = createConfigureRoomPresenter(
|
||||
matrixClient = matrixClient
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = initialState()
|
||||
val createRoomResult = Result.success(RoomId("!createRoomResult:domain"))
|
||||
|
||||
matrixClient.givenCreateRoomResult(createRoomResult)
|
||||
|
||||
val parentSpace = aSpaceRoom()
|
||||
initialState.eventSink(ConfigureRoomEvents.SetParentSpace(parentSpace))
|
||||
assertThat(awaitItem().config.parentSpace).isEqualTo(parentSpace)
|
||||
|
||||
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.AskToJoin))
|
||||
assertThat(awaitItem().config.visibilityState.joinRuleItem).isEqualTo(JoinRuleItem.PublicVisibility.AskToJoin)
|
||||
|
||||
initialState.eventSink(ConfigureRoomEvents.CreateRoom)
|
||||
|
||||
// We immediately receive the room power levels info needed for adding the child to a space
|
||||
val powerLevels = RoomPowerLevels(
|
||||
RoomPowerLevelsValues(
|
||||
ban = 0,
|
||||
invite = 0,
|
||||
kick = 0,
|
||||
eventsDefault = 0,
|
||||
stateDefault = 0,
|
||||
redactEvents = 0,
|
||||
roomName = 0,
|
||||
roomAvatar = 0,
|
||||
roomTopic = 0,
|
||||
spaceChild = 0
|
||||
),
|
||||
users = persistentMapOf(),
|
||||
)
|
||||
roomInfoFlow.value = Optional.of(aRoomInfo(roomPowerLevels = powerLevels))
|
||||
|
||||
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
|
||||
val stateAfterCreateRoom = awaitItem()
|
||||
|
||||
// The room info flow was read
|
||||
getRoomInfoFlowLambda.assertions().isCalledOnce()
|
||||
// Since it contained the power levels, the operation continued
|
||||
addChildToSpaceResult.assertions().isCalledOnce()
|
||||
|
||||
// And the child room was created and then added to the parent space
|
||||
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Success::class.java)
|
||||
assertThat(stateAfterCreateRoom.createRoomAction.dataOrNull()).isEqualTo(createRoomResult.getOrNull())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - record analytics when creating room`() = runTest {
|
||||
val matrixClient = createMatrixClient()
|
||||
|
|
@ -311,7 +422,7 @@ class ConfigureRoomPresenterTest {
|
|||
)
|
||||
presenter.test {
|
||||
val initialState = initialState()
|
||||
initialState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public))
|
||||
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
|
||||
skipItems(1)
|
||||
initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("invalid address"))
|
||||
skipItems(1)
|
||||
|
|
@ -331,7 +442,7 @@ class ConfigureRoomPresenterTest {
|
|||
)
|
||||
presenter.test {
|
||||
val initialState = initialState()
|
||||
initialState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public))
|
||||
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
|
||||
skipItems(1)
|
||||
initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("address"))
|
||||
skipItems(1)
|
||||
|
|
@ -351,7 +462,7 @@ class ConfigureRoomPresenterTest {
|
|||
)
|
||||
presenter.test {
|
||||
val initialState = initialState()
|
||||
initialState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public))
|
||||
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
|
||||
skipItems(1)
|
||||
initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("address"))
|
||||
skipItems(1)
|
||||
|
|
@ -362,12 +473,51 @@ class ConfigureRoomPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - when a space is selected, the selected join rule is reset to private`() = runTest {
|
||||
val presenter = createConfigureRoomPresenter()
|
||||
presenter.test {
|
||||
val initialState = initialState()
|
||||
|
||||
// First change the join rule to public
|
||||
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
|
||||
assertThat(awaitItem().config.visibilityState).isInstanceOf(RoomVisibilityState.Public::class.java)
|
||||
|
||||
// Then check changing the parent space resets it to private
|
||||
initialState.eventSink(ConfigureRoomEvents.SetParentSpace(aSpaceRoom()))
|
||||
assertThat(awaitItem().config.visibilityState).isEqualTo(RoomVisibilityState.Private())
|
||||
|
||||
// If we change the join rule back to public
|
||||
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
|
||||
assertThat(awaitItem().config.visibilityState).isInstanceOf(RoomVisibilityState.Public::class.java)
|
||||
|
||||
// Then remove the parent space, it'll be private again
|
||||
initialState.eventSink(ConfigureRoomEvents.SetParentSpace(null))
|
||||
assertThat(awaitItem().config.visibilityState).isEqualTo(RoomVisibilityState.Private())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - setting a parent space for a space currently throws an error`() = runTest {
|
||||
val presenter = createConfigureRoomPresenter(isSpace = true)
|
||||
presenter.test {
|
||||
val initialState = initialState()
|
||||
|
||||
initialState.eventSink(ConfigureRoomEvents.SetParentSpace(aSpaceRoom()))
|
||||
|
||||
assertThat(awaitError())
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun TurbineTestContext<ConfigureRoomState>.initialState(): ConfigureRoomState {
|
||||
skipItems(1)
|
||||
return awaitItem()
|
||||
}
|
||||
|
||||
private fun createMatrixClient(isAliasAvailable: Boolean = true) = FakeMatrixClient(
|
||||
private fun createMatrixClient(
|
||||
isAliasAvailable: Boolean = true,
|
||||
spaceService: FakeSpaceService = FakeSpaceService(),
|
||||
) = FakeMatrixClient(
|
||||
userIdServerNameLambda = { "matrix.org" },
|
||||
resolveRoomAliasResult = {
|
||||
val resolvedRoomAlias = if (isAliasAvailable) {
|
||||
|
|
@ -376,11 +526,13 @@ class ConfigureRoomPresenterTest {
|
|||
Optional.of(ResolvedRoomAlias(A_ROOM_ID, emptyList()))
|
||||
}
|
||||
Result.success(resolvedRoomAlias)
|
||||
}
|
||||
},
|
||||
spaceService = spaceService,
|
||||
)
|
||||
|
||||
private fun createConfigureRoomPresenter(
|
||||
isSpace: Boolean = false,
|
||||
initialParenSpaceId: RoomId? = null,
|
||||
roomAliasHelper: RoomAliasHelper = FakeRoomAliasHelper(),
|
||||
dataStore: CreateRoomConfigStore = CreateRoomConfigStore(roomAliasHelper),
|
||||
matrixClient: MatrixClient = createMatrixClient(),
|
||||
|
|
@ -392,6 +544,7 @@ class ConfigureRoomPresenterTest {
|
|||
mediaOptimizationConfigProvider: FakeMediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(),
|
||||
) = ConfigureRoomPresenter(
|
||||
isSpace = isSpace,
|
||||
initialParentSpaceId = initialParenSpaceId,
|
||||
dataStore = dataStore,
|
||||
matrixClient = matrixClient,
|
||||
mediaPickerProvider = pickerProvider,
|
||||
|
|
@ -14,6 +14,7 @@ import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
|
|||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.node.TestParentNode
|
||||
import org.junit.Rule
|
||||
|
|
@ -36,15 +37,16 @@ class DefaultCreateRoomEntryPointTest {
|
|||
plugins = plugins,
|
||||
)
|
||||
}
|
||||
val buildContext = BuildContext.root(null)
|
||||
|
||||
val callback = object : CreateRoomEntryPoint.Callback {
|
||||
override fun onRoomCreated(roomId: RoomId) = lambdaError()
|
||||
}
|
||||
val result = entryPoint.createNode(
|
||||
isSpace = false,
|
||||
parentNode = parentNode,
|
||||
buildContext = BuildContext.root(null),
|
||||
callback = callback,
|
||||
)
|
||||
val result = entryPoint
|
||||
.builder(parentNode, buildContext, callback)
|
||||
.setIsSpace(true)
|
||||
.setParentSpace(A_ROOM_ID)
|
||||
.build()
|
||||
assertThat(result.plugins).contains(callback)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.createroom.impl
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.createroom.impl.configureroom.JoinRuleItem
|
||||
import io.element.android.libraries.matrix.api.room.join.AllowRule
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.junit.Test
|
||||
|
||||
class JoinRuleItemTest {
|
||||
@Test
|
||||
fun `toJoinRule works as expected`() {
|
||||
assertThat(JoinRuleItem.Private.toJoinRule()).isEqualTo(JoinRule.Private)
|
||||
assertThat(JoinRuleItem.PublicVisibility.Public.toJoinRule()).isEqualTo(JoinRule.Public)
|
||||
assertThat(JoinRuleItem.PublicVisibility.AskToJoin.toJoinRule()).isEqualTo(JoinRule.Knock)
|
||||
assertThat(JoinRuleItem.PublicVisibility.Restricted(A_ROOM_ID).toJoinRule())
|
||||
.isEqualTo(JoinRule.Restricted(persistentListOf(AllowRule.RoomMembership(A_ROOM_ID))))
|
||||
assertThat(JoinRuleItem.PublicVisibility.AskToJoinRestricted(A_ROOM_ID).toJoinRule())
|
||||
.isEqualTo(JoinRule.KnockRestricted(persistentListOf(AllowRule.RoomMembership(A_ROOM_ID))))
|
||||
}
|
||||
}
|
||||
|
|
@ -16,5 +16,6 @@ android {
|
|||
dependencies {
|
||||
implementation(projects.features.createroom.api)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.tests.testutils)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,13 +10,19 @@ package io.element.android.features.createroom.api
|
|||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeCreateRoomEntryPoint : CreateRoomEntryPoint {
|
||||
override fun createNode(
|
||||
isSpace: Boolean,
|
||||
class Builder : CreateRoomEntryPoint.Builder {
|
||||
override fun setIsSpace(isSpace: Boolean): Builder = this
|
||||
override fun setParentSpace(parentSpaceId: RoomId): Builder = this
|
||||
override fun build(): Node = lambdaError()
|
||||
}
|
||||
|
||||
override fun builder(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
callback: CreateRoomEntryPoint.Callback,
|
||||
): Node = lambdaError()
|
||||
): Builder = lambdaError()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,9 +94,9 @@ class HomePresenter(
|
|||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(homeSpacesState.spaceRooms.isEmpty()) {
|
||||
// If the last space is left, ensure that the Chat view is rendered.
|
||||
if (homeSpacesState.spaceRooms.isEmpty()) {
|
||||
LaunchedEffect(homeSpacesState.canCreateSpaces, homeSpacesState.spaceRooms.isEmpty()) {
|
||||
// If the flag to create spaces is disabled and the last space is left, ensure that the Chat view is rendered.
|
||||
if (!homeSpacesState.canCreateSpaces && homeSpacesState.spaceRooms.isEmpty()) {
|
||||
currentHomeNavigationBarItemOrdinal = HomeNavigationBarItem.Chats.ordinal
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,5 +33,5 @@ data class HomeState(
|
|||
) {
|
||||
val displayActions = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats
|
||||
val displayRoomListFilters = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats && roomListState.displayFilters
|
||||
val showNavigationBar = homeSpacesState.spaceRooms.isNotEmpty()
|
||||
val showNavigationBar = homeSpacesState.canCreateSpaces || homeSpacesState.spaceRooms.isNotEmpty()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -268,7 +268,10 @@ private fun HomeScaffold(
|
|||
lazyListState = spacesLazyListState,
|
||||
onSpaceClick = { spaceId ->
|
||||
onRoomClick(spaceId)
|
||||
}
|
||||
},
|
||||
onCreateSpaceClick = onCreateSpaceClick,
|
||||
// TODO use actual callbacks for this
|
||||
onExploreClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@
|
|||
|
||||
package io.element.android.features.home.impl.search
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.features.home.impl.datasource.RoomListRoomSummaryFactory
|
||||
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
|
|
@ -18,6 +20,7 @@ import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
|||
import io.element.android.libraries.matrix.api.roomlist.loadAllIncrementally
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
|
|
@ -25,16 +28,23 @@ import kotlinx.coroutines.flow.map
|
|||
|
||||
private const val PAGE_SIZE = 30
|
||||
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class RoomListSearchDataSource(
|
||||
@Assisted coroutineScope: CoroutineScope,
|
||||
roomListService: RoomListService,
|
||||
coroutineDispatchers: CoroutineDispatchers,
|
||||
private val roomSummaryFactory: RoomListRoomSummaryFactory,
|
||||
) {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(coroutineScope: CoroutineScope): RoomListSearchDataSource
|
||||
}
|
||||
|
||||
private val roomList = roomListService.createRoomList(
|
||||
pageSize = PAGE_SIZE,
|
||||
initialFilter = RoomListFilter.None,
|
||||
source = RoomList.Source.All,
|
||||
coroutineScope = coroutineScope
|
||||
)
|
||||
|
||||
val roomSummaries: Flow<ImmutableList<RoomListRoomSummary>> = roomList.filteredSummaries
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import androidx.compose.runtime.collectAsState
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
|
|
@ -23,7 +24,7 @@ import kotlinx.collections.immutable.persistentListOf
|
|||
|
||||
@Inject
|
||||
class RoomListSearchPresenter(
|
||||
private val dataSource: RoomListSearchDataSource,
|
||||
private val dataSourceFactory: RoomListSearchDataSource.Factory,
|
||||
) : Presenter<RoomListSearchState> {
|
||||
@Composable
|
||||
override fun present(): RoomListSearchState {
|
||||
|
|
@ -33,6 +34,9 @@ class RoomListSearchPresenter(
|
|||
}
|
||||
val searchQuery = rememberTextFieldState()
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val dataSource = remember { dataSourceFactory.create(coroutineScope) }
|
||||
|
||||
LaunchedEffect(isSearchActive) {
|
||||
dataSource.setIsActive(isSearchActive)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ class HomeSpacesPresenter(
|
|||
seenSpaceInvites = seenSpaceInvites,
|
||||
hideInvitesAvatar = hideInvitesAvatar,
|
||||
canCreateSpaces = canCreateSpaces,
|
||||
// TODO enable once we can link to the screen to explore public spaces
|
||||
canExploreSpaces = false,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ data class HomeSpacesState(
|
|||
val seenSpaceInvites: ImmutableSet<RoomId>,
|
||||
val hideInvitesAvatar: Boolean,
|
||||
val canCreateSpaces: Boolean,
|
||||
val canExploreSpaces: Boolean,
|
||||
val eventSink: (HomeSpacesEvents) -> Unit,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,11 @@ open class HomeSpacesStateProvider : PreviewParameterProvider<HomeSpacesState> {
|
|||
spaceRooms = aListOfSpaceRooms(),
|
||||
canCreateSpaces = false,
|
||||
),
|
||||
aHomeSpacesState(
|
||||
space = CurrentSpace.Root,
|
||||
spaceRooms = emptyList(),
|
||||
canCreateSpaces = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -46,6 +51,7 @@ internal fun aHomeSpacesState(
|
|||
seenSpaceInvites: Set<RoomId> = emptySet(),
|
||||
hideInvitesAvatar: Boolean = false,
|
||||
canCreateSpaces: Boolean = true,
|
||||
canExploreSpaces: Boolean = true,
|
||||
eventSink: (HomeSpacesEvents) -> Unit = {},
|
||||
) = HomeSpacesState(
|
||||
space = space,
|
||||
|
|
@ -53,6 +59,7 @@ internal fun aHomeSpacesState(
|
|||
seenSpaceInvites = seenSpaceInvites.toImmutableSet(),
|
||||
hideInvitesAvatar = hideInvitesAvatar,
|
||||
canCreateSpaces = canCreateSpaces,
|
||||
canExploreSpaces = canExploreSpaces,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,23 +8,40 @@
|
|||
|
||||
package io.element.android.features.home.impl.spaces
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.ui.components.SpaceHeaderRootView
|
||||
import io.element.android.libraries.matrix.ui.components.SpaceHeaderView
|
||||
import io.element.android.libraries.matrix.ui.components.SpaceRoomItemView
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
|
|
@ -32,56 +49,119 @@ fun HomeSpacesView(
|
|||
state: HomeSpacesState,
|
||||
lazyListState: LazyListState,
|
||||
onSpaceClick: (RoomId) -> Unit,
|
||||
onCreateSpaceClick: () -> Unit,
|
||||
onExploreClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = modifier,
|
||||
state = lazyListState
|
||||
) {
|
||||
val space = state.space
|
||||
when (space) {
|
||||
CurrentSpace.Root -> {
|
||||
item {
|
||||
SpaceHeaderRootView(numberOfSpaces = state.spaceRooms.size)
|
||||
if (state.canCreateSpaces && state.spaceRooms.isEmpty()) {
|
||||
EmptySpaceHomeView(
|
||||
modifier = modifier,
|
||||
onCreateSpaceClick = onCreateSpaceClick,
|
||||
onExploreClick = onExploreClick,
|
||||
canExploreSpaces = state.canExploreSpaces,
|
||||
)
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = modifier,
|
||||
state = lazyListState
|
||||
) {
|
||||
val space = state.space
|
||||
when (space) {
|
||||
CurrentSpace.Root -> {
|
||||
item {
|
||||
SpaceHeaderRootView(numberOfSpaces = state.spaceRooms.size)
|
||||
}
|
||||
}
|
||||
is CurrentSpace.Space -> {
|
||||
item {
|
||||
SpaceHeaderView(
|
||||
avatarData = space.spaceRoom.getAvatarData(AvatarSize.SpaceHeader),
|
||||
name = space.spaceRoom.displayName,
|
||||
topic = space.spaceRoom.topic,
|
||||
visibility = space.spaceRoom.visibility,
|
||||
heroes = space.spaceRoom.heroes.toImmutableList(),
|
||||
numberOfMembers = space.spaceRoom.numJoinedMembers,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is CurrentSpace.Space -> item {
|
||||
SpaceHeaderView(
|
||||
avatarData = space.spaceRoom.getAvatarData(AvatarSize.SpaceHeader),
|
||||
name = space.spaceRoom.displayName,
|
||||
topic = space.spaceRoom.topic,
|
||||
visibility = space.spaceRoom.visibility,
|
||||
heroes = space.spaceRoom.heroes.toImmutableList(),
|
||||
numberOfMembers = space.spaceRoom.numJoinedMembers,
|
||||
)
|
||||
}
|
||||
}
|
||||
item {
|
||||
HorizontalDivider()
|
||||
}
|
||||
itemsIndexed(
|
||||
items = state.spaceRooms,
|
||||
key = { _, spaceRoom -> spaceRoom.roomId }
|
||||
) { index, spaceRoom ->
|
||||
val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED
|
||||
SpaceRoomItemView(
|
||||
spaceRoom = spaceRoom,
|
||||
showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites,
|
||||
hideAvatars = isInvitation && state.hideInvitesAvatar,
|
||||
onClick = {
|
||||
onSpaceClick(spaceRoom.roomId)
|
||||
},
|
||||
onLongClick = {
|
||||
// TODO
|
||||
},
|
||||
)
|
||||
if (index != state.spaceRooms.lastIndex) {
|
||||
|
||||
item {
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
itemsIndexed(
|
||||
items = state.spaceRooms,
|
||||
key = { _, spaceRoom -> spaceRoom.roomId }
|
||||
) { index, spaceRoom ->
|
||||
val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED
|
||||
SpaceRoomItemView(
|
||||
spaceRoom = spaceRoom,
|
||||
showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites,
|
||||
hideAvatars = isInvitation && state.hideInvitesAvatar,
|
||||
onClick = {
|
||||
onSpaceClick(spaceRoom.roomId)
|
||||
},
|
||||
onLongClick = {
|
||||
// TODO
|
||||
},
|
||||
)
|
||||
if (index != state.spaceRooms.lastIndex) {
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmptySpaceHomeView(
|
||||
onCreateSpaceClick: () -> Unit,
|
||||
onExploreClick: () -> Unit,
|
||||
canExploreSpaces: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
HeaderFooterPage(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 32.dp, bottom = 16.dp, start = 40.dp, end = 40.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
BigIcon(
|
||||
style = BigIcon.Style.Default(CompoundIcons.SpaceSolid())
|
||||
)
|
||||
Text(
|
||||
text = stringResource(CommonStrings.screen_space_list_empty_state_title),
|
||||
style = ElementTheme.typography.fontHeadingLgBold,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
},
|
||||
footer = {
|
||||
ButtonColumnMolecule {
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResource(CommonStrings.action_create_space),
|
||||
onClick = onCreateSpaceClick,
|
||||
)
|
||||
if (canExploreSpaces) {
|
||||
TextButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResource(CommonStrings.action_explore_public_spaces),
|
||||
onClick = onExploreClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun HomeSpacesViewPreview(
|
||||
|
|
@ -91,6 +171,7 @@ internal fun HomeSpacesViewPreview(
|
|||
state = state,
|
||||
lazyListState = rememberLazyListState(),
|
||||
onSpaceClick = {},
|
||||
modifier = Modifier,
|
||||
onCreateSpaceClick = {},
|
||||
onExploreClick = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ Du har ingen ulæste beskeder!"</string>
|
|||
<string name="screen_roomlist_mark_as_read">"Marker som læst"</string>
|
||||
<string name="screen_roomlist_mark_as_unread">"Marker som ulæst"</string>
|
||||
<string name="screen_roomlist_tombstoned_room_description">"Dette rum er blevet opgraderet"</string>
|
||||
<string name="screen_roomlist_your_spaces">"Dine grupper"</string>
|
||||
<string name="session_verification_banner_message">"Det ser ud til, at du bruger en ny enhed. Bekræft med en anden enhed for at få adgang til dine krypterede meddelelser."</string>
|
||||
<string name="session_verification_banner_title">"Bekræft, at det er dig"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ Du hast keine ungelesenen Nachrichten!"</string>
|
|||
<string name="screen_roomlist_mark_as_read">"Als gelesen markieren"</string>
|
||||
<string name="screen_roomlist_mark_as_unread">"Als ungelesen markieren"</string>
|
||||
<string name="screen_roomlist_tombstoned_room_description">"Die Chat-Version wurde aktualisiert"</string>
|
||||
<string name="screen_roomlist_your_spaces">"Deine Spaces"</string>
|
||||
<string name="session_verification_banner_message">"Es sieht aus, als würdest du ein neues Gerät verwenden. Verifiziere es mit einem anderen Gerät, damit du auf deine verschlüsselten Nachrichten zugreifen kannst."</string>
|
||||
<string name="session_verification_banner_title">"Verifiziere deine Identität"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ Vous n’avez plus de messages non-lus !"</string>
|
|||
<string name="screen_roomlist_mark_as_read">"Marquer comme lu"</string>
|
||||
<string name="screen_roomlist_mark_as_unread">"Marquer comme non lu"</string>
|
||||
<string name="screen_roomlist_tombstoned_room_description">"Ce salon a été mis à niveau."</string>
|
||||
<string name="screen_roomlist_your_spaces">"Vos espaces"</string>
|
||||
<string name="session_verification_banner_message">"Il semblerait que vous utilisiez un nouvel appareil. Vérifiez la session avec un autre de vos appareils pour accéder à vos messages chiffrés."</string>
|
||||
<string name="session_verification_banner_title">"Vérifier que c’est bien vous"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ Nincs olvasatlan üzenete!"</string>
|
|||
<string name="screen_roomlist_mark_as_read">"Megjelölés olvasottként"</string>
|
||||
<string name="screen_roomlist_mark_as_unread">"Megjelölés olvasatlanként"</string>
|
||||
<string name="screen_roomlist_tombstoned_room_description">"A szoba verzióját frissítették"</string>
|
||||
<string name="screen_roomlist_your_spaces">"Saját terek"</string>
|
||||
<string name="session_verification_banner_message">"Úgy tűnik, hogy új eszközt használ. Ellenőrizze egy másik eszközzel, hogy a továbbiakban elérje a titkosított üzeneteket."</string>
|
||||
<string name="session_verification_banner_title">"Ellenőrizze, hogy Ön az"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ Você não tem nenhuma mensagem não lida!"</string>
|
|||
<string name="screen_roomlist_mark_as_read">"Marcar como lida"</string>
|
||||
<string name="screen_roomlist_mark_as_unread">"Marcar como não lida"</string>
|
||||
<string name="screen_roomlist_tombstoned_room_description">"Esta sala foi atualizada"</string>
|
||||
<string name="screen_roomlist_your_spaces">"Seus espaços"</string>
|
||||
<string name="session_verification_banner_message">"Parece que você está usando um novo dispositivo. Verifique com outro dispositivo para acessar suas mensagens criptografadas."</string>
|
||||
<string name="session_verification_banner_title">"Verifique se é você"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ Nemáte žiadne neprečítané správy!"</string>
|
|||
<string name="screen_roomlist_mark_as_read">"Označiť ako prečítané"</string>
|
||||
<string name="screen_roomlist_mark_as_unread">"Označiť ako neprečítané"</string>
|
||||
<string name="screen_roomlist_tombstoned_room_description">"Táto miestnosť bola aktualizovaná"</string>
|
||||
<string name="screen_roomlist_your_spaces">"Vaše priestory"</string>
|
||||
<string name="session_verification_banner_message">"Vyzerá to tak, že používate nové zariadenie. Overte svoj prístup k zašifrovaným správam pomocou vášho druhého zariadenia."</string>
|
||||
<string name="session_verification_banner_title">"Overte, že ste to vy"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ You don’t have any unread messages!"</string>
|
|||
<string name="screen_roomlist_mark_as_read">"Mark as read"</string>
|
||||
<string name="screen_roomlist_mark_as_unread">"Mark as unread"</string>
|
||||
<string name="screen_roomlist_tombstoned_room_description">"This room has been upgraded"</string>
|
||||
<string name="screen_roomlist_your_spaces">"Your spaces"</string>
|
||||
<string name="session_verification_banner_message">"Looks like you’re using a new device. Verify with another device to access your encrypted messages."</string>
|
||||
<string name="session_verification_banner_title">"Verify it’s you"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ class HomePresenterTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `present - NavigationBar is hidden when the last space is left`() = runTest {
|
||||
fun `present - NavigationBar is hidden when the last space is left when the user can't create new spaces`() = runTest {
|
||||
val homeSpacesPresenter = MutablePresenter(aHomeSpacesState())
|
||||
val presenter = createHomePresenter(
|
||||
sessionStore = InMemorySessionStore(
|
||||
|
|
@ -193,7 +193,7 @@ class HomePresenterTest {
|
|||
val spaceState = awaitItem()
|
||||
assertThat(spaceState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Spaces)
|
||||
// The last space is left
|
||||
homeSpacesPresenter.updateState(aHomeSpacesState(spaceRooms = emptyList()))
|
||||
homeSpacesPresenter.updateState(aHomeSpacesState(spaceRooms = emptyList(), canCreateSpaces = false))
|
||||
skipItems(1)
|
||||
val finalState = awaitItem()
|
||||
// We are back to Chats
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
|||
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
|
@ -122,13 +123,18 @@ fun TestScope.createRoomListSearchPresenter(
|
|||
roomListService: RoomListService = FakeRoomListService(),
|
||||
): RoomListSearchPresenter {
|
||||
return RoomListSearchPresenter(
|
||||
dataSource = RoomListSearchDataSource(
|
||||
roomListService = roomListService,
|
||||
roomSummaryFactory = aRoomListRoomSummaryFactory(
|
||||
dateFormatter = FakeDateFormatter(),
|
||||
roomLatestEventFormatter = FakeRoomLatestEventFormatter(),
|
||||
),
|
||||
coroutineDispatchers = testCoroutineDispatchers(),
|
||||
),
|
||||
dataSourceFactory = object : RoomListSearchDataSource.Factory {
|
||||
override fun create(coroutineScope: CoroutineScope): RoomListSearchDataSource {
|
||||
return RoomListSearchDataSource(
|
||||
roomListService = roomListService,
|
||||
roomSummaryFactory = aRoomListRoomSummaryFactory(
|
||||
dateFormatter = FakeDateFormatter(),
|
||||
roomLatestEventFormatter = FakeRoomLatestEventFormatter(),
|
||||
),
|
||||
coroutineDispatchers = testCoroutineDispatchers(),
|
||||
coroutineScope = coroutineScope,
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,5 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
|
|||
|
||||
sealed interface DefaultInvitePeopleEvents : InvitePeopleEvents {
|
||||
data class ToggleUser(val user: MatrixUser) : DefaultInvitePeopleEvents
|
||||
data class UpdateSearchQuery(val query: String) : DefaultInvitePeopleEvents
|
||||
data class OnSearchActiveChanged(val active: Boolean) : DefaultInvitePeopleEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,12 @@
|
|||
|
||||
package io.element.android.features.invitepeople.impl
|
||||
|
||||
import androidx.compose.foundation.text.input.clearText
|
||||
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
|
|
@ -39,6 +42,7 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom
|
|||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
import io.element.android.libraries.matrix.api.room.filterMembers
|
||||
import io.element.android.libraries.matrix.api.room.recent.getRecentDirectRooms
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.libraries.usersearch.api.UserRepository
|
||||
|
|
@ -47,11 +51,16 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.filterNot
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.take
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
private const val MAX_SUGGESTIONS_COUNT = 5
|
||||
|
||||
@AssistedInject
|
||||
class DefaultInvitePeoplePresenter(
|
||||
@Assisted private val joinedRoom: JoinedRoom?,
|
||||
|
|
@ -73,11 +82,39 @@ class DefaultInvitePeoplePresenter(
|
|||
val roomMembers = remember { mutableStateOf<AsyncData<ImmutableList<RoomMember>>>(AsyncData.Loading()) }
|
||||
val selectedUsers = remember { mutableStateOf<ImmutableList<MatrixUser>>(persistentListOf()) }
|
||||
val searchResults = remember { mutableStateOf<SearchBarResultState<ImmutableList<InvitableUser>>>(SearchBarResultState.Initial()) }
|
||||
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||
val queryState = rememberTextFieldState()
|
||||
var searchActive by rememberSaveable { mutableStateOf(false) }
|
||||
val showSearchLoader = rememberSaveable { mutableStateOf(false) }
|
||||
val sendInvitesAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||
|
||||
val recentDirectRooms by produceState(emptyList(), roomMembers.value) {
|
||||
if (roomMembers.value.isSuccess()) {
|
||||
val activeMemberIds = roomMembers.value.dataOrNull().orEmpty()
|
||||
.filter { it.membership.isActive() }
|
||||
.mapTo(mutableSetOf()) { it.userId }
|
||||
|
||||
value = matrixClient.getRecentDirectRooms()
|
||||
.filterNot { it.matrixUser.userId in activeMemberIds }
|
||||
.take(MAX_SUGGESTIONS_COUNT)
|
||||
.toList()
|
||||
}
|
||||
}
|
||||
|
||||
// Convert recent direct rooms to InvitableUser for display
|
||||
val suggestions by remember {
|
||||
derivedStateOf {
|
||||
recentDirectRooms.map { recentDirectRoom ->
|
||||
InvitableUser(
|
||||
matrixUser = recentDirectRoom.matrixUser,
|
||||
isSelected = recentDirectRoom.matrixUser in selectedUsers.value,
|
||||
isAlreadyJoined = false,
|
||||
isAlreadyInvited = false,
|
||||
isUnresolved = false,
|
||||
)
|
||||
}.toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
val room by produceState(if (joinedRoom != null) AsyncData.Success(joinedRoom) else AsyncData.Loading()) {
|
||||
if (joinedRoom == null) {
|
||||
val result = matrixClient.getJoinedRoom(roomId)
|
||||
|
|
@ -94,6 +131,7 @@ class DefaultInvitePeoplePresenter(
|
|||
fetchMembers(it, roomMembers)
|
||||
}
|
||||
}
|
||||
val searchQuery = queryState.text.toString()
|
||||
LaunchedEffect(searchQuery, roomMembers) {
|
||||
performSearch(
|
||||
searchResults = searchResults,
|
||||
|
|
@ -108,16 +146,15 @@ class DefaultInvitePeoplePresenter(
|
|||
when (event) {
|
||||
is DefaultInvitePeopleEvents.OnSearchActiveChanged -> {
|
||||
searchActive = event.active
|
||||
searchQuery = ""
|
||||
}
|
||||
|
||||
is DefaultInvitePeopleEvents.UpdateSearchQuery -> {
|
||||
searchQuery = event.query
|
||||
if (!event.active) {
|
||||
queryState.clearText()
|
||||
}
|
||||
}
|
||||
|
||||
is DefaultInvitePeopleEvents.ToggleUser -> {
|
||||
selectedUsers.toggleUser(event.user)
|
||||
searchResults.toggleUser(event.user)
|
||||
// suggestions will automatically update via derivedStateOf when selectedUsers changes
|
||||
}
|
||||
is InvitePeopleEvents.SendInvites -> {
|
||||
room.dataOrNull()?.let {
|
||||
|
|
@ -126,7 +163,7 @@ class DefaultInvitePeoplePresenter(
|
|||
}
|
||||
is InvitePeopleEvents.CloseSearch -> {
|
||||
searchActive = false
|
||||
searchQuery = ""
|
||||
queryState.clearText()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -135,11 +172,12 @@ class DefaultInvitePeoplePresenter(
|
|||
room = room.map { },
|
||||
canInvite = selectedUsers.value.isNotEmpty() && !sendInvitesAction.value.isLoading(),
|
||||
selectedUsers = selectedUsers.value,
|
||||
searchQuery = searchQuery,
|
||||
searchQuery = queryState,
|
||||
isSearchActive = searchActive,
|
||||
searchResults = searchResults.value,
|
||||
showSearchLoader = showSearchLoader.value,
|
||||
sendInvitesAction = sendInvitesAction.value,
|
||||
suggestions = suggestions,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
package io.element.android.features.invitepeople.impl
|
||||
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import io.element.android.features.invitepeople.api.InvitePeopleEvents
|
||||
import io.element.android.features.invitepeople.api.InvitePeopleState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
|
|
@ -19,11 +20,12 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
data class DefaultInvitePeopleState(
|
||||
val room: AsyncData<Unit>,
|
||||
override val canInvite: Boolean,
|
||||
val searchQuery: String,
|
||||
val searchQuery: TextFieldState,
|
||||
val showSearchLoader: Boolean,
|
||||
val searchResults: SearchBarResultState<ImmutableList<InvitableUser>>,
|
||||
val selectedUsers: ImmutableList<MatrixUser>,
|
||||
override val isSearchActive: Boolean,
|
||||
override val sendInvitesAction: AsyncAction<Unit>,
|
||||
val suggestions: ImmutableList<InvitableUser>,
|
||||
override val eventSink: (InvitePeopleEvents) -> Unit
|
||||
) : InvitePeopleState
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
package io.element.android.features.invitepeople.impl
|
||||
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
|
|
@ -101,16 +102,20 @@ private fun aDefaultInvitePeopleState(
|
|||
isSearchActive: Boolean = false,
|
||||
showSearchLoader: Boolean = false,
|
||||
sendInvitesAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
suggestions: List<InvitableUser> = aMatrixUserList()
|
||||
.take(5)
|
||||
.map { user -> anInvitableUser(matrixUser = user, isSelected = user in selectedUsers) },
|
||||
): DefaultInvitePeopleState {
|
||||
return DefaultInvitePeopleState(
|
||||
room = room,
|
||||
canInvite = canInvite,
|
||||
searchQuery = searchQuery,
|
||||
searchQuery = TextFieldState(initialText = searchQuery),
|
||||
searchResults = searchResults,
|
||||
selectedUsers = selectedUsers,
|
||||
isSearchActive = isSearchActive,
|
||||
showSearchLoader = showSearchLoader,
|
||||
sendInvitesAction = sendInvitesAction,
|
||||
suggestions = suggestions.toImmutableList(),
|
||||
eventSink = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -31,6 +32,8 @@ import io.element.android.libraries.designsystem.components.async.AsyncLoading
|
|||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
import io.element.android.libraries.designsystem.theme.components.ListSectionHeader
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBar
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
|
|
@ -82,9 +85,13 @@ private fun InvitePeopleContentView(
|
|||
modifier = modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
fun toggleUser(user: MatrixUser) {
|
||||
state.eventSink(DefaultInvitePeopleEvents.ToggleUser(user))
|
||||
}
|
||||
|
||||
InvitePeopleSearchBar(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
query = state.searchQuery,
|
||||
queryState = state.searchQuery,
|
||||
showLoader = state.showSearchLoader,
|
||||
selectedUsers = state.selectedUsers,
|
||||
state = state.searchResults,
|
||||
|
|
@ -96,18 +103,45 @@ private fun InvitePeopleContentView(
|
|||
)
|
||||
)
|
||||
},
|
||||
onTextChange = { state.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery(it)) },
|
||||
onToggleUser = { state.eventSink(DefaultInvitePeopleEvents.ToggleUser(it)) },
|
||||
onToggleUser = ::toggleUser,
|
||||
)
|
||||
|
||||
if (!state.isSearchActive) {
|
||||
SelectedUsersRowList(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
selectedUsers = state.selectedUsers,
|
||||
autoScroll = true,
|
||||
onUserRemove = { state.eventSink(DefaultInvitePeopleEvents.ToggleUser(it)) },
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
)
|
||||
if (state.selectedUsers.isNotEmpty()) {
|
||||
SelectedUsersRowList(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
selectedUsers = state.selectedUsers,
|
||||
autoScroll = true,
|
||||
onUserRemove = ::toggleUser,
|
||||
contentPadding = PaddingValues(all = 16.dp),
|
||||
)
|
||||
}
|
||||
if (state.suggestions.isNotEmpty()) {
|
||||
LazyColumn {
|
||||
item {
|
||||
ListSectionHeader(
|
||||
title = stringResource(id = CommonStrings.common_suggestions),
|
||||
hasDivider = false,
|
||||
)
|
||||
}
|
||||
itemsIndexed(state.suggestions) { index, invitableUser ->
|
||||
CheckableUserRow(
|
||||
checked = invitableUser.isSelected,
|
||||
onCheckedChange = {
|
||||
state.eventSink(DefaultInvitePeopleEvents.ToggleUser(invitableUser.matrixUser))
|
||||
},
|
||||
data = CheckableUserRowData.Resolved(
|
||||
avatarData = invitableUser.matrixUser.getAvatarData(AvatarSize.UserListItem),
|
||||
name = invitableUser.matrixUser.getBestName(),
|
||||
subtext = invitableUser.matrixUser.userId.value,
|
||||
),
|
||||
)
|
||||
if (index < state.suggestions.lastIndex) {
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -115,20 +149,18 @@ private fun InvitePeopleContentView(
|
|||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun InvitePeopleSearchBar(
|
||||
query: String,
|
||||
queryState: TextFieldState,
|
||||
state: SearchBarResultState<ImmutableList<InvitableUser>>,
|
||||
showLoader: Boolean,
|
||||
selectedUsers: ImmutableList<MatrixUser>,
|
||||
active: Boolean,
|
||||
onActiveChange: (Boolean) -> Unit,
|
||||
onTextChange: (String) -> Unit,
|
||||
onToggleUser: (MatrixUser) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
placeHolderTitle: String = stringResource(CommonStrings.common_search_for_someone),
|
||||
) {
|
||||
SearchBar(
|
||||
query = query,
|
||||
onQueryChange = onTextChange,
|
||||
queryState = queryState,
|
||||
active = active,
|
||||
onActiveChange = onActiveChange,
|
||||
modifier = modifier,
|
||||
|
|
@ -140,7 +172,7 @@ private fun InvitePeopleSearchBar(
|
|||
selectedUsers = selectedUsers,
|
||||
autoScroll = true,
|
||||
onUserRemove = onToggleUser,
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
contentPadding = PaddingValues(all = 16.dp),
|
||||
)
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
package io.element.android.features.invitepeople.impl
|
||||
|
||||
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||
import app.cash.turbine.ReceiveTurbine
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.invitepeople.api.InvitePeopleEvents
|
||||
|
|
@ -17,6 +18,7 @@ import io.element.android.libraries.designsystem.theme.components.SearchBarResul
|
|||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembersState
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
|
|
@ -26,7 +28,9 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
|
|||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||
import io.element.android.libraries.matrix.test.room.aRoomMember
|
||||
import io.element.android.libraries.matrix.test.room.aRoomMemberList
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
|
|
@ -65,31 +69,34 @@ internal class DefaultInvitePeoplePresenterTest {
|
|||
assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java)
|
||||
assertThat(initialState.isSearchActive).isFalse()
|
||||
assertThat(initialState.canInvite).isFalse()
|
||||
assertThat(initialState.searchQuery).isEmpty()
|
||||
assertThat(initialState.searchQuery.text.toString()).isEmpty()
|
||||
|
||||
skipItems(1)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - updates search active state`() = runTest {
|
||||
val presenter = createDefaultInvitePeoplePresenter()
|
||||
val presenter = createDefaultInvitePeoplePresenter(
|
||||
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
skipItems(1)
|
||||
|
||||
initialState.eventSink(DefaultInvitePeopleEvents.OnSearchActiveChanged(true))
|
||||
|
||||
val resultState = awaitItem()
|
||||
val resultState = awaitItemAsDefault()
|
||||
assertThat(resultState.isSearchActive).isTrue()
|
||||
resultState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query"))
|
||||
assertThat(awaitItemAsDefault().searchQuery).isEqualTo("some query")
|
||||
resultState.searchQuery.setTextAndPlaceCursorAtEnd("some query")
|
||||
assertThat(awaitItemAsDefault().searchQuery.text.toString()).isEqualTo("some query")
|
||||
resultState.eventSink(InvitePeopleEvents.CloseSearch)
|
||||
skipItems(1)
|
||||
skipItems(2)
|
||||
awaitItemAsDefault().also {
|
||||
assertThat(it.isSearchActive).isFalse()
|
||||
assertThat(it.searchQuery).isEmpty()
|
||||
assertThat(it.searchQuery.text.toString()).isEmpty()
|
||||
}
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -101,8 +108,8 @@ internal class DefaultInvitePeoplePresenterTest {
|
|||
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query"))
|
||||
val initialState = awaitItemAsDefault()
|
||||
initialState.searchQuery.setTextAndPlaceCursorAtEnd("some query")
|
||||
assertThat(repository.providedQuery).isEqualTo("some query")
|
||||
repository.emitState(UserSearchResultState(results = emptyList(), isSearching = true))
|
||||
skipItems(3)
|
||||
|
|
@ -126,10 +133,10 @@ internal class DefaultInvitePeoplePresenterTest {
|
|||
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
val initialState = awaitItemAsDefault()
|
||||
skipItems(1)
|
||||
|
||||
initialState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query"))
|
||||
initialState.searchQuery.setTextAndPlaceCursorAtEnd("some query")
|
||||
skipItems(1)
|
||||
|
||||
assertThat(repository.providedQuery).isEqualTo("some query")
|
||||
|
|
@ -179,10 +186,10 @@ internal class DefaultInvitePeoplePresenterTest {
|
|||
coroutineDispatchers = coroutineDispatchers,
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
val initialState = awaitItemAsDefault()
|
||||
skipItems(1)
|
||||
|
||||
initialState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query"))
|
||||
initialState.searchQuery.setTextAndPlaceCursorAtEnd("some query")
|
||||
skipItems(1)
|
||||
|
||||
assertThat(repository.providedQuery).isEqualTo("some query")
|
||||
|
|
@ -239,10 +246,10 @@ internal class DefaultInvitePeoplePresenterTest {
|
|||
)
|
||||
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
val initialState = awaitItemAsDefault()
|
||||
skipItems(1)
|
||||
|
||||
initialState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query"))
|
||||
initialState.searchQuery.setTextAndPlaceCursorAtEnd("some query")
|
||||
skipItems(1)
|
||||
|
||||
assertThat(repository.providedQuery).isEqualTo("some query")
|
||||
|
|
@ -275,7 +282,7 @@ internal class DefaultInvitePeoplePresenterTest {
|
|||
val repository = FakeUserRepository()
|
||||
val presenter = createDefaultInvitePeoplePresenter(
|
||||
userRepository = repository,
|
||||
coroutineDispatchers = testCoroutineDispatchers()
|
||||
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
|
|
@ -306,14 +313,14 @@ internal class DefaultInvitePeoplePresenterTest {
|
|||
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
val initialState = awaitItemAsDefault()
|
||||
skipItems(1)
|
||||
|
||||
val selectedUser = aMatrixUser()
|
||||
|
||||
initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(selectedUser))
|
||||
|
||||
initialState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query"))
|
||||
initialState.searchQuery.setTextAndPlaceCursorAtEnd("some query")
|
||||
skipItems(1)
|
||||
|
||||
assertThat(repository.providedQuery).isEqualTo("some query")
|
||||
|
|
@ -344,13 +351,13 @@ internal class DefaultInvitePeoplePresenterTest {
|
|||
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
val initialState = awaitItemAsDefault()
|
||||
skipItems(1)
|
||||
|
||||
val selectedUser = aMatrixUser()
|
||||
|
||||
// Given a query is made
|
||||
initialState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query"))
|
||||
initialState.searchQuery.setTextAndPlaceCursorAtEnd("some query")
|
||||
skipItems(1)
|
||||
|
||||
assertThat(repository.providedQuery).isEqualTo("some query")
|
||||
|
|
@ -519,6 +526,85 @@ internal class DefaultInvitePeoplePresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - suggestions are loaded from recent direct rooms`() = runTest {
|
||||
val dmRoomId = RoomId("!dm_room:server.org")
|
||||
val otherUserId = UserId("@frank:server.org")
|
||||
val matrixClient = FakeMatrixClient(sessionId = A_USER_ID).apply {
|
||||
// Track the DM room as recently visited
|
||||
trackRecentlyVisitedRoom(dmRoomId)
|
||||
// Set up a DM room with the other user
|
||||
givenGetRoomResult(
|
||||
dmRoomId,
|
||||
FakeBaseRoom(
|
||||
sessionId = A_USER_ID,
|
||||
roomId = dmRoomId,
|
||||
initialRoomInfo = aRoomInfo(
|
||||
id = dmRoomId,
|
||||
isDirect = true,
|
||||
activeMembersCount = 2,
|
||||
currentUserMembership = CurrentUserMembership.JOINED,
|
||||
),
|
||||
getDirectRoomMemberResult = { aRoomMember(userId = otherUserId, displayName = "Frank") }
|
||||
)
|
||||
)
|
||||
}
|
||||
val presenter = createDefaultInvitePeoplePresenter(
|
||||
matrixClient = matrixClient,
|
||||
// Use empty room members so the suggestion doesn't get filtered
|
||||
roomMembersState = RoomMembersState.Ready(persistentListOf()),
|
||||
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(2)
|
||||
val state = awaitItemAsDefault()
|
||||
assertThat(state.suggestions).hasSize(1)
|
||||
assertThat(state.suggestions.first().matrixUser.userId).isEqualTo(otherUserId)
|
||||
assertThat(state.suggestions.first().isSelected).isFalse()
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - suggestions filters out existing room members`() = runTest {
|
||||
val dmRoomId = RoomId("!dm_room:server.org")
|
||||
val alreadyJoinedUserId = UserId("@frank:server.org")
|
||||
val matrixClient = FakeMatrixClient(sessionId = A_USER_ID).apply {
|
||||
trackRecentlyVisitedRoom(dmRoomId)
|
||||
givenGetRoomResult(
|
||||
dmRoomId,
|
||||
FakeBaseRoom(
|
||||
sessionId = A_USER_ID,
|
||||
roomId = dmRoomId,
|
||||
initialRoomInfo = aRoomInfo(
|
||||
id = dmRoomId,
|
||||
isDirect = true,
|
||||
activeMembersCount = 2,
|
||||
currentUserMembership = CurrentUserMembership.JOINED,
|
||||
),
|
||||
getDirectRoomMemberResult = { aRoomMember(userId = alreadyJoinedUserId, displayName = "Frank") }
|
||||
)
|
||||
)
|
||||
}
|
||||
// The user in the suggestion is already a member of the target room
|
||||
val presenter = createDefaultInvitePeoplePresenter(
|
||||
matrixClient = matrixClient,
|
||||
roomMembersState = RoomMembersState.Ready(
|
||||
persistentListOf(
|
||||
aRoomMember(userId = alreadyJoinedUserId, membership = RoomMembershipState.JOIN)
|
||||
)
|
||||
),
|
||||
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
// The suggestion should be filtered out because the user is already a room member
|
||||
val state = awaitItemAsDefault()
|
||||
assertThat(state.suggestions).isEmpty()
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun FakeUserRepository.emitStateWithUsers(
|
||||
users: List<MatrixUser>,
|
||||
isSearching: Boolean = false
|
||||
|
|
|
|||
|
|
@ -1,16 +1,33 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_link_new_device_desktop_scanning_title">"Scan QR-koden"</string>
|
||||
<string name="screen_link_new_device_desktop_step1">"Åbn %1$s på en bærbar eller stationær computer"</string>
|
||||
<string name="screen_link_new_device_desktop_step3">"Scan QR-koden med denne enhed"</string>
|
||||
<string name="screen_link_new_device_desktop_submit">"Klar til at scanne"</string>
|
||||
<string name="screen_link_new_device_desktop_title">"Åbn %1$s på en stationær computer for at få QR-koden"</string>
|
||||
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"Tallene stemmer ikke overens"</string>
|
||||
<string name="screen_link_new_device_enter_number_notice">"Indtast 2-cifret kode"</string>
|
||||
<string name="screen_link_new_device_enter_number_subtitle">"Dette vil bekræfte, at forbindelsen til din anden enhed er sikker."</string>
|
||||
<string name="screen_link_new_device_enter_number_title">"Indtast nummeret, der vises på din anden enhed"</string>
|
||||
<string name="screen_link_new_device_error_app_not_supported_subtitle">"Din kontoudbyder understøtter ikke %1$s."</string>
|
||||
<string name="screen_link_new_device_error_app_not_supported_title">"%1$s understøttes ikke"</string>
|
||||
<string name="screen_link_new_device_error_not_supported_subtitle">"Din kontoudbyder understøtter ikke login på en ny enhed med en QR-kode."</string>
|
||||
<string name="screen_link_new_device_error_not_supported_title">"QR-kode understøttes ikke"</string>
|
||||
<string name="screen_link_new_device_error_request_cancelled_subtitle">"Login blev annulleret på den anden enhed."</string>
|
||||
<string name="screen_link_new_device_error_request_cancelled_title">"Anmodning om login annulleret"</string>
|
||||
<string name="screen_link_new_device_error_request_timeout_subtitle">"Login er udløbet. Prøv venligst igen."</string>
|
||||
<string name="screen_link_new_device_error_request_timeout_title">"Login blev ikke afsluttet i tide"</string>
|
||||
<string name="screen_link_new_device_mobile_step1">"Åbn %1$s på den anden enhed"</string>
|
||||
<string name="screen_link_new_device_mobile_step2">"Vælg %1$s"</string>
|
||||
<string name="screen_link_new_device_mobile_step2_action">"\"Log ind med QR-kode\""</string>
|
||||
<string name="screen_link_new_device_mobile_step3">"Scan QR-koden vist her med den anden enhed"</string>
|
||||
<string name="screen_link_new_device_mobile_title">"Åbn %1$s på den anden enhed"</string>
|
||||
<string name="screen_link_new_device_root_desktop_computer">"Stationær computer"</string>
|
||||
<string name="screen_link_new_device_root_loading_qr_code">"Indlæser QR-kode…"</string>
|
||||
<string name="screen_link_new_device_root_mobile_device">"Mobil enhed"</string>
|
||||
<string name="screen_link_new_device_root_title">"Hvilken type enhed vil du linke?"</string>
|
||||
<string name="screen_link_new_device_wrong_number_subtitle">"Prøv igen, og sørg for, at du har indtastet den 2-cifrede kode korrekt. Hvis tallene stadig ikke stemmer overens, skal du kontakte din kontoudbyder."</string>
|
||||
<string name="screen_link_new_device_wrong_number_title">"Tallene stemmer ikke overens"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_description">"Der kunne ikke oprettes en sikker forbindelse til den nye enhed. Dine eksisterende enheder er stadig sikre, og du behøver ikke bekymre dig om dem."</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Hvad nu?"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Prøv at logge ind igen med en QR-kode, hvis dette skyldtes et netværksproblem"</string>
|
||||
|
|
@ -21,6 +38,8 @@
|
|||
<string name="screen_qr_code_login_error_cancelled_title">"Anmodning om login annulleret"</string>
|
||||
<string name="screen_qr_code_login_error_declined_subtitle">"Login blev afvist på den anden enhed."</string>
|
||||
<string name="screen_qr_code_login_error_declined_title">"Login afvist"</string>
|
||||
<string name="screen_qr_code_login_error_device_already_signed_in_subtitle">"Du behøver ikke gøre andet."</string>
|
||||
<string name="screen_qr_code_login_error_device_already_signed_in_title">"Din anden enhed er allerede logget ind"</string>
|
||||
<string name="screen_qr_code_login_error_expired_subtitle">"Login er udløbet. Prøv venligst igen."</string>
|
||||
<string name="screen_qr_code_login_error_expired_title">"Login blev ikke afsluttet i tide"</string>
|
||||
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Din anden enhed understøtter ikke at logge ind på %s med en QR-kode.
|
||||
|
|
|
|||
|
|
@ -1,16 +1,33 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_link_new_device_desktop_scanning_title">"Olvassa be a QR-kódot"</string>
|
||||
<string name="screen_link_new_device_desktop_step1">"Nyissa meg az %1$s alkalmazást egy laptopon vagy asztali számítógépen."</string>
|
||||
<string name="screen_link_new_device_desktop_step3">"Olvassa be a QR-kódot ezzel az eszközzel"</string>
|
||||
<string name="screen_link_new_device_desktop_submit">"Készen áll a beolvasásra"</string>
|
||||
<string name="screen_link_new_device_desktop_title">"Nyissa meg az asztali %1$s alkalmazást, hogy megkapja a QR-kódot"</string>
|
||||
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"A számok nem egyeznek"</string>
|
||||
<string name="screen_link_new_device_enter_number_notice">"Írja be a kétjegyű kódot"</string>
|
||||
<string name="screen_link_new_device_enter_number_subtitle">"Ezzel ellenőrizni fogja, hogy a másik eszközzel való kapcsolat biztonságos-e."</string>
|
||||
<string name="screen_link_new_device_enter_number_title">"Adja meg a másik eszközön megjelenő számot"</string>
|
||||
<string name="screen_link_new_device_error_app_not_supported_subtitle">"A fiókszolgáltatója nem támogatja az %1$s-et."</string>
|
||||
<string name="screen_link_new_device_error_app_not_supported_title">"Az %1$s nem támogatott"</string>
|
||||
<string name="screen_link_new_device_error_not_supported_subtitle">"Fiókszolgáltatója nem támogatja a QR-kóddal történő bejelentkezést egy új eszközre."</string>
|
||||
<string name="screen_link_new_device_error_not_supported_title">"A QR-kód nem támogatott"</string>
|
||||
<string name="screen_link_new_device_error_request_cancelled_subtitle">"A bejelentkezést megszakították a másik eszközön."</string>
|
||||
<string name="screen_link_new_device_error_request_cancelled_title">"Bejelentkezési kérés törölve"</string>
|
||||
<string name="screen_link_new_device_error_request_timeout_subtitle">"A bejelentkezés lejárt. Próbálja újra."</string>
|
||||
<string name="screen_link_new_device_error_request_timeout_title">"A bejelentkezés nem fejeződött be időben"</string>
|
||||
<string name="screen_link_new_device_mobile_step1">"Nyissa meg a másik eszközön ezt: %1$s"</string>
|
||||
<string name="screen_link_new_device_mobile_step2">"Válassza ezt: %1$s"</string>
|
||||
<string name="screen_link_new_device_mobile_step2_action">"„Bejelentkezés QR-kóddal”"</string>
|
||||
<string name="screen_link_new_device_mobile_step3">"Olvassa le az itt látható QR-kódot a másik eszközzel"</string>
|
||||
<string name="screen_link_new_device_mobile_title">"Nyissa meg a másik eszközön ezt: %1$s"</string>
|
||||
<string name="screen_link_new_device_root_desktop_computer">"Asztali számítógép"</string>
|
||||
<string name="screen_link_new_device_root_loading_qr_code">"QR-kód betöltése…"</string>
|
||||
<string name="screen_link_new_device_root_mobile_device">"Mobil eszköz"</string>
|
||||
<string name="screen_link_new_device_root_title">"Milyen típusú eszközt szeretne összekapcsolni?"</string>
|
||||
<string name="screen_link_new_device_wrong_number_subtitle">"Próbálja meg újra, és ellenőrizze, hogy helyesen adta meg a 2 számjegyű kódot. Ha a számok továbbra sem egyeznek, vegye fel a kapcsolatot a fiókszolgáltatójával."</string>
|
||||
<string name="screen_link_new_device_wrong_number_title">"A számok nem egyeznek"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_description">"Nem sikerült biztonságos kapcsolatot létesíteni az új eszközzel. A meglévő eszközei továbbra is biztonságban vannak, és nem kell aggódnia miattuk."</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Most mi lesz?"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Próbáljon meg újra bejelentkezni egy QR-kóddal, ha ez hálózati probléma volt."</string>
|
||||
|
|
@ -21,6 +38,8 @@
|
|||
<string name="screen_qr_code_login_error_cancelled_title">"Bejelentkezési kérés törölve"</string>
|
||||
<string name="screen_qr_code_login_error_declined_subtitle">"A bejelentkezést elutasították a másik eszközön."</string>
|
||||
<string name="screen_qr_code_login_error_declined_title">"A bejelentkezés elutasítva"</string>
|
||||
<string name="screen_qr_code_login_error_device_already_signed_in_subtitle">"Semmi mást nem kell tennie."</string>
|
||||
<string name="screen_qr_code_login_error_device_already_signed_in_title">"A másik eszköz már be van jelentkezve"</string>
|
||||
<string name="screen_qr_code_login_error_expired_subtitle">"A bejelentkezés lejárt. Próbálja újra."</string>
|
||||
<string name="screen_qr_code_login_error_expired_title">"A bejelentkezés nem fejeződött be időben"</string>
|
||||
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"A másik eszköz nem támogatja QR-kóddal történő bejelentkezést az %sbe.
|
||||
|
|
|
|||
|
|
@ -1,16 +1,33 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_link_new_device_desktop_scanning_title">"Сканировать QR-код"</string>
|
||||
<string name="screen_link_new_device_desktop_step1">"Откройте %1$s на ноутбуке или компьютере"</string>
|
||||
<string name="screen_link_new_device_desktop_step3">"Отсканируйте QR-код с помощью этого устройства"</string>
|
||||
<string name="screen_link_new_device_desktop_submit">"Готово к сканированию"</string>
|
||||
<string name="screen_link_new_device_desktop_title">"Открой %1$s на компьютере, чтобы получить QR-код"</string>
|
||||
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"Цифры не совпадают"</string>
|
||||
<string name="screen_link_new_device_enter_number_notice">"Введите 2-значный код"</string>
|
||||
<string name="screen_link_new_device_enter_number_subtitle">"Это позволит убедиться в безопасности соединения с другим вашим устройством."</string>
|
||||
<string name="screen_link_new_device_enter_number_title">"Введите номер, отображаемый на другом устройстве"</string>
|
||||
<string name="screen_link_new_device_error_app_not_supported_subtitle">"Поставщик учетной записи не поддерживает %1$s."</string>
|
||||
<string name="screen_link_new_device_error_app_not_supported_title">"%1$s не поддерживается"</string>
|
||||
<string name="screen_link_new_device_error_not_supported_subtitle">"Поставщик учетной записи не поддерживает вход на новое устройство с помощью QR-кода."</string>
|
||||
<string name="screen_link_new_device_error_not_supported_title">"QR-код не поддерживается"</string>
|
||||
<string name="screen_link_new_device_error_request_cancelled_subtitle">"Вход на другом устройстве был отменен."</string>
|
||||
<string name="screen_link_new_device_error_request_cancelled_title">"Запрос на вход отменен"</string>
|
||||
<string name="screen_link_new_device_error_request_timeout_subtitle">"Срок действия входа истек. Пожалуйста, попробуйте еще раз."</string>
|
||||
<string name="screen_link_new_device_error_request_timeout_title">"Вход в систему не был выполнен вовремя"</string>
|
||||
<string name="screen_link_new_device_mobile_step1">"Открыть %1$s на другом устройстве"</string>
|
||||
<string name="screen_link_new_device_mobile_step2">"Выберите %1$s"</string>
|
||||
<string name="screen_link_new_device_mobile_step2_action">"«Вход с помощью QR-кода»"</string>
|
||||
<string name="screen_link_new_device_mobile_step3">"Отсканируйте показанный здесь QR-код на другом устройстве."</string>
|
||||
<string name="screen_link_new_device_mobile_title">"Открыть %1$s на другом устройстве"</string>
|
||||
<string name="screen_link_new_device_root_desktop_computer">"Настольный компьютер"</string>
|
||||
<string name="screen_link_new_device_root_loading_qr_code">"Загрузка QR-кода…"</string>
|
||||
<string name="screen_link_new_device_root_mobile_device">"Мобильное устройство"</string>
|
||||
<string name="screen_link_new_device_root_title">"Какой тип устройства вы хотите подключить?"</string>
|
||||
<string name="screen_link_new_device_wrong_number_subtitle">"Пожалуйста, попробуйте еще раз и убедитесь, что вы правильно ввели 2-значный код. Если цифры по-прежнему не совпадают, обратитесь к своему поставщику услуг."</string>
|
||||
<string name="screen_link_new_device_wrong_number_title">"Цифры не совпадают"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_description">"Не удалось установить безопасное соединение с новым устройством. Существующие устройства по-прежнему в безопасности, и вам не нужно беспокоиться о них."</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Что теперь?"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Попробуйте снова войти в систему с помощью QR-кода, если это была проблема с соединением"</string>
|
||||
|
|
@ -21,6 +38,8 @@
|
|||
<string name="screen_qr_code_login_error_cancelled_title">"Запрос на вход отменен"</string>
|
||||
<string name="screen_qr_code_login_error_declined_subtitle">"Вход в систему был отклонен на другом устройстве."</string>
|
||||
<string name="screen_qr_code_login_error_declined_title">"Вход отклонен"</string>
|
||||
<string name="screen_qr_code_login_error_device_already_signed_in_subtitle">"Больше ничего не нужно делать."</string>
|
||||
<string name="screen_qr_code_login_error_device_already_signed_in_title">"Вход уже выполнен на другом устройстве"</string>
|
||||
<string name="screen_qr_code_login_error_expired_subtitle">"Срок действия входа истек. Пожалуйста, попробуйте еще раз."</string>
|
||||
<string name="screen_qr_code_login_error_expired_title">"Вход в систему не был выполнен вовремя"</string>
|
||||
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Другое устройство не поддерживает вход в %s с помощью QR-кода.
|
||||
|
|
|
|||
|
|
@ -1,16 +1,33 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_link_new_device_desktop_scanning_title">"Naskenovať QR kód"</string>
|
||||
<string name="screen_link_new_device_desktop_step1">"Otvorte %1$s na notebooku alebo stolnom počítači"</string>
|
||||
<string name="screen_link_new_device_desktop_step3">"Naskenujte QR kód pomocou tohto zariadenia"</string>
|
||||
<string name="screen_link_new_device_desktop_submit">"Pripravené na skenovanie"</string>
|
||||
<string name="screen_link_new_device_desktop_title">"Otvorte %1$s na stolnom počítači, aby ste získali QR kód"</string>
|
||||
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"Čísla sa nezhodujú"</string>
|
||||
<string name="screen_link_new_device_enter_number_notice">"Zadajte 2-miestny kód"</string>
|
||||
<string name="screen_link_new_device_enter_number_subtitle">"Týmto sa overí, že pripojenie k vášmu druhému zariadeniu je bezpečné."</string>
|
||||
<string name="screen_link_new_device_enter_number_title">"Zadajte číslo zobrazené na vašom druhom zariadení"</string>
|
||||
<string name="screen_link_new_device_error_app_not_supported_subtitle">"Poskytovateľ vášho účtu nepodporuje %1$s."</string>
|
||||
<string name="screen_link_new_device_error_app_not_supported_title">"%1$s nie je podporovaný"</string>
|
||||
<string name="screen_link_new_device_error_not_supported_subtitle">"Poskytovateľ vášho účtu nepodporuje prihlásenie do nového zariadenia pomocou QR kódu."</string>
|
||||
<string name="screen_link_new_device_error_not_supported_title">"QR kód nie je podporovaný"</string>
|
||||
<string name="screen_link_new_device_error_request_cancelled_subtitle">"Prihlásenie bolo zrušené na druhom zariadení."</string>
|
||||
<string name="screen_link_new_device_error_request_cancelled_title">"Žiadosť o prihlásenie bola zrušená"</string>
|
||||
<string name="screen_link_new_device_error_request_timeout_subtitle">"Platnosť prihlásenia vypršala. Skúste to prosím znova."</string>
|
||||
<string name="screen_link_new_device_error_request_timeout_title">"Prihlásenie nebolo včas dokončené"</string>
|
||||
<string name="screen_link_new_device_mobile_step1">"Otvorte %1$s na druhom zariadení"</string>
|
||||
<string name="screen_link_new_device_mobile_step2">"Vyberte %1$s"</string>
|
||||
<string name="screen_link_new_device_mobile_step2_action">"„Prihláste sa pomocou QR kódu“"</string>
|
||||
<string name="screen_link_new_device_mobile_step3">"Naskenujte tu zobrazený QR kód pomocou druhého zariadenia"</string>
|
||||
<string name="screen_link_new_device_mobile_title">"Otvorte %1$s na druhom zariadení"</string>
|
||||
<string name="screen_link_new_device_root_desktop_computer">"Stolný počítač"</string>
|
||||
<string name="screen_link_new_device_root_loading_qr_code">"Načítava sa QR kód…"</string>
|
||||
<string name="screen_link_new_device_root_mobile_device">"Mobilné zariadenie"</string>
|
||||
<string name="screen_link_new_device_root_title">"Aký typ zariadenia chcete prepojiť?"</string>
|
||||
<string name="screen_link_new_device_wrong_number_subtitle">"Skúste to prosím znova a uistite sa, že ste zadali 2-ciferný kód správne. Ak sa čísla stále nezhodujú, kontaktujte poskytovateľa vášho účtu."</string>
|
||||
<string name="screen_link_new_device_wrong_number_title">"Čísla sa nezhodujú"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_description">"K novému zariadeniu sa nepodarilo vytvoriť bezpečné pripojenie. Vaše existujúce zariadenia sú stále v bezpečí a nemusíte sa o ne obávať."</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Čo teraz?"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Skúste sa znova prihlásiť pomocou QR kódu v prípade, že ide o problém so sieťou"</string>
|
||||
|
|
@ -21,6 +38,8 @@
|
|||
<string name="screen_qr_code_login_error_cancelled_title">"Žiadosť o prihlásenie bola zrušená"</string>
|
||||
<string name="screen_qr_code_login_error_declined_subtitle">"Prihlásenie bolo zamietnuté na druhom zariadení."</string>
|
||||
<string name="screen_qr_code_login_error_declined_title">"Prihlásenie bolo odmietnuté"</string>
|
||||
<string name="screen_qr_code_login_error_device_already_signed_in_subtitle">"Nemusíte spraviť nič iné."</string>
|
||||
<string name="screen_qr_code_login_error_device_already_signed_in_title">"Vaše druhé zariadenie je už prihlásené"</string>
|
||||
<string name="screen_qr_code_login_error_expired_subtitle">"Platnosť prihlásenia vypršala. Skúste to prosím znova."</string>
|
||||
<string name="screen_qr_code_login_error_expired_title">"Prihlásenie nebolo včas dokončené"</string>
|
||||
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Vaše druhé zariadenie nepodporuje prihlásenie do aplikácie %s pomocou QR kódu.
|
||||
|
|
|
|||
|
|
@ -60,4 +60,6 @@ dependencies {
|
|||
testImplementation(projects.libraries.permissions.test)
|
||||
testImplementation(projects.libraries.sessionStorage.test)
|
||||
testImplementation(projects.libraries.wellknown.test)
|
||||
testImplementation(libs.androidx.camera.camera2)
|
||||
testImplementation(libs.androidx.camera.lifecycle)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,8 @@
|
|||
<string name="screen_qr_code_login_error_cancelled_title">"Anmodning om login annulleret"</string>
|
||||
<string name="screen_qr_code_login_error_declined_subtitle">"Login blev afvist på den anden enhed."</string>
|
||||
<string name="screen_qr_code_login_error_declined_title">"Login afvist"</string>
|
||||
<string name="screen_qr_code_login_error_device_already_signed_in_subtitle">"Du behøver ikke gøre andet."</string>
|
||||
<string name="screen_qr_code_login_error_device_already_signed_in_title">"Din anden enhed er allerede logget ind"</string>
|
||||
<string name="screen_qr_code_login_error_expired_subtitle">"Login er udløbet. Prøv venligst igen."</string>
|
||||
<string name="screen_qr_code_login_error_expired_title">"Login blev ikke afsluttet i tide"</string>
|
||||
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Din anden enhed understøtter ikke at logge ind på %s med en QR-kode.
|
||||
|
|
|
|||
|
|
@ -60,6 +60,8 @@
|
|||
<string name="screen_qr_code_login_error_cancelled_title">"Bejelentkezési kérés törölve"</string>
|
||||
<string name="screen_qr_code_login_error_declined_subtitle">"A bejelentkezést elutasították a másik eszközön."</string>
|
||||
<string name="screen_qr_code_login_error_declined_title">"A bejelentkezés elutasítva"</string>
|
||||
<string name="screen_qr_code_login_error_device_already_signed_in_subtitle">"Semmi mást nem kell tennie."</string>
|
||||
<string name="screen_qr_code_login_error_device_already_signed_in_title">"A másik eszköz már be van jelentkezve"</string>
|
||||
<string name="screen_qr_code_login_error_expired_subtitle">"A bejelentkezés lejárt. Próbálja újra."</string>
|
||||
<string name="screen_qr_code_login_error_expired_title">"A bejelentkezés nem fejeződött be időben"</string>
|
||||
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"A másik eszköz nem támogatja QR-kóddal történő bejelentkezést az %sbe.
|
||||
|
|
|
|||
|
|
@ -60,6 +60,8 @@
|
|||
<string name="screen_qr_code_login_error_cancelled_title">"Запрос на вход отменен"</string>
|
||||
<string name="screen_qr_code_login_error_declined_subtitle">"Вход в систему был отклонен на другом устройстве."</string>
|
||||
<string name="screen_qr_code_login_error_declined_title">"Вход отклонен"</string>
|
||||
<string name="screen_qr_code_login_error_device_already_signed_in_subtitle">"Больше ничего не нужно делать."</string>
|
||||
<string name="screen_qr_code_login_error_device_already_signed_in_title">"Вход уже выполнен на другом устройстве"</string>
|
||||
<string name="screen_qr_code_login_error_expired_subtitle">"Срок действия входа истек. Пожалуйста, попробуйте еще раз."</string>
|
||||
<string name="screen_qr_code_login_error_expired_title">"Вход в систему не был выполнен вовремя"</string>
|
||||
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Другое устройство не поддерживает вход в %s с помощью QR-кода.
|
||||
|
|
|
|||
|
|
@ -60,6 +60,8 @@
|
|||
<string name="screen_qr_code_login_error_cancelled_title">"Žiadosť o prihlásenie bola zrušená"</string>
|
||||
<string name="screen_qr_code_login_error_declined_subtitle">"Prihlásenie bolo zamietnuté na druhom zariadení."</string>
|
||||
<string name="screen_qr_code_login_error_declined_title">"Prihlásenie bolo odmietnuté"</string>
|
||||
<string name="screen_qr_code_login_error_device_already_signed_in_subtitle">"Nemusíte spraviť nič iné."</string>
|
||||
<string name="screen_qr_code_login_error_device_already_signed_in_title">"Vaše druhé zariadenie je už prihlásené"</string>
|
||||
<string name="screen_qr_code_login_error_expired_subtitle">"Platnosť prihlásenia vypršala. Skúste to prosím znova."</string>
|
||||
<string name="screen_qr_code_login_error_expired_title">"Prihlásenie nebolo včas dokončené"</string>
|
||||
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Vaše druhé zariadenie nepodporuje prihlásenie do aplikácie %s pomocou QR kódu.
|
||||
|
|
|
|||
|
|
@ -9,9 +9,11 @@
|
|||
package io.element.android.features.login.impl.screens.qrcode.scan
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
|
||||
import io.element.android.libraries.matrix.test.auth.qrlogin.FakeMatrixQrCodeLoginData
|
||||
|
|
@ -20,6 +22,8 @@ import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
|||
import io.element.android.tests.testutils.ensureCalledOnce
|
||||
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
||||
import io.element.android.tests.testutils.pressBackKey
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TestRule
|
||||
|
|
@ -30,6 +34,19 @@ class QrCodeScanViewTest {
|
|||
@get:Rule
|
||||
val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
private var provider: ProcessCameraProvider? = null
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
provider = ProcessCameraProvider.getInstance(context).get()
|
||||
}
|
||||
|
||||
@After
|
||||
fun teardown() {
|
||||
provider?.unbindAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on back pressed - calls the expected callback`() {
|
||||
ensureCalledOnce { callback ->
|
||||
|
|
|
|||
|
|
@ -68,7 +68,6 @@ dependencies {
|
|||
implementation(libs.jsoup)
|
||||
implementation(libs.androidx.constraintlayout)
|
||||
implementation(libs.androidx.constraintlayout.compose)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
implementation(libs.androidx.media3.exoplayer)
|
||||
implementation(libs.androidx.media3.ui)
|
||||
implementation(libs.sigpwned.emoji4j)
|
||||
|
|
|
|||
|
|
@ -13,13 +13,12 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
|||
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
|
||||
sealed interface MessagesEvents {
|
||||
data class HandleAction(val action: TimelineItemAction, val event: TimelineItem.Event) : MessagesEvents
|
||||
data class ToggleReaction(val emoji: String, val eventOrTransactionId: EventOrTransactionId) : MessagesEvents
|
||||
data class InviteDialogDismissed(val action: InviteDialogAction) : MessagesEvents
|
||||
data class OnUserClicked(val user: MatrixUser) : MessagesEvents
|
||||
data object Dismiss : MessagesEvents
|
||||
data object MarkAsFullyReadAndExit : MessagesEvents
|
||||
sealed interface MessagesEvent {
|
||||
data class HandleAction(val action: TimelineItemAction, val event: TimelineItem.Event) : MessagesEvent
|
||||
data class ToggleReaction(val emoji: String, val eventOrTransactionId: EventOrTransactionId) : MessagesEvent
|
||||
data class InviteDialogDismissed(val action: InviteDialogAction) : MessagesEvent
|
||||
data class OnUserClicked(val user: MatrixUser) : MessagesEvent
|
||||
data object MarkAsFullyReadAndExit : MessagesEvent
|
||||
}
|
||||
|
||||
enum class InviteDialogAction {
|
||||
|
|
@ -36,7 +36,7 @@ import io.element.android.features.messages.impl.attachments.Attachment
|
|||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvent
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
|
||||
import io.element.android.features.messages.impl.timeline.TimelineController
|
||||
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
||||
import io.element.android.features.messages.impl.timeline.TimelineEvent
|
||||
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
||||
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
|
||||
import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories
|
||||
|
|
@ -151,7 +151,7 @@ class MessagesNode(
|
|||
activity: Activity,
|
||||
darkTheme: Boolean,
|
||||
url: String,
|
||||
eventSink: (TimelineEvents) -> Unit,
|
||||
eventSink: (TimelineEvent) -> Unit,
|
||||
customTab: Boolean
|
||||
) {
|
||||
when (val permalink = permalinkParser.parse(url)) {
|
||||
|
|
@ -178,12 +178,12 @@ class MessagesNode(
|
|||
|
||||
private fun handleRoomLinkClick(
|
||||
roomLink: PermalinkData.RoomLink,
|
||||
eventSink: (TimelineEvents) -> Unit,
|
||||
eventSink: (TimelineEvent) -> Unit,
|
||||
) {
|
||||
if (room.matches(roomLink.roomIdOrAlias)) {
|
||||
val eventId = roomLink.eventId
|
||||
if (eventId != null) {
|
||||
eventSink(TimelineEvents.FocusOnEvent(eventId))
|
||||
eventSink(TimelineEvent.FocusOnEvent(eventId))
|
||||
} else {
|
||||
// Click on the same room, ignore
|
||||
displaySameRoomToast()
|
||||
|
|
@ -242,7 +242,7 @@ class MessagesNode(
|
|||
val state = presenter.present()
|
||||
|
||||
BackHandler {
|
||||
state.eventSink(MessagesEvents.MarkAsFullyReadAndExit)
|
||||
state.eventSink(MessagesEvent.MarkAsFullyReadAndExit)
|
||||
}
|
||||
|
||||
OnLifecycleEvent { _, event ->
|
||||
|
|
@ -253,7 +253,7 @@ class MessagesNode(
|
|||
}
|
||||
MessagesView(
|
||||
state = state,
|
||||
onBackClick = { state.eventSink(MessagesEvents.MarkAsFullyReadAndExit) },
|
||||
onBackClick = { state.eventSink(MessagesEvent.MarkAsFullyReadAndExit) },
|
||||
onRoomDetailsClick = callback::navigateToRoomDetails,
|
||||
onEventContentClick = { isLive, event ->
|
||||
if (isLive) {
|
||||
|
|
@ -305,7 +305,7 @@ class MessagesNode(
|
|||
}
|
||||
LaunchedEffect(focusedEventId) {
|
||||
if (focusedEventId != null) {
|
||||
state.timelineState.eventSink(TimelineEvents.FocusOnEvent(focusedEventId!!))
|
||||
state.timelineState.eventSink(TimelineEvent.FocusOnEvent(focusedEventId!!))
|
||||
focusedEventId = null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,10 +27,8 @@ import dev.zacsweers.metro.AssistedInject
|
|||
import im.vector.app.features.analytics.plan.PinUnpinAction
|
||||
import io.element.android.appconfig.MessageComposerConfig
|
||||
import io.element.android.features.messages.api.timeline.HtmlConverterProvider
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListEvents
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListState
|
||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||
import io.element.android.features.messages.impl.crypto.historyvisible.HistoryVisibleState
|
||||
import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState
|
||||
import io.element.android.features.messages.impl.link.LinkState
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvent
|
||||
|
|
@ -38,7 +36,7 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer
|
|||
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState
|
||||
import io.element.android.features.messages.impl.timeline.MarkAsFullyRead
|
||||
import io.element.android.features.messages.impl.timeline.TimelineController
|
||||
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
||||
import io.element.android.features.messages.impl.timeline.TimelineEvent
|
||||
import io.element.android.features.messages.impl.timeline.TimelineState
|
||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
|
||||
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
|
||||
|
|
@ -76,6 +74,7 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
|||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomInfo
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembersState
|
||||
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
|
||||
import io.element.android.libraries.matrix.api.room.isDm
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
||||
|
|
@ -102,7 +101,6 @@ class MessagesPresenter(
|
|||
@Assisted private val timelinePresenter: Presenter<TimelineState>,
|
||||
private val timelineProtectionPresenter: Presenter<TimelineProtectionState>,
|
||||
private val identityChangeStatePresenter: Presenter<IdentityChangeState>,
|
||||
private val historyVisibleStatePresenter: Presenter<HistoryVisibleState>,
|
||||
private val linkPresenter: Presenter<LinkState>,
|
||||
@Assisted private val actionListPresenter: Presenter<ActionListState>,
|
||||
private val customReactionPresenter: Presenter<CustomReactionState>,
|
||||
|
|
@ -154,7 +152,6 @@ class MessagesPresenter(
|
|||
val timelineState = timelinePresenter.present()
|
||||
val timelineProtectionState = timelineProtectionPresenter.present()
|
||||
val identityChangeState = identityChangeStatePresenter.present()
|
||||
val historyVisibleState = historyVisibleStatePresenter.present()
|
||||
val actionListState = actionListPresenter.present()
|
||||
val linkState = linkPresenter.present()
|
||||
val customReactionState = customReactionPresenter.present()
|
||||
|
|
@ -208,6 +205,16 @@ class MessagesPresenter(
|
|||
val dmRoomMember by room.getDirectRoomMember(membersState)
|
||||
val roomMemberIdentityStateChanges = identityChangeState.roomMemberIdentityStateChanges
|
||||
|
||||
val isKeyShareOnInviteEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.EnableKeyShareOnInvite).collectAsState(initial = false)
|
||||
// The top bar should show a "history" icon if:
|
||||
// * History sharing is enabled,
|
||||
// * The room is encrypted, and:
|
||||
// * The room's history_visibility allows future users to see content.
|
||||
val showSharedHistoryIcon = isKeyShareOnInviteEnabled &&
|
||||
roomInfo.isEncrypted == true &&
|
||||
(roomInfo.historyVisibility == RoomHistoryVisibility.Shared ||
|
||||
roomInfo.historyVisibility == RoomHistoryVisibility.WorldReadable)
|
||||
|
||||
LifecycleResumeEffect(dmRoomMember, roomInfo.isEncrypted) {
|
||||
if (roomInfo.isEncrypted == true) {
|
||||
val dmRoomMemberId = dmRoomMember?.userId
|
||||
|
|
@ -221,9 +228,9 @@ class MessagesPresenter(
|
|||
onPauseOrDispose {}
|
||||
}
|
||||
|
||||
fun handleEvent(event: MessagesEvents) {
|
||||
fun handleEvent(event: MessagesEvent) {
|
||||
when (event) {
|
||||
is MessagesEvents.HandleAction -> {
|
||||
is MessagesEvent.HandleAction -> {
|
||||
localCoroutineScope.handleTimelineAction(
|
||||
action = event.action,
|
||||
targetEvent = event.event,
|
||||
|
|
@ -233,21 +240,20 @@ class MessagesPresenter(
|
|||
timelineProtectionState = timelineProtectionState,
|
||||
)
|
||||
}
|
||||
is MessagesEvents.ToggleReaction -> {
|
||||
is MessagesEvent.ToggleReaction -> {
|
||||
localCoroutineScope.toggleReaction(event.emoji, event.eventOrTransactionId)
|
||||
}
|
||||
is MessagesEvents.InviteDialogDismissed -> {
|
||||
is MessagesEvent.InviteDialogDismissed -> {
|
||||
hasDismissedInviteDialog = true
|
||||
|
||||
if (event.action == InviteDialogAction.Invite) {
|
||||
localCoroutineScope.reinviteOtherUser(inviteProgress)
|
||||
}
|
||||
}
|
||||
is MessagesEvents.Dismiss -> actionListState.eventSink(ActionListEvents.Clear)
|
||||
is MessagesEvents.OnUserClicked -> {
|
||||
is MessagesEvent.OnUserClicked -> {
|
||||
roomMemberModerationState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(event.user))
|
||||
}
|
||||
is MessagesEvents.MarkAsFullyReadAndExit -> coroutineScope.launch {
|
||||
is MessagesEvent.MarkAsFullyReadAndExit -> coroutineScope.launch {
|
||||
if (!markingAsReadAndExiting.getAndSet(true)) {
|
||||
val latestEventId = room.liveTimeline.getLatestEventId().getOrElse {
|
||||
Timber.w(it, "Failed to get latest event id to mark as fully read")
|
||||
|
|
@ -277,7 +283,6 @@ class MessagesPresenter(
|
|||
timelineState = timelineState,
|
||||
timelineProtectionState = timelineProtectionState,
|
||||
identityChangeState = identityChangeState,
|
||||
historyVisibleState = historyVisibleState,
|
||||
linkState = linkState,
|
||||
actionListState = actionListState,
|
||||
customReactionState = customReactionState,
|
||||
|
|
@ -292,6 +297,7 @@ class MessagesPresenter(
|
|||
pinnedMessagesBannerState = pinnedMessagesBannerState,
|
||||
dmUserVerificationState = dmUserVerificationState,
|
||||
roomMemberModerationState = roomMemberModerationState,
|
||||
showSharedHistoryIcon = showSharedHistoryIcon,
|
||||
successorRoom = roomInfo.successorRoom,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
|
|
@ -529,7 +535,7 @@ class MessagesPresenter(
|
|||
event: TimelineItem.Event,
|
||||
timelineState: TimelineState,
|
||||
) {
|
||||
event.eventId?.let { timelineState.eventSink(TimelineEvents.EndPoll(it)) }
|
||||
event.eventId?.let { timelineState.eventSink(TimelineEvent.EndPoll(it)) }
|
||||
}
|
||||
|
||||
private suspend fun handleCopyLink(event: TimelineItem.Event) {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ package io.element.android.features.messages.impl
|
|||
|
||||
import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerState
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListState
|
||||
import io.element.android.features.messages.impl.crypto.historyvisible.HistoryVisibleState
|
||||
import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState
|
||||
import io.element.android.features.messages.impl.link.LinkState
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
|
||||
|
|
@ -41,7 +40,6 @@ data class MessagesState(
|
|||
val timelineState: TimelineState,
|
||||
val timelineProtectionState: TimelineProtectionState,
|
||||
val identityChangeState: IdentityChangeState,
|
||||
val historyVisibleState: HistoryVisibleState,
|
||||
val linkState: LinkState,
|
||||
val actionListState: ActionListState,
|
||||
val customReactionState: CustomReactionState,
|
||||
|
|
@ -56,8 +54,10 @@ data class MessagesState(
|
|||
val pinnedMessagesBannerState: PinnedMessagesBannerState,
|
||||
val dmUserVerificationState: IdentityState?,
|
||||
val roomMemberModerationState: RoomMemberModerationState,
|
||||
/** Should the top bar include the "history" icon? */
|
||||
val showSharedHistoryIcon: Boolean,
|
||||
val successorRoom: SuccessorRoom?,
|
||||
val eventSink: (MessagesEvents) -> Unit
|
||||
val eventSink: (MessagesEvent) -> Unit
|
||||
) {
|
||||
val isTombstoned = successorRoom != null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@ import io.element.android.features.messages.api.timeline.voicemessages.composer.
|
|||
import io.element.android.features.messages.api.timeline.voicemessages.composer.aVoiceMessagePreviewState
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListState
|
||||
import io.element.android.features.messages.impl.actionlist.anActionListState
|
||||
import io.element.android.features.messages.impl.crypto.historyvisible.HistoryVisibleState
|
||||
import io.element.android.features.messages.impl.crypto.historyvisible.aHistoryVisibleState
|
||||
import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState
|
||||
import io.element.android.features.messages.impl.crypto.identity.aRoomMemberIdentityStateChange
|
||||
import io.element.android.features.messages.impl.crypto.identity.anIdentityChangeState
|
||||
|
|
@ -28,11 +26,11 @@ import io.element.android.features.messages.impl.pinned.banner.aLoadedPinnedMess
|
|||
import io.element.android.features.messages.impl.timeline.TimelineState
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemList
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineState
|
||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvents
|
||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvent
|
||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
|
||||
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryEvents
|
||||
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryEvent
|
||||
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
|
||||
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetEvents
|
||||
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetEvent
|
||||
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||
|
|
@ -92,15 +90,6 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
|
|||
composerState = aMessageComposerState(textEditorState = aTextEditorStateMarkdown()),
|
||||
identityChangeState = anIdentityChangeState(listOf(aRoomMemberIdentityStateChange()))
|
||||
),
|
||||
aMessagesState(
|
||||
composerState = aMessageComposerState(textEditorState = aTextEditorStateMarkdown()),
|
||||
historyVisibleState = aHistoryVisibleState(showAlert = true)
|
||||
),
|
||||
aMessagesState(
|
||||
composerState = aMessageComposerState(textEditorState = aTextEditorStateMarkdown()),
|
||||
identityChangeState = anIdentityChangeState(listOf(aRoomMemberIdentityStateChange())),
|
||||
historyVisibleState = aHistoryVisibleState(showAlert = true)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -121,7 +110,6 @@ fun aMessagesState(
|
|||
),
|
||||
timelineProtectionState: TimelineProtectionState = aTimelineProtectionState(),
|
||||
identityChangeState: IdentityChangeState = anIdentityChangeState(),
|
||||
historyVisibleState: HistoryVisibleState = aHistoryVisibleState(),
|
||||
linkState: LinkState = aLinkState(),
|
||||
readReceiptBottomSheetState: ReadReceiptBottomSheetState = aReadReceiptBottomSheetState(),
|
||||
actionListState: ActionListState = anActionListState(),
|
||||
|
|
@ -132,8 +120,9 @@ fun aMessagesState(
|
|||
pinnedMessagesBannerState: PinnedMessagesBannerState = aLoadedPinnedMessagesBannerState(),
|
||||
dmUserVerificationState: IdentityState? = null,
|
||||
roomMemberModerationState: RoomMemberModerationState = aRoomMemberModerationState(),
|
||||
showSharedHistoryIcon: Boolean = false,
|
||||
successorRoom: SuccessorRoom? = null,
|
||||
eventSink: (MessagesEvents) -> Unit = {},
|
||||
eventSink: (MessagesEvent) -> Unit = {},
|
||||
) = MessagesState(
|
||||
roomId = RoomId("!id:domain"),
|
||||
roomName = roomName,
|
||||
|
|
@ -144,7 +133,6 @@ fun aMessagesState(
|
|||
voiceMessageComposerState = voiceMessageComposerState,
|
||||
timelineProtectionState = timelineProtectionState,
|
||||
identityChangeState = identityChangeState,
|
||||
historyVisibleState = historyVisibleState,
|
||||
linkState = linkState,
|
||||
timelineState = timelineState,
|
||||
readReceiptBottomSheetState = readReceiptBottomSheetState,
|
||||
|
|
@ -160,6 +148,7 @@ fun aMessagesState(
|
|||
pinnedMessagesBannerState = pinnedMessagesBannerState,
|
||||
dmUserVerificationState = dmUserVerificationState,
|
||||
roomMemberModerationState = roomMemberModerationState,
|
||||
showSharedHistoryIcon = showSharedHistoryIcon,
|
||||
successorRoom = successorRoom,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
|
@ -187,7 +176,7 @@ fun aUserEventPermissions(
|
|||
|
||||
fun aReactionSummaryState(
|
||||
target: ReactionSummaryState.Summary? = null,
|
||||
eventSink: (ReactionSummaryEvents) -> Unit = {}
|
||||
eventSink: (ReactionSummaryEvent) -> Unit = {}
|
||||
) = ReactionSummaryState(
|
||||
target = target,
|
||||
eventSink = eventSink,
|
||||
|
|
@ -196,7 +185,7 @@ fun aReactionSummaryState(
|
|||
fun aCustomReactionState(
|
||||
target: CustomReactionState.Target = CustomReactionState.Target.None,
|
||||
recentEmojis: ImmutableList<String> = persistentListOf(),
|
||||
eventSink: (CustomReactionEvents) -> Unit = {},
|
||||
eventSink: (CustomReactionEvent) -> Unit = {},
|
||||
) = CustomReactionState(
|
||||
target = target,
|
||||
recentEmojis = recentEmojis,
|
||||
|
|
@ -206,7 +195,7 @@ fun aCustomReactionState(
|
|||
|
||||
fun aReadReceiptBottomSheetState(
|
||||
selectedEvent: TimelineItem.Event? = null,
|
||||
eventSink: (ReadReceiptBottomSheetEvents) -> Unit = {},
|
||||
eventSink: (ReadReceiptBottomSheetEvent) -> Unit = {},
|
||||
) = ReadReceiptBottomSheetState(
|
||||
selectedEvent = selectedEvent,
|
||||
eventSink = eventSink,
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -51,12 +52,11 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerEvent
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListEvents
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListEvent
|
||||
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.crypto.historyvisible.HistoryVisibleStateView
|
||||
import io.element.android.features.messages.impl.crypto.identity.IdentityChangeStateView
|
||||
import io.element.android.features.messages.impl.link.LinkEvents
|
||||
import io.element.android.features.messages.impl.link.LinkEvent
|
||||
import io.element.android.features.messages.impl.link.LinkView
|
||||
import io.element.android.features.messages.impl.messagecomposer.AttachmentsBottomSheet
|
||||
import io.element.android.features.messages.impl.messagecomposer.DisabledComposerView
|
||||
|
|
@ -67,18 +67,18 @@ import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBan
|
|||
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerView
|
||||
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerViewDefaults
|
||||
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.TimelineEvent
|
||||
import io.element.android.features.messages.impl.timeline.TimelineView
|
||||
import io.element.android.features.messages.impl.timeline.aGroupedEvents
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemDaySeparator
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineState
|
||||
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.reactionsummary.ReactionSummaryEvents
|
||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvent
|
||||
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryEvent
|
||||
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryView
|
||||
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheet
|
||||
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetEvents
|
||||
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetEvent
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
||||
|
|
@ -168,7 +168,7 @@ fun MessagesView(
|
|||
Timber.v("OnMessageLongClicked= ${event.id}")
|
||||
hidingKeyboard {
|
||||
state.actionListState.eventSink(
|
||||
ActionListEvents.ComputeForMessage(
|
||||
ActionListEvent.ComputeForMessage(
|
||||
event = event,
|
||||
userEventPermissions = state.userEventPermissions,
|
||||
)
|
||||
|
|
@ -177,20 +177,20 @@ fun MessagesView(
|
|||
}
|
||||
|
||||
fun onActionSelected(action: TimelineItemAction, event: TimelineItem.Event) {
|
||||
state.eventSink(MessagesEvents.HandleAction(action, event))
|
||||
state.eventSink(MessagesEvent.HandleAction(action, event))
|
||||
}
|
||||
|
||||
fun onEmojiReactionClick(emoji: String, event: TimelineItem.Event) {
|
||||
state.eventSink(MessagesEvents.ToggleReaction(emoji, event.eventOrTransactionId))
|
||||
state.eventSink(MessagesEvent.ToggleReaction(emoji, event.eventOrTransactionId))
|
||||
}
|
||||
|
||||
fun onEmojiReactionLongClick(emoji: String, event: TimelineItem.Event) {
|
||||
if (event.eventId == null) return
|
||||
state.reactionSummaryState.eventSink(ReactionSummaryEvents.ShowReactionSummary(event.eventId, event.reactionsState.reactions, emoji))
|
||||
state.reactionSummaryState.eventSink(ReactionSummaryEvent.ShowReactionSummary(event.eventId, event.reactionsState.reactions, emoji))
|
||||
}
|
||||
|
||||
fun onMoreReactionsClick(event: TimelineItem.Event) {
|
||||
state.customReactionState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(event))
|
||||
state.customReactionState.eventSink(CustomReactionEvent.ShowCustomReactionSheet(event))
|
||||
}
|
||||
|
||||
val expandableState = rememberExpandableBottomSheetLayoutState()
|
||||
|
|
@ -225,6 +225,7 @@ fun MessagesView(
|
|||
heroes = state.heroes,
|
||||
roomCallState = state.roomCallState,
|
||||
dmUserIdentityState = state.dmUserVerificationState,
|
||||
showSharedHistoryIcon = state.showSharedHistoryIcon,
|
||||
onBackClick = { hidingKeyboard { onBackClick() } },
|
||||
onRoomDetailsClick = { hidingKeyboard { onRoomDetailsClick() } },
|
||||
onJoinCallClick = onJoinCallClick,
|
||||
|
|
@ -243,7 +244,7 @@ fun MessagesView(
|
|||
onMessageLongClick = ::onMessageLongClick,
|
||||
onUserDataClick = {
|
||||
hidingKeyboard {
|
||||
state.eventSink(MessagesEvents.OnUserClicked(it))
|
||||
state.eventSink(MessagesEvent.OnUserClicked(it))
|
||||
}
|
||||
},
|
||||
onLinkClick = { link, customTab ->
|
||||
|
|
@ -251,19 +252,19 @@ fun MessagesView(
|
|||
onLinkClick(link.url, true)
|
||||
// Do not check those links, they are internal link only
|
||||
} else {
|
||||
state.linkState.eventSink(LinkEvents.OnLinkClick(link))
|
||||
state.linkState.eventSink(LinkEvent.OnLinkClick(link))
|
||||
}
|
||||
},
|
||||
onReactionClick = ::onEmojiReactionClick,
|
||||
onReactionLongClick = ::onEmojiReactionLongClick,
|
||||
onMoreReactionsClick = ::onMoreReactionsClick,
|
||||
onReadReceiptClick = { event ->
|
||||
state.readReceiptBottomSheetState.eventSink(ReadReceiptBottomSheetEvents.EventSelected(event))
|
||||
state.readReceiptBottomSheetState.eventSink(ReadReceiptBottomSheetEvent.EventSelected(event))
|
||||
},
|
||||
onSendLocationClick = onSendLocationClick,
|
||||
onCreatePollClick = onCreatePollClick,
|
||||
onSwipeToReply = { targetEvent ->
|
||||
state.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, targetEvent))
|
||||
state.eventSink(MessagesEvent.HandleAction(TimelineItemAction.Reply, targetEvent))
|
||||
},
|
||||
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
|
||||
onJoinCallClick = onJoinCallClick,
|
||||
|
|
@ -300,7 +301,7 @@ fun MessagesView(
|
|||
state = state,
|
||||
onLinkClick = { url, customTab -> onLinkClick(url, customTab) },
|
||||
onRoomSuccessorClick = { roomId ->
|
||||
state.timelineState.eventSink(TimelineEvents.NavigateToPredecessorOrSuccessorRoom(roomId = roomId))
|
||||
state.timelineState.eventSink(TimelineEvent.NavigateToPredecessorOrSuccessorRoom(roomId = roomId))
|
||||
},
|
||||
)
|
||||
},
|
||||
|
|
@ -341,22 +342,43 @@ fun MessagesView(
|
|||
maxBottomSheetContentHeight = maxComposerHeightPx.toDp(),
|
||||
)
|
||||
|
||||
var endPollConfirmingEvent: TimelineItem.Event? by remember { mutableStateOf(null) }
|
||||
|
||||
if (endPollConfirmingEvent != null) {
|
||||
ConfirmationDialog(
|
||||
content = stringResource(id = CommonStrings.common_poll_end_confirmation),
|
||||
onSubmitClick = {
|
||||
endPollConfirmingEvent?.let { event ->
|
||||
onActionSelected(TimelineItemAction.EndPoll, event)
|
||||
}
|
||||
endPollConfirmingEvent = null
|
||||
},
|
||||
onDismiss = { endPollConfirmingEvent = null },
|
||||
)
|
||||
}
|
||||
|
||||
ActionListView(
|
||||
state = state.actionListState,
|
||||
onSelectAction = ::onActionSelected,
|
||||
onSelectAction = { action: TimelineItemAction, event: TimelineItem.Event ->
|
||||
if (action == TimelineItemAction.EndPoll) {
|
||||
endPollConfirmingEvent = event
|
||||
} else {
|
||||
onActionSelected(action, event)
|
||||
}
|
||||
},
|
||||
onCustomReactionClick = { event ->
|
||||
state.customReactionState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(event))
|
||||
state.customReactionState.eventSink(CustomReactionEvent.ShowCustomReactionSheet(event))
|
||||
},
|
||||
onEmojiReactionClick = ::onEmojiReactionClick,
|
||||
onVerifiedUserSendFailureClick = { event ->
|
||||
state.timelineState.eventSink(TimelineEvents.ComputeVerifiedUserSendFailure(event))
|
||||
state.timelineState.eventSink(TimelineEvent.ComputeVerifiedUserSendFailure(event))
|
||||
},
|
||||
)
|
||||
|
||||
CustomReactionBottomSheet(
|
||||
state = state.customReactionState,
|
||||
onSelectEmoji = { uniqueId, emoji ->
|
||||
state.eventSink(MessagesEvents.ToggleReaction(emoji.unicode, uniqueId))
|
||||
state.eventSink(MessagesEvent.ToggleReaction(emoji.unicode, uniqueId))
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -382,8 +404,8 @@ private fun ReinviteDialog(state: MessagesState) {
|
|||
content = stringResource(id = R.string.screen_room_invite_again_alert_message),
|
||||
cancelText = stringResource(id = CommonStrings.action_cancel),
|
||||
submitText = stringResource(id = CommonStrings.action_invite),
|
||||
onSubmitClick = { state.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Invite)) },
|
||||
onDismiss = { state.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Cancel)) }
|
||||
onSubmitClick = { state.eventSink(MessagesEvent.InviteDialogDismissed(InviteDialogAction.Invite)) },
|
||||
onDismiss = { state.eventSink(MessagesEvent.InviteDialogDismissed(InviteDialogAction.Cancel)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -467,7 +489,7 @@ private fun MessagesViewContent(
|
|||
) {
|
||||
fun focusOnPinnedEvent(eventId: EventId) {
|
||||
state.timelineState.eventSink(
|
||||
TimelineEvents.FocusOnEvent(eventId = eventId, debounce = FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS.milliseconds)
|
||||
TimelineEvent.FocusOnEvent(eventId = eventId, debounce = FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS.milliseconds)
|
||||
)
|
||||
}
|
||||
PinnedMessagesBannerView(
|
||||
|
|
@ -497,17 +519,10 @@ private fun MessagesViewComposerBottomSheetContents(
|
|||
// Do not show the identity change if user is composing a Rich message or is seeing suggestion(s).
|
||||
if (state.composerState.suggestions.isEmpty() &&
|
||||
state.composerState.textEditorState is TextEditorState.Markdown) {
|
||||
if (state.identityChangeState.roomMemberIdentityStateChanges.isNotEmpty()) {
|
||||
IdentityChangeStateView(
|
||||
state = state.identityChangeState,
|
||||
onLinkClick = onLinkClick,
|
||||
)
|
||||
} else {
|
||||
HistoryVisibleStateView(
|
||||
state = state.historyVisibleState,
|
||||
onLinkClick = onLinkClick,
|
||||
)
|
||||
}
|
||||
IdentityChangeStateView(
|
||||
state = state.identityChangeState,
|
||||
onLinkClick = onLinkClick,
|
||||
)
|
||||
}
|
||||
val verificationViolation = state.identityChangeState.roomMemberIdentityStateChanges.firstOrNull {
|
||||
it.identityState == IdentityState.VerificationViolation
|
||||
|
|
|
|||
|
|
@ -11,10 +11,10 @@ package io.element.android.features.messages.impl.actionlist
|
|||
import io.element.android.features.messages.impl.UserEventPermissions
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
|
||||
sealed interface ActionListEvents {
|
||||
data object Clear : ActionListEvents
|
||||
sealed interface ActionListEvent {
|
||||
data object Clear : ActionListEvent
|
||||
data class ComputeForMessage(
|
||||
val event: TimelineItem.Event,
|
||||
val userEventPermissions: UserEventPermissions,
|
||||
) : ActionListEvents
|
||||
) : ActionListEvent
|
||||
}
|
||||
|
|
@ -107,10 +107,10 @@ class DefaultActionListPresenter(
|
|||
|
||||
val isThreadsEnabled = featureFlagService.isFeatureEnabledFlow(FeatureFlags.Threads).collectAsState(false)
|
||||
|
||||
fun handleEvent(event: ActionListEvents) {
|
||||
fun handleEvent(event: ActionListEvent) {
|
||||
when (event) {
|
||||
ActionListEvents.Clear -> target.value = ActionListState.Target.None
|
||||
is ActionListEvents.ComputeForMessage -> localCoroutineScope.computeForMessage(
|
||||
ActionListEvent.Clear -> target.value = ActionListState.Target.None
|
||||
is ActionListEvent.ComputeForMessage -> localCoroutineScope.computeForMessage(
|
||||
timelineItem = event.event,
|
||||
usersEventPermissions = event.userEventPermissions,
|
||||
isDeveloperModeEnabled = isDeveloperModeEnabled,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
|
||||
data class ActionListState(
|
||||
val target: Target,
|
||||
val eventSink: (ActionListEvents) -> Unit,
|
||||
val eventSink: (ActionListEvent) -> Unit,
|
||||
) {
|
||||
@Immutable
|
||||
sealed interface Target {
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
|||
|
||||
fun anActionListState(
|
||||
target: ActionListState.Target = ActionListState.Target.None,
|
||||
eventSink: (ActionListEvents) -> Unit = {},
|
||||
eventSink: (ActionListEvent) -> Unit = {},
|
||||
) = ActionListState(
|
||||
target = target,
|
||||
eventSink = eventSink
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ fun ActionListView(
|
|||
) {
|
||||
if (targetItem == null) return
|
||||
sheetState.hide(coroutineScope) {
|
||||
state.eventSink(ActionListEvents.Clear)
|
||||
state.eventSink(ActionListEvent.Clear)
|
||||
onSelectAction(itemAction, targetItem)
|
||||
}
|
||||
}
|
||||
|
|
@ -126,7 +126,7 @@ fun ActionListView(
|
|||
fun onEmojiReactionClick(emoji: String) {
|
||||
if (targetItem == null) return
|
||||
sheetState.hide(coroutineScope) {
|
||||
state.eventSink(ActionListEvents.Clear)
|
||||
state.eventSink(ActionListEvent.Clear)
|
||||
onEmojiReactionClick(emoji, targetItem)
|
||||
}
|
||||
}
|
||||
|
|
@ -134,19 +134,19 @@ fun ActionListView(
|
|||
fun onCustomReactionClick() {
|
||||
if (targetItem == null) return
|
||||
sheetState.hide(coroutineScope) {
|
||||
state.eventSink(ActionListEvents.Clear)
|
||||
state.eventSink(ActionListEvent.Clear)
|
||||
onCustomReactionClick(targetItem)
|
||||
}
|
||||
}
|
||||
|
||||
fun onDismiss() {
|
||||
state.eventSink(ActionListEvents.Clear)
|
||||
state.eventSink(ActionListEvent.Clear)
|
||||
}
|
||||
|
||||
fun onVerifiedUserSendFailureClick() {
|
||||
if (targetItem == null) return
|
||||
sheetState.hide(coroutineScope) {
|
||||
state.eventSink(ActionListEvents.Clear)
|
||||
state.eventSink(ActionListEvent.Clear)
|
||||
onVerifiedUserSendFailureClick(targetItem)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@
|
|||
|
||||
package io.element.android.features.messages.impl.attachments.preview
|
||||
|
||||
sealed interface AttachmentsPreviewEvents {
|
||||
data object SendAttachment : AttachmentsPreviewEvents
|
||||
data object CancelAndDismiss : AttachmentsPreviewEvents
|
||||
data object CancelAndClearSendState : AttachmentsPreviewEvents
|
||||
sealed interface AttachmentsPreviewEvent {
|
||||
data object SendAttachment : AttachmentsPreviewEvent
|
||||
data object CancelAndDismiss : AttachmentsPreviewEvent
|
||||
data object CancelAndClearSendState : AttachmentsPreviewEvent
|
||||
}
|
||||
|
|
@ -98,7 +98,7 @@ class AttachmentsPreviewPresenter(
|
|||
val mediaOptimizationSelectorPresenter = remember {
|
||||
mediaOptimizationSelectorPresenterFactory.create(mediaAttachment.localMedia)
|
||||
}
|
||||
val mediaOptimizationSelectorState = mediaOptimizationSelectorPresenter.present()
|
||||
val mediaOptimizationSelectorState by rememberUpdatedState(mediaOptimizationSelectorPresenter.present())
|
||||
|
||||
val observableSendState = snapshotFlow { sendActionState.value }
|
||||
|
||||
|
|
@ -140,9 +140,9 @@ class AttachmentsPreviewPresenter(
|
|||
}
|
||||
}
|
||||
|
||||
fun handleEvent(event: AttachmentsPreviewEvents) {
|
||||
fun handleEvent(event: AttachmentsPreviewEvent) {
|
||||
when (event) {
|
||||
is AttachmentsPreviewEvents.SendAttachment -> {
|
||||
is AttachmentsPreviewEvent.SendAttachment -> {
|
||||
ongoingSendAttachmentJob.value = coroutineScope.launch {
|
||||
// If the media optimization selector is displayed, we need to wait for the user to select the options
|
||||
// before we can pre-process the media.
|
||||
|
|
@ -191,7 +191,7 @@ class AttachmentsPreviewPresenter(
|
|||
}
|
||||
}
|
||||
}
|
||||
AttachmentsPreviewEvents.CancelAndDismiss -> {
|
||||
AttachmentsPreviewEvent.CancelAndDismiss -> {
|
||||
displayFileTooLargeError = false
|
||||
|
||||
// Cancel media preprocessing and sending
|
||||
|
|
@ -206,7 +206,7 @@ class AttachmentsPreviewPresenter(
|
|||
sendActionState,
|
||||
)
|
||||
}
|
||||
AttachmentsPreviewEvents.CancelAndClearSendState -> {
|
||||
AttachmentsPreviewEvent.CancelAndClearSendState -> {
|
||||
// Cancel media sending
|
||||
ongoingSendAttachmentJob.value?.let {
|
||||
it.cancel()
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ data class AttachmentsPreviewState(
|
|||
val textEditorState: TextEditorState,
|
||||
val mediaOptimizationSelectorState: MediaOptimizationSelectorState,
|
||||
val displayFileTooLargeError: Boolean,
|
||||
val eventSink: (AttachmentsPreviewEvents) -> Unit
|
||||
val eventSink: (AttachmentsPreviewEvent) -> Unit,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
|
|
|
|||
|
|
@ -82,15 +82,15 @@ fun AttachmentsPreviewView(
|
|||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun postSendAttachment() {
|
||||
state.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||
state.eventSink(AttachmentsPreviewEvent.SendAttachment)
|
||||
}
|
||||
|
||||
fun postCancel() {
|
||||
state.eventSink(AttachmentsPreviewEvents.CancelAndDismiss)
|
||||
state.eventSink(AttachmentsPreviewEvent.CancelAndDismiss)
|
||||
}
|
||||
|
||||
fun postClearSendState() {
|
||||
state.eventSink(AttachmentsPreviewEvents.CancelAndClearSendState)
|
||||
state.eventSink(AttachmentsPreviewEvent.CancelAndClearSendState)
|
||||
}
|
||||
|
||||
BackHandler(enabled = state.sendActionState !is SendActionState.Sending.Uploading && state.sendActionState !is SendActionState.Done) {
|
||||
|
|
@ -199,7 +199,7 @@ private fun AttachmentPreviewContent(
|
|||
AlertDialog(
|
||||
title = stringResource(CommonStrings.dialog_file_too_large_to_upload_title),
|
||||
content = content,
|
||||
onDismiss = { state.eventSink(AttachmentsPreviewEvents.CancelAndDismiss) },
|
||||
onDismiss = { state.eventSink(AttachmentsPreviewEvent.CancelAndDismiss) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.impl.crypto.historyvisible
|
||||
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.androidutils.hash.hash
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
interface HistoryVisibleAcknowledgementRepository {
|
||||
fun hasAcknowledged(roomId: RoomId): Flow<Boolean>
|
||||
suspend fun setAcknowledged(roomId: RoomId, value: Boolean)
|
||||
}
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class DefaultHistoryVisibleAcknowledgementRepository(
|
||||
sessionId: SessionId,
|
||||
preferenceDataStoreFactory: PreferenceDataStoreFactory,
|
||||
) : HistoryVisibleAcknowledgementRepository {
|
||||
val store =
|
||||
sessionId.value.hash().take(16).let { hash ->
|
||||
preferenceDataStoreFactory.create("elementx_historyvisible_$hash")
|
||||
}
|
||||
|
||||
override fun hasAcknowledged(roomId: RoomId): Flow<Boolean> {
|
||||
return store.data.map { prefs ->
|
||||
val acknowledged = prefs[booleanPreferencesKey(roomId.value)] ?: false
|
||||
acknowledged
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setAcknowledged(roomId: RoomId, value: Boolean) {
|
||||
store.edit { prefs ->
|
||||
prefs[booleanPreferencesKey(roomId.value)] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.impl.crypto.historyvisible
|
||||
|
||||
sealed interface HistoryVisibleEvent {
|
||||
data object Acknowledge : HistoryVisibleEvent
|
||||
}
|
||||
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