Merge branch 'release/26.01.2' into main
This commit is contained in:
commit
ef6797497f
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/localazy/importSupportedLocalesFromLocalazy.py
|
||||||
./tools/test/generateAllScreenshots.py
|
./tools/test/generateAllScreenshots.py
|
||||||
- name: Create Pull Request for Strings
|
- 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:
|
with:
|
||||||
token: ${{ secrets.DANGER_GITHUB_API_TOKEN }}
|
token: ${{ secrets.DANGER_GITHUB_API_TOKEN }}
|
||||||
commit-message: Sync Strings from Localazy
|
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
|
- name: Run SAS String script
|
||||||
run: ./tools/sas/import_sas_strings.py
|
run: ./tools/sas/import_sas_strings.py
|
||||||
- name: Create Pull Request for SAS Strings
|
- 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:
|
with:
|
||||||
commit-message: Sync SAS Strings
|
commit-message: Sync SAS Strings
|
||||||
title: 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
|
Changes in Element X v26.01.0
|
||||||
=============================
|
=============================
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -132,8 +132,23 @@ android {
|
||||||
optimization {
|
optimization {
|
||||||
enable = true
|
enable = true
|
||||||
keepRules {
|
keepRules {
|
||||||
files.add(File(projectDir, "proguard-rules.pro"))
|
files.add(File(projectDir, "common-proguard-rules.pro"))
|
||||||
files.add(getDefaultProguardFile("proguard-android-optimize.txt"))
|
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
|
# For more details, see
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# Increase optimizations passes to 3
|
||||||
|
-optimizationpasses 3
|
||||||
|
|
||||||
# JNA
|
# JNA
|
||||||
-dontwarn java.awt.*
|
-dontwarn java.awt.*
|
||||||
-keep class com.sun.jna.** { *; }
|
-keep class com.sun.jna.** { *; }
|
||||||
|
|
@ -41,7 +44,6 @@
|
||||||
static int windowAttachCount(android.view.View);
|
static int windowAttachCount(android.view.View);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Keep LogSessionId class and related classes (https://github.com/androidx/media/issues/2535)
|
# Keep LogSessionId class and related classes (https://github.com/androidx/media/issues/2535)
|
||||||
-keep class android.media.metrics.LogSessionId { *; }
|
-keep class android.media.metrics.LogSessionId { *; }
|
||||||
-keep class android.media.metrics.** { *; }
|
-keep class android.media.metrics.** { *; }
|
||||||
|
|
@ -66,10 +68,9 @@
|
||||||
-dontwarn androidx.window.sidecar.SidecarWindowLayoutInfo
|
-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
|
# 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,allowoptimization,allowshrinking class org.matrix.rustcomponents.sdk.** { *;}
|
||||||
-keep,allowshrinking class uniffi.** { *;}
|
-keep,allowoptimization,allowshrinking class uniffi.** { *;}
|
||||||
-keep,allowshrinking class io.element.android.x.di.** { *; }
|
-keep,allowoptimization,allowshrinking class io.element.android.x.di.** { *; }
|
||||||
-keepclasseswithmembernames,allowoptimization,allowshrinking class io.element.android.** { *; }
|
|
||||||
|
|
||||||
# Keep Metro classes
|
# 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.
|
* The maximum size of a single log file.
|
||||||
*/
|
*/
|
||||||
const val MAX_LOG_CONTENT_SIZE = 100 * 1024 * 1024L
|
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()))
|
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 -> {
|
is NavTarget.SecureBackup -> {
|
||||||
secureBackupEntryPoint.createNode(
|
secureBackupEntryPoint.createNode(
|
||||||
|
|
|
||||||
|
|
@ -214,6 +214,10 @@ class RoomFlowNode(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is NavTarget.JoinRoom -> {
|
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(
|
val inputs = JoinRoomEntryPoint.Inputs(
|
||||||
roomId = navTarget.roomId,
|
roomId = navTarget.roomId,
|
||||||
roomIdOrAlias = inputs.roomIdOrAlias,
|
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.api.room.JoinedRoom
|
||||||
import io.element.android.libraries.matrix.ui.room.LoadingRoomState
|
import io.element.android.libraries.matrix.ui.room.LoadingRoomState
|
||||||
import io.element.android.libraries.matrix.ui.room.LoadingRoomStateFlowFactory
|
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.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
@ -53,6 +57,7 @@ class JoinedRoomFlowNode(
|
||||||
@Assisted val buildContext: BuildContext,
|
@Assisted val buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
loadingRoomStateFlowFactory: LoadingRoomStateFlowFactory,
|
loadingRoomStateFlowFactory: LoadingRoomStateFlowFactory,
|
||||||
|
private val analyticsService: AnalyticsService,
|
||||||
) :
|
) :
|
||||||
BaseFlowNode<JoinedRoomFlowNode.NavTarget>(
|
BaseFlowNode<JoinedRoomFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
|
|
@ -81,6 +86,11 @@ class JoinedRoomFlowNode(
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
super.onBuilt()
|
super.onBuilt()
|
||||||
|
|
||||||
|
val parentTransaction = analyticsService.getLongRunningTransaction(NotificationToMessage)
|
||||||
|
val openRoomTransaction = analyticsService.startLongRunningTransaction(OpenRoom, parentTransaction)
|
||||||
|
analyticsService.startLongRunningTransaction(LoadJoinedRoomFlow, openRoomTransaction)
|
||||||
|
|
||||||
loadingRoomStateStateFlow
|
loadingRoomStateStateFlow
|
||||||
.map {
|
.map {
|
||||||
it is LoadingRoomState.Loaded
|
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.permalink.PermalinkData
|
||||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
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.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.AnalyticsService
|
||||||
import io.element.android.services.analytics.api.finishLongRunningTransaction
|
import io.element.android.services.analytics.api.finishLongRunningTransaction
|
||||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||||
|
|
@ -106,8 +104,6 @@ class JoinedRoomLoadedFlowNode(
|
||||||
init {
|
init {
|
||||||
lifecycle.subscribe(
|
lifecycle.subscribe(
|
||||||
onCreate = {
|
onCreate = {
|
||||||
val parent = analyticsService.getLongRunningTransaction(OpenRoom)
|
|
||||||
analyticsService.startLongRunningTransaction(LoadMessagesUi, parent)
|
|
||||||
Timber.v("OnCreate => ${inputs.room.roomId}")
|
Timber.v("OnCreate => ${inputs.room.roomId}")
|
||||||
appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId)
|
appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId)
|
||||||
activeRoomsHolder.addRoom(inputs.room)
|
activeRoomsHolder.addRoom(inputs.room)
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,9 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
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.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.features.rageshake.api.detection.RageshakeDetectionView
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
|
@ -39,8 +39,8 @@ fun RootView(
|
||||||
children()
|
children()
|
||||||
|
|
||||||
fun onOpenBugReport() {
|
fun onOpenBugReport() {
|
||||||
state.crashDetectionState.eventSink(CrashDetectionEvents.ResetAppHasCrashed)
|
state.crashDetectionState.eventSink(CrashDetectionEvent.ResetAppHasCrashed)
|
||||||
state.rageshakeDetectionState.eventSink(RageshakeDetectionEvents.Dismiss)
|
state.rageshakeDetectionState.eventSink(RageshakeDetectionEvent.Dismiss)
|
||||||
onOpenBugReport.invoke()
|
onOpenBugReport.invoke()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ allprojects {
|
||||||
config.from(files("$rootDir/tools/detekt/detekt.yml"))
|
config.from(files("$rootDir/tools/detekt/detekt.yml"))
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
detektPlugins("io.nlopez.compose.rules:detekt:0.5.3")
|
detektPlugins("io.nlopez.compose.rules:detekt:0.5.6")
|
||||||
detektPlugins(project(":tests:detekt-rules"))
|
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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_space_announcement_item1">"Просмотр пространств, которые вы создали или к которым присоединились"</string>
|
<string name="screen_space_announcement_item1">"Просмотреть пространства, которые вы создали или к которым присоединились"</string>
|
||||||
<string name="screen_space_announcement_item2">"Принимать или отклонять приглашения в пространства"</string>
|
<string name="screen_space_announcement_item2">"Принимать или отклонять приглашения в пространства"</string>
|
||||||
<string name="screen_space_announcement_item3">"Откройте для себя все комнаты, к которым вы можете присоединиться в своих пространствах."</string>
|
<string name="screen_space_announcement_item3">"Найти все комнаты, к которым можно присоединиться в ваших пространствах"</string>
|
||||||
<string name="screen_space_announcement_item4">"Присоединиться к публичному пространству"</string>
|
<string name="screen_space_announcement_item4">"Присоединяться к публичным пространствам"</string>
|
||||||
<string name="screen_space_announcement_item5">"Покинуть все пространства, к которым вы присоединились"</string>
|
<string name="screen_space_announcement_item5">"Покинуть все пространства, к которым вы присоединились"</string>
|
||||||
<string name="screen_space_announcement_notice">"Работа с пространствами скоро станет доступна"</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>
|
<string name="screen_space_announcement_title">"Представляем пространства"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,13 @@ import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
interface CreateRoomEntryPoint : FeatureEntryPoint {
|
interface CreateRoomEntryPoint : FeatureEntryPoint {
|
||||||
fun createNode(
|
interface Builder {
|
||||||
isSpace: Boolean,
|
fun setIsSpace(isSpace: Boolean): Builder
|
||||||
parentNode: Node,
|
fun setParentSpace(parentSpaceId: RoomId): Builder
|
||||||
buildContext: BuildContext,
|
fun build(): Node
|
||||||
callback: Callback,
|
}
|
||||||
): Node
|
|
||||||
|
fun builder(parentNode: Node, buildContext: BuildContext, callback: Callback): Builder
|
||||||
|
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun onRoomCreated(roomId: RoomId)
|
fun onRoomCreated(roomId: RoomId)
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ dependencies {
|
||||||
implementation(projects.libraries.mediapickers.api)
|
implementation(projects.libraries.mediapickers.api)
|
||||||
implementation(projects.libraries.mediaupload.api)
|
implementation(projects.libraries.mediaupload.api)
|
||||||
implementation(projects.libraries.permissions.api)
|
implementation(projects.libraries.permissions.api)
|
||||||
|
implementation(projects.libraries.previewutils)
|
||||||
implementation(projects.libraries.usersearch.impl)
|
implementation(projects.libraries.usersearch.impl)
|
||||||
implementation(projects.services.analytics.api)
|
implementation(projects.services.analytics.api)
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ class CreateRoomFlowNode(
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
) : BaseFlowNode<CreateRoomFlowNode.NavTarget>(
|
) : BaseFlowNode<CreateRoomFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = NavTarget.ConfigureRoom(isSpace = plugins.filterIsInstance<Inputs>().first().isSpace),
|
initialElement = initialElementFromInputs(plugins.filterIsInstance<Inputs>().first()),
|
||||||
savedStateMap = buildContext.savedStateMap,
|
savedStateMap = buildContext.savedStateMap,
|
||||||
),
|
),
|
||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
|
|
@ -46,7 +46,8 @@ class CreateRoomFlowNode(
|
||||||
) {
|
) {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Inputs(
|
data class Inputs(
|
||||||
val isSpace: Boolean
|
val isSpace: Boolean,
|
||||||
|
val parentSpaceId: RoomId?,
|
||||||
) : NodeInputs, Parcelable
|
) : NodeInputs, Parcelable
|
||||||
|
|
||||||
private val callback: CreateRoomEntryPoint.Callback = callback()
|
private val callback: CreateRoomEntryPoint.Callback = callback()
|
||||||
|
|
@ -54,7 +55,7 @@ class CreateRoomFlowNode(
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
return when (navTarget) {
|
return when (navTarget) {
|
||||||
is NavTarget.ConfigureRoom -> {
|
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 {
|
val callback = object : ConfigureRoomNode.Callback {
|
||||||
override fun onCreateRoomSuccess(roomId: RoomId) {
|
override fun onCreateRoomSuccess(roomId: RoomId) {
|
||||||
backstack.replace(NavTarget.AddPeople(roomId))
|
backstack.replace(NavTarget.AddPeople(roomId))
|
||||||
|
|
@ -81,9 +82,14 @@ class CreateRoomFlowNode(
|
||||||
|
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class ConfigureRoom(val isSpace: Boolean) : NavTarget
|
data class ConfigureRoom(val isSpace: Boolean, val parentSpaceId: RoomId?) : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class AddPeople(val roomId: RoomId) : NavTarget
|
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.features.createroom.api.CreateRoomEntryPoint
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
@ContributesBinding(SessionScope::class)
|
@ContributesBinding(SessionScope::class)
|
||||||
class DefaultCreateRoomEntryPoint : CreateRoomEntryPoint {
|
class DefaultCreateRoomEntryPoint : CreateRoomEntryPoint {
|
||||||
override fun createNode(
|
class Builder(
|
||||||
isSpace: Boolean,
|
private val parentNode: Node,
|
||||||
parentNode: Node,
|
private val buildContext: BuildContext,
|
||||||
buildContext: BuildContext,
|
private val callback: CreateRoomEntryPoint.Callback,
|
||||||
callback: CreateRoomEntryPoint.Callback,
|
) : CreateRoomEntryPoint.Builder {
|
||||||
): Node {
|
private var isSpace = false
|
||||||
val inputs = CreateRoomFlowNode.Inputs(isSpace)
|
private var parentSpaceId: RoomId? = null
|
||||||
return parentNode.createNode<CreateRoomFlowNode>(buildContext, listOf(inputs, callback))
|
|
||||||
|
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
|
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
|
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
||||||
|
|
||||||
sealed interface ConfigureRoomEvents {
|
sealed interface ConfigureRoomEvents {
|
||||||
data class RoomNameChanged(val name: String) : ConfigureRoomEvents
|
data class RoomNameChanged(val name: String) : ConfigureRoomEvents
|
||||||
data class TopicChanged(val topic: String) : ConfigureRoomEvents
|
data class TopicChanged(val topic: String) : ConfigureRoomEvents
|
||||||
data class RoomVisibilityChanged(val visibilityItem: RoomVisibilityItem) : ConfigureRoomEvents
|
data class JoinRuleChanged(val joinRuleItem: JoinRuleItem) : ConfigureRoomEvents
|
||||||
data class RoomAccessChanged(val roomAccess: RoomAccessItem) : ConfigureRoomEvents
|
|
||||||
data class RoomAddressChanged(val roomAddress: String) : ConfigureRoomEvents
|
data class RoomAddressChanged(val roomAddress: String) : ConfigureRoomEvents
|
||||||
data object CreateRoom : ConfigureRoomEvents
|
data object CreateRoom : ConfigureRoomEvents
|
||||||
data class HandleAvatarAction(val action: AvatarAction) : ConfigureRoomEvents
|
data class HandleAvatarAction(val action: AvatarAction) : ConfigureRoomEvents
|
||||||
|
data class SetParentSpace(val space: SpaceRoom?) : ConfigureRoomEvents
|
||||||
data object CancelCreateRoom : ConfigureRoomEvents
|
data object CancelCreateRoom : ConfigureRoomEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,11 +42,12 @@ class ConfigureRoomNode(
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Inputs(
|
data class Inputs(
|
||||||
val isSpace: Boolean,
|
val isSpace: Boolean,
|
||||||
|
val parentSpaceId: RoomId?,
|
||||||
) : NodeInputs, Parcelable
|
) : NodeInputs, Parcelable
|
||||||
|
|
||||||
private val inputs = inputs<Inputs>()
|
private val inputs = inputs<Inputs>()
|
||||||
|
|
||||||
private val presenter = presenterFactory.create(inputs.isSpace)
|
private val presenter = presenterFactory.create(inputs.isSpace, inputs.parentSpaceId)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
lifecycle.subscribe(
|
lifecycle.subscribe(
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.AssistedFactory
|
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.createroom.RoomPreset
|
||||||
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
|
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.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.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.media.AvatarAction
|
||||||
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity
|
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity
|
||||||
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidityEffect
|
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.PermissionsEvent
|
||||||
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
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.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import kotlin.jvm.optionals.getOrDefault
|
import kotlin.jvm.optionals.getOrDefault
|
||||||
|
import kotlin.jvm.optionals.getOrNull
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@AssistedInject
|
@AssistedInject
|
||||||
class ConfigureRoomPresenter(
|
class ConfigureRoomPresenter(
|
||||||
@Assisted private val isSpace: Boolean,
|
@Assisted private val isSpace: Boolean,
|
||||||
|
@Assisted private val initialParentSpaceId: RoomId?,
|
||||||
private val dataStore: CreateRoomConfigStore,
|
private val dataStore: CreateRoomConfigStore,
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
private val mediaPickerProvider: PickerProvider,
|
private val mediaPickerProvider: PickerProvider,
|
||||||
|
|
@ -66,7 +76,7 @@ class ConfigureRoomPresenter(
|
||||||
) : Presenter<ConfigureRoomState> {
|
) : Presenter<ConfigureRoomState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(isSpace: Boolean): ConfigureRoomPresenter
|
fun create(isSpace: Boolean, parentSpaceId: RoomId?): ConfigureRoomPresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
private val cameraPermissionPresenter: PermissionsPresenter = permissionsPresenterFactory.create(android.Manifest.permission.CAMERA)
|
private val cameraPermissionPresenter: PermissionsPresenter = permissionsPresenterFactory.create(android.Manifest.permission.CAMERA)
|
||||||
|
|
@ -78,6 +88,7 @@ class ConfigureRoomPresenter(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): ConfigureRoomState {
|
override fun present(): ConfigureRoomState {
|
||||||
|
val canAddRoomToSpace by featureFlagService.isFeatureEnabledFlow(FeatureFlags.CreateSpaces).collectAsState(false)
|
||||||
val cameraPermissionState = cameraPermissionPresenter.present()
|
val cameraPermissionState = cameraPermissionPresenter.present()
|
||||||
val createRoomConfig by dataStore.getCreateRoomConfigFlow().collectAsState()
|
val createRoomConfig by dataStore.getCreateRoomConfigFlow().collectAsState()
|
||||||
val homeserverName = remember { matrixClient.userIdServerName() }
|
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) {
|
LaunchedEffect(cameraPermissionState.permissionGranted) {
|
||||||
if (cameraPermissionState.permissionGranted && pendingPermissionRequest) {
|
if (cameraPermissionState.permissionGranted && pendingPermissionRequest) {
|
||||||
pendingPermissionRequest = false
|
pendingPermissionRequest = false
|
||||||
|
|
@ -115,7 +138,7 @@ class ConfigureRoomPresenter(
|
||||||
RoomAddressValidityEffect(
|
RoomAddressValidityEffect(
|
||||||
client = matrixClient,
|
client = matrixClient,
|
||||||
roomAliasHelper = roomAliasHelper,
|
roomAliasHelper = roomAliasHelper,
|
||||||
newRoomAddress = createRoomConfig.roomVisibility.roomAddress().getOrDefault(""),
|
newRoomAddress = createRoomConfig.visibilityState.roomAddress().getOrDefault(""),
|
||||||
knownRoomAddress = null,
|
knownRoomAddress = null,
|
||||||
) { newRoomAddressValidity ->
|
) { newRoomAddressValidity ->
|
||||||
roomAddressValidity.value = newRoomAddressValidity
|
roomAddressValidity.value = newRoomAddressValidity
|
||||||
|
|
@ -124,6 +147,27 @@ class ConfigureRoomPresenter(
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
val createRoomAction: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
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) {
|
fun createRoom(config: CreateRoomConfig) {
|
||||||
createRoomAction.value = AsyncAction.Uninitialized
|
createRoomAction.value = AsyncAction.Uninitialized
|
||||||
localCoroutineScope.createRoom(config, createRoomAction)
|
localCoroutineScope.createRoom(config, createRoomAction)
|
||||||
|
|
@ -133,8 +177,7 @@ class ConfigureRoomPresenter(
|
||||||
when (event) {
|
when (event) {
|
||||||
is ConfigureRoomEvents.RoomNameChanged -> dataStore.setRoomName(event.name)
|
is ConfigureRoomEvents.RoomNameChanged -> dataStore.setRoomName(event.name)
|
||||||
is ConfigureRoomEvents.TopicChanged -> dataStore.setTopic(event.topic)
|
is ConfigureRoomEvents.TopicChanged -> dataStore.setTopic(event.topic)
|
||||||
is ConfigureRoomEvents.RoomVisibilityChanged -> dataStore.setRoomVisibility(event.visibilityItem)
|
is ConfigureRoomEvents.JoinRuleChanged -> dataStore.setJoinRule(event.joinRuleItem)
|
||||||
is ConfigureRoomEvents.RoomAccessChanged -> dataStore.setRoomAccess(event.roomAccess)
|
|
||||||
is ConfigureRoomEvents.RoomAddressChanged -> dataStore.setRoomAddress(event.roomAddress)
|
is ConfigureRoomEvents.RoomAddressChanged -> dataStore.setRoomAddress(event.roomAddress)
|
||||||
is ConfigureRoomEvents.CreateRoom -> createRoom(createRoomConfig)
|
is ConfigureRoomEvents.CreateRoom -> createRoom(createRoomConfig)
|
||||||
is ConfigureRoomEvents.HandleAvatarAction -> {
|
is ConfigureRoomEvents.HandleAvatarAction -> {
|
||||||
|
|
@ -149,19 +192,24 @@ class ConfigureRoomPresenter(
|
||||||
AvatarAction.Remove -> dataStore.setAvatarUri(uri = null)
|
AvatarAction.Remove -> dataStore.setAvatarUri(uri = null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is ConfigureRoomEvents.SetParentSpace -> {
|
||||||
ConfigureRoomEvents.CancelCreateRoom -> createRoomAction.value = AsyncAction.Uninitialized
|
dataStore.setParentSpace(event.space)
|
||||||
|
}
|
||||||
|
ConfigureRoomEvents.CancelCreateRoom -> {
|
||||||
|
createRoomAction.value = AsyncAction.Uninitialized
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ConfigureRoomState(
|
return ConfigureRoomState(
|
||||||
isKnockFeatureEnabled = isKnockFeatureEnabled,
|
|
||||||
config = createRoomConfig,
|
config = createRoomConfig,
|
||||||
avatarActions = avatarActions,
|
avatarActions = avatarActions,
|
||||||
createRoomAction = createRoomAction.value,
|
createRoomAction = createRoomAction.value,
|
||||||
cameraPermissionState = cameraPermissionState,
|
cameraPermissionState = cameraPermissionState,
|
||||||
homeserverName = homeserverName,
|
homeserverName = homeserverName,
|
||||||
roomAddressValidity = roomAddressValidity.value,
|
roomAddressValidity = roomAddressValidity.value,
|
||||||
|
availableJoinRules = availableJoinRules,
|
||||||
|
spaces = spaces,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -172,25 +220,27 @@ class ConfigureRoomPresenter(
|
||||||
) = launch {
|
) = launch {
|
||||||
suspend {
|
suspend {
|
||||||
val avatarUrl = config.avatarUri?.let { uploadAvatar(it.toUri()) }
|
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(
|
CreateRoomParameters(
|
||||||
name = config.roomName,
|
name = config.roomName,
|
||||||
topic = config.topic,
|
topic = config.topic,
|
||||||
isEncrypted = false,
|
isEncrypted = false,
|
||||||
isDirect = false,
|
isDirect = false,
|
||||||
visibility = RoomVisibility.Public,
|
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,
|
preset = RoomPreset.PUBLIC_CHAT,
|
||||||
invite = config.invites.map { it.userId },
|
invite = config.invites.map { it.userId },
|
||||||
avatar = avatarUrl,
|
avatar = avatarUrl,
|
||||||
roomAliasName = config.roomVisibility.roomAddress(),
|
roomAliasName = config.visibilityState.roomAddress(),
|
||||||
isSpace = isSpace,
|
isSpace = isSpace,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
CreateRoomParameters(
|
CreateRoomParameters(
|
||||||
name = config.roomName,
|
name = config.roomName,
|
||||||
topic = config.topic,
|
topic = config.topic,
|
||||||
isEncrypted = config.roomVisibility is RoomVisibilityState.Private,
|
isEncrypted = config.visibilityState is RoomVisibilityState.Private,
|
||||||
isDirect = false,
|
isDirect = false,
|
||||||
visibility = RoomVisibility.Private,
|
visibility = RoomVisibility.Private,
|
||||||
historyVisibilityOverride = RoomHistoryVisibility.Invited,
|
historyVisibilityOverride = RoomHistoryVisibility.Invited,
|
||||||
|
|
@ -200,7 +250,7 @@ class ConfigureRoomPresenter(
|
||||||
isSpace = isSpace,
|
isSpace = isSpace,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
matrixClient.createRoom(params)
|
val roomId = matrixClient.createRoom(params)
|
||||||
.onFailure { failure ->
|
.onFailure { failure ->
|
||||||
Timber.e(failure, "Failed to create room")
|
Timber.e(failure, "Failed to create room")
|
||||||
}
|
}
|
||||||
|
|
@ -209,7 +259,22 @@ class ConfigureRoomPresenter(
|
||||||
analyticsService.capture(CreatedRoom(isDM = false))
|
analyticsService.capture(CreatedRoom(isDM = false))
|
||||||
}
|
}
|
||||||
.getOrThrow()
|
.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)
|
}.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 {
|
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.architecture.AsyncAction
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||||
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
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.RoomAddressValidity
|
||||||
import io.element.android.libraries.permissions.api.PermissionsState
|
import io.element.android.libraries.permissions.api.PermissionsState
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class ConfigureRoomState(
|
data class ConfigureRoomState(
|
||||||
val isKnockFeatureEnabled: Boolean,
|
|
||||||
val config: CreateRoomConfig,
|
val config: CreateRoomConfig,
|
||||||
val avatarActions: ImmutableList<AvatarAction>,
|
val avatarActions: ImmutableList<AvatarAction>,
|
||||||
val createRoomAction: AsyncAction<RoomId>,
|
val createRoomAction: AsyncAction<RoomId>,
|
||||||
val cameraPermissionState: PermissionsState,
|
val cameraPermissionState: PermissionsState,
|
||||||
val roomAddressValidity: RoomAddressValidity,
|
val roomAddressValidity: RoomAddressValidity,
|
||||||
val homeserverName: String,
|
val homeserverName: String,
|
||||||
|
val availableJoinRules: ImmutableList<JoinRuleItem>,
|
||||||
|
val spaces: ImmutableList<SpaceRoom>,
|
||||||
val eventSink: (ConfigureRoomEvents) -> Unit
|
val eventSink: (ConfigureRoomEvents) -> Unit
|
||||||
) {
|
) {
|
||||||
val isValid: Boolean = config.roomName?.isNotEmpty() == true &&
|
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 androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
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.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.components.aMatrixUserList
|
||||||
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
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.RoomAddressValidity
|
||||||
import io.element.android.libraries.permissions.api.PermissionsState
|
import io.element.android.libraries.permissions.api.PermissionsState
|
||||||
import io.element.android.libraries.permissions.api.aPermissionsState
|
import io.element.android.libraries.permissions.api.aPermissionsState
|
||||||
|
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomState> {
|
open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomState> {
|
||||||
|
|
@ -28,9 +31,9 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
|
||||||
roomName = "Room 101",
|
roomName = "Room 101",
|
||||||
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
||||||
invites = aMatrixUserList().toImmutableList(),
|
invites = aMatrixUserList().toImmutableList(),
|
||||||
roomVisibility = RoomVisibilityState.Public(
|
visibilityState = RoomVisibilityState.Public(
|
||||||
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
||||||
roomAccess = RoomAccess.Knocking,
|
joinRuleItem = JoinRuleItem.PublicVisibility.AskToJoin,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -39,9 +42,9 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
|
||||||
roomName = "Room 101",
|
roomName = "Room 101",
|
||||||
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
||||||
invites = aMatrixUserList().toImmutableList(),
|
invites = aMatrixUserList().toImmutableList(),
|
||||||
roomVisibility = RoomVisibilityState.Public(
|
visibilityState = RoomVisibilityState.Public(
|
||||||
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
||||||
roomAccess = RoomAccess.Knocking,
|
joinRuleItem = JoinRuleItem.PublicVisibility.AskToJoin,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -49,9 +52,9 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
|
||||||
config = CreateRoomConfig(
|
config = CreateRoomConfig(
|
||||||
roomName = "Room 101",
|
roomName = "Room 101",
|
||||||
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
||||||
roomVisibility = RoomVisibilityState.Public(
|
visibilityState = RoomVisibilityState.Public(
|
||||||
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
||||||
roomAccess = RoomAccess.Knocking,
|
joinRuleItem = JoinRuleItem.PublicVisibility.AskToJoin,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
roomAddressValidity = RoomAddressValidity.NotAvailable,
|
roomAddressValidity = RoomAddressValidity.NotAvailable,
|
||||||
|
|
@ -60,9 +63,9 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
|
||||||
config = CreateRoomConfig(
|
config = CreateRoomConfig(
|
||||||
roomName = "Room 101",
|
roomName = "Room 101",
|
||||||
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
||||||
roomVisibility = RoomVisibilityState.Public(
|
visibilityState = RoomVisibilityState.Public(
|
||||||
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
||||||
roomAccess = RoomAccess.Knocking,
|
joinRuleItem = JoinRuleItem.PublicVisibility.AskToJoin,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
roomAddressValidity = RoomAddressValidity.InvalidSymbols,
|
roomAddressValidity = RoomAddressValidity.InvalidSymbols,
|
||||||
|
|
@ -71,9 +74,9 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
|
||||||
config = CreateRoomConfig(
|
config = CreateRoomConfig(
|
||||||
roomName = "Room 101",
|
roomName = "Room 101",
|
||||||
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
||||||
roomVisibility = RoomVisibilityState.Public(
|
visibilityState = RoomVisibilityState.Public(
|
||||||
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
||||||
roomAccess = RoomAccess.Knocking,
|
joinRuleItem = JoinRuleItem.PublicVisibility.AskToJoin,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
roomAddressValidity = RoomAddressValidity.Valid,
|
roomAddressValidity = RoomAddressValidity.Valid,
|
||||||
|
|
@ -83,13 +86,41 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
|
||||||
isSpace = true,
|
isSpace = true,
|
||||||
roomName = "Space 101",
|
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",
|
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"),
|
roomAddress = RoomAddress.AutoFilled("Space-101"),
|
||||||
roomAccess = RoomAccess.Anyone,
|
joinRuleItem = JoinRuleItem.PublicVisibility.Public,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
roomAddressValidity = RoomAddressValidity.Valid,
|
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),
|
cameraPermissionState: PermissionsState = aPermissionsState(showDialog = false),
|
||||||
homeserverName: String = "matrix.org",
|
homeserverName: String = "matrix.org",
|
||||||
roomAddressValidity: RoomAddressValidity = RoomAddressValidity.Valid,
|
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 = { },
|
eventSink: (ConfigureRoomEvents) -> Unit = { },
|
||||||
) = ConfigureRoomState(
|
) = ConfigureRoomState(
|
||||||
config = config,
|
config = config,
|
||||||
isKnockFeatureEnabled = isKnockFeatureEnabled,
|
|
||||||
avatarActions = avatarActions.toImmutableList(),
|
avatarActions = avatarActions.toImmutableList(),
|
||||||
createRoomAction = createRoomAction,
|
createRoomAction = createRoomAction,
|
||||||
cameraPermissionState = cameraPermissionState,
|
cameraPermissionState = cameraPermissionState,
|
||||||
homeserverName = homeserverName,
|
homeserverName = homeserverName,
|
||||||
roomAddressValidity = roomAddressValidity,
|
roomAddressValidity = roomAddressValidity,
|
||||||
|
availableJoinRules = availableVisibilityOptions.toImmutableList(),
|
||||||
|
spaces = spaces.toImmutableList(),
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.createroom.impl.R
|
import io.element.android.features.createroom.impl.R
|
||||||
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
|
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom
|
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.avatar.AvatarType
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
import io.element.android.libraries.designsystem.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.modifiers.clearFocusOnTap
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
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.TextField
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||||
import io.element.android.libraries.matrix.ui.components.AvatarActionBottomSheet
|
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.AvatarPickerState
|
||||||
import io.element.android.libraries.matrix.ui.components.AvatarPickerView
|
import io.element.android.libraries.matrix.ui.components.AvatarPickerView
|
||||||
import io.element.android.libraries.matrix.ui.room.address.RoomAddressField
|
import io.element.android.libraries.matrix.ui.room.address.RoomAddressField
|
||||||
import io.element.android.libraries.permissions.api.PermissionsView
|
import io.element.android.libraries.permissions.api.PermissionsView
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlin.jvm.optionals.getOrNull
|
import kotlin.jvm.optionals.getOrNull
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -119,27 +121,30 @@ fun ConfigureRoomView(
|
||||||
onTopicChange = { state.eventSink(ConfigureRoomEvents.TopicChanged(it)) },
|
onTopicChange = { state.eventSink(ConfigureRoomEvents.TopicChanged(it)) },
|
||||||
)
|
)
|
||||||
|
|
||||||
RoomVisibilityAndAccessOptions(
|
if (!state.config.isSpace && state.spaces.isNotEmpty()) {
|
||||||
selected = when (state.config.roomVisibility) {
|
SelectParentSpaceOptions(
|
||||||
is RoomVisibilityState.Private -> RoomVisibilityItem.Private
|
spaces = state.spaces,
|
||||||
is RoomVisibilityState.Public -> when (state.config.roomVisibility.roomAccess) {
|
selectedSpace = state.config.parentSpace,
|
||||||
RoomAccess.Knocking -> RoomVisibilityItem.AskToJoin
|
onSelectSpace = { state.eventSink(ConfigureRoomEvents.SetParentSpace(it)) },
|
||||||
RoomAccess.Anyone -> RoomVisibilityItem.Public
|
)
|
||||||
}
|
}
|
||||||
},
|
|
||||||
isKnockingEnabled = state.isKnockFeatureEnabled,
|
RoomJoinRuleOptions(
|
||||||
|
options = state.availableJoinRules,
|
||||||
|
selected = state.config.visibilityState.joinRuleItem,
|
||||||
|
parentSpace = state.config.parentSpace,
|
||||||
onOptionClick = {
|
onOptionClick = {
|
||||||
focusManager.clearFocus()
|
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 {
|
Column {
|
||||||
ListSectionHeader(title = stringResource(R.string.screen_create_room_room_address_section_title))
|
ListSectionHeader(title = stringResource(R.string.screen_create_room_room_address_section_title))
|
||||||
RoomAddressField(
|
RoomAddressField(
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
address = state.config.roomVisibility.roomAddress().getOrNull().orEmpty(),
|
address = state.config.visibilityState.roomAddress().getOrNull().orEmpty(),
|
||||||
homeserverName = state.homeserverName,
|
homeserverName = state.homeserverName,
|
||||||
addressValidity = state.roomAddressValidity,
|
addressValidity = state.roomAddressValidity,
|
||||||
onAddressChange = { state.eventSink(ConfigureRoomEvents.RoomAddressChanged(it)) },
|
onAddressChange = { state.eventSink(ConfigureRoomEvents.RoomAddressChanged(it)) },
|
||||||
|
|
@ -264,7 +269,7 @@ private fun RoomTopic(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ConfigureRoomOptions(
|
internal fun ConfigureRoomOptions(
|
||||||
title: String,
|
title: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
content: @Composable ColumnScope.() -> Unit,
|
content: @Composable ColumnScope.() -> Unit,
|
||||||
|
|
@ -278,30 +283,29 @@ private fun ConfigureRoomOptions(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RoomVisibilityAndAccessOptions(
|
private fun RoomJoinRuleOptions(
|
||||||
selected: RoomVisibilityItem,
|
options: ImmutableList<JoinRuleItem>,
|
||||||
isKnockingEnabled: Boolean,
|
selected: JoinRuleItem,
|
||||||
onOptionClick: (RoomVisibilityItem) -> Unit,
|
onOptionClick: (JoinRuleItem) -> Unit,
|
||||||
|
parentSpace: SpaceRoom?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
ConfigureRoomOptions(
|
ConfigureRoomOptions(
|
||||||
title = stringResource(R.string.screen_create_room_room_access_section_title),
|
title = stringResource(R.string.screen_create_room_room_access_section_title),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
RoomVisibilityItem.entries.forEach { item ->
|
options.forEach { item ->
|
||||||
if (item == RoomVisibilityItem.AskToJoin && !isKnockingEnabled) {
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
|
|
||||||
val isSelected = item == selected
|
val isSelected = item == selected
|
||||||
ListItem(
|
ListItem(
|
||||||
leadingContent = ListItemContent.Custom {
|
leadingContent = ListItemContent.Custom {
|
||||||
RoundedIconAtom(
|
RoundedIconAtom(
|
||||||
size = RoundedIconAtomSize.Big,
|
size = RoundedIconAtomSize.Big,
|
||||||
resourceId = when (item) {
|
imageVector = when (item) {
|
||||||
RoomVisibilityItem.Public -> CompoundDrawables.ic_compound_public
|
JoinRuleItem.PublicVisibility.Public -> CompoundIcons.Public()
|
||||||
RoomVisibilityItem.AskToJoin -> CompoundDrawables.ic_compound_user_add
|
is JoinRuleItem.PublicVisibility.Restricted -> CompoundIcons.Space()
|
||||||
RoomVisibilityItem.Private -> CompoundDrawables.ic_compound_lock
|
JoinRuleItem.PublicVisibility.AskToJoin,
|
||||||
|
is JoinRuleItem.PublicVisibility.AskToJoinRestricted -> CompoundIcons.UserAdd()
|
||||||
|
JoinRuleItem.Private -> CompoundIcons.Lock()
|
||||||
},
|
},
|
||||||
tint = if (isSelected) ElementTheme.colors.iconPrimary else ElementTheme.colors.iconSecondary,
|
tint = if (isSelected) ElementTheme.colors.iconPrimary else ElementTheme.colors.iconSecondary,
|
||||||
backgroundTint = Color.Transparent,
|
backgroundTint = Color.Transparent,
|
||||||
|
|
@ -309,18 +313,29 @@ private fun RoomVisibilityAndAccessOptions(
|
||||||
},
|
},
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
val title = when (item) {
|
val title = when (item) {
|
||||||
RoomVisibilityItem.Public -> stringResource(R.string.screen_create_room_public_option_title)
|
JoinRuleItem.PublicVisibility.Public -> stringResource(R.string.screen_create_room_room_access_section_public_option_title)
|
||||||
RoomVisibilityItem.AskToJoin -> stringResource(R.string.screen_create_room_room_access_section_knocking_option_title)
|
is JoinRuleItem.PublicVisibility.Restricted -> stringResource(R.string.screen_create_room_room_access_section_restricted_option_title)
|
||||||
RoomVisibilityItem.Private -> stringResource(R.string.screen_create_room_private_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)
|
Text(text = title)
|
||||||
},
|
},
|
||||||
supportingContent = {
|
supportingContent = {
|
||||||
// TODO handle description of items in a certain space/org
|
|
||||||
val description = when (item) {
|
val description = when (item) {
|
||||||
RoomVisibilityItem.Public -> stringResource(R.string.screen_create_room_public_option_short_description)
|
JoinRuleItem.PublicVisibility.Public -> stringResource(R.string.screen_create_room_room_access_section_public_option_description)
|
||||||
RoomVisibilityItem.AskToJoin -> stringResource(R.string.screen_create_room_room_access_section_knocking_option_description)
|
is JoinRuleItem.PublicVisibility.Restricted -> stringResource(
|
||||||
RoomVisibilityItem.Private -> stringResource(R.string.screen_create_room_private_option_description)
|
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)
|
Text(text = description)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl.configureroom
|
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 io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
@ -18,5 +19,6 @@ data class CreateRoomConfig(
|
||||||
val topic: String? = null,
|
val topic: String? = null,
|
||||||
val avatarUri: String? = null,
|
val avatarUri: String? = null,
|
||||||
val invites: ImmutableList<MatrixUser> = persistentListOf(),
|
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 dev.zacsweers.metro.Inject
|
||||||
import io.element.android.libraries.androidutils.file.safeDelete
|
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.room.alias.RoomAliasHelper
|
||||||
|
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.getAndUpdate
|
import kotlinx.coroutines.flow.getAndUpdate
|
||||||
|
|
@ -33,23 +34,20 @@ class CreateRoomConfigStore(
|
||||||
|
|
||||||
fun setRoomName(roomName: String) {
|
fun setRoomName(roomName: String) {
|
||||||
createRoomConfigFlow.getAndUpdate { config ->
|
createRoomConfigFlow.getAndUpdate { config ->
|
||||||
val newVisibility = when (config.roomVisibility) {
|
val roomAccessWithNewAddress = if (config.visibilityState is RoomVisibilityState.Public) {
|
||||||
is RoomVisibilityState.Public -> {
|
val roomAddress = config.visibilityState.roomAddress
|
||||||
val roomAddress = config.roomVisibility.roomAddress
|
if (roomAddress is RoomAddress.AutoFilled || roomName.isEmpty()) {
|
||||||
if (roomAddress is RoomAddress.AutoFilled || roomName.isEmpty()) {
|
val roomAliasName = roomAliasHelper.roomAliasNameFromRoomDisplayName(roomName)
|
||||||
val roomAliasName = roomAliasHelper.roomAliasNameFromRoomDisplayName(roomName)
|
config.visibilityState.copy(roomAddress = RoomAddress.AutoFilled(roomAliasName))
|
||||||
config.roomVisibility.copy(
|
} else {
|
||||||
roomAddress = RoomAddress.AutoFilled(roomAliasName),
|
config.visibilityState
|
||||||
)
|
|
||||||
} else {
|
|
||||||
config.roomVisibility
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else -> config.roomVisibility
|
} else {
|
||||||
|
config.visibilityState
|
||||||
}
|
}
|
||||||
config.copy(
|
config.copy(
|
||||||
roomName = roomName.takeIf { it.isNotEmpty() },
|
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 ->
|
createRoomConfigFlow.getAndUpdate { config ->
|
||||||
config.copy(
|
config.copy(
|
||||||
roomVisibility = when (visibility) {
|
visibilityState = when (joinRule) {
|
||||||
RoomVisibilityItem.Private -> RoomVisibilityState.Private
|
JoinRuleItem.Private -> RoomVisibilityState.Private()
|
||||||
RoomVisibilityItem.Public, RoomVisibilityItem.AskToJoin -> {
|
is JoinRuleItem.PublicVisibility -> {
|
||||||
val roomAliasName = roomAliasHelper.roomAliasNameFromRoomDisplayName(config.roomName.orEmpty())
|
val roomAliasName = roomAliasHelper.roomAliasNameFromRoomDisplayName(config.roomName.orEmpty())
|
||||||
RoomVisibilityState.Public(
|
RoomVisibilityState.Public(
|
||||||
roomAddress = RoomAddress.AutoFilled(roomAliasName),
|
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) {
|
fun setRoomAddress(address: String) {
|
||||||
createRoomConfigFlow.getAndUpdate { config ->
|
createRoomConfigFlow.getAndUpdate { config ->
|
||||||
config.copy(
|
config.copy(
|
||||||
roomVisibility = when (config.roomVisibility) {
|
visibilityState = when (config.visibilityState) {
|
||||||
is RoomVisibilityState.Public -> {
|
is RoomVisibilityState.Public -> {
|
||||||
val sanitizedAddress = address.lowercase()
|
val sanitizedAddress = address.lowercase()
|
||||||
config.roomVisibility.copy(roomAddress = RoomAddress.Edited(sanitizedAddress))
|
config.visibilityState.copy(roomAddress = RoomAddress.Edited(sanitizedAddress))
|
||||||
}
|
}
|
||||||
else -> config.roomVisibility
|
else -> config.visibilityState
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -120,6 +105,15 @@ class CreateRoomConfigStore(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setParentSpace(parentSpace: SpaceRoom?) {
|
||||||
|
createRoomConfigFlow.getAndUpdate { config ->
|
||||||
|
config.copy(
|
||||||
|
parentSpace = parentSpace,
|
||||||
|
visibilityState = RoomVisibilityState.Private(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun clearCachedData() {
|
fun clearCachedData() {
|
||||||
cachedAvatarUri = null
|
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
|
import java.util.Optional
|
||||||
|
|
||||||
sealed interface RoomVisibilityState {
|
sealed interface RoomVisibilityState {
|
||||||
data object Private : RoomVisibilityState
|
val joinRuleItem: JoinRuleItem
|
||||||
|
data class Private(override val joinRuleItem: JoinRuleItem.Private = JoinRuleItem.Private) : RoomVisibilityState
|
||||||
|
|
||||||
data class Public(
|
data class Public(
|
||||||
val roomAddress: RoomAddress,
|
val roomAddress: RoomAddress,
|
||||||
val roomAccess: RoomAccess,
|
override val joinRuleItem: JoinRuleItem.PublicVisibility,
|
||||||
) : RoomVisibilityState
|
) : RoomVisibilityState
|
||||||
|
|
||||||
fun roomAddress(): Optional<String> {
|
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_action_create_room">"Nyt rum"</string>
|
||||||
<string name="screen_create_room_add_people_title">"Invitér andre"</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_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.
|
<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>
|
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_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_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_access_section_public_option_title">"Offentlig"</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_access_section_restricted_option_description">"Alle i %1$s kan deltage."</string>
|
||||||
<string name="screen_create_room_room_address_section_title">"Rummets adresse"</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_room_visibility_section_title">"Rummets synlighed"</string>
|
||||||
<string name="screen_create_room_topic_label">"Emne (valgfrit)"</string>
|
<string name="screen_create_room_topic_label">"Emne (valgfrit)"</string>
|
||||||
|
<string name="screen_create_room_topic_placeholder">"Tilføj beskrivelse…"</string>
|
||||||
</resources>
|
</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_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_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_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_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_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_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_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_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_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_address_section_title">"Adresse"</string>
|
||||||
<string name="screen_create_room_room_visibility_section_title">" Sichtbarkeit des Chats"</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_label">"Thema (optional)"</string>
|
||||||
<string name="screen_create_room_topic_placeholder">"Beschreibung hinzufügen…"</string>
|
<string name="screen_create_room_topic_placeholder">"Beschreibung hinzufügen…"</string>
|
||||||
</resources>
|
</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_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_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_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_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_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_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_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_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_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_address_section_title">"Adresse"</string>
|
||||||
<string name="screen_create_room_room_visibility_section_title">"Visibilité du salon"</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_label">"Sujet (facultatif)"</string>
|
||||||
<string name="screen_create_room_topic_placeholder">"Ajouter une description…"</string>
|
<string name="screen_create_room_topic_placeholder">"Ajouter une description…"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,33 @@
|
||||||
<string name="screen_create_room_action_create_room">"Új szoba"</string>
|
<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_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_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.
|
<string name="screen_create_room_public_option_description">"Bárki megtalálhatja ezt a szobát.
|
||||||
Ezt bármikor módosíthatja a szobabeállításokban."</string>
|
Ezt bármikor módosíthatja a szobabeállításokban."</string>
|
||||||
|
<string name="screen_create_room_public_option_short_description">"Bárki csatlakozhat."</string>
|
||||||
<string name="screen_create_room_public_option_title">"Nyilvános szoba"</string>
|
<string name="screen_create_room_public_option_title">"Nyilvános szoba"</string>
|
||||||
<string name="screen_create_room_room_access_section_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_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_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_public_option_title">"Bárki"</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_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_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_label">"Téma (nem kötelező)"</string>
|
||||||
|
<string name="screen_create_room_topic_placeholder">"Leírás hozzáadása…"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,17 @@
|
||||||
<string name="screen_create_room_action_create_room">"Nytt rom"</string>
|
<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_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_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.
|
<string name="screen_create_room_public_option_description">"Alle kan finne dette rommet.
|
||||||
Du kan endre dette når som helst i rominnstillingene."</string>
|
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_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_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_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_footer">"Du trenger en adresse for å gjøre den synlig i den offentlige katalogen."</string>
|
||||||
<string name="screen_create_room_room_address_section_title">"Romadresse"</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_room_visibility_section_title">"Romsynlighet"</string>
|
||||||
<string name="screen_create_room_topic_label">"Emne (valgfritt)"</string>
|
<string name="screen_create_room_topic_label">"Emne (valgfritt)"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@
|
||||||
<string name="screen_create_room_add_people_title">"Convidar pessoas"</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_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_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_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_description">"Apenas pessoas convidadas podem entrar."</string>
|
||||||
<string name="screen_create_room_private_option_title">"Privada"</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_short_description">"Qualquer um pode entrar."</string>
|
||||||
<string name="screen_create_room_public_option_title">"Publica"</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_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_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_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_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_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 da sala"</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_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_label">"Tópico (opcional)"</string>
|
||||||
|
<string name="screen_create_room_topic_placeholder">"Adicione a descrição…"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,17 @@
|
||||||
<string name="screen_create_room_action_create_room">"Nova sala"</string>
|
<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_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_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.
|
<string name="screen_create_room_public_option_description">"Qualquer um pode encontrar esta sala.
|
||||||
Pode alterar esta opção nas definições da sala."</string>
|
Pode alterar esta opção nas definições da sala."</string>
|
||||||
<string name="screen_create_room_public_option_title">"Sala pública"</string>
|
<string name="screen_create_room_public_option_title">"Sala pública"</string>
|
||||||
<string name="screen_create_room_room_access_section_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_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_description">"Qualquer pessoa 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_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 de salas, precisas de um endereço de sala."</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 da sala"</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_room_visibility_section_title">"Visibilidade da sala"</string>
|
||||||
<string name="screen_create_room_topic_label">"Descrição (opcional)"</string>
|
<string name="screen_create_room_topic_label">"Descrição (opcional)"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,26 @@
|
||||||
<string name="screen_create_room_action_create_room">"Создать новую комнату"</string>
|
<string name="screen_create_room_action_create_room">"Создать новую комнату"</string>
|
||||||
<string name="screen_create_room_add_people_title">"Пригласить в комнату"</string>
|
<string name="screen_create_room_add_people_title">"Пригласить в комнату"</string>
|
||||||
<string name="screen_create_room_error_creating_room">"Произошла ошибка при создании комнаты"</string>
|
<string name="screen_create_room_error_creating_room">"Произошла ошибка при создании комнаты"</string>
|
||||||
<string name="screen_create_room_private_option_description">"Доступ в эту комнату имеют только приглашенные пользователи. Все сообщения защищены сквозным шифрованием."</string>
|
<string name="screen_create_room_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 name="screen_create_room_public_option_description">"Любой желающий может найти эту комнату.
|
||||||
Вы можете изменить это в любое время в настройках комнаты."</string>
|
Вы можете изменить это в любое время в настройках комнаты."</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_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_description">"Любой желающий может подать заявку на присоединение к комнате, но администратор или модератор должен будет принять запрос."</string>
|
||||||
<string name="screen_create_room_room_access_section_knocking_option_title">"Попросить присоединиться"</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_private_option_description">"Присоединиться могут только приглашенные."</string>
|
||||||
<string name="screen_create_room_room_access_section_public_option_title">"Любой"</string>
|
<string name="screen_create_room_room_access_section_private_option_title">"Частный"</string>
|
||||||
<string name="screen_create_room_room_address_section_footer">"Чтобы эта комната была видна в каталоге общедоступных, вам необходим ее адрес"</string>
|
<string name="screen_create_room_room_access_section_public_option_description">"Присоединиться может любой желающий."</string>
|
||||||
<string name="screen_create_room_room_address_section_title">"Адрес комнаты"</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_room_visibility_section_title">"Видимость комнаты"</string>
|
||||||
<string name="screen_create_room_topic_label">"Тема (необязательно)"</string>
|
<string name="screen_create_room_topic_label">"Тема (необязательно)"</string>
|
||||||
|
<string name="screen_create_room_topic_placeholder">"Добавить описание…"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,33 @@
|
||||||
<string name="screen_create_room_action_create_room">"Nová miestnosť"</string>
|
<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_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_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.
|
<string name="screen_create_room_public_option_description">"Túto miestnosť môže nájsť ktokoľvek.
|
||||||
Môžete to kedykoľvek zmeniť v nastaveniach miestnosti."</string>
|
Môžete to kedykoľvek zmeniť v nastaveniach miestnosti."</string>
|
||||||
|
<string name="screen_create_room_public_option_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_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_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">"Požiadať o pripojenie"</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_public_option_description">"Do tejto miestnosti sa môže pripojiť ktokoľvek"</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_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_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_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_label">"Téma (voliteľné)"</string>
|
||||||
|
<string name="screen_create_room_topic_placeholder">"Pridať popis…"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@
|
||||||
<string name="screen_create_room_name_placeholder">"Add name…"</string>
|
<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_room_title">"New room"</string>
|
||||||
<string name="screen_create_room_new_space_title">"New space"</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_description">"Only people invited can join."</string>
|
||||||
<string name="screen_create_room_private_option_title">"Private"</string>
|
<string name="screen_create_room_private_option_title">"Private"</string>
|
||||||
<string name="screen_create_room_public_option_description">"Anyone can find this room.
|
<string name="screen_create_room_public_option_description">"Anyone can find this room.
|
||||||
|
|
@ -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_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_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_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_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_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_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_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_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_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_address_section_title">"Address"</string>
|
||||||
<string name="screen_create_room_room_visibility_section_title">"Room visibility"</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_label">"Topic (optional)"</string>
|
||||||
<string name="screen_create_room_topic_placeholder">"Add description…"</string>
|
<string name="screen_create_room_topic_placeholder">"Add description…"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
* Copyright 2023-2025 New Vector Ltd.
|
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* 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 android.net.Uri
|
||||||
import androidx.core.net.toUri
|
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.ConfigureRoomState
|
||||||
import io.element.android.features.createroom.impl.configureroom.CreateRoomConfig
|
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.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.RoomAddress
|
||||||
import io.element.android.features.createroom.impl.configureroom.RoomVisibilityItem
|
|
||||||
import io.element.android.features.createroom.impl.configureroom.RoomVisibilityState
|
import io.element.android.features.createroom.impl.configureroom.RoomVisibilityState
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomInfo
|
||||||
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
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.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_AVATAR_URL
|
||||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
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_MESSAGE
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
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.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.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.media.AvatarAction
|
||||||
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity
|
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity
|
||||||
import io.element.android.libraries.mediapickers.api.PickerProvider
|
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.api.PermissionsPresenter
|
||||||
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
|
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
|
||||||
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
|
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
|
||||||
|
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.test
|
import io.element.android.tests.testutils.test
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.test.advanceUntilIdle
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
|
@ -81,7 +89,7 @@ class ConfigureRoomPresenterTest {
|
||||||
assertThat(initialState.config.topic).isNull()
|
assertThat(initialState.config.topic).isNull()
|
||||||
assertThat(initialState.config.invites).isEmpty()
|
assertThat(initialState.config.invites).isEmpty()
|
||||||
assertThat(initialState.config.avatarUri).isNull()
|
assertThat(initialState.config.avatarUri).isNull()
|
||||||
assertThat(initialState.config.roomVisibility).isEqualTo(RoomVisibilityState.Private)
|
assertThat(initialState.config.visibilityState).isEqualTo(RoomVisibilityState.Private())
|
||||||
assertThat(initialState.createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
assertThat(initialState.createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||||
assertThat(initialState.homeserverName).isEqualTo("matrix.org")
|
assertThat(initialState.homeserverName).isEqualTo("matrix.org")
|
||||||
}
|
}
|
||||||
|
|
@ -174,12 +182,12 @@ class ConfigureRoomPresenterTest {
|
||||||
assertThat(newState.config).isEqualTo(expectedConfig)
|
assertThat(newState.config).isEqualTo(expectedConfig)
|
||||||
|
|
||||||
// Room privacy
|
// Room privacy
|
||||||
newState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public))
|
newState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
|
||||||
newState = awaitItem()
|
newState = awaitItem()
|
||||||
expectedConfig = expectedConfig.copy(
|
expectedConfig = expectedConfig.copy(
|
||||||
roomVisibility = RoomVisibilityState.Public(
|
visibilityState = RoomVisibilityState.Public(
|
||||||
roomAddress = RoomAddress.AutoFilled(roomAliasHelper.roomAliasNameFromRoomDisplayName(expectedConfig.roomName ?: "")),
|
roomAddress = RoomAddress.AutoFilled(roomAliasHelper.roomAliasNameFromRoomDisplayName(expectedConfig.roomName ?: "")),
|
||||||
roomAccess = RoomAccess.Anyone,
|
joinRuleItem = JoinRuleItem.PublicVisibility.Public,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assertThat(newState.config).isEqualTo(expectedConfig)
|
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
|
@Test
|
||||||
fun `present - record analytics when creating room`() = runTest {
|
fun `present - record analytics when creating room`() = runTest {
|
||||||
val matrixClient = createMatrixClient()
|
val matrixClient = createMatrixClient()
|
||||||
|
|
@ -311,7 +422,7 @@ class ConfigureRoomPresenterTest {
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = initialState()
|
val initialState = initialState()
|
||||||
initialState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public))
|
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("invalid address"))
|
initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("invalid address"))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
@ -331,7 +442,7 @@ class ConfigureRoomPresenterTest {
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = initialState()
|
val initialState = initialState()
|
||||||
initialState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public))
|
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("address"))
|
initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("address"))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
@ -351,7 +462,7 @@ class ConfigureRoomPresenterTest {
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = initialState()
|
val initialState = initialState()
|
||||||
initialState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public))
|
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("address"))
|
initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("address"))
|
||||||
skipItems(1)
|
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 {
|
private suspend fun TurbineTestContext<ConfigureRoomState>.initialState(): ConfigureRoomState {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
return awaitItem()
|
return awaitItem()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createMatrixClient(isAliasAvailable: Boolean = true) = FakeMatrixClient(
|
private fun createMatrixClient(
|
||||||
|
isAliasAvailable: Boolean = true,
|
||||||
|
spaceService: FakeSpaceService = FakeSpaceService(),
|
||||||
|
) = FakeMatrixClient(
|
||||||
userIdServerNameLambda = { "matrix.org" },
|
userIdServerNameLambda = { "matrix.org" },
|
||||||
resolveRoomAliasResult = {
|
resolveRoomAliasResult = {
|
||||||
val resolvedRoomAlias = if (isAliasAvailable) {
|
val resolvedRoomAlias = if (isAliasAvailable) {
|
||||||
|
|
@ -376,11 +526,13 @@ class ConfigureRoomPresenterTest {
|
||||||
Optional.of(ResolvedRoomAlias(A_ROOM_ID, emptyList()))
|
Optional.of(ResolvedRoomAlias(A_ROOM_ID, emptyList()))
|
||||||
}
|
}
|
||||||
Result.success(resolvedRoomAlias)
|
Result.success(resolvedRoomAlias)
|
||||||
}
|
},
|
||||||
|
spaceService = spaceService,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun createConfigureRoomPresenter(
|
private fun createConfigureRoomPresenter(
|
||||||
isSpace: Boolean = false,
|
isSpace: Boolean = false,
|
||||||
|
initialParenSpaceId: RoomId? = null,
|
||||||
roomAliasHelper: RoomAliasHelper = FakeRoomAliasHelper(),
|
roomAliasHelper: RoomAliasHelper = FakeRoomAliasHelper(),
|
||||||
dataStore: CreateRoomConfigStore = CreateRoomConfigStore(roomAliasHelper),
|
dataStore: CreateRoomConfigStore = CreateRoomConfigStore(roomAliasHelper),
|
||||||
matrixClient: MatrixClient = createMatrixClient(),
|
matrixClient: MatrixClient = createMatrixClient(),
|
||||||
|
|
@ -392,6 +544,7 @@ class ConfigureRoomPresenterTest {
|
||||||
mediaOptimizationConfigProvider: FakeMediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(),
|
mediaOptimizationConfigProvider: FakeMediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(),
|
||||||
) = ConfigureRoomPresenter(
|
) = ConfigureRoomPresenter(
|
||||||
isSpace = isSpace,
|
isSpace = isSpace,
|
||||||
|
initialParentSpaceId = initialParenSpaceId,
|
||||||
dataStore = dataStore,
|
dataStore = dataStore,
|
||||||
matrixClient = matrixClient,
|
matrixClient = matrixClient,
|
||||||
mediaPickerProvider = pickerProvider,
|
mediaPickerProvider = pickerProvider,
|
||||||
|
|
@ -14,6 +14,7 @@ import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
import io.element.android.tests.testutils.node.TestParentNode
|
import io.element.android.tests.testutils.node.TestParentNode
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
|
@ -36,15 +37,16 @@ class DefaultCreateRoomEntryPointTest {
|
||||||
plugins = plugins,
|
plugins = plugins,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
val buildContext = BuildContext.root(null)
|
||||||
|
|
||||||
val callback = object : CreateRoomEntryPoint.Callback {
|
val callback = object : CreateRoomEntryPoint.Callback {
|
||||||
override fun onRoomCreated(roomId: RoomId) = lambdaError()
|
override fun onRoomCreated(roomId: RoomId) = lambdaError()
|
||||||
}
|
}
|
||||||
val result = entryPoint.createNode(
|
val result = entryPoint
|
||||||
isSpace = false,
|
.builder(parentNode, buildContext, callback)
|
||||||
parentNode = parentNode,
|
.setIsSpace(true)
|
||||||
buildContext = BuildContext.root(null),
|
.setParentSpace(A_ROOM_ID)
|
||||||
callback = callback,
|
.build()
|
||||||
)
|
|
||||||
assertThat(result.plugins).contains(callback)
|
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 {
|
dependencies {
|
||||||
implementation(projects.features.createroom.api)
|
implementation(projects.features.createroom.api)
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.tests.testutils)
|
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.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
|
||||||
class FakeCreateRoomEntryPoint : CreateRoomEntryPoint {
|
class FakeCreateRoomEntryPoint : CreateRoomEntryPoint {
|
||||||
override fun createNode(
|
class Builder : CreateRoomEntryPoint.Builder {
|
||||||
isSpace: Boolean,
|
override fun setIsSpace(isSpace: Boolean): Builder = this
|
||||||
|
override fun setParentSpace(parentSpaceId: RoomId): Builder = this
|
||||||
|
override fun build(): Node = lambdaError()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun builder(
|
||||||
parentNode: Node,
|
parentNode: Node,
|
||||||
buildContext: BuildContext,
|
buildContext: BuildContext,
|
||||||
callback: CreateRoomEntryPoint.Callback,
|
callback: CreateRoomEntryPoint.Callback,
|
||||||
): Node = lambdaError()
|
): Builder = lambdaError()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -94,9 +94,9 @@ class HomePresenter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(homeSpacesState.spaceRooms.isEmpty()) {
|
LaunchedEffect(homeSpacesState.canCreateSpaces, homeSpacesState.spaceRooms.isEmpty()) {
|
||||||
// If the last space is left, ensure that the Chat view is rendered.
|
// If the flag to create spaces is disabled and the last space is left, ensure that the Chat view is rendered.
|
||||||
if (homeSpacesState.spaceRooms.isEmpty()) {
|
if (!homeSpacesState.canCreateSpaces && homeSpacesState.spaceRooms.isEmpty()) {
|
||||||
currentHomeNavigationBarItemOrdinal = HomeNavigationBarItem.Chats.ordinal
|
currentHomeNavigationBarItemOrdinal = HomeNavigationBarItem.Chats.ordinal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,5 +33,5 @@ data class HomeState(
|
||||||
) {
|
) {
|
||||||
val displayActions = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats
|
val displayActions = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats
|
||||||
val displayRoomListFilters = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats && roomListState.displayFilters
|
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,
|
lazyListState = spacesLazyListState,
|
||||||
onSpaceClick = { spaceId ->
|
onSpaceClick = { spaceId ->
|
||||||
onRoomClick(spaceId)
|
onRoomClick(spaceId)
|
||||||
}
|
},
|
||||||
|
onCreateSpaceClick = onCreateSpaceClick,
|
||||||
|
// TODO use actual callbacks for this
|
||||||
|
onExploreClick = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,9 @@
|
||||||
|
|
||||||
package io.element.android.features.home.impl.search
|
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.datasource.RoomListRoomSummaryFactory
|
||||||
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
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 io.element.android.libraries.matrix.api.roomlist.loadAllIncrementally
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
|
@ -25,16 +28,23 @@ import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
private const val PAGE_SIZE = 30
|
private const val PAGE_SIZE = 30
|
||||||
|
|
||||||
@Inject
|
@AssistedInject
|
||||||
class RoomListSearchDataSource(
|
class RoomListSearchDataSource(
|
||||||
|
@Assisted coroutineScope: CoroutineScope,
|
||||||
roomListService: RoomListService,
|
roomListService: RoomListService,
|
||||||
coroutineDispatchers: CoroutineDispatchers,
|
coroutineDispatchers: CoroutineDispatchers,
|
||||||
private val roomSummaryFactory: RoomListRoomSummaryFactory,
|
private val roomSummaryFactory: RoomListRoomSummaryFactory,
|
||||||
) {
|
) {
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory {
|
||||||
|
fun create(coroutineScope: CoroutineScope): RoomListSearchDataSource
|
||||||
|
}
|
||||||
|
|
||||||
private val roomList = roomListService.createRoomList(
|
private val roomList = roomListService.createRoomList(
|
||||||
pageSize = PAGE_SIZE,
|
pageSize = PAGE_SIZE,
|
||||||
initialFilter = RoomListFilter.None,
|
initialFilter = RoomListFilter.None,
|
||||||
source = RoomList.Source.All,
|
source = RoomList.Source.All,
|
||||||
|
coroutineScope = coroutineScope
|
||||||
)
|
)
|
||||||
|
|
||||||
val roomSummaries: Flow<ImmutableList<RoomListRoomSummary>> = roomList.filteredSummaries
|
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.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
|
@ -23,7 +24,7 @@ import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
class RoomListSearchPresenter(
|
class RoomListSearchPresenter(
|
||||||
private val dataSource: RoomListSearchDataSource,
|
private val dataSourceFactory: RoomListSearchDataSource.Factory,
|
||||||
) : Presenter<RoomListSearchState> {
|
) : Presenter<RoomListSearchState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): RoomListSearchState {
|
override fun present(): RoomListSearchState {
|
||||||
|
|
@ -33,6 +34,9 @@ class RoomListSearchPresenter(
|
||||||
}
|
}
|
||||||
val searchQuery = rememberTextFieldState()
|
val searchQuery = rememberTextFieldState()
|
||||||
|
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val dataSource = remember { dataSourceFactory.create(coroutineScope) }
|
||||||
|
|
||||||
LaunchedEffect(isSearchActive) {
|
LaunchedEffect(isSearchActive) {
|
||||||
dataSource.setIsActive(isSearchActive)
|
dataSource.setIsActive(isSearchActive)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,8 @@ class HomeSpacesPresenter(
|
||||||
seenSpaceInvites = seenSpaceInvites,
|
seenSpaceInvites = seenSpaceInvites,
|
||||||
hideInvitesAvatar = hideInvitesAvatar,
|
hideInvitesAvatar = hideInvitesAvatar,
|
||||||
canCreateSpaces = canCreateSpaces,
|
canCreateSpaces = canCreateSpaces,
|
||||||
|
// TODO enable once we can link to the screen to explore public spaces
|
||||||
|
canExploreSpaces = false,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ data class HomeSpacesState(
|
||||||
val seenSpaceInvites: ImmutableSet<RoomId>,
|
val seenSpaceInvites: ImmutableSet<RoomId>,
|
||||||
val hideInvitesAvatar: Boolean,
|
val hideInvitesAvatar: Boolean,
|
||||||
val canCreateSpaces: Boolean,
|
val canCreateSpaces: Boolean,
|
||||||
|
val canExploreSpaces: Boolean,
|
||||||
val eventSink: (HomeSpacesEvents) -> Unit,
|
val eventSink: (HomeSpacesEvents) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,11 @@ open class HomeSpacesStateProvider : PreviewParameterProvider<HomeSpacesState> {
|
||||||
spaceRooms = aListOfSpaceRooms(),
|
spaceRooms = aListOfSpaceRooms(),
|
||||||
canCreateSpaces = false,
|
canCreateSpaces = false,
|
||||||
),
|
),
|
||||||
|
aHomeSpacesState(
|
||||||
|
space = CurrentSpace.Root,
|
||||||
|
spaceRooms = emptyList(),
|
||||||
|
canCreateSpaces = true,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,6 +51,7 @@ internal fun aHomeSpacesState(
|
||||||
seenSpaceInvites: Set<RoomId> = emptySet(),
|
seenSpaceInvites: Set<RoomId> = emptySet(),
|
||||||
hideInvitesAvatar: Boolean = false,
|
hideInvitesAvatar: Boolean = false,
|
||||||
canCreateSpaces: Boolean = true,
|
canCreateSpaces: Boolean = true,
|
||||||
|
canExploreSpaces: Boolean = true,
|
||||||
eventSink: (HomeSpacesEvents) -> Unit = {},
|
eventSink: (HomeSpacesEvents) -> Unit = {},
|
||||||
) = HomeSpacesState(
|
) = HomeSpacesState(
|
||||||
space = space,
|
space = space,
|
||||||
|
|
@ -53,6 +59,7 @@ internal fun aHomeSpacesState(
|
||||||
seenSpaceInvites = seenSpaceInvites.toImmutableSet(),
|
seenSpaceInvites = seenSpaceInvites.toImmutableSet(),
|
||||||
hideInvitesAvatar = hideInvitesAvatar,
|
hideInvitesAvatar = hideInvitesAvatar,
|
||||||
canCreateSpaces = canCreateSpaces,
|
canCreateSpaces = canCreateSpaces,
|
||||||
|
canExploreSpaces = canExploreSpaces,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,23 +8,40 @@
|
||||||
|
|
||||||
package io.element.android.features.home.impl.spaces
|
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.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import 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.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Button
|
||||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
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.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
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.SpaceHeaderRootView
|
||||||
import io.element.android.libraries.matrix.ui.components.SpaceHeaderView
|
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.components.SpaceRoomItemView
|
||||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -32,56 +49,119 @@ fun HomeSpacesView(
|
||||||
state: HomeSpacesState,
|
state: HomeSpacesState,
|
||||||
lazyListState: LazyListState,
|
lazyListState: LazyListState,
|
||||||
onSpaceClick: (RoomId) -> Unit,
|
onSpaceClick: (RoomId) -> Unit,
|
||||||
|
onCreateSpaceClick: () -> Unit,
|
||||||
|
onExploreClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
if (state.canCreateSpaces && state.spaceRooms.isEmpty()) {
|
||||||
modifier = modifier,
|
EmptySpaceHomeView(
|
||||||
state = lazyListState
|
modifier = modifier,
|
||||||
) {
|
onCreateSpaceClick = onCreateSpaceClick,
|
||||||
val space = state.space
|
onExploreClick = onExploreClick,
|
||||||
when (space) {
|
canExploreSpaces = state.canExploreSpaces,
|
||||||
CurrentSpace.Root -> {
|
)
|
||||||
item {
|
} else {
|
||||||
SpaceHeaderRootView(numberOfSpaces = state.spaceRooms.size)
|
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(
|
item {
|
||||||
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) {
|
|
||||||
HorizontalDivider()
|
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
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun HomeSpacesViewPreview(
|
internal fun HomeSpacesViewPreview(
|
||||||
|
|
@ -91,6 +171,7 @@ internal fun HomeSpacesViewPreview(
|
||||||
state = state,
|
state = state,
|
||||||
lazyListState = rememberLazyListState(),
|
lazyListState = rememberLazyListState(),
|
||||||
onSpaceClick = {},
|
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_read">"Marker som læst"</string>
|
||||||
<string name="screen_roomlist_mark_as_unread">"Marker som ulæ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_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_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>
|
<string name="session_verification_banner_title">"Bekræft, at det er dig"</string>
|
||||||
</resources>
|
</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_read">"Als gelesen markieren"</string>
|
||||||
<string name="screen_roomlist_mark_as_unread">"Als ungelesen 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_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_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>
|
<string name="session_verification_banner_title">"Verifiziere deine Identität"</string>
|
||||||
</resources>
|
</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_read">"Marquer comme lu"</string>
|
||||||
<string name="screen_roomlist_mark_as_unread">"Marquer comme non 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_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_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>
|
<string name="session_verification_banner_title">"Vérifier que c’est bien vous"</string>
|
||||||
</resources>
|
</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_read">"Megjelölés olvasottként"</string>
|
||||||
<string name="screen_roomlist_mark_as_unread">"Megjelölés olvasatlanké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_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_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>
|
<string name="session_verification_banner_title">"Ellenőrizze, hogy Ön az"</string>
|
||||||
</resources>
|
</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_read">"Marcar como lida"</string>
|
||||||
<string name="screen_roomlist_mark_as_unread">"Marcar como não 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_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_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>
|
<string name="session_verification_banner_title">"Verifique se é você"</string>
|
||||||
</resources>
|
</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_read">"Označiť ako prečítané"</string>
|
||||||
<string name="screen_roomlist_mark_as_unread">"Označiť ako neprečí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_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_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>
|
<string name="session_verification_banner_title">"Overte, že ste to vy"</string>
|
||||||
</resources>
|
</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_read">"Mark as read"</string>
|
||||||
<string name="screen_roomlist_mark_as_unread">"Mark as unread"</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_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_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>
|
<string name="session_verification_banner_title">"Verify it’s you"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -173,7 +173,7 @@ class HomePresenterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 homeSpacesPresenter = MutablePresenter(aHomeSpacesState())
|
||||||
val presenter = createHomePresenter(
|
val presenter = createHomePresenter(
|
||||||
sessionStore = InMemorySessionStore(
|
sessionStore = InMemorySessionStore(
|
||||||
|
|
@ -193,7 +193,7 @@ class HomePresenterTest {
|
||||||
val spaceState = awaitItem()
|
val spaceState = awaitItem()
|
||||||
assertThat(spaceState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Spaces)
|
assertThat(spaceState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Spaces)
|
||||||
// The last space is left
|
// The last space is left
|
||||||
homeSpacesPresenter.updateState(aHomeSpacesState(spaceRooms = emptyList()))
|
homeSpacesPresenter.updateState(aHomeSpacesState(spaceRooms = emptyList(), canCreateSpaces = false))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
// We are back to Chats
|
// 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.room.aRoomSummary
|
||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -122,13 +123,18 @@ fun TestScope.createRoomListSearchPresenter(
|
||||||
roomListService: RoomListService = FakeRoomListService(),
|
roomListService: RoomListService = FakeRoomListService(),
|
||||||
): RoomListSearchPresenter {
|
): RoomListSearchPresenter {
|
||||||
return RoomListSearchPresenter(
|
return RoomListSearchPresenter(
|
||||||
dataSource = RoomListSearchDataSource(
|
dataSourceFactory = object : RoomListSearchDataSource.Factory {
|
||||||
roomListService = roomListService,
|
override fun create(coroutineScope: CoroutineScope): RoomListSearchDataSource {
|
||||||
roomSummaryFactory = aRoomListRoomSummaryFactory(
|
return RoomListSearchDataSource(
|
||||||
dateFormatter = FakeDateFormatter(),
|
roomListService = roomListService,
|
||||||
roomLatestEventFormatter = FakeRoomLatestEventFormatter(),
|
roomSummaryFactory = aRoomListRoomSummaryFactory(
|
||||||
),
|
dateFormatter = FakeDateFormatter(),
|
||||||
coroutineDispatchers = testCoroutineDispatchers(),
|
roomLatestEventFormatter = FakeRoomLatestEventFormatter(),
|
||||||
),
|
),
|
||||||
|
coroutineDispatchers = testCoroutineDispatchers(),
|
||||||
|
coroutineScope = coroutineScope,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,5 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
|
||||||
sealed interface DefaultInvitePeopleEvents : InvitePeopleEvents {
|
sealed interface DefaultInvitePeopleEvents : InvitePeopleEvents {
|
||||||
data class ToggleUser(val user: MatrixUser) : DefaultInvitePeopleEvents
|
data class ToggleUser(val user: MatrixUser) : DefaultInvitePeopleEvents
|
||||||
data class UpdateSearchQuery(val query: String) : DefaultInvitePeopleEvents
|
|
||||||
data class OnSearchActiveChanged(val active: Boolean) : DefaultInvitePeopleEvents
|
data class OnSearchActiveChanged(val active: Boolean) : DefaultInvitePeopleEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,12 @@
|
||||||
|
|
||||||
package io.element.android.features.invitepeople.impl
|
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.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.produceState
|
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.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
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.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.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import io.element.android.libraries.usersearch.api.UserRepository
|
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.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.filterNot
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.take
|
||||||
|
import kotlinx.coroutines.flow.toList
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
private const val MAX_SUGGESTIONS_COUNT = 5
|
||||||
|
|
||||||
@AssistedInject
|
@AssistedInject
|
||||||
class DefaultInvitePeoplePresenter(
|
class DefaultInvitePeoplePresenter(
|
||||||
@Assisted private val joinedRoom: JoinedRoom?,
|
@Assisted private val joinedRoom: JoinedRoom?,
|
||||||
|
|
@ -73,11 +82,39 @@ class DefaultInvitePeoplePresenter(
|
||||||
val roomMembers = remember { mutableStateOf<AsyncData<ImmutableList<RoomMember>>>(AsyncData.Loading()) }
|
val roomMembers = remember { mutableStateOf<AsyncData<ImmutableList<RoomMember>>>(AsyncData.Loading()) }
|
||||||
val selectedUsers = remember { mutableStateOf<ImmutableList<MatrixUser>>(persistentListOf()) }
|
val selectedUsers = remember { mutableStateOf<ImmutableList<MatrixUser>>(persistentListOf()) }
|
||||||
val searchResults = remember { mutableStateOf<SearchBarResultState<ImmutableList<InvitableUser>>>(SearchBarResultState.Initial()) }
|
val searchResults = remember { mutableStateOf<SearchBarResultState<ImmutableList<InvitableUser>>>(SearchBarResultState.Initial()) }
|
||||||
var searchQuery by rememberSaveable { mutableStateOf("") }
|
val queryState = rememberTextFieldState()
|
||||||
var searchActive by rememberSaveable { mutableStateOf(false) }
|
var searchActive by rememberSaveable { mutableStateOf(false) }
|
||||||
val showSearchLoader = rememberSaveable { mutableStateOf(false) }
|
val showSearchLoader = rememberSaveable { mutableStateOf(false) }
|
||||||
val sendInvitesAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
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()) {
|
val room by produceState(if (joinedRoom != null) AsyncData.Success(joinedRoom) else AsyncData.Loading()) {
|
||||||
if (joinedRoom == null) {
|
if (joinedRoom == null) {
|
||||||
val result = matrixClient.getJoinedRoom(roomId)
|
val result = matrixClient.getJoinedRoom(roomId)
|
||||||
|
|
@ -94,6 +131,7 @@ class DefaultInvitePeoplePresenter(
|
||||||
fetchMembers(it, roomMembers)
|
fetchMembers(it, roomMembers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val searchQuery = queryState.text.toString()
|
||||||
LaunchedEffect(searchQuery, roomMembers) {
|
LaunchedEffect(searchQuery, roomMembers) {
|
||||||
performSearch(
|
performSearch(
|
||||||
searchResults = searchResults,
|
searchResults = searchResults,
|
||||||
|
|
@ -108,16 +146,15 @@ class DefaultInvitePeoplePresenter(
|
||||||
when (event) {
|
when (event) {
|
||||||
is DefaultInvitePeopleEvents.OnSearchActiveChanged -> {
|
is DefaultInvitePeopleEvents.OnSearchActiveChanged -> {
|
||||||
searchActive = event.active
|
searchActive = event.active
|
||||||
searchQuery = ""
|
if (!event.active) {
|
||||||
}
|
queryState.clearText()
|
||||||
|
}
|
||||||
is DefaultInvitePeopleEvents.UpdateSearchQuery -> {
|
|
||||||
searchQuery = event.query
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is DefaultInvitePeopleEvents.ToggleUser -> {
|
is DefaultInvitePeopleEvents.ToggleUser -> {
|
||||||
selectedUsers.toggleUser(event.user)
|
selectedUsers.toggleUser(event.user)
|
||||||
searchResults.toggleUser(event.user)
|
searchResults.toggleUser(event.user)
|
||||||
|
// suggestions will automatically update via derivedStateOf when selectedUsers changes
|
||||||
}
|
}
|
||||||
is InvitePeopleEvents.SendInvites -> {
|
is InvitePeopleEvents.SendInvites -> {
|
||||||
room.dataOrNull()?.let {
|
room.dataOrNull()?.let {
|
||||||
|
|
@ -126,7 +163,7 @@ class DefaultInvitePeoplePresenter(
|
||||||
}
|
}
|
||||||
is InvitePeopleEvents.CloseSearch -> {
|
is InvitePeopleEvents.CloseSearch -> {
|
||||||
searchActive = false
|
searchActive = false
|
||||||
searchQuery = ""
|
queryState.clearText()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -135,11 +172,12 @@ class DefaultInvitePeoplePresenter(
|
||||||
room = room.map { },
|
room = room.map { },
|
||||||
canInvite = selectedUsers.value.isNotEmpty() && !sendInvitesAction.value.isLoading(),
|
canInvite = selectedUsers.value.isNotEmpty() && !sendInvitesAction.value.isLoading(),
|
||||||
selectedUsers = selectedUsers.value,
|
selectedUsers = selectedUsers.value,
|
||||||
searchQuery = searchQuery,
|
searchQuery = queryState,
|
||||||
isSearchActive = searchActive,
|
isSearchActive = searchActive,
|
||||||
searchResults = searchResults.value,
|
searchResults = searchResults.value,
|
||||||
showSearchLoader = showSearchLoader.value,
|
showSearchLoader = showSearchLoader.value,
|
||||||
sendInvitesAction = sendInvitesAction.value,
|
sendInvitesAction = sendInvitesAction.value,
|
||||||
|
suggestions = suggestions,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
package io.element.android.features.invitepeople.impl
|
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.InvitePeopleEvents
|
||||||
import io.element.android.features.invitepeople.api.InvitePeopleState
|
import io.element.android.features.invitepeople.api.InvitePeopleState
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
|
@ -19,11 +20,12 @@ import kotlinx.collections.immutable.ImmutableList
|
||||||
data class DefaultInvitePeopleState(
|
data class DefaultInvitePeopleState(
|
||||||
val room: AsyncData<Unit>,
|
val room: AsyncData<Unit>,
|
||||||
override val canInvite: Boolean,
|
override val canInvite: Boolean,
|
||||||
val searchQuery: String,
|
val searchQuery: TextFieldState,
|
||||||
val showSearchLoader: Boolean,
|
val showSearchLoader: Boolean,
|
||||||
val searchResults: SearchBarResultState<ImmutableList<InvitableUser>>,
|
val searchResults: SearchBarResultState<ImmutableList<InvitableUser>>,
|
||||||
val selectedUsers: ImmutableList<MatrixUser>,
|
val selectedUsers: ImmutableList<MatrixUser>,
|
||||||
override val isSearchActive: Boolean,
|
override val isSearchActive: Boolean,
|
||||||
override val sendInvitesAction: AsyncAction<Unit>,
|
override val sendInvitesAction: AsyncAction<Unit>,
|
||||||
|
val suggestions: ImmutableList<InvitableUser>,
|
||||||
override val eventSink: (InvitePeopleEvents) -> Unit
|
override val eventSink: (InvitePeopleEvents) -> Unit
|
||||||
) : InvitePeopleState
|
) : InvitePeopleState
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
package io.element.android.features.invitepeople.impl
|
package io.element.android.features.invitepeople.impl
|
||||||
|
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
|
@ -101,16 +102,20 @@ private fun aDefaultInvitePeopleState(
|
||||||
isSearchActive: Boolean = false,
|
isSearchActive: Boolean = false,
|
||||||
showSearchLoader: Boolean = false,
|
showSearchLoader: Boolean = false,
|
||||||
sendInvitesAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
sendInvitesAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
|
suggestions: List<InvitableUser> = aMatrixUserList()
|
||||||
|
.take(5)
|
||||||
|
.map { user -> anInvitableUser(matrixUser = user, isSelected = user in selectedUsers) },
|
||||||
): DefaultInvitePeopleState {
|
): DefaultInvitePeopleState {
|
||||||
return DefaultInvitePeopleState(
|
return DefaultInvitePeopleState(
|
||||||
room = room,
|
room = room,
|
||||||
canInvite = canInvite,
|
canInvite = canInvite,
|
||||||
searchQuery = searchQuery,
|
searchQuery = TextFieldState(initialText = searchQuery),
|
||||||
searchResults = searchResults,
|
searchResults = searchResults,
|
||||||
selectedUsers = selectedUsers,
|
selectedUsers = selectedUsers,
|
||||||
isSearchActive = isSearchActive,
|
isSearchActive = isSearchActive,
|
||||||
showSearchLoader = showSearchLoader,
|
showSearchLoader = showSearchLoader,
|
||||||
sendInvitesAction = sendInvitesAction,
|
sendInvitesAction = sendInvitesAction,
|
||||||
|
suggestions = suggestions.toImmutableList(),
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
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.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.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.SearchBar
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
|
@ -82,9 +85,13 @@ private fun InvitePeopleContentView(
|
||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
) {
|
) {
|
||||||
|
fun toggleUser(user: MatrixUser) {
|
||||||
|
state.eventSink(DefaultInvitePeopleEvents.ToggleUser(user))
|
||||||
|
}
|
||||||
|
|
||||||
InvitePeopleSearchBar(
|
InvitePeopleSearchBar(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
query = state.searchQuery,
|
queryState = state.searchQuery,
|
||||||
showLoader = state.showSearchLoader,
|
showLoader = state.showSearchLoader,
|
||||||
selectedUsers = state.selectedUsers,
|
selectedUsers = state.selectedUsers,
|
||||||
state = state.searchResults,
|
state = state.searchResults,
|
||||||
|
|
@ -96,18 +103,45 @@ private fun InvitePeopleContentView(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onTextChange = { state.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery(it)) },
|
onToggleUser = ::toggleUser,
|
||||||
onToggleUser = { state.eventSink(DefaultInvitePeopleEvents.ToggleUser(it)) },
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!state.isSearchActive) {
|
if (!state.isSearchActive) {
|
||||||
SelectedUsersRowList(
|
if (state.selectedUsers.isNotEmpty()) {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
SelectedUsersRowList(
|
||||||
selectedUsers = state.selectedUsers,
|
modifier = Modifier.fillMaxWidth(),
|
||||||
autoScroll = true,
|
selectedUsers = state.selectedUsers,
|
||||||
onUserRemove = { state.eventSink(DefaultInvitePeopleEvents.ToggleUser(it)) },
|
autoScroll = true,
|
||||||
contentPadding = PaddingValues(16.dp),
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun InvitePeopleSearchBar(
|
private fun InvitePeopleSearchBar(
|
||||||
query: String,
|
queryState: TextFieldState,
|
||||||
state: SearchBarResultState<ImmutableList<InvitableUser>>,
|
state: SearchBarResultState<ImmutableList<InvitableUser>>,
|
||||||
showLoader: Boolean,
|
showLoader: Boolean,
|
||||||
selectedUsers: ImmutableList<MatrixUser>,
|
selectedUsers: ImmutableList<MatrixUser>,
|
||||||
active: Boolean,
|
active: Boolean,
|
||||||
onActiveChange: (Boolean) -> Unit,
|
onActiveChange: (Boolean) -> Unit,
|
||||||
onTextChange: (String) -> Unit,
|
|
||||||
onToggleUser: (MatrixUser) -> Unit,
|
onToggleUser: (MatrixUser) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
placeHolderTitle: String = stringResource(CommonStrings.common_search_for_someone),
|
placeHolderTitle: String = stringResource(CommonStrings.common_search_for_someone),
|
||||||
) {
|
) {
|
||||||
SearchBar(
|
SearchBar(
|
||||||
query = query,
|
queryState = queryState,
|
||||||
onQueryChange = onTextChange,
|
|
||||||
active = active,
|
active = active,
|
||||||
onActiveChange = onActiveChange,
|
onActiveChange = onActiveChange,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|
@ -140,7 +172,7 @@ private fun InvitePeopleSearchBar(
|
||||||
selectedUsers = selectedUsers,
|
selectedUsers = selectedUsers,
|
||||||
autoScroll = true,
|
autoScroll = true,
|
||||||
onUserRemove = onToggleUser,
|
onUserRemove = onToggleUser,
|
||||||
contentPadding = PaddingValues(16.dp),
|
contentPadding = PaddingValues(all = 16.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
package io.element.android.features.invitepeople.impl
|
package io.element.android.features.invitepeople.impl
|
||||||
|
|
||||||
|
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||||
import app.cash.turbine.ReceiveTurbine
|
import app.cash.turbine.ReceiveTurbine
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.invitepeople.api.InvitePeopleEvents
|
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.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
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.RoomMembersState
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
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
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
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.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.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.aRoomMember
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomMemberList
|
import io.element.android.libraries.matrix.test.room.aRoomMemberList
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
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.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java)
|
||||||
assertThat(initialState.isSearchActive).isFalse()
|
assertThat(initialState.isSearchActive).isFalse()
|
||||||
assertThat(initialState.canInvite).isFalse()
|
assertThat(initialState.canInvite).isFalse()
|
||||||
assertThat(initialState.searchQuery).isEmpty()
|
assertThat(initialState.searchQuery.text.toString()).isEmpty()
|
||||||
|
|
||||||
skipItems(1)
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - updates search active state`() = runTest {
|
fun `present - updates search active state`() = runTest {
|
||||||
val presenter = createDefaultInvitePeoplePresenter()
|
val presenter = createDefaultInvitePeoplePresenter(
|
||||||
|
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
||||||
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
||||||
initialState.eventSink(DefaultInvitePeopleEvents.OnSearchActiveChanged(true))
|
initialState.eventSink(DefaultInvitePeopleEvents.OnSearchActiveChanged(true))
|
||||||
|
|
||||||
val resultState = awaitItem()
|
val resultState = awaitItemAsDefault()
|
||||||
assertThat(resultState.isSearchActive).isTrue()
|
assertThat(resultState.isSearchActive).isTrue()
|
||||||
resultState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query"))
|
resultState.searchQuery.setTextAndPlaceCursorAtEnd("some query")
|
||||||
assertThat(awaitItemAsDefault().searchQuery).isEqualTo("some query")
|
assertThat(awaitItemAsDefault().searchQuery.text.toString()).isEqualTo("some query")
|
||||||
resultState.eventSink(InvitePeopleEvents.CloseSearch)
|
resultState.eventSink(InvitePeopleEvents.CloseSearch)
|
||||||
skipItems(1)
|
skipItems(2)
|
||||||
awaitItemAsDefault().also {
|
awaitItemAsDefault().also {
|
||||||
assertThat(it.isSearchActive).isFalse()
|
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)
|
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItemAsDefault()
|
||||||
initialState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query"))
|
initialState.searchQuery.setTextAndPlaceCursorAtEnd("some query")
|
||||||
assertThat(repository.providedQuery).isEqualTo("some query")
|
assertThat(repository.providedQuery).isEqualTo("some query")
|
||||||
repository.emitState(UserSearchResultState(results = emptyList(), isSearching = true))
|
repository.emitState(UserSearchResultState(results = emptyList(), isSearching = true))
|
||||||
skipItems(3)
|
skipItems(3)
|
||||||
|
|
@ -126,10 +133,10 @@ internal class DefaultInvitePeoplePresenterTest {
|
||||||
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItemAsDefault()
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
||||||
initialState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query"))
|
initialState.searchQuery.setTextAndPlaceCursorAtEnd("some query")
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
||||||
assertThat(repository.providedQuery).isEqualTo("some query")
|
assertThat(repository.providedQuery).isEqualTo("some query")
|
||||||
|
|
@ -179,10 +186,10 @@ internal class DefaultInvitePeoplePresenterTest {
|
||||||
coroutineDispatchers = coroutineDispatchers,
|
coroutineDispatchers = coroutineDispatchers,
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItemAsDefault()
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
||||||
initialState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query"))
|
initialState.searchQuery.setTextAndPlaceCursorAtEnd("some query")
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
||||||
assertThat(repository.providedQuery).isEqualTo("some query")
|
assertThat(repository.providedQuery).isEqualTo("some query")
|
||||||
|
|
@ -239,10 +246,10 @@ internal class DefaultInvitePeoplePresenterTest {
|
||||||
)
|
)
|
||||||
|
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItemAsDefault()
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
||||||
initialState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query"))
|
initialState.searchQuery.setTextAndPlaceCursorAtEnd("some query")
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
||||||
assertThat(repository.providedQuery).isEqualTo("some query")
|
assertThat(repository.providedQuery).isEqualTo("some query")
|
||||||
|
|
@ -275,7 +282,7 @@ internal class DefaultInvitePeoplePresenterTest {
|
||||||
val repository = FakeUserRepository()
|
val repository = FakeUserRepository()
|
||||||
val presenter = createDefaultInvitePeoplePresenter(
|
val presenter = createDefaultInvitePeoplePresenter(
|
||||||
userRepository = repository,
|
userRepository = repository,
|
||||||
coroutineDispatchers = testCoroutineDispatchers()
|
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -306,14 +313,14 @@ internal class DefaultInvitePeoplePresenterTest {
|
||||||
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItemAsDefault()
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
||||||
val selectedUser = aMatrixUser()
|
val selectedUser = aMatrixUser()
|
||||||
|
|
||||||
initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(selectedUser))
|
initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(selectedUser))
|
||||||
|
|
||||||
initialState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query"))
|
initialState.searchQuery.setTextAndPlaceCursorAtEnd("some query")
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
||||||
assertThat(repository.providedQuery).isEqualTo("some query")
|
assertThat(repository.providedQuery).isEqualTo("some query")
|
||||||
|
|
@ -344,13 +351,13 @@ internal class DefaultInvitePeoplePresenterTest {
|
||||||
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItemAsDefault()
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
||||||
val selectedUser = aMatrixUser()
|
val selectedUser = aMatrixUser()
|
||||||
|
|
||||||
// Given a query is made
|
// Given a query is made
|
||||||
initialState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query"))
|
initialState.searchQuery.setTextAndPlaceCursorAtEnd("some query")
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
||||||
assertThat(repository.providedQuery).isEqualTo("some query")
|
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(
|
private suspend fun FakeUserRepository.emitStateWithUsers(
|
||||||
users: List<MatrixUser>,
|
users: List<MatrixUser>,
|
||||||
isSearching: Boolean = false
|
isSearching: Boolean = false
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,33 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_link_new_device_desktop_scanning_title">"Scan QR-koden"</string>
|
<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_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_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_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_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_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_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_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_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_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">"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_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_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>
|
<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_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_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_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_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_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.
|
<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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_link_new_device_desktop_scanning_title">"Olvassa be a QR-kódot"</string>
|
<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_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_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_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_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_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_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_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_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_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">"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_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_header">"Most mi lesz?"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Próbáljon meg újra bejelentkezni egy QR-kóddal, ha ez hálózati probléma volt."</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_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_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_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_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_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_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.
|
<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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_link_new_device_desktop_scanning_title">"Сканировать QR-код"</string>
|
<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_step3">"Отсканируйте QR-код с помощью этого устройства"</string>
|
||||||
<string name="screen_link_new_device_desktop_submit">"Готово к сканированию"</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_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_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_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_subtitle">"Вход на другом устройстве был отменен."</string>
|
||||||
<string name="screen_link_new_device_error_request_cancelled_title">"Запрос на вход отменен"</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_subtitle">"Срок действия входа истек. Пожалуйста, попробуйте еще раз."</string>
|
||||||
<string name="screen_link_new_device_error_request_timeout_title">"Вход в систему не был выполнен вовремя"</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">"Выберите %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_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_header">"Что теперь?"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Попробуйте снова войти в систему с помощью QR-кода, если это была проблема с соединением"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Попробуйте снова войти в систему с помощью QR-кода, если это была проблема с соединением"</string>
|
||||||
|
|
@ -21,6 +38,8 @@
|
||||||
<string name="screen_qr_code_login_error_cancelled_title">"Запрос на вход отменен"</string>
|
<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_subtitle">"Вход в систему был отклонен на другом устройстве."</string>
|
||||||
<string name="screen_qr_code_login_error_declined_title">"Вход отклонен"</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_subtitle">"Срок действия входа истек. Пожалуйста, попробуйте еще раз."</string>
|
||||||
<string name="screen_qr_code_login_error_expired_title">"Вход в систему не был выполнен вовремя"</string>
|
<string name="screen_qr_code_login_error_expired_title">"Вход в систему не был выполнен вовремя"</string>
|
||||||
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Другое устройство не поддерживает вход в %s с помощью QR-кода.
|
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Другое устройство не поддерживает вход в %s с помощью QR-кода.
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,33 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_link_new_device_desktop_scanning_title">"Naskenovať QR kód"</string>
|
<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_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_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_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_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_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_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_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_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_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">"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_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_header">"Čo teraz?"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Skúste sa znova prihlásiť pomocou QR kódu v prípade, že ide o problém so sieťou"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_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_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_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_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_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_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.
|
<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.permissions.test)
|
||||||
testImplementation(projects.libraries.sessionStorage.test)
|
testImplementation(projects.libraries.sessionStorage.test)
|
||||||
testImplementation(projects.libraries.wellknown.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_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_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_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_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_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.
|
<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_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_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_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_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_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.
|
<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_cancelled_title">"Запрос на вход отменен"</string>
|
||||||
<string name="screen_qr_code_login_error_declined_subtitle">"Вход в систему был отклонен на другом устройстве."</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_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_subtitle">"Срок действия входа истек. Пожалуйста, попробуйте еще раз."</string>
|
||||||
<string name="screen_qr_code_login_error_expired_title">"Вход в систему не был выполнен вовремя"</string>
|
<string name="screen_qr_code_login_error_expired_title">"Вход в систему не был выполнен вовремя"</string>
|
||||||
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Другое устройство не поддерживает вход в %s с помощью QR-кода.
|
<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_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_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_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_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_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.
|
<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
|
package io.element.android.features.login.impl.screens.qrcode.scan
|
||||||
|
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
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.architecture.AsyncAction
|
||||||
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
|
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
|
||||||
import io.element.android.libraries.matrix.test.auth.qrlogin.FakeMatrixQrCodeLoginData
|
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.ensureCalledOnce
|
||||||
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
||||||
import io.element.android.tests.testutils.pressBackKey
|
import io.element.android.tests.testutils.pressBackKey
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.rules.TestRule
|
import org.junit.rules.TestRule
|
||||||
|
|
@ -30,6 +34,19 @@ class QrCodeScanViewTest {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val rule = createAndroidComposeRule<ComponentActivity>()
|
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
|
@Test
|
||||||
fun `on back pressed - calls the expected callback`() {
|
fun `on back pressed - calls the expected callback`() {
|
||||||
ensureCalledOnce { callback ->
|
ensureCalledOnce { callback ->
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,6 @@ dependencies {
|
||||||
implementation(libs.jsoup)
|
implementation(libs.jsoup)
|
||||||
implementation(libs.androidx.constraintlayout)
|
implementation(libs.androidx.constraintlayout)
|
||||||
implementation(libs.androidx.constraintlayout.compose)
|
implementation(libs.androidx.constraintlayout.compose)
|
||||||
implementation(libs.androidx.datastore.preferences)
|
|
||||||
implementation(libs.androidx.media3.exoplayer)
|
implementation(libs.androidx.media3.exoplayer)
|
||||||
implementation(libs.androidx.media3.ui)
|
implementation(libs.androidx.media3.ui)
|
||||||
implementation(libs.sigpwned.emoji4j)
|
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.timeline.item.event.EventOrTransactionId
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
|
||||||
sealed interface MessagesEvents {
|
sealed interface MessagesEvent {
|
||||||
data class HandleAction(val action: TimelineItemAction, val event: TimelineItem.Event) : MessagesEvents
|
data class HandleAction(val action: TimelineItemAction, val event: TimelineItem.Event) : MessagesEvent
|
||||||
data class ToggleReaction(val emoji: String, val eventOrTransactionId: EventOrTransactionId) : MessagesEvents
|
data class ToggleReaction(val emoji: String, val eventOrTransactionId: EventOrTransactionId) : MessagesEvent
|
||||||
data class InviteDialogDismissed(val action: InviteDialogAction) : MessagesEvents
|
data class InviteDialogDismissed(val action: InviteDialogAction) : MessagesEvent
|
||||||
data class OnUserClicked(val user: MatrixUser) : MessagesEvents
|
data class OnUserClicked(val user: MatrixUser) : MessagesEvent
|
||||||
data object Dismiss : MessagesEvents
|
data object MarkAsFullyReadAndExit : MessagesEvent
|
||||||
data object MarkAsFullyReadAndExit : MessagesEvents
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class InviteDialogAction {
|
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.MessageComposerEvent
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
|
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.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.TimelinePresenter
|
||||||
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
|
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
|
||||||
import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories
|
import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories
|
||||||
|
|
@ -151,7 +151,7 @@ class MessagesNode(
|
||||||
activity: Activity,
|
activity: Activity,
|
||||||
darkTheme: Boolean,
|
darkTheme: Boolean,
|
||||||
url: String,
|
url: String,
|
||||||
eventSink: (TimelineEvents) -> Unit,
|
eventSink: (TimelineEvent) -> Unit,
|
||||||
customTab: Boolean
|
customTab: Boolean
|
||||||
) {
|
) {
|
||||||
when (val permalink = permalinkParser.parse(url)) {
|
when (val permalink = permalinkParser.parse(url)) {
|
||||||
|
|
@ -178,12 +178,12 @@ class MessagesNode(
|
||||||
|
|
||||||
private fun handleRoomLinkClick(
|
private fun handleRoomLinkClick(
|
||||||
roomLink: PermalinkData.RoomLink,
|
roomLink: PermalinkData.RoomLink,
|
||||||
eventSink: (TimelineEvents) -> Unit,
|
eventSink: (TimelineEvent) -> Unit,
|
||||||
) {
|
) {
|
||||||
if (room.matches(roomLink.roomIdOrAlias)) {
|
if (room.matches(roomLink.roomIdOrAlias)) {
|
||||||
val eventId = roomLink.eventId
|
val eventId = roomLink.eventId
|
||||||
if (eventId != null) {
|
if (eventId != null) {
|
||||||
eventSink(TimelineEvents.FocusOnEvent(eventId))
|
eventSink(TimelineEvent.FocusOnEvent(eventId))
|
||||||
} else {
|
} else {
|
||||||
// Click on the same room, ignore
|
// Click on the same room, ignore
|
||||||
displaySameRoomToast()
|
displaySameRoomToast()
|
||||||
|
|
@ -242,7 +242,7 @@ class MessagesNode(
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
|
|
||||||
BackHandler {
|
BackHandler {
|
||||||
state.eventSink(MessagesEvents.MarkAsFullyReadAndExit)
|
state.eventSink(MessagesEvent.MarkAsFullyReadAndExit)
|
||||||
}
|
}
|
||||||
|
|
||||||
OnLifecycleEvent { _, event ->
|
OnLifecycleEvent { _, event ->
|
||||||
|
|
@ -253,7 +253,7 @@ class MessagesNode(
|
||||||
}
|
}
|
||||||
MessagesView(
|
MessagesView(
|
||||||
state = state,
|
state = state,
|
||||||
onBackClick = { state.eventSink(MessagesEvents.MarkAsFullyReadAndExit) },
|
onBackClick = { state.eventSink(MessagesEvent.MarkAsFullyReadAndExit) },
|
||||||
onRoomDetailsClick = callback::navigateToRoomDetails,
|
onRoomDetailsClick = callback::navigateToRoomDetails,
|
||||||
onEventContentClick = { isLive, event ->
|
onEventContentClick = { isLive, event ->
|
||||||
if (isLive) {
|
if (isLive) {
|
||||||
|
|
@ -305,7 +305,7 @@ class MessagesNode(
|
||||||
}
|
}
|
||||||
LaunchedEffect(focusedEventId) {
|
LaunchedEffect(focusedEventId) {
|
||||||
if (focusedEventId != null) {
|
if (focusedEventId != null) {
|
||||||
state.timelineState.eventSink(TimelineEvents.FocusOnEvent(focusedEventId!!))
|
state.timelineState.eventSink(TimelineEvent.FocusOnEvent(focusedEventId!!))
|
||||||
focusedEventId = null
|
focusedEventId = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,10 +27,8 @@ import dev.zacsweers.metro.AssistedInject
|
||||||
import im.vector.app.features.analytics.plan.PinUnpinAction
|
import im.vector.app.features.analytics.plan.PinUnpinAction
|
||||||
import io.element.android.appconfig.MessageComposerConfig
|
import io.element.android.appconfig.MessageComposerConfig
|
||||||
import io.element.android.features.messages.api.timeline.HtmlConverterProvider
|
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.ActionListState
|
||||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
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.crypto.identity.IdentityChangeState
|
||||||
import io.element.android.features.messages.impl.link.LinkState
|
import io.element.android.features.messages.impl.link.LinkState
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvent
|
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.pinned.banner.PinnedMessagesBannerState
|
||||||
import io.element.android.features.messages.impl.timeline.MarkAsFullyRead
|
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.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.TimelineState
|
||||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
|
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
|
||||||
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
|
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.JoinedRoom
|
||||||
import io.element.android.libraries.matrix.api.room.RoomInfo
|
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.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.isDm
|
||||||
import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
|
import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
||||||
|
|
@ -102,7 +101,6 @@ class MessagesPresenter(
|
||||||
@Assisted private val timelinePresenter: Presenter<TimelineState>,
|
@Assisted private val timelinePresenter: Presenter<TimelineState>,
|
||||||
private val timelineProtectionPresenter: Presenter<TimelineProtectionState>,
|
private val timelineProtectionPresenter: Presenter<TimelineProtectionState>,
|
||||||
private val identityChangeStatePresenter: Presenter<IdentityChangeState>,
|
private val identityChangeStatePresenter: Presenter<IdentityChangeState>,
|
||||||
private val historyVisibleStatePresenter: Presenter<HistoryVisibleState>,
|
|
||||||
private val linkPresenter: Presenter<LinkState>,
|
private val linkPresenter: Presenter<LinkState>,
|
||||||
@Assisted private val actionListPresenter: Presenter<ActionListState>,
|
@Assisted private val actionListPresenter: Presenter<ActionListState>,
|
||||||
private val customReactionPresenter: Presenter<CustomReactionState>,
|
private val customReactionPresenter: Presenter<CustomReactionState>,
|
||||||
|
|
@ -154,7 +152,6 @@ class MessagesPresenter(
|
||||||
val timelineState = timelinePresenter.present()
|
val timelineState = timelinePresenter.present()
|
||||||
val timelineProtectionState = timelineProtectionPresenter.present()
|
val timelineProtectionState = timelineProtectionPresenter.present()
|
||||||
val identityChangeState = identityChangeStatePresenter.present()
|
val identityChangeState = identityChangeStatePresenter.present()
|
||||||
val historyVisibleState = historyVisibleStatePresenter.present()
|
|
||||||
val actionListState = actionListPresenter.present()
|
val actionListState = actionListPresenter.present()
|
||||||
val linkState = linkPresenter.present()
|
val linkState = linkPresenter.present()
|
||||||
val customReactionState = customReactionPresenter.present()
|
val customReactionState = customReactionPresenter.present()
|
||||||
|
|
@ -208,6 +205,16 @@ class MessagesPresenter(
|
||||||
val dmRoomMember by room.getDirectRoomMember(membersState)
|
val dmRoomMember by room.getDirectRoomMember(membersState)
|
||||||
val roomMemberIdentityStateChanges = identityChangeState.roomMemberIdentityStateChanges
|
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) {
|
LifecycleResumeEffect(dmRoomMember, roomInfo.isEncrypted) {
|
||||||
if (roomInfo.isEncrypted == true) {
|
if (roomInfo.isEncrypted == true) {
|
||||||
val dmRoomMemberId = dmRoomMember?.userId
|
val dmRoomMemberId = dmRoomMember?.userId
|
||||||
|
|
@ -221,9 +228,9 @@ class MessagesPresenter(
|
||||||
onPauseOrDispose {}
|
onPauseOrDispose {}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleEvent(event: MessagesEvents) {
|
fun handleEvent(event: MessagesEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is MessagesEvents.HandleAction -> {
|
is MessagesEvent.HandleAction -> {
|
||||||
localCoroutineScope.handleTimelineAction(
|
localCoroutineScope.handleTimelineAction(
|
||||||
action = event.action,
|
action = event.action,
|
||||||
targetEvent = event.event,
|
targetEvent = event.event,
|
||||||
|
|
@ -233,21 +240,20 @@ class MessagesPresenter(
|
||||||
timelineProtectionState = timelineProtectionState,
|
timelineProtectionState = timelineProtectionState,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is MessagesEvents.ToggleReaction -> {
|
is MessagesEvent.ToggleReaction -> {
|
||||||
localCoroutineScope.toggleReaction(event.emoji, event.eventOrTransactionId)
|
localCoroutineScope.toggleReaction(event.emoji, event.eventOrTransactionId)
|
||||||
}
|
}
|
||||||
is MessagesEvents.InviteDialogDismissed -> {
|
is MessagesEvent.InviteDialogDismissed -> {
|
||||||
hasDismissedInviteDialog = true
|
hasDismissedInviteDialog = true
|
||||||
|
|
||||||
if (event.action == InviteDialogAction.Invite) {
|
if (event.action == InviteDialogAction.Invite) {
|
||||||
localCoroutineScope.reinviteOtherUser(inviteProgress)
|
localCoroutineScope.reinviteOtherUser(inviteProgress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is MessagesEvents.Dismiss -> actionListState.eventSink(ActionListEvents.Clear)
|
is MessagesEvent.OnUserClicked -> {
|
||||||
is MessagesEvents.OnUserClicked -> {
|
|
||||||
roomMemberModerationState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(event.user))
|
roomMemberModerationState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(event.user))
|
||||||
}
|
}
|
||||||
is MessagesEvents.MarkAsFullyReadAndExit -> coroutineScope.launch {
|
is MessagesEvent.MarkAsFullyReadAndExit -> coroutineScope.launch {
|
||||||
if (!markingAsReadAndExiting.getAndSet(true)) {
|
if (!markingAsReadAndExiting.getAndSet(true)) {
|
||||||
val latestEventId = room.liveTimeline.getLatestEventId().getOrElse {
|
val latestEventId = room.liveTimeline.getLatestEventId().getOrElse {
|
||||||
Timber.w(it, "Failed to get latest event id to mark as fully read")
|
Timber.w(it, "Failed to get latest event id to mark as fully read")
|
||||||
|
|
@ -277,7 +283,6 @@ class MessagesPresenter(
|
||||||
timelineState = timelineState,
|
timelineState = timelineState,
|
||||||
timelineProtectionState = timelineProtectionState,
|
timelineProtectionState = timelineProtectionState,
|
||||||
identityChangeState = identityChangeState,
|
identityChangeState = identityChangeState,
|
||||||
historyVisibleState = historyVisibleState,
|
|
||||||
linkState = linkState,
|
linkState = linkState,
|
||||||
actionListState = actionListState,
|
actionListState = actionListState,
|
||||||
customReactionState = customReactionState,
|
customReactionState = customReactionState,
|
||||||
|
|
@ -292,6 +297,7 @@ class MessagesPresenter(
|
||||||
pinnedMessagesBannerState = pinnedMessagesBannerState,
|
pinnedMessagesBannerState = pinnedMessagesBannerState,
|
||||||
dmUserVerificationState = dmUserVerificationState,
|
dmUserVerificationState = dmUserVerificationState,
|
||||||
roomMemberModerationState = roomMemberModerationState,
|
roomMemberModerationState = roomMemberModerationState,
|
||||||
|
showSharedHistoryIcon = showSharedHistoryIcon,
|
||||||
successorRoom = roomInfo.successorRoom,
|
successorRoom = roomInfo.successorRoom,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
|
|
@ -529,7 +535,7 @@ class MessagesPresenter(
|
||||||
event: TimelineItem.Event,
|
event: TimelineItem.Event,
|
||||||
timelineState: TimelineState,
|
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) {
|
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.api.timeline.voicemessages.composer.VoiceMessageComposerState
|
||||||
import io.element.android.features.messages.impl.actionlist.ActionListState
|
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.crypto.identity.IdentityChangeState
|
||||||
import io.element.android.features.messages.impl.link.LinkState
|
import io.element.android.features.messages.impl.link.LinkState
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
|
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
|
||||||
|
|
@ -41,7 +40,6 @@ data class MessagesState(
|
||||||
val timelineState: TimelineState,
|
val timelineState: TimelineState,
|
||||||
val timelineProtectionState: TimelineProtectionState,
|
val timelineProtectionState: TimelineProtectionState,
|
||||||
val identityChangeState: IdentityChangeState,
|
val identityChangeState: IdentityChangeState,
|
||||||
val historyVisibleState: HistoryVisibleState,
|
|
||||||
val linkState: LinkState,
|
val linkState: LinkState,
|
||||||
val actionListState: ActionListState,
|
val actionListState: ActionListState,
|
||||||
val customReactionState: CustomReactionState,
|
val customReactionState: CustomReactionState,
|
||||||
|
|
@ -56,8 +54,10 @@ data class MessagesState(
|
||||||
val pinnedMessagesBannerState: PinnedMessagesBannerState,
|
val pinnedMessagesBannerState: PinnedMessagesBannerState,
|
||||||
val dmUserVerificationState: IdentityState?,
|
val dmUserVerificationState: IdentityState?,
|
||||||
val roomMemberModerationState: RoomMemberModerationState,
|
val roomMemberModerationState: RoomMemberModerationState,
|
||||||
|
/** Should the top bar include the "history" icon? */
|
||||||
|
val showSharedHistoryIcon: Boolean,
|
||||||
val successorRoom: SuccessorRoom?,
|
val successorRoom: SuccessorRoom?,
|
||||||
val eventSink: (MessagesEvents) -> Unit
|
val eventSink: (MessagesEvent) -> Unit
|
||||||
) {
|
) {
|
||||||
val isTombstoned = successorRoom != null
|
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.api.timeline.voicemessages.composer.aVoiceMessagePreviewState
|
||||||
import io.element.android.features.messages.impl.actionlist.ActionListState
|
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.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.IdentityChangeState
|
||||||
import io.element.android.features.messages.impl.crypto.identity.aRoomMemberIdentityStateChange
|
import io.element.android.features.messages.impl.crypto.identity.aRoomMemberIdentityStateChange
|
||||||
import io.element.android.features.messages.impl.crypto.identity.anIdentityChangeState
|
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.TimelineState
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineItemList
|
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.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.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.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.components.receipt.bottomsheet.ReadReceiptBottomSheetState
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||||
|
|
@ -92,15 +90,6 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
|
||||||
composerState = aMessageComposerState(textEditorState = aTextEditorStateMarkdown()),
|
composerState = aMessageComposerState(textEditorState = aTextEditorStateMarkdown()),
|
||||||
identityChangeState = anIdentityChangeState(listOf(aRoomMemberIdentityStateChange()))
|
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(),
|
timelineProtectionState: TimelineProtectionState = aTimelineProtectionState(),
|
||||||
identityChangeState: IdentityChangeState = anIdentityChangeState(),
|
identityChangeState: IdentityChangeState = anIdentityChangeState(),
|
||||||
historyVisibleState: HistoryVisibleState = aHistoryVisibleState(),
|
|
||||||
linkState: LinkState = aLinkState(),
|
linkState: LinkState = aLinkState(),
|
||||||
readReceiptBottomSheetState: ReadReceiptBottomSheetState = aReadReceiptBottomSheetState(),
|
readReceiptBottomSheetState: ReadReceiptBottomSheetState = aReadReceiptBottomSheetState(),
|
||||||
actionListState: ActionListState = anActionListState(),
|
actionListState: ActionListState = anActionListState(),
|
||||||
|
|
@ -132,8 +120,9 @@ fun aMessagesState(
|
||||||
pinnedMessagesBannerState: PinnedMessagesBannerState = aLoadedPinnedMessagesBannerState(),
|
pinnedMessagesBannerState: PinnedMessagesBannerState = aLoadedPinnedMessagesBannerState(),
|
||||||
dmUserVerificationState: IdentityState? = null,
|
dmUserVerificationState: IdentityState? = null,
|
||||||
roomMemberModerationState: RoomMemberModerationState = aRoomMemberModerationState(),
|
roomMemberModerationState: RoomMemberModerationState = aRoomMemberModerationState(),
|
||||||
|
showSharedHistoryIcon: Boolean = false,
|
||||||
successorRoom: SuccessorRoom? = null,
|
successorRoom: SuccessorRoom? = null,
|
||||||
eventSink: (MessagesEvents) -> Unit = {},
|
eventSink: (MessagesEvent) -> Unit = {},
|
||||||
) = MessagesState(
|
) = MessagesState(
|
||||||
roomId = RoomId("!id:domain"),
|
roomId = RoomId("!id:domain"),
|
||||||
roomName = roomName,
|
roomName = roomName,
|
||||||
|
|
@ -144,7 +133,6 @@ fun aMessagesState(
|
||||||
voiceMessageComposerState = voiceMessageComposerState,
|
voiceMessageComposerState = voiceMessageComposerState,
|
||||||
timelineProtectionState = timelineProtectionState,
|
timelineProtectionState = timelineProtectionState,
|
||||||
identityChangeState = identityChangeState,
|
identityChangeState = identityChangeState,
|
||||||
historyVisibleState = historyVisibleState,
|
|
||||||
linkState = linkState,
|
linkState = linkState,
|
||||||
timelineState = timelineState,
|
timelineState = timelineState,
|
||||||
readReceiptBottomSheetState = readReceiptBottomSheetState,
|
readReceiptBottomSheetState = readReceiptBottomSheetState,
|
||||||
|
|
@ -160,6 +148,7 @@ fun aMessagesState(
|
||||||
pinnedMessagesBannerState = pinnedMessagesBannerState,
|
pinnedMessagesBannerState = pinnedMessagesBannerState,
|
||||||
dmUserVerificationState = dmUserVerificationState,
|
dmUserVerificationState = dmUserVerificationState,
|
||||||
roomMemberModerationState = roomMemberModerationState,
|
roomMemberModerationState = roomMemberModerationState,
|
||||||
|
showSharedHistoryIcon = showSharedHistoryIcon,
|
||||||
successorRoom = successorRoom,
|
successorRoom = successorRoom,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
@ -187,7 +176,7 @@ fun aUserEventPermissions(
|
||||||
|
|
||||||
fun aReactionSummaryState(
|
fun aReactionSummaryState(
|
||||||
target: ReactionSummaryState.Summary? = null,
|
target: ReactionSummaryState.Summary? = null,
|
||||||
eventSink: (ReactionSummaryEvents) -> Unit = {}
|
eventSink: (ReactionSummaryEvent) -> Unit = {}
|
||||||
) = ReactionSummaryState(
|
) = ReactionSummaryState(
|
||||||
target = target,
|
target = target,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
|
|
@ -196,7 +185,7 @@ fun aReactionSummaryState(
|
||||||
fun aCustomReactionState(
|
fun aCustomReactionState(
|
||||||
target: CustomReactionState.Target = CustomReactionState.Target.None,
|
target: CustomReactionState.Target = CustomReactionState.Target.None,
|
||||||
recentEmojis: ImmutableList<String> = persistentListOf(),
|
recentEmojis: ImmutableList<String> = persistentListOf(),
|
||||||
eventSink: (CustomReactionEvents) -> Unit = {},
|
eventSink: (CustomReactionEvent) -> Unit = {},
|
||||||
) = CustomReactionState(
|
) = CustomReactionState(
|
||||||
target = target,
|
target = target,
|
||||||
recentEmojis = recentEmojis,
|
recentEmojis = recentEmojis,
|
||||||
|
|
@ -206,7 +195,7 @@ fun aCustomReactionState(
|
||||||
|
|
||||||
fun aReadReceiptBottomSheetState(
|
fun aReadReceiptBottomSheetState(
|
||||||
selectedEvent: TimelineItem.Event? = null,
|
selectedEvent: TimelineItem.Event? = null,
|
||||||
eventSink: (ReadReceiptBottomSheetEvents) -> Unit = {},
|
eventSink: (ReadReceiptBottomSheetEvent) -> Unit = {},
|
||||||
) = ReadReceiptBottomSheetState(
|
) = ReadReceiptBottomSheetState(
|
||||||
selectedEvent = selectedEvent,
|
selectedEvent = selectedEvent,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
|
@ -51,12 +52,11 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerEvent
|
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.ActionListView
|
||||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
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.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.link.LinkView
|
||||||
import io.element.android.features.messages.impl.messagecomposer.AttachmentsBottomSheet
|
import io.element.android.features.messages.impl.messagecomposer.AttachmentsBottomSheet
|
||||||
import io.element.android.features.messages.impl.messagecomposer.DisabledComposerView
|
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.PinnedMessagesBannerView
|
||||||
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerViewDefaults
|
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.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.TimelineView
|
||||||
import io.element.android.features.messages.impl.timeline.aGroupedEvents
|
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.aTimelineItemDaySeparator
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineState
|
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.CustomReactionBottomSheet
|
||||||
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.reactionsummary.ReactionSummaryEvents
|
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.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.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.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
|
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
||||||
|
|
@ -168,7 +168,7 @@ fun MessagesView(
|
||||||
Timber.v("OnMessageLongClicked= ${event.id}")
|
Timber.v("OnMessageLongClicked= ${event.id}")
|
||||||
hidingKeyboard {
|
hidingKeyboard {
|
||||||
state.actionListState.eventSink(
|
state.actionListState.eventSink(
|
||||||
ActionListEvents.ComputeForMessage(
|
ActionListEvent.ComputeForMessage(
|
||||||
event = event,
|
event = event,
|
||||||
userEventPermissions = state.userEventPermissions,
|
userEventPermissions = state.userEventPermissions,
|
||||||
)
|
)
|
||||||
|
|
@ -177,20 +177,20 @@ fun MessagesView(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onActionSelected(action: TimelineItemAction, event: TimelineItem.Event) {
|
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) {
|
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) {
|
fun onEmojiReactionLongClick(emoji: String, event: TimelineItem.Event) {
|
||||||
if (event.eventId == null) return
|
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) {
|
fun onMoreReactionsClick(event: TimelineItem.Event) {
|
||||||
state.customReactionState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(event))
|
state.customReactionState.eventSink(CustomReactionEvent.ShowCustomReactionSheet(event))
|
||||||
}
|
}
|
||||||
|
|
||||||
val expandableState = rememberExpandableBottomSheetLayoutState()
|
val expandableState = rememberExpandableBottomSheetLayoutState()
|
||||||
|
|
@ -225,6 +225,7 @@ fun MessagesView(
|
||||||
heroes = state.heroes,
|
heroes = state.heroes,
|
||||||
roomCallState = state.roomCallState,
|
roomCallState = state.roomCallState,
|
||||||
dmUserIdentityState = state.dmUserVerificationState,
|
dmUserIdentityState = state.dmUserVerificationState,
|
||||||
|
showSharedHistoryIcon = state.showSharedHistoryIcon,
|
||||||
onBackClick = { hidingKeyboard { onBackClick() } },
|
onBackClick = { hidingKeyboard { onBackClick() } },
|
||||||
onRoomDetailsClick = { hidingKeyboard { onRoomDetailsClick() } },
|
onRoomDetailsClick = { hidingKeyboard { onRoomDetailsClick() } },
|
||||||
onJoinCallClick = onJoinCallClick,
|
onJoinCallClick = onJoinCallClick,
|
||||||
|
|
@ -243,7 +244,7 @@ fun MessagesView(
|
||||||
onMessageLongClick = ::onMessageLongClick,
|
onMessageLongClick = ::onMessageLongClick,
|
||||||
onUserDataClick = {
|
onUserDataClick = {
|
||||||
hidingKeyboard {
|
hidingKeyboard {
|
||||||
state.eventSink(MessagesEvents.OnUserClicked(it))
|
state.eventSink(MessagesEvent.OnUserClicked(it))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLinkClick = { link, customTab ->
|
onLinkClick = { link, customTab ->
|
||||||
|
|
@ -251,19 +252,19 @@ fun MessagesView(
|
||||||
onLinkClick(link.url, true)
|
onLinkClick(link.url, true)
|
||||||
// Do not check those links, they are internal link only
|
// Do not check those links, they are internal link only
|
||||||
} else {
|
} else {
|
||||||
state.linkState.eventSink(LinkEvents.OnLinkClick(link))
|
state.linkState.eventSink(LinkEvent.OnLinkClick(link))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onReactionClick = ::onEmojiReactionClick,
|
onReactionClick = ::onEmojiReactionClick,
|
||||||
onReactionLongClick = ::onEmojiReactionLongClick,
|
onReactionLongClick = ::onEmojiReactionLongClick,
|
||||||
onMoreReactionsClick = ::onMoreReactionsClick,
|
onMoreReactionsClick = ::onMoreReactionsClick,
|
||||||
onReadReceiptClick = { event ->
|
onReadReceiptClick = { event ->
|
||||||
state.readReceiptBottomSheetState.eventSink(ReadReceiptBottomSheetEvents.EventSelected(event))
|
state.readReceiptBottomSheetState.eventSink(ReadReceiptBottomSheetEvent.EventSelected(event))
|
||||||
},
|
},
|
||||||
onSendLocationClick = onSendLocationClick,
|
onSendLocationClick = onSendLocationClick,
|
||||||
onCreatePollClick = onCreatePollClick,
|
onCreatePollClick = onCreatePollClick,
|
||||||
onSwipeToReply = { targetEvent ->
|
onSwipeToReply = { targetEvent ->
|
||||||
state.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, targetEvent))
|
state.eventSink(MessagesEvent.HandleAction(TimelineItemAction.Reply, targetEvent))
|
||||||
},
|
},
|
||||||
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
|
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
|
||||||
onJoinCallClick = onJoinCallClick,
|
onJoinCallClick = onJoinCallClick,
|
||||||
|
|
@ -300,7 +301,7 @@ fun MessagesView(
|
||||||
state = state,
|
state = state,
|
||||||
onLinkClick = { url, customTab -> onLinkClick(url, customTab) },
|
onLinkClick = { url, customTab -> onLinkClick(url, customTab) },
|
||||||
onRoomSuccessorClick = { roomId ->
|
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(),
|
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(
|
ActionListView(
|
||||||
state = state.actionListState,
|
state = state.actionListState,
|
||||||
onSelectAction = ::onActionSelected,
|
onSelectAction = { action: TimelineItemAction, event: TimelineItem.Event ->
|
||||||
|
if (action == TimelineItemAction.EndPoll) {
|
||||||
|
endPollConfirmingEvent = event
|
||||||
|
} else {
|
||||||
|
onActionSelected(action, event)
|
||||||
|
}
|
||||||
|
},
|
||||||
onCustomReactionClick = { event ->
|
onCustomReactionClick = { event ->
|
||||||
state.customReactionState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(event))
|
state.customReactionState.eventSink(CustomReactionEvent.ShowCustomReactionSheet(event))
|
||||||
},
|
},
|
||||||
onEmojiReactionClick = ::onEmojiReactionClick,
|
onEmojiReactionClick = ::onEmojiReactionClick,
|
||||||
onVerifiedUserSendFailureClick = { event ->
|
onVerifiedUserSendFailureClick = { event ->
|
||||||
state.timelineState.eventSink(TimelineEvents.ComputeVerifiedUserSendFailure(event))
|
state.timelineState.eventSink(TimelineEvent.ComputeVerifiedUserSendFailure(event))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
CustomReactionBottomSheet(
|
CustomReactionBottomSheet(
|
||||||
state = state.customReactionState,
|
state = state.customReactionState,
|
||||||
onSelectEmoji = { uniqueId, emoji ->
|
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),
|
content = stringResource(id = R.string.screen_room_invite_again_alert_message),
|
||||||
cancelText = stringResource(id = CommonStrings.action_cancel),
|
cancelText = stringResource(id = CommonStrings.action_cancel),
|
||||||
submitText = stringResource(id = CommonStrings.action_invite),
|
submitText = stringResource(id = CommonStrings.action_invite),
|
||||||
onSubmitClick = { state.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Invite)) },
|
onSubmitClick = { state.eventSink(MessagesEvent.InviteDialogDismissed(InviteDialogAction.Invite)) },
|
||||||
onDismiss = { state.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Cancel)) }
|
onDismiss = { state.eventSink(MessagesEvent.InviteDialogDismissed(InviteDialogAction.Cancel)) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -467,7 +489,7 @@ private fun MessagesViewContent(
|
||||||
) {
|
) {
|
||||||
fun focusOnPinnedEvent(eventId: EventId) {
|
fun focusOnPinnedEvent(eventId: EventId) {
|
||||||
state.timelineState.eventSink(
|
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(
|
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).
|
// Do not show the identity change if user is composing a Rich message or is seeing suggestion(s).
|
||||||
if (state.composerState.suggestions.isEmpty() &&
|
if (state.composerState.suggestions.isEmpty() &&
|
||||||
state.composerState.textEditorState is TextEditorState.Markdown) {
|
state.composerState.textEditorState is TextEditorState.Markdown) {
|
||||||
if (state.identityChangeState.roomMemberIdentityStateChanges.isNotEmpty()) {
|
IdentityChangeStateView(
|
||||||
IdentityChangeStateView(
|
state = state.identityChangeState,
|
||||||
state = state.identityChangeState,
|
onLinkClick = onLinkClick,
|
||||||
onLinkClick = onLinkClick,
|
)
|
||||||
)
|
|
||||||
} else {
|
|
||||||
HistoryVisibleStateView(
|
|
||||||
state = state.historyVisibleState,
|
|
||||||
onLinkClick = onLinkClick,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
val verificationViolation = state.identityChangeState.roomMemberIdentityStateChanges.firstOrNull {
|
val verificationViolation = state.identityChangeState.roomMemberIdentityStateChanges.firstOrNull {
|
||||||
it.identityState == IdentityState.VerificationViolation
|
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.UserEventPermissions
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
|
||||||
sealed interface ActionListEvents {
|
sealed interface ActionListEvent {
|
||||||
data object Clear : ActionListEvents
|
data object Clear : ActionListEvent
|
||||||
data class ComputeForMessage(
|
data class ComputeForMessage(
|
||||||
val event: TimelineItem.Event,
|
val event: TimelineItem.Event,
|
||||||
val userEventPermissions: UserEventPermissions,
|
val userEventPermissions: UserEventPermissions,
|
||||||
) : ActionListEvents
|
) : ActionListEvent
|
||||||
}
|
}
|
||||||
|
|
@ -107,10 +107,10 @@ class DefaultActionListPresenter(
|
||||||
|
|
||||||
val isThreadsEnabled = featureFlagService.isFeatureEnabledFlow(FeatureFlags.Threads).collectAsState(false)
|
val isThreadsEnabled = featureFlagService.isFeatureEnabledFlow(FeatureFlags.Threads).collectAsState(false)
|
||||||
|
|
||||||
fun handleEvent(event: ActionListEvents) {
|
fun handleEvent(event: ActionListEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
ActionListEvents.Clear -> target.value = ActionListState.Target.None
|
ActionListEvent.Clear -> target.value = ActionListState.Target.None
|
||||||
is ActionListEvents.ComputeForMessage -> localCoroutineScope.computeForMessage(
|
is ActionListEvent.ComputeForMessage -> localCoroutineScope.computeForMessage(
|
||||||
timelineItem = event.event,
|
timelineItem = event.event,
|
||||||
usersEventPermissions = event.userEventPermissions,
|
usersEventPermissions = event.userEventPermissions,
|
||||||
isDeveloperModeEnabled = isDeveloperModeEnabled,
|
isDeveloperModeEnabled = isDeveloperModeEnabled,
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class ActionListState(
|
data class ActionListState(
|
||||||
val target: Target,
|
val target: Target,
|
||||||
val eventSink: (ActionListEvents) -> Unit,
|
val eventSink: (ActionListEvent) -> Unit,
|
||||||
) {
|
) {
|
||||||
@Immutable
|
@Immutable
|
||||||
sealed interface Target {
|
sealed interface Target {
|
||||||
|
|
|
||||||
|
|
@ -192,7 +192,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
|
|
||||||
fun anActionListState(
|
fun anActionListState(
|
||||||
target: ActionListState.Target = ActionListState.Target.None,
|
target: ActionListState.Target = ActionListState.Target.None,
|
||||||
eventSink: (ActionListEvents) -> Unit = {},
|
eventSink: (ActionListEvent) -> Unit = {},
|
||||||
) = ActionListState(
|
) = ActionListState(
|
||||||
target = target,
|
target = target,
|
||||||
eventSink = eventSink
|
eventSink = eventSink
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,7 @@ fun ActionListView(
|
||||||
) {
|
) {
|
||||||
if (targetItem == null) return
|
if (targetItem == null) return
|
||||||
sheetState.hide(coroutineScope) {
|
sheetState.hide(coroutineScope) {
|
||||||
state.eventSink(ActionListEvents.Clear)
|
state.eventSink(ActionListEvent.Clear)
|
||||||
onSelectAction(itemAction, targetItem)
|
onSelectAction(itemAction, targetItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -126,7 +126,7 @@ fun ActionListView(
|
||||||
fun onEmojiReactionClick(emoji: String) {
|
fun onEmojiReactionClick(emoji: String) {
|
||||||
if (targetItem == null) return
|
if (targetItem == null) return
|
||||||
sheetState.hide(coroutineScope) {
|
sheetState.hide(coroutineScope) {
|
||||||
state.eventSink(ActionListEvents.Clear)
|
state.eventSink(ActionListEvent.Clear)
|
||||||
onEmojiReactionClick(emoji, targetItem)
|
onEmojiReactionClick(emoji, targetItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -134,19 +134,19 @@ fun ActionListView(
|
||||||
fun onCustomReactionClick() {
|
fun onCustomReactionClick() {
|
||||||
if (targetItem == null) return
|
if (targetItem == null) return
|
||||||
sheetState.hide(coroutineScope) {
|
sheetState.hide(coroutineScope) {
|
||||||
state.eventSink(ActionListEvents.Clear)
|
state.eventSink(ActionListEvent.Clear)
|
||||||
onCustomReactionClick(targetItem)
|
onCustomReactionClick(targetItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDismiss() {
|
fun onDismiss() {
|
||||||
state.eventSink(ActionListEvents.Clear)
|
state.eventSink(ActionListEvent.Clear)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onVerifiedUserSendFailureClick() {
|
fun onVerifiedUserSendFailureClick() {
|
||||||
if (targetItem == null) return
|
if (targetItem == null) return
|
||||||
sheetState.hide(coroutineScope) {
|
sheetState.hide(coroutineScope) {
|
||||||
state.eventSink(ActionListEvents.Clear)
|
state.eventSink(ActionListEvent.Clear)
|
||||||
onVerifiedUserSendFailureClick(targetItem)
|
onVerifiedUserSendFailureClick(targetItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.attachments.preview
|
package io.element.android.features.messages.impl.attachments.preview
|
||||||
|
|
||||||
sealed interface AttachmentsPreviewEvents {
|
sealed interface AttachmentsPreviewEvent {
|
||||||
data object SendAttachment : AttachmentsPreviewEvents
|
data object SendAttachment : AttachmentsPreviewEvent
|
||||||
data object CancelAndDismiss : AttachmentsPreviewEvents
|
data object CancelAndDismiss : AttachmentsPreviewEvent
|
||||||
data object CancelAndClearSendState : AttachmentsPreviewEvents
|
data object CancelAndClearSendState : AttachmentsPreviewEvent
|
||||||
}
|
}
|
||||||
|
|
@ -98,7 +98,7 @@ class AttachmentsPreviewPresenter(
|
||||||
val mediaOptimizationSelectorPresenter = remember {
|
val mediaOptimizationSelectorPresenter = remember {
|
||||||
mediaOptimizationSelectorPresenterFactory.create(mediaAttachment.localMedia)
|
mediaOptimizationSelectorPresenterFactory.create(mediaAttachment.localMedia)
|
||||||
}
|
}
|
||||||
val mediaOptimizationSelectorState = mediaOptimizationSelectorPresenter.present()
|
val mediaOptimizationSelectorState by rememberUpdatedState(mediaOptimizationSelectorPresenter.present())
|
||||||
|
|
||||||
val observableSendState = snapshotFlow { sendActionState.value }
|
val observableSendState = snapshotFlow { sendActionState.value }
|
||||||
|
|
||||||
|
|
@ -140,9 +140,9 @@ class AttachmentsPreviewPresenter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleEvent(event: AttachmentsPreviewEvents) {
|
fun handleEvent(event: AttachmentsPreviewEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is AttachmentsPreviewEvents.SendAttachment -> {
|
is AttachmentsPreviewEvent.SendAttachment -> {
|
||||||
ongoingSendAttachmentJob.value = coroutineScope.launch {
|
ongoingSendAttachmentJob.value = coroutineScope.launch {
|
||||||
// If the media optimization selector is displayed, we need to wait for the user to select the options
|
// 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.
|
// before we can pre-process the media.
|
||||||
|
|
@ -191,7 +191,7 @@ class AttachmentsPreviewPresenter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AttachmentsPreviewEvents.CancelAndDismiss -> {
|
AttachmentsPreviewEvent.CancelAndDismiss -> {
|
||||||
displayFileTooLargeError = false
|
displayFileTooLargeError = false
|
||||||
|
|
||||||
// Cancel media preprocessing and sending
|
// Cancel media preprocessing and sending
|
||||||
|
|
@ -206,7 +206,7 @@ class AttachmentsPreviewPresenter(
|
||||||
sendActionState,
|
sendActionState,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
AttachmentsPreviewEvents.CancelAndClearSendState -> {
|
AttachmentsPreviewEvent.CancelAndClearSendState -> {
|
||||||
// Cancel media sending
|
// Cancel media sending
|
||||||
ongoingSendAttachmentJob.value?.let {
|
ongoingSendAttachmentJob.value?.let {
|
||||||
it.cancel()
|
it.cancel()
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ data class AttachmentsPreviewState(
|
||||||
val textEditorState: TextEditorState,
|
val textEditorState: TextEditorState,
|
||||||
val mediaOptimizationSelectorState: MediaOptimizationSelectorState,
|
val mediaOptimizationSelectorState: MediaOptimizationSelectorState,
|
||||||
val displayFileTooLargeError: Boolean,
|
val displayFileTooLargeError: Boolean,
|
||||||
val eventSink: (AttachmentsPreviewEvents) -> Unit
|
val eventSink: (AttachmentsPreviewEvent) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
|
|
||||||
|
|
@ -82,15 +82,15 @@ fun AttachmentsPreviewView(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
fun postSendAttachment() {
|
fun postSendAttachment() {
|
||||||
state.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
state.eventSink(AttachmentsPreviewEvent.SendAttachment)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun postCancel() {
|
fun postCancel() {
|
||||||
state.eventSink(AttachmentsPreviewEvents.CancelAndDismiss)
|
state.eventSink(AttachmentsPreviewEvent.CancelAndDismiss)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun postClearSendState() {
|
fun postClearSendState() {
|
||||||
state.eventSink(AttachmentsPreviewEvents.CancelAndClearSendState)
|
state.eventSink(AttachmentsPreviewEvent.CancelAndClearSendState)
|
||||||
}
|
}
|
||||||
|
|
||||||
BackHandler(enabled = state.sendActionState !is SendActionState.Sending.Uploading && state.sendActionState !is SendActionState.Done) {
|
BackHandler(enabled = state.sendActionState !is SendActionState.Sending.Uploading && state.sendActionState !is SendActionState.Done) {
|
||||||
|
|
@ -199,7 +199,7 @@ private fun AttachmentPreviewContent(
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
title = stringResource(CommonStrings.dialog_file_too_large_to_upload_title),
|
title = stringResource(CommonStrings.dialog_file_too_large_to_upload_title),
|
||||||
content = content,
|
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