Merge branch 'release/26.01.1'
This commit is contained in:
commit
36756b917d
100 changed files with 1915 additions and 169 deletions
142
CHANGES.md
142
CHANGES.md
|
|
@ -1,3 +1,145 @@
|
||||||
|
Changes in Element X v26.01.0
|
||||||
|
=============================
|
||||||
|
|
||||||
|
<!-- Release notes generated using configuration in .github/release.yml at v26.01.0 -->
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
### ✨ Features
|
||||||
|
* Link new device using QrCode - First version by @bmarty in https://github.com/element-hq/element-x-android/pull/5909
|
||||||
|
* Voice message: variable play back speed by @bmarty in https://github.com/element-hq/element-x-android/pull/5963
|
||||||
|
* Change Room’s Access to/from Space members by @ganfra in https://github.com/element-hq/element-x-android/pull/5979
|
||||||
|
* Create spaces by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5982
|
||||||
|
### 🙌 Improvements
|
||||||
|
* change(room member): make sure we never display name/avatar when member is banned by @ganfra in https://github.com/element-hq/element-x-android/pull/5826
|
||||||
|
* Change : room details edit by @ganfra in https://github.com/element-hq/element-x-android/pull/5844
|
||||||
|
* Space feature flags by @ganfra in https://github.com/element-hq/element-x-android/pull/5827
|
||||||
|
* Update unsaved change dialog by @bmarty in https://github.com/element-hq/element-x-android/pull/5845
|
||||||
|
* change(notification): handle invite notification for spaces by @ganfra in https://github.com/element-hq/element-x-android/pull/5854
|
||||||
|
* Change : space settings iteration by @ganfra in https://github.com/element-hq/element-x-android/pull/5908
|
||||||
|
* Change : add "settings" entry menu by @ganfra in https://github.com/element-hq/element-x-android/pull/5948
|
||||||
|
* Changes : iterate again on permissions by @ganfra in https://github.com/element-hq/element-x-android/pull/5950
|
||||||
|
### 🐛 Bugfixes
|
||||||
|
* fix: usersWithRole(Owner) returns creators only if privilegedCreatorRole is true by @ganfra in https://github.com/element-hq/element-x-android/pull/5832
|
||||||
|
* Limit composer height dynamically by @bmarty in https://github.com/element-hq/element-x-android/pull/5835
|
||||||
|
* Fix work requests for inaccessible sessions being re-scheduled indefinitely by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5849
|
||||||
|
* Fix permission setting navigation by @bmarty in https://github.com/element-hq/element-x-android/pull/5877
|
||||||
|
* URL-encode deep link path segments and decode them when parsing by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5880
|
||||||
|
* Fix crash when calling `Room.predecessorRoom` when the room is destroyed by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5894
|
||||||
|
* fix: edit moderators not working by @ganfra in https://github.com/element-hq/element-x-android/pull/5906
|
||||||
|
* Use the right video preset when sharing videos by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5892
|
||||||
|
* Add `threadInfo` field to message like timeline events by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5930
|
||||||
|
* Fix unverified account after account creation by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5914
|
||||||
|
* Fix class cast exception by @bmarty in https://github.com/element-hq/element-x-android/pull/5958
|
||||||
|
* Fix : iterate on unban permissions by @ganfra in https://github.com/element-hq/element-x-android/pull/5959
|
||||||
|
* Use `VerificationState.VERIFIED` as soon as it's available by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5973
|
||||||
|
* Make the notification silent when the message is an outgoing message by @bmarty in https://github.com/element-hq/element-x-android/pull/5961
|
||||||
|
* Remove previously used id filtering from `RoomSyncSubscriber` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5985
|
||||||
|
* When handling incoming share, reuse existing room screen if possible by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6001
|
||||||
|
* When a duplicate room list entry is found, report it and remove it by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6006
|
||||||
|
### 🗣 Translations
|
||||||
|
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5860
|
||||||
|
* Sync Strings - Adding translations for Croatian by @ElementBot in https://github.com/element-hq/element-x-android/pull/5904
|
||||||
|
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5946
|
||||||
|
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5956
|
||||||
|
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5971
|
||||||
|
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5994
|
||||||
|
### 🧱 Build
|
||||||
|
* Restore `no-unused-imports` behaviour for `ktlintFormat` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5847
|
||||||
|
* Fix: use the right `BuildTimeConfig` field for the SDK DSN by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5856
|
||||||
|
* Add a way to configure value of useLegacyPackaging by @bmarty in https://github.com/element-hq/element-x-android/pull/5862
|
||||||
|
* Improve proguard config to keep the names in the classes in our packages by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5882
|
||||||
|
* Fix crash when changing the push provider in nightlies by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5951
|
||||||
|
### Dependency upgrades
|
||||||
|
* fix(deps): update dependency androidx.exifinterface:exifinterface to v1.4.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5846
|
||||||
|
* fix(deps): update metro to v0.8.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5833
|
||||||
|
* fix(deps): update dependency org.maplibre.gl:android-sdk to v12.2.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5831
|
||||||
|
* chore(deps): update plugin sonarqube to v7.2.0.6526 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5851
|
||||||
|
* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.12.4 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5855
|
||||||
|
* fix(deps): update dependency io.sentry:sentry-android to v8.28.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5853
|
||||||
|
* fix(deps): update dependency io.nlopez.compose.rules:detekt to v0.5.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5852
|
||||||
|
* Update dependency io.mockk:mockk to v1.14.7 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5866
|
||||||
|
* Update metro to v0.8.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5867
|
||||||
|
* Update peter-evans/create-pull-request action to v7.0.11 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5865
|
||||||
|
* Update camera to v1.5.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5857
|
||||||
|
* fix(deps): update showkase to v1.0.5 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5868
|
||||||
|
* chore(deps): update codecov/codecov-action action to v5.5.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5874
|
||||||
|
* fix(deps): update dependency org.maplibre.gl:android-sdk to v12.2.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5876
|
||||||
|
* fix(deps): update dependency net.zetetic:sqlcipher-android to v4.12.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5872
|
||||||
|
* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.12.10 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5881
|
||||||
|
* Update android.gradle.plugin to v8.13.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5887
|
||||||
|
* fix(deps): update dependency com.google.crypto.tink:tink-android to v1.20.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5875
|
||||||
|
* Update Compose BOM to version 2025.12.00. by @bmarty in https://github.com/element-hq/element-x-android/pull/5179
|
||||||
|
* Sync compound tokens https://github.com/element-hq/compound-design-tokens/releases/tag/v6.4.3 by @bmarty in https://github.com/element-hq/element-x-android/pull/5913
|
||||||
|
* fix(deps): update lifecycle to v2.10.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5240
|
||||||
|
* fix(deps): update dependency io.nlopez.compose.rules:detekt to v0.5.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5911
|
||||||
|
* fix(deps): update kotlin by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5417
|
||||||
|
* fix(deps): update activity to v1.12.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5770
|
||||||
|
* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.12.17 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5912
|
||||||
|
* fix(deps): update dependency io.sentry:sentry-android to v8.29.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5918
|
||||||
|
* fix(deps): update dependency com.google.firebase:firebase-bom to v34.7.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5915
|
||||||
|
* fix(deps): update haze to v1.7.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5712
|
||||||
|
* chore(deps): update peter-evans/create-pull-request action to v8 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5878
|
||||||
|
* fix(deps): update dependency com.posthog:posthog-android to v3.27.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5871
|
||||||
|
* chore(deps): update plugin sonarqube to v7.2.1.6560 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5905
|
||||||
|
* fix(deps): update metro to v0.9.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5920
|
||||||
|
* fix(deps): update activity to v1.12.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5924
|
||||||
|
* Update plugin sonarqube to v7.2.2.6593 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5927
|
||||||
|
* fix(deps): update media3 to v1.9.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5931
|
||||||
|
* fix(deps): update metro to v0.9.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5940
|
||||||
|
* fix(deps): update dependency io.nlopez.compose.rules:detekt to v0.5.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5939
|
||||||
|
* fix(deps): update dependency com.google.zxing:core to v3.5.4 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5935
|
||||||
|
* Upgrade robolectric to version 4.16 by @bmarty in https://github.com/element-hq/element-x-android/pull/5923
|
||||||
|
* fix(deps): update dependency androidx.webkit:webkit to v1.15.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5925
|
||||||
|
* chore(deps): update github artifact actions (major) by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5932
|
||||||
|
* fix(deps): update dependency org.maplibre.gl:android-sdk to v12.3.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5883
|
||||||
|
* fix(deps): update dependency io.github.sergio-sastre.composablepreviewscanner:android to v0.8.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5916
|
||||||
|
* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.12.19 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5943
|
||||||
|
* fix(deps): update kotlin by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5917
|
||||||
|
* fix(deps): update dependency com.posthog:posthog-android to v3.28.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5941
|
||||||
|
* fix(deps): update wysiwyg to v2.41.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5921
|
||||||
|
* fix(deps): update roborazzi to v1.53.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5962
|
||||||
|
* fix(deps): update roborazzi to v1.54.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5970
|
||||||
|
* fix(deps): update dependency org.unifiedpush.android:connector to v3.2.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5972
|
||||||
|
* fix(deps): update metro to v0.9.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5967
|
||||||
|
* Upgrade compose to 2025.12.01 by @bmarty in https://github.com/element-hq/element-x-android/pull/5969
|
||||||
|
* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v26 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5977
|
||||||
|
* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v26.1.9 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5986
|
||||||
|
* fix(deps): update roborazzi to v1.56.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5987
|
||||||
|
* fix(deps): update dependency com.posthog:posthog-android to v3.28.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5988
|
||||||
|
* fix(deps): update metro to v0.9.4 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5991
|
||||||
|
* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v26.1.12 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5999
|
||||||
|
### Others
|
||||||
|
* Enable Sentry in the SDK and allow bridging spans by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5808
|
||||||
|
* Add alert to encrypted rooms with visible history (Android). by @kaylendog in https://github.com/element-hq/element-x-android/pull/5709
|
||||||
|
* Add accessibility to the "sending" picto. by @bmarty in https://github.com/element-hq/element-x-android/pull/5869
|
||||||
|
* Add SDK database vacuuming operations by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5858
|
||||||
|
* Sync compound tokens https://github.com/element-hq/compound-design-tokens/releases/tag/v6.4.2 by @bmarty in https://github.com/element-hq/element-x-android/pull/5897
|
||||||
|
* RoomSummary: move the icon related to the last message state on start of the message. by @bmarty in https://github.com/element-hq/element-x-android/pull/5888
|
||||||
|
* Qr code scanner cleanup by @bmarty in https://github.com/element-hq/element-x-android/pull/5891
|
||||||
|
* Design : update user rows by @ganfra in https://github.com/element-hq/element-x-android/pull/5900
|
||||||
|
* misc : rework power levels apis by @ganfra in https://github.com/element-hq/element-x-android/pull/5879
|
||||||
|
* Fix preview name by @bmarty in https://github.com/element-hq/element-x-android/pull/5919
|
||||||
|
* Allow uploading extra data to Sentry when analytics is enabled by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5910
|
||||||
|
* Show history visibility banner strictly for `shared` rooms instead of `invited`. by @kaylendog in https://github.com/element-hq/element-x-android/pull/5936
|
||||||
|
* Simplify the copy of the history visibility settings by @bmarty in https://github.com/element-hq/element-x-android/pull/5942
|
||||||
|
* Use only font from compound by @bmarty in https://github.com/element-hq/element-x-android/pull/5945
|
||||||
|
* Cleanup FFI object fixtures. by @bmarty in https://github.com/element-hq/element-x-android/pull/5957
|
||||||
|
* Add variable playback speed feature for voice messages by @Medformatik in https://github.com/element-hq/element-x-android/pull/5504
|
||||||
|
* Ensure that avatars always have a content description. by @bmarty in https://github.com/element-hq/element-x-android/pull/5968
|
||||||
|
* Ensure space feature is enabled by @ganfra in https://github.com/element-hq/element-x-android/pull/5960
|
||||||
|
* Adjust metrics to the new specifications by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5937
|
||||||
|
* Use `TextFieldState` for room list search by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5975
|
||||||
|
* fix(deps): update roborazzi to v1.55.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5976
|
||||||
|
* Iterate on verification screen by @bmarty in https://github.com/element-hq/element-x-android/pull/5981
|
||||||
|
* Add preview with a11y details. by @bmarty in https://github.com/element-hq/element-x-android/pull/5984
|
||||||
|
* Change the title for `AnalyticsTransactions.coldStart` and `.catchUp` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5998
|
||||||
|
* [a11y] voice message improvements by @bmarty in https://github.com/element-hq/element-x-android/pull/5980
|
||||||
|
|
||||||
|
## New Contributors
|
||||||
|
* @Medformatik made their first contribution in https://github.com/element-hq/element-x-android/pull/5504
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.12.0...v26.01.0
|
||||||
|
|
||||||
Changes in Element X v25.12.0
|
Changes in Element X v25.12.0
|
||||||
=============================
|
=============================
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,4 +25,9 @@ object RageshakeConfig {
|
||||||
* The maximum size of the upload request. Default value is just below CloudFlare's max request size.
|
* The maximum size of the upload request. Default value is just below CloudFlare's max request size.
|
||||||
*/
|
*/
|
||||||
const val MAX_LOG_UPLOAD_SIZE = 50 * 1024 * 1024L
|
const val MAX_LOG_UPLOAD_SIZE = 50 * 1024 * 1024L
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum size of a single log file.
|
||||||
|
*/
|
||||||
|
const val MAX_LOG_CONTENT_SIZE = 100 * 1024 * 1024L
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
fastlane/metadata/android/en-US/changelogs/202601010.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/202601010.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
Main changes in this version: iterated on spaces, improved the room list stability and performance, fix an issue with the cached well-known config.
|
||||||
|
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||||
|
|
@ -46,6 +46,7 @@ import io.element.android.features.home.impl.model.RoomListRoomSummaryProvider
|
||||||
import io.element.android.features.home.impl.model.RoomSummaryDisplayType
|
import io.element.android.features.home.impl.model.RoomSummaryDisplayType
|
||||||
import io.element.android.features.home.impl.roomlist.RoomListEvents
|
import io.element.android.features.home.impl.roomlist.RoomListEvents
|
||||||
import io.element.android.libraries.core.extensions.orEmpty
|
import io.element.android.libraries.core.extensions.orEmpty
|
||||||
|
import io.element.android.libraries.core.extensions.toSafeLength
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom
|
import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.InviteButtonsRowMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.InviteButtonsRowMolecule
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
|
|
@ -227,7 +228,7 @@ private fun NameAndTimestampRow(
|
||||||
// Name
|
// Name
|
||||||
Text(
|
Text(
|
||||||
style = ElementTheme.typography.fontBodyLgMedium,
|
style = ElementTheme.typography.fontBodyLgMedium,
|
||||||
text = name ?: stringResource(id = CommonStrings.common_no_room_name),
|
text = name?.toSafeLength(ellipsize = true) ?: stringResource(id = CommonStrings.common_no_room_name),
|
||||||
fontStyle = FontStyle.Italic.takeIf { name == null },
|
fontStyle = FontStyle.Italic.takeIf { name == null },
|
||||||
color = ElementTheme.colors.roomListRoomName,
|
color = ElementTheme.colors.roomListRoomName,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
|
|
@ -380,7 +381,7 @@ private fun InviteNameAndIndicatorRow(
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
style = ElementTheme.typography.fontBodyLgMedium,
|
style = ElementTheme.typography.fontBodyLgMedium,
|
||||||
text = name ?: stringResource(id = CommonStrings.common_no_room_name),
|
text = name?.toSafeLength(ellipsize = true) ?: stringResource(id = CommonStrings.common_no_room_name),
|
||||||
fontStyle = FontStyle.Italic.takeIf { name == null },
|
fontStyle = FontStyle.Italic.takeIf { name == null },
|
||||||
color = ElementTheme.colors.roomListRoomName,
|
color = ElementTheme.colors.roomListRoomName,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ fun EnterNumberView(
|
||||||
subTitle = stringResource(R.string.screen_link_new_device_enter_number_subtitle),
|
subTitle = stringResource(R.string.screen_link_new_device_enter_number_subtitle),
|
||||||
iconStyle = BigIcon.Style.Default(CompoundIcons.Computer()),
|
iconStyle = BigIcon.Style.Default(CompoundIcons.Computer()),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
isScrollable = true,
|
||||||
buttons = {
|
buttons = {
|
||||||
Button(
|
Button(
|
||||||
text = stringResource(CommonStrings.action_continue),
|
text = stringResource(CommonStrings.action_continue),
|
||||||
|
|
|
||||||
|
|
@ -15,4 +15,6 @@
|
||||||
</intent>
|
</intent>
|
||||||
</queries>
|
</queries>
|
||||||
|
|
||||||
|
<!-- Permission to read data from Element classic -->
|
||||||
|
<uses-permission android:name="im.vector.app.READ_DATA" />
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ import dev.zacsweers.metro.Binds
|
||||||
import dev.zacsweers.metro.ContributesTo
|
import dev.zacsweers.metro.ContributesTo
|
||||||
import io.element.android.features.login.impl.changeserver.ChangeServerPresenter
|
import io.element.android.features.login.impl.changeserver.ChangeServerPresenter
|
||||||
import io.element.android.features.login.impl.changeserver.ChangeServerState
|
import io.element.android.features.login.impl.changeserver.ChangeServerState
|
||||||
|
import io.element.android.features.login.impl.screens.onboarding.classic.LoginWithClassicPresenter
|
||||||
|
import io.element.android.features.login.impl.screens.onboarding.classic.LoginWithClassicState
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
|
||||||
@ContributesTo(AppScope::class)
|
@ContributesTo(AppScope::class)
|
||||||
|
|
@ -21,4 +23,7 @@ import io.element.android.libraries.architecture.Presenter
|
||||||
interface LoginModule {
|
interface LoginModule {
|
||||||
@Binds
|
@Binds
|
||||||
fun bindChangeServerPresenter(presenter: ChangeServerPresenter): Presenter<ChangeServerState>
|
fun bindChangeServerPresenter(presenter: ChangeServerPresenter): Presenter<ChangeServerState>
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
fun bindLoginWithClassicPresenter(presenter: LoginWithClassicPresenter): Presenter<LoginWithClassicState>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import io.element.android.features.enterprise.api.canConnectToAnyHomeserver
|
||||||
import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl
|
import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl
|
||||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||||
import io.element.android.features.login.impl.login.LoginHelper
|
import io.element.android.features.login.impl.login.LoginHelper
|
||||||
|
import io.element.android.features.login.impl.screens.onboarding.classic.LoginWithClassicState
|
||||||
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
|
|
@ -44,6 +45,7 @@ class OnBoardingPresenter(
|
||||||
private val onBoardingLogoResIdProvider: OnBoardingLogoResIdProvider,
|
private val onBoardingLogoResIdProvider: OnBoardingLogoResIdProvider,
|
||||||
private val sessionStore: SessionStore,
|
private val sessionStore: SessionStore,
|
||||||
private val accountProviderDataSource: AccountProviderDataSource,
|
private val accountProviderDataSource: AccountProviderDataSource,
|
||||||
|
private val loginWithClassicPresenter: Presenter<LoginWithClassicState>,
|
||||||
) : Presenter<OnBoardingState> {
|
) : Presenter<OnBoardingState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
|
@ -99,6 +101,8 @@ class OnBoardingPresenter(
|
||||||
|
|
||||||
val loginMode by loginHelper.collectLoginMode()
|
val loginMode by loginHelper.collectLoginMode()
|
||||||
|
|
||||||
|
val loginWithClassicState = loginWithClassicPresenter.present()
|
||||||
|
|
||||||
fun handleEvent(event: OnBoardingEvents) {
|
fun handleEvent(event: OnBoardingEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is OnBoardingEvents.OnSignIn -> localCoroutineScope.launch {
|
is OnBoardingEvents.OnSignIn -> localCoroutineScope.launch {
|
||||||
|
|
@ -132,6 +136,7 @@ class OnBoardingPresenter(
|
||||||
loginMode = loginMode,
|
loginMode = loginMode,
|
||||||
version = buildMeta.versionName,
|
version = buildMeta.versionName,
|
||||||
onBoardingLogoResId = onBoardingLogoResId,
|
onBoardingLogoResId = onBoardingLogoResId,
|
||||||
|
loginWithClassicState = loginWithClassicState,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ package io.element.android.features.login.impl.screens.onboarding
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import io.element.android.features.login.impl.login.LoginMode
|
import io.element.android.features.login.impl.login.LoginMode
|
||||||
|
import io.element.android.features.login.impl.screens.onboarding.classic.LoginWithClassicState
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
|
||||||
data class OnBoardingState(
|
data class OnBoardingState(
|
||||||
|
|
@ -24,6 +25,7 @@ data class OnBoardingState(
|
||||||
@DrawableRes
|
@DrawableRes
|
||||||
val onBoardingLogoResId: Int?,
|
val onBoardingLogoResId: Int?,
|
||||||
val loginMode: AsyncData<LoginMode>,
|
val loginMode: AsyncData<LoginMode>,
|
||||||
|
val loginWithClassicState: LoginWithClassicState,
|
||||||
val eventSink: (OnBoardingEvents) -> Unit,
|
val eventSink: (OnBoardingEvents) -> Unit,
|
||||||
) {
|
) {
|
||||||
val submitEnabled: Boolean
|
val submitEnabled: Boolean
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ package io.element.android.features.login.impl.screens.onboarding
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.login.impl.login.LoginMode
|
import io.element.android.features.login.impl.login.LoginMode
|
||||||
|
import io.element.android.features.login.impl.screens.onboarding.classic.LoginWithClassicState
|
||||||
|
import io.element.android.features.login.impl.screens.onboarding.classic.aLoginWithClassicState
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.designsystem.R
|
import io.element.android.libraries.designsystem.R
|
||||||
|
|
||||||
|
|
@ -44,6 +46,7 @@ fun anOnBoardingState(
|
||||||
@DrawableRes
|
@DrawableRes
|
||||||
customLogoResId: Int? = null,
|
customLogoResId: Int? = null,
|
||||||
loginMode: AsyncData<LoginMode> = AsyncData.Uninitialized,
|
loginMode: AsyncData<LoginMode> = AsyncData.Uninitialized,
|
||||||
|
loginWithClassicState: LoginWithClassicState = aLoginWithClassicState(),
|
||||||
eventSink: (OnBoardingEvents) -> Unit = {},
|
eventSink: (OnBoardingEvents) -> Unit = {},
|
||||||
) = OnBoardingState(
|
) = OnBoardingState(
|
||||||
isAddingAccount = isAddingAccount,
|
isAddingAccount = isAddingAccount,
|
||||||
|
|
@ -56,5 +59,6 @@ fun anOnBoardingState(
|
||||||
version = version,
|
version = version,
|
||||||
loginMode = loginMode,
|
loginMode = loginMode,
|
||||||
onBoardingLogoResId = customLogoResId,
|
onBoardingLogoResId = customLogoResId,
|
||||||
|
loginWithClassicState = loginWithClassicState,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -31,10 +31,15 @@ import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.compose.LifecycleEventEffect
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.login.impl.R
|
import io.element.android.features.login.impl.R
|
||||||
import io.element.android.features.login.impl.login.LoginModeView
|
import io.element.android.features.login.impl.login.LoginModeView
|
||||||
|
import io.element.android.features.login.impl.screens.onboarding.classic.ConfirmingLoginWithElementClassic
|
||||||
|
import io.element.android.features.login.impl.screens.onboarding.classic.LoginWithClassicEvent
|
||||||
|
import io.element.android.features.login.impl.screens.onboarding.classic.LoginWithClassicState
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtom
|
import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtom
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtomSize
|
import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtomSize
|
||||||
|
|
@ -42,6 +47,8 @@ import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMo
|
||||||
import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
|
import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
|
||||||
import io.element.android.libraries.designsystem.atomic.pages.OnBoardingPage
|
import io.element.android.libraries.designsystem.atomic.pages.OnBoardingPage
|
||||||
import io.element.android.libraries.designsystem.components.BigIcon
|
import io.element.android.libraries.designsystem.components.BigIcon
|
||||||
|
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.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Button
|
import io.element.android.libraries.designsystem.theme.components.Button
|
||||||
|
|
@ -109,6 +116,43 @@ fun OnBoardingView(
|
||||||
buttons = buttons,
|
buttons = buttons,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LoginWithElementClassicView(
|
||||||
|
state = state.loginWithClassicState,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LoginWithElementClassicView(
|
||||||
|
state: LoginWithClassicState,
|
||||||
|
) {
|
||||||
|
LifecycleEventEffect(Lifecycle.Event.ON_RESUME) {
|
||||||
|
state.eventSink(LoginWithClassicEvent.RefreshData)
|
||||||
|
}
|
||||||
|
AsyncActionView(
|
||||||
|
async = state.loginWithClassicAction,
|
||||||
|
confirmationDialog = { confirming ->
|
||||||
|
when (confirming) {
|
||||||
|
is ConfirmingLoginWithElementClassic -> {
|
||||||
|
// TODO i18n
|
||||||
|
ConfirmationDialog(
|
||||||
|
title = "Sign in with Element Classic",
|
||||||
|
content = "You are signing in as ${confirming.userId} on Element Classic." +
|
||||||
|
" Your existing session on Element Classic will not be signed out. Do you want to continue?",
|
||||||
|
submitText = stringResource(CommonStrings.action_continue),
|
||||||
|
onSubmitClick = { state.eventSink(LoginWithClassicEvent.DoLoginWithClassic) },
|
||||||
|
onDismiss = { state.eventSink(LoginWithClassicEvent.CloseDialog) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onErrorDismiss = {
|
||||||
|
state.eventSink(LoginWithClassicEvent.CloseDialog)
|
||||||
|
},
|
||||||
|
onSuccess = {
|
||||||
|
// noop, the view will be closed
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -239,6 +283,18 @@ private fun OnBoardingButtons(
|
||||||
} else {
|
} else {
|
||||||
CommonStrings.action_continue
|
CommonStrings.action_continue
|
||||||
}
|
}
|
||||||
|
if (state.loginWithClassicState.canLoginWithClassic) {
|
||||||
|
Button(
|
||||||
|
text = "Sign in with Element Classic",
|
||||||
|
leadingIcon = IconSource.Vector(CompoundIcons.Mobile()),
|
||||||
|
onClick = {
|
||||||
|
state.loginWithClassicState.eventSink(
|
||||||
|
LoginWithClassicEvent.StartLoginWithClassic
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
if (state.canLoginWithQrCode) {
|
if (state.canLoginWithQrCode) {
|
||||||
Button(
|
Button(
|
||||||
text = stringResource(id = R.string.screen_onboarding_sign_in_with_qr_code),
|
text = stringResource(id = R.string.screen_onboarding_sign_in_with_qr_code),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.login.impl.screens.onboarding.classic
|
||||||
|
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
||||||
|
class ConfirmingLoginWithElementClassic(
|
||||||
|
val userId: UserId,
|
||||||
|
) : AsyncAction.Confirming
|
||||||
|
|
@ -0,0 +1,249 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.login.impl.screens.onboarding.classic
|
||||||
|
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Context.BIND_AUTO_CREATE
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.ServiceConnection
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.os.Message
|
||||||
|
import android.os.Messenger
|
||||||
|
import android.os.RemoteException
|
||||||
|
import dev.zacsweers.metro.AppScope
|
||||||
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
|
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||||
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
|
import io.element.android.libraries.core.meta.BuildType
|
||||||
|
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||||
|
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
interface ElementClassicConnection {
|
||||||
|
fun start()
|
||||||
|
fun stop()
|
||||||
|
fun requestData()
|
||||||
|
val stateFlow: StateFlow<ElementClassicConnectionState>
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface ElementClassicConnectionState {
|
||||||
|
object Idle : ElementClassicConnectionState
|
||||||
|
object ElementClassicNotFound : ElementClassicConnectionState
|
||||||
|
object ElementClassicReadyNoSession : ElementClassicConnectionState
|
||||||
|
data class ElementClassicReady(val userId: UserId) : ElementClassicConnectionState
|
||||||
|
data class Error(val error: String) : ElementClassicConnectionState
|
||||||
|
}
|
||||||
|
|
||||||
|
private val loggerTag = LoggerTag("ECConnection")
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class DefaultElementClassicConnection(
|
||||||
|
@ApplicationContext
|
||||||
|
private val context: Context,
|
||||||
|
@AppCoroutineScope
|
||||||
|
private val coroutineScope: CoroutineScope,
|
||||||
|
private val buildMeta: BuildMeta,
|
||||||
|
) : ElementClassicConnection {
|
||||||
|
// Messenger for communicating with the service.
|
||||||
|
private var messenger: Messenger? = null
|
||||||
|
|
||||||
|
// Target we publish for external service to send messages to IncomingHandler.
|
||||||
|
private val incomingMessenger: Messenger = Messenger(IncomingHandler())
|
||||||
|
|
||||||
|
// Flag indicating whether we have called bind on the service.
|
||||||
|
private var bound: Boolean = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for interacting with the main interface of the service.
|
||||||
|
*/
|
||||||
|
private val serviceConnection = object : ServiceConnection {
|
||||||
|
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
||||||
|
Timber.tag(loggerTag.value).d("onServiceConnected")
|
||||||
|
// This is called when the connection with the service has been
|
||||||
|
// established, giving us the object we can use to
|
||||||
|
// interact with the service. We are communicating with the
|
||||||
|
// service using a Messenger, so here we get a client-side
|
||||||
|
// representation of that from the raw IBinder object.
|
||||||
|
messenger = Messenger(service)
|
||||||
|
bound = true
|
||||||
|
// Request the data as soon as possible
|
||||||
|
requestData()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(className: ComponentName) {
|
||||||
|
Timber.tag(loggerTag.value).d("onServiceDisconnected")
|
||||||
|
// This is called when the connection with the service has been
|
||||||
|
// unexpectedly disconnected—that is, its process crashed.
|
||||||
|
messenger = null
|
||||||
|
bound = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun start() {
|
||||||
|
Timber.tag(loggerTag.value).w("start()")
|
||||||
|
coroutineScope.launch {
|
||||||
|
// Establish a connection with the service. We use an explicit
|
||||||
|
// class name because there is no reason to be able to let other
|
||||||
|
// applications replace our component.
|
||||||
|
try {
|
||||||
|
val intentService = Intent()
|
||||||
|
intentService.setComponent(getElementClassicComponent(buildMeta))
|
||||||
|
if (context.bindService(intentService, serviceConnection, BIND_AUTO_CREATE)) {
|
||||||
|
Timber.tag(loggerTag.value).d("Binding returned true")
|
||||||
|
} else {
|
||||||
|
// This happen when the app is not installed
|
||||||
|
Timber.tag(loggerTag.value).d("Binding returned false")
|
||||||
|
mutableStateFlow.emit(ElementClassicConnectionState.ElementClassicNotFound)
|
||||||
|
}
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
Timber.tag(loggerTag.value).e(e, "Can't bind to Service")
|
||||||
|
mutableStateFlow.emit(ElementClassicConnectionState.Error(e.localizedMessage.orEmpty()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stop() {
|
||||||
|
Timber.tag(loggerTag.value).w("stop(): Unbinding (bound=$bound)")
|
||||||
|
if (bound) {
|
||||||
|
// Detach our existing connection.
|
||||||
|
context.unbindService(serviceConnection)
|
||||||
|
bound = false
|
||||||
|
}
|
||||||
|
coroutineScope.launch {
|
||||||
|
mutableStateFlow.emit(ElementClassicConnectionState.Idle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun requestData() {
|
||||||
|
Timber.tag(loggerTag.value).w("requestData()")
|
||||||
|
coroutineScope.launch {
|
||||||
|
val finalMessenger = messenger
|
||||||
|
if (finalMessenger == null) {
|
||||||
|
Timber.tag(loggerTag.value).w("The messenger is null, can't request data")
|
||||||
|
mutableStateFlow.emit(ElementClassicConnectionState.Error("The messenger is null, can't request data"))
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
// Get the data
|
||||||
|
val msg = Message.obtain(null, MSG_GET_DATA)
|
||||||
|
msg.replyTo = incomingMessenger
|
||||||
|
finalMessenger.send(msg)
|
||||||
|
} catch (e: RemoteException) {
|
||||||
|
// In this case the service has crashed before we could even
|
||||||
|
// do anything with it; we can count on soon being
|
||||||
|
// disconnected (and then reconnected if it can be restarted)
|
||||||
|
// so there is no need to do anything here.
|
||||||
|
Timber.tag(loggerTag.value).e(e, "RemoteException")
|
||||||
|
mutableStateFlow.emit(ElementClassicConnectionState.Error(e.localizedMessage.orEmpty()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val mutableStateFlow = MutableStateFlow<ElementClassicConnectionState>(ElementClassicConnectionState.Idle)
|
||||||
|
override val stateFlow = mutableStateFlow.asStateFlow()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler of incoming messages from service.
|
||||||
|
*/
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
inner class IncomingHandler : Handler() {
|
||||||
|
override fun handleMessage(msg: Message) {
|
||||||
|
Timber.tag(loggerTag.value).d("IncomingHandler handling message ${msg.what}")
|
||||||
|
when (msg.what) {
|
||||||
|
MSG_GET_DATA -> {
|
||||||
|
// The data must be extracted from the bundle before we launch the coroutine, else the bundle will be emptied
|
||||||
|
val state = msg.data.toElementClassicConnectionState()
|
||||||
|
emitElementClassicState(state)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
super.handleMessage(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun emitElementClassicState(state: ElementClassicConnectionState) = coroutineScope.launch {
|
||||||
|
when (state) {
|
||||||
|
is ElementClassicConnectionState.Error -> {
|
||||||
|
Timber.tag(loggerTag.value).w("Received error from Element Classic: %s", state.error)
|
||||||
|
mutableStateFlow.emit(state)
|
||||||
|
}
|
||||||
|
is ElementClassicConnectionState.ElementClassicReady -> {
|
||||||
|
Timber.tag(loggerTag.value).d("Received userId from Element Classic: %s", state.userId)
|
||||||
|
mutableStateFlow.emit(state)
|
||||||
|
}
|
||||||
|
ElementClassicConnectionState.ElementClassicReadyNoSession -> {
|
||||||
|
Timber.tag(loggerTag.value).d("Received no session from Element Classic")
|
||||||
|
mutableStateFlow.emit(state)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// Should not happen
|
||||||
|
Timber.tag(loggerTag.value).w("Received unexpected state from Element Classic: %s", state)
|
||||||
|
mutableStateFlow.emit(ElementClassicConnectionState.Idle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getElementClassicComponent(buildMeta: BuildMeta) = ComponentName(
|
||||||
|
buildString {
|
||||||
|
append(ELEMENT_CLASSIC_APP_ID)
|
||||||
|
append(
|
||||||
|
when (buildMeta.buildType) {
|
||||||
|
BuildType.DEBUG -> ELEMENT_CLASSIC_APP_ID_DEBUG_SUFFIX
|
||||||
|
BuildType.NIGHTLY -> ELEMENT_CLASSIC_APP_ID_NIGHTLY_SUFFIX
|
||||||
|
BuildType.RELEASE -> ELEMENT_CLASSIC_APP_ID_RELEASE_SUFFIX
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
ELEMENT_CLASSIC_SERVICE_FULL_CLASS_NAME,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun Bundle?.toElementClassicConnectionState(): ElementClassicConnectionState {
|
||||||
|
return if (this == null) {
|
||||||
|
ElementClassicConnectionState.Error("No data received from Element Classic")
|
||||||
|
} else {
|
||||||
|
val error = getString(KEY_ERROR_STR)
|
||||||
|
if (error != null) {
|
||||||
|
ElementClassicConnectionState.Error(error)
|
||||||
|
} else {
|
||||||
|
val userId = getString(KEY_USER_ID_STR)?.let(::UserId)
|
||||||
|
if (userId != null) {
|
||||||
|
ElementClassicConnectionState.ElementClassicReady(userId)
|
||||||
|
} else {
|
||||||
|
ElementClassicConnectionState.ElementClassicReadyNoSession
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Everything in this companion object must match what is defined in Element Classic
|
||||||
|
private companion object {
|
||||||
|
// Command to the service to get the data.
|
||||||
|
const val MSG_GET_DATA = 1
|
||||||
|
|
||||||
|
const val ELEMENT_CLASSIC_APP_ID = "im.vector.app"
|
||||||
|
const val ELEMENT_CLASSIC_APP_ID_DEBUG_SUFFIX = ".debug"
|
||||||
|
const val ELEMENT_CLASSIC_APP_ID_NIGHTLY_SUFFIX = ".nightly"
|
||||||
|
const val ELEMENT_CLASSIC_APP_ID_RELEASE_SUFFIX = ""
|
||||||
|
|
||||||
|
const val ELEMENT_CLASSIC_SERVICE_FULL_CLASS_NAME = "im.vector.app.features.importer.ImporterService"
|
||||||
|
|
||||||
|
// Keys for the bundle returned from the service
|
||||||
|
const val KEY_ERROR_STR = "error"
|
||||||
|
const val KEY_USER_ID_STR = "userId"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.login.impl.screens.onboarding.classic
|
||||||
|
|
||||||
|
sealed interface LoginWithClassicEvent {
|
||||||
|
data object RefreshData : LoginWithClassicEvent
|
||||||
|
data object StartLoginWithClassic : LoginWithClassicEvent
|
||||||
|
data object DoLoginWithClassic : LoginWithClassicEvent
|
||||||
|
data object CloseDialog : LoginWithClassicEvent
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.login.impl.screens.onboarding.classic
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
|
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||||
|
import io.element.android.libraries.sessionstorage.api.toUserListFlow
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
class LoginWithClassicPresenter(
|
||||||
|
private val elementClassicConnection: ElementClassicConnection,
|
||||||
|
private val sessionStore: SessionStore,
|
||||||
|
private val featureFlagService: FeatureFlagService,
|
||||||
|
) : Presenter<LoginWithClassicState> {
|
||||||
|
@Composable
|
||||||
|
override fun present(): LoginWithClassicState {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val isSignInWithClassicEnabled by remember {
|
||||||
|
featureFlagService.isFeatureEnabledFlow(FeatureFlags.SignInWithClassic)
|
||||||
|
}.collectAsState(initial = false)
|
||||||
|
|
||||||
|
if (isSignInWithClassicEnabled) {
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
elementClassicConnection.start()
|
||||||
|
onDispose {
|
||||||
|
elementClassicConnection.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val state by elementClassicConnection.stateFlow.collectAsState()
|
||||||
|
val loginWithClassicAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||||
|
|
||||||
|
val existingSession by remember {
|
||||||
|
sessionStore.sessionsFlow().toUserListFlow()
|
||||||
|
}.collectAsState(emptyList())
|
||||||
|
|
||||||
|
val canLoginWithClassic by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
when (val finalState = state) {
|
||||||
|
is ElementClassicConnectionState.ElementClassicReady -> {
|
||||||
|
// Ensure there is no existing session with the same Id.
|
||||||
|
finalState.userId.value !in existingSession && isSignInWithClassicEnabled
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleEvent(event: LoginWithClassicEvent) {
|
||||||
|
when (event) {
|
||||||
|
LoginWithClassicEvent.RefreshData -> {
|
||||||
|
elementClassicConnection.requestData()
|
||||||
|
}
|
||||||
|
LoginWithClassicEvent.StartLoginWithClassic -> {
|
||||||
|
val currentState = elementClassicConnection.stateFlow.value
|
||||||
|
if (currentState is ElementClassicConnectionState.ElementClassicReady) {
|
||||||
|
loginWithClassicAction.value = ConfirmingLoginWithElementClassic(
|
||||||
|
userId = currentState.userId,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
loginWithClassicAction.value = AsyncAction.Failure(IllegalStateException("Element Classic is not ready"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LoginWithClassicEvent.DoLoginWithClassic -> coroutineScope.launch {
|
||||||
|
// TODO Implement real login logic here
|
||||||
|
loginWithClassicAction.value = AsyncAction.Loading
|
||||||
|
delay(1000)
|
||||||
|
loginWithClassicAction.value = AsyncAction.Success(Unit)
|
||||||
|
}
|
||||||
|
LoginWithClassicEvent.CloseDialog -> {
|
||||||
|
loginWithClassicAction.value = AsyncAction.Uninitialized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return LoginWithClassicState(
|
||||||
|
canLoginWithClassic = canLoginWithClassic,
|
||||||
|
loginWithClassicAction = loginWithClassicAction.value,
|
||||||
|
eventSink = ::handleEvent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.login.impl.screens.onboarding.classic
|
||||||
|
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
|
||||||
|
data class LoginWithClassicState(
|
||||||
|
val canLoginWithClassic: Boolean,
|
||||||
|
val loginWithClassicAction: AsyncAction<Unit>,
|
||||||
|
val eventSink: (LoginWithClassicEvent) -> Unit,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.login.impl.screens.onboarding.classic
|
||||||
|
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
|
||||||
|
fun aLoginWithClassicState(
|
||||||
|
canLoginWithClassic: Boolean = false,
|
||||||
|
loginWithClassicAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
|
eventSink: (LoginWithClassicEvent) -> Unit = {},
|
||||||
|
) = LoginWithClassicState(
|
||||||
|
canLoginWithClassic = canLoginWithClassic,
|
||||||
|
loginWithClassicAction = loginWithClassicAction,
|
||||||
|
eventSink = eventSink,
|
||||||
|
)
|
||||||
|
|
@ -16,6 +16,7 @@ import io.element.android.features.enterprise.test.FakeEnterpriseService
|
||||||
import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl
|
import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl
|
||||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||||
import io.element.android.features.login.impl.login.LoginHelper
|
import io.element.android.features.login.impl.login.LoginHelper
|
||||||
|
import io.element.android.features.login.impl.screens.onboarding.classic.aLoginWithClassicState
|
||||||
import io.element.android.features.login.impl.web.FakeWebClientUrlForAuthenticationRetriever
|
import io.element.android.features.login.impl.web.FakeWebClientUrlForAuthenticationRetriever
|
||||||
import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever
|
import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever
|
||||||
import io.element.android.features.wellknown.test.FakeWellknownRetriever
|
import io.element.android.features.wellknown.test.FakeWellknownRetriever
|
||||||
|
|
@ -88,7 +89,10 @@ class OnBoardingPresenterTest {
|
||||||
assertThat(initialState.canCreateAccount).isEqualTo(OnBoardingConfig.CAN_CREATE_ACCOUNT)
|
assertThat(initialState.canCreateAccount).isEqualTo(OnBoardingConfig.CAN_CREATE_ACCOUNT)
|
||||||
assertThat(initialState.canReportBug).isFalse()
|
assertThat(initialState.canReportBug).isFalse()
|
||||||
assertThat(initialState.isAddingAccount).isFalse()
|
assertThat(initialState.isAddingAccount).isFalse()
|
||||||
assertThat(awaitItem().canLoginWithQrCode).isTrue()
|
assertThat(initialState.loginWithClassicState.canLoginWithClassic).isFalse()
|
||||||
|
val finalState = awaitItem()
|
||||||
|
assertThat(finalState.canLoginWithQrCode).isTrue()
|
||||||
|
assertThat(finalState.loginWithClassicState.canLoginWithClassic).isFalse()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -283,6 +287,7 @@ private fun createPresenter(
|
||||||
onBoardingLogoResIdProvider = onBoardingLogoResIdProvider,
|
onBoardingLogoResIdProvider = onBoardingLogoResIdProvider,
|
||||||
sessionStore = sessionStore,
|
sessionStore = sessionStore,
|
||||||
accountProviderDataSource = accountProviderDataSource,
|
accountProviderDataSource = accountProviderDataSource,
|
||||||
|
loginWithClassicPresenter = { aLoginWithClassicState() },
|
||||||
)
|
)
|
||||||
|
|
||||||
fun createLoginHelper(
|
fun createLoginHelper(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.login.impl.screens.onboarding.classic
|
||||||
|
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
|
class FakeElementClassicConnection(
|
||||||
|
private val startResult: () -> Unit = { lambdaError() },
|
||||||
|
private val stopResult: () -> Unit = { lambdaError() },
|
||||||
|
private val requestDataResult: () -> Unit = { lambdaError() },
|
||||||
|
initialState: ElementClassicConnectionState = ElementClassicConnectionState.Idle
|
||||||
|
) : ElementClassicConnection {
|
||||||
|
override fun start() = startResult()
|
||||||
|
override fun stop() = stopResult()
|
||||||
|
override fun requestData() = requestDataResult()
|
||||||
|
private val mutableStateFlow = MutableStateFlow(initialState)
|
||||||
|
override val stateFlow: StateFlow<ElementClassicConnectionState> = mutableStateFlow.asStateFlow()
|
||||||
|
suspend fun emitState(state: ElementClassicConnectionState) {
|
||||||
|
mutableStateFlow.emit(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,213 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
||||||
|
package io.element.android.features.login.impl.screens.onboarding.classic
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
|
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||||
|
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||||
|
import io.element.android.libraries.sessionstorage.test.aSessionData
|
||||||
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
import io.element.android.tests.testutils.test
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class LoginWithClassicPresenterTest {
|
||||||
|
@get:Rule
|
||||||
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - initial state - feature disabled - start is not invoked`() = runTest {
|
||||||
|
val presenter = createPresenter(
|
||||||
|
elementClassicConnection = FakeElementClassicConnection(
|
||||||
|
startResult = {
|
||||||
|
error("start should not be invoked when feature is disabled")
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.canLoginWithClassic).isFalse()
|
||||||
|
assertThat(initialState.loginWithClassicAction.isUninitialized()).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - feature enabled - start is invoked`() = runTest {
|
||||||
|
val startResult = lambdaRecorder<Unit> {}
|
||||||
|
val presenter = createPresenter(
|
||||||
|
elementClassicConnection = FakeElementClassicConnection(
|
||||||
|
startResult = startResult,
|
||||||
|
),
|
||||||
|
isFeatureEnabled = true,
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.canLoginWithClassic).isFalse()
|
||||||
|
assertThat(initialState.loginWithClassicAction.isUninitialized()).isTrue()
|
||||||
|
val finalState = awaitItem()
|
||||||
|
assertThat(finalState.canLoginWithClassic).isFalse()
|
||||||
|
}
|
||||||
|
startResult.assertions().isCalledOnce()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - emit request data invokes the expected method`() = runTest {
|
||||||
|
val requestDataResult = lambdaRecorder<Unit> {}
|
||||||
|
val presenter = createPresenter(
|
||||||
|
elementClassicConnection = FakeElementClassicConnection(
|
||||||
|
startResult = {},
|
||||||
|
requestDataResult = requestDataResult,
|
||||||
|
),
|
||||||
|
isFeatureEnabled = true,
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.canLoginWithClassic).isFalse()
|
||||||
|
assertThat(initialState.loginWithClassicAction.isUninitialized()).isTrue()
|
||||||
|
val nextState = awaitItem()
|
||||||
|
assertThat(nextState.canLoginWithClassic).isFalse()
|
||||||
|
nextState.eventSink(LoginWithClassicEvent.RefreshData)
|
||||||
|
}
|
||||||
|
requestDataResult.assertions().isCalledOnce()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - start login with wrong state emits an error`() = runTest {
|
||||||
|
val presenter = createPresenter(
|
||||||
|
elementClassicConnection = FakeElementClassicConnection(
|
||||||
|
startResult = {},
|
||||||
|
),
|
||||||
|
isFeatureEnabled = true,
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(1)
|
||||||
|
val state = awaitItem()
|
||||||
|
state.eventSink(LoginWithClassicEvent.StartLoginWithClassic)
|
||||||
|
val errorState = awaitItem()
|
||||||
|
assertThat(errorState.loginWithClassicAction.isFailure()).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - start login with correct state - user cancel`() = runTest {
|
||||||
|
val elementClassicConnection = FakeElementClassicConnection(
|
||||||
|
startResult = {},
|
||||||
|
)
|
||||||
|
val presenter = createPresenter(
|
||||||
|
elementClassicConnection = elementClassicConnection,
|
||||||
|
isFeatureEnabled = true,
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(2)
|
||||||
|
elementClassicConnection.emitState(
|
||||||
|
ElementClassicConnectionState.ElementClassicReady(userId = A_USER_ID)
|
||||||
|
)
|
||||||
|
val readyState = awaitItem()
|
||||||
|
assertThat(readyState.canLoginWithClassic).isTrue()
|
||||||
|
readyState.eventSink(LoginWithClassicEvent.StartLoginWithClassic)
|
||||||
|
val confirmingState = awaitItem()
|
||||||
|
assertThat(confirmingState.loginWithClassicAction.isConfirming()).isTrue()
|
||||||
|
assertThat((confirmingState.loginWithClassicAction as ConfirmingLoginWithElementClassic).userId).isEqualTo(A_USER_ID)
|
||||||
|
confirmingState.eventSink(LoginWithClassicEvent.CloseDialog)
|
||||||
|
val finalState = awaitItem()
|
||||||
|
assertThat(finalState.loginWithClassicAction.isUninitialized()).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - start login with correct state - user confirms`() = runTest {
|
||||||
|
val elementClassicConnection = FakeElementClassicConnection(
|
||||||
|
startResult = {},
|
||||||
|
)
|
||||||
|
val presenter = createPresenter(
|
||||||
|
elementClassicConnection = elementClassicConnection,
|
||||||
|
isFeatureEnabled = true,
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(2)
|
||||||
|
elementClassicConnection.emitState(
|
||||||
|
ElementClassicConnectionState.ElementClassicReady(userId = A_USER_ID)
|
||||||
|
)
|
||||||
|
val readyState = awaitItem()
|
||||||
|
assertThat(readyState.canLoginWithClassic).isTrue()
|
||||||
|
readyState.eventSink(LoginWithClassicEvent.StartLoginWithClassic)
|
||||||
|
val confirmingState = awaitItem()
|
||||||
|
assertThat(confirmingState.loginWithClassicAction.isConfirming()).isTrue()
|
||||||
|
assertThat((confirmingState.loginWithClassicAction as ConfirmingLoginWithElementClassic).userId).isEqualTo(A_USER_ID)
|
||||||
|
confirmingState.eventSink(LoginWithClassicEvent.DoLoginWithClassic)
|
||||||
|
val loadingState = awaitItem()
|
||||||
|
assertThat(loadingState.loginWithClassicAction.isLoading()).isTrue()
|
||||||
|
val finalState = awaitItem()
|
||||||
|
assertThat(finalState.loginWithClassicAction.isSuccess()).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - cannot sign in if a session with the same account already exists`() = runTest {
|
||||||
|
val elementClassicConnection = FakeElementClassicConnection(
|
||||||
|
startResult = {},
|
||||||
|
)
|
||||||
|
val presenter = createPresenter(
|
||||||
|
elementClassicConnection = elementClassicConnection,
|
||||||
|
isFeatureEnabled = true,
|
||||||
|
sessionStore = InMemorySessionStore(
|
||||||
|
initialList = listOf(
|
||||||
|
aSessionData(
|
||||||
|
sessionId = A_USER_ID.value,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(2)
|
||||||
|
elementClassicConnection.emitState(
|
||||||
|
ElementClassicConnectionState.ElementClassicReady(userId = A_USER_ID)
|
||||||
|
)
|
||||||
|
// No new item, because canLoginWithClassic is still false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - cannot sign in if the feature is disabled`() = runTest {
|
||||||
|
val elementClassicConnection = FakeElementClassicConnection()
|
||||||
|
val presenter = createPresenter(
|
||||||
|
elementClassicConnection = elementClassicConnection,
|
||||||
|
isFeatureEnabled = false,
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(1)
|
||||||
|
// Note: it should not happen IRL
|
||||||
|
elementClassicConnection.emitState(
|
||||||
|
ElementClassicConnectionState.ElementClassicReady(userId = A_USER_ID)
|
||||||
|
)
|
||||||
|
// No new item, because canLoginWithClassic is still false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createPresenter(
|
||||||
|
elementClassicConnection: ElementClassicConnection = FakeElementClassicConnection(),
|
||||||
|
sessionStore: SessionStore = InMemorySessionStore(),
|
||||||
|
isFeatureEnabled: Boolean = false,
|
||||||
|
featureFlagService: FeatureFlagService = FakeFeatureFlagService(
|
||||||
|
initialState = mapOf(FeatureFlags.SignInWithClassic.key to isFeatureEnabled)
|
||||||
|
),
|
||||||
|
) = LoginWithClassicPresenter(
|
||||||
|
elementClassicConnection = elementClassicConnection,
|
||||||
|
sessionStore = sessionStore,
|
||||||
|
featureFlagService = featureFlagService,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.migration.impl.migrations
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.AppScope
|
||||||
|
import dev.zacsweers.metro.ContributesIntoSet
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure we clear the well-known cached config, since it could be invalid due to an SDK issue.
|
||||||
|
*/
|
||||||
|
@ContributesIntoSet(AppScope::class)
|
||||||
|
@Inject
|
||||||
|
class AppMigration09(
|
||||||
|
private val sessionStore: SessionStore,
|
||||||
|
private val matrixClientProvider: MatrixClientProvider,
|
||||||
|
) : AppMigration {
|
||||||
|
override val order: Int = 9
|
||||||
|
|
||||||
|
override suspend fun migrate(isFreshInstall: Boolean) {
|
||||||
|
if (isFreshInstall) return
|
||||||
|
|
||||||
|
val sessions = sessionStore.getAllSessions()
|
||||||
|
|
||||||
|
for (session in sessions) {
|
||||||
|
val client = matrixClientProvider.getOrRestore(SessionId(session.userId)).getOrNull() ?: continue
|
||||||
|
client.resetWellKnownConfig()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.migration.impl.migrations
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
|
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
||||||
|
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||||
|
import io.element.android.libraries.sessionstorage.test.aSessionData
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class AppMigration09Test {
|
||||||
|
@Test
|
||||||
|
fun `migration on fresh install does nothing`() = runTest {
|
||||||
|
val sessionStore = InMemorySessionStore(initialList = listOf(aSessionData()))
|
||||||
|
val getClientLambda = lambdaRecorder<SessionId, Result<MatrixClient>> { Result.success(FakeMatrixClient()) }
|
||||||
|
val clientProvider = FakeMatrixClientProvider(getClient = getClientLambda)
|
||||||
|
val migration = AppMigration09(sessionStore, clientProvider)
|
||||||
|
migration.migrate(isFreshInstall = true)
|
||||||
|
|
||||||
|
getClientLambda.assertions().isNeverCalled()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `migration on upgrade should invoke the resetWellKnownConfig method`() = runTest {
|
||||||
|
val sessionStore = InMemorySessionStore(initialList = listOf(aSessionData()))
|
||||||
|
val resetWellKnownLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
|
||||||
|
val getClientLambda = lambdaRecorder<SessionId, Result<MatrixClient>> {
|
||||||
|
Result.success(FakeMatrixClient(resetWellKnownConfigLambda = resetWellKnownLambda))
|
||||||
|
}
|
||||||
|
val clientProvider = FakeMatrixClientProvider(getClient = getClientLambda)
|
||||||
|
val migration = AppMigration09(sessionStore, clientProvider)
|
||||||
|
migration.migrate(isFreshInstall = false)
|
||||||
|
|
||||||
|
getClientLambda.assertions().isCalledOnce()
|
||||||
|
resetWellKnownLambda.assertions().isCalledOnce()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -158,6 +158,7 @@ class DefaultBugReporter(
|
||||||
}
|
}
|
||||||
if (withCrashLogs || withDevicesLogs) {
|
if (withCrashLogs || withDevicesLogs) {
|
||||||
saveLogCat()
|
saveLogCat()
|
||||||
|
?.takeIf { it.length() < RageshakeConfig.MAX_LOG_CONTENT_SIZE }
|
||||||
?.let { logCatFile ->
|
?.let { logCatFile ->
|
||||||
compressFile(logCatFile).also {
|
compressFile(logCatFile).also {
|
||||||
logCatFile.safeDelete()
|
logCatFile.safeDelete()
|
||||||
|
|
@ -191,6 +192,7 @@ class DefaultBugReporter(
|
||||||
.addFormDataPart("label", buildMeta.versionName)
|
.addFormDataPart("label", buildMeta.versionName)
|
||||||
.addFormDataPart("label", buildMeta.flavorDescription)
|
.addFormDataPart("label", buildMeta.flavorDescription)
|
||||||
.addFormDataPart("branch_name", buildMeta.gitBranchName)
|
.addFormDataPart("branch_name", buildMeta.gitBranchName)
|
||||||
|
|
||||||
userId?.let {
|
userId?.let {
|
||||||
matrixClientProvider.getOrNull(it)?.let { client ->
|
matrixClientProvider.getOrNull(it)?.let { client ->
|
||||||
val curveKey = client.encryptionService.deviceCurve25519()
|
val curveKey = client.encryptionService.deviceCurve25519()
|
||||||
|
|
@ -390,7 +392,11 @@ class DefaultBugReporter(
|
||||||
) {
|
) {
|
||||||
val logDirectory = logDirectory()
|
val logDirectory = logDirectory()
|
||||||
logDirectory.listFiles()
|
logDirectory.listFiles()
|
||||||
?.filter { it.isFile && !it.name.endsWith(LOG_CAT_FILENAME) }
|
?.filter {
|
||||||
|
it.isFile &&
|
||||||
|
!it.name.endsWith(LOG_CAT_FILENAME) &&
|
||||||
|
it.length() < RageshakeConfig.MAX_LOG_CONTENT_SIZE
|
||||||
|
}
|
||||||
}.orEmpty()
|
}.orEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
package io.element.android.features.space.impl.root
|
package io.element.android.features.space.impl.root
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||||
|
|
||||||
sealed interface SpaceEvents {
|
sealed interface SpaceEvents {
|
||||||
|
|
@ -19,4 +20,12 @@ sealed interface SpaceEvents {
|
||||||
|
|
||||||
data class ShowTopicViewer(val topic: String) : SpaceEvents
|
data class ShowTopicViewer(val topic: String) : SpaceEvents
|
||||||
data object HideTopicViewer : SpaceEvents
|
data object HideTopicViewer : SpaceEvents
|
||||||
|
|
||||||
|
// Manage mode events
|
||||||
|
data object EnterManageMode : SpaceEvents
|
||||||
|
data object ExitManageMode : SpaceEvents
|
||||||
|
data class ToggleRoomSelection(val roomId: RoomId) : SpaceEvents
|
||||||
|
data object ConfirmRoomRemoval : SpaceEvents
|
||||||
|
data object RemoveSelectedRooms : SpaceEvents
|
||||||
|
data object ClearRemoveAction : SpaceEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.space.impl.root
|
||||||
|
|
||||||
|
import io.element.android.features.space.impl.settings.SpaceSettingsPermissions
|
||||||
|
import io.element.android.features.space.impl.settings.spaceSettingsPermissions
|
||||||
|
import io.element.android.libraries.matrix.api.room.StateEventType
|
||||||
|
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPermissions
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permissions needed for different actions in the Space screen.
|
||||||
|
* @param settingsPermissions Permissions related to space settings.
|
||||||
|
* @param canEditSpaceGraph Whether the user can edit the space graph (add/remove children).
|
||||||
|
*/
|
||||||
|
data class SpacePermissions(
|
||||||
|
val settingsPermissions: SpaceSettingsPermissions,
|
||||||
|
val canEditSpaceGraph: Boolean,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
val DEFAULT = SpacePermissions(
|
||||||
|
settingsPermissions = SpaceSettingsPermissions.DEFAULT,
|
||||||
|
canEditSpaceGraph = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun RoomPermissions.spacePermissions(): SpacePermissions {
|
||||||
|
return SpacePermissions(
|
||||||
|
settingsPermissions = spaceSettingsPermissions(),
|
||||||
|
canEditSpaceGraph = canOwnUserSendState(StateEventType.SpaceChild) || canOwnUserSendState(StateEventType.SpaceParent),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -23,8 +23,6 @@ import io.element.android.features.invite.api.SeenInvitesStore
|
||||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
|
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
|
||||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
|
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
|
||||||
import io.element.android.features.invite.api.toInviteData
|
import io.element.android.features.invite.api.toInviteData
|
||||||
import io.element.android.features.space.impl.settings.SpaceSettingsPermissions
|
|
||||||
import io.element.android.features.space.impl.settings.spaceSettingsPermissions
|
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.coroutine.mapState
|
import io.element.android.libraries.core.coroutine.mapState
|
||||||
|
|
@ -40,6 +38,7 @@ import io.element.android.libraries.matrix.api.room.join.JoinRoom
|
||||||
import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
|
import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
|
||||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
||||||
|
import io.element.android.libraries.matrix.api.spaces.SpaceService
|
||||||
import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar
|
import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
@ -48,9 +47,10 @@ import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableMap
|
import kotlinx.collections.immutable.toImmutableMap
|
||||||
import kotlinx.collections.immutable.toImmutableSet
|
import kotlinx.collections.immutable.toImmutableSet
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.jvm.optionals.getOrNull
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
class SpacePresenter(
|
class SpacePresenter(
|
||||||
|
|
@ -62,6 +62,7 @@ class SpacePresenter(
|
||||||
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
||||||
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
|
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
|
private val spaceService: SpaceService,
|
||||||
) : Presenter<SpaceState> {
|
) : Presenter<SpaceState> {
|
||||||
private var children by mutableStateOf<ImmutableList<SpaceRoom>>(persistentListOf())
|
private var children by mutableStateOf<ImmutableList<SpaceRoom>>(persistentListOf())
|
||||||
|
|
||||||
|
|
@ -88,8 +89,8 @@ class SpacePresenter(
|
||||||
}
|
}
|
||||||
}.collectAsState()
|
}.collectAsState()
|
||||||
|
|
||||||
val permissions by room.permissionsAsState(SpaceSettingsPermissions.DEFAULT) { perms ->
|
val permissions by room.permissionsAsState(SpacePermissions.DEFAULT) { perms ->
|
||||||
perms.spaceSettingsPermissions()
|
perms.spacePermissions()
|
||||||
}
|
}
|
||||||
val isSpaceSettingsEnabled by remember {
|
val isSpaceSettingsEnabled by remember {
|
||||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.SpaceSettings)
|
featureFlagService.isFeatureEnabledFlow(FeatureFlags.SpaceSettings)
|
||||||
|
|
@ -97,13 +98,37 @@ class SpacePresenter(
|
||||||
|
|
||||||
val roomInfo by room.roomInfoFlow.collectAsState()
|
val roomInfo by room.roomInfoFlow.collectAsState()
|
||||||
val canAccessSpaceSettings by remember {
|
val canAccessSpaceSettings by remember {
|
||||||
derivedStateOf { isSpaceSettingsEnabled && permissions.hasAny(roomInfo.joinRule) }
|
derivedStateOf { isSpaceSettingsEnabled && permissions.settingsPermissions.hasAny(roomInfo.joinRule) }
|
||||||
|
}
|
||||||
|
val canEditSpaceGraph by remember {
|
||||||
|
derivedStateOf { isSpaceSettingsEnabled && permissions.canEditSpaceGraph }
|
||||||
}
|
}
|
||||||
val currentSpace by spaceRoomList.currentSpaceFlow.collectAsState()
|
|
||||||
val (joinActions, setJoinActions) = remember { mutableStateOf(emptyMap<RoomId, AsyncAction<Unit>>()) }
|
val (joinActions, setJoinActions) = remember { mutableStateOf(emptyMap<RoomId, AsyncAction<Unit>>()) }
|
||||||
|
|
||||||
var topicViewerState: TopicViewerState by remember { mutableStateOf(TopicViewerState.Hidden) }
|
var topicViewerState: TopicViewerState by remember { mutableStateOf(TopicViewerState.Hidden) }
|
||||||
|
|
||||||
|
// Manage mode state
|
||||||
|
var isManageMode by remember { mutableStateOf(false) }
|
||||||
|
var selectedRoomIds by remember { mutableStateOf<Set<RoomId>>(emptySet()) }
|
||||||
|
var removeRoomsAction by remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||||
|
var removedRoomIds by remember { mutableStateOf<Set<RoomId>>(emptySet()) }
|
||||||
|
|
||||||
|
val filteredChildren by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
children
|
||||||
|
.filterNot { it.roomId in removedRoomIds }
|
||||||
|
.let { list ->
|
||||||
|
if (isManageMode) {
|
||||||
|
// In manage mode, only show rooms (not spaces)
|
||||||
|
list.filter { !it.isSpace }
|
||||||
|
} else {
|
||||||
|
list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toImmutableList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(children) {
|
LaunchedEffect(children) {
|
||||||
// Remove joined children from the join actions
|
// Remove joined children from the join actions
|
||||||
val joinedChildren = children
|
val joinedChildren = children
|
||||||
|
|
@ -138,11 +163,60 @@ class SpacePresenter(
|
||||||
}
|
}
|
||||||
SpaceEvents.HideTopicViewer -> topicViewerState = TopicViewerState.Hidden
|
SpaceEvents.HideTopicViewer -> topicViewerState = TopicViewerState.Hidden
|
||||||
is SpaceEvents.ShowTopicViewer -> topicViewerState = TopicViewerState.Shown(event.topic)
|
is SpaceEvents.ShowTopicViewer -> topicViewerState = TopicViewerState.Shown(event.topic)
|
||||||
|
|
||||||
|
// Manage mode events
|
||||||
|
SpaceEvents.EnterManageMode -> {
|
||||||
|
isManageMode = true
|
||||||
|
selectedRoomIds = emptySet()
|
||||||
|
}
|
||||||
|
SpaceEvents.ExitManageMode -> {
|
||||||
|
isManageMode = false
|
||||||
|
selectedRoomIds = emptySet()
|
||||||
|
}
|
||||||
|
is SpaceEvents.ToggleRoomSelection -> {
|
||||||
|
selectedRoomIds = if (event.roomId in selectedRoomIds) {
|
||||||
|
selectedRoomIds - event.roomId
|
||||||
|
} else {
|
||||||
|
selectedRoomIds + event.roomId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SpaceEvents.RemoveSelectedRooms -> {
|
||||||
|
removeRoomsAction = AsyncAction.ConfirmingNoParams
|
||||||
|
}
|
||||||
|
SpaceEvents.ConfirmRoomRemoval -> {
|
||||||
|
localCoroutineScope.launch {
|
||||||
|
removeRoomsAction = AsyncAction.Loading
|
||||||
|
val spaceId = spaceRoomList.roomId
|
||||||
|
val roomsToRemove = selectedRoomIds.toSet()
|
||||||
|
val successfullyRemoved = mutableSetOf<RoomId>()
|
||||||
|
val results = roomsToRemove.map { roomId ->
|
||||||
|
async {
|
||||||
|
spaceService.removeChildFromSpace(spaceId, roomId)
|
||||||
|
.onSuccess { successfullyRemoved.add(roomId) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results.awaitAll()
|
||||||
|
if (successfullyRemoved.isNotEmpty()) {
|
||||||
|
removedRoomIds = removedRoomIds + successfullyRemoved
|
||||||
|
}
|
||||||
|
val hasError = successfullyRemoved.size < roomsToRemove.size
|
||||||
|
if (hasError) {
|
||||||
|
removeRoomsAction = AsyncAction.Failure(Exception("Failed to remove some rooms"))
|
||||||
|
} else {
|
||||||
|
removeRoomsAction = AsyncAction.Success(Unit)
|
||||||
|
isManageMode = false
|
||||||
|
selectedRoomIds = emptySet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SpaceEvents.ClearRemoveAction -> {
|
||||||
|
removeRoomsAction = AsyncAction.Uninitialized
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return SpaceState(
|
return SpaceState(
|
||||||
currentSpace = currentSpace.getOrNull(),
|
spaceInfo = roomInfo,
|
||||||
children = children,
|
children = filteredChildren,
|
||||||
seenSpaceInvites = seenSpaceInvites,
|
seenSpaceInvites = seenSpaceInvites,
|
||||||
hideInvitesAvatar = hideInvitesAvatar,
|
hideInvitesAvatar = hideInvitesAvatar,
|
||||||
hasMoreToLoad = hasMoreToLoad,
|
hasMoreToLoad = hasMoreToLoad,
|
||||||
|
|
@ -150,6 +224,10 @@ class SpacePresenter(
|
||||||
acceptDeclineInviteState = acceptDeclineInviteState,
|
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||||
topicViewerState = topicViewerState,
|
topicViewerState = topicViewerState,
|
||||||
canAccessSpaceSettings = canAccessSpaceSettings,
|
canAccessSpaceSettings = canAccessSpaceSettings,
|
||||||
|
isManageMode = isManageMode,
|
||||||
|
selectedRoomIds = selectedRoomIds.toImmutableSet(),
|
||||||
|
canEditSpaceGraph = canEditSpaceGraph,
|
||||||
|
removeRoomsAction = removeRoomsAction,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,14 @@ import androidx.compose.runtime.Immutable
|
||||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
|
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomInfo
|
||||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.ImmutableMap
|
import kotlinx.collections.immutable.ImmutableMap
|
||||||
import kotlinx.collections.immutable.ImmutableSet
|
import kotlinx.collections.immutable.ImmutableSet
|
||||||
|
|
||||||
data class SpaceState(
|
data class SpaceState(
|
||||||
val currentSpace: SpaceRoom?,
|
val spaceInfo: RoomInfo,
|
||||||
val children: ImmutableList<SpaceRoom>,
|
val children: ImmutableList<SpaceRoom>,
|
||||||
val seenSpaceInvites: ImmutableSet<RoomId>,
|
val seenSpaceInvites: ImmutableSet<RoomId>,
|
||||||
val hideInvitesAvatar: Boolean,
|
val hideInvitesAvatar: Boolean,
|
||||||
|
|
@ -27,12 +28,21 @@ data class SpaceState(
|
||||||
val acceptDeclineInviteState: AcceptDeclineInviteState,
|
val acceptDeclineInviteState: AcceptDeclineInviteState,
|
||||||
val topicViewerState: TopicViewerState,
|
val topicViewerState: TopicViewerState,
|
||||||
val canAccessSpaceSettings: Boolean,
|
val canAccessSpaceSettings: Boolean,
|
||||||
|
val isManageMode: Boolean,
|
||||||
|
val selectedRoomIds: ImmutableSet<RoomId>,
|
||||||
|
val canEditSpaceGraph: Boolean,
|
||||||
|
val removeRoomsAction: AsyncAction<Unit>,
|
||||||
val eventSink: (SpaceEvents) -> Unit
|
val eventSink: (SpaceEvents) -> Unit
|
||||||
) {
|
) {
|
||||||
fun isJoining(spaceId: RoomId): Boolean = joinActions[spaceId] == AsyncAction.Loading
|
fun isJoining(spaceId: RoomId): Boolean = joinActions[spaceId] == AsyncAction.Loading
|
||||||
val hasAnyFailure: Boolean = joinActions.values.any {
|
fun isSelected(spaceId: RoomId): Boolean = selectedRoomIds.contains(spaceId)
|
||||||
|
val hasAnyJoinFailures: Boolean = joinActions.values.any {
|
||||||
it is AsyncAction.Failure
|
it is AsyncAction.Failure
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val showManageRoomsAction: Boolean = canEditSpaceGraph && children.any { spaceRoom -> !spaceRoom.isSpace }
|
||||||
|
val selectedCount: Int = selectedRoomIds.size
|
||||||
|
val isRemoveButtonEnabled: Boolean = selectedRoomIds.isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInvit
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomInfo
|
||||||
|
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
|
||||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||||
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||||
|
|
@ -27,11 +29,11 @@ open class SpaceStateProvider : PreviewParameterProvider<SpaceState> {
|
||||||
override val values: Sequence<SpaceState>
|
override val values: Sequence<SpaceState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aSpaceState(),
|
aSpaceState(),
|
||||||
aSpaceState(parentSpace = aParentSpace(joinRule = JoinRule.Public)),
|
aSpaceState(spaceInfo = aSpaceInfo(joinRule = JoinRule.Public)),
|
||||||
aSpaceState(parentSpace = aParentSpace(joinRule = JoinRule.Restricted(persistentListOf()))),
|
aSpaceState(spaceInfo = aSpaceInfo(joinRule = JoinRule.Restricted(persistentListOf()))),
|
||||||
aSpaceState(children = aListOfSpaceRooms()),
|
aSpaceState(children = aListOfSpaceRooms()),
|
||||||
aSpaceState(
|
aSpaceState(
|
||||||
parentSpace = aParentSpace(),
|
spaceInfo = aSpaceInfo(),
|
||||||
children = aListOfSpaceRooms(),
|
children = aListOfSpaceRooms(),
|
||||||
joiningRooms = setOf(RoomId("!spaceId0:example.com")),
|
joiningRooms = setOf(RoomId("!spaceId0:example.com")),
|
||||||
hasMoreToLoad = false
|
hasMoreToLoad = false
|
||||||
|
|
@ -39,12 +41,31 @@ open class SpaceStateProvider : PreviewParameterProvider<SpaceState> {
|
||||||
aSpaceState(
|
aSpaceState(
|
||||||
topicViewerState = TopicViewerState.Shown(topic = "Space description goes here." + LoremIpsum(20).values.first()),
|
topicViewerState = TopicViewerState.Shown(topic = "Space description goes here." + LoremIpsum(20).values.first()),
|
||||||
),
|
),
|
||||||
// Add other states here
|
// Manage mode states
|
||||||
|
aSpaceState(
|
||||||
|
spaceInfo = aSpaceInfo(),
|
||||||
|
children = aListOfSpaceRooms(),
|
||||||
|
isManageMode = true,
|
||||||
|
selectedRoomIds = emptySet(),
|
||||||
|
),
|
||||||
|
aSpaceState(
|
||||||
|
spaceInfo = aSpaceInfo(),
|
||||||
|
children = aListOfSpaceRooms(),
|
||||||
|
isManageMode = true,
|
||||||
|
selectedRoomIds = setOf(RoomId("!spaceId0:example.com"), RoomId("!spaceId1:example.com")),
|
||||||
|
),
|
||||||
|
aSpaceState(
|
||||||
|
spaceInfo = aSpaceInfo(),
|
||||||
|
children = aListOfSpaceRooms(),
|
||||||
|
isManageMode = true,
|
||||||
|
selectedRoomIds = setOf(RoomId("!spaceId0:example.com")),
|
||||||
|
removeRoomsAction = AsyncAction.ConfirmingNoParams,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aSpaceState(
|
fun aSpaceState(
|
||||||
parentSpace: SpaceRoom? = aParentSpace(),
|
spaceInfo: RoomInfo = aSpaceInfo(),
|
||||||
children: List<SpaceRoom> = emptyList(),
|
children: List<SpaceRoom> = emptyList(),
|
||||||
seenSpaceInvites: Set<RoomId> = emptySet(),
|
seenSpaceInvites: Set<RoomId> = emptySet(),
|
||||||
joiningRooms: Set<RoomId> = emptySet(),
|
joiningRooms: Set<RoomId> = emptySet(),
|
||||||
|
|
@ -54,9 +75,13 @@ fun aSpaceState(
|
||||||
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
|
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
|
||||||
topicViewerState: TopicViewerState = TopicViewerState.Hidden,
|
topicViewerState: TopicViewerState = TopicViewerState.Hidden,
|
||||||
canAccessSpaceSettings: Boolean = true,
|
canAccessSpaceSettings: Boolean = true,
|
||||||
|
isManageMode: Boolean = false,
|
||||||
|
selectedRoomIds: Set<RoomId> = emptySet(),
|
||||||
|
canManageRooms: Boolean = true,
|
||||||
|
removeRoomsAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
eventSink: (SpaceEvents) -> Unit = { },
|
eventSink: (SpaceEvents) -> Unit = { },
|
||||||
) = SpaceState(
|
) = SpaceState(
|
||||||
currentSpace = parentSpace,
|
spaceInfo = spaceInfo,
|
||||||
children = children.toImmutableList(),
|
children = children.toImmutableList(),
|
||||||
seenSpaceInvites = seenSpaceInvites.toImmutableSet(),
|
seenSpaceInvites = seenSpaceInvites.toImmutableSet(),
|
||||||
hideInvitesAvatar = hideInvitesAvatar,
|
hideInvitesAvatar = hideInvitesAvatar,
|
||||||
|
|
@ -65,19 +90,52 @@ fun aSpaceState(
|
||||||
acceptDeclineInviteState = acceptDeclineInviteState,
|
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||||
topicViewerState = topicViewerState,
|
topicViewerState = topicViewerState,
|
||||||
canAccessSpaceSettings = canAccessSpaceSettings,
|
canAccessSpaceSettings = canAccessSpaceSettings,
|
||||||
|
isManageMode = isManageMode,
|
||||||
|
selectedRoomIds = selectedRoomIds.toImmutableSet(),
|
||||||
|
canEditSpaceGraph = canManageRooms,
|
||||||
|
removeRoomsAction = removeRoomsAction,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun aParentSpace(
|
private fun aSpaceInfo(
|
||||||
joinRule: JoinRule? = null,
|
joinRule: JoinRule? = null,
|
||||||
): SpaceRoom {
|
): RoomInfo {
|
||||||
return aSpaceRoom(
|
return RoomInfo(
|
||||||
numJoinedMembers = 5,
|
id = RoomId("!spaceId0:example.com"),
|
||||||
childrenCount = 10,
|
name = "A Space",
|
||||||
worldReadable = true,
|
rawName = "A Space",
|
||||||
joinRule = joinRule,
|
|
||||||
roomId = RoomId("!spaceId0:example.com"),
|
|
||||||
topic = "Space description goes here. " + LoremIpsum(20).values.first(),
|
topic = "Space description goes here. " + LoremIpsum(20).values.first(),
|
||||||
|
avatarUrl = null,
|
||||||
|
isPublic = true,
|
||||||
|
isDirect = false,
|
||||||
|
isEncrypted = false,
|
||||||
|
joinRule = joinRule,
|
||||||
|
isSpace = true,
|
||||||
|
isFavorite = false,
|
||||||
|
canonicalAlias = null,
|
||||||
|
alternativeAliases = persistentListOf(),
|
||||||
|
currentUserMembership = CurrentUserMembership.JOINED,
|
||||||
|
inviter = null,
|
||||||
|
activeMembersCount = 5,
|
||||||
|
invitedMembersCount = 0,
|
||||||
|
joinedMembersCount = 5,
|
||||||
|
roomPowerLevels = null,
|
||||||
|
highlightCount = 0,
|
||||||
|
notificationCount = 0,
|
||||||
|
userDefinedNotificationMode = null,
|
||||||
|
hasRoomCall = false,
|
||||||
|
activeRoomCallParticipants = persistentListOf(),
|
||||||
|
isMarkedUnread = false,
|
||||||
|
numUnreadMessages = 0,
|
||||||
|
numUnreadNotifications = 0,
|
||||||
|
numUnreadMentions = 0,
|
||||||
|
heroes = persistentListOf(),
|
||||||
|
pinnedEventIds = persistentListOf(),
|
||||||
|
creators = persistentListOf(),
|
||||||
|
historyVisibility = RoomHistoryVisibility.Joined,
|
||||||
|
successorRoom = null,
|
||||||
|
roomVersion = "11",
|
||||||
|
privilegedCreatorRole = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,17 @@
|
||||||
|
|
||||||
package io.element.android.features.space.impl.root
|
package io.element.android.features.space.impl.root
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.expandVertically
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
|
@ -31,6 +38,7 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.heading
|
import androidx.compose.ui.semantics.heading
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
|
|
@ -40,9 +48,12 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
|
import io.element.android.features.space.impl.R
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.InviteButtonsRowMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.InviteButtonsRowMolecule
|
||||||
import io.element.android.libraries.designsystem.components.ClickableLinkText
|
import io.element.android.libraries.designsystem.components.ClickableLinkText
|
||||||
import io.element.android.libraries.designsystem.components.SimpleModalBottomSheet
|
import io.element.android.libraries.designsystem.components.SimpleModalBottomSheet
|
||||||
|
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||||
import io.element.android.libraries.designsystem.components.async.AsyncIndicator
|
import io.element.android.libraries.designsystem.components.async.AsyncIndicator
|
||||||
import io.element.android.libraries.designsystem.components.async.AsyncIndicatorHost
|
import io.element.android.libraries.designsystem.components.async.AsyncIndicatorHost
|
||||||
import io.element.android.libraries.designsystem.components.async.rememberAsyncIndicatorState
|
import io.element.android.libraries.designsystem.components.async.rememberAsyncIndicatorState
|
||||||
|
|
@ -51,8 +62,10 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarType
|
import io.element.android.libraries.designsystem.components.avatar.AvatarType
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
|
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Checkbox
|
||||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||||
import io.element.android.libraries.designsystem.theme.components.DropdownMenu
|
import io.element.android.libraries.designsystem.theme.components.DropdownMenu
|
||||||
import io.element.android.libraries.designsystem.theme.components.DropdownMenuItem
|
import io.element.android.libraries.designsystem.theme.components.DropdownMenuItem
|
||||||
|
|
@ -61,15 +74,18 @@ import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomInfo
|
||||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||||
|
import io.element.android.libraries.matrix.api.spaces.SpaceRoomVisibility
|
||||||
import io.element.android.libraries.matrix.ui.components.JoinButton
|
import io.element.android.libraries.matrix.ui.components.JoinButton
|
||||||
import io.element.android.libraries.matrix.ui.components.SpaceHeaderView
|
import io.element.android.libraries.matrix.ui.components.SpaceHeaderView
|
||||||
import io.element.android.libraries.matrix.ui.components.SpaceRoomItemView
|
import io.element.android.libraries.matrix.ui.components.SpaceRoomItemView
|
||||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
|
import io.element.android.libraries.ui.strings.CommonPlurals
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
|
@ -85,18 +101,48 @@ fun SpaceView(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
acceptDeclineInviteView: @Composable () -> Unit,
|
acceptDeclineInviteView: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
BackHandler {
|
||||||
|
if (state.isManageMode) {
|
||||||
|
state.eventSink(SpaceEvents.ExitManageMode)
|
||||||
|
} else {
|
||||||
|
onBackClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
topBar = {
|
topBar = {
|
||||||
SpaceViewTopBar(
|
Box {
|
||||||
currentSpace = state.currentSpace,
|
AnimatedVisibility(
|
||||||
canAccessSpaceSettings = state.canAccessSpaceSettings,
|
visible = state.isManageMode,
|
||||||
onBackClick = onBackClick,
|
enter = fadeIn(),
|
||||||
onLeaveSpaceClick = onLeaveSpaceClick,
|
exit = fadeOut()
|
||||||
onShareSpace = onShareSpace,
|
) {
|
||||||
onSettingsClick = onSettingsClick,
|
ManageModeTopBar(
|
||||||
onViewMembersClick = onViewMembersClick,
|
selectedCount = state.selectedCount,
|
||||||
)
|
isRemoveButtonEnabled = state.isRemoveButtonEnabled,
|
||||||
|
onCancelClick = { state.eventSink(SpaceEvents.ExitManageMode) },
|
||||||
|
onRemoveClick = { state.eventSink(SpaceEvents.RemoveSelectedRooms) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = !state.isManageMode,
|
||||||
|
enter = fadeIn(),
|
||||||
|
exit = fadeOut()
|
||||||
|
) {
|
||||||
|
SpaceViewTopBar(
|
||||||
|
spaceInfo = state.spaceInfo,
|
||||||
|
canAccessSpaceSettings = state.canAccessSpaceSettings,
|
||||||
|
showManageRoomsAction = state.showManageRoomsAction,
|
||||||
|
onBackClick = onBackClick,
|
||||||
|
onLeaveSpaceClick = onLeaveSpaceClick,
|
||||||
|
onSettingsClick = onSettingsClick,
|
||||||
|
onShareSpace = onShareSpace,
|
||||||
|
onViewMembersClick = onViewMembersClick,
|
||||||
|
onManageRoomsClick = { state.eventSink(SpaceEvents.EnterManageMode) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
content = { padding ->
|
content = { padding ->
|
||||||
Box(
|
Box(
|
||||||
|
|
@ -104,15 +150,28 @@ fun SpaceView(
|
||||||
) {
|
) {
|
||||||
SpaceViewContent(
|
SpaceViewContent(
|
||||||
state = state,
|
state = state,
|
||||||
onRoomClick = onRoomClick,
|
onRoomClick = { spaceRoom ->
|
||||||
|
if (state.isManageMode) {
|
||||||
|
state.eventSink(SpaceEvents.ToggleRoomSelection(spaceRoom.roomId))
|
||||||
|
} else {
|
||||||
|
onRoomClick(spaceRoom)
|
||||||
|
}
|
||||||
|
},
|
||||||
onTopicClick = { topic ->
|
onTopicClick = { topic ->
|
||||||
state.eventSink(SpaceEvents.ShowTopicViewer(topic))
|
state.eventSink(SpaceEvents.ShowTopicViewer(topic))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
JoinRoomFailureEffect(
|
JoinFailuresEffect(
|
||||||
hasAnyFailure = state.hasAnyFailure,
|
hasAnyFailure = state.hasAnyJoinFailures,
|
||||||
eventSink = state.eventSink
|
eventSink = state.eventSink
|
||||||
)
|
)
|
||||||
|
RemoveRoomsActionView(
|
||||||
|
spaceDisplayName = state.spaceInfo.name ?: state.spaceInfo.id.value,
|
||||||
|
removeRoomsAction = state.removeRoomsAction,
|
||||||
|
selectedCount = state.selectedCount,
|
||||||
|
onConfirm = { state.eventSink(SpaceEvents.ConfirmRoomRemoval) },
|
||||||
|
onDismiss = { state.eventSink(SpaceEvents.ClearRemoveAction) },
|
||||||
|
)
|
||||||
acceptDeclineInviteView()
|
acceptDeclineInviteView()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -128,7 +187,7 @@ fun SpaceView(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun JoinRoomFailureEffect(
|
private fun JoinFailuresEffect(
|
||||||
hasAnyFailure: Boolean,
|
hasAnyFailure: Boolean,
|
||||||
eventSink: (SpaceEvents) -> Unit,
|
eventSink: (SpaceEvents) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
|
@ -176,22 +235,26 @@ private fun SpaceViewContent(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
LazyColumn(modifier.fillMaxSize()) {
|
LazyColumn(modifier.fillMaxSize()) {
|
||||||
val currentSpace = state.currentSpace
|
val spaceInfo = state.spaceInfo
|
||||||
if (currentSpace != null) {
|
item(key = "space_header") {
|
||||||
item {
|
AnimatedVisibility(
|
||||||
SpaceHeaderView(
|
!state.isManageMode,
|
||||||
avatarData = currentSpace.getAvatarData(AvatarSize.SpaceHeader),
|
enter = fadeIn() + expandVertically(),
|
||||||
name = currentSpace.displayName,
|
exit = fadeOut() + shrinkVertically()
|
||||||
topic = currentSpace.topic,
|
) {
|
||||||
topicMaxLines = 2,
|
Column {
|
||||||
visibility = currentSpace.visibility,
|
SpaceHeaderView(
|
||||||
heroes = currentSpace.heroes.toImmutableList(),
|
avatarData = spaceInfo.getAvatarData(AvatarSize.SpaceHeader),
|
||||||
numberOfMembers = currentSpace.numJoinedMembers,
|
name = spaceInfo.name,
|
||||||
onTopicClick = onTopicClick
|
topic = spaceInfo.topic,
|
||||||
)
|
topicMaxLines = 2,
|
||||||
}
|
visibility = SpaceRoomVisibility.fromJoinRule(spaceInfo.joinRule),
|
||||||
item {
|
heroes = spaceInfo.heroes,
|
||||||
HorizontalDivider()
|
numberOfMembers = spaceInfo.joinedMembersCount.toInt(),
|
||||||
|
onTopicClick = onTopicClick
|
||||||
|
)
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
itemsIndexed(
|
itemsIndexed(
|
||||||
|
|
@ -200,9 +263,11 @@ private fun SpaceViewContent(
|
||||||
) { index, spaceRoom ->
|
) { index, spaceRoom ->
|
||||||
val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED
|
val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED
|
||||||
val isCurrentlyJoining = state.isJoining(spaceRoom.roomId)
|
val isCurrentlyJoining = state.isJoining(spaceRoom.roomId)
|
||||||
|
val isSelected = state.isSelected(spaceRoom.roomId)
|
||||||
|
val showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites && !state.isManageMode
|
||||||
SpaceRoomItemView(
|
SpaceRoomItemView(
|
||||||
spaceRoom = spaceRoom,
|
spaceRoom = spaceRoom,
|
||||||
showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites,
|
showUnreadIndicator = showUnreadIndicator,
|
||||||
hideAvatars = isInvitation && state.hideInvitesAvatar,
|
hideAvatars = isInvitation && state.hideInvitesAvatar,
|
||||||
onClick = {
|
onClick = {
|
||||||
onRoomClick(spaceRoom)
|
onRoomClick(spaceRoom)
|
||||||
|
|
@ -210,17 +275,30 @@ private fun SpaceViewContent(
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
// TODO
|
// TODO
|
||||||
},
|
},
|
||||||
trailingAction = spaceRoom.trailingAction(isCurrentlyJoining = isCurrentlyJoining) {
|
trailingAction = if (state.isManageMode) {
|
||||||
state.eventSink(SpaceEvents.Join(spaceRoom))
|
{
|
||||||
},
|
Checkbox(
|
||||||
bottomAction = spaceRoom.inviteButtons(
|
checked = isSelected,
|
||||||
onAcceptClick = {
|
onCheckedChange = null,
|
||||||
state.eventSink(SpaceEvents.AcceptInvite(spaceRoom))
|
)
|
||||||
},
|
|
||||||
onDeclineClick = {
|
|
||||||
state.eventSink(SpaceEvents.DeclineInvite(spaceRoom))
|
|
||||||
}
|
}
|
||||||
)
|
} else {
|
||||||
|
spaceRoom.trailingAction(isCurrentlyJoining = isCurrentlyJoining) {
|
||||||
|
state.eventSink(SpaceEvents.Join(spaceRoom))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bottomAction = if (state.isManageMode) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
spaceRoom.inviteButtons(
|
||||||
|
onAcceptClick = {
|
||||||
|
state.eventSink(SpaceEvents.AcceptInvite(spaceRoom))
|
||||||
|
},
|
||||||
|
onDeclineClick = {
|
||||||
|
state.eventSink(SpaceEvents.DeclineInvite(spaceRoom))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
if (index != state.children.lastIndex) {
|
if (index != state.children.lastIndex) {
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
|
|
@ -257,13 +335,15 @@ private fun LoadingMoreIndicator(
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun SpaceViewTopBar(
|
private fun SpaceViewTopBar(
|
||||||
currentSpace: SpaceRoom?,
|
spaceInfo: RoomInfo,
|
||||||
canAccessSpaceSettings: Boolean,
|
canAccessSpaceSettings: Boolean,
|
||||||
|
showManageRoomsAction: Boolean,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
onLeaveSpaceClick: () -> Unit,
|
onLeaveSpaceClick: () -> Unit,
|
||||||
onSettingsClick: () -> Unit,
|
onSettingsClick: () -> Unit,
|
||||||
onShareSpace: () -> Unit,
|
onShareSpace: () -> Unit,
|
||||||
onViewMembersClick: () -> Unit,
|
onViewMembersClick: () -> Unit,
|
||||||
|
onManageRoomsClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
|
|
@ -272,16 +352,14 @@ private fun SpaceViewTopBar(
|
||||||
BackButton(onClick = onBackClick)
|
BackButton(onClick = onBackClick)
|
||||||
},
|
},
|
||||||
title = {
|
title = {
|
||||||
if (currentSpace != null) {
|
val roundedCornerShape = RoundedCornerShape(8.dp)
|
||||||
val roundedCornerShape = RoundedCornerShape(8.dp)
|
SpaceAvatarAndNameRow(
|
||||||
SpaceAvatarAndNameRow(
|
name = spaceInfo.name,
|
||||||
name = currentSpace.displayName,
|
avatarData = spaceInfo.getAvatarData(AvatarSize.TimelineRoom),
|
||||||
avatarData = currentSpace.getAvatarData(AvatarSize.TimelineRoom),
|
modifier = Modifier
|
||||||
modifier = Modifier
|
.clip(roundedCornerShape)
|
||||||
.clip(roundedCornerShape)
|
.clickable(enabled = canAccessSpaceSettings, onClick = onSettingsClick)
|
||||||
.clickable(enabled = canAccessSpaceSettings, onClick = onSettingsClick)
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
var showMenu by remember { mutableStateOf(false) }
|
var showMenu by remember { mutableStateOf(false) }
|
||||||
|
|
@ -297,8 +375,19 @@ private fun SpaceViewTopBar(
|
||||||
expanded = showMenu,
|
expanded = showMenu,
|
||||||
onDismissRequest = { showMenu = false }
|
onDismissRequest = { showMenu = false }
|
||||||
) {
|
) {
|
||||||
|
if (showManageRoomsAction) {
|
||||||
|
SpaceMenuItem(
|
||||||
|
titleRes = CommonStrings.action_manage_rooms,
|
||||||
|
icon = CompoundIcons.Edit(),
|
||||||
|
onClick = {
|
||||||
|
showMenu = false
|
||||||
|
onManageRoomsClick()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
SpaceMenuItem(
|
SpaceMenuItem(
|
||||||
titleRes = CommonStrings.screen_space_menu_action_members,
|
titleRes = R.string.screen_space_menu_action_members,
|
||||||
icon = CompoundIcons.User(),
|
icon = CompoundIcons.User(),
|
||||||
onClick = {
|
onClick = {
|
||||||
showMenu = false
|
showMenu = false
|
||||||
|
|
@ -323,6 +412,7 @@ private fun SpaceViewTopBar(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
HorizontalDivider()
|
||||||
SpaceMenuItem(
|
SpaceMenuItem(
|
||||||
titleRes = CommonStrings.action_leave_space,
|
titleRes = CommonStrings.action_leave_space,
|
||||||
icon = CompoundIcons.Leave(),
|
icon = CompoundIcons.Leave(),
|
||||||
|
|
@ -337,6 +427,39 @@ private fun SpaceViewTopBar(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun ManageModeTopBar(
|
||||||
|
selectedCount: Int,
|
||||||
|
isRemoveButtonEnabled: Boolean,
|
||||||
|
onCancelClick: () -> Unit,
|
||||||
|
onRemoveClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
TopAppBar(
|
||||||
|
modifier = modifier,
|
||||||
|
navigationIcon = {
|
||||||
|
BackButton(
|
||||||
|
onClick = onCancelClick,
|
||||||
|
imageVector = CompoundIcons.Close()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = pluralStringResource(CommonPlurals.common_selected_count, selectedCount, selectedCount),
|
||||||
|
style = ElementTheme.typography.fontBodyLgMedium,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
TextButton(
|
||||||
|
text = stringResource(CommonStrings.action_remove),
|
||||||
|
onClick = onRemoveClick,
|
||||||
|
enabled = isRemoveButtonEnabled,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SpaceMenuItem(
|
private fun SpaceMenuItem(
|
||||||
@StringRes titleRes: Int,
|
@StringRes titleRes: Int,
|
||||||
|
|
@ -425,6 +548,45 @@ private fun SpaceRoom.inviteButtons(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RemoveRoomsActionView(
|
||||||
|
spaceDisplayName: String,
|
||||||
|
removeRoomsAction: AsyncAction<Unit>,
|
||||||
|
selectedCount: Int,
|
||||||
|
onConfirm: () -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
) {
|
||||||
|
AsyncActionView(
|
||||||
|
async = removeRoomsAction,
|
||||||
|
confirmationDialog = {
|
||||||
|
ConfirmationDialog(
|
||||||
|
title = pluralStringResource(R.plurals.screen_space_remove_rooms_confirmation_title, selectedCount, selectedCount, spaceDisplayName),
|
||||||
|
content = stringResource(R.string.screen_space_remove_rooms_confirmation_content),
|
||||||
|
submitText = stringResource(CommonStrings.action_remove),
|
||||||
|
onSubmitClick = onConfirm,
|
||||||
|
onDismiss = onDismiss,
|
||||||
|
destructiveSubmit = true,
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = CompoundIcons.Error(),
|
||||||
|
tint = ElementTheme.colors.textCriticalPrimary,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onRetry = onConfirm,
|
||||||
|
errorTitle = {
|
||||||
|
stringResource(CommonStrings.common_something_went_wrong)
|
||||||
|
},
|
||||||
|
errorMessage = {
|
||||||
|
stringResource(CommonStrings.error_network_or_server_issue)
|
||||||
|
},
|
||||||
|
onSuccess = { onDismiss() },
|
||||||
|
onErrorDismiss = onDismiss,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SpaceViewPreview(
|
internal fun SpaceViewPreview(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_space_menu_action_members">"Преглед на членовете"</string>
|
||||||
<string name="screen_space_settings_leave_space">"Напускане на пространството"</string>
|
<string name="screen_space_settings_leave_space">"Напускане на пространството"</string>
|
||||||
<string name="screen_space_settings_roles_and_permissions">"Роли и разрешения"</string>
|
<string name="screen_space_settings_roles_and_permissions">"Роли и разрешения"</string>
|
||||||
<string name="screen_space_settings_security_and_privacy">"Защита и поверителност"</string>
|
<string name="screen_space_settings_security_and_privacy">"Защита и поверителност"</string>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
<string name="screen_leave_space_subtitle_only_last_admin">"Z následujících místností nebudete odstraněni, protože jste jediným administrátorem:"</string>
|
<string name="screen_leave_space_subtitle_only_last_admin">"Z následujících místností nebudete odstraněni, protože jste jediným administrátorem:"</string>
|
||||||
<string name="screen_leave_space_title">"Opustit %1$s?"</string>
|
<string name="screen_leave_space_title">"Opustit %1$s?"</string>
|
||||||
<string name="screen_leave_space_title_last_admin">"Jste jediným administrátorem pro %1$s"</string>
|
<string name="screen_leave_space_title_last_admin">"Jste jediným administrátorem pro %1$s"</string>
|
||||||
|
<string name="screen_space_menu_action_members">"Zobrazit členy"</string>
|
||||||
<string name="screen_space_settings_leave_space">"Opustit prostor"</string>
|
<string name="screen_space_settings_leave_space">"Opustit prostor"</string>
|
||||||
<string name="screen_space_settings_roles_and_permissions">"Role a oprávnění"</string>
|
<string name="screen_space_settings_roles_and_permissions">"Role a oprávnění"</string>
|
||||||
<string name="screen_space_settings_security_and_privacy">"Zabezpečení a soukromí"</string>
|
<string name="screen_space_settings_security_and_privacy">"Zabezpečení a soukromí"</string>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
<string name="screen_leave_space_subtitle_only_last_admin">"Du vil ikke blive fjernet fra følgende rum, fordi du er den eneste administrator:"</string>
|
<string name="screen_leave_space_subtitle_only_last_admin">"Du vil ikke blive fjernet fra følgende rum, fordi du er den eneste administrator:"</string>
|
||||||
<string name="screen_leave_space_title">"Forlad %1$s?"</string>
|
<string name="screen_leave_space_title">"Forlad %1$s?"</string>
|
||||||
<string name="screen_leave_space_title_last_admin">"Du er den eneste administrator for %1$s"</string>
|
<string name="screen_leave_space_title_last_admin">"Du er den eneste administrator for %1$s"</string>
|
||||||
|
<string name="screen_space_menu_action_members">"Vis medlemmer"</string>
|
||||||
<string name="screen_space_settings_leave_space">"Forlad gruppe"</string>
|
<string name="screen_space_settings_leave_space">"Forlad gruppe"</string>
|
||||||
<string name="screen_space_settings_roles_and_permissions">"Roller og tilladelser"</string>
|
<string name="screen_space_settings_roles_and_permissions">"Roller og tilladelser"</string>
|
||||||
<string name="screen_space_settings_security_and_privacy">"Sikkerhed og privatliv"</string>
|
<string name="screen_space_settings_security_and_privacy">"Sikkerhed og privatliv"</string>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,12 @@
|
||||||
<string name="screen_leave_space_subtitle_only_last_admin">"Du wirst aus den folgenden Chats nicht entfernt, weil du der einzige Admin bist:"</string>
|
<string name="screen_leave_space_subtitle_only_last_admin">"Du wirst aus den folgenden Chats nicht entfernt, weil du der einzige Admin bist:"</string>
|
||||||
<string name="screen_leave_space_title">"%1$s verlassen?"</string>
|
<string name="screen_leave_space_title">"%1$s verlassen?"</string>
|
||||||
<string name="screen_leave_space_title_last_admin">"Du bist der einzige Administrator für %1$s"</string>
|
<string name="screen_leave_space_title_last_admin">"Du bist der einzige Administrator für %1$s"</string>
|
||||||
|
<string name="screen_space_menu_action_members">"Mitglieder anzeigen"</string>
|
||||||
|
<string name="screen_space_remove_rooms_confirmation_content">"Das Entfernen eines Chats hat keinen Einfluss auf die Beitrittsregeln. Um die Regeln zu ändern, gehe zu \"Raum Info\" und dann zu \"Datenschutz und Sicherheit\""</string>
|
||||||
|
<plurals name="screen_space_remove_rooms_confirmation_title">
|
||||||
|
<item quantity="one">"%1$d chat aus %2$s entfernen"</item>
|
||||||
|
<item quantity="other">"%1$d chats aus %2$s entfernen"</item>
|
||||||
|
</plurals>
|
||||||
<string name="screen_space_settings_leave_space">"Space verlassen"</string>
|
<string name="screen_space_settings_leave_space">"Space verlassen"</string>
|
||||||
<string name="screen_space_settings_roles_and_permissions">"Rollen und Berechtigungen"</string>
|
<string name="screen_space_settings_roles_and_permissions">"Rollen und Berechtigungen"</string>
|
||||||
<string name="screen_space_settings_security_and_privacy">"Sicherheit & Datenschutz"</string>
|
<string name="screen_space_settings_security_and_privacy">"Sicherheit & Datenschutz"</string>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
<string name="screen_leave_space_subtitle_only_last_admin">"Sind ei saa järgnevatest jututubadest eemaldada, kuna oled seal/neis ainus peakasutaja:"</string>
|
<string name="screen_leave_space_subtitle_only_last_admin">"Sind ei saa järgnevatest jututubadest eemaldada, kuna oled seal/neis ainus peakasutaja:"</string>
|
||||||
<string name="screen_leave_space_title">"Kas lahkud %1$s kogukonnast?"</string>
|
<string name="screen_leave_space_title">"Kas lahkud %1$s kogukonnast?"</string>
|
||||||
<string name="screen_leave_space_title_last_admin">"Sa oled siin ainus peakasutaja: %1$s"</string>
|
<string name="screen_leave_space_title_last_admin">"Sa oled siin ainus peakasutaja: %1$s"</string>
|
||||||
|
<string name="screen_space_menu_action_members">"Vaata liikmeid"</string>
|
||||||
<string name="screen_space_settings_leave_space">"Lahku kogukonnast"</string>
|
<string name="screen_space_settings_leave_space">"Lahku kogukonnast"</string>
|
||||||
<string name="screen_space_settings_roles_and_permissions">"Rollid ja õigused"</string>
|
<string name="screen_space_settings_roles_and_permissions">"Rollid ja õigused"</string>
|
||||||
<string name="screen_space_settings_security_and_privacy">"Turvalisus ja privaatsus"</string>
|
<string name="screen_space_settings_security_and_privacy">"Turvalisus ja privaatsus"</string>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
<string name="screen_leave_space_subtitle_only_last_admin">"از اتاق(های) زیر برداشته نخواهید شد؛ چرا که تنها مدیر هستید:"</string>
|
<string name="screen_leave_space_subtitle_only_last_admin">"از اتاق(های) زیر برداشته نخواهید شد؛ چرا که تنها مدیر هستید:"</string>
|
||||||
<string name="screen_leave_space_title">"ترک %1$s؟"</string>
|
<string name="screen_leave_space_title">"ترک %1$s؟"</string>
|
||||||
<string name="screen_leave_space_title_last_admin">"تنها مدیر %1$s هستید"</string>
|
<string name="screen_leave_space_title_last_admin">"تنها مدیر %1$s هستید"</string>
|
||||||
|
<string name="screen_space_menu_action_members">"دیدن اعضا"</string>
|
||||||
<string name="screen_space_settings_leave_space">"ترک فضا"</string>
|
<string name="screen_space_settings_leave_space">"ترک فضا"</string>
|
||||||
<string name="screen_space_settings_roles_and_permissions">"نقشها و اجازهها"</string>
|
<string name="screen_space_settings_roles_and_permissions">"نقشها و اجازهها"</string>
|
||||||
<string name="screen_space_settings_security_and_privacy">"امنیت و محرمانگی"</string>
|
<string name="screen_space_settings_security_and_privacy">"امنیت و محرمانگی"</string>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
<string name="screen_leave_space_subtitle_only_last_admin">"Sinua ei poisteta seuraavista huoneista, koska olet ainoa ylläpitäjä:"</string>
|
<string name="screen_leave_space_subtitle_only_last_admin">"Sinua ei poisteta seuraavista huoneista, koska olet ainoa ylläpitäjä:"</string>
|
||||||
<string name="screen_leave_space_title">"Haluatko poistua tilasta %1$s?"</string>
|
<string name="screen_leave_space_title">"Haluatko poistua tilasta %1$s?"</string>
|
||||||
<string name="screen_leave_space_title_last_admin">"Olet ainoa ylläpitäjä tilassa %1$s"</string>
|
<string name="screen_leave_space_title_last_admin">"Olet ainoa ylläpitäjä tilassa %1$s"</string>
|
||||||
|
<string name="screen_space_menu_action_members">"Näytä jäsenet"</string>
|
||||||
<string name="screen_space_settings_leave_space">"Poistu tilasta"</string>
|
<string name="screen_space_settings_leave_space">"Poistu tilasta"</string>
|
||||||
<string name="screen_space_settings_roles_and_permissions">"Roolit ja oikeudet"</string>
|
<string name="screen_space_settings_roles_and_permissions">"Roolit ja oikeudet"</string>
|
||||||
<string name="screen_space_settings_security_and_privacy">"Turvallisuus ja yksityisyys"</string>
|
<string name="screen_space_settings_security_and_privacy">"Turvallisuus ja yksityisyys"</string>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
<string name="screen_leave_space_subtitle_only_last_admin">"Vous ne quitterez pas le ou les salons suivants car vous y êtes le seul administrateur:"</string>
|
<string name="screen_leave_space_subtitle_only_last_admin">"Vous ne quitterez pas le ou les salons suivants car vous y êtes le seul administrateur:"</string>
|
||||||
<string name="screen_leave_space_title">"Quitter %1$s?"</string>
|
<string name="screen_leave_space_title">"Quitter %1$s?"</string>
|
||||||
<string name="screen_leave_space_title_last_admin">"Vous êtes le seul administrateur de %1$s"</string>
|
<string name="screen_leave_space_title_last_admin">"Vous êtes le seul administrateur de %1$s"</string>
|
||||||
|
<string name="screen_space_menu_action_members">"Voir les membres"</string>
|
||||||
<string name="screen_space_settings_leave_space">"Quitter l’espace"</string>
|
<string name="screen_space_settings_leave_space">"Quitter l’espace"</string>
|
||||||
<string name="screen_space_settings_roles_and_permissions">"Rôles & autorisations"</string>
|
<string name="screen_space_settings_roles_and_permissions">"Rôles & autorisations"</string>
|
||||||
<string name="screen_space_settings_security_and_privacy">"Sécurité & confidentialité"</string>
|
<string name="screen_space_settings_security_and_privacy">"Sécurité & confidentialité"</string>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
<string name="screen_leave_space_subtitle_only_last_admin">"Nećete biti uklonjeni iz sljedećih soba jer ste jedini administrator:"</string>
|
<string name="screen_leave_space_subtitle_only_last_admin">"Nećete biti uklonjeni iz sljedećih soba jer ste jedini administrator:"</string>
|
||||||
<string name="screen_leave_space_title">"Želite li napustiti %1$s?"</string>
|
<string name="screen_leave_space_title">"Želite li napustiti %1$s?"</string>
|
||||||
<string name="screen_leave_space_title_last_admin">"Vi ste jedini administrator za %1$s"</string>
|
<string name="screen_leave_space_title_last_admin">"Vi ste jedini administrator za %1$s"</string>
|
||||||
|
<string name="screen_space_menu_action_members">"Prikaži članove"</string>
|
||||||
<string name="screen_space_settings_leave_space">"Napusti prostor"</string>
|
<string name="screen_space_settings_leave_space">"Napusti prostor"</string>
|
||||||
<string name="screen_space_settings_roles_and_permissions">"Uloge i dopuštenja"</string>
|
<string name="screen_space_settings_roles_and_permissions">"Uloge i dopuštenja"</string>
|
||||||
<string name="screen_space_settings_security_and_privacy">"Sigurnost i privatnost"</string>
|
<string name="screen_space_settings_security_and_privacy">"Sigurnost i privatnost"</string>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
<string name="screen_leave_space_subtitle_only_last_admin">"Nem lesz eltávolítva a következő szobá(k)ból, mert ön az egyetlen adminisztrátor:"</string>
|
<string name="screen_leave_space_subtitle_only_last_admin">"Nem lesz eltávolítva a következő szobá(k)ból, mert ön az egyetlen adminisztrátor:"</string>
|
||||||
<string name="screen_leave_space_title">"Kilép innen: %1$s?"</string>
|
<string name="screen_leave_space_title">"Kilép innen: %1$s?"</string>
|
||||||
<string name="screen_leave_space_title_last_admin">"Ön az egyetlen adminisztrátor itt: %1$s"</string>
|
<string name="screen_leave_space_title_last_admin">"Ön az egyetlen adminisztrátor itt: %1$s"</string>
|
||||||
|
<string name="screen_space_menu_action_members">"Tagok megtekintése"</string>
|
||||||
<string name="screen_space_settings_leave_space">"Tér elhagyása"</string>
|
<string name="screen_space_settings_leave_space">"Tér elhagyása"</string>
|
||||||
<string name="screen_space_settings_roles_and_permissions">"Szerepkörök és jogosultságok"</string>
|
<string name="screen_space_settings_roles_and_permissions">"Szerepkörök és jogosultságok"</string>
|
||||||
<string name="screen_space_settings_security_and_privacy">"Biztonság és adatvédelem"</string>
|
<string name="screen_space_settings_security_and_privacy">"Biztonság és adatvédelem"</string>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
<string name="screen_leave_space_subtitle_only_last_admin">"Non verrai rimosso dalle seguenti stanze perché sei l\'unico amministratore:"</string>
|
<string name="screen_leave_space_subtitle_only_last_admin">"Non verrai rimosso dalle seguenti stanze perché sei l\'unico amministratore:"</string>
|
||||||
<string name="screen_leave_space_title">"Uscire da %1$s?"</string>
|
<string name="screen_leave_space_title">"Uscire da %1$s?"</string>
|
||||||
<string name="screen_leave_space_title_last_admin">"Sei l\'unico amministratore di %1$s"</string>
|
<string name="screen_leave_space_title_last_admin">"Sei l\'unico amministratore di %1$s"</string>
|
||||||
|
<string name="screen_space_menu_action_members">"Visualizza membri"</string>
|
||||||
<string name="screen_space_settings_leave_space">"Esci dallo spazio"</string>
|
<string name="screen_space_settings_leave_space">"Esci dallo spazio"</string>
|
||||||
<string name="screen_space_settings_roles_and_permissions">"Ruoli e autorizzazioni"</string>
|
<string name="screen_space_settings_roles_and_permissions">"Ruoli e autorizzazioni"</string>
|
||||||
<string name="screen_space_settings_security_and_privacy">"Sicurezza e privacy"</string>
|
<string name="screen_space_settings_security_and_privacy">"Sicurezza e privacy"</string>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
<string name="screen_leave_space_subtitle_only_last_admin">"Du vil ikke bli fjernet fra følgende rom fordi du er den eneste administratoren:"</string>
|
<string name="screen_leave_space_subtitle_only_last_admin">"Du vil ikke bli fjernet fra følgende rom fordi du er den eneste administratoren:"</string>
|
||||||
<string name="screen_leave_space_title">"Forlat %1$s?"</string>
|
<string name="screen_leave_space_title">"Forlat %1$s?"</string>
|
||||||
<string name="screen_leave_space_title_last_admin">"Du er den eneste administratoren for %1$s"</string>
|
<string name="screen_leave_space_title_last_admin">"Du er den eneste administratoren for %1$s"</string>
|
||||||
|
<string name="screen_space_menu_action_members">"Vis medlemmer"</string>
|
||||||
<string name="screen_space_settings_leave_space">"Forlat område"</string>
|
<string name="screen_space_settings_leave_space">"Forlat område"</string>
|
||||||
<string name="screen_space_settings_roles_and_permissions">"Roller og tillatelser"</string>
|
<string name="screen_space_settings_roles_and_permissions">"Roller og tillatelser"</string>
|
||||||
<string name="screen_space_settings_security_and_privacy">"Sikkerhet og personvern"</string>
|
<string name="screen_space_settings_security_and_privacy">"Sikkerhet og personvern"</string>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
<string name="screen_leave_space_subtitle_only_last_admin">"Você não será removido das seguintes salas porque você é o único administrador:"</string>
|
<string name="screen_leave_space_subtitle_only_last_admin">"Você não será removido das seguintes salas porque você é o único administrador:"</string>
|
||||||
<string name="screen_leave_space_title">"Sair de %1$s?"</string>
|
<string name="screen_leave_space_title">"Sair de %1$s?"</string>
|
||||||
<string name="screen_leave_space_title_last_admin">"Você é o único administrador de %1$s"</string>
|
<string name="screen_leave_space_title_last_admin">"Você é o único administrador de %1$s"</string>
|
||||||
|
<string name="screen_space_menu_action_members">"Ver membros"</string>
|
||||||
<string name="screen_space_settings_leave_space">"Sair do espaço"</string>
|
<string name="screen_space_settings_leave_space">"Sair do espaço"</string>
|
||||||
<string name="screen_space_settings_roles_and_permissions">"Cargos e permissões"</string>
|
<string name="screen_space_settings_roles_and_permissions">"Cargos e permissões"</string>
|
||||||
<string name="screen_space_settings_security_and_privacy">"Segurança e privacidade"</string>
|
<string name="screen_space_settings_security_and_privacy">"Segurança e privacidade"</string>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
<string name="screen_leave_space_subtitle_only_last_admin">"Nu veți părăsi următoarele camere deoarece sunteți singurul administrator:"</string>
|
<string name="screen_leave_space_subtitle_only_last_admin">"Nu veți părăsi următoarele camere deoarece sunteți singurul administrator:"</string>
|
||||||
<string name="screen_leave_space_title">"Părăsiți %1$s?"</string>
|
<string name="screen_leave_space_title">"Părăsiți %1$s?"</string>
|
||||||
<string name="screen_leave_space_title_last_admin">"Sunteți singurul administrator pentru %1$s"</string>
|
<string name="screen_leave_space_title_last_admin">"Sunteți singurul administrator pentru %1$s"</string>
|
||||||
|
<string name="screen_space_menu_action_members">"Vizualizați membrii"</string>
|
||||||
<string name="screen_space_settings_leave_space">"Părăsiți spațiul"</string>
|
<string name="screen_space_settings_leave_space">"Părăsiți spațiul"</string>
|
||||||
<string name="screen_space_settings_roles_and_permissions">"Roluri și permisiuni"</string>
|
<string name="screen_space_settings_roles_and_permissions">"Roluri și permisiuni"</string>
|
||||||
<string name="screen_space_settings_security_and_privacy">"Securitate & confidențialitate"</string>
|
<string name="screen_space_settings_security_and_privacy">"Securitate & confidențialitate"</string>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
<string name="screen_leave_space_subtitle_only_last_admin">"Вы не будете удалены из следующих комнат, поскольку вы являетесь единственным администратором:"</string>
|
<string name="screen_leave_space_subtitle_only_last_admin">"Вы не будете удалены из следующих комнат, поскольку вы являетесь единственным администратором:"</string>
|
||||||
<string name="screen_leave_space_title">"Выйти из %1$s?"</string>
|
<string name="screen_leave_space_title">"Выйти из %1$s?"</string>
|
||||||
<string name="screen_leave_space_title_last_admin">"Вы единственный администратор для %1$s"</string>
|
<string name="screen_leave_space_title_last_admin">"Вы единственный администратор для %1$s"</string>
|
||||||
|
<string name="screen_space_menu_action_members">"Просмотреть участников"</string>
|
||||||
<string name="screen_space_settings_leave_space">"Покинуть пространство"</string>
|
<string name="screen_space_settings_leave_space">"Покинуть пространство"</string>
|
||||||
<string name="screen_space_settings_roles_and_permissions">"Роли и разрешения"</string>
|
<string name="screen_space_settings_roles_and_permissions">"Роли и разрешения"</string>
|
||||||
<string name="screen_space_settings_security_and_privacy">"Безопасность и конфиденциальность"</string>
|
<string name="screen_space_settings_security_and_privacy">"Безопасность и конфиденциальность"</string>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
<string name="screen_leave_space_subtitle_only_last_admin">"Z nasledujúcich miestností nebudete odstránený/á, pretože ste jediným správcom:"</string>
|
<string name="screen_leave_space_subtitle_only_last_admin">"Z nasledujúcich miestností nebudete odstránený/á, pretože ste jediným správcom:"</string>
|
||||||
<string name="screen_leave_space_title">"Opustiť %1$s?"</string>
|
<string name="screen_leave_space_title">"Opustiť %1$s?"</string>
|
||||||
<string name="screen_leave_space_title_last_admin">"Ste jediným administrátorom pre %1$s"</string>
|
<string name="screen_leave_space_title_last_admin">"Ste jediným administrátorom pre %1$s"</string>
|
||||||
|
<string name="screen_space_menu_action_members">"Zobraziť členov"</string>
|
||||||
<string name="screen_space_settings_leave_space">"Opustiť priestor"</string>
|
<string name="screen_space_settings_leave_space">"Opustiť priestor"</string>
|
||||||
<string name="screen_space_settings_roles_and_permissions">"Roly a povolenia"</string>
|
<string name="screen_space_settings_roles_and_permissions">"Roly a povolenia"</string>
|
||||||
<string name="screen_space_settings_security_and_privacy">"Bezpečnosť a súkromie"</string>
|
<string name="screen_space_settings_security_and_privacy">"Bezpečnosť a súkromie"</string>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
<string name="screen_leave_space_subtitle_only_last_admin">"您不會被從以下聊天室移除,因為您是唯一的管理員:"</string>
|
<string name="screen_leave_space_subtitle_only_last_admin">"您不會被從以下聊天室移除,因為您是唯一的管理員:"</string>
|
||||||
<string name="screen_leave_space_title">"離開 %1$s?"</string>
|
<string name="screen_leave_space_title">"離開 %1$s?"</string>
|
||||||
<string name="screen_leave_space_title_last_admin">"您是 %1$s 唯一的管理員"</string>
|
<string name="screen_leave_space_title_last_admin">"您是 %1$s 唯一的管理員"</string>
|
||||||
|
<string name="screen_space_menu_action_members">"檢視成員"</string>
|
||||||
<string name="screen_space_settings_leave_space">"離開空間"</string>
|
<string name="screen_space_settings_leave_space">"離開空間"</string>
|
||||||
<string name="screen_space_settings_roles_and_permissions">"角色與權限"</string>
|
<string name="screen_space_settings_roles_and_permissions">"角色與權限"</string>
|
||||||
<string name="screen_space_settings_security_and_privacy">"安全與隱私"</string>
|
<string name="screen_space_settings_security_and_privacy">"安全與隱私"</string>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
<string name="screen_leave_space_subtitle_only_last_admin">"您不会从以下房间中被移除,因为您是唯一的管理员:"</string>
|
<string name="screen_leave_space_subtitle_only_last_admin">"您不会从以下房间中被移除,因为您是唯一的管理员:"</string>
|
||||||
<string name="screen_leave_space_title">"离开%1$s?"</string>
|
<string name="screen_leave_space_title">"离开%1$s?"</string>
|
||||||
<string name="screen_leave_space_title_last_admin">"您是 %1$s 的唯一管理员"</string>
|
<string name="screen_leave_space_title_last_admin">"您是 %1$s 的唯一管理员"</string>
|
||||||
|
<string name="screen_space_menu_action_members">"查看成员"</string>
|
||||||
<string name="screen_space_settings_leave_space">"离开空间"</string>
|
<string name="screen_space_settings_leave_space">"离开空间"</string>
|
||||||
<string name="screen_space_settings_roles_and_permissions">"角色与权限"</string>
|
<string name="screen_space_settings_roles_and_permissions">"角色与权限"</string>
|
||||||
<string name="screen_space_settings_security_and_privacy">"安全与隐私"</string>
|
<string name="screen_space_settings_security_and_privacy">"安全与隐私"</string>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,12 @@
|
||||||
<string name="screen_leave_space_subtitle_only_last_admin">"You will not be removed from the following room(s) because you\'re the only administrator:"</string>
|
<string name="screen_leave_space_subtitle_only_last_admin">"You will not be removed from the following room(s) because you\'re the only administrator:"</string>
|
||||||
<string name="screen_leave_space_title">"Leave %1$s?"</string>
|
<string name="screen_leave_space_title">"Leave %1$s?"</string>
|
||||||
<string name="screen_leave_space_title_last_admin">"You are the only admin for %1$s"</string>
|
<string name="screen_leave_space_title_last_admin">"You are the only admin for %1$s"</string>
|
||||||
|
<string name="screen_space_menu_action_members">"View members"</string>
|
||||||
|
<string name="screen_space_remove_rooms_confirmation_content">"Removing a room will not affect the room access. To change the access go to Room info > Privacy & security."</string>
|
||||||
|
<plurals name="screen_space_remove_rooms_confirmation_title">
|
||||||
|
<item quantity="one">"Remove %1$d room from %2$s"</item>
|
||||||
|
<item quantity="other">"Remove %1$d rooms from %2$s"</item>
|
||||||
|
</plurals>
|
||||||
<string name="screen_space_settings_leave_space">"Leave space"</string>
|
<string name="screen_space_settings_leave_space">"Leave space"</string>
|
||||||
<string name="screen_space_settings_roles_and_permissions">"Roles & permissions"</string>
|
<string name="screen_space_settings_roles_and_permissions">"Roles & permissions"</string>
|
||||||
<string name="screen_space_settings_security_and_privacy">"Security & privacy"</string>
|
<string name="screen_space_settings_security_and_privacy">"Security & privacy"</string>
|
||||||
|
|
|
||||||
|
|
@ -22,20 +22,24 @@ import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||||
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||||
import io.element.android.libraries.matrix.api.room.BaseRoom
|
import io.element.android.libraries.matrix.api.room.BaseRoom
|
||||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomType
|
||||||
import io.element.android.libraries.matrix.api.room.join.JoinRoom
|
import io.element.android.libraries.matrix.api.room.join.JoinRoom
|
||||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
||||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID_3
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||||
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
|
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
|
||||||
import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions
|
import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions
|
||||||
import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList
|
import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList
|
||||||
|
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
|
||||||
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||||
import io.element.android.tests.testutils.EventsRecorder
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
|
@ -59,7 +63,7 @@ class SpacePresenterTest {
|
||||||
val presenter = createSpacePresenter(spaceRoomList = spaceRoomList)
|
val presenter = createSpacePresenter(spaceRoomList = spaceRoomList)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val state = awaitItem()
|
val state = awaitItem()
|
||||||
assertThat(state.currentSpace).isNull()
|
assertThat(state.spaceInfo).isNotNull()
|
||||||
assertThat(state.children).isEmpty()
|
assertThat(state.children).isEmpty()
|
||||||
assertThat(state.seenSpaceInvites).isEmpty()
|
assertThat(state.seenSpaceInvites).isEmpty()
|
||||||
assertThat(state.hideInvitesAvatar).isFalse()
|
assertThat(state.hideInvitesAvatar).isFalse()
|
||||||
|
|
@ -139,23 +143,6 @@ class SpacePresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - current space value`() = runTest {
|
|
||||||
val paginateResult = lambdaRecorder<Result<Unit>> {
|
|
||||||
Result.success(Unit)
|
|
||||||
}
|
|
||||||
val spaceRoomList = FakeSpaceRoomList(paginateResult = paginateResult)
|
|
||||||
val presenter = createSpacePresenter(spaceRoomList = spaceRoomList)
|
|
||||||
presenter.test {
|
|
||||||
val state = awaitItem()
|
|
||||||
advanceUntilIdle()
|
|
||||||
assertThat(state.currentSpace).isNull()
|
|
||||||
val aSpace = aSpaceRoom()
|
|
||||||
spaceRoomList.emitCurrentSpace(aSpace)
|
|
||||||
assertThat(awaitItem().currentSpace).isEqualTo(aSpace)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - children value`() = runTest {
|
fun `present - children value`() = runTest {
|
||||||
val paginateResult = lambdaRecorder<Result<Unit>> {
|
val paginateResult = lambdaRecorder<Result<Unit>> {
|
||||||
|
|
@ -353,6 +340,216 @@ class SpacePresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - enter manage mode`() = runTest {
|
||||||
|
val presenter = createSpacePresenter()
|
||||||
|
presenter.test {
|
||||||
|
val state = awaitItem()
|
||||||
|
assertThat(state.isManageMode).isFalse()
|
||||||
|
state.eventSink(SpaceEvents.EnterManageMode)
|
||||||
|
val manageModeState = awaitItem()
|
||||||
|
assertThat(manageModeState.isManageMode).isTrue()
|
||||||
|
assertThat(manageModeState.selectedRoomIds).isEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - exit manage mode clears selection`() = runTest {
|
||||||
|
val presenter = createSpacePresenter()
|
||||||
|
presenter.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink(SpaceEvents.EnterManageMode)
|
||||||
|
initialState.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID))
|
||||||
|
initialState.eventSink(SpaceEvents.ExitManageMode)
|
||||||
|
val finalState = expectMostRecentItem()
|
||||||
|
assertThat(finalState.isManageMode).isFalse()
|
||||||
|
assertThat(finalState.selectedRoomIds).isEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - toggle room selection`() = runTest {
|
||||||
|
val presenter = createSpacePresenter()
|
||||||
|
presenter.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink(SpaceEvents.EnterManageMode)
|
||||||
|
// Select a room
|
||||||
|
initialState.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID))
|
||||||
|
var latestState = expectMostRecentItem()
|
||||||
|
assertThat(latestState.selectedRoomIds).containsExactly(A_ROOM_ID)
|
||||||
|
// Deselect the room
|
||||||
|
latestState.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID))
|
||||||
|
latestState = expectMostRecentItem()
|
||||||
|
assertThat(latestState.selectedRoomIds).isEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - remove rooms success`() = runTest {
|
||||||
|
val removeChildFromSpaceResult = lambdaRecorder<RoomId, RoomId, Result<Unit>> { _, _ ->
|
||||||
|
Result.success(Unit)
|
||||||
|
}
|
||||||
|
val aRoom = aSpaceRoom(
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
roomType = RoomType.Room,
|
||||||
|
)
|
||||||
|
val fakeSpaceRoomList = FakeSpaceRoomList(
|
||||||
|
initialSpaceRoomsValue = listOf(aRoom),
|
||||||
|
paginateResult = { Result.success(Unit) },
|
||||||
|
)
|
||||||
|
val presenter = createSpacePresenter(
|
||||||
|
spaceRoomList = fakeSpaceRoomList,
|
||||||
|
spaceService = FakeSpaceService(
|
||||||
|
removeChildFromSpaceResult = removeChildFromSpaceResult,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
awaitItem() // Initial empty state
|
||||||
|
advanceUntilIdle()
|
||||||
|
val stateWithChildren = awaitItem()
|
||||||
|
assertThat(stateWithChildren.children).hasSize(1)
|
||||||
|
stateWithChildren.eventSink(SpaceEvents.EnterManageMode)
|
||||||
|
stateWithChildren.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID))
|
||||||
|
stateWithChildren.eventSink(SpaceEvents.RemoveSelectedRooms)
|
||||||
|
stateWithChildren.eventSink(SpaceEvents.ConfirmRoomRemoval)
|
||||||
|
advanceUntilIdle()
|
||||||
|
val successState = expectMostRecentItem()
|
||||||
|
assertThat(successState.removeRoomsAction).isEqualTo(AsyncAction.Success(Unit))
|
||||||
|
assertThat(successState.isManageMode).isFalse()
|
||||||
|
assertThat(successState.children).isEmpty()
|
||||||
|
removeChildFromSpaceResult.assertions().isCalledOnce()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - remove rooms partial failure`() = runTest {
|
||||||
|
val aRoom1 = aSpaceRoom(
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
roomType = RoomType.Room,
|
||||||
|
)
|
||||||
|
val aRoom2 = aSpaceRoom(
|
||||||
|
roomId = A_ROOM_ID_2,
|
||||||
|
roomType = RoomType.Room,
|
||||||
|
)
|
||||||
|
val removeChildFromSpaceResult = lambdaRecorder<RoomId, RoomId, Result<Unit>> { _, childId ->
|
||||||
|
if (childId == A_ROOM_ID_2) {
|
||||||
|
Result.failure(AN_EXCEPTION)
|
||||||
|
} else {
|
||||||
|
Result.success(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val fakeSpaceRoomList = FakeSpaceRoomList(
|
||||||
|
initialSpaceRoomsValue = listOf(aRoom1, aRoom2),
|
||||||
|
paginateResult = { Result.success(Unit) },
|
||||||
|
)
|
||||||
|
val presenter = createSpacePresenter(
|
||||||
|
spaceRoomList = fakeSpaceRoomList,
|
||||||
|
spaceService = FakeSpaceService(
|
||||||
|
removeChildFromSpaceResult = removeChildFromSpaceResult,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
awaitItem() // Initial empty state
|
||||||
|
advanceUntilIdle()
|
||||||
|
val stateWithChildren = awaitItem()
|
||||||
|
assertThat(stateWithChildren.children).hasSize(2)
|
||||||
|
stateWithChildren.eventSink(SpaceEvents.EnterManageMode)
|
||||||
|
stateWithChildren.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID))
|
||||||
|
stateWithChildren.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID_2))
|
||||||
|
stateWithChildren.eventSink(SpaceEvents.RemoveSelectedRooms)
|
||||||
|
stateWithChildren.eventSink(SpaceEvents.ConfirmRoomRemoval)
|
||||||
|
advanceUntilIdle()
|
||||||
|
val failureState = expectMostRecentItem()
|
||||||
|
assertThat(failureState.removeRoomsAction.isFailure()).isTrue()
|
||||||
|
// Successfully removed room should be filtered out
|
||||||
|
assertThat(failureState.children.map { it.roomId }).doesNotContain(A_ROOM_ID)
|
||||||
|
// Failed room should still be present
|
||||||
|
assertThat(failureState.children.map { it.roomId }).contains(A_ROOM_ID_2)
|
||||||
|
removeChildFromSpaceResult.assertions().isCalledExactly(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - children filtered in manage mode shows only rooms`() = runTest {
|
||||||
|
val aRoom = aSpaceRoom(
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
roomType = RoomType.Room,
|
||||||
|
)
|
||||||
|
val aSubSpace = aSpaceRoom(
|
||||||
|
roomId = A_ROOM_ID_2,
|
||||||
|
roomType = RoomType.Space,
|
||||||
|
)
|
||||||
|
val fakeSpaceRoomList = FakeSpaceRoomList(
|
||||||
|
initialSpaceRoomsValue = listOf(aRoom, aSubSpace),
|
||||||
|
paginateResult = { Result.success(Unit) },
|
||||||
|
)
|
||||||
|
val presenter = createSpacePresenter(spaceRoomList = fakeSpaceRoomList)
|
||||||
|
presenter.test {
|
||||||
|
awaitItem() // Initial empty state
|
||||||
|
advanceUntilIdle()
|
||||||
|
val stateWithChildren = awaitItem()
|
||||||
|
// Both room and space visible initially
|
||||||
|
assertThat(stateWithChildren.children).hasSize(2)
|
||||||
|
assertThat(stateWithChildren.isManageMode).isFalse()
|
||||||
|
stateWithChildren.eventSink(SpaceEvents.EnterManageMode)
|
||||||
|
val manageModeState = expectMostRecentItem()
|
||||||
|
// Only rooms visible in manage mode
|
||||||
|
assertThat(manageModeState.children).hasSize(1)
|
||||||
|
assertThat(manageModeState.children.first().roomId).isEqualTo(A_ROOM_ID)
|
||||||
|
assertThat(manageModeState.children.first().isSpace).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - removed rooms persist after flow update`() = runTest {
|
||||||
|
val removeChildFromSpaceResult = lambdaRecorder<RoomId, RoomId, Result<Unit>> { _, _ ->
|
||||||
|
Result.success(Unit)
|
||||||
|
}
|
||||||
|
val aRoom1 = aSpaceRoom(
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
roomType = RoomType.Room,
|
||||||
|
)
|
||||||
|
val aRoom2 = aSpaceRoom(
|
||||||
|
roomId = A_ROOM_ID_2,
|
||||||
|
roomType = RoomType.Room,
|
||||||
|
)
|
||||||
|
val aRoom3 = aSpaceRoom(
|
||||||
|
roomId = A_ROOM_ID_3,
|
||||||
|
roomType = RoomType.Room,
|
||||||
|
)
|
||||||
|
val spaceRoomList = FakeSpaceRoomList(
|
||||||
|
initialSpaceRoomsValue = listOf(aRoom1, aRoom2),
|
||||||
|
paginateResult = { Result.success(Unit) },
|
||||||
|
)
|
||||||
|
val presenter = createSpacePresenter(
|
||||||
|
spaceRoomList = spaceRoomList,
|
||||||
|
spaceService = FakeSpaceService(
|
||||||
|
removeChildFromSpaceResult = removeChildFromSpaceResult,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
awaitItem() // Initial empty state
|
||||||
|
advanceUntilIdle()
|
||||||
|
val stateWithChildren = awaitItem()
|
||||||
|
stateWithChildren.eventSink(SpaceEvents.EnterManageMode)
|
||||||
|
stateWithChildren.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID))
|
||||||
|
stateWithChildren.eventSink(SpaceEvents.RemoveSelectedRooms)
|
||||||
|
stateWithChildren.eventSink(SpaceEvents.ConfirmRoomRemoval)
|
||||||
|
advanceUntilIdle()
|
||||||
|
val successState = expectMostRecentItem()
|
||||||
|
assertThat(successState.children.map { it.roomId }).doesNotContain(A_ROOM_ID)
|
||||||
|
// Emit new flow update with a new room added (simulating server refresh)
|
||||||
|
spaceRoomList.emitSpaceRooms(listOf(aRoom1, aRoom2, aRoom3))
|
||||||
|
advanceUntilIdle()
|
||||||
|
val afterFlowUpdate = awaitItem()
|
||||||
|
// A_ROOM_ID should still be filtered out even though it's in the new emission
|
||||||
|
assertThat(afterFlowUpdate.children.map { it.roomId }).doesNotContain(A_ROOM_ID)
|
||||||
|
// But the other rooms should be present
|
||||||
|
assertThat(afterFlowUpdate.children.map { it.roomId }).contains(A_ROOM_ID_2)
|
||||||
|
assertThat(afterFlowUpdate.children.map { it.roomId }).contains(A_ROOM_ID_3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun TestScope.createSpacePresenter(
|
private fun TestScope.createSpacePresenter(
|
||||||
client: MatrixClient = FakeMatrixClient(),
|
client: MatrixClient = FakeMatrixClient(),
|
||||||
room: BaseRoom = FakeBaseRoom(),
|
room: BaseRoom = FakeBaseRoom(),
|
||||||
|
|
@ -365,6 +562,7 @@ class SpacePresenterTest {
|
||||||
),
|
),
|
||||||
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
|
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
|
||||||
spaceSettingsEnabled: Boolean = false,
|
spaceSettingsEnabled: Boolean = false,
|
||||||
|
spaceService: FakeSpaceService = FakeSpaceService(),
|
||||||
): SpacePresenter {
|
): SpacePresenter {
|
||||||
return SpacePresenter(
|
return SpacePresenter(
|
||||||
client = client,
|
client = client,
|
||||||
|
|
@ -379,6 +577,7 @@ class SpacePresenterTest {
|
||||||
FeatureFlags.SpaceSettings.key to spaceSettingsEnabled,
|
FeatureFlags.SpaceSettings.key to spaceSettingsEnabled,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
spaceService = spaceService,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,17 +10,19 @@ package io.element.android.features.space.impl.root
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomType
|
||||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_3
|
import io.element.android.libraries.matrix.test.A_ROOM_ID_3
|
||||||
|
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class SpaceStateTest {
|
class SpaceStateTest {
|
||||||
@Test
|
@Test
|
||||||
fun `test default state`() {
|
fun `test default state`() {
|
||||||
val state = aSpaceState()
|
val state = aSpaceState()
|
||||||
assertThat(state.hasAnyFailure).isFalse()
|
assertThat(state.hasAnyJoinFailures).isFalse()
|
||||||
assertThat(state.isJoining(A_ROOM_ID)).isFalse()
|
assertThat(state.isJoining(A_ROOM_ID)).isFalse()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,7 +35,7 @@ class SpaceStateTest {
|
||||||
A_ROOM_ID_3 to AsyncAction.Success(Unit),
|
A_ROOM_ID_3 to AsyncAction.Success(Unit),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assertThat(state.hasAnyFailure).isTrue()
|
assertThat(state.hasAnyJoinFailures).isTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -45,4 +47,80 @@ class SpaceStateTest {
|
||||||
)
|
)
|
||||||
assertThat(state.isJoining(A_ROOM_ID)).isTrue()
|
assertThat(state.isJoining(A_ROOM_ID)).isTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test isSelected returns true for selected room`() {
|
||||||
|
val state = aSpaceState(
|
||||||
|
selectedRoomIds = setOf(A_ROOM_ID)
|
||||||
|
)
|
||||||
|
assertThat(state.isSelected(A_ROOM_ID)).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test isSelected returns false for non-selected room`() {
|
||||||
|
val state = aSpaceState(
|
||||||
|
selectedRoomIds = setOf(A_ROOM_ID)
|
||||||
|
)
|
||||||
|
assertThat(state.isSelected(A_ROOM_ID_2)).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test showManageRoomsAction true when canManageRooms and has room children`() {
|
||||||
|
val state = aSpaceState(
|
||||||
|
canManageRooms = true,
|
||||||
|
children = listOf(aSpaceRoom(roomType = RoomType.Room))
|
||||||
|
)
|
||||||
|
assertThat(state.showManageRoomsAction).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test showManageRoomsAction false when canManageRooms but children empty`() {
|
||||||
|
val state = aSpaceState(
|
||||||
|
canManageRooms = true,
|
||||||
|
children = emptyList()
|
||||||
|
)
|
||||||
|
assertThat(state.showManageRoomsAction).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test showManageRoomsAction false when canManageRooms but only space children`() {
|
||||||
|
val state = aSpaceState(
|
||||||
|
canManageRooms = true,
|
||||||
|
children = listOf(aSpaceRoom(roomType = RoomType.Space))
|
||||||
|
)
|
||||||
|
assertThat(state.showManageRoomsAction).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test showManageRoomsAction false when has room children but canManageRooms false`() {
|
||||||
|
val state = aSpaceState(
|
||||||
|
canManageRooms = false,
|
||||||
|
children = listOf(aSpaceRoom(roomType = RoomType.Room))
|
||||||
|
)
|
||||||
|
assertThat(state.showManageRoomsAction).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test selectedCount returns correct count`() {
|
||||||
|
val state = aSpaceState(
|
||||||
|
selectedRoomIds = setOf(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3)
|
||||||
|
)
|
||||||
|
assertThat(state.selectedCount).isEqualTo(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test isRemoveButtonEnabled true when selectedRoomIds not empty`() {
|
||||||
|
val state = aSpaceState(
|
||||||
|
selectedRoomIds = setOf(A_ROOM_ID)
|
||||||
|
)
|
||||||
|
assertThat(state.isRemoveButtonEnabled).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test isRemoveButtonEnabled false when selectedRoomIds empty`() {
|
||||||
|
val state = aSpaceState(
|
||||||
|
selectedRoomIds = emptySet()
|
||||||
|
)
|
||||||
|
assertThat(state.isRemoveButtonEnabled).isFalse()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,13 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
import androidx.compose.ui.test.onNodeWithText
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_TOPIC
|
import io.element.android.libraries.matrix.test.A_ROOM_TOPIC
|
||||||
|
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||||
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||||
|
|
@ -29,6 +31,7 @@ import io.element.android.tests.testutils.clickOn
|
||||||
import io.element.android.tests.testutils.ensureCalledOnce
|
import io.element.android.tests.testutils.ensureCalledOnce
|
||||||
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
||||||
import io.element.android.tests.testutils.pressBack
|
import io.element.android.tests.testutils.pressBack
|
||||||
|
import io.element.android.tests.testutils.pressBackKey
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.rules.TestRule
|
import org.junit.rules.TestRule
|
||||||
|
|
@ -124,7 +127,7 @@ class SpaceViewTest {
|
||||||
val eventsRecorder = EventsRecorder<SpaceEvents>()
|
val eventsRecorder = EventsRecorder<SpaceEvents>()
|
||||||
rule.setSpaceView(
|
rule.setSpaceView(
|
||||||
aSpaceState(
|
aSpaceState(
|
||||||
parentSpace = aSpaceRoom(topic = A_ROOM_TOPIC),
|
spaceInfo = aRoomInfo(topic = A_ROOM_TOPIC),
|
||||||
hasMoreToLoad = false,
|
hasMoreToLoad = false,
|
||||||
eventSink = eventsRecorder,
|
eventSink = eventsRecorder,
|
||||||
)
|
)
|
||||||
|
|
@ -132,6 +135,71 @@ class SpaceViewTest {
|
||||||
rule.onNodeWithText(A_ROOM_TOPIC).performClick()
|
rule.onNodeWithText(A_ROOM_TOPIC).performClick()
|
||||||
eventsRecorder.assertSingle(SpaceEvents.ShowTopicViewer(A_ROOM_TOPIC))
|
eventsRecorder.assertSingle(SpaceEvents.ShowTopicViewer(A_ROOM_TOPIC))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking back in manage mode emits ExitManageMode event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<SpaceEvents>()
|
||||||
|
rule.setSpaceView(
|
||||||
|
aSpaceState(
|
||||||
|
hasMoreToLoad = false,
|
||||||
|
isManageMode = true,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
rule.pressBackKey()
|
||||||
|
eventsRecorder.assertSingle(SpaceEvents.ExitManageMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on room in manage mode emits ToggleRoomSelection event`() {
|
||||||
|
val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, displayName = A_ROOM_NAME)
|
||||||
|
val eventsRecorder = EventsRecorder<SpaceEvents>()
|
||||||
|
rule.setSpaceView(
|
||||||
|
aSpaceState(
|
||||||
|
children = listOf(aSpaceRoom),
|
||||||
|
hasMoreToLoad = false,
|
||||||
|
isManageMode = true,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
rule.onNodeWithText(A_ROOM_NAME).performClick()
|
||||||
|
eventsRecorder.assertSingle(SpaceEvents.ToggleRoomSelection(A_ROOM_ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking remove button emits RemoveSelectedRooms event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<SpaceEvents>()
|
||||||
|
rule.setSpaceView(
|
||||||
|
aSpaceState(
|
||||||
|
children = listOf(aSpaceRoom(roomId = A_ROOM_ID)),
|
||||||
|
hasMoreToLoad = false,
|
||||||
|
isManageMode = true,
|
||||||
|
selectedRoomIds = setOf(A_ROOM_ID),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_remove)
|
||||||
|
eventsRecorder.assertSingle(SpaceEvents.RemoveSelectedRooms)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Config(qualifiers = "h1024dp")
|
||||||
|
@Test
|
||||||
|
fun `clicking confirm in removal dialog emits ConfirmRoomRemoval event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<SpaceEvents>()
|
||||||
|
rule.setSpaceView(
|
||||||
|
aSpaceState(
|
||||||
|
children = listOf(aSpaceRoom(roomId = A_ROOM_ID)),
|
||||||
|
hasMoreToLoad = false,
|
||||||
|
isManageMode = true,
|
||||||
|
selectedRoomIds = setOf(A_ROOM_ID),
|
||||||
|
removeRoomsAction = AsyncAction.ConfirmingNoParams,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
// Click on the Remove button in the confirmation dialog
|
||||||
|
rule.clickOn(CommonStrings.action_remove, inDialog = true)
|
||||||
|
eventsRecorder.assertSingle(SpaceEvents.ConfirmRoomRemoval)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSpaceView(
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSpaceView(
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ camera = "1.5.2"
|
||||||
work = "2.11.0"
|
work = "2.11.0"
|
||||||
|
|
||||||
# Compose
|
# Compose
|
||||||
compose_bom = "2025.12.01"
|
compose_bom = "2026.01.00"
|
||||||
|
|
||||||
# Coroutines
|
# Coroutines
|
||||||
coroutines = "1.10.2"
|
coroutines = "1.10.2"
|
||||||
|
|
@ -78,7 +78,7 @@ kotlinpoet-ksp = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlin
|
||||||
kover_gradle_plugin = { module = "org.jetbrains.kotlinx:kover-gradle-plugin", version.ref = "kover" }
|
kover_gradle_plugin = { module = "org.jetbrains.kotlinx:kover-gradle-plugin", version.ref = "kover" }
|
||||||
ksp_gradle_plugin = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" }
|
ksp_gradle_plugin = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" }
|
||||||
# https://firebase.google.com/docs/android/setup#available-libraries
|
# https://firebase.google.com/docs/android/setup#available-libraries
|
||||||
google_firebase_bom = "com.google.firebase:firebase-bom:34.7.0"
|
google_firebase_bom = "com.google.firebase:firebase-bom:34.8.0"
|
||||||
firebase_appdistribution_gradle = { module = "com.google.firebase:firebase-appdistribution-gradle", version.ref = "firebaseAppDistribution" }
|
firebase_appdistribution_gradle = { module = "com.google.firebase:firebase-appdistribution-gradle", version.ref = "firebaseAppDistribution" }
|
||||||
autonomousapps_dependencyanalysis_plugin = { module = "com.autonomousapps:dependency-analysis-gradle-plugin", version.ref = "dependencyAnalysis" }
|
autonomousapps_dependencyanalysis_plugin = { module = "com.autonomousapps:dependency-analysis-gradle-plugin", version.ref = "dependencyAnalysis" }
|
||||||
ksp_plugin = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }
|
ksp_plugin = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }
|
||||||
|
|
@ -109,7 +109,7 @@ androidx_media3_ui = { module = "androidx.media3:media3-ui", version.ref = "medi
|
||||||
androidx_media3_transformer = { module = "androidx.media3:media3-transformer", version.ref = "media3" }
|
androidx_media3_transformer = { module = "androidx.media3:media3-transformer", version.ref = "media3" }
|
||||||
androidx_media3_effect = { module = "androidx.media3:media3-effect", version.ref = "media3" }
|
androidx_media3_effect = { module = "androidx.media3:media3-effect", version.ref = "media3" }
|
||||||
androidx_media3_common = { module = "androidx.media3:media3-common", version.ref = "media3" }
|
androidx_media3_common = { module = "androidx.media3:media3-common", version.ref = "media3" }
|
||||||
androidx_biometric = "androidx.biometric:biometric-ktx:1.2.0-alpha05"
|
androidx_biometric = "androidx.biometric:biometric-ktx:1.4.0-alpha02"
|
||||||
|
|
||||||
androidx_activity_activity = { module = "androidx.activity:activity", version.ref = "activity" }
|
androidx_activity_activity = { module = "androidx.activity:activity", version.ref = "activity" }
|
||||||
androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "activity" }
|
androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "activity" }
|
||||||
|
|
@ -219,7 +219,7 @@ color_picker = "io.mhssn:colorpicker:1.0.0"
|
||||||
|
|
||||||
# Analytics
|
# Analytics
|
||||||
posthog = "com.posthog:posthog-android:3.28.1"
|
posthog = "com.posthog:posthog-android:3.28.1"
|
||||||
sentry = "io.sentry:sentry-android:8.29.0"
|
sentry = "io.sentry:sentry-android:8.30.0"
|
||||||
# main branch can be tested replacing the version with main-SNAPSHOT
|
# main branch can be tested replacing the version with main-SNAPSHOT
|
||||||
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.29.2"
|
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.29.2"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,8 @@ fun String.containsRtLOverride() = contains(RTL_OVERRIDE_CHAR)
|
||||||
|
|
||||||
fun String.filterDirectionOverrides() = filterNot { it == RTL_OVERRIDE_CHAR || it == LTR_OVERRIDE_CHAR }
|
fun String.filterDirectionOverrides() = filterNot { it == RTL_OVERRIDE_CHAR || it == LTR_OVERRIDE_CHAR }
|
||||||
|
|
||||||
|
const val DEFAULT_SAFE_LENGTH = 500
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This works around https://github.com/element-hq/element-x-android/issues/2105.
|
* This works around https://github.com/element-hq/element-x-android/issues/2105.
|
||||||
* @param maxLength Max characters to retrieve. Defaults to `500`.
|
* @param maxLength Max characters to retrieve. Defaults to `500`.
|
||||||
|
|
@ -107,7 +109,7 @@ fun String.filterDirectionOverrides() = filterNot { it == RTL_OVERRIDE_CHAR || i
|
||||||
* @return The string truncated to [maxLength] characters, with an optional ellipsis if larger.
|
* @return The string truncated to [maxLength] characters, with an optional ellipsis if larger.
|
||||||
*/
|
*/
|
||||||
fun String.toSafeLength(
|
fun String.toSafeLength(
|
||||||
maxLength: Int = 500,
|
maxLength: Int = DEFAULT_SAFE_LENGTH,
|
||||||
ellipsize: Boolean = false,
|
ellipsize: Boolean = false,
|
||||||
): String {
|
): String {
|
||||||
return if (ellipsize) {
|
return if (ellipsize) {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
package io.element.android.libraries.eventformatter.impl
|
package io.element.android.libraries.eventformatter.impl
|
||||||
|
|
||||||
import dev.zacsweers.metro.ContributesBinding
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
|
import io.element.android.libraries.core.extensions.DEFAULT_SAFE_LENGTH
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.eventformatter.api.RoomLatestEventFormatter
|
import io.element.android.libraries.eventformatter.api.RoomLatestEventFormatter
|
||||||
import io.element.android.libraries.eventformatter.impl.mode.RenderingMode
|
import io.element.android.libraries.eventformatter.impl.mode.RenderingMode
|
||||||
|
|
@ -54,11 +55,6 @@ class DefaultRoomLatestEventFormatter(
|
||||||
private val stateContentFormatter: StateContentFormatter,
|
private val stateContentFormatter: StateContentFormatter,
|
||||||
private val permalinkParser: PermalinkParser,
|
private val permalinkParser: PermalinkParser,
|
||||||
) : RoomLatestEventFormatter {
|
) : RoomLatestEventFormatter {
|
||||||
companion object {
|
|
||||||
// Max characters to display in the last message. This works around https://github.com/element-hq/element-x-android/issues/2105
|
|
||||||
private const val MAX_SAFE_LENGTH = 500
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun format(
|
override fun format(
|
||||||
latestEvent: LatestEventValue.Local,
|
latestEvent: LatestEventValue.Local,
|
||||||
isDmRoom: Boolean,
|
isDmRoom: Boolean,
|
||||||
|
|
@ -121,7 +117,7 @@ class DefaultRoomLatestEventFormatter(
|
||||||
}
|
}
|
||||||
is LegacyCallInviteContent -> sp.getString(CommonStrings.common_unsupported_call)
|
is LegacyCallInviteContent -> sp.getString(CommonStrings.common_unsupported_call)
|
||||||
is CallNotifyContent -> sp.getString(CommonStrings.common_call_started)
|
is CallNotifyContent -> sp.getString(CommonStrings.common_call_started)
|
||||||
}?.take(MAX_SAFE_LENGTH)
|
}?.take(DEFAULT_SAFE_LENGTH)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MessageContent.process(
|
private fun MessageContent.process(
|
||||||
|
|
|
||||||
|
|
@ -133,4 +133,11 @@ enum class FeatureFlags(
|
||||||
defaultValue = { false },
|
defaultValue = { false },
|
||||||
isFinished = false,
|
isFinished = false,
|
||||||
),
|
),
|
||||||
|
SignInWithClassic(
|
||||||
|
key = "feature.signin_with_classic",
|
||||||
|
title = "Sign in with Element Classic",
|
||||||
|
description = "Allow the application to sign in to the current Element Classic account.",
|
||||||
|
defaultValue = { false },
|
||||||
|
isFinished = false,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -214,7 +214,15 @@ interface MatrixClient {
|
||||||
*/
|
*/
|
||||||
fun createLinkDesktopHandler(): Result<LinkDesktopHandler>
|
fun createLinkDesktopHandler(): Result<LinkDesktopHandler>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a database optimization that should flush cached data and improve performance.
|
||||||
|
*/
|
||||||
suspend fun performDatabaseVacuum(): Result<Unit>
|
suspend fun performDatabaseVacuum(): Result<Unit>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the cached client `well-known` config by the SDK.
|
||||||
|
*/
|
||||||
|
suspend fun resetWellKnownConfig(): Result<Unit>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -22,4 +22,12 @@ interface SpaceService {
|
||||||
fun spaceRoomList(id: RoomId): SpaceRoomList
|
fun spaceRoomList(id: RoomId): SpaceRoomList
|
||||||
|
|
||||||
fun getLeaveSpaceHandle(spaceId: RoomId): LeaveSpaceHandle
|
fun getLeaveSpaceHandle(spaceId: RoomId): LeaveSpaceHandle
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a child room from a space.
|
||||||
|
* @param spaceId The space ID from which to remove the child.
|
||||||
|
* @param childId The room ID of the child to remove.
|
||||||
|
* @return A result indicating success or failure.
|
||||||
|
*/
|
||||||
|
suspend fun removeChildFromSpace(spaceId: RoomId, childId: RoomId): Result<Unit>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -791,6 +791,13 @@ class RustMatrixClient(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun resetWellKnownConfig(): Result<Unit> {
|
||||||
|
return runCatchingExceptions {
|
||||||
|
Timber.d("Resetting well-known config for session $sessionId")
|
||||||
|
innerClient.resetWellKnown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun getCacheSize(
|
private suspend fun getCacheSize(
|
||||||
includeCryptoDb: Boolean = false,
|
includeCryptoDb: Boolean = false,
|
||||||
): Long = withContext(sessionDispatcher) {
|
): Long = withContext(sessionDispatcher) {
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,12 @@ class RustSpaceService(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun removeChildFromSpace(spaceId: RoomId, childId: RoomId): Result<Unit> = withContext(sessionDispatcher) {
|
||||||
|
runCatchingExceptions {
|
||||||
|
innerSpaceService.removeChildFromSpace(childId = childId.value, spaceId = spaceId.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
innerSpaceService
|
innerSpaceService
|
||||||
.spaceListUpdate()
|
.spaceListUpdate()
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,7 @@ class FakeMatrixClient(
|
||||||
private val markRoomAsFullyReadResult: (RoomId, EventId) -> Result<Unit> = { _, _ -> lambdaError() },
|
private val markRoomAsFullyReadResult: (RoomId, EventId) -> Result<Unit> = { _, _ -> lambdaError() },
|
||||||
private val performDatabaseVacuumLambda: () -> Result<Unit> = { lambdaError() },
|
private val performDatabaseVacuumLambda: () -> Result<Unit> = { lambdaError() },
|
||||||
private val getDatabaseSizesLambda: () -> Result<SdkStoreSizes> = { lambdaError() },
|
private val getDatabaseSizesLambda: () -> Result<SdkStoreSizes> = { lambdaError() },
|
||||||
|
private val resetWellKnownConfigLambda: () -> Result<Unit> = { lambdaError() },
|
||||||
) : MatrixClient {
|
) : MatrixClient {
|
||||||
var setDisplayNameCalled: Boolean = false
|
var setDisplayNameCalled: Boolean = false
|
||||||
private set
|
private set
|
||||||
|
|
@ -379,4 +380,8 @@ class FakeMatrixClient(
|
||||||
override fun createLinkMobileHandler(): Result<LinkMobileHandler> {
|
override fun createLinkMobileHandler(): Result<LinkMobileHandler> {
|
||||||
return createLinkMobileHandlerResult()
|
return createLinkMobileHandlerResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun resetWellKnownConfig(): Result<Unit> {
|
||||||
|
return resetWellKnownConfigLambda()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ class FakeSpaceService(
|
||||||
private val joinedSpacesResult: () -> Result<List<SpaceRoom>> = { lambdaError() },
|
private val joinedSpacesResult: () -> Result<List<SpaceRoom>> = { lambdaError() },
|
||||||
private val spaceRoomListResult: (RoomId) -> SpaceRoomList = { lambdaError() },
|
private val spaceRoomListResult: (RoomId) -> SpaceRoomList = { lambdaError() },
|
||||||
private val leaveSpaceHandleResult: (RoomId) -> LeaveSpaceHandle = { lambdaError() },
|
private val leaveSpaceHandleResult: (RoomId) -> LeaveSpaceHandle = { lambdaError() },
|
||||||
|
private val removeChildFromSpaceResult: (RoomId, RoomId) -> Result<Unit> = { _, _ -> lambdaError() },
|
||||||
private val joinedParentsResult: (RoomId) -> Result<List<SpaceRoom>> = { lambdaError() },
|
private val joinedParentsResult: (RoomId) -> Result<List<SpaceRoom>> = { lambdaError() },
|
||||||
private val getSpaceRoomResult: (RoomId) -> SpaceRoom? = { lambdaError() },
|
private val getSpaceRoomResult: (RoomId) -> SpaceRoom? = { lambdaError() },
|
||||||
) : SpaceService {
|
) : SpaceService {
|
||||||
|
|
@ -53,4 +54,8 @@ class FakeSpaceService(
|
||||||
override fun getLeaveSpaceHandle(spaceId: RoomId): LeaveSpaceHandle {
|
override fun getLeaveSpaceHandle(spaceId: RoomId): LeaveSpaceHandle {
|
||||||
return leaveSpaceHandleResult(spaceId)
|
return leaveSpaceHandleResult(spaceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun removeChildFromSpace(spaceId: RoomId, childId: RoomId): Result<Unit> = simulateLongTask {
|
||||||
|
removeChildFromSpaceResult(spaceId, childId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -333,7 +333,6 @@
|
||||||
<string name="screen_share_this_location_action">"Споделяне на това местоположение"</string>
|
<string name="screen_share_this_location_action">"Споделяне на това местоположение"</string>
|
||||||
<string name="screen_space_list_parent_space">"%1$s пространство"</string>
|
<string name="screen_space_list_parent_space">"%1$s пространство"</string>
|
||||||
<string name="screen_space_list_title">"Пространства"</string>
|
<string name="screen_space_list_title">"Пространства"</string>
|
||||||
<string name="screen_space_menu_action_members">"Преглед на членовете"</string>
|
|
||||||
<string name="screen_view_location_title">"Местоположение"</string>
|
<string name="screen_view_location_title">"Местоположение"</string>
|
||||||
<string name="settings_version_number">"Версия: %1$s (%2$s)"</string>
|
<string name="settings_version_number">"Версия: %1$s (%2$s)"</string>
|
||||||
<string name="test_language_identifier">"bg"</string>
|
<string name="test_language_identifier">"bg"</string>
|
||||||
|
|
|
||||||
|
|
@ -483,7 +483,6 @@ Opravdu chcete pokračovat?"</string>
|
||||||
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
||||||
<string name="screen_space_list_parent_space">"%1$s prostor"</string>
|
<string name="screen_space_list_parent_space">"%1$s prostor"</string>
|
||||||
<string name="screen_space_list_title">"Prostory"</string>
|
<string name="screen_space_list_title">"Prostory"</string>
|
||||||
<string name="screen_space_menu_action_members">"Zobrazit členy"</string>
|
|
||||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Zpráva nebyla odeslána, protože ověřená identita uživatele %1$s se změnila."</string>
|
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Zpráva nebyla odeslána, protože ověřená identita uživatele %1$s se změnila."</string>
|
||||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Zpráva nebyla odeslána, protože%1$s neověřil(a) všechna zařízení."</string>
|
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Zpráva nebyla odeslána, protože%1$s neověřil(a) všechna zařízení."</string>
|
||||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Zpráva nebyla odeslána, protože jste neověřili jedno nebo více zařízení."</string>
|
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Zpráva nebyla odeslána, protože jste neověřili jedno nebo více zařízení."</string>
|
||||||
|
|
|
||||||
|
|
@ -469,7 +469,6 @@ Er du sikker på, at du vil fortsætte?"</string>
|
||||||
<string name="screen_space_list_details">"%1$s•%2$s"</string>
|
<string name="screen_space_list_details">"%1$s•%2$s"</string>
|
||||||
<string name="screen_space_list_parent_space">"%1$s gruppe"</string>
|
<string name="screen_space_list_parent_space">"%1$s gruppe"</string>
|
||||||
<string name="screen_space_list_title">"Grupper"</string>
|
<string name="screen_space_list_title">"Grupper"</string>
|
||||||
<string name="screen_space_menu_action_members">"Vis medlemmer"</string>
|
|
||||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Beskeden blev ikke sendt fordi %1$s s bekræftede identitet blev nulstillet."</string>
|
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Beskeden blev ikke sendt fordi %1$s s bekræftede identitet blev nulstillet."</string>
|
||||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Meddelelsen er ikke sendt, fordi %1$s ikke har bekræftet alle enheder."</string>
|
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Meddelelsen er ikke sendt, fordi %1$s ikke har bekræftet alle enheder."</string>
|
||||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Beskeden er ikke sendt, fordi du ikke har verificeret en eller flere af dine enheder."</string>
|
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Beskeden er ikke sendt, fordi du ikke har verificeret en eller flere af dine enheder."</string>
|
||||||
|
|
|
||||||
|
|
@ -483,12 +483,6 @@ Möchtest du wirklich fortfahren?"</string>
|
||||||
<string name="screen_space_list_empty_state_title">"Erstelle einen Space, um Chats zu organisieren"</string>
|
<string name="screen_space_list_empty_state_title">"Erstelle einen Space, um Chats zu organisieren"</string>
|
||||||
<string name="screen_space_list_parent_space">"%1$s Space"</string>
|
<string name="screen_space_list_parent_space">"%1$s Space"</string>
|
||||||
<string name="screen_space_list_title">"Spaces"</string>
|
<string name="screen_space_list_title">"Spaces"</string>
|
||||||
<string name="screen_space_menu_action_members">"Mitglieder anzeigen"</string>
|
|
||||||
<string name="screen_space_remove_rooms_confirmation_content">"Das Entfernen eines Chats hat keinen Einfluss auf die Beitrittsregeln. Um die Regeln zu ändern, gehe zu \"Raum Info\" und dann zu \"Datenschutz und Sicherheit\""</string>
|
|
||||||
<plurals name="screen_space_remove_rooms_confirmation_title">
|
|
||||||
<item quantity="one">"Chat aus %1$s entfernen"</item>
|
|
||||||
<item quantity="other">"%1$d chats aus %2$s entfernen"</item>
|
|
||||||
</plurals>
|
|
||||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Nachricht nicht gesendet, weil sich die verifizierte Identität von %1$s geändert hat."</string>
|
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Nachricht nicht gesendet, weil sich die verifizierte Identität von %1$s geändert hat."</string>
|
||||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Die Nachricht wurde nicht gesendet, weil %1$s nicht alle Geräte verifiziert hat."</string>
|
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Die Nachricht wurde nicht gesendet, weil %1$s nicht alle Geräte verifiziert hat."</string>
|
||||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Die Nachricht wurde nicht gesendet, weil du eines oder mehrere deiner Geräte nicht verifiziert hast."</string>
|
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Die Nachricht wurde nicht gesendet, weil du eines oder mehrere deiner Geräte nicht verifiziert hast."</string>
|
||||||
|
|
|
||||||
|
|
@ -476,7 +476,6 @@ Kas sa oled kindel, et soovid jätkata?"</string>
|
||||||
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
||||||
<string name="screen_space_list_parent_space">"Kogukond: %1$s"</string>
|
<string name="screen_space_list_parent_space">"Kogukond: %1$s"</string>
|
||||||
<string name="screen_space_list_title">"Kogukonnad"</string>
|
<string name="screen_space_list_title">"Kogukonnad"</string>
|
||||||
<string name="screen_space_menu_action_members">"Vaata liikmeid"</string>
|
|
||||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Sõnum on saatmata, kuna kasutaja %1$s verifitseeritud identiteet on lähtestatud."</string>
|
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Sõnum on saatmata, kuna kasutaja %1$s verifitseeritud identiteet on lähtestatud."</string>
|
||||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Sõnum on saatmata, kuna %1$s pole verifitseerinud kõiki oma seadmeid."</string>
|
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Sõnum on saatmata, kuna %1$s pole verifitseerinud kõiki oma seadmeid."</string>
|
||||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Kuna sa pole üks või enamgi oma seadet verifitseerinud, siis sinu sõnum on saatmata."</string>
|
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Kuna sa pole üks või enamgi oma seadet verifitseerinud, siis sinu sõnum on saatmata."</string>
|
||||||
|
|
|
||||||
|
|
@ -400,7 +400,6 @@
|
||||||
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
||||||
<string name="screen_space_list_parent_space">"%1$s فضا"</string>
|
<string name="screen_space_list_parent_space">"%1$s فضا"</string>
|
||||||
<string name="screen_space_list_title">"فضاها"</string>
|
<string name="screen_space_list_title">"فضاها"</string>
|
||||||
<string name="screen_space_menu_action_members">"دیدن اعضا"</string>
|
|
||||||
<string name="screen_view_location_title">"مکان"</string>
|
<string name="screen_view_location_title">"مکان"</string>
|
||||||
<string name="settings_version_number">"نگارش : %1$s (%2$s)"</string>
|
<string name="settings_version_number">"نگارش : %1$s (%2$s)"</string>
|
||||||
<string name="test_language_identifier">"fa"</string>
|
<string name="test_language_identifier">"fa"</string>
|
||||||
|
|
|
||||||
|
|
@ -470,7 +470,6 @@ Haluatko varmasti jatkaa?"</string>
|
||||||
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
||||||
<string name="screen_space_list_parent_space">"%1$s tila"</string>
|
<string name="screen_space_list_parent_space">"%1$s tila"</string>
|
||||||
<string name="screen_space_list_title">"Tilat"</string>
|
<string name="screen_space_list_title">"Tilat"</string>
|
||||||
<string name="screen_space_menu_action_members">"Näytä jäsenet"</string>
|
|
||||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Viestiä ei lähetetty, koska käyttäjän %1$s vahvistettu identiteetti nollattiin."</string>
|
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Viestiä ei lähetetty, koska käyttäjän %1$s vahvistettu identiteetti nollattiin."</string>
|
||||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Viestiä ei lähetetty, koska %1$s ei ole vahvistanut kaikkia laitteitaan."</string>
|
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Viestiä ei lähetetty, koska %1$s ei ole vahvistanut kaikkia laitteitaan."</string>
|
||||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Viestiä ei lähetetty, koska et ole vahvistanut yhtä tai useampaa laitettasi."</string>
|
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Viestiä ei lähetetty, koska et ole vahvistanut yhtä tai useampaa laitettasi."</string>
|
||||||
|
|
|
||||||
|
|
@ -483,7 +483,6 @@ Raison : %1$s."</string>
|
||||||
<string name="screen_space_list_empty_state_title">"Créer des espaces pour organiser les salons"</string>
|
<string name="screen_space_list_empty_state_title">"Créer des espaces pour organiser les salons"</string>
|
||||||
<string name="screen_space_list_parent_space">"Espace %1$s"</string>
|
<string name="screen_space_list_parent_space">"Espace %1$s"</string>
|
||||||
<string name="screen_space_list_title">"Espaces"</string>
|
<string name="screen_space_list_title">"Espaces"</string>
|
||||||
<string name="screen_space_menu_action_members">"Voir les membres"</string>
|
|
||||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Le message n’a pas été envoyé car l’identité vérifiée de %1$s a été réinitialisée."</string>
|
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Le message n’a pas été envoyé car l’identité vérifiée de %1$s a été réinitialisée."</string>
|
||||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Le message n’a pas été envoyé car %1$s n’a pas vérifié tous ses appareils."</string>
|
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Le message n’a pas été envoyé car %1$s n’a pas vérifié tous ses appareils."</string>
|
||||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Message non envoyé car vous n’avez pas vérifié tous vos appareils."</string>
|
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Message non envoyé car vous n’avez pas vérifié tous vos appareils."</string>
|
||||||
|
|
|
||||||
|
|
@ -485,7 +485,6 @@ Jeste li sigurni da želite nastaviti?"</string>
|
||||||
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
||||||
<string name="screen_space_list_parent_space">"Prostor %1$s"</string>
|
<string name="screen_space_list_parent_space">"Prostor %1$s"</string>
|
||||||
<string name="screen_space_list_title">"Prostori"</string>
|
<string name="screen_space_list_title">"Prostori"</string>
|
||||||
<string name="screen_space_menu_action_members">"Prikaži članove"</string>
|
|
||||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Poruka nije poslana jer je poništen potvrđeni identitet korisnika %1$s."</string>
|
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Poruka nije poslana jer je poništen potvrđeni identitet korisnika %1$s."</string>
|
||||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Poruka nije poslana jer %1$s nije potvrdio sve uređaje."</string>
|
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Poruka nije poslana jer %1$s nije potvrdio sve uređaje."</string>
|
||||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Poruka nije poslana jer niste potvrdili jedan svoj uređaj ili više njih."</string>
|
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Poruka nije poslana jer niste potvrdili jedan svoj uređaj ili više njih."</string>
|
||||||
|
|
|
||||||
|
|
@ -469,7 +469,6 @@ Biztos, hogy folytatja?"</string>
|
||||||
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
||||||
<string name="screen_space_list_parent_space">"%1$s tér"</string>
|
<string name="screen_space_list_parent_space">"%1$s tér"</string>
|
||||||
<string name="screen_space_list_title">"Terek"</string>
|
<string name="screen_space_list_title">"Terek"</string>
|
||||||
<string name="screen_space_menu_action_members">"Tagok megtekintése"</string>
|
|
||||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Az üzenet nem lett elküldve, mert %1$s ellenőrzött személyazonossága megváltozott."</string>
|
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Az üzenet nem lett elküldve, mert %1$s ellenőrzött személyazonossága megváltozott."</string>
|
||||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Az üzenet nem lett elküldve, mert %1$s nem ellenőrizte az összes eszközét."</string>
|
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Az üzenet nem lett elküldve, mert %1$s nem ellenőrizte az összes eszközét."</string>
|
||||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Az üzenet nem lett elküldve, mert egy vagy több eszközét nem ellenőrizte."</string>
|
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Az üzenet nem lett elküldve, mert egy vagy több eszközét nem ellenőrizte."</string>
|
||||||
|
|
|
||||||
|
|
@ -470,7 +470,6 @@ Sei sicuro di voler continuare?"</string>
|
||||||
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
||||||
<string name="screen_space_list_parent_space">"%1$s spazio"</string>
|
<string name="screen_space_list_parent_space">"%1$s spazio"</string>
|
||||||
<string name="screen_space_list_title">"Spazi"</string>
|
<string name="screen_space_list_title">"Spazi"</string>
|
||||||
<string name="screen_space_menu_action_members">"Visualizza membri"</string>
|
|
||||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Messaggio non inviato perché l\'identità verificata di %1$s è stata reimpostata."</string>
|
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Messaggio non inviato perché l\'identità verificata di %1$s è stata reimpostata."</string>
|
||||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Messaggio non inviato perché %1$s non ha verificato tutti i dispositivi."</string>
|
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Messaggio non inviato perché %1$s non ha verificato tutti i dispositivi."</string>
|
||||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Messaggio non inviato perché non hai verificato uno o più dispositivi."</string>
|
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Messaggio non inviato perché non hai verificato uno o più dispositivi."</string>
|
||||||
|
|
|
||||||
|
|
@ -467,7 +467,6 @@ Er du sikker på at du vil fortsette?"</string>
|
||||||
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
||||||
<string name="screen_space_list_parent_space">"%1$s område"</string>
|
<string name="screen_space_list_parent_space">"%1$s område"</string>
|
||||||
<string name="screen_space_list_title">"Områder"</string>
|
<string name="screen_space_list_title">"Områder"</string>
|
||||||
<string name="screen_space_menu_action_members">"Vis medlemmer"</string>
|
|
||||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Meldingen ble ikke sendt fordi %1$ss verifiserte identitet er tilbakestilt."</string>
|
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Meldingen ble ikke sendt fordi %1$ss verifiserte identitet er tilbakestilt."</string>
|
||||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Meldingen ble ikke sendt fordi %1$s ikke har verifisert alle enheter."</string>
|
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Meldingen ble ikke sendt fordi %1$s ikke har verifisert alle enheter."</string>
|
||||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Meldingen ble ikke sendt fordi du ikke har verifisert en eller flere av enhetene dine."</string>
|
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Meldingen ble ikke sendt fordi du ikke har verifisert en eller flere av enhetene dine."</string>
|
||||||
|
|
|
||||||
|
|
@ -479,7 +479,6 @@ Você tem certeza de que deseja continuar?"</string>
|
||||||
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
||||||
<string name="screen_space_list_parent_space">"Espaço %1$s"</string>
|
<string name="screen_space_list_parent_space">"Espaço %1$s"</string>
|
||||||
<string name="screen_space_list_title">"Espaços"</string>
|
<string name="screen_space_list_title">"Espaços"</string>
|
||||||
<string name="screen_space_menu_action_members">"Ver membros"</string>
|
|
||||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Mensagem não enviada porque a identidade verificada de %1$s foi redefinida."</string>
|
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Mensagem não enviada porque a identidade verificada de %1$s foi redefinida."</string>
|
||||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"A mensagem não foi enviada porque %1$s não verificou todos os dispositivos."</string>
|
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"A mensagem não foi enviada porque %1$s não verificou todos os dispositivos."</string>
|
||||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Mensagem não enviada porque você não verificou um ou mais dos seus dispositivos."</string>
|
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Mensagem não enviada porque você não verificou um ou mais dos seus dispositivos."</string>
|
||||||
|
|
|
||||||
|
|
@ -484,7 +484,6 @@ Sunteți sigur că doriți să continuați?"</string>
|
||||||
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
||||||
<string name="screen_space_list_parent_space">"Spațiu %1$s"</string>
|
<string name="screen_space_list_parent_space">"Spațiu %1$s"</string>
|
||||||
<string name="screen_space_list_title">"Spații"</string>
|
<string name="screen_space_list_title">"Spații"</string>
|
||||||
<string name="screen_space_menu_action_members">"Vizualizați membrii"</string>
|
|
||||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Mesajul nu a fost trimis deoarece identitatea verificată a lui %1$s s-a schimbat."</string>
|
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Mesajul nu a fost trimis deoarece identitatea verificată a lui %1$s s-a schimbat."</string>
|
||||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Mesajul nu a fost trimis deoarece %1$s nu a verificat toate dispozitivele."</string>
|
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Mesajul nu a fost trimis deoarece %1$s nu a verificat toate dispozitivele."</string>
|
||||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Mesajul nu a fost trimis deoarece nu ați verificat unul sau mai multe dispozitive."</string>
|
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Mesajul nu a fost trimis deoarece nu ați verificat unul sau mai multe dispozitive."</string>
|
||||||
|
|
|
||||||
|
|
@ -479,7 +479,6 @@
|
||||||
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
||||||
<string name="screen_space_list_parent_space">"%1$s пространство"</string>
|
<string name="screen_space_list_parent_space">"%1$s пространство"</string>
|
||||||
<string name="screen_space_list_title">"Пространства"</string>
|
<string name="screen_space_list_title">"Пространства"</string>
|
||||||
<string name="screen_space_menu_action_members">"Просмотреть участников"</string>
|
|
||||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Сообщение не отправлено, потому что подтвержденная личность %1$s была сброшена."</string>
|
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Сообщение не отправлено, потому что подтвержденная личность %1$s была сброшена."</string>
|
||||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Сообщение не отправлено, потому что %1$s не проверил одно или несколько устройств."</string>
|
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Сообщение не отправлено, потому что %1$s не проверил одно или несколько устройств."</string>
|
||||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Сообщение не отправлено, поскольку вы не подтвердили одно или несколько своих устройств."</string>
|
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Сообщение не отправлено, поскольку вы не подтвердили одно или несколько своих устройств."</string>
|
||||||
|
|
|
||||||
|
|
@ -481,7 +481,6 @@ Naozaj chcete pokračovať?"</string>
|
||||||
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
||||||
<string name="screen_space_list_parent_space">"%1$s priestor"</string>
|
<string name="screen_space_list_parent_space">"%1$s priestor"</string>
|
||||||
<string name="screen_space_list_title">"Priestory"</string>
|
<string name="screen_space_list_title">"Priestory"</string>
|
||||||
<string name="screen_space_menu_action_members">"Zobraziť členov"</string>
|
|
||||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Správa nebola odoslaná, pretože sa zmenila overená totožnosť používateľa %1$s."</string>
|
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Správa nebola odoslaná, pretože sa zmenila overená totožnosť používateľa %1$s."</string>
|
||||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Správa nebola odoslaná, pretože %1$s neoveril/a všetky zariadenia."</string>
|
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Správa nebola odoslaná, pretože %1$s neoveril/a všetky zariadenia."</string>
|
||||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Správa nebola odoslaná, pretože ste neoverili jedno alebo viac svojich zariadení."</string>
|
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Správa nebola odoslaná, pretože ste neoverili jedno alebo viac svojich zariadení."</string>
|
||||||
|
|
|
||||||
|
|
@ -461,7 +461,6 @@
|
||||||
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
||||||
<string name="screen_space_list_parent_space">"%1$s 空間"</string>
|
<string name="screen_space_list_parent_space">"%1$s 空間"</string>
|
||||||
<string name="screen_space_list_title">"空間"</string>
|
<string name="screen_space_list_title">"空間"</string>
|
||||||
<string name="screen_space_menu_action_members">"檢視成員"</string>
|
|
||||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"因為 %1$s 的驗證身份已重設,因此未傳送訊息。"</string>
|
<string name="screen_timeline_item_menu_send_failure_changed_identity">"因為 %1$s 的驗證身份已重設,因此未傳送訊息。"</string>
|
||||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"訊息未傳送,因為 %1$s 尚未驗證所有裝置。"</string>
|
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"訊息未傳送,因為 %1$s 尚未驗證所有裝置。"</string>
|
||||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"因為您尚未驗證一個或多個裝置,因此未傳送訊息"</string>
|
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"因為您尚未驗證一個或多個裝置,因此未傳送訊息"</string>
|
||||||
|
|
|
||||||
|
|
@ -460,7 +460,6 @@
|
||||||
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
||||||
<string name="screen_space_list_parent_space">"%1$s空间"</string>
|
<string name="screen_space_list_parent_space">"%1$s空间"</string>
|
||||||
<string name="screen_space_list_title">"空间"</string>
|
<string name="screen_space_list_title">"空间"</string>
|
||||||
<string name="screen_space_menu_action_members">"查看成员"</string>
|
|
||||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"消息未发送,因为%1$s的已验证身份已被重置。"</string>
|
<string name="screen_timeline_item_menu_send_failure_changed_identity">"消息未发送,因为%1$s的已验证身份已被重置。"</string>
|
||||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"消息未发送,因为%1$s尚未验证所有设备。"</string>
|
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"消息未发送,因为%1$s尚未验证所有设备。"</string>
|
||||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"消息未发送,因为您有尚未验证的设备。"</string>
|
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"消息未发送,因为您有尚未验证的设备。"</string>
|
||||||
|
|
|
||||||
|
|
@ -317,6 +317,10 @@ Reason: %1$s."</string>
|
||||||
<string name="common_security">"Security"</string>
|
<string name="common_security">"Security"</string>
|
||||||
<string name="common_seen_by">"Seen by"</string>
|
<string name="common_seen_by">"Seen by"</string>
|
||||||
<string name="common_select_account">"Select an account"</string>
|
<string name="common_select_account">"Select an account"</string>
|
||||||
|
<plurals name="common_selected_count">
|
||||||
|
<item quantity="one">"%1$d selected"</item>
|
||||||
|
<item quantity="other">"%1$d selected"</item>
|
||||||
|
</plurals>
|
||||||
<string name="common_send_to">"Send to"</string>
|
<string name="common_send_to">"Send to"</string>
|
||||||
<string name="common_sending">"Sending…"</string>
|
<string name="common_sending">"Sending…"</string>
|
||||||
<string name="common_sending_failed">"Sending failed"</string>
|
<string name="common_sending_failed">"Sending failed"</string>
|
||||||
|
|
@ -484,12 +488,6 @@ Are you sure you want to continue?"</string>
|
||||||
<string name="screen_space_list_empty_state_title">"Create spaces to organize rooms"</string>
|
<string name="screen_space_list_empty_state_title">"Create spaces to organize rooms"</string>
|
||||||
<string name="screen_space_list_parent_space">"%1$s space"</string>
|
<string name="screen_space_list_parent_space">"%1$s space"</string>
|
||||||
<string name="screen_space_list_title">"Spaces"</string>
|
<string name="screen_space_list_title">"Spaces"</string>
|
||||||
<string name="screen_space_menu_action_members">"View members"</string>
|
|
||||||
<string name="screen_space_remove_rooms_confirmation_content">"Removing a room will not affect the room access. To change the access go to Room info > Privacy & security."</string>
|
|
||||||
<plurals name="screen_space_remove_rooms_confirmation_title">
|
|
||||||
<item quantity="one">"Remove room from %1$s"</item>
|
|
||||||
<item quantity="other">"Remove %1$d rooms from %2$s"</item>
|
|
||||||
</plurals>
|
|
||||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Message not sent because %1$s’s verified identity was reset."</string>
|
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Message not sent because %1$s’s verified identity was reset."</string>
|
||||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Message not sent because %1$s has not verified all devices."</string>
|
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Message not sent because %1$s has not verified all devices."</string>
|
||||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Message not sent because you have not verified one or more of your devices."</string>
|
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Message not sent because you have not verified one or more of your devices."</string>
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ private const val versionMonth = 1
|
||||||
* Release number in the month. Value must be in [0,99].
|
* Release number in the month. Value must be in [0,99].
|
||||||
* Do not update this value. it is updated by the release script.
|
* Do not update this value. it is updated by the release script.
|
||||||
*/
|
*/
|
||||||
private const val versionReleaseNumber = 0
|
private const val versionReleaseNumber = 1
|
||||||
|
|
||||||
object Versions {
|
object Versions {
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:c55277089a4447b618a0e8c058718ecf9d3da6d437322f0e23e5fd70019f6b00
|
oid sha256:e59a9e2ae6ef36f28e61534b5639314cc840953df51bb1660e77e8d565865357
|
||||||
size 34585
|
size 32998
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:241f5500cb7212fac174466bbe7855ccf39de3e3764a83202388b947d90ae807
|
oid sha256:b854ce2b0618ebcbc88eff9952d6869bceeb8b43f9eccb8f8e5feef225e0a4c2
|
||||||
size 34770
|
size 33181
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:92785cf3a4010779b0fbcd58be3437a22808b0a2f02a19a5cfd50eb3bd58ed26
|
oid sha256:e1cf063ee5c9fbc50a53445050780ba239d4fb0fd1e9903a578eab8dd3bfc257
|
||||||
size 35058
|
size 33496
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:de26882f13bac98b2cb5365d98e06e781d516d179adb8328cf22cf524e6fd79e
|
oid sha256:669287557e8effe8154682d33de45430fc852ddefa25ed7399aa3917730e2893
|
||||||
size 62568
|
size 61083
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a121fdb9473512b0264e48294df1799a7a6bf9b469df973fbe41f31bbf98f1d0
|
oid sha256:d2ef07a1af8f872a5b7bb708314bc180d4eefe9545c763a0860f895af6e3ce37
|
||||||
size 63248
|
size 61755
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a3506b4f646408262450ae51b612f86c1171ed972c1d7ea8871c4dc090556c7a
|
oid sha256:525059397001897f705630b8ac5a661439a502f2e623fdca252f9e86f97133e4
|
||||||
size 59702
|
size 58181
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:59af097a2152b235c7df83aa76eea880ceef65edebebecb86f00745ec39712f4
|
||||||
|
size 35937
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a5151bbd3effcd6a46724a0cfb13730e845a3b8ba489826146c76d0a797403e0
|
||||||
|
size 36502
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:2b445ae9e0ff01dea22b8ffb98aaaf4c5593093e2b2bea2ff9508cc43832e687
|
||||||
|
size 48283
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f2407444889af236ef21a90c47a5d3e05df8b15b9cc9483e84377e3af8794772
|
oid sha256:47c8bdf4d153ecebe749ae3cacfe4c9a1c59ac836106fb0903dca80305aedac2
|
||||||
size 33996
|
size 32412
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:fee41efefc2ca1d6670d8455ac756c6b314aab54510eab8a4e597f1cc1edf3f8
|
oid sha256:33917297cb1c6d38e5b955757de50f4ff6b73844bb87e8e88d0885daa01b266b
|
||||||
size 34141
|
size 32556
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ea955839cbd1aeba5de2780cee413628c7d46383398b10125cd3a900fb41d5a5
|
oid sha256:930f763051533ea3aa4032e483e15e0c1ccb1d213d2e59bebe7f934517b500ba
|
||||||
size 34459
|
size 32868
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:cba2c99744aeb2a869ae2ed700d7241b1d0b6ed979b16d2be9774ddbc5f8f28a
|
oid sha256:e38c4a6ff464f77da95b2aa0eaba647bc54ee590c2c4319478fcd54a2400b886
|
||||||
size 61381
|
size 59877
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b32d65accabc357208efeb2ec61374182479541299ade28184f82938e59bfdd5
|
oid sha256:8d91c27ee6dc7938ce905774868b66157b06944ae7c230935c30d6be8ce189be
|
||||||
size 61932
|
size 60430
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7c161ff55e8a235fe403e53ee179b299fd2563d85ef64bfe6d0dd9295228685b
|
oid sha256:dd673c2cf628285836848a732d3972a17421a2394464a3a77e7a49e4c5a862f1
|
||||||
size 57925
|
size 56636
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:b459fa8aa195dcf85995ac51c2f079e9d2505efede138bcef94c0a5049e1f271
|
||||||
|
size 35266
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:36fd123fcff07169723857c99e4e375fe9bbb8193b2a6e97508343ea025d5787
|
||||||
|
size 35782
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a5eed7b1690ac5fbd24312e37029ca880b5cf4424e3796b116829074ef891f2d
|
||||||
|
size 45433
|
||||||
|
|
@ -233,7 +233,8 @@
|
||||||
"name" : ":features:space:impl",
|
"name" : ":features:space:impl",
|
||||||
"includeRegex" : [
|
"includeRegex" : [
|
||||||
"screen\\.leave_space\\..*",
|
"screen\\.leave_space\\..*",
|
||||||
"screen\\.space_settings\\..*"
|
"screen\\.space_settings\\..*",
|
||||||
|
"screen\\.space\\..*"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue