Merge branch 'release/25.08.1'
This commit is contained in:
commit
499a99a5bb
228 changed files with 2757 additions and 1666 deletions
58
CHANGES.md
58
CHANGES.md
|
|
@ -1,3 +1,61 @@
|
|||
Changes in Element X v25.08.0
|
||||
=============================
|
||||
|
||||
<!-- Release notes generated using configuration in .github/release.yml at v25.08.0 -->
|
||||
|
||||
## What's Changed
|
||||
### 🐛 Bugfixes
|
||||
* Fix `toPlainText` where `<ol start='n'>` tags appear by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5044
|
||||
* Remove the scaling added in `Player.Listener.onVideoSizeChanged` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5055
|
||||
* Make sure we clean up the pre-processed and uploaded media by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5039
|
||||
* Calculate video output size taking into account portrait mode by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5068
|
||||
* Prevent loop when exiting the attachments preview screen by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5078
|
||||
* Prevent crash caused by re-release of wakelock in calls by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5077
|
||||
* Make sure we display errors when we create a recovery key and it fails by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5079
|
||||
* Fix crash when trying to get active notifications by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5085
|
||||
* Adapt 'change roles' screens to the new creator/owner role by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5076
|
||||
### 🗣 Translations
|
||||
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5021
|
||||
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5054
|
||||
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5083
|
||||
### 🧱 Build
|
||||
* Disable Element Call Maestro tests for the time being by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5064
|
||||
### 📄 Documentation
|
||||
* Grammar fixes for docs and comments by @andybalaam in https://github.com/element-hq/element-x-android/pull/5043
|
||||
* Note how to switch back to the published SDK after building locally by @andybalaam in https://github.com/element-hq/element-x-android/pull/5042
|
||||
### Dependency upgrades
|
||||
* Update dependency io.mockk:mockk to v1.14.5 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5037
|
||||
* Update dependency androidx.lifecycle:lifecycle-process to v2.9.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5036
|
||||
* Update dagger to v2.57 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5038
|
||||
* Update haze to v1.6.9 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5045
|
||||
* Update dependency io.nlopez.compose.rules:detekt to v0.4.24 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5053
|
||||
* Update dependency io.nlopez.compose.rules:detekt to v0.4.25 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5058
|
||||
* Update coil to v3.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5063
|
||||
* Update dependency io.nlopez.compose.rules:detekt to v0.4.26 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5065
|
||||
* Update dependency com.posthog:posthog-android to v3.20.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5067
|
||||
* Update dependency com.google.firebase:firebase-bom to v34 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5061
|
||||
* Update dependency org.matrix.rustcomponents:sdk-android to v25.7.23 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5073
|
||||
* Update dependency com.posthog:posthog-android to v3.20.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5087
|
||||
* Update dependency org.matrix.rustcomponents:sdk-android to v25.7.28 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5088
|
||||
* Update dependency org.maplibre.gl:android-sdk to v11.13.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5093
|
||||
* Update dependency androidx.test:runner to v1.7.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5102
|
||||
* Update test.core to v1.7.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5104
|
||||
* Update dependency androidx.test.ext:junit to v1.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5103
|
||||
* Update dependency io.sentry:sentry-android to v8.18.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5097
|
||||
### Others
|
||||
* Iterate on FloatingActionButton shape and colors. by @bmarty in https://github.com/element-hq/element-x-android/pull/5033
|
||||
* [a11y] Improve session verification screens by @bmarty in https://github.com/element-hq/element-x-android/pull/5017
|
||||
* misc (room id) : add room id regex pattern to match new versions by @ganfra in https://github.com/element-hq/element-x-android/pull/5040
|
||||
* Use lower level APIs to draw the message bubbles by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5056
|
||||
* misc (store description) : update store description for fastlane by @ganfra in https://github.com/element-hq/element-x-android/pull/5060
|
||||
* [a11y] Improve accessibility on avatar when creating a room. by @bmarty in https://github.com/element-hq/element-x-android/pull/5046
|
||||
* Add fallback notifications from UTDs to the push history by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5047
|
||||
* feature (media send queue) : enable send queue by default by @ganfra in https://github.com/element-hq/element-x-android/pull/5098
|
||||
* misc : re-enable share pos by default by @ganfra in https://github.com/element-hq/element-x-android/pull/5108
|
||||
|
||||
|
||||
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.07.1...v25.08.0
|
||||
|
||||
Changes in Element X v25.07.1
|
||||
=============================
|
||||
|
||||
|
|
|
|||
|
|
@ -318,6 +318,7 @@ licensee {
|
|||
allow("MIT")
|
||||
allow("BSD-2-Clause")
|
||||
allow("BSD-3-Clause")
|
||||
allow("EPL-1.0")
|
||||
allowUrl("https://opensource.org/licenses/MIT")
|
||||
allowUrl("https://developer.android.com/studio/terms.html")
|
||||
allowUrl("https://www.zetetic.net/sqlcipher/license/")
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.MembershipCha
|
|||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
|
@ -28,6 +29,7 @@ class LoggedInEventProcessor @Inject constructor(
|
|||
fun observeEvents(coroutineScope: CoroutineScope) {
|
||||
observingJob = roomMembershipObserver.updates
|
||||
.filter { !it.isUserInRoom }
|
||||
.distinctUntilChanged()
|
||||
.onEach {
|
||||
when (it.change) {
|
||||
MembershipChange.LEFT -> displayMessage(CommonStrings.common_current_user_left_room)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="banner_migrate_to_native_sliding_sync_action">"خروج و ارتقا"</string>
|
||||
<string name="banner_migrate_to_native_sliding_sync_app_force_logout_title">"%1$s دیگر از شیوهنامهٔ قدیمی پشتیبانی نمیکند. لطفاً برای ادامهٔ استفاده از کاره، خارج شده و دوباره وارد شوید."</string>
|
||||
</resources>
|
||||
|
|
|
|||
4
fastlane/metadata/android/en-US/changelogs/202508010.txt
Normal file
4
fastlane/metadata/android/en-US/changelogs/202508010.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
Main changes in this version:
|
||||
- Fixes for the room v12 changes.
|
||||
- Several bug fixes, centered on media and calls.
|
||||
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||
|
|
@ -33,14 +33,11 @@ import io.element.android.libraries.architecture.Presenter
|
|||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
|
||||
import io.element.android.libraries.network.useragent.UserAgentProvider
|
||||
import io.element.android.services.analytics.api.ScreenTracker
|
||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||
import io.element.android.services.appnavstate.api.AppForegroundStateService
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -64,7 +61,6 @@ class CallScreenPresenter @AssistedInject constructor(
|
|||
private val activeCallManager: ActiveCallManager,
|
||||
private val languageTagProvider: LanguageTagProvider,
|
||||
private val appForegroundStateService: AppForegroundStateService,
|
||||
private val activeRoomsHolder: ActiveRoomsHolder,
|
||||
@AppCoroutineScope
|
||||
private val appCoroutineScope: CoroutineScope,
|
||||
) : Presenter<CallScreenState> {
|
||||
|
|
@ -75,7 +71,6 @@ class CallScreenPresenter @AssistedInject constructor(
|
|||
|
||||
private val isInWidgetMode = callType is CallType.RoomCall
|
||||
private val userAgent = userAgentProvider.provide()
|
||||
private var notifiedCallStart = false
|
||||
|
||||
@Composable
|
||||
override fun present(): CallScreenState {
|
||||
|
|
@ -248,9 +243,7 @@ class CallScreenPresenter @AssistedInject constructor(
|
|||
Timber.d("Observing sync state in-call for sessionId: ${roomCallType.sessionId}")
|
||||
client.syncService().syncState
|
||||
.collect { state ->
|
||||
if (state == SyncState.Running) {
|
||||
client.notifyCallStartIfNeeded(callType.roomId)
|
||||
} else {
|
||||
if (state != SyncState.Running) {
|
||||
appForegroundStateService.updateIsInCallState(true)
|
||||
}
|
||||
}
|
||||
|
|
@ -263,32 +256,6 @@ class CallScreenPresenter @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun MatrixClient.notifyCallStartIfNeeded(roomId: RoomId) {
|
||||
if (notifiedCallStart) return
|
||||
|
||||
val activeRoomForSession = activeRoomsHolder.getActiveRoomMatching(sessionId, roomId)
|
||||
val sendCallNotificationResult = if (activeRoomForSession != null) {
|
||||
Timber.d("Notifying call start for room $roomId. Has room call: ${activeRoomForSession.info().hasRoomCall}")
|
||||
activeRoomForSession.sendCallNotificationIfNeeded()
|
||||
} else {
|
||||
// Instantiate the room from the session and roomId and send the notification
|
||||
getJoinedRoom(roomId)?.use { room ->
|
||||
Timber.d("Notifying call start for room $roomId. Has room call: ${room.info().hasRoomCall}")
|
||||
room.sendCallNotificationIfNeeded()
|
||||
} ?: run {
|
||||
Timber.w("No room found for session $sessionId and room $roomId, skipping call notification.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
sendCallNotificationResult.fold(
|
||||
onSuccess = { notifiedCallStart = true },
|
||||
onFailure = { error ->
|
||||
Timber.e(error, "Failed to send call notification for room $roomId.")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseMessage(message: String): WidgetMessage? {
|
||||
return WidgetMessageSerializer.deserialize(message).getOrNull()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import io.element.android.libraries.di.AppScope
|
|||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.room.isDm
|
||||
import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
|
||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||
|
|
@ -44,7 +45,7 @@ class DefaultCallWidgetProvider @Inject constructor(
|
|||
val baseUrl = customBaseUrl ?: EMBEDDED_CALL_WIDGET_BASE_URL
|
||||
|
||||
val isEncrypted = room.info().isEncrypted ?: room.getUpdatedIsEncrypted().getOrThrow()
|
||||
val widgetSettings = callWidgetSettingsProvider.provide(baseUrl, encrypted = isEncrypted)
|
||||
val widgetSettings = callWidgetSettingsProvider.provide(baseUrl, encrypted = isEncrypted, direct = room.isDm())
|
||||
val callUrl = room.generateWidgetWebViewUrl(
|
||||
widgetSettings = widgetSettings,
|
||||
clientId = clientId,
|
||||
|
|
|
|||
|
|
@ -3,5 +3,6 @@
|
|||
<string name="call_foreground_service_channel_title_android">"Käynnissä oleva puhelu"</string>
|
||||
<string name="call_foreground_service_message_android">"Palaa puheluun napauttamalla"</string>
|
||||
<string name="call_foreground_service_title_android">"☎️ Puhelu käynnissä"</string>
|
||||
<string name="call_invalid_audio_device_bluetooth_devices_disabled">"Element Call ei tue Bluetooth-äänilaitteiden käyttöä tässä Android-versiossa. Valitse toinen äänilaite."</string>
|
||||
<string name="screen_incoming_call_subtitle_android">"Saapuva Element Call -puhelu"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -26,13 +26,11 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
|
|||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||
import io.element.android.libraries.matrix.test.sync.FakeSyncService
|
||||
import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
|
||||
import io.element.android.libraries.network.useragent.UserAgentProvider
|
||||
import io.element.android.services.analytics.api.ScreenTracker
|
||||
import io.element.android.services.analytics.test.FakeScreenTracker
|
||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
|
|
@ -82,19 +80,12 @@ import kotlin.time.Duration.Companion.seconds
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `present - with CallType RoomCall sets call as active, loads URL, runs WidgetDriver and notifies the other clients a call started`() = runTest {
|
||||
val sendCallNotificationIfNeededLambda = lambdaRecorder<Result<Boolean>> { Result.success(true) }
|
||||
val syncService = FakeSyncService(SyncState.Running)
|
||||
val fakeRoom = FakeJoinedRoom(sendCallNotificationIfNeededResult = sendCallNotificationIfNeededLambda)
|
||||
val client = FakeMatrixClient(syncService = syncService).apply {
|
||||
givenGetRoomResult(A_ROOM_ID, fakeRoom)
|
||||
}
|
||||
fun `present - with CallType RoomCall sets call as active, loads URL and runs WidgetDriver`() = runTest {
|
||||
val widgetDriver = FakeMatrixWidgetDriver()
|
||||
val widgetProvider = FakeCallWidgetProvider(widgetDriver)
|
||||
val analyticsLambda = lambdaRecorder<MobileScreen.ScreenName, Unit> {}
|
||||
val joinedCallLambda = lambdaRecorder<CallType, Unit> {}
|
||||
val presenter = createCallScreenPresenter(
|
||||
matrixClientsProvider = FakeMatrixClientProvider(getClient = { Result.success(client) }),
|
||||
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
|
||||
widgetDriver = widgetDriver,
|
||||
widgetProvider = widgetProvider,
|
||||
|
|
@ -116,7 +107,6 @@ import kotlin.time.Duration.Companion.seconds
|
|||
assertThat(widgetProvider.getWidgetCalled).isTrue()
|
||||
assertThat(widgetDriver.runCalledCount).isEqualTo(1)
|
||||
analyticsLambda.assertions().isCalledOnce().with(value(MobileScreen.ScreenName.RoomCall))
|
||||
sendCallNotificationIfNeededLambda.assertions().isCalledOnce()
|
||||
|
||||
// Wait until the WidgetDriver is loaded
|
||||
skipItems(1)
|
||||
|
|
@ -399,7 +389,6 @@ import kotlin.time.Duration.Companion.seconds
|
|||
activeCallManager: FakeActiveCallManager = FakeActiveCallManager(),
|
||||
screenTracker: ScreenTracker = FakeScreenTracker(),
|
||||
appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(),
|
||||
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
|
||||
): CallScreenPresenter {
|
||||
val userAgentProvider = object : UserAgentProvider {
|
||||
override fun provide(): String {
|
||||
|
|
@ -420,7 +409,6 @@ import kotlin.time.Duration.Companion.seconds
|
|||
languageTagProvider = FakeLanguageTagProvider("en-US"),
|
||||
appForegroundStateService = appForegroundStateService,
|
||||
appCoroutineScope = backgroundScope,
|
||||
activeRoomsHolder = activeRoomsHolder,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
27
features/changeroommemberroles/api/build.gradle.kts
Normal file
27
features/changeroommemberroles/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import extension.setupAnvil
|
||||
|
||||
/*
|
||||
* Copyright 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.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
id("kotlin-parcelize")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.changeroommemberroles.api"
|
||||
}
|
||||
|
||||
setupAnvil()
|
||||
|
||||
dependencies {
|
||||
implementation(projects.anvilannotations)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
api(projects.libraries.usersearch.api)
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright 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.changeroommemberroes.api
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
|
||||
interface ChangeRoomMemberRolesEntryPoint : FeatureEntryPoint {
|
||||
fun builder(parentNode: Node, buildContext: BuildContext): Builder
|
||||
|
||||
interface Builder {
|
||||
fun room(room: JoinedRoom): Builder
|
||||
fun listType(changeRoomMemberRolesListType: ChangeRoomMemberRolesListType): Builder
|
||||
fun build(): Node
|
||||
}
|
||||
|
||||
interface NodeProxy {
|
||||
val roomId: RoomId
|
||||
suspend fun waitForRoleChanged()
|
||||
}
|
||||
}
|
||||
|
||||
enum class ChangeRoomMemberRolesListType : NodeInputs {
|
||||
SelectNewOwnersWhenLeaving,
|
||||
Admins,
|
||||
Moderators
|
||||
}
|
||||
51
features/changeroommemberroles/impl/build.gradle.kts
Normal file
51
features/changeroommemberroles/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import extension.setupAnvil
|
||||
|
||||
/*
|
||||
* Copyright 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.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
id("kotlin-parcelize")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.changeroommemberroles.impl"
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
isIncludeAndroidResources = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setupAnvil()
|
||||
|
||||
dependencies {
|
||||
api(projects.features.changeroommemberroles.api)
|
||||
implementation(projects.appnav)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
// For test fixtures used in previews
|
||||
implementation(projects.libraries.previewutils)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.services.analytics.api)
|
||||
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles
|
||||
package io.element.android.features.changeroommemberroles.impl
|
||||
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
|
||||
|
|
@ -1,14 +1,15 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
* Copyright 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.roomdetails.impl.rolesandpermissions.changeroles
|
||||
package io.element.android.features.changeroommemberroles.impl
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
|
|
@ -16,11 +17,13 @@ import com.bumble.appyx.core.plugin.Plugin
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesListType
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.appyx.launchMolecule
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
class ChangeRolesNode @AssistedInject constructor(
|
||||
|
|
@ -28,31 +31,30 @@ class ChangeRolesNode @AssistedInject constructor(
|
|||
@Assisted plugins: List<Plugin>,
|
||||
presenterFactory: ChangeRolesPresenter.Factory,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
sealed interface ListType : Parcelable {
|
||||
@Parcelize
|
||||
data object Admins : ListType
|
||||
@Parcelize
|
||||
data object Moderators : ListType
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class Inputs(
|
||||
val listType: ListType,
|
||||
) : NodeInputs, Parcelable
|
||||
val listType: ChangeRoomMemberRolesListType,
|
||||
) : NodeInputs
|
||||
|
||||
private val inputs: Inputs = inputs()
|
||||
|
||||
private val presenter = presenterFactory.run {
|
||||
val role = when (inputs.listType) {
|
||||
is ListType.Admins -> RoomMember.Role.Admin
|
||||
is ListType.Moderators -> RoomMember.Role.Moderator
|
||||
ChangeRoomMemberRolesListType.Admins -> RoomMember.Role.Admin
|
||||
ChangeRoomMemberRolesListType.Moderators -> RoomMember.Role.Moderator
|
||||
ChangeRoomMemberRolesListType.SelectNewOwnersWhenLeaving -> RoomMember.Role.Owner(isCreator = false)
|
||||
}
|
||||
create(role)
|
||||
}
|
||||
|
||||
private val stateFlow = launchMolecule { presenter.present() }
|
||||
|
||||
suspend fun waitForRoleChanged() {
|
||||
stateFlow.first { it.savingState.isSuccess() }
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
val state by stateFlow.collectAsState()
|
||||
ChangeRolesView(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
* Copyright 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.roomdetails.impl.rolesandpermissions.changeroles
|
||||
package io.element.android.features.changeroommemberroles.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
|
|
@ -22,9 +22,6 @@ import dagger.assisted.Assisted
|
|||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.RoomModeration
|
||||
import io.element.android.features.roomdetails.impl.analytics.toAnalyticsMemberRole
|
||||
import io.element.android.features.roomdetails.impl.members.PowerLevelRoomMemberComparator
|
||||
import io.element.android.features.roomdetails.impl.members.RoomMemberListDataSource
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
|
|
@ -37,6 +34,7 @@ import io.element.android.libraries.matrix.api.room.powerlevels.usersWithRole
|
|||
import io.element.android.libraries.matrix.api.room.toMatrixUser
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.model.roleOf
|
||||
import io.element.android.libraries.matrix.ui.room.PowerLevelRoomMemberComparator
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
|
|
@ -136,8 +134,9 @@ class ChangeRolesPresenter @AssistedInject constructor(
|
|||
val isModifyingAdmins = role == RoomMember.Role.Admin
|
||||
val hasChanges = selectedUsers != usersWithRole
|
||||
val isConfirming = saveState.value.isConfirming()
|
||||
val modifyingOwners = role is RoomMember.Role.Owner
|
||||
|
||||
val needsConfirmation = currentUserIsAdmin && isModifyingAdmins && hasChanges && !isConfirming
|
||||
val needsConfirmation = (modifyingOwners || currentUserIsAdmin && isModifyingAdmins) && hasChanges && !isConfirming
|
||||
|
||||
when {
|
||||
needsConfirmation -> {
|
||||
|
|
@ -229,3 +228,10 @@ class ChangeRolesPresenter @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun RoomMember.Role.toAnalyticsMemberRole(): RoomModeration.Role = when (this) {
|
||||
is RoomMember.Role.Owner -> RoomModeration.Role.Administrator // TODO - distinguish creator from admin
|
||||
RoomMember.Role.Admin -> RoomModeration.Role.Administrator
|
||||
RoomMember.Role.Moderator -> RoomModeration.Role.Moderator
|
||||
RoomMember.Role.User -> RoomModeration.Role.User
|
||||
}
|
||||
|
|
@ -5,14 +5,14 @@
|
|||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles
|
||||
package io.element.android.features.changeroommemberroles.impl
|
||||
|
||||
import io.element.android.features.roomdetails.impl.members.PowerLevelRoomMemberComparator
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.room.PowerLevelRoomMemberComparator
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
|
|
@ -5,11 +5,9 @@
|
|||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles
|
||||
package io.element.android.features.changeroommemberroles.impl
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
||||
import io.element.android.features.roomdetails.impl.members.aRoomMemberList
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
|
@ -18,6 +16,8 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
|||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
import io.element.android.libraries.previewutils.room.aRoomMember
|
||||
import io.element.android.libraries.previewutils.room.aRoomMemberList
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
|
@ -44,6 +44,7 @@ class ChangeRolesStateProvider : PreviewParameterProvider<ChangeRolesState> {
|
|||
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.Success(Unit)),
|
||||
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.Failure(Exception("boom"))),
|
||||
aChangeRolesStateWithOwners(),
|
||||
aChangeRolesStateWithOwners().copy(role = RoomMember.Role.Owner(isCreator = false)),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
* Copyright 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.roomdetails.impl.rolesandpermissions.changeroles
|
||||
package io.element.android.features.changeroommemberroles.impl
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
|
|
@ -40,7 +40,6 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.roomdetails.impl.R
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||
|
|
@ -87,7 +86,6 @@ fun ChangeRolesView(
|
|||
BackHandler(enabled = !state.isSearchActive) {
|
||||
state.eventSink(ChangeRolesEvent.Exit)
|
||||
}
|
||||
|
||||
Box(modifier = modifier) {
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
|
|
@ -97,9 +95,10 @@ fun ChangeRolesView(
|
|||
AnimatedVisibility(visible = !state.isSearchActive) {
|
||||
TopAppBar(
|
||||
titleStr = when (state.role) {
|
||||
is RoomMember.Role.Owner -> stringResource(R.string.screen_room_change_role_owners_title)
|
||||
RoomMember.Role.Admin -> stringResource(R.string.screen_room_change_role_administrators_title)
|
||||
RoomMember.Role.Moderator -> stringResource(R.string.screen_room_change_role_moderators_title)
|
||||
is RoomMember.Role.Owner, RoomMember.Role.User -> error("This should never be reached")
|
||||
RoomMember.Role.User -> error("This should never be reached")
|
||||
},
|
||||
navigationIcon = {
|
||||
BackButton(onClick = { state.eventSink(ChangeRolesEvent.Exit) })
|
||||
|
|
@ -188,14 +187,26 @@ fun ChangeRolesView(
|
|||
|
||||
when (state.savingState) {
|
||||
is AsyncAction.Confirming -> {
|
||||
if (state.role == RoomMember.Role.Admin) {
|
||||
// Confirm adding new admins dialogs
|
||||
ConfirmationDialog(
|
||||
title = stringResource(R.string.screen_room_change_role_confirm_add_admin_title),
|
||||
content = stringResource(R.string.screen_room_change_role_confirm_add_admin_description),
|
||||
onSubmitClick = { state.eventSink(ChangeRolesEvent.Save) },
|
||||
onDismiss = { state.eventSink(ChangeRolesEvent.ClearError) }
|
||||
)
|
||||
when (state.role) {
|
||||
is RoomMember.Role.Owner -> {
|
||||
ConfirmationDialog(
|
||||
title = stringResource(R.string.screen_room_change_role_confirm_change_owners_title),
|
||||
content = stringResource(R.string.screen_room_change_role_confirm_change_owners_description),
|
||||
submitText = stringResource(CommonStrings.action_continue),
|
||||
onSubmitClick = { state.eventSink(ChangeRolesEvent.Save) },
|
||||
onDismiss = { state.eventSink(ChangeRolesEvent.ClearError) },
|
||||
destructiveSubmit = true,
|
||||
)
|
||||
}
|
||||
is RoomMember.Role.Admin -> {
|
||||
ConfirmationDialog(
|
||||
title = stringResource(R.string.screen_room_change_role_confirm_add_admin_title),
|
||||
content = stringResource(R.string.screen_room_change_role_confirm_add_admin_description),
|
||||
onSubmitClick = { state.eventSink(ChangeRolesEvent.Save) },
|
||||
onDismiss = { state.eventSink(ChangeRolesEvent.ClearError) }
|
||||
)
|
||||
}
|
||||
else -> Unit // No confirmation needed for Moderator or User roles
|
||||
}
|
||||
}
|
||||
is AsyncAction.Loading -> {
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright 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.changeroommemberroles.impl
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.composable.Children
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.node.ParentNode
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.appnav.di.RoomComponentFactory
|
||||
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesEntryPoint
|
||||
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesListType
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.di.DaggerComponentOwner
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
class ChangeRoomMemberRolesRootNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
roomComponentFactory: RoomComponentFactory,
|
||||
) : ParentNode<ChangeRoomMemberRolesRootNode.NavTarget>(
|
||||
navModel = PermanentNavModel(
|
||||
navTargets = setOf(NavTarget.Root),
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
), DaggerComponentOwner, ChangeRoomMemberRolesEntryPoint.NodeProxy {
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
object Root : NavTarget
|
||||
}
|
||||
|
||||
data class Inputs(
|
||||
val joinedRoom: JoinedRoom,
|
||||
val listType: ChangeRoomMemberRolesListType,
|
||||
) : NodeInputs
|
||||
|
||||
private val inputs = inputs<Inputs>()
|
||||
|
||||
override val daggerComponent = roomComponentFactory.create(inputs.joinedRoom)
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
NavTarget.Root -> {
|
||||
createNode<ChangeRolesNode>(
|
||||
buildContext = buildContext,
|
||||
plugins = listOf(ChangeRolesNode.Inputs(listType = inputs.listType)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
Children(modifier = modifier, navModel = navModel)
|
||||
}
|
||||
|
||||
override val roomId: RoomId = inputs.joinedRoom.roomId
|
||||
|
||||
override suspend fun waitForRoleChanged() {
|
||||
waitForChildAttached<ChangeRolesNode>().waitForRoleChanged()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright 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.changeroommemberroles.impl
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesEntryPoint
|
||||
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesListType
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class DefaultChangeRoomMemberRolesEntyPoint @Inject constructor() : ChangeRoomMemberRolesEntryPoint {
|
||||
override fun builder(parentNode: Node, buildContext: BuildContext): ChangeRoomMemberRolesEntryPoint.Builder {
|
||||
return object : ChangeRoomMemberRolesEntryPoint.Builder {
|
||||
private lateinit var changeRoomMemberRolesListType: ChangeRoomMemberRolesListType
|
||||
private lateinit var room: JoinedRoom
|
||||
|
||||
override fun room(room: JoinedRoom): ChangeRoomMemberRolesEntryPoint.Builder {
|
||||
this.room = room
|
||||
return this
|
||||
}
|
||||
|
||||
override fun listType(changeRoomMemberRolesListType: ChangeRoomMemberRolesListType): ChangeRoomMemberRolesEntryPoint.Builder {
|
||||
this.changeRoomMemberRolesListType = changeRoomMemberRolesListType
|
||||
return this
|
||||
}
|
||||
|
||||
override fun build(): Node {
|
||||
return parentNode.createNode<ChangeRoomMemberRolesRootNode>(
|
||||
buildContext = buildContext,
|
||||
plugins = listOf(
|
||||
ChangeRoomMemberRolesRootNode.Inputs(joinedRoom = room, listType = changeRoomMemberRolesListType),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright 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.changeroommemberroles.impl
|
||||
|
||||
import io.element.android.libraries.core.bool.orFalse
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.matrix.api.room.BaseRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomMemberListDataSource @Inject constructor(
|
||||
private val room: BaseRoom,
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
) {
|
||||
suspend fun search(query: String): List<RoomMember> = withContext(coroutineDispatchers.io) {
|
||||
val roomMembersState = room.membersStateFlow.value
|
||||
val activeRoomMembers = roomMembersState.roomMembers()
|
||||
?.filter { it.membership.isActive() }
|
||||
.orEmpty()
|
||||
val filteredMembers = if (query.isBlank()) {
|
||||
activeRoomMembers
|
||||
} else {
|
||||
activeRoomMembers.filter { member ->
|
||||
member.userId.value.contains(query, ignoreCase = true) ||
|
||||
member.displayName?.contains(query, ignoreCase = true).orFalse()
|
||||
}
|
||||
}
|
||||
filteredMembers
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_change_permissions_administrators">"Admins only"</string>
|
||||
<string name="screen_room_change_permissions_ban_people">"Ban people"</string>
|
||||
<string name="screen_room_change_permissions_delete_messages">"Remove messages"</string>
|
||||
<string name="screen_room_change_permissions_everyone">"Everyone"</string>
|
||||
<string name="screen_room_change_permissions_invite_people">"Invite people and accept requests to join"</string>
|
||||
<string name="screen_room_change_permissions_member_moderation">"Member moderation"</string>
|
||||
<string name="screen_room_change_permissions_messages_and_content">"Messages and content"</string>
|
||||
<string name="screen_room_change_permissions_moderators">"Admins and moderators"</string>
|
||||
<string name="screen_room_change_permissions_remove_people">"Remove people and decline requests to join"</string>
|
||||
<string name="screen_room_change_permissions_room_avatar">"Change room avatar"</string>
|
||||
<string name="screen_room_change_permissions_room_details">"Room details"</string>
|
||||
<string name="screen_room_change_permissions_room_name">"Change room name"</string>
|
||||
<string name="screen_room_change_permissions_room_topic">"Change room topic"</string>
|
||||
<string name="screen_room_change_permissions_send_messages">"Send messages"</string>
|
||||
<string name="screen_room_change_role_administrators_title">"Edit Admins"</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_description">"You will not be able to undo this action. You are promoting the user to have the same power level as you."</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_title">"Add Admin?"</string>
|
||||
<string name="screen_room_change_role_confirm_change_owners_description">"You will not be able to undo this action. You are transferring the ownership to the selected users. Once you leave this will be permanent."</string>
|
||||
<string name="screen_room_change_role_confirm_change_owners_title">"Transfer ownership?"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_action">"Demote"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_description">"You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges."</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_title">"Demote yourself?"</string>
|
||||
<string name="screen_room_change_role_invited_member_name">"%1$s (Pending)"</string>
|
||||
<string name="screen_room_change_role_invited_member_name_android">"(Pending)"</string>
|
||||
<string name="screen_room_change_role_moderators_admin_section_footer">"Admins automatically have moderator privileges"</string>
|
||||
<string name="screen_room_change_role_moderators_owner_section_footer">"Owners automatically have admin privileges."</string>
|
||||
<string name="screen_room_change_role_moderators_title">"Edit Moderators"</string>
|
||||
<string name="screen_room_change_role_owners_title">"Choose Owners"</string>
|
||||
<string name="screen_room_change_role_section_administrators">"Admins"</string>
|
||||
<string name="screen_room_change_role_section_moderators">"Moderators"</string>
|
||||
<string name="screen_room_change_role_section_users">"Members"</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_description">"You have unsaved changes."</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_title">"Save changes?"</string>
|
||||
<string name="screen_room_member_list_banned_empty">"There are no banned users in this room."</string>
|
||||
<plurals name="screen_room_member_list_header_title">
|
||||
<item quantity="one">"%1$d person"</item>
|
||||
<item quantity="other">"%1$d people"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_member_list_manage_member_remove_confirmation_ban">"Ban from room"</string>
|
||||
<string name="screen_room_member_list_manage_member_remove_confirmation_kick">"Only remove member"</string>
|
||||
<string name="screen_room_member_list_manage_member_unban_action">"Unban"</string>
|
||||
<string name="screen_room_member_list_manage_member_unban_message">"They will be able to join this room again if invited."</string>
|
||||
<string name="screen_room_member_list_manage_member_unban_title">"Unban user"</string>
|
||||
<string name="screen_room_member_list_mode_banned">"Banned"</string>
|
||||
<string name="screen_room_member_list_mode_members">"Members"</string>
|
||||
<string name="screen_room_member_list_pending_header_title">"Pending"</string>
|
||||
<string name="screen_room_member_list_role_administrator">"Admin"</string>
|
||||
<string name="screen_room_member_list_role_moderator">"Moderator"</string>
|
||||
<string name="screen_room_member_list_role_owner">"Owner"</string>
|
||||
<string name="screen_room_member_list_room_members_header_title">"Room members"</string>
|
||||
<string name="screen_room_member_list_unbanning_user">"Unbanning %1$s"</string>
|
||||
<string name="screen_room_roles_and_permissions_admins">"Admins"</string>
|
||||
<string name="screen_room_roles_and_permissions_admins_and_owners">"Admins and owners"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_my_role">"Change my role"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_role_demote_to_member">"Demote to member"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_role_demote_to_moderator">"Demote to moderator"</string>
|
||||
<string name="screen_room_roles_and_permissions_member_moderation">"Member moderation"</string>
|
||||
<string name="screen_room_roles_and_permissions_messages_and_content">"Messages and content"</string>
|
||||
<string name="screen_room_roles_and_permissions_moderators">"Moderators"</string>
|
||||
<string name="screen_room_roles_and_permissions_owners">"Owners"</string>
|
||||
<string name="screen_room_roles_and_permissions_permissions_header">"Permissions"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset">"Reset permissions"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset_confirm_description">"Once you reset permissions, you will lose the current settings."</string>
|
||||
<string name="screen_room_roles_and_permissions_reset_confirm_title">"Reset permissions?"</string>
|
||||
<string name="screen_room_roles_and_permissions_roles_header">"Roles"</string>
|
||||
<string name="screen_room_roles_and_permissions_room_details">"Room details"</string>
|
||||
<string name="screen_room_roles_and_permissions_title">"Roles and permissions"</string>
|
||||
</resources>
|
||||
|
|
@ -1,19 +1,17 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
* Copyright 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.roomdetails.impl.rolesandpermissions.changeroles
|
||||
package io.element.android.features.changeroommemberroles.impl
|
||||
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.RoomModeration
|
||||
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
||||
import io.element.android.features.roomdetails.impl.members.aRoomMemberList
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
|
|
@ -28,14 +26,18 @@ import io.element.android.libraries.matrix.test.A_USER_ID_3
|
|||
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||
import io.element.android.libraries.matrix.test.room.aRoomMember
|
||||
import io.element.android.libraries.matrix.test.room.defaultRoomPowerLevelValues
|
||||
import io.element.android.libraries.previewutils.room.aRoomMemberList
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.collections.immutable.toPersistentMap
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import kotlin.collections.plus
|
||||
|
||||
class ChangeRolesPresenterTest {
|
||||
@Test
|
||||
|
|
@ -430,6 +432,44 @@ class ChangeRolesPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - Save will ask for confirmation before assigning new owners`() = runTest {
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
val room = FakeJoinedRoom(
|
||||
updateUserRoleResult = { Result.success(Unit) },
|
||||
baseRoom = FakeBaseRoom(updateMembersResult = { Result.success(Unit) }),
|
||||
).apply {
|
||||
givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList()))
|
||||
givenRoomInfo(
|
||||
aRoomInfo(
|
||||
roomCreators = listOf(sessionId),
|
||||
roomPowerLevels = roomPowerLevelsWithRoles(
|
||||
A_USER_ID to RoomMember.Role.Owner(isCreator = false),
|
||||
A_USER_ID_2 to RoomMember.Role.Admin,
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
val presenter = createChangeRolesPresenter(
|
||||
role = RoomMember.Role.Owner(isCreator = false),
|
||||
room = room,
|
||||
analyticsService = analyticsService
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.selectedUsers).hasSize(1)
|
||||
|
||||
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(MatrixUser(A_USER_ID_2)))
|
||||
|
||||
awaitItem().eventSink(ChangeRolesEvent.Save)
|
||||
|
||||
assertThat(awaitItem().savingState.isConfirming()).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - Save will just save the changes if the current user is a room creator and the selected users are not`() = runTest {
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
|
|
@ -510,9 +550,16 @@ class ChangeRolesPresenterTest {
|
|||
)
|
||||
}
|
||||
|
||||
private fun roomPowerLevelsWithRoles(vararg pairs: Pair<UserId, RoomMember.Role>): RoomPowerLevels {
|
||||
return RoomPowerLevels(
|
||||
values = defaultRoomPowerLevelValues(),
|
||||
users = pairs.associate { (userId, role) -> userId to role.powerLevel }.toPersistentMap()
|
||||
)
|
||||
}
|
||||
|
||||
private fun TestScope.createChangeRolesPresenter(
|
||||
role: RoomMember.Role = RoomMember.Role.Admin,
|
||||
room: FakeJoinedRoom = FakeJoinedRoom(),
|
||||
room: FakeJoinedRoom = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {})),
|
||||
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
||||
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
|
||||
): ChangeRolesPresenter {
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
* Copyright 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.roomdetails.impl.rolesandpermissions.changeroles
|
||||
package io.element.android.features.changeroommemberroles.impl
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
|
|
@ -54,20 +54,6 @@ class ChangeRolesViewTest {
|
|||
assertThat(exception).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `passing an 'Owner' role throws an exception`() {
|
||||
val exception = runCatchingExceptions {
|
||||
rule.setChangeRolesContent(
|
||||
state = aChangeRolesState(
|
||||
role = RoomMember.Role.Owner(isCreator = true),
|
||||
eventSink = EnsureNeverCalledWithParam(),
|
||||
),
|
||||
)
|
||||
}.exceptionOrNull()
|
||||
|
||||
assertThat(exception).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `back key - with search active toggles the search`() {
|
||||
val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
|
||||
|
|
@ -192,6 +178,23 @@ class ChangeRolesViewTest {
|
|||
eventsRecorder.assertSingle(ChangeRolesEvent.Save)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `save owners confirmation dialog - continue saves the changes`() {
|
||||
val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
|
||||
rule.setChangeRolesContent(
|
||||
state = aChangeRolesState(
|
||||
role = RoomMember.Role.Owner(isCreator = false),
|
||||
isSearchActive = true,
|
||||
savingState = AsyncAction.ConfirmingNoParams,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
|
||||
rule.clickOn(CommonStrings.action_continue)
|
||||
|
||||
eventsRecorder.assertSingle(ChangeRolesEvent.Save)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `save confirmation dialog - cancel removes the dialog`() {
|
||||
val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles
|
||||
package io.element.android.features.changeroommemberroles.impl
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
|
|
@ -54,6 +54,7 @@ dependencies {
|
|||
implementation(libs.haze)
|
||||
implementation(libs.haze.materials)
|
||||
implementation(projects.features.reportroom.api)
|
||||
implementation(projects.features.changeroommemberroles.api)
|
||||
api(projects.features.home.api)
|
||||
|
||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,11 @@ import android.app.Activity
|
|||
import android.os.Parcelable
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import com.bumble.appyx.core.lifecycle.subscribe
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
|
|
@ -19,31 +23,43 @@ import com.bumble.appyx.core.node.node
|
|||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.pop
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.MobileScreen
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesEntryPoint
|
||||
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesListType
|
||||
import io.element.android.features.home.api.HomeEntryPoint
|
||||
import io.element.android.features.home.impl.components.RoomListMenuAction
|
||||
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
||||
import io.element.android.features.home.impl.roomlist.RoomListEvents
|
||||
import io.element.android.features.invite.api.InviteData
|
||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteView
|
||||
import io.element.android.features.invite.api.declineandblock.DeclineInviteAndBlockEntryPoint
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomRenderer
|
||||
import io.element.android.features.logout.api.direct.DirectLogoutView
|
||||
import io.element.android.features.reportroom.api.ReportRoomEntryPoint
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.appyx.launchMolecule
|
||||
import io.element.android.libraries.deeplink.usecase.InviteFriendsUseCase
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
class HomeFlowNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val matrixClient: MatrixClient,
|
||||
private val presenter: HomePresenter,
|
||||
private val inviteFriendsUseCase: InviteFriendsUseCase,
|
||||
private val analyticsService: AnalyticsService,
|
||||
|
|
@ -51,6 +67,8 @@ class HomeFlowNode @AssistedInject constructor(
|
|||
private val directLogoutView: DirectLogoutView,
|
||||
private val reportRoomEntryPoint: ReportRoomEntryPoint,
|
||||
private val declineInviteAndBlockUserEntryPoint: DeclineInviteAndBlockEntryPoint,
|
||||
private val changeRoomMemberRolesEntryPoint: ChangeRoomMemberRolesEntryPoint,
|
||||
private val leaveRoomRenderer: LeaveRoomRenderer,
|
||||
) : BaseFlowNode<HomeFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Root,
|
||||
|
|
@ -59,12 +77,25 @@ class HomeFlowNode @AssistedInject constructor(
|
|||
buildContext = buildContext,
|
||||
plugins = plugins
|
||||
) {
|
||||
init {
|
||||
private val stateFlow = launchMolecule { presenter.present() }
|
||||
|
||||
override fun onBuilt() {
|
||||
super.onBuilt()
|
||||
lifecycle.subscribe(
|
||||
onResume = {
|
||||
analyticsService.screen(MobileScreen(screenName = MobileScreen.ScreenName.Home))
|
||||
}
|
||||
)
|
||||
whenChildAttached { commonLifecycle: Lifecycle,
|
||||
changeRoomMemberRolesNode: ChangeRoomMemberRolesEntryPoint.NodeProxy ->
|
||||
commonLifecycle.coroutineScope.launch {
|
||||
changeRoomMemberRolesNode.waitForRoleChanged()
|
||||
withContext(NonCancellable) {
|
||||
backstack.pop()
|
||||
onNewOwnersSelected(changeRoomMemberRolesNode.roomId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
|
|
@ -76,6 +107,9 @@ class HomeFlowNode @AssistedInject constructor(
|
|||
|
||||
@Parcelize
|
||||
data class DeclineInviteAndBlockUser(val inviteData: InviteData) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class SelectNewOwnersWhenLeavingRoom(val roomId: RoomId) : NavTarget
|
||||
}
|
||||
|
||||
private fun onRoomClick(roomId: RoomId) {
|
||||
|
|
@ -121,11 +155,18 @@ class HomeFlowNode @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun onSelectNewOwnersWhenLeavingRoom(roomId: RoomId) {
|
||||
backstack.push(NavTarget.SelectNewOwnersWhenLeavingRoom(roomId))
|
||||
}
|
||||
|
||||
private fun onNewOwnersSelected(roomId: RoomId) {
|
||||
stateFlow.value.roomListState.eventSink(RoomListEvents.LeaveRoom(roomId, needsConfirmation = false))
|
||||
}
|
||||
|
||||
fun rootNode(buildContext: BuildContext): Node {
|
||||
return node(buildContext) { modifier ->
|
||||
val state = presenter.present()
|
||||
val state by stateFlow.collectAsState()
|
||||
val activity = requireNotNull(LocalActivity.current)
|
||||
|
||||
HomeView(
|
||||
homeState = state,
|
||||
onRoomClick = this::onRoomClick,
|
||||
|
|
@ -138,15 +179,22 @@ class HomeFlowNode @AssistedInject constructor(
|
|||
onReportRoomClick = this::onReportRoomClick,
|
||||
onDeclineInviteAndBlockUser = this::onDeclineInviteAndBlockUserClick,
|
||||
modifier = modifier,
|
||||
) {
|
||||
acceptDeclineInviteView.Render(
|
||||
state = state.roomListState.acceptDeclineInviteState,
|
||||
onAcceptInviteSuccess = this::onRoomClick,
|
||||
onDeclineInviteSuccess = { },
|
||||
modifier = Modifier
|
||||
)
|
||||
}
|
||||
|
||||
acceptDeclineInviteView = {
|
||||
acceptDeclineInviteView.Render(
|
||||
state = state.roomListState.acceptDeclineInviteState,
|
||||
onAcceptInviteSuccess = this::onRoomClick,
|
||||
onDeclineInviteSuccess = { },
|
||||
modifier = Modifier
|
||||
)
|
||||
},
|
||||
leaveRoomView = {
|
||||
leaveRoomRenderer.Render(
|
||||
state = state.roomListState.leaveRoomState,
|
||||
onSelectNewOwners = this::onSelectNewOwnersWhenLeavingRoom,
|
||||
modifier = Modifier
|
||||
)
|
||||
}
|
||||
)
|
||||
directLogoutView.Render(state.directLogoutState)
|
||||
}
|
||||
}
|
||||
|
|
@ -160,6 +208,13 @@ class HomeFlowNode @AssistedInject constructor(
|
|||
return when (navTarget) {
|
||||
is NavTarget.ReportRoom -> reportRoomEntryPoint.createNode(this, buildContext, navTarget.roomId)
|
||||
is NavTarget.DeclineInviteAndBlockUser -> declineInviteAndBlockUserEntryPoint.createNode(this, buildContext, navTarget.inviteData)
|
||||
is NavTarget.SelectNewOwnersWhenLeavingRoom -> {
|
||||
val room = runBlocking { matrixClient.getJoinedRoom(navTarget.roomId) } ?: error("Room ${navTarget.roomId} not found")
|
||||
changeRoomMemberRolesEntryPoint.builder(this, buildContext)
|
||||
.room(room)
|
||||
.listType(ChangeRoomMemberRolesListType.SelectNewOwnersWhenLeaving)
|
||||
.build()
|
||||
}
|
||||
NavTarget.Root -> rootNode(buildContext)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,6 @@ import io.element.android.features.home.impl.roomlist.RoomListDeclineInviteMenu
|
|||
import io.element.android.features.home.impl.roomlist.RoomListEvents
|
||||
import io.element.android.features.home.impl.roomlist.RoomListState
|
||||
import io.element.android.features.home.impl.search.RoomListSearchView
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomView
|
||||
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorContainer
|
||||
import io.element.android.libraries.androidutils.throttler.FirstThrottler
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
|
|
@ -78,8 +77,9 @@ fun HomeView(
|
|||
onMenuActionClick: (RoomListMenuAction) -> Unit,
|
||||
onReportRoomClick: (roomId: RoomId) -> Unit,
|
||||
onDeclineInviteAndBlockUser: (roomSummary: RoomListRoomSummary) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
acceptDeclineInviteView: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
leaveRoomView: @Composable () -> Unit,
|
||||
) {
|
||||
val state: RoomListState = homeState.roomListState
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
|
@ -108,7 +108,7 @@ fun HomeView(
|
|||
)
|
||||
}
|
||||
|
||||
LeaveRoomView(state = state.leaveRoomState)
|
||||
leaveRoomView()
|
||||
|
||||
HomeScaffold(
|
||||
state = homeState,
|
||||
|
|
@ -304,5 +304,6 @@ internal fun HomeViewPreview(@PreviewParameter(HomeStateProvider::class) state:
|
|||
onMenuActionClick = {},
|
||||
onDeclineInviteAndBlockUser = {},
|
||||
acceptDeclineInviteView = {},
|
||||
leaveRoomView = {}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ fun RoomListContextMenu(
|
|||
},
|
||||
onLeaveRoomClick = {
|
||||
eventSink(RoomListEvents.HideContextMenu)
|
||||
eventSink(RoomListEvents.LeaveRoom(contextMenu.roomId))
|
||||
eventSink(RoomListEvents.LeaveRoom(contextMenu.roomId, needsConfirmation = true))
|
||||
},
|
||||
onFavoriteChange = { isFavorite ->
|
||||
eventSink(RoomListEvents.SetRoomIsFavorite(contextMenu.roomId, isFavorite))
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ sealed interface RoomListEvents {
|
|||
|
||||
sealed interface ContextMenuEvents : RoomListEvents
|
||||
data object HideContextMenu : ContextMenuEvents
|
||||
data class LeaveRoom(val roomId: RoomId) : ContextMenuEvents
|
||||
data class LeaveRoom(val roomId: RoomId, val needsConfirmation: Boolean) : ContextMenuEvents
|
||||
data class MarkAsRead(val roomId: RoomId) : ContextMenuEvents
|
||||
data class MarkAsUnread(val roomId: RoomId) : ContextMenuEvents
|
||||
data class SetRoomIsFavorite(val roomId: RoomId, val isFavorite: Boolean) : ContextMenuEvents
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import io.element.android.features.invite.api.SeenInvitesStore
|
|||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents.AcceptInvite
|
||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents.DeclineInvite
|
||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent.ShowConfirmation
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
|
|
@ -127,7 +127,9 @@ class RoomListPresenter @Inject constructor(
|
|||
is RoomListEvents.HideContextMenu -> {
|
||||
contextMenu.value = RoomListState.ContextMenu.Hidden
|
||||
}
|
||||
is RoomListEvents.LeaveRoom -> leaveRoomState.eventSink(ShowConfirmation(event.roomId))
|
||||
is RoomListEvents.LeaveRoom -> {
|
||||
leaveRoomState.eventSink(LeaveRoomEvent.LeaveRoom(event.roomId, needsConfirmation = event.needsConfirmation))
|
||||
}
|
||||
is RoomListEvents.SetRoomIsFavorite -> coroutineScope.setRoomIsFavorite(event.roomId, event.isFavorite)
|
||||
is RoomListEvents.MarkAsRead -> coroutineScope.markAsRead(event.roomId)
|
||||
is RoomListEvents.MarkAsUnread -> coroutineScope.markAsUnread(event.roomId)
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ import io.element.android.features.home.impl.search.RoomListSearchState
|
|||
import io.element.android.features.home.impl.search.aRoomListSearchState
|
||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
|
||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.features.leaveroom.api.aLeaveRoomState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
|
|
@ -70,6 +70,12 @@ internal fun aRoomListState(
|
|||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
internal fun aLeaveRoomState(
|
||||
eventSink: (LeaveRoomEvent) -> Unit = {}
|
||||
) = object : LeaveRoomState {
|
||||
override val eventSink: (LeaveRoomEvent) -> Unit = eventSink
|
||||
}
|
||||
|
||||
internal fun anAcceptDeclineInviteState(
|
||||
acceptAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
|
||||
declineAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@
|
|||
<string name="confirm_recovery_key_banner_title">"Dit nøglelager er ikke synkroniseret"</string>
|
||||
<string name="full_screen_intent_banner_message">"For at sikre, at du aldrig går glip af et vigtigt opkald, skal du ændre dine indstillinger til at tillade underretninger i fuld skærm, når din telefon er låst."</string>
|
||||
<string name="full_screen_intent_banner_title">"Gør din opkaldsoplevelse bedre"</string>
|
||||
<string name="screen_home_tab_chats">"Samtaler"</string>
|
||||
<string name="screen_home_tab_spaces">"Klynger"</string>
|
||||
<string name="screen_invites_decline_chat_message">"Er du sikker på, at du vil afvise invitationen til at deltage i %1$s?"</string>
|
||||
<string name="screen_invites_decline_chat_title">"Afvis invitation"</string>
|
||||
<string name="screen_invites_decline_direct_chat_message">"Er du sikker på, at du vil afvise denne private samtale med %1$s?"</string>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="banner_battery_optimization_content_android">"Disable battery optimization for this app, to make sure all notifications are received."</string>
|
||||
<string name="banner_battery_optimization_submit_android">"Disable optimization"</string>
|
||||
<string name="screen_roomlist_filter_favourites">"Favorites"</string>
|
||||
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"You can add a chat to your favorites in the chat settings.
|
||||
For now, you can deselect filters in order to see your other chats"</string>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="banner_battery_optimization_content_android">"از کار انداختن بهینهسازی باتری برای این کاره برای اطمینان از گرفتن همهٔ آگاهیها."</string>
|
||||
<string name="banner_battery_optimization_submit_android">"از کار انداختن بهینه سازی"</string>
|
||||
<string name="banner_battery_optimization_title_android">"آگاهیها نمیرسند؟"</string>
|
||||
<string name="banner_set_up_recovery_content">"بازگردانی تاریخچهٔ پیامها و هویت رمزنگاشتهتان با کلید بازیابی در صورت از دست دادن همهٔ افزارههای موجودتان."</string>
|
||||
<string name="banner_set_up_recovery_submit">"برپایی بازیابی"</string>
|
||||
<string name="banner_set_up_recovery_title">"برپایی بازیابی"</string>
|
||||
<string name="confirm_recovery_key_banner_primary_button_title">"ورود کلید بازیابیتان"</string>
|
||||
<string name="confirm_recovery_key_banner_title">"ذخیرهساز کلیدتان از همگام بودن در آمده"</string>
|
||||
<string name="full_screen_intent_banner_title">"بهبود تجریهٔ تماستان"</string>
|
||||
<string name="screen_home_tab_chats">"گپها"</string>
|
||||
<string name="screen_home_tab_spaces">"فضاها"</string>
|
||||
<string name="screen_invites_decline_chat_message">"مطمئنید که میخواهید دعوت پیوستن به %1$s را رد کنید؟"</string>
|
||||
<string name="screen_invites_decline_chat_title">"رد دعوت"</string>
|
||||
<string name="screen_invites_decline_direct_chat_message">"مطمئنید که میخواهید این گپ خصوصی با %1$s را رد کنید؟"</string>
|
||||
|
|
@ -34,6 +40,6 @@
|
|||
<string name="screen_roomlist_main_space_title">"گپها"</string>
|
||||
<string name="screen_roomlist_mark_as_read">"علامتگذاری به عنوان خوانده شده"</string>
|
||||
<string name="screen_roomlist_mark_as_unread">"نشان به ناخوانده"</string>
|
||||
<string name="session_verification_banner_message">"به نظر می رسد از دستگاه جدیدی استفاده می کنید. برای دسترسی به پیام های رمزگذاری شده خود، با دستگاه دیگری این دستگاه را تأیید کنید."</string>
|
||||
<string name="session_verification_banner_message">"گویا از افزارهای جدید استفاده میکنید. تأیید با افزارهای دیگر برای دسترسی به پیامهای رمزنگاری شدهتان."</string>
|
||||
<string name="session_verification_banner_title">"تأیید کنید که خودتانید"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="banner_battery_optimization_content_android">"Ota tämän sovelluksen akunkäytön optimointi pois käytöstä varmistaaksesi, että kaikki ilmoitukset tulevat perille."</string>
|
||||
<string name="banner_battery_optimization_submit_android">"Ota optimointi pois käytöstä"</string>
|
||||
<string name="banner_battery_optimization_title_android">"Eikö ilmoitukset tule perille?"</string>
|
||||
<string name="banner_set_up_recovery_content">"Palauta kryptografinen identiteettisi ja viestihistoriasi palautusavaimella, mikäli menetät pääsyn kaikkiin laitteisiisi."</string>
|
||||
<string name="banner_set_up_recovery_submit">"Ota palautus käyttöön"</string>
|
||||
<string name="banner_set_up_recovery_title">"Ota palautus käyttöön tilisi suojaamiseksi"</string>
|
||||
|
|
@ -20,6 +21,7 @@
|
|||
<string name="screen_migration_message">"Tämä on kertaluonteinen prosessi, kiitos odottamisesta."</string>
|
||||
<string name="screen_migration_title">"Tiliä määritetään."</string>
|
||||
<string name="screen_roomlist_a11y_create_message">"Luo uusi keskustelu tai huone"</string>
|
||||
<string name="screen_roomlist_clear_filters">"Tyhjennä suodattimet"</string>
|
||||
<string name="screen_roomlist_empty_message">"Aloita lähettämällä viesti jollekin."</string>
|
||||
<string name="screen_roomlist_empty_title">"Sinulla ei ole vielä keskusteluja."</string>
|
||||
<string name="screen_roomlist_filter_favourites">"Suosikit"</string>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="banner_battery_optimization_submit_android">"Optimalisatie uitschakelen"</string>
|
||||
<string name="banner_set_up_recovery_content">"Herstel je cryptografische identiteit en berichtengeschiedenis met een herstelsleutel voor als je al je bestaande apparaten kwijt bent."</string>
|
||||
<string name="banner_set_up_recovery_submit">"Herstelmogelijkheid instellen"</string>
|
||||
<string name="banner_set_up_recovery_title">"Herstel instellen om je account te beschermen"</string>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@
|
|||
<string name="confirm_recovery_key_banner_title">"Ваше сховище ключів не синхронізовано"</string>
|
||||
<string name="full_screen_intent_banner_message">"Щоб ніколи не пропустити важливий виклик, змініть налаштування, щоб увімкнути повноекранні сповіщення, коли телефон заблоковано."</string>
|
||||
<string name="full_screen_intent_banner_title">"Покращуйте досвід дзвінків"</string>
|
||||
<string name="screen_home_tab_chats">"Бесіди"</string>
|
||||
<string name="screen_home_tab_spaces">"Простори"</string>
|
||||
<string name="screen_invites_decline_chat_message">"Ви впевнені, що хочете відхилити запрошення приєднатися до %1$s?"</string>
|
||||
<string name="screen_invites_decline_chat_title">"Відхилити запрошення"</string>
|
||||
<string name="screen_invites_decline_direct_chat_message">"Ви дійсно хочете відмовитися від приватної бесіди з %1$s?"</string>
|
||||
|
|
@ -21,6 +23,7 @@
|
|||
<string name="screen_migration_message">"Це одноразовий процес, дякую за очікування."</string>
|
||||
<string name="screen_migration_title">"Налаштування облікового запису."</string>
|
||||
<string name="screen_roomlist_a11y_create_message">"Створити нову розмову або кімнату"</string>
|
||||
<string name="screen_roomlist_clear_filters">"Очистити фільтри"</string>
|
||||
<string name="screen_roomlist_empty_message">"Почніть з обміну повідомленнями з кимось."</string>
|
||||
<string name="screen_roomlist_empty_title">"Ще немає бесід."</string>
|
||||
<string name="screen_roomlist_filter_favourites">"Обране"</string>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="banner_battery_optimization_content_android">"Disable battery optimization for this app, to make sure all notifications are received."</string>
|
||||
<string name="banner_battery_optimization_submit_android">"Disable optimization"</string>
|
||||
<string name="banner_battery_optimization_content_android">"Disable battery optimisation for this app, to make sure all notifications are received."</string>
|
||||
<string name="banner_battery_optimization_submit_android">"Disable optimisation"</string>
|
||||
<string name="banner_battery_optimization_title_android">"Notifications not arriving?"</string>
|
||||
<string name="banner_set_up_recovery_content">"Recover your cryptographic identity and message history with a recovery key if you have lost all your existing devices."</string>
|
||||
<string name="banner_set_up_recovery_submit">"Set up recovery"</string>
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ class RoomListContextMenuTest {
|
|||
eventsRecorder.assertList(
|
||||
listOf(
|
||||
RoomListEvents.HideContextMenu,
|
||||
RoomListEvents.LeaveRoom(contextMenu.roomId),
|
||||
RoomListEvents.LeaveRoom(contextMenu.roomId, needsConfirmation = true),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteS
|
|||
import io.element.android.features.invite.test.InMemorySeenInvitesStore
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.features.leaveroom.api.aLeaveRoomState
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||
|
|
@ -319,8 +318,8 @@ class RoomListPresenterTest {
|
|||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(RoomListEvents.LeaveRoom(A_ROOM_ID))
|
||||
leaveRoomEventsRecorder.assertSingle(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID))
|
||||
initialState.eventSink(RoomListEvents.LeaveRoom(A_ROOM_ID, needsConfirmation = true))
|
||||
leaveRoomEventsRecorder.assertSingle(LeaveRoomEvent.LeaveRoom(A_ROOM_ID, needsConfirmation = true))
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -289,7 +289,8 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomL
|
|||
onMenuActionClick = onMenuActionClick,
|
||||
onDeclineInviteAndBlockUser = onDeclineInviteAndBlockUser,
|
||||
onReportRoomClick = onReportRoomClick,
|
||||
acceptDeclineInviteView = { },
|
||||
acceptDeclineInviteView = {},
|
||||
leaveRoomView = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,15 +56,11 @@ class AcceptDeclineInvitePresenter @Inject constructor(
|
|||
)
|
||||
}
|
||||
}
|
||||
is InternalAcceptDeclineInviteEvents.CancelDeclineInvite -> {
|
||||
declinedAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
|
||||
is InternalAcceptDeclineInviteEvents.DismissAcceptError -> {
|
||||
is InternalAcceptDeclineInviteEvents.ClearAcceptActionState -> {
|
||||
acceptedAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
|
||||
is InternalAcceptDeclineInviteEvents.DismissDeclineError -> {
|
||||
is InternalAcceptDeclineInviteEvents.ClearDeclineActionState -> {
|
||||
declinedAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,9 +35,12 @@ fun AcceptDeclineInviteView(
|
|||
Box(modifier = modifier) {
|
||||
AsyncActionView(
|
||||
async = state.acceptAction,
|
||||
onSuccess = onAcceptInviteSuccess,
|
||||
onSuccess = { roomId ->
|
||||
state.eventSink(InternalAcceptDeclineInviteEvents.ClearAcceptActionState)
|
||||
onAcceptInviteSuccess(roomId)
|
||||
},
|
||||
onErrorDismiss = {
|
||||
state.eventSink(InternalAcceptDeclineInviteEvents.DismissAcceptError)
|
||||
state.eventSink(InternalAcceptDeclineInviteEvents.ClearAcceptActionState)
|
||||
},
|
||||
errorTitle = {
|
||||
stringResource(CommonStrings.common_something_went_wrong)
|
||||
|
|
@ -52,9 +55,12 @@ fun AcceptDeclineInviteView(
|
|||
)
|
||||
AsyncActionView(
|
||||
async = state.declineAction,
|
||||
onSuccess = onDeclineInviteSuccess,
|
||||
onSuccess = { roomId ->
|
||||
state.eventSink(InternalAcceptDeclineInviteEvents.ClearDeclineActionState)
|
||||
onDeclineInviteSuccess(roomId)
|
||||
},
|
||||
onErrorDismiss = {
|
||||
state.eventSink(InternalAcceptDeclineInviteEvents.DismissDeclineError)
|
||||
state.eventSink(InternalAcceptDeclineInviteEvents.ClearDeclineActionState)
|
||||
},
|
||||
errorTitle = {
|
||||
stringResource(CommonStrings.common_something_went_wrong)
|
||||
|
|
@ -78,7 +84,7 @@ fun AcceptDeclineInviteView(
|
|||
)
|
||||
},
|
||||
onDismissClick = {
|
||||
state.eventSink(InternalAcceptDeclineInviteEvents.CancelDeclineInvite)
|
||||
state.eventSink(InternalAcceptDeclineInviteEvents.ClearDeclineActionState)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ package io.element.android.features.invite.impl.acceptdecline
|
|||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
|
||||
|
||||
sealed interface InternalAcceptDeclineInviteEvents : AcceptDeclineInviteEvents {
|
||||
data object CancelDeclineInvite : InternalAcceptDeclineInviteEvents
|
||||
data object DismissAcceptError : InternalAcceptDeclineInviteEvents
|
||||
data object DismissDeclineError : InternalAcceptDeclineInviteEvents
|
||||
data object ClearAcceptActionState : InternalAcceptDeclineInviteEvents
|
||||
data object ClearDeclineActionState : InternalAcceptDeclineInviteEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ class AcceptDeclineInvitePresenterTest {
|
|||
awaitItem().also { state ->
|
||||
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, false))
|
||||
state.eventSink(
|
||||
InternalAcceptDeclineInviteEvents.CancelDeclineInvite
|
||||
InternalAcceptDeclineInviteEvents.ClearDeclineActionState
|
||||
)
|
||||
}
|
||||
awaitItem().also { state ->
|
||||
|
|
@ -90,7 +90,7 @@ class AcceptDeclineInvitePresenterTest {
|
|||
awaitItem().also { state ->
|
||||
assertThat(state.declineAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||
state.eventSink(
|
||||
InternalAcceptDeclineInviteEvents.DismissDeclineError
|
||||
InternalAcceptDeclineInviteEvents.ClearDeclineActionState
|
||||
)
|
||||
}
|
||||
awaitItem().also { state ->
|
||||
|
|
@ -154,7 +154,7 @@ class AcceptDeclineInvitePresenterTest {
|
|||
awaitItem().also { state ->
|
||||
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||
state.eventSink(
|
||||
InternalAcceptDeclineInviteEvents.DismissAcceptError
|
||||
InternalAcceptDeclineInviteEvents.ClearAcceptActionState
|
||||
)
|
||||
}
|
||||
awaitItem().also { state ->
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
<string name="screen_join_room_join_action">"Deltag i rummet"</string>
|
||||
<string name="screen_join_room_join_restricted_message">"Du skal muligvis være inviteret eller være medlem af en klynge for at deltage."</string>
|
||||
<string name="screen_join_room_knock_action">"Send anmodning om at deltage"</string>
|
||||
<string name="screen_join_room_knock_message_characters_count">"Tilladte tegn %1$d af %2$d"</string>
|
||||
<string name="screen_join_room_knock_message_description">"Besked (valgfrit)"</string>
|
||||
<string name="screen_join_room_knock_sent_description">"Du vil modtage en invitation til at deltage i rummet, hvis din anmodning accepteres."</string>
|
||||
<string name="screen_join_room_knock_sent_title">"Anmodning om at deltage sendt"</string>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
<string name="screen_join_room_join_action">"Liity huoneeseen"</string>
|
||||
<string name="screen_join_room_join_restricted_message">"Saatat tarvita kutsun tai olla tilan jäsen, jotta voit liittyä."</string>
|
||||
<string name="screen_join_room_knock_action">"Lähetä liittymispyyntö"</string>
|
||||
<string name="screen_join_room_knock_message_characters_count">"Sallitut merkit %1$d / %2$d"</string>
|
||||
<string name="screen_join_room_knock_message_description">"Viesti (valinnainen)"</string>
|
||||
<string name="screen_join_room_knock_sent_description">"Saat kutsun liittyä huoneeseen, jos pyyntösi hyväksytään."</string>
|
||||
<string name="screen_join_room_knock_sent_title">"Liittymispyyntö lähetetty"</string>
|
||||
|
|
|
|||
|
|
@ -12,19 +12,20 @@
|
|||
<string name="screen_join_room_decline_and_block_alert_title">"Refuser l’invitation et bloquer"</string>
|
||||
<string name="screen_join_room_decline_and_block_button_title">"Refuser et bloquer"</string>
|
||||
<string name="screen_join_room_fail_message">"Rejoindre le salon a échoué."</string>
|
||||
<string name="screen_join_room_fail_reason">"Ce salon est accessible uniquement sur invitation ou il peut y avoir des restrictions d’accès au niveau du Space."</string>
|
||||
<string name="screen_join_room_fail_reason">"Ce salon est accessible uniquement sur invitation ou il peut y avoir des restrictions d’accès au niveau de l’espace."</string>
|
||||
<string name="screen_join_room_forget_action">"Oublier ce salon"</string>
|
||||
<string name="screen_join_room_invite_required_message">"Vous avez besoin d’une invitation pour rejoindre ce salon"</string>
|
||||
<string name="screen_join_room_join_action">"Rejoindre"</string>
|
||||
<string name="screen_join_room_join_restricted_message">"Il est possible que vous deviez être invité ou être membre d’un Space pour pouvoir rejoindre le salon."</string>
|
||||
<string name="screen_join_room_join_restricted_message">"Il est possible que vous deviez être invité ou être membre d’un Espace pour pouvoir rejoindre le salon."</string>
|
||||
<string name="screen_join_room_knock_action">"Demander à joindre"</string>
|
||||
<string name="screen_join_room_knock_message_characters_count">"Caractères autorisés %1$d sur %2$d"</string>
|
||||
<string name="screen_join_room_knock_message_description">"Message (facultatif)"</string>
|
||||
<string name="screen_join_room_knock_sent_description">"Vous recevrez une invitation à rejoindre le salon si votre demande est acceptée."</string>
|
||||
<string name="screen_join_room_knock_sent_title">"Demande de rejoindre le salon envoyée"</string>
|
||||
<string name="screen_join_room_loading_alert_message">"Impossible d’afficher l’aperçu du salon. Cela peut être dû à des problèmes de réseau ou de serveur."</string>
|
||||
<string name="screen_join_room_loading_alert_title">"Impossible d’afficher l’aperçu de ce salon"</string>
|
||||
<string name="screen_join_room_space_not_supported_description">"Les Spaces ne sont pas encore pris en charge par %1$s. Vous pouvez voir les Spaces sur le Web."</string>
|
||||
<string name="screen_join_room_space_not_supported_title">"Les Spaces ne sont pas encore pris en charge"</string>
|
||||
<string name="screen_join_room_space_not_supported_description">"Les Espaces ne sont pas encore pris en charge par %1$s. Vous pouvez voir les Espaces sur le Web."</string>
|
||||
<string name="screen_join_room_space_not_supported_title">"Les Espaces ne sont pas encore pris en charge"</string>
|
||||
<string name="screen_join_room_subtitle_knock">"Cliquez ci-dessous et un administrateur sera prévenu. Une fois votre demande approuvée, pour pourrez rejoindre la discussion."</string>
|
||||
<string name="screen_join_room_subtitle_no_preview">"Vous devez être un membre du salon pour pouvoir lire l’historique des messages."</string>
|
||||
<string name="screen_join_room_title_knock">"Vous souhaitez rejoindre ce salon ?"</string>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_join_room_ban_reason">"Reden: %1$s."</string>
|
||||
<string name="screen_join_room_cancel_knock_action">"Verzoek annuleren"</string>
|
||||
<string name="screen_join_room_forget_action">"Deze kamer vergeten"</string>
|
||||
<string name="screen_join_room_join_action">"Toetreden tot de kamer"</string>
|
||||
<string name="screen_join_room_knock_action">"Klop om deel te nemen"</string>
|
||||
<string name="screen_join_room_knock_message_description">"Bericht (optioneel)"</string>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
<string name="screen_join_room_join_action">"Dołącz do pokoju"</string>
|
||||
<string name="screen_join_room_join_restricted_message">"Aby dołączyć, musisz uzyskać zaproszenie lub być członkiem danej przestrzeni."</string>
|
||||
<string name="screen_join_room_knock_action">"Wyślij prośbę o dołączenie"</string>
|
||||
<string name="screen_join_room_knock_message_characters_count">"Dozwolone znaki %1$d z %2$d"</string>
|
||||
<string name="screen_join_room_knock_message_description">"Wiadomość (opcjonalne)"</string>
|
||||
<string name="screen_join_room_knock_sent_description">"Otrzymasz zaproszenie dołączenia do pokoju, jeśli prośba zostanie zaakceptowana."</string>
|
||||
<string name="screen_join_room_knock_sent_title">"Wysłano prośbę o dołączenie"</string>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
<string name="screen_join_room_join_action">"Приєднатися до кімнати"</string>
|
||||
<string name="screen_join_room_join_restricted_message">"Можливо, вам знадобиться отримати запрошення або стати учасником простору, щоб приєднатися."</string>
|
||||
<string name="screen_join_room_knock_action">"Постукати, щоб приєднатися"</string>
|
||||
<string name="screen_join_room_knock_message_characters_count">"Дозволені символи %1$d з %2$d"</string>
|
||||
<string name="screen_join_room_knock_message_description">"Повідомлення (необов\'язково)"</string>
|
||||
<string name="screen_join_room_knock_sent_description">"Ви отримаєте запрошення приєднатися до кімнати, якщо ваш запит буде прийнятий."</string>
|
||||
<string name="screen_join_room_knock_sent_title">"Запит на приєднання надіслано"</string>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,5 @@ android {
|
|||
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,9 +9,6 @@ package io.element.android.features.leaveroom.api
|
|||
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
sealed interface LeaveRoomEvent {
|
||||
data class ShowConfirmation(val roomId: RoomId) : LeaveRoomEvent
|
||||
data object HideConfirmation : LeaveRoomEvent
|
||||
data class LeaveRoom(val roomId: RoomId) : LeaveRoomEvent
|
||||
data object HideError : LeaveRoomEvent
|
||||
interface LeaveRoomEvent {
|
||||
data class LeaveRoom(val roomId: RoomId, val needsConfirmation: Boolean) : LeaveRoomEvent
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright 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.leaveroom.api
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
interface LeaveRoomRenderer {
|
||||
@Composable
|
||||
fun Render(
|
||||
state: LeaveRoomState,
|
||||
onSelectNewOwners: (RoomId) -> Unit,
|
||||
modifier: Modifier,
|
||||
)
|
||||
}
|
||||
|
|
@ -7,29 +7,6 @@
|
|||
|
||||
package io.element.android.features.leaveroom.api
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
data class LeaveRoomState(
|
||||
val confirmation: Confirmation,
|
||||
val progress: Progress,
|
||||
val error: Error,
|
||||
val eventSink: (LeaveRoomEvent) -> Unit,
|
||||
) {
|
||||
sealed interface Confirmation {
|
||||
data object Hidden : Confirmation
|
||||
data class Dm(val roomId: RoomId) : Confirmation
|
||||
data class Generic(val roomId: RoomId) : Confirmation
|
||||
data class PrivateRoom(val roomId: RoomId) : Confirmation
|
||||
data class LastUserInRoom(val roomId: RoomId) : Confirmation
|
||||
}
|
||||
|
||||
sealed interface Progress {
|
||||
data object Hidden : Progress
|
||||
data object Shown : Progress
|
||||
}
|
||||
|
||||
sealed interface Error {
|
||||
data object Hidden : Error
|
||||
data object Shown : Error
|
||||
}
|
||||
interface LeaveRoomState {
|
||||
val eventSink: (LeaveRoomEvent) -> Unit
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,66 +0,0 @@
|
|||
/*
|
||||
* Copyright 2023, 2024 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.leaveroom.api
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
class LeaveRoomStateProvider : PreviewParameterProvider<LeaveRoomState> {
|
||||
override val values: Sequence<LeaveRoomState>
|
||||
get() = sequenceOf(
|
||||
aLeaveRoomState(
|
||||
confirmation = LeaveRoomState.Confirmation.Hidden,
|
||||
progress = LeaveRoomState.Progress.Hidden,
|
||||
error = LeaveRoomState.Error.Hidden,
|
||||
),
|
||||
aLeaveRoomState(
|
||||
confirmation = LeaveRoomState.Confirmation.Generic(roomId = A_ROOM_ID),
|
||||
progress = LeaveRoomState.Progress.Hidden,
|
||||
error = LeaveRoomState.Error.Hidden,
|
||||
),
|
||||
aLeaveRoomState(
|
||||
confirmation = LeaveRoomState.Confirmation.PrivateRoom(roomId = A_ROOM_ID),
|
||||
progress = LeaveRoomState.Progress.Hidden,
|
||||
error = LeaveRoomState.Error.Hidden,
|
||||
),
|
||||
aLeaveRoomState(
|
||||
confirmation = LeaveRoomState.Confirmation.LastUserInRoom(roomId = A_ROOM_ID),
|
||||
progress = LeaveRoomState.Progress.Hidden,
|
||||
error = LeaveRoomState.Error.Hidden,
|
||||
),
|
||||
aLeaveRoomState(
|
||||
confirmation = LeaveRoomState.Confirmation.Hidden,
|
||||
progress = LeaveRoomState.Progress.Shown,
|
||||
error = LeaveRoomState.Error.Hidden,
|
||||
),
|
||||
aLeaveRoomState(
|
||||
confirmation = LeaveRoomState.Confirmation.Hidden,
|
||||
progress = LeaveRoomState.Progress.Hidden,
|
||||
error = LeaveRoomState.Error.Shown,
|
||||
),
|
||||
aLeaveRoomState(
|
||||
confirmation = LeaveRoomState.Confirmation.Dm(roomId = A_ROOM_ID),
|
||||
progress = LeaveRoomState.Progress.Hidden,
|
||||
error = LeaveRoomState.Error.Hidden,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private val A_ROOM_ID = RoomId("!aRoomId:aDomain")
|
||||
|
||||
fun aLeaveRoomState(
|
||||
confirmation: LeaveRoomState.Confirmation = LeaveRoomState.Confirmation.Hidden,
|
||||
progress: LeaveRoomState.Progress = LeaveRoomState.Progress.Hidden,
|
||||
error: LeaveRoomState.Error = LeaveRoomState.Error.Hidden,
|
||||
eventSink: (LeaveRoomEvent) -> Unit = {},
|
||||
) = LeaveRoomState(
|
||||
confirmation = confirmation,
|
||||
progress = progress,
|
||||
error = error,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
/*
|
||||
* Copyright 2023, 2024 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.leaveroom.api
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
fun LeaveRoomView(
|
||||
state: LeaveRoomState
|
||||
) {
|
||||
LeaveRoomConfirmationDialog(state)
|
||||
LeaveRoomProgressDialog(state)
|
||||
LeaveRoomErrorDialog(state)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LeaveRoomConfirmationDialog(
|
||||
state: LeaveRoomState,
|
||||
) {
|
||||
when (state.confirmation) {
|
||||
is LeaveRoomState.Confirmation.Hidden -> {}
|
||||
|
||||
is LeaveRoomState.Confirmation.Dm -> LeaveRoomConfirmationDialog(
|
||||
text = R.string.leave_room_alert_private_subtitle,
|
||||
roomId = state.confirmation.roomId,
|
||||
isDm = false,
|
||||
eventSink = state.eventSink,
|
||||
)
|
||||
|
||||
is LeaveRoomState.Confirmation.PrivateRoom -> LeaveRoomConfirmationDialog(
|
||||
text = R.string.leave_room_alert_private_subtitle,
|
||||
roomId = state.confirmation.roomId,
|
||||
isDm = false,
|
||||
eventSink = state.eventSink,
|
||||
)
|
||||
|
||||
is LeaveRoomState.Confirmation.LastUserInRoom -> LeaveRoomConfirmationDialog(
|
||||
text = R.string.leave_room_alert_empty_subtitle,
|
||||
roomId = state.confirmation.roomId,
|
||||
isDm = false,
|
||||
eventSink = state.eventSink,
|
||||
)
|
||||
|
||||
is LeaveRoomState.Confirmation.Generic -> LeaveRoomConfirmationDialog(
|
||||
text = R.string.leave_room_alert_subtitle,
|
||||
roomId = state.confirmation.roomId,
|
||||
isDm = false,
|
||||
eventSink = state.eventSink,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LeaveRoomConfirmationDialog(
|
||||
@StringRes text: Int,
|
||||
roomId: RoomId,
|
||||
isDm: Boolean,
|
||||
eventSink: (LeaveRoomEvent) -> Unit,
|
||||
) {
|
||||
ConfirmationDialog(
|
||||
title = stringResource(if (isDm) CommonStrings.action_leave_conversation else CommonStrings.action_leave_room),
|
||||
content = stringResource(text),
|
||||
submitText = stringResource(CommonStrings.action_leave),
|
||||
onSubmitClick = { eventSink(LeaveRoomEvent.LeaveRoom(roomId)) },
|
||||
onDismiss = { eventSink(LeaveRoomEvent.HideConfirmation) },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LeaveRoomProgressDialog(
|
||||
state: LeaveRoomState,
|
||||
) {
|
||||
when (state.progress) {
|
||||
is LeaveRoomState.Progress.Hidden -> {}
|
||||
is LeaveRoomState.Progress.Shown -> ProgressDialog(
|
||||
text = stringResource(CommonStrings.common_leaving_room),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LeaveRoomErrorDialog(
|
||||
state: LeaveRoomState,
|
||||
) {
|
||||
when (state.error) {
|
||||
is LeaveRoomState.Error.Hidden -> {}
|
||||
is LeaveRoomState.Error.Shown -> ErrorDialog(
|
||||
content = stringResource(CommonStrings.error_unknown),
|
||||
onSubmit = { state.eventSink(LeaveRoomEvent.HideError) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun LeaveRoomViewPreview(
|
||||
@PreviewParameter(LeaveRoomStateProvider::class) state: LeaveRoomState
|
||||
) = ElementPreview {
|
||||
Box(
|
||||
modifier = Modifier.size(300.dp, 300.dp),
|
||||
propagateMinConstraints = true,
|
||||
) {
|
||||
LeaveRoomView(state = state)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,5 +3,8 @@
|
|||
<string name="leave_conversation_alert_subtitle">"Er du sikker på, at du vil forlade denne samtale? Denne samtale er ikke offentlig, og du kan ikke deltage igen uden en invitation."</string>
|
||||
<string name="leave_room_alert_empty_subtitle">"Er du sikker på, at du vil forlade dette rum? Du er den eneste person her. Hvis du går, vil ingen kunne tilslutte sig det i fremtiden, heller ikke dig."</string>
|
||||
<string name="leave_room_alert_private_subtitle">"Er du sikker på, at du vil forlade dette rum? Rummet er ikke offentligt, så du vil ikke kunne deltage igen uden en invitation."</string>
|
||||
<string name="leave_room_alert_select_new_owner_action">"Vælg ejere"</string>
|
||||
<string name="leave_room_alert_select_new_owner_subtitle">"Du er den eneste ejer af dette rum. Du skal overføre ejerskabet til en anden, før du forlader rummet."</string>
|
||||
<string name="leave_room_alert_select_new_owner_title">"Overdrag ejerskab"</string>
|
||||
<string name="leave_room_alert_subtitle">"Er du sikker på, at du ønsker at forlade rummet?"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -3,5 +3,8 @@
|
|||
<string name="leave_conversation_alert_subtitle">"Kas sa oled kindel, et soovid sellest vestlusest lahkuda? See vestlus pole avalik ja uuesti liitumiseks vajad kutset."</string>
|
||||
<string name="leave_room_alert_empty_subtitle">"Kas sa oled kindel, et soovid sellest jututoast lahkuda? Sa oled siin viimane osaleja ja peale sinu lahkumist ei saa keegi enam liituda, isegi sina mitte."</string>
|
||||
<string name="leave_room_alert_private_subtitle">"Kas sa oled kindel, et soovid sellest jututoast lahkuda? See jututuba pole avalik ja uuesti liitumiseks vajad kutset."</string>
|
||||
<string name="leave_room_alert_select_new_owner_action">"Vali omanikud"</string>
|
||||
<string name="leave_room_alert_select_new_owner_subtitle">"Sa oled selle jututoa ainus omanik. Enne jututoast lahkumist pead omandi üle andma kellelegi teisele."</string>
|
||||
<string name="leave_room_alert_select_new_owner_title">"Anna omand üle"</string>
|
||||
<string name="leave_room_alert_subtitle">"Kas sa oled kindel, et soovid sellest jututoast lahkuda?"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="leave_room_alert_empty_subtitle">"آیا مطمئن هستید که می خواهید این اتاق را ترک کنید؟ شما تنها کسی هستید که اینجا هستید. اگر ترک کنید، هیچ کس نمی تواند در آینده به آن بپیوندد.از جمله خود شما."</string>
|
||||
<string name="leave_room_alert_private_subtitle">"آیا مطمئن هستید که می خواهید از این اتاق خارج شوید؟ این اتاق عمومی نیست و نمیتوانید بدون دعوت دوباره بپیوندید."</string>
|
||||
<string name="leave_room_alert_subtitle">"آیا مطمئن هستید که می خواهید اتاق را ترک کنید؟"</string>
|
||||
<string name="leave_room_alert_empty_subtitle">"مطمئنید که میخواهید این اتاق را ترک کنید؟ تنها فرد اینجا هستید. در صورت ترک، هیچکسی از جمله خودتان در آینده نخواهد توانست به آن بپیوندد."</string>
|
||||
<string name="leave_room_alert_private_subtitle">"مطمئنید که میخواهید این اتاق را ترک کنید؟ این اتاق عمومی نبوده قادر نخواهید بود بدون دعوت دوباره بپیوندید."</string>
|
||||
<string name="leave_room_alert_subtitle">"مطمئنید که میخواهید این اتاق را ترک کنید؟"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -3,5 +3,8 @@
|
|||
<string name="leave_conversation_alert_subtitle">"Êtes-vous sûr de vouloir quitter cette discussion ? Vous ne pourrez pas la rejoindre à nouveau sans y être invité."</string>
|
||||
<string name="leave_room_alert_empty_subtitle">"Êtes-vous sûr de vouloir quitter ce salon ? Vous êtes la seule personne ici. Si vous partez, personne ne pourra rejoindre le salon à l’avenir, y compris vous."</string>
|
||||
<string name="leave_room_alert_private_subtitle">"Êtes-vous sûr de vouloir quitter ce salon ? Ce salon n’est pas public et vous ne pourrez pas le rejoindre sans invitation."</string>
|
||||
<string name="leave_room_alert_select_new_owner_action">"Choisissez les propriétaires"</string>
|
||||
<string name="leave_room_alert_select_new_owner_subtitle">"Vous êtes le seul propriétaire de ce salon. Vous devez en transférer la propriété à quelqu’un d’autre avant de le quitter."</string>
|
||||
<string name="leave_room_alert_select_new_owner_title">"Transférer la propriété"</string>
|
||||
<string name="leave_room_alert_subtitle">"Êtes-vous sûr de vouloir quitter le salon ?"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -3,5 +3,8 @@
|
|||
<string name="leave_conversation_alert_subtitle">"Czy na pewno chcesz opuścić tę konwersację? Konwersacja nie jest publiczna i nie będziesz mógł dołączyć ponownie bez zaproszenia."</string>
|
||||
<string name="leave_room_alert_empty_subtitle">"Jesteś pewien, że chcesz opuścić ten pokój? Jesteś tu jedyną osobą. Jeśli wyjdziesz, nikt nie będzie mógł dołączyć, w tym Ty."</string>
|
||||
<string name="leave_room_alert_private_subtitle">"Czy na pewno chcesz opuścić ten pokój? Ten pokój nie jest publiczny i nie będziesz mógł do niego wrócić bez zaproszenia."</string>
|
||||
<string name="leave_room_alert_select_new_owner_action">"Wybierz właścicieli"</string>
|
||||
<string name="leave_room_alert_select_new_owner_subtitle">"Jesteś jedynym właścicielem tego pokoju. Musisz przenieść własność na kogoś innego, zanim go opuścisz."</string>
|
||||
<string name="leave_room_alert_select_new_owner_title">"Przenieś własność"</string>
|
||||
<string name="leave_room_alert_subtitle">"Jesteś pewien, że chcesz wyjść z tego pokoju?"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -3,5 +3,8 @@
|
|||
<string name="leave_conversation_alert_subtitle">"Ste si istí, že chcete opustiť konverzáciu?"</string>
|
||||
<string name="leave_room_alert_empty_subtitle">"Ste si istí, že chcete opustiť túto miestnosť? Ste tu jediná osoba. Ak odídete, nikto sa do nej nebude môcť v budúcnosti pripojiť, vrátane vás."</string>
|
||||
<string name="leave_room_alert_private_subtitle">"Ste si istí, že chcete opustiť túto miestnosť? Táto miestnosť nie je verejná a bez pozvania sa do nej nebudete môcť vrátiť."</string>
|
||||
<string name="leave_room_alert_select_new_owner_action">"Vybrať vlastníkov"</string>
|
||||
<string name="leave_room_alert_select_new_owner_subtitle">"Ste jediným vlastníkom tejto miestnosti. Pred opustením miestnosti musíte previesť vlastníctvo na niekoho iného."</string>
|
||||
<string name="leave_room_alert_select_new_owner_title">"Preniesť vlastníctvo"</string>
|
||||
<string name="leave_room_alert_subtitle">"Ste si istí, že chcete opustiť miestnosť?"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -3,5 +3,8 @@
|
|||
<string name="leave_conversation_alert_subtitle">"Are you sure that you want to leave this conversation? This conversation is not public and you won\'t be able to rejoin without an invite."</string>
|
||||
<string name="leave_room_alert_empty_subtitle">"Are you sure that you want to leave this room? You\'re the only person here. If you leave, no one will be able to join in the future, including you."</string>
|
||||
<string name="leave_room_alert_private_subtitle">"Are you sure that you want to leave this room? This room is not public and you won\'t be able to rejoin without an invite."</string>
|
||||
<string name="leave_room_alert_select_new_owner_action">"Choose owners"</string>
|
||||
<string name="leave_room_alert_select_new_owner_subtitle">"You\'re the only owner of this room. You need to transfer ownership to someone else before you leave the room."</string>
|
||||
<string name="leave_room_alert_select_new_owner_title">"Transfer ownership"</string>
|
||||
<string name="leave_room_alert_subtitle">"Are you sure that you want to leave the room?"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -18,10 +18,12 @@ android {
|
|||
setupAnvil()
|
||||
|
||||
dependencies {
|
||||
api(projects.features.leaveroom.api)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
api(projects.features.leaveroom.api)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright 2023, 2024 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.leaveroom.impl
|
||||
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
|
||||
sealed interface InternalLeaveRoomEvent : LeaveRoomEvent {
|
||||
data object ResetState : InternalLeaveRoomEvent
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright 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.leaveroom.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomRenderer
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class InternalLeaveRoomRenderer @Inject constructor() : LeaveRoomRenderer {
|
||||
@Composable
|
||||
override fun Render(state: LeaveRoomState, onSelectNewOwners: (RoomId) -> Unit, modifier: Modifier) {
|
||||
if (state is InternalLeaveRoomState) {
|
||||
LeaveRoomView(state, onSelectNewOwners)
|
||||
} else {
|
||||
error("Unsupported state type ${state.javaClass}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright 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.leaveroom.impl
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
data class InternalLeaveRoomState(
|
||||
val leaveAction: AsyncAction<Unit>,
|
||||
override val eventSink: (LeaveRoomEvent) -> Unit
|
||||
) : LeaveRoomState
|
||||
|
||||
@Immutable
|
||||
sealed interface Confirmation : AsyncAction.Confirming {
|
||||
data class Dm(val roomId: RoomId) : Confirmation
|
||||
data class Generic(val roomId: RoomId) : Confirmation
|
||||
data class PrivateRoom(val roomId: RoomId) : Confirmation
|
||||
data class LastUserInRoom(val roomId: RoomId) : Confirmation
|
||||
data class LastOwnerInRoom(val roomId: RoomId) : Confirmation
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright 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.leaveroom.impl
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
class InternalLeaveRoomStateProvider : PreviewParameterProvider<InternalLeaveRoomState> {
|
||||
override val values: Sequence<InternalLeaveRoomState>
|
||||
get() = sequenceOf(
|
||||
aLeaveRoomState(),
|
||||
aLeaveRoomState(
|
||||
leaveAction = Confirmation.Generic(roomId = A_ROOM_ID),
|
||||
),
|
||||
aLeaveRoomState(
|
||||
leaveAction = Confirmation.PrivateRoom(roomId = A_ROOM_ID),
|
||||
),
|
||||
aLeaveRoomState(
|
||||
leaveAction = Confirmation.LastUserInRoom(roomId = A_ROOM_ID),
|
||||
),
|
||||
aLeaveRoomState(
|
||||
leaveAction = Confirmation.Dm(roomId = A_ROOM_ID),
|
||||
),
|
||||
aLeaveRoomState(
|
||||
leaveAction = Confirmation.LastOwnerInRoom(roomId = A_ROOM_ID),
|
||||
),
|
||||
aLeaveRoomState(
|
||||
leaveAction = AsyncAction.Loading,
|
||||
),
|
||||
aLeaveRoomState(
|
||||
leaveAction = AsyncAction.Failure(RuntimeException("Something went wrong")),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private val A_ROOM_ID = RoomId("!aRoomId:aDomain")
|
||||
|
||||
fun aLeaveRoomState(
|
||||
leaveAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
eventSink: (LeaveRoomEvent) -> Unit = {},
|
||||
) = InternalLeaveRoomState(
|
||||
leaveAction = leaveAction,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
|
@ -14,15 +14,17 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.Dm
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.Generic
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.LastUserInRoom
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.PrivateRoom
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.BaseRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.isDm
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.usersWithRole
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
|
@ -35,71 +37,65 @@ class LeaveRoomPresenter @Inject constructor(
|
|||
@Composable
|
||||
override fun present(): LeaveRoomState {
|
||||
val scope = rememberCoroutineScope()
|
||||
val confirmation = remember { mutableStateOf<LeaveRoomState.Confirmation>(LeaveRoomState.Confirmation.Hidden) }
|
||||
val progress = remember { mutableStateOf<LeaveRoomState.Progress>(LeaveRoomState.Progress.Hidden) }
|
||||
val error = remember { mutableStateOf<LeaveRoomState.Error>(LeaveRoomState.Error.Hidden) }
|
||||
|
||||
return LeaveRoomState(
|
||||
confirmation = confirmation.value,
|
||||
progress = progress.value,
|
||||
error = error.value,
|
||||
val leaveAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||
return InternalLeaveRoomState(
|
||||
leaveAction = leaveAction.value,
|
||||
) { event ->
|
||||
when (event) {
|
||||
is LeaveRoomEvent.ShowConfirmation -> scope.launch(dispatchers.io) {
|
||||
showLeaveRoomAlert(
|
||||
matrixClient = client,
|
||||
roomId = event.roomId,
|
||||
confirmation = confirmation,
|
||||
)
|
||||
}
|
||||
|
||||
is LeaveRoomEvent.HideConfirmation -> confirmation.value = LeaveRoomState.Confirmation.Hidden
|
||||
is LeaveRoomEvent.LeaveRoom -> scope.launch(dispatchers.io) {
|
||||
client.leaveRoom(
|
||||
roomId = event.roomId,
|
||||
confirmation = confirmation,
|
||||
progress = progress,
|
||||
error = error,
|
||||
)
|
||||
}
|
||||
|
||||
is LeaveRoomEvent.HideError -> error.value = LeaveRoomState.Error.Hidden
|
||||
is LeaveRoomEvent.LeaveRoom ->
|
||||
if (event.needsConfirmation) {
|
||||
scope.showLeaveRoomAlert(roomId = event.roomId, leaveAction = leaveAction)
|
||||
} else {
|
||||
scope.leaveRoom(roomId = event.roomId, leaveAction = leaveAction)
|
||||
}
|
||||
InternalLeaveRoomEvent.ResetState -> leaveAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun showLeaveRoomAlert(
|
||||
matrixClient: MatrixClient,
|
||||
roomId: RoomId,
|
||||
confirmation: MutableState<LeaveRoomState.Confirmation>,
|
||||
) {
|
||||
matrixClient.getRoom(roomId)?.use { room ->
|
||||
val roomInfo = room.roomInfoFlow.first()
|
||||
confirmation.value = when {
|
||||
roomInfo.isDm -> Dm(roomId)
|
||||
// If unknown, assume the room is private
|
||||
roomInfo.isPublic == null || roomInfo.isPublic == false -> PrivateRoom(roomId)
|
||||
roomInfo.joinedMembersCount == 1L -> LastUserInRoom(roomId)
|
||||
else -> Generic(roomId)
|
||||
private fun CoroutineScope.showLeaveRoomAlert(
|
||||
roomId: RoomId,
|
||||
leaveAction: MutableState<AsyncAction<Unit>>,
|
||||
) = launch(dispatchers.io) {
|
||||
client.getRoom(roomId)?.use { room ->
|
||||
val roomInfo = room.roomInfoFlow.first()
|
||||
leaveAction.value = when {
|
||||
roomInfo.isDm -> Confirmation.Dm(roomId)
|
||||
room.isLastOwner() && roomInfo.joinedMembersCount > 1L -> Confirmation.LastOwnerInRoom(roomId)
|
||||
// If unknown, assume the room is private
|
||||
roomInfo.isPublic == null || roomInfo.isPublic == false -> Confirmation.PrivateRoom(roomId)
|
||||
roomInfo.joinedMembersCount == 1L -> Confirmation.LastUserInRoom(roomId)
|
||||
else -> Confirmation.Generic(roomId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.leaveRoom(
|
||||
roomId: RoomId,
|
||||
leaveAction: MutableState<AsyncAction<Unit>>,
|
||||
) = launch(dispatchers.io) {
|
||||
leaveAction.runCatchingUpdatingState {
|
||||
client.getRoom(roomId)!!.use { room ->
|
||||
room
|
||||
.leave()
|
||||
.onFailure { Timber.e(it, "Error while leaving room ${room.roomId}") }
|
||||
.getOrThrow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun BaseRoom.isLastOwner(): Boolean {
|
||||
if (roomInfoFlow.value.isDm) {
|
||||
// DMs are not owned by the user, so we can return false
|
||||
return false
|
||||
} else {
|
||||
val hasPrivilegedCreatorRole = roomInfoFlow.value.privilegedCreatorRole
|
||||
if (!hasPrivilegedCreatorRole) return false
|
||||
|
||||
val creators = usersWithRole(RoomMember.Role.Owner(isCreator = true)).first()
|
||||
val superAdmins = usersWithRole(RoomMember.Role.Owner(isCreator = false)).first()
|
||||
val owners = creators + superAdmins
|
||||
return owners.size == 1 && owners.first().userId == sessionId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun MatrixClient.leaveRoom(
|
||||
roomId: RoomId,
|
||||
confirmation: MutableState<LeaveRoomState.Confirmation>,
|
||||
progress: MutableState<LeaveRoomState.Progress>,
|
||||
error: MutableState<LeaveRoomState.Error>,
|
||||
) {
|
||||
confirmation.value = LeaveRoomState.Confirmation.Hidden
|
||||
progress.value = LeaveRoomState.Progress.Shown
|
||||
getRoom(roomId)?.use { room ->
|
||||
room.leave()
|
||||
.onFailure {
|
||||
Timber.e(it, "Error while leaving room ${room.roomId}")
|
||||
error.value = LeaveRoomState.Error.Shown
|
||||
}
|
||||
}
|
||||
progress.value = LeaveRoomState.Progress.Hidden
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* Copyright 2023, 2024 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.leaveroom.impl
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.features.leaveroom.api.R
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Suppress("LambdaParameterEventTrailing")
|
||||
@Composable
|
||||
fun LeaveRoomView(
|
||||
state: InternalLeaveRoomState,
|
||||
onSelectNewOwners: (RoomId) -> Unit,
|
||||
) {
|
||||
AsyncActionView(
|
||||
state.leaveAction,
|
||||
onSuccess = {
|
||||
state.eventSink(InternalLeaveRoomEvent.ResetState)
|
||||
},
|
||||
onErrorDismiss = {
|
||||
state.eventSink(InternalLeaveRoomEvent.ResetState)
|
||||
},
|
||||
confirmationDialog = { confirmation ->
|
||||
if (confirmation is Confirmation) {
|
||||
LeaveRoomConfirmationDialog(
|
||||
confirmation = confirmation,
|
||||
eventSink = state.eventSink,
|
||||
onSelectNewOwners = onSelectNewOwners,
|
||||
)
|
||||
}
|
||||
},
|
||||
errorTitle = { stringResource(CommonStrings.common_something_went_wrong) },
|
||||
errorMessage = { stringResource(CommonStrings.error_network_or_server_issue) },
|
||||
progressDialog = { LeaveRoomProgressDialog() },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LeaveRoomConfirmationDialog(
|
||||
confirmation: Confirmation,
|
||||
eventSink: (LeaveRoomEvent) -> Unit,
|
||||
onSelectNewOwners: (RoomId) -> Unit,
|
||||
) {
|
||||
val defaultOnSubmitClick = { roomId: RoomId -> { eventSink(LeaveRoomEvent.LeaveRoom(roomId, needsConfirmation = false)) } }
|
||||
val defaultDismissAction = { eventSink(InternalLeaveRoomEvent.ResetState) }
|
||||
when (confirmation) {
|
||||
is Confirmation.Dm -> LeaveRoomConfirmationDialog(
|
||||
text = stringResource(R.string.leave_room_alert_private_subtitle),
|
||||
isDm = false,
|
||||
onSubmitClick = defaultOnSubmitClick(confirmation.roomId),
|
||||
onDismiss = defaultDismissAction,
|
||||
)
|
||||
|
||||
is Confirmation.PrivateRoom -> LeaveRoomConfirmationDialog(
|
||||
text = stringResource(R.string.leave_room_alert_private_subtitle),
|
||||
isDm = false,
|
||||
onSubmitClick = defaultOnSubmitClick(confirmation.roomId),
|
||||
onDismiss = defaultDismissAction,
|
||||
)
|
||||
|
||||
is Confirmation.LastUserInRoom -> LeaveRoomConfirmationDialog(
|
||||
text = stringResource(R.string.leave_room_alert_empty_subtitle),
|
||||
isDm = false,
|
||||
onSubmitClick = defaultOnSubmitClick(confirmation.roomId),
|
||||
onDismiss = defaultDismissAction,
|
||||
)
|
||||
|
||||
is Confirmation.LastOwnerInRoom -> LeaveRoomConfirmationDialog(
|
||||
title = stringResource(R.string.leave_room_alert_select_new_owner_title),
|
||||
text = stringResource(R.string.leave_room_alert_select_new_owner_subtitle),
|
||||
isDm = false,
|
||||
submitText = stringResource(R.string.leave_room_alert_select_new_owner_action),
|
||||
destructiveSubmit = true,
|
||||
onSubmitClick = {
|
||||
onSelectNewOwners(confirmation.roomId)
|
||||
eventSink(InternalLeaveRoomEvent.ResetState)
|
||||
},
|
||||
onDismiss = defaultDismissAction,
|
||||
)
|
||||
|
||||
is Confirmation.Generic -> LeaveRoomConfirmationDialog(
|
||||
text = stringResource(R.string.leave_room_alert_subtitle),
|
||||
isDm = false,
|
||||
onSubmitClick = defaultOnSubmitClick(confirmation.roomId),
|
||||
onDismiss = defaultDismissAction,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LeaveRoomConfirmationDialog(
|
||||
isDm: Boolean,
|
||||
text: String,
|
||||
onSubmitClick: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
title: String = stringResource(if (isDm) CommonStrings.action_leave_conversation else CommonStrings.action_leave_room),
|
||||
submitText: String = stringResource(CommonStrings.action_leave),
|
||||
destructiveSubmit: Boolean = false,
|
||||
) {
|
||||
ConfirmationDialog(
|
||||
title = title,
|
||||
content = text,
|
||||
submitText = submitText,
|
||||
onSubmitClick = onSubmitClick,
|
||||
onDismiss = onDismiss,
|
||||
destructiveSubmit = destructiveSubmit,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LeaveRoomProgressDialog(modifier: Modifier = Modifier) {
|
||||
ProgressDialog(
|
||||
text = stringResource(CommonStrings.common_leaving_room),
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun LeaveRoomViewPreview(
|
||||
@PreviewParameter(InternalLeaveRoomStateProvider::class) state: InternalLeaveRoomState
|
||||
) = ElementPreview {
|
||||
Box(
|
||||
modifier = Modifier.size(300.dp, 300.dp),
|
||||
propagateMinConstraints = true,
|
||||
) {
|
||||
LeaveRoomView(state = state, onSelectNewOwners = {})
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ import app.cash.molecule.moleculeFlow
|
|||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
|
|
@ -23,6 +23,8 @@ import io.element.android.tests.testutils.lambda.assert
|
|||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
|
@ -36,15 +38,12 @@ class LeaveBaseRoomPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - initial state hides all dialogs`() = runTest {
|
||||
val presenter = createLeaveRoomPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.confirmation).isEqualTo(LeaveRoomState.Confirmation.Hidden)
|
||||
assertThat(initialState.progress).isEqualTo(LeaveRoomState.Progress.Hidden)
|
||||
assertThat(initialState.error).isEqualTo(LeaveRoomState.Error.Hidden)
|
||||
}
|
||||
createLeaveRoomPresenter()
|
||||
.stateFlow()
|
||||
.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.leaveAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -59,13 +58,11 @@ class LeaveBaseRoomPresenterTest {
|
|||
)
|
||||
}
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.stateFlow().test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID))
|
||||
initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID, needsConfirmation = true))
|
||||
val confirmationState = awaitItem()
|
||||
assertThat(confirmationState.confirmation).isEqualTo(LeaveRoomState.Confirmation.Generic(A_ROOM_ID))
|
||||
assertThat(confirmationState.leaveAction).isEqualTo(Confirmation.Generic(A_ROOM_ID))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -81,13 +78,11 @@ class LeaveBaseRoomPresenterTest {
|
|||
)
|
||||
}
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.stateFlow().test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID))
|
||||
initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID, needsConfirmation = true))
|
||||
val confirmationState = awaitItem()
|
||||
assertThat(confirmationState.confirmation).isEqualTo(LeaveRoomState.Confirmation.PrivateRoom(A_ROOM_ID))
|
||||
assertThat(confirmationState.leaveAction).isEqualTo(Confirmation.PrivateRoom(A_ROOM_ID))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -103,13 +98,11 @@ class LeaveBaseRoomPresenterTest {
|
|||
)
|
||||
}
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.stateFlow().test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID))
|
||||
initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID, needsConfirmation = true))
|
||||
val confirmationState = awaitItem()
|
||||
assertThat(confirmationState.confirmation).isEqualTo(LeaveRoomState.Confirmation.LastUserInRoom(A_ROOM_ID))
|
||||
assertThat(confirmationState.leaveAction).isEqualTo(Confirmation.LastUserInRoom(A_ROOM_ID))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -125,13 +118,11 @@ class LeaveBaseRoomPresenterTest {
|
|||
)
|
||||
}
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.stateFlow().test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID))
|
||||
initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID, needsConfirmation = true))
|
||||
val confirmationState = awaitItem()
|
||||
assertThat(confirmationState.confirmation).isEqualTo(LeaveRoomState.Confirmation.Dm(A_ROOM_ID))
|
||||
assertThat(confirmationState.leaveAction).isEqualTo(Confirmation.Dm(A_ROOM_ID))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -148,11 +139,9 @@ class LeaveBaseRoomPresenterTest {
|
|||
)
|
||||
},
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.stateFlow().test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID))
|
||||
initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID, needsConfirmation = false))
|
||||
advanceUntilIdle()
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
assert(leaveRoomLambda)
|
||||
|
|
@ -173,44 +162,19 @@ class LeaveBaseRoomPresenterTest {
|
|||
)
|
||||
}
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.stateFlow().test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID))
|
||||
skipItems(1) // Skip show progress state
|
||||
initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID, needsConfirmation = false))
|
||||
val progressState = awaitItem()
|
||||
assertThat(progressState.leaveAction).isEqualTo(AsyncAction.Loading)
|
||||
val errorState = awaitItem()
|
||||
assertThat(errorState.error).isEqualTo(LeaveRoomState.Error.Shown)
|
||||
assertThat(errorState.leaveAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - show progress indicator while leaving a room`() = runTest {
|
||||
val presenter = createLeaveRoomPresenter(
|
||||
client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(
|
||||
roomId = A_ROOM_ID,
|
||||
result = FakeBaseRoom(
|
||||
leaveRoomLambda = { Result.success(Unit) }
|
||||
),
|
||||
)
|
||||
}
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID))
|
||||
val progressState = awaitItem()
|
||||
assertThat(progressState.progress).isEqualTo(LeaveRoomState.Progress.Shown)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.progress).isEqualTo(LeaveRoomState.Progress.Hidden)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - hide error hides the error`() = runTest {
|
||||
fun `present - reset state after error`() = runTest {
|
||||
val presenter = createLeaveRoomPresenter(
|
||||
client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(
|
||||
|
|
@ -221,20 +185,23 @@ class LeaveBaseRoomPresenterTest {
|
|||
)
|
||||
}
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.stateFlow().test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID))
|
||||
initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID, needsConfirmation = false))
|
||||
skipItems(1) // Skip show progress state
|
||||
val errorState = awaitItem()
|
||||
assertThat(errorState.error).isEqualTo(LeaveRoomState.Error.Shown)
|
||||
skipItems(1) // Skip hide progress state
|
||||
errorState.eventSink(LeaveRoomEvent.HideError)
|
||||
assertThat(errorState.leaveAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||
errorState.eventSink(InternalLeaveRoomEvent.ResetState)
|
||||
val hiddenErrorState = awaitItem()
|
||||
assertThat(hiddenErrorState.error).isEqualTo(LeaveRoomState.Error.Hidden)
|
||||
assertThat(hiddenErrorState.leaveAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
private fun LeaveRoomPresenter.stateFlow(): Flow<InternalLeaveRoomState> {
|
||||
return moleculeFlow(RecompositionMode.Immediate) {
|
||||
present()
|
||||
}.filterIsInstance(InternalLeaveRoomState::class)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TestScope.createLeaveRoomPresenter(
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@
|
|||
<string name="screen_app_lock_settings_remove_pin_alert_title">"برداشتن پین؟"</string>
|
||||
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"اجازه به %1$s"</string>
|
||||
<string name="screen_app_lock_setup_biometric_unlock_skip">"ترجیح میدهم از پین استفاده کنم"</string>
|
||||
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"زمیانتان را ذخیره کرده و از %1$s برای قفلگشایی هربارهٔ کاره استفاده کنید"</string>
|
||||
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"زمانتان را ذخیره کرده و از %1$s برای قفلگشایی هربارهٔ کاره استفاده کنید"</string>
|
||||
<string name="screen_app_lock_setup_choose_pin">"گزینش پین"</string>
|
||||
<string name="screen_app_lock_setup_confirm_pin">"تأیید پین"</string>
|
||||
<string name="screen_app_lock_setup_pin_context">"قفل %1$s برای افزودن امنیت بیشتر به گفتگوهایتان.
|
||||
<string name="screen_app_lock_setup_pin_context">"قفل کردن %1$s برای افزودن امنیت بیشتر به گفتگوهایتان.
|
||||
|
||||
چیزی به یاد ماندنی انتخاب کنید. اگر این پین را فراموش کنید، از برنامه خارج خواهید شد."</string>
|
||||
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"به دلیل امنیتی نمیتوانید این پین را برگزینید"</string>
|
||||
|
|
@ -24,14 +24,6 @@
|
|||
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"پینها مطابق نیستند"</string>
|
||||
<string name="screen_app_lock_signout_alert_message">"برای ادامه باید دوباره وارد شده و پینی جدید ایجاد کنید"</string>
|
||||
<string name="screen_app_lock_signout_alert_title">"دارید خارج میشوید"</string>
|
||||
<plurals name="screen_app_lock_subtitle">
|
||||
<item quantity="one">"شما %1$d تلاش برای باز کردن قفل دارید"</item>
|
||||
<item quantity="other">"شما %1$d تلاش برای باز کردن قفل دارید"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_app_lock_subtitle_wrong_pin">
|
||||
<item quantity="one">"پین اشتباه است. شما %1$d شانس دیگر دارید"</item>
|
||||
<item quantity="other">"پین اشتباه است. شما %1$d شانس دیگر دارید"</item>
|
||||
</plurals>
|
||||
<string name="screen_app_lock_use_biometric_android">"استفاده از زیستسنجی"</string>
|
||||
<string name="screen_app_lock_use_pin_android">"استفاده از پین"</string>
|
||||
<string name="screen_signout_in_progress_dialog_content">"خارج شدن…"</string>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@
|
|||
<string name="screen_change_account_provider_other">"Anden"</string>
|
||||
<string name="screen_change_account_provider_subtitle">"Brug en anden kontoudbyder, f.eks. din egen private server eller en arbejdskonto."</string>
|
||||
<string name="screen_change_account_provider_title">"Skift kontoudbyder"</string>
|
||||
<string name="screen_change_server_error_element_pro_required_message">"Element Pro-appen er påkrævet på %1$s Download den venligst fra din app store."</string>
|
||||
<string name="screen_change_server_error_element_pro_required_title">"Element Pro kræves"</string>
|
||||
<string name="screen_change_server_error_invalid_homeserver">"Vi kunne ikke nå denne hjemmeserver. Kontroller, at du har indtastet hjemmeserverens URL korrekt. Hvis URL-adressen er korrekt, skal du kontakte administratoren på din hjemmeserver for at få yderligere hjælp."</string>
|
||||
<string name="screen_change_server_error_invalid_well_known">"Serveren er ikke tilgængelig på grund af et problem i .well-known-filen:
|
||||
%1$s"</string>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
<string name="screen_change_account_provider_other">"دیگر"</string>
|
||||
<string name="screen_change_account_provider_subtitle">"استفاده از فراهم کنندهٔ حسابی دیگر چون کارساز خصوصی خوتان یا حسابی کاری."</string>
|
||||
<string name="screen_change_account_provider_title">"تغییر فراهم کنندهٔ حساب"</string>
|
||||
<string name="screen_change_server_error_invalid_homeserver">"ما نتوانستیم به این سرور خانگی برسیم. لطفاً بررسی کنید که URL سرور اصلی را به درستی وارد کرده اید. اگر URL صحیح است، برای کمک بیشتر با مدیر سرور خانگی خود تماس بگیرید."</string>
|
||||
<string name="screen_change_server_error_invalid_homeserver">"ما نتوانستیم به این کارساز خانگی برسیم. لطفاً بررسی کنید که URL کارساز اصلی را به درستی وارد کرده اید. اگر URL صحیح است، برای کمک بیشتر با مدیر کارساز خانگی خود تماس بگیرید."</string>
|
||||
<string name="screen_change_server_form_header">"نشانی کارساز خانگی"</string>
|
||||
<string name="screen_change_server_form_notice">"ورود نشانی دامنه."</string>
|
||||
<string name="screen_change_server_subtitle">"نشانی کارسازتان چیست؟"</string>
|
||||
|
|
@ -22,12 +22,14 @@
|
|||
<string name="screen_login_error_deactivated_account">"این حساب از کار افتاده است."</string>
|
||||
<string name="screen_login_error_invalid_credentials">"نام کاربری یا گذرواژه نامعتبر است"</string>
|
||||
<string name="screen_login_error_invalid_user_id">"این یک شناسه کاربری معتبر نیست. قالب صحیح: «@user:homeserver.or"</string>
|
||||
<string name="screen_login_error_unsupported_authentication">"سرور اصلی انتخاب شده از رمز عبور یا ورود OIDC پشتیبانی نمی کند. لطفا با مدیر خود تماس بگیرید یا یک سرور خانگی دیگر را انتخاب کنید."</string>
|
||||
<string name="screen_login_error_unsupported_authentication">"کارساز اصلی انتخاب شده از رمز عبور یا ورود OIDC پشتیبانی نمی کند. لطفا با مدیر خود تماس بگیرید یا یک کارساز خانگی دیگر را انتخاب کنید."</string>
|
||||
<string name="screen_login_form_header">"جزییاتتان را وارد کنید"</string>
|
||||
<string name="screen_login_subtitle">"ماتریکس شبکهای بار برای ارتباطات نامتمرکز و امن است."</string>
|
||||
<string name="screen_login_title">"خوش برگشتید!"</string>
|
||||
<string name="screen_login_title_with_homeserver">"ورود به %1$s"</string>
|
||||
<string name="screen_onboarding_app_version">"نگارش %1$s"</string>
|
||||
<string name="screen_onboarding_sign_in_manually">"ورود دستی"</string>
|
||||
<string name="screen_onboarding_sign_in_to">"ورود به %1$s"</string>
|
||||
<string name="screen_onboarding_sign_in_with_qr_code">"ورود با کد QR"</string>
|
||||
<string name="screen_onboarding_sign_up">"ایجاد حساب"</string>
|
||||
<string name="screen_onboarding_welcome_message">"به سریعترین %1$s خوش آمدید. بازطرّاحی شده برای سرعت و سادگی."</string>
|
||||
|
|
@ -74,7 +76,7 @@
|
|||
<string name="screen_qr_code_login_verify_code_subtitle">"ممکن است فراهم کنندهٔ حسابتان کد زیر را برای تأیید ورود بخواهد."</string>
|
||||
<string name="screen_qr_code_login_verify_code_title">"کد تأییدتان"</string>
|
||||
<string name="screen_server_confirmation_change_server">"تغییر فراهم کنندهٔ حساب"</string>
|
||||
<string name="screen_server_confirmation_message_login_element_dot_io">"یک سرور خصوصی برای کارمندان Element."</string>
|
||||
<string name="screen_server_confirmation_message_login_element_dot_io">"کارساز خصوصی برای کارمندان المنت."</string>
|
||||
<string name="screen_server_confirmation_message_login_matrix_dot_org">"ماتریکس شبکهای بار برای ارتباطات نامتمرکز و امن است."</string>
|
||||
<string name="screen_server_confirmation_message_register">"جایی که گفتوگوهایتان خواهند زیست — درست مثل استفادهتان از فراهم کنندهٔ رایانامهای برای نگه داشتن رایانامههایتان."</string>
|
||||
<string name="screen_server_confirmation_title_login">"دارید به %1$s وارد میشوید"</string>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@
|
|||
<string name="screen_change_account_provider_other">"Muu"</string>
|
||||
<string name="screen_change_account_provider_subtitle">"Käytä toista palveluntarjoajaa, kuten omaa yksityistä palvelintasi tai työpaikkaasi."</string>
|
||||
<string name="screen_change_account_provider_title">"Vaihda palveluntarjoajaa"</string>
|
||||
<string name="screen_change_server_error_element_pro_required_message">"Element Pro -sovellus on pakollinen %1$s -palvelimella. Lataa se sovelluskaupasta."</string>
|
||||
<string name="screen_change_server_error_element_pro_required_title">"Element Pro vaaditaan"</string>
|
||||
<string name="screen_change_server_error_invalid_homeserver">"Kotipalvelimeen ei saatu yhteyttä. Varmista, että olet syöttänyt osoitteen oikein. Jos osoite on oikein, ota yhteyttä palvelimesi ylläpitäjään."</string>
|
||||
<string name="screen_change_server_error_invalid_well_known">"Palvelin ei ole saatavilla .well-known tiedostossa olevan ongelman vuoksi:
|
||||
%1$s"</string>
|
||||
|
|
@ -34,6 +36,7 @@
|
|||
<string name="screen_login_subtitle">"Matrix on avoin verkko turvallista, hajautettua viestintää varten."</string>
|
||||
<string name="screen_login_title">"Tervetuloa takaisin!"</string>
|
||||
<string name="screen_login_title_with_homeserver">"Kirjaudu sisään %1$s -palvelimelle"</string>
|
||||
<string name="screen_onboarding_app_version">"Versio %1$s"</string>
|
||||
<string name="screen_onboarding_sign_in_manually">"Kirjaudu sisään manuaalisesti"</string>
|
||||
<string name="screen_onboarding_sign_in_to">"Kirjaudu sisään %1$s -palvelimelle"</string>
|
||||
<string name="screen_onboarding_sign_in_with_qr_code">"Kirjaudu sisään QR-koodilla"</string>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
<string name="screen_change_account_provider_subtitle">"Gebruik een andere accountprovider, zoals je eigen privéserver of een zakelijke account."</string>
|
||||
<string name="screen_change_account_provider_title">"Wijzig accountprovider"</string>
|
||||
<string name="screen_change_server_error_invalid_homeserver">"We konden deze homeserver niet bereiken. Controleer of je de homeserver-URL juist hebt ingevoerd. Als de URL juist is, neem dan contact op met de beheerder van je homeserver voor verdere hulp."</string>
|
||||
<string name="screen_change_server_error_invalid_well_known">"Sliding sync is niet beschikbaar vanwege een probleem in het well-known bestand:
|
||||
<string name="screen_change_server_error_invalid_well_known">"Server is niet beschikbaar vanwege een probleem in het well-known bestand:
|
||||
%1$s"</string>
|
||||
<string name="screen_change_server_form_header">"Homeserver-URL"</string>
|
||||
<string name="screen_change_server_subtitle">"Wat is het adres van je server?"</string>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@
|
|||
<string name="screen_change_account_provider_other">"Інше"</string>
|
||||
<string name="screen_change_account_provider_subtitle">"Використати іншого провайдера облікових записів, наприклад, власний приватний сервер або робочий обліковий запис."</string>
|
||||
<string name="screen_change_account_provider_title">"Змінити провайдера облікового запису"</string>
|
||||
<string name="screen_change_server_error_element_pro_required_message">"Необхідно встановити застосунок Element Pro на %1$s. Завантажте його з магазину."</string>
|
||||
<string name="screen_change_server_error_element_pro_required_title">"Потрібен Element Pro"</string>
|
||||
<string name="screen_change_server_error_invalid_homeserver">"Не вдалося під\'єднатися до цього домашнього сервера. Перевірте правильність введеної URL-адреси домашнього сервера. Якщо URL-адреса правильна, зверніться по додаткову допомогу до адміністратора домашнього сервера."</string>
|
||||
<string name="screen_change_server_error_invalid_well_known">"Сервер недоступний через помилку у файлі well-known:
|
||||
%1$s"</string>
|
||||
|
|
@ -34,6 +36,7 @@
|
|||
<string name="screen_login_subtitle">"Matrix — це відкрита мережа для безпечної, децентралізованої комунікації."</string>
|
||||
<string name="screen_login_title">"З поверненням!"</string>
|
||||
<string name="screen_login_title_with_homeserver">"Увійти в %1$s"</string>
|
||||
<string name="screen_onboarding_app_version">"Версія %1$s"</string>
|
||||
<string name="screen_onboarding_sign_in_manually">"Увійти вручну"</string>
|
||||
<string name="screen_onboarding_sign_in_to">"Увійти в %1$s"</string>
|
||||
<string name="screen_onboarding_sign_in_with_qr_code">"Увійти за допомогою QR-коду"</string>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
<string name="emoji_picker_category_symbols">"نمادها"</string>
|
||||
<string name="screen_report_content_block_user">"انسداد کاربر"</string>
|
||||
<string name="screen_report_content_block_user_hint">"اگر میخواهید همه پیامهای فعلی و آینده را از این کاربر را پنهان کنید، علامت بزنید"</string>
|
||||
<string name="screen_report_content_explanation">"این پیام به مدیر سرور خانگی شما گزارش خواهد شد. آنها قادر به خواندن پیام های رمزگذاری شده نخواهند بود."</string>
|
||||
<string name="screen_report_content_explanation">"این پیام به مدیر کارساز خانگی شما گزارش خواهد شد. آنها قادر به خواندن پیام های رمزگذاری شده نخواهند بود."</string>
|
||||
<string name="screen_report_content_hint">"دلیل گزارش این محتوا"</string>
|
||||
<string name="screen_room_attachment_source_camera">"دوربین"</string>
|
||||
<string name="screen_room_attachment_source_camera_photo">"عکس گرفتن"</string>
|
||||
|
|
@ -36,9 +36,5 @@
|
|||
<string name="screen_room_timeline_reactions_show_less">"نمایش کمتر"</string>
|
||||
<string name="screen_room_timeline_reactions_show_more">"نمایش بیشتر"</string>
|
||||
<string name="screen_room_timeline_read_marker_title">"جدید"</string>
|
||||
<plurals name="screen_room_timeline_state_changes">
|
||||
<item quantity="one">"%1$dتغییر اتاق"</item>
|
||||
<item quantity="other">"%1$dتغییر اتاق"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_typing_two_members">"%1$s و %2$s"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -28,15 +28,25 @@
|
|||
<string name="screen_room_mentions_at_room_title">"Kaikki"</string>
|
||||
<string name="screen_room_retry_send_menu_send_again_action">"Lähetä uudelleen"</string>
|
||||
<string name="screen_room_retry_send_menu_title">"Viestisi lähettäminen epäonnistui"</string>
|
||||
<string name="screen_room_timeline_add_reaction">"Lisää emoji"</string>
|
||||
<string name="screen_room_timeline_add_reaction">"Lisää reaktio"</string>
|
||||
<string name="screen_room_timeline_beginning_of_room">"Tämä on huoneen %1$s alku."</string>
|
||||
<string name="screen_room_timeline_beginning_of_room_no_name">"Tämä on tämän keskustelun alku."</string>
|
||||
<string name="screen_room_timeline_legacy_call">"Puhelu, jota ei tueta. Kysy, voiko soittaja käyttää uutta Element X -sovellusta."</string>
|
||||
<string name="screen_room_timeline_less_reactions">"Näytä vähemmän"</string>
|
||||
<string name="screen_room_timeline_message_copied">"Viesti kopioitu"</string>
|
||||
<string name="screen_room_timeline_no_permission_to_post">"Sinulla ei ole oikeutta kirjoittaa tässä huoneessa"</string>
|
||||
<plurals name="screen_room_timeline_reaction_a11y">
|
||||
<item quantity="one">"%1$d jäsen reagoi %2$s"</item>
|
||||
<item quantity="other">"%1$d jäsentä reagoivat %2$s"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_room_timeline_reaction_including_you_a11y">
|
||||
<item quantity="one">"Sinä ja %1$d jäsen reagoitte %2$s"</item>
|
||||
<item quantity="other">"Sinä ja %1$d jäsentä reagoitte %2$s"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_timeline_reaction_you_a11y">"Reagoit seuraavasti: %1$s"</string>
|
||||
<string name="screen_room_timeline_reactions_show_less">"Näytä vähemmän"</string>
|
||||
<string name="screen_room_timeline_reactions_show_more">"Näytä lisää"</string>
|
||||
<string name="screen_room_timeline_reactions_show_reactions_summary">"Näytä reaktioiden yhteenveto"</string>
|
||||
<string name="screen_room_timeline_read_marker_title">"Uusi"</string>
|
||||
<plurals name="screen_room_timeline_state_changes">
|
||||
<item quantity="one">"%1$d muutos huoneeseen"</item>
|
||||
|
|
|
|||
|
|
@ -4,5 +4,6 @@
|
|||
<item quantity="one">"%1$d% af de samlede stemmer"</item>
|
||||
<item quantity="other">"%1$d procent af det samlede antal stemmer"</item>
|
||||
</plurals>
|
||||
<string name="a11y_polls_will_remove_selection">"Fjerner tidligere valg"</string>
|
||||
<string name="a11y_polls_winning_answer">"Dette er det vindende svar"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="a11y_polls_will_remove_selection">"گزینش پیشین را برخواهد داشت"</string>
|
||||
<string name="a11y_polls_winning_answer">"این پاسخ برنده است"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<plurals name="a11y_polls_percent_of_total">
|
||||
<item quantity="one">"%1$d prosentti kaikista äänistä"</item>
|
||||
<item quantity="other">"%1$d prosenttia kaikista äänistä"</item>
|
||||
</plurals>
|
||||
<string name="a11y_polls_will_remove_selection">"Poistaa edellisen valinnan"</string>
|
||||
<string name="a11y_polls_winning_answer">"Tämä on voittava vastaus"</string>
|
||||
</resources>
|
||||
|
|
@ -5,5 +5,6 @@
|
|||
<item quantity="few">"%1$d відсотки від усіх голосів"</item>
|
||||
<item quantity="many">"%1$d відсотків від усіх голосів"</item>
|
||||
</plurals>
|
||||
<string name="a11y_polls_will_remove_selection">"Попередній вибір буде прибрано"</string>
|
||||
<string name="a11y_polls_winning_answer">"Ця відповідь перемогла"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
<string name="screen_create_poll_anonymous_headline">"Piilota äänet"</string>
|
||||
<string name="screen_create_poll_answer_hint">"Vaihtoehto %1$d"</string>
|
||||
<string name="screen_create_poll_cancel_confirmation_content_android">"Muutoksiasi ei ole tallennettu. Haluatko varmasti palata takaisin?"</string>
|
||||
<string name="screen_create_poll_delete_option_a11y">"Poista vaihtoehto %1$s"</string>
|
||||
<string name="screen_create_poll_question_desc">"Kysymys tai aihe"</string>
|
||||
<string name="screen_create_poll_question_hint">"Mistä kyselyssä on kyse?"</string>
|
||||
<string name="screen_create_poll_title">"Luo kysely"</string>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
<string name="screen_advanced_settings_element_call_base_url">"نشانی پایهٔ تماس المنتی سفارشی"</string>
|
||||
<string name="screen_advanced_settings_element_call_base_url_description">"تنظمی نشانی پایهای سفارشی برای تماس المنتی."</string>
|
||||
<string name="screen_advanced_settings_element_call_base_url_validation_error">"URL نامعتبر، لطفا مطمئن شوید که پروتکل (http/https) و آدرس صحیح را درج کرده اید."</string>
|
||||
<string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"نهفتن چهرکها در درخواستهای دعوت اتاق"</string>
|
||||
<string name="screen_advanced_settings_hide_timeline_media_toggle_title">"نهفتن رسانه در خط زمانی"</string>
|
||||
<string name="screen_advanced_settings_media_compression_title">"بهینه سازی کیفیت رسانه"</string>
|
||||
<string name="screen_advanced_settings_moderation_and_safety_section_title">"نظارت و امنیت"</string>
|
||||
|
|
@ -17,6 +18,7 @@
|
|||
<string name="screen_advanced_settings_show_media_timeline_always_hide">"نهفتن همیشگی"</string>
|
||||
<string name="screen_advanced_settings_show_media_timeline_always_show">"نمایش همیشگی"</string>
|
||||
<string name="screen_advanced_settings_show_media_timeline_private_rooms">"در اتاقهای خصوصی"</string>
|
||||
<string name="screen_advanced_settings_show_media_timeline_subtitle">"رسانههای نهفته همواره خواهند توانست با زدن رویشان نمایان شوند"</string>
|
||||
<string name="screen_advanced_settings_show_media_timeline_title">"نمایش رسانه در خط زمانی"</string>
|
||||
<string name="screen_blocked_users_empty">"هیچ کاربر مسدودی ندارید"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"رفع انسداد"</string>
|
||||
|
|
@ -56,6 +58,7 @@
|
|||
<string name="screen_notification_settings_system_notifications_action_required_content_link">"تنظیمات سامانه"</string>
|
||||
<string name="screen_notification_settings_system_notifications_turned_off">"آگاهیهای سامانهای خاموش شدند"</string>
|
||||
<string name="screen_notification_settings_title">"آگاهیها"</string>
|
||||
<string name="troubleshoot_notifications_entry_point_push_history_title">"تاریخچهٔ فرستادن"</string>
|
||||
<string name="troubleshoot_notifications_entry_point_section">"رفعاشکال"</string>
|
||||
<string name="troubleshoot_notifications_entry_point_title">"رفعاشکال آگاهیها"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@
|
|||
<string name="screen_bug_report_contact_me">"اگر پرسش دیگری دارید، میتوانید با من در تماس باشید."</string>
|
||||
<string name="screen_bug_report_contact_me_title">"تماس با من"</string>
|
||||
<string name="screen_bug_report_edit_screenshot">"ویرایش نماگرفت"</string>
|
||||
<string name="screen_bug_report_editor_description">"لطفا مشکل را توصیف کنید. چيکار کردي؟ انتظار داشتید چه اتفاقی بیفتد؟ واقعا چه اتفاقی افتاد لطفا تا جایی که می توانید جزئیات را وارد کنید."</string>
|
||||
<string name="screen_bug_report_editor_description">"لطفاً مشکل را شرح دهید. چهکار کردید؟ انتظار داشتید چه بشود؟ ولی چه شد؟ لطفاًتا جای ممکن وارد جزییات شوید."</string>
|
||||
<string name="screen_bug_report_editor_placeholder">"شرح مشکل…"</string>
|
||||
<string name="screen_bug_report_editor_supporting">"ترجیحاً توضیحات را به زبان انگلیسی بنویسید."</string>
|
||||
<string name="screen_bug_report_include_crash_logs">"ارسال رخدادنگارهای خطا"</string>
|
||||
<string name="screen_bug_report_include_logs">"اجازه به گزارشها"</string>
|
||||
<string name="screen_bug_report_include_screenshot">"ارسال تصویر صفحه"</string>
|
||||
<string name="screen_bug_report_logs_description">"گزارش ها در پیام شما گنجانده می شوند تا مطمئن شوید همه چیز به درستی کار می کند. برای ارسال پیام بدون گزارش، این تنظیم را خاموش کنید."</string>
|
||||
<string name="screen_bug_report_logs_description">"برای اطمینان از درست کار کردن همهچیز گزارشها در پیامتان قرار خواهد گرفت. برای فرستادن پیام بدون گزارشها این تنظیم را خاموش کنید."</string>
|
||||
<string name="screen_bug_report_rash_logs_alert_title">"%1$sآخرین باری که استفاده شد، از کار افتاد. آیا مایلید گزارش خرابی را با ما به اشتراک بگذارید؟"</string>
|
||||
<string name="screen_bug_report_view_logs">"دیدن گزارشها"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
<string name="screen_bug_report_error_description_too_short">"Kuvaus on liian lyhyt. Kerro tarkemmin mitä tapahtui, kiitos!"</string>
|
||||
<string name="screen_bug_report_include_crash_logs">"Lähetä kaatumislokit"</string>
|
||||
<string name="screen_bug_report_include_logs">"Lähetä lokitiedostot"</string>
|
||||
<string name="screen_bug_report_include_logs_error">"Lokitiedostosi ovat liian suuria, joten niitä ei voida sisällyttää tähän raporttiin. Lähetä ne meille toisella tavalla."</string>
|
||||
<string name="screen_bug_report_include_screenshot">"Lähetä kuvakaappaus"</string>
|
||||
<string name="screen_bug_report_logs_description">"Lähetä lokitiedostot viestisi kanssa, jotta voimme varmistaa, että kaikki toimii oikein. Jos haluat lähettää viestisi ilman lokeja, jätä tämä asetus valitsematta."</string>
|
||||
<string name="screen_bug_report_rash_logs_alert_title">"%1$s kaatui edellisellä käyttökerralla. Haluatko jakaa virheraportin kanssamme?"</string>
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ dependencies {
|
|||
implementation(projects.features.verifysession.api)
|
||||
implementation(projects.features.reportroom.api)
|
||||
implementation(projects.features.roommembermoderation.api)
|
||||
implementation(projects.features.changeroommemberroles.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
package io.element.android.features.roomdetails.impl
|
||||
|
||||
sealed interface RoomDetailsEvent {
|
||||
data object LeaveRoom : RoomDetailsEvent
|
||||
data class LeaveRoom(val needsConfirmation: Boolean) : RoomDetailsEvent
|
||||
data object MuteNotification : RoomDetailsEvent
|
||||
data object UnmuteNotification : RoomDetailsEvent
|
||||
data class CopyToClipboard(val text: String) : RoomDetailsEvent
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import android.os.Parcelable
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
|
|
@ -25,6 +27,8 @@ import io.element.android.anvilannotations.ContributesNode
|
|||
import io.element.android.appconfig.LearnMoreConfig
|
||||
import io.element.android.features.call.api.CallType
|
||||
import io.element.android.features.call.api.ElementCallEntryPoint
|
||||
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesEntryPoint
|
||||
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesListType
|
||||
import io.element.android.features.knockrequests.api.list.KnockRequestsListEntryPoint
|
||||
import io.element.android.features.messages.api.MessagesEntryPoint
|
||||
import io.element.android.features.poll.api.history.PollHistoryEntryPoint
|
||||
|
|
@ -51,12 +55,15 @@ 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.toRoomIdOrAlias
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||
import io.element.android.libraries.matrix.api.room.BaseRoom
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.api.verification.VerificationRequest
|
||||
import io.element.android.libraries.mediaviewer.api.MediaGalleryEntryPoint
|
||||
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
|
|
@ -65,7 +72,7 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
|||
@Assisted plugins: List<Plugin>,
|
||||
private val pollHistoryEntryPoint: PollHistoryEntryPoint,
|
||||
private val elementCallEntryPoint: ElementCallEntryPoint,
|
||||
private val room: BaseRoom,
|
||||
private val room: JoinedRoom,
|
||||
private val analyticsService: AnalyticsService,
|
||||
private val messagesEntryPoint: MessagesEntryPoint,
|
||||
private val knockRequestsListEntryPoint: KnockRequestsListEntryPoint,
|
||||
|
|
@ -73,6 +80,7 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
|||
private val mediaGalleryEntryPoint: MediaGalleryEntryPoint,
|
||||
private val outgoingVerificationEntryPoint: OutgoingVerificationEntryPoint,
|
||||
private val reportRoomEntryPoint: ReportRoomEntryPoint,
|
||||
private val changeRoomMemberRolesEntryPoint: ChangeRoomMemberRolesEntryPoint,
|
||||
) : BaseFlowNode<RoomDetailsFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = plugins.filterIsInstance<RoomDetailsEntryPoint.Params>().first().initialElement.toNavTarget(),
|
||||
|
|
@ -132,6 +140,24 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
|||
|
||||
@Parcelize
|
||||
data object ReportRoom : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object SelectNewOwnersWhenLeaving : NavTarget
|
||||
}
|
||||
|
||||
override fun onBuilt() {
|
||||
super.onBuilt()
|
||||
whenChildrenAttached { commonLifecycle: Lifecycle,
|
||||
roomDetailsNode: RoomDetailsNode,
|
||||
changeRoomMemberRolesNode: ChangeRoomMemberRolesEntryPoint.NodeProxy ->
|
||||
commonLifecycle.coroutineScope.launch {
|
||||
changeRoomMemberRolesNode.waitForRoleChanged()
|
||||
withContext(NonCancellable) {
|
||||
backstack.pop()
|
||||
roomDetailsNode.onNewOwnersSelected()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
|
|
@ -198,6 +224,10 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
|||
override fun openReportRoom() {
|
||||
backstack.push(NavTarget.ReportRoom)
|
||||
}
|
||||
|
||||
override fun onSelectNewOwnersWhenLeaving() {
|
||||
backstack.push(NavTarget.SelectNewOwnersWhenLeaving)
|
||||
}
|
||||
}
|
||||
createNode<RoomDetailsNode>(buildContext, listOf(roomDetailsCallback))
|
||||
}
|
||||
|
|
@ -330,7 +360,7 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
|||
is NavTarget.VerifyUser -> {
|
||||
val params = OutgoingVerificationEntryPoint.Params(
|
||||
showDeviceVerifiedScreen = true,
|
||||
verificationRequest = VerificationRequest.Outgoing.User(userId = navTarget.userId,)
|
||||
verificationRequest = VerificationRequest.Outgoing.User(userId = navTarget.userId)
|
||||
)
|
||||
outgoingVerificationEntryPoint.nodeBuilder(this, buildContext)
|
||||
.params(params)
|
||||
|
|
@ -352,6 +382,13 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
|||
is NavTarget.ReportRoom -> {
|
||||
reportRoomEntryPoint.createNode(this, buildContext, room.roomId)
|
||||
}
|
||||
|
||||
is NavTarget.SelectNewOwnersWhenLeaving -> {
|
||||
changeRoomMemberRolesEntryPoint.builder(this, buildContext)
|
||||
.room(room)
|
||||
.listType(ChangeRoomMemberRolesListType.SelectNewOwnersWhenLeaving)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ package io.element.android.features.roomdetails.impl
|
|||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
|
@ -21,7 +23,9 @@ import dagger.assisted.Assisted
|
|||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.MobileScreen
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomRenderer
|
||||
import io.element.android.libraries.androidutils.system.startSharePlainTextIntent
|
||||
import io.element.android.libraries.architecture.appyx.launchMolecule
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.BaseRoom
|
||||
|
|
@ -38,6 +42,7 @@ class RoomDetailsNode @AssistedInject constructor(
|
|||
private val presenter: RoomDetailsPresenter,
|
||||
private val room: BaseRoom,
|
||||
private val analyticsService: AnalyticsService,
|
||||
private val leaveRoomRenderer: LeaveRoomRenderer,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
interface Callback : Plugin {
|
||||
fun openRoomMemberList()
|
||||
|
|
@ -54,9 +59,10 @@ class RoomDetailsNode @AssistedInject constructor(
|
|||
fun openDmUserProfile(userId: UserId)
|
||||
fun onJoinCall()
|
||||
fun openReportRoom()
|
||||
fun onSelectNewOwnersWhenLeaving()
|
||||
}
|
||||
|
||||
private val callbacks = plugins<Callback>()
|
||||
private val callback = plugins<Callback>().first()
|
||||
|
||||
init {
|
||||
lifecycle.subscribe(
|
||||
|
|
@ -67,27 +73,27 @@ class RoomDetailsNode @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun openRoomMemberList() {
|
||||
callbacks.forEach { it.openRoomMemberList() }
|
||||
callback.openRoomMemberList()
|
||||
}
|
||||
|
||||
private fun openRoomNotificationSettings() {
|
||||
callbacks.forEach { it.openRoomNotificationSettings() }
|
||||
callback.openRoomNotificationSettings()
|
||||
}
|
||||
|
||||
private fun invitePeople() {
|
||||
callbacks.forEach { it.openInviteMembers() }
|
||||
callback.openInviteMembers()
|
||||
}
|
||||
|
||||
private fun openPollHistory() {
|
||||
callbacks.forEach { it.openPollHistory() }
|
||||
callback.openPollHistory()
|
||||
}
|
||||
|
||||
private fun openMediaGallery() {
|
||||
callbacks.forEach { it.openMediaGallery() }
|
||||
callback.openMediaGallery()
|
||||
}
|
||||
|
||||
private fun onJoinCall() {
|
||||
callbacks.forEach { it.onJoinCall() }
|
||||
callback.onJoinCall()
|
||||
}
|
||||
|
||||
private fun CoroutineScope.onShareRoom(context: Context) = launch {
|
||||
|
|
@ -106,41 +112,51 @@ class RoomDetailsNode @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun onEditRoomDetails() {
|
||||
callbacks.forEach { it.editRoomDetails() }
|
||||
callback.editRoomDetails()
|
||||
}
|
||||
|
||||
private fun openAvatarPreview(name: String, url: String) {
|
||||
callbacks.forEach { it.openAvatarPreview(name, url) }
|
||||
callback.openAvatarPreview(name, url)
|
||||
}
|
||||
|
||||
private fun openAdminSettings() {
|
||||
callbacks.forEach { it.openAdminSettings() }
|
||||
callback.openAdminSettings()
|
||||
}
|
||||
|
||||
private fun openPinnedMessages() {
|
||||
callbacks.forEach { it.openPinnedMessagesList() }
|
||||
callback.openPinnedMessagesList()
|
||||
}
|
||||
|
||||
private fun openKnockRequestsLists() {
|
||||
callbacks.forEach { it.openKnockRequestsList() }
|
||||
callback.openKnockRequestsList()
|
||||
}
|
||||
|
||||
private fun openSecurityAndPrivacy() {
|
||||
callbacks.forEach { it.openSecurityAndPrivacy() }
|
||||
callback.openSecurityAndPrivacy()
|
||||
}
|
||||
|
||||
private fun onProfileClick(userId: UserId) {
|
||||
callbacks.forEach { it.openDmUserProfile(userId) }
|
||||
callback.openDmUserProfile(userId)
|
||||
}
|
||||
|
||||
private fun onReportRoomClick() {
|
||||
callbacks.forEach { it.openReportRoom() }
|
||||
callback.openReportRoom()
|
||||
}
|
||||
|
||||
private fun onSelectNewOwnersWhenLeaving() {
|
||||
return callback.onSelectNewOwnersWhenLeaving()
|
||||
}
|
||||
|
||||
private val stateFlow = launchMolecule { presenter.present() }
|
||||
|
||||
fun onNewOwnersSelected() {
|
||||
stateFlow.value.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = false))
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val context = LocalContext.current
|
||||
val state = presenter.present()
|
||||
val state by stateFlow.collectAsState()
|
||||
|
||||
fun onShareRoom() {
|
||||
lifecycleScope.onShareRoom(context)
|
||||
|
|
@ -172,6 +188,13 @@ class RoomDetailsNode @AssistedInject constructor(
|
|||
onSecurityAndPrivacyClick = ::openSecurityAndPrivacy,
|
||||
onProfileClick = ::onProfileClick,
|
||||
onReportRoomClick = ::onReportRoomClick,
|
||||
leaveRoomView = {
|
||||
leaveRoomRenderer.Render(
|
||||
state = state.leaveRoomState,
|
||||
onSelectNewOwners = { onSelectNewOwnersWhenLeaving() },
|
||||
modifier = Modifier
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -149,8 +149,9 @@ class RoomDetailsPresenter @Inject constructor(
|
|||
|
||||
fun handleEvents(event: RoomDetailsEvent) {
|
||||
when (event) {
|
||||
RoomDetailsEvent.LeaveRoom ->
|
||||
leaveRoomState.eventSink(LeaveRoomEvent.ShowConfirmation(room.roomId))
|
||||
is RoomDetailsEvent.LeaveRoom -> {
|
||||
leaveRoomState.eventSink(LeaveRoomEvent.LeaveRoom(room.roomId, needsConfirmation = event.needsConfirmation))
|
||||
}
|
||||
RoomDetailsEvent.MuteNotification -> {
|
||||
scope.launch(dispatchers.io) {
|
||||
client.notificationSettingsService().muteRoom(room.roomId)
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@
|
|||
package io.element.android.features.roomdetails.impl
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.features.leaveroom.api.aLeaveRoomState
|
||||
import io.element.android.features.roomcall.api.RoomCallState
|
||||
import io.element.android.features.roomcall.api.aStandByCallState
|
||||
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
||||
|
|
@ -156,6 +156,12 @@ fun aRoomDetailsState(
|
|||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
internal fun aLeaveRoomState(
|
||||
eventSink: (LeaveRoomEvent) -> Unit = {}
|
||||
) = object : LeaveRoomState {
|
||||
override val eventSink: (LeaveRoomEvent) -> Unit = eventSink
|
||||
}
|
||||
|
||||
fun aRoomNotificationSettings(
|
||||
mode: RoomNotificationMode = RoomNotificationMode.MUTE,
|
||||
isDefault: Boolean = false,
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ import androidx.compose.ui.unit.dp
|
|||
import im.vector.app.features.analytics.plan.Interaction
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomView
|
||||
import io.element.android.features.roomcall.api.hasPermissionToJoin
|
||||
import io.element.android.features.userprofile.api.UserProfileVerificationState
|
||||
import io.element.android.features.userprofile.shared.blockuser.BlockUserDialogs
|
||||
|
|
@ -112,6 +111,7 @@ fun RoomDetailsView(
|
|||
onProfileClick: (UserId) -> Unit,
|
||||
onReportRoomClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
leaveRoomView: @Composable () -> Unit,
|
||||
) {
|
||||
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
|
||||
Scaffold(
|
||||
|
|
@ -131,7 +131,7 @@ fun RoomDetailsView(
|
|||
.verticalScroll(rememberScrollState())
|
||||
.consumeWindowInsets(padding)
|
||||
) {
|
||||
LeaveRoomView(state = state.leaveRoomState)
|
||||
leaveRoomView()
|
||||
|
||||
when (state.roomType) {
|
||||
RoomDetailsType.Room -> {
|
||||
|
|
@ -262,7 +262,7 @@ fun RoomDetailsView(
|
|||
OtherActionsSection(
|
||||
canReportRoom = state.canReportRoom,
|
||||
onReportRoomClick = onReportRoomClick,
|
||||
onLeaveRoomClick = { state.eventSink(RoomDetailsEvent.LeaveRoom) }
|
||||
onLeaveRoomClick = { state.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true)) }
|
||||
)
|
||||
|
||||
if (state.showDebugInfo) {
|
||||
|
|
@ -776,5 +776,6 @@ private fun ContentToPreview(state: RoomDetailsState) {
|
|||
onSecurityAndPrivacyClick = {},
|
||||
onProfileClick = {},
|
||||
onReportRoomClick = {},
|
||||
leaveRoomView = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,8 +38,10 @@ import kotlinx.collections.immutable.ImmutableMap
|
|||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentMap
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
|
@ -64,6 +66,11 @@ class RoomMemberListPresenter @Inject constructor(
|
|||
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
||||
val canInvite by room.canInviteAsState(syncUpdateFlow.value)
|
||||
val roomModerationState = roomMembersModerationPresenter.present()
|
||||
val activeRoomMemberCount by produceState(0L) {
|
||||
room.roomInfoFlow.map { it.activeMembersCount }
|
||||
.distinctUntilChanged()
|
||||
.collect { value = it }
|
||||
}
|
||||
|
||||
val roomMemberIdentityStates by produceState(persistentMapOf<UserId, IdentityState>()) {
|
||||
room.roomMemberIdentityStateChange(waitForEncryption = true)
|
||||
|
|
@ -73,8 +80,8 @@ class RoomMemberListPresenter @Inject constructor(
|
|||
.launchIn(this)
|
||||
}
|
||||
|
||||
// Ensure we load the latest data when entering this screen
|
||||
LaunchedEffect(Unit) {
|
||||
// Update the room members when the screen is loaded or the active member count changes
|
||||
LaunchedEffect(activeRoomMemberCount) {
|
||||
room.updateMembers()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,27 +10,34 @@ package io.element.android.features.roomdetails.impl.rolesandpermissions
|
|||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.pop
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles.ChangeRolesNode
|
||||
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesEntryPoint
|
||||
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesListType
|
||||
import io.element.android.features.roomdetails.impl.rolesandpermissions.permissions.ChangeRoomPermissionsNode
|
||||
import io.element.android.features.roomdetails.impl.rolesandpermissions.permissions.ChangeRoomPermissionsSection
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
class RolesAndPermissionsFlowNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val changeRoomMemberRolesEntryPoint: ChangeRoomMemberRolesEntryPoint,
|
||||
private val joinedRoom: JoinedRoom,
|
||||
) : BaseFlowNode<RolesAndPermissionsFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.AdminSettings,
|
||||
|
|
@ -53,6 +60,16 @@ class RolesAndPermissionsFlowNode @AssistedInject constructor(
|
|||
data class ChangeRoomPermissions(val section: ChangeRoomPermissionsSection) : NavTarget
|
||||
}
|
||||
|
||||
override fun onBuilt() {
|
||||
super.onBuilt()
|
||||
whenChildAttached { lifecycle, node: ChangeRoomMemberRolesEntryPoint.NodeProxy ->
|
||||
lifecycle.coroutineScope.launch {
|
||||
node.waitForRoleChanged()
|
||||
backstack.pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
is NavTarget.AdminSettings -> {
|
||||
|
|
@ -83,18 +100,16 @@ class RolesAndPermissionsFlowNode @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
is NavTarget.AdminList -> {
|
||||
val inputs = ChangeRolesNode.Inputs(ChangeRolesNode.ListType.Admins)
|
||||
createNode<ChangeRolesNode>(
|
||||
buildContext = buildContext,
|
||||
plugins = listOf(inputs),
|
||||
)
|
||||
changeRoomMemberRolesEntryPoint.builder(this, buildContext)
|
||||
.room(joinedRoom)
|
||||
.listType(ChangeRoomMemberRolesListType.Admins)
|
||||
.build()
|
||||
}
|
||||
is NavTarget.ModeratorList -> {
|
||||
val inputs = ChangeRolesNode.Inputs(ChangeRolesNode.ListType.Moderators)
|
||||
createNode<ChangeRolesNode>(
|
||||
buildContext = buildContext,
|
||||
plugins = listOf(inputs),
|
||||
)
|
||||
changeRoomMemberRolesEntryPoint.builder(this, buildContext)
|
||||
.room(joinedRoom)
|
||||
.listType(ChangeRoomMemberRolesListType.Moderators)
|
||||
.build()
|
||||
}
|
||||
is NavTarget.ChangeRoomPermissions -> {
|
||||
val inputs = ChangeRoomPermissionsNode.Inputs(navTarget.section)
|
||||
|
|
|
|||
|
|
@ -22,13 +22,17 @@
|
|||
<string name="screen_room_change_role_administrators_title">"Redigér admins"</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_description">"Du kan ikke fortryde denne handling. Du forfremmer brugeren til at have samme magtniveau som dig."</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_title">"Tilføj Admin?"</string>
|
||||
<string name="screen_room_change_role_confirm_change_owners_description">"Du kan ikke fortryde denne handling. Du overfører ejerskabet til de valgte brugere. Når du forlader siden, vil dette være permanent."</string>
|
||||
<string name="screen_room_change_role_confirm_change_owners_title">"Overdrag ejerskab?"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_action">"Nedgradering"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_description">"Du vil ikke være i stand til at fortryde denne ændring, da du degraderer dig selv. Hvis du er den sidste privilegerede bruger i rummet, vil det være umuligt at genvinde privilegier."</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_title">"Nedgrader dig selv?"</string>
|
||||
<string name="screen_room_change_role_invited_member_name">"%1$s (Afventer)"</string>
|
||||
<string name="screen_room_change_role_invited_member_name_android">"(Afventer)"</string>
|
||||
<string name="screen_room_change_role_moderators_admin_section_footer">"Administratorer har automatisk moderatorrettigheder"</string>
|
||||
<string name="screen_room_change_role_moderators_owner_section_footer">"Ejere har automatisk administratorrettigheder."</string>
|
||||
<string name="screen_room_change_role_moderators_title">"Redigér moderatorer"</string>
|
||||
<string name="screen_room_change_role_owners_title">"Vælg ejere"</string>
|
||||
<string name="screen_room_change_role_section_administrators">"Administratorer"</string>
|
||||
<string name="screen_room_change_role_section_moderators">"Moderatorer"</string>
|
||||
<string name="screen_room_change_role_section_users">"Medlemmer"</string>
|
||||
|
|
@ -81,6 +85,7 @@
|
|||
<string name="screen_room_member_list_pending_header_title">"Afventer"</string>
|
||||
<string name="screen_room_member_list_role_administrator">"Admin"</string>
|
||||
<string name="screen_room_member_list_role_moderator">"Moderator"</string>
|
||||
<string name="screen_room_member_list_role_owner">"Ejeren"</string>
|
||||
<string name="screen_room_member_list_room_members_header_title">"Medlemmer af rummet"</string>
|
||||
<string name="screen_room_member_list_unbanning_user">"Ophæver spærring af %1$s"</string>
|
||||
<string name="screen_room_notification_settings_allow_custom">"Tillad brugerdefineret indstilling"</string>
|
||||
|
|
@ -98,12 +103,14 @@
|
|||
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Kun omtaler og nøgleord"</string>
|
||||
<string name="screen_room_notification_settings_room_custom_settings_title">"Giv mig besked i dette rum for"</string>
|
||||
<string name="screen_room_roles_and_permissions_admins">"Administratorer"</string>
|
||||
<string name="screen_room_roles_and_permissions_admins_and_owners">"Administratorer og ejere"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_my_role">"Skift min rolle"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_role_demote_to_member">"Nedgrader til medlem"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_role_demote_to_moderator">"Nedgradering til moderator"</string>
|
||||
<string name="screen_room_roles_and_permissions_member_moderation">"Moderation af medlemmer"</string>
|
||||
<string name="screen_room_roles_and_permissions_messages_and_content">"Beskeder og indhold"</string>
|
||||
<string name="screen_room_roles_and_permissions_moderators">"Moderatorer"</string>
|
||||
<string name="screen_room_roles_and_permissions_owners">"Ejere"</string>
|
||||
<string name="screen_room_roles_and_permissions_permissions_header">"Tilladelser"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset">"Nulstil tilladelser"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset_confirm_description">"Når du nulstiller tilladelserne, mister du de nuværende indstillinger."</string>
|
||||
|
|
|
|||
|
|
@ -22,13 +22,17 @@
|
|||
<string name="screen_room_change_role_administrators_title">"Muuda peakasutajaid"</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_description">"Kuna sa annad teisele kasutajale sinu õigustega võrreldes samad õigused, siis sa ei saa seda muudatust hiljem tagasi pöörata."</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_title">"Lisame peakasutaja?"</string>
|
||||
<string name="screen_room_change_role_confirm_change_owners_description">"Seda tegevust ei saa tagasi pöörata. Järgnevaga annad jututoa omandi üle valitud kasutajatele. Kui lahkus, siis muutub see muudatus püsivaks."</string>
|
||||
<string name="screen_room_change_role_confirm_change_owners_title">"Kas soovid omandi üle anda?"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_action">"Vähenda õigusi"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_description">"Kui sa võtad endalt kõik õigused ära ja oled viimane peakasutaja selles jututoas, siis sa ei saa seda muudatust hiljem tagasi pöörata."</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_title">"Kas vähendad enda õigusi?"</string>
|
||||
<string name="screen_room_change_role_invited_member_name">"%1$s (ootel)"</string>
|
||||
<string name="screen_room_change_role_invited_member_name_android">"(ootel)"</string>
|
||||
<string name="screen_room_change_role_moderators_admin_section_footer">"Peakasutajatel on automaatselt ka moderaatori õigused"</string>
|
||||
<string name="screen_room_change_role_moderators_owner_section_footer">"Omanikel on automaatselt ka peakasutaja õigused."</string>
|
||||
<string name="screen_room_change_role_moderators_title">"Muuda moderaatoreid"</string>
|
||||
<string name="screen_room_change_role_owners_title">"Vali omanikud"</string>
|
||||
<string name="screen_room_change_role_section_administrators">"Peakasutajad"</string>
|
||||
<string name="screen_room_change_role_section_moderators">"Moderaatorid"</string>
|
||||
<string name="screen_room_change_role_section_users">"Liikmed"</string>
|
||||
|
|
@ -81,6 +85,7 @@
|
|||
<string name="screen_room_member_list_pending_header_title">"Ootel"</string>
|
||||
<string name="screen_room_member_list_role_administrator">"Peakasutaja"</string>
|
||||
<string name="screen_room_member_list_role_moderator">"Moderaator"</string>
|
||||
<string name="screen_room_member_list_role_owner">"Omanik"</string>
|
||||
<string name="screen_room_member_list_room_members_header_title">"Jututoas osalejad"</string>
|
||||
<string name="screen_room_member_list_unbanning_user">"Eemaldame suhtluskeelu kasutajalt %1$s"</string>
|
||||
<string name="screen_room_notification_settings_allow_custom">"Kasuta kohandatud seadistusi"</string>
|
||||
|
|
@ -98,12 +103,14 @@
|
|||
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Mainimiste ja võtmesõnade alusel"</string>
|
||||
<string name="screen_room_notification_settings_room_custom_settings_title">"Selles jututoas teavita mind"</string>
|
||||
<string name="screen_room_roles_and_permissions_admins">"Peakasutajad"</string>
|
||||
<string name="screen_room_roles_and_permissions_admins_and_owners">"Peakasutajad ja omanikud"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_my_role">"Muuda minu rolli"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_role_demote_to_member">"Muuda tavaliikmeks"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_role_demote_to_moderator">"Muuda moderaatoriks"</string>
|
||||
<string name="screen_room_roles_and_permissions_member_moderation">"Jututoas osalejate modereerimine"</string>
|
||||
<string name="screen_room_roles_and_permissions_messages_and_content">"Sõnumid ja sisu"</string>
|
||||
<string name="screen_room_roles_and_permissions_moderators">"Moderaatorid"</string>
|
||||
<string name="screen_room_roles_and_permissions_owners">"Omanikud"</string>
|
||||
<string name="screen_room_roles_and_permissions_permissions_header">"Õigused"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset">"Lähtesta õigused"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset_confirm_description">"Kui lähtestad õigused, siis praegune õiguste kombinatsioon läheb kaotsi."</string>
|
||||
|
|
|
|||
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