Merge branch 'develop' into renovate/kotlin
This commit is contained in:
80
CHANGES.md
80
CHANGES.md
@@ -1,3 +1,83 @@
|
||||
Changes in Element X v0.7.2 (2024-10-29)
|
||||
========================================
|
||||
|
||||
## What's Changed
|
||||
### 🙌 Improvements
|
||||
* Add setting to compress image and video by @bmarty in https://github.com/element-hq/element-x-android/pull/3744
|
||||
### 🗣 Translations
|
||||
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/3743
|
||||
### 🧱 Build
|
||||
* Release script improvement by @bmarty in https://github.com/element-hq/element-x-android/pull/3741
|
||||
### Dependency upgrades
|
||||
* Update dependency org.maplibre.gl:android-sdk to v11.5.2 by @renovate in https://github.com/element-hq/element-x-android/pull/3720
|
||||
* Update dependency io.sentry:sentry-android to v7.16.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3726
|
||||
* Update dependencyAnalysis to v2.3.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3740
|
||||
* Update dependency org.matrix.rustcomponents:sdk-android to v0.2.58 by @renovate in https://github.com/element-hq/element-x-android/pull/3749
|
||||
|
||||
Changes in Element X v0.7.1 (2024-10-25)
|
||||
========================================
|
||||
|
||||
## What's Changed
|
||||
### ✨ Features
|
||||
* Verified user badge by @bmarty in https://github.com/element-hq/element-x-android/pull/3718
|
||||
### 🙌 Improvements
|
||||
* Add userId in identity change warning banner by @bmarty in https://github.com/element-hq/element-x-android/pull/3686
|
||||
* OIDC prompt by @bmarty in https://github.com/element-hq/element-x-android/pull/3694
|
||||
* Bump rust-sdk version to rust-sdk 0.2.57 by @BillCarsonFr in https://github.com/element-hq/element-x-android/pull/3735
|
||||
### 🐛 Bugfixes
|
||||
* Refresh room summaries when date or time changes in the device by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3683
|
||||
* Call: ensure that the microphone is working when the application is backgrounded. by @bmarty in https://github.com/element-hq/element-x-android/pull/3685
|
||||
* RTL: ensure sender information are correctly rendered in the timeline by @bmarty in https://github.com/element-hq/element-x-android/pull/3681
|
||||
* Improve composer paddings by @bmarty in https://github.com/element-hq/element-x-android/pull/3695
|
||||
* UI: fix list item colors by @bmarty in https://github.com/element-hq/element-x-android/pull/3706
|
||||
* Small UI iteration on pin feature. by @bmarty in https://github.com/element-hq/element-x-android/pull/3714
|
||||
* Use BigIcon and fix colors by @bmarty in https://github.com/element-hq/element-x-android/pull/3719
|
||||
### 🗣 Translations
|
||||
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/3665
|
||||
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/3713
|
||||
### 🧱 Build
|
||||
* Update Gradle Wrapper from 8.10 to 8.10.2 by @ElementBot in https://github.com/element-hq/element-x-android/pull/3663
|
||||
* fix: import path broken in module template by @torrybr in https://github.com/element-hq/element-x-android/pull/3710
|
||||
### 📄 Documentation
|
||||
* Update store description by @bmarty in https://github.com/element-hq/element-x-android/pull/3680
|
||||
### 🚧 In development 🚧
|
||||
* Feature: knock request to join by @ganfra in https://github.com/element-hq/element-x-android/pull/3725
|
||||
### Dependency upgrades
|
||||
* Update anvil to v0.3.2 by @renovate in https://github.com/element-hq/element-x-android/pull/3662
|
||||
* Update dependency io.nlopez.compose.rules:detekt to v0.4.16 by @renovate in https://github.com/element-hq/element-x-android/pull/3675
|
||||
* Update dependency com.posthog:posthog-android to v3.8.2 by @renovate in https://github.com/element-hq/element-x-android/pull/3674
|
||||
* Update dependency io.element.android:compound-android to v0.1.1 - Better support for RTL icons. by @renovate in https://github.com/element-hq/element-x-android/pull/3676
|
||||
* Update android.gradle.plugin to v8.7.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3677
|
||||
* Update dependency io.sentry:sentry-android to v7.15.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3640
|
||||
* Update mobile-dev-inc/action-maestro-cloud action to v1.9.2 by @renovate in https://github.com/element-hq/element-x-android/pull/3641
|
||||
* Update plugin licensee to v1.12.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3687
|
||||
* Update dependency app.cash.turbine:turbine to v1.2.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3696
|
||||
* Update activity to v1.9.3 by @renovate in https://github.com/element-hq/element-x-android/pull/3697
|
||||
* Update dependency androidx.compose:compose-bom to v2024.10.00 by @renovate in https://github.com/element-hq/element-x-android/pull/3699
|
||||
* Update dependency org.matrix.rustcomponents:sdk-android to v0.2.55 by @renovate in https://github.com/element-hq/element-x-android/pull/3701
|
||||
* Update dependencyAnalysis to v2.2.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3707
|
||||
* Update anvil to v0.3.3 by @renovate in https://github.com/element-hq/element-x-android/pull/3711
|
||||
* Update dependency androidx.annotation:annotation-jvm to v1.9.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3698
|
||||
* Update dependency com.google.firebase:firebase-bom to v33.5.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3716
|
||||
* Update dependency org.matrix.rustcomponents:sdk-android to v0.2.56 by @renovate in https://github.com/element-hq/element-x-android/pull/3715
|
||||
* Update dependency com.squareup:kotlinpoet-ksp to v2 by @renovate in https://github.com/element-hq/element-x-android/pull/3722
|
||||
* Update dependency org.maplibre.gl:android-sdk-ktx-v7 to v3.0.2 by @renovate in https://github.com/element-hq/element-x-android/pull/3703
|
||||
* Dependencies : makes sure to use same version for all kotlinpoet dependencies by @ganfra in https://github.com/element-hq/element-x-android/pull/3727
|
||||
* Update dependency com.google.firebase:firebase-bom to v33.5.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3731
|
||||
### Others
|
||||
* No need to launch a coroutine here. by @bmarty in https://github.com/element-hq/element-x-android/pull/3668
|
||||
* Fix issue on canInvite refresh. by @bmarty in https://github.com/element-hq/element-x-android/pull/3670
|
||||
* AsyncAction confirming with param by @bmarty in https://github.com/element-hq/element-x-android/pull/3667
|
||||
* Cleanup tests by @bmarty in https://github.com/element-hq/element-x-android/pull/3672
|
||||
* Ensure selectedRoomMember is not null to reduce code indentation. by @bmarty in https://github.com/element-hq/element-x-android/pull/3669
|
||||
* Improve preview provider name consistency by @bmarty in https://github.com/element-hq/element-x-android/pull/3673
|
||||
* Clarify model for Event with attachment by @bmarty in https://github.com/element-hq/element-x-android/pull/3574
|
||||
* Improve room moderation by @bmarty in https://github.com/element-hq/element-x-android/pull/3671
|
||||
* Remove duplicated code regarding user (room member and user profile) screens by @bmarty in https://github.com/element-hq/element-x-android/pull/3700
|
||||
* Rename some function to avoid name clash by @bmarty in https://github.com/element-hq/element-x-android/pull/3705
|
||||
* Fix flaky tests. by @bmarty in https://github.com/element-hq/element-x-android/pull/3717
|
||||
* Update accent color for `Checkbox`, `RadioButton` and `Switch` components by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3728
|
||||
|
||||
Changes in Element X v0.7.0 (2024-10-10)
|
||||
========================================
|
||||
|
||||
|
||||
@@ -25,8 +25,10 @@ import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.pop
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import com.bumble.appyx.navmodel.backstack.operation.replace
|
||||
import com.bumble.appyx.navmodel.backstack.operation.singleTop
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.JoinedRoom
|
||||
@@ -50,6 +52,7 @@ import io.element.android.features.roomlist.api.RoomListEntryPoint
|
||||
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
|
||||
import io.element.android.features.share.api.ShareEntryPoint
|
||||
import io.element.android.features.userprofile.api.UserProfileEntryPoint
|
||||
import io.element.android.features.verifysession.api.IncomingVerificationEntryPoint
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
@@ -66,6 +69,8 @@ import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener
|
||||
import io.element.android.libraries.preferences.api.store.EnableNativeSlidingSyncUseCase
|
||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -99,6 +104,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||
private val matrixClient: MatrixClient,
|
||||
private val sendingQueue: SendQueues,
|
||||
private val logoutEntryPoint: LogoutEntryPoint,
|
||||
private val incomingVerificationEntryPoint: IncomingVerificationEntryPoint,
|
||||
private val enableNativeSlidingSyncUseCase: EnableNativeSlidingSyncUseCase,
|
||||
snackbarDispatcher: SnackbarDispatcher,
|
||||
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
|
||||
@@ -123,6 +129,12 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||
matrixClient.roomMembershipObserver(),
|
||||
)
|
||||
|
||||
private val verificationListener = object : SessionVerificationServiceListener {
|
||||
override fun onIncomingSessionRequest(sessionVerificationRequestDetails: SessionVerificationRequestDetails) {
|
||||
backstack.singleTop(NavTarget.IncomingVerificationRequest(sessionVerificationRequestDetails))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBuilt() {
|
||||
super.onBuilt()
|
||||
lifecycle.subscribe(
|
||||
@@ -131,6 +143,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||
// TODO We do not support Space yet, so directly navigate to main space
|
||||
appNavigationStateService.onNavigateToSpace(id, MAIN_SPACE)
|
||||
loggedInFlowProcessor.observeEvents(coroutineScope)
|
||||
matrixClient.sessionVerificationService().setListener(verificationListener)
|
||||
|
||||
ftueService.state
|
||||
.onEach { ftueState ->
|
||||
@@ -152,6 +165,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||
appNavigationStateService.onLeavingSpace(id)
|
||||
appNavigationStateService.onLeavingSession(id)
|
||||
loggedInFlowProcessor.stopObserving()
|
||||
matrixClient.sessionVerificationService().setListener(null)
|
||||
}
|
||||
)
|
||||
observeSyncStateAndNetworkStatus()
|
||||
@@ -232,6 +246,9 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||
|
||||
@Parcelize
|
||||
data object LogoutForNativeSlidingSyncMigrationNeeded : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class IncomingVerificationRequest(val data: SessionVerificationRequestDetails) : NavTarget
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
@@ -260,7 +277,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||
}
|
||||
|
||||
override fun onSetUpRecoveryClick() {
|
||||
backstack.push(NavTarget.SecureBackup(initialElement = SecureBackupEntryPoint.InitialTarget.SetUpRecovery))
|
||||
backstack.push(NavTarget.SecureBackup(initialElement = SecureBackupEntryPoint.InitialTarget.Root))
|
||||
}
|
||||
|
||||
override fun onSessionConfirmRecoveryKeyClick() {
|
||||
@@ -432,6 +449,16 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||
.callback(callback)
|
||||
.build()
|
||||
}
|
||||
is NavTarget.IncomingVerificationRequest -> {
|
||||
incomingVerificationEntryPoint.nodeBuilder(this, buildContext)
|
||||
.params(IncomingVerificationEntryPoint.Params(navTarget.data))
|
||||
.callback(object : IncomingVerificationEntryPoint.Callback {
|
||||
override fun onDone() {
|
||||
backstack.pop()
|
||||
}
|
||||
})
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ allprojects {
|
||||
config.from(files("$rootDir/tools/detekt/detekt.yml"))
|
||||
}
|
||||
dependencies {
|
||||
detektPlugins("io.nlopez.compose.rules:detekt:0.4.16")
|
||||
detektPlugins("io.nlopez.compose.rules:detekt:0.4.17")
|
||||
}
|
||||
|
||||
// KtLint
|
||||
|
||||
2
fastlane/metadata/android/en-US/changelogs/40007010.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40007010.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Main changes in this version: bug fixes and performance improvement.
|
||||
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||
@@ -3,10 +3,11 @@
|
||||
<string name="screen_create_room_action_create_room">"Nová místnost"</string>
|
||||
<string name="screen_create_room_add_people_title">"Pozvat přátele"</string>
|
||||
<string name="screen_create_room_error_creating_room">"Při vytváření místnosti došlo k chybě"</string>
|
||||
<string name="screen_create_room_private_option_description">"Zprávy v této místnosti jsou šifrované. Šifrování nelze později vypnout."</string>
|
||||
<string name="screen_create_room_private_option_title">"Soukromá místnost (jen pro pozvané)"</string>
|
||||
<string name="screen_create_room_public_option_description">"Zprávy nejsou šifrované a může si je přečíst kdokoli. Šifrování můžete povolit později."</string>
|
||||
<string name="screen_create_room_public_option_title">"Veřejná místnost (kdokoli)"</string>
|
||||
<string name="screen_create_room_private_option_description">"Do této místnosti mají přístup pouze pozvaní lidé. Všechny zprávy jsou koncově šifrovány."</string>
|
||||
<string name="screen_create_room_private_option_title">"Soukromá místnost"</string>
|
||||
<string name="screen_create_room_public_option_description">"Tuto místnost může najít kdokoli.
|
||||
To můžete kdykoli změnit v nastavení místnosti."</string>
|
||||
<string name="screen_create_room_public_option_title">"Veřejná místnost"</string>
|
||||
<string name="screen_create_room_room_name_label">"Název místnosti"</string>
|
||||
<string name="screen_create_room_title">"Vytvořit místnost"</string>
|
||||
<string name="screen_create_room_topic_label">"Téma (nepovinné)"</string>
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
<string name="screen_create_room_action_create_room">"Νέο δωμάτιο"</string>
|
||||
<string name="screen_create_room_add_people_title">"Πρόσκληση ατόμων"</string>
|
||||
<string name="screen_create_room_error_creating_room">"Παρουσιάστηκε σφάλμα κατά τη δημιουργία του δωματίου"</string>
|
||||
<string name="screen_create_room_private_option_description">"Τα μηνύματα σε αυτό το δωμάτιο είναι κρυπτογραφημένα. Η κρυπτογράφηση δεν μπορεί να απενεργοποιηθεί αργότερα."</string>
|
||||
<string name="screen_create_room_private_option_title">"Ιδιωτικό δωμάτιο (μόνο με πρόσκληση)"</string>
|
||||
<string name="screen_create_room_public_option_description">"Τα μηνύματα δεν είναι κρυπτογραφημένα και ο καθένας μπορεί να τα διαβάσει. Μπορείς να ενεργοποιήσεις την κρυπτογράφηση αργότερα."</string>
|
||||
<string name="screen_create_room_public_option_title">"Δημόσιο δωμάτιο (οποιοσδήποτε)"</string>
|
||||
<string name="screen_create_room_private_option_description">"Μόνο άτομα που έχουν προσκληθεί μπορούν να έχουν πρόσβαση σε αυτό το δωμάτιο. Όλα τα μηνύματα είναι κρυπτογραφημένα από άκρο σε άκρο."</string>
|
||||
<string name="screen_create_room_private_option_title">"Ιδιωτικό δωμάτιο"</string>
|
||||
<string name="screen_create_room_public_option_description">"Ο καθένας μπορεί να βρει αυτό το δωμάτιο.
|
||||
Μπορείς να το αλλάξεις ανά πάσα στιγμή στις ρυθμίσεις δωματίου."</string>
|
||||
<string name="screen_create_room_public_option_title">"Δημόσιο δωμάτιο"</string>
|
||||
<string name="screen_create_room_room_name_label">"Όνομα δωματίου"</string>
|
||||
<string name="screen_create_room_title">"Δημιούργησε ένα δωμάτιο"</string>
|
||||
<string name="screen_create_room_topic_label">"Θέμα (προαιρετικό)"</string>
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
<string name="screen_create_room_action_create_room">"Uus jututuba"</string>
|
||||
<string name="screen_create_room_add_people_title">"Kutsu osalejaid"</string>
|
||||
<string name="screen_create_room_error_creating_room">"Jututoa loomisel tekkis viga"</string>
|
||||
<string name="screen_create_room_private_option_description">"Sõnumid siin jututoas on krüptitud ja seda ei saa hiljem välja lülitada."</string>
|
||||
<string name="screen_create_room_private_option_title">"Privaatne jututuba (liitumine vaid kutsega)"</string>
|
||||
<string name="screen_create_room_public_option_description">"Sõnumid pole krüptitud ja neid saavad kõik lugeda. Soovi korral saad hiljem krüptimise sisse lülitada."</string>
|
||||
<string name="screen_create_room_public_option_title">"Avalik jututuba (avatud kõigile)"</string>
|
||||
<string name="screen_create_room_private_option_description">"Ligipääs siia jututuppa on vaid kutse alusel. Kõik sõnumid siin jututoas on läbivalt krüptitud."</string>
|
||||
<string name="screen_create_room_private_option_title">"Privaatne jututuba"</string>
|
||||
<string name="screen_create_room_public_option_description">"Kõik saavad seda jututuba leida.
|
||||
Sa võid seda jututoa seadistustest alati muuta."</string>
|
||||
<string name="screen_create_room_public_option_title">"Avalik jututuba"</string>
|
||||
<string name="screen_create_room_room_name_label">"Jututoa nimi"</string>
|
||||
<string name="screen_create_room_title">"Loo jututuba"</string>
|
||||
<string name="screen_create_room_topic_label">"Teema (kui soovid lisada)"</string>
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
<string name="screen_create_room_action_create_room">"Ruangan baru"</string>
|
||||
<string name="screen_create_room_add_people_title">"Undang orang-orang"</string>
|
||||
<string name="screen_create_room_error_creating_room">"Terjadi kesalahan saat membuat ruangan"</string>
|
||||
<string name="screen_create_room_private_option_description">"Pesan di ruangan ini dienkripsi. Enkripsi tidak dapat dinonaktifkan setelahnya."</string>
|
||||
<string name="screen_create_room_private_option_title">"Ruangan pribadi (hanya undangan)"</string>
|
||||
<string name="screen_create_room_public_option_description">"Pesan tidak dienkripsi dan siapa pun dapat membacanya. Anda dapat mengaktifkan enkripsi di kemudian hari."</string>
|
||||
<string name="screen_create_room_public_option_title">"Ruang publik (siapa saja)"</string>
|
||||
<string name="screen_create_room_private_option_description">"Hanya orang-orang yang diundang dapat mengakses ruangan ini. Semua pesan terenkripsi secara ujung ke ujung."</string>
|
||||
<string name="screen_create_room_private_option_title">"Ruangan pribadi"</string>
|
||||
<string name="screen_create_room_public_option_description">"Siapa pun dapat mencari ruangan ini.
|
||||
Anda dapat mengubah ini kapan pun dalam pengaturan ruangan."</string>
|
||||
<string name="screen_create_room_public_option_title">"Ruangan publik"</string>
|
||||
<string name="screen_create_room_room_name_label">"Nama ruangan"</string>
|
||||
<string name="screen_create_room_title">"Buat ruangan"</string>
|
||||
<string name="screen_create_room_topic_label">"Topik (opsional)"</string>
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
<string name="screen_create_room_action_create_room">"Создать новую комнату"</string>
|
||||
<string name="screen_create_room_add_people_title">"Пригласить в комнату"</string>
|
||||
<string name="screen_create_room_error_creating_room">"Произошла ошибка при создании комнаты"</string>
|
||||
<string name="screen_create_room_private_option_description">"Сообщения в этой комнате будут зашифрованы. Отключить шифрование позже будет невозможно."</string>
|
||||
<string name="screen_create_room_private_option_title">"Частная комната (только по приглашениям)"</string>
|
||||
<string name="screen_create_room_public_option_description">"Сообщения не будут зашифрованы и каждый сможет их прочитать. Шифрование можно будет включить позже."</string>
|
||||
<string name="screen_create_room_public_option_title">"Общедоступная комната (для всех)"</string>
|
||||
<string name="screen_create_room_private_option_description">"Доступ в эту комнату имеют только приглашенные пользователи. Все сообщения защищены сквозным шифрованием."</string>
|
||||
<string name="screen_create_room_private_option_title">"Частная комната"</string>
|
||||
<string name="screen_create_room_public_option_description">"Любой желающий может найти эту комнату.
|
||||
Вы можете изменить это в любое время в настройках комнаты."</string>
|
||||
<string name="screen_create_room_public_option_title">"Общедоступная комната"</string>
|
||||
<string name="screen_create_room_room_name_label">"Название комнаты"</string>
|
||||
<string name="screen_create_room_title">"Создать комнату"</string>
|
||||
<string name="screen_create_room_topic_label">"Тема (необязательно)"</string>
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
<string name="screen_create_room_action_create_room">"Nová miestnosť"</string>
|
||||
<string name="screen_create_room_add_people_title">"Pozvať ľudí"</string>
|
||||
<string name="screen_create_room_error_creating_room">"Pri vytváraní miestnosti došlo k chybe"</string>
|
||||
<string name="screen_create_room_private_option_description">"Správy v tejto miestnosti sú šifrované. Šifrovanie už potom nie je možné vypnúť."</string>
|
||||
<string name="screen_create_room_private_option_title">"Súkromná miestnosť (len pre pozvaných)"</string>
|
||||
<string name="screen_create_room_public_option_description">"Správy nie sú šifrované a môže si ich prečítať ktokoľvek. Šifrovanie môžete zapnúť neskôr."</string>
|
||||
<string name="screen_create_room_public_option_title">"Verejná miestnosť (ktokoľvek)"</string>
|
||||
<string name="screen_create_room_private_option_description">"Do tejto miestnosti majú prístup iba pozvaní ľudia. Všetky správy sú end-to-end šifrované."</string>
|
||||
<string name="screen_create_room_private_option_title">"Súkromná miestnosť"</string>
|
||||
<string name="screen_create_room_public_option_description">"Túto miestnosť môže nájsť ktokoľvek.
|
||||
Môžete to kedykoľvek zmeniť v nastaveniach miestnosti."</string>
|
||||
<string name="screen_create_room_public_option_title">"Verejná miestnosť"</string>
|
||||
<string name="screen_create_room_room_name_label">"Názov miestnosti"</string>
|
||||
<string name="screen_create_room_title">"Vytvoriť miestnosť"</string>
|
||||
<string name="screen_create_room_topic_label">"Téma (voliteľné)"</string>
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
<string name="screen_create_room_action_create_room">"Nytt rum"</string>
|
||||
<string name="screen_create_room_add_people_title">"Bjud in personer"</string>
|
||||
<string name="screen_create_room_error_creating_room">"Ett fel uppstod när rummet skapades"</string>
|
||||
<string name="screen_create_room_private_option_description">"Meddelanden i det här rummet är krypterade. Kryptering kan inte inaktiveras efteråt."</string>
|
||||
<string name="screen_create_room_private_option_title">"Privat rum (endast inbjudan)"</string>
|
||||
<string name="screen_create_room_public_option_description">"Meddelanden är inte krypterade och vem som helst kan läsa dem. Du kan aktivera kryptering vid ett senare tillfälle."</string>
|
||||
<string name="screen_create_room_public_option_title">"Offentligt rum (vem som helst)"</string>
|
||||
<string name="screen_create_room_private_option_description">"Endast inbjudna personer har tillgång till detta rum. Alla meddelanden är totalsträckskrypterade."</string>
|
||||
<string name="screen_create_room_private_option_title">"Privat rum"</string>
|
||||
<string name="screen_create_room_public_option_description">"Vem som helst kan hitta det här rummet.
|
||||
Du kan ändra detta när som helst i rumsinställningarna."</string>
|
||||
<string name="screen_create_room_public_option_title">"Offentligt rum"</string>
|
||||
<string name="screen_create_room_room_name_label">"Rumsnamn"</string>
|
||||
<string name="screen_create_room_title">"Skapa ett rum"</string>
|
||||
<string name="screen_create_room_topic_label">"Ämne (valfritt)"</string>
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
<string name="screen_create_room_action_create_room">"New room"</string>
|
||||
<string name="screen_create_room_add_people_title">"Invite people"</string>
|
||||
<string name="screen_create_room_error_creating_room">"An error occurred when creating the room"</string>
|
||||
<string name="screen_create_room_private_option_description">"Messages in this room are encrypted. Encryption can’t be disabled afterwards."</string>
|
||||
<string name="screen_create_room_private_option_title">"Private room (invite only)"</string>
|
||||
<string name="screen_create_room_public_option_description">"Messages are not encrypted and anyone can read them. You can enable encryption at a later date."</string>
|
||||
<string name="screen_create_room_public_option_title">"Public room (anyone)"</string>
|
||||
<string name="screen_create_room_private_option_description">"Only people invited can access this room. All messages are end-to-end encrypted."</string>
|
||||
<string name="screen_create_room_private_option_title">"Private room"</string>
|
||||
<string name="screen_create_room_public_option_description">"Anyone can find this room.
|
||||
You can change this anytime in room settings."</string>
|
||||
<string name="screen_create_room_public_option_title">"Public room"</string>
|
||||
<string name="screen_create_room_room_name_label">"Room name"</string>
|
||||
<string name="screen_create_room_title">"Create a room"</string>
|
||||
<string name="screen_create_room_topic_label">"Topic (optional)"</string>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_deactivate_account_confirmation_dialog_content">"Подтвердите, что вы хотите деактивировать свою учетную запись. Это действие не может быть отменено."</string>
|
||||
<string name="screen_deactivate_account_confirmation_dialog_content">"Вы уверены, что хотите отключить свою учётную запись? Данное действие не может быть отменено."</string>
|
||||
<string name="screen_deactivate_account_delete_all_messages">"Удалить все мои сообщения"</string>
|
||||
<string name="screen_deactivate_account_delete_all_messages_notice">"Предупреждение: будущие пользователи могут увидеть незавершенные разговоры."</string>
|
||||
<string name="screen_deactivate_account_description">"Деактивация вашей учетной записи %1$s означает следующее:"</string>
|
||||
<string name="screen_deactivate_account_description_bold_part">"необратимый"</string>
|
||||
<string name="screen_deactivate_account_list_item_1">"%1$s вашей учетной записи (вы не можете войти в систему снова, и ваш ID не может быть использован повторно)."</string>
|
||||
<string name="screen_deactivate_account_description">"Отключение вашей учетной записи %1$s и означает следующее:"</string>
|
||||
<string name="screen_deactivate_account_description_bold_part">"необратимо"</string>
|
||||
<string name="screen_deactivate_account_list_item_1">"Ваша учётная запись будет %1$s (вы не сможете войти в неё снова, и ваш ID не может быть использован повторно)."</string>
|
||||
<string name="screen_deactivate_account_list_item_1_bold_part">"Отключить навсегда"</string>
|
||||
<string name="screen_deactivate_account_list_item_2">"Удалите вас из всех чатов."</string>
|
||||
<string name="screen_deactivate_account_list_item_3">"Удалите данные своей учетной записи с нашего сервера идентификации."</string>
|
||||
<string name="screen_deactivate_account_list_item_2">"Вы будете удалены из всех чатов."</string>
|
||||
<string name="screen_deactivate_account_list_item_3">"Данные вашей учётной записи будут удалены с нашего сервера идентификации."</string>
|
||||
<string name="screen_deactivate_account_list_item_4">"Ваши сообщения по-прежнему будут видны зарегистрированным пользователям, но не будут доступны новым или незарегистрированным пользователям, если вы решите удалить их."</string>
|
||||
<string name="screen_deactivate_account_title">"Отключить учётную запись"</string>
|
||||
</resources>
|
||||
|
||||
@@ -35,7 +35,7 @@ class DefaultFtueServiceTest {
|
||||
@Test
|
||||
fun `given any check being false and session verification state being loaded, FtueState is Incomplete`() = runTest {
|
||||
val sessionVerificationService = FakeSessionVerificationService().apply {
|
||||
givenVerifiedStatus(SessionVerifiedStatus.Unknown)
|
||||
emitVerifiedStatus(SessionVerifiedStatus.Unknown)
|
||||
}
|
||||
val service = createDefaultFtueService(
|
||||
sessionVerificationService = sessionVerificationService,
|
||||
@@ -46,7 +46,7 @@ class DefaultFtueServiceTest {
|
||||
assertThat(awaitItem()).isEqualTo(FtueState.Unknown)
|
||||
|
||||
// Verification state is known, we should display the flow if any check is false
|
||||
sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
|
||||
sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.NotVerified)
|
||||
assertThat(awaitItem()).isEqualTo(FtueState.Incomplete)
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,7 @@ class DefaultFtueServiceTest {
|
||||
lockScreenService = lockScreenService,
|
||||
)
|
||||
|
||||
sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.Verified)
|
||||
sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified)
|
||||
analyticsService.setDidAskUserConsent()
|
||||
permissionStateProvider.setPermissionGranted()
|
||||
lockScreenService.setIsPinSetup(true)
|
||||
@@ -76,7 +76,7 @@ class DefaultFtueServiceTest {
|
||||
@Test
|
||||
fun `traverse flow`() = runTest {
|
||||
val sessionVerificationService = FakeSessionVerificationService().apply {
|
||||
givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
|
||||
emitVerifiedStatus(SessionVerifiedStatus.NotVerified)
|
||||
}
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false)
|
||||
@@ -91,7 +91,7 @@ class DefaultFtueServiceTest {
|
||||
|
||||
// Session verification
|
||||
steps.add(service.getNextStep(steps.lastOrNull()))
|
||||
sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
|
||||
sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.NotVerified)
|
||||
|
||||
// Notifications opt in
|
||||
steps.add(service.getNextStep(steps.lastOrNull()))
|
||||
@@ -132,7 +132,7 @@ class DefaultFtueServiceTest {
|
||||
)
|
||||
|
||||
// Skip first 3 steps
|
||||
sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.Verified)
|
||||
sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified)
|
||||
permissionStateProvider.setPermissionGranted()
|
||||
lockScreenService.setIsPinSetup(true)
|
||||
|
||||
@@ -155,7 +155,7 @@ class DefaultFtueServiceTest {
|
||||
lockScreenService = lockScreenService,
|
||||
)
|
||||
|
||||
sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.Verified)
|
||||
sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified)
|
||||
lockScreenService.setIsPinSetup(true)
|
||||
|
||||
assertThat(service.getNextStep()).isEqualTo(FtueStep.AnalyticsOptIn)
|
||||
|
||||
@@ -94,8 +94,8 @@ class AcceptDeclineInvitePresenter @Inject constructor(
|
||||
|
||||
private fun CoroutineScope.declineInvite(roomId: RoomId, declinedAction: MutableState<AsyncAction<RoomId>>) = launch {
|
||||
suspend {
|
||||
client.getInvitedRoom(roomId)?.use {
|
||||
it.declineInvite().getOrThrow()
|
||||
client.getPendingRoom(roomId)?.use {
|
||||
it.leave().getOrThrow()
|
||||
notificationCleaner.clearMembershipNotificationForRoom(client.sessionId, roomId)
|
||||
}
|
||||
roomId
|
||||
|
||||
@@ -22,7 +22,7 @@ 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_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.room.FakeInvitedRoom
|
||||
import io.element.android.libraries.matrix.test.room.FakePendingRoom
|
||||
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
|
||||
import io.element.android.libraries.push.api.notifications.NotificationCleaner
|
||||
import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner
|
||||
@@ -78,7 +78,7 @@ class AcceptDeclineInvitePresenterTest {
|
||||
Result.failure<Unit>(RuntimeException("Failed to leave room"))
|
||||
}
|
||||
val client = FakeMatrixClient().apply {
|
||||
getInvitedRoomResults[A_ROOM_ID] = FakeInvitedRoom(declineInviteResult = declineInviteFailure)
|
||||
getPendingRoomResults[A_ROOM_ID] = FakePendingRoom(declineInviteResult = declineInviteFailure)
|
||||
}
|
||||
val presenter = createAcceptDeclineInvitePresenter(client = client)
|
||||
presenter.test {
|
||||
@@ -121,7 +121,7 @@ class AcceptDeclineInvitePresenterTest {
|
||||
Result.success(Unit)
|
||||
}
|
||||
val client = FakeMatrixClient().apply {
|
||||
getInvitedRoomResults[A_ROOM_ID] = FakeInvitedRoom(declineInviteResult = declineInviteSuccess)
|
||||
getPendingRoomResults[A_ROOM_ID] = FakePendingRoom(declineInviteResult = declineInviteSuccess)
|
||||
}
|
||||
val presenter = createAcceptDeclineInvitePresenter(
|
||||
client = client,
|
||||
|
||||
@@ -11,7 +11,9 @@ sealed interface JoinRoomEvents {
|
||||
data object RetryFetchingContent : JoinRoomEvents
|
||||
data object JoinRoom : JoinRoomEvents
|
||||
data object KnockRoom : JoinRoomEvents
|
||||
data object ClearError : JoinRoomEvents
|
||||
data class CancelKnock(val requiresConfirmation: Boolean) : JoinRoomEvents
|
||||
data class UpdateKnockMessage(val message: String) : JoinRoomEvents
|
||||
data object ClearActionStates : JoinRoomEvents
|
||||
data object AcceptInvite : JoinRoomEvents
|
||||
data object DeclineInvite : JoinRoomEvents
|
||||
}
|
||||
|
||||
@@ -43,7 +43,8 @@ class JoinRoomNode @AssistedInject constructor(
|
||||
state = state,
|
||||
onBackClick = ::navigateUp,
|
||||
onJoinSuccess = ::navigateUp,
|
||||
onKnockSuccess = ::navigateUp,
|
||||
onCancelKnockSuccess = ::navigateUp,
|
||||
onKnockSuccess = { },
|
||||
modifier = modifier
|
||||
)
|
||||
acceptDeclineInviteView.Render(
|
||||
|
||||
@@ -17,6 +17,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
@@ -24,6 +25,7 @@ import im.vector.app.features.analytics.plan.JoinedRoom
|
||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||
import io.element.android.features.invite.api.response.InviteData
|
||||
import io.element.android.features.joinroom.impl.di.CancelKnockRoom
|
||||
import io.element.android.features.joinroom.impl.di.KnockRoom
|
||||
import io.element.android.features.roomdirectory.api.RoomDescription
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
@@ -46,6 +48,8 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Optional
|
||||
|
||||
private const val MAX_KNOCK_MESSAGE_LENGTH = 500
|
||||
|
||||
class JoinRoomPresenter @AssistedInject constructor(
|
||||
@Assisted private val roomId: RoomId,
|
||||
@Assisted private val roomIdOrAlias: RoomIdOrAlias,
|
||||
@@ -55,6 +59,7 @@ class JoinRoomPresenter @AssistedInject constructor(
|
||||
private val matrixClient: MatrixClient,
|
||||
private val joinRoom: JoinRoom,
|
||||
private val knockRoom: KnockRoom,
|
||||
private val cancelKnockRoom: CancelKnockRoom,
|
||||
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
||||
private val buildMeta: BuildMeta,
|
||||
) : Presenter<JoinRoomState> {
|
||||
@@ -75,6 +80,8 @@ class JoinRoomPresenter @AssistedInject constructor(
|
||||
val roomInfo by matrixClient.getRoomInfoFlow(roomId.toRoomIdOrAlias()).collectAsState(initial = Optional.empty())
|
||||
val joinAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||
val knockAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||
val cancelKnockAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||
var knockMessage by rememberSaveable { mutableStateOf("") }
|
||||
val contentState by produceState<ContentState>(
|
||||
initialValue = ContentState.Loading(roomIdOrAlias),
|
||||
key1 = roomInfo,
|
||||
@@ -110,7 +117,7 @@ class JoinRoomPresenter @AssistedInject constructor(
|
||||
fun handleEvents(event: JoinRoomEvents) {
|
||||
when (event) {
|
||||
JoinRoomEvents.JoinRoom -> coroutineScope.joinRoom(joinAction)
|
||||
JoinRoomEvents.KnockRoom -> coroutineScope.knockRoom(knockAction)
|
||||
is JoinRoomEvents.KnockRoom -> coroutineScope.knockRoom(knockAction, knockMessage)
|
||||
JoinRoomEvents.AcceptInvite -> {
|
||||
val inviteData = contentState.toInviteData() ?: return
|
||||
acceptDeclineInviteState.eventSink(
|
||||
@@ -123,12 +130,17 @@ class JoinRoomPresenter @AssistedInject constructor(
|
||||
AcceptDeclineInviteEvents.DeclineInvite(inviteData)
|
||||
)
|
||||
}
|
||||
is JoinRoomEvents.CancelKnock -> coroutineScope.cancelKnockRoom(event.requiresConfirmation, cancelKnockAction)
|
||||
JoinRoomEvents.RetryFetchingContent -> {
|
||||
retryCount++
|
||||
}
|
||||
JoinRoomEvents.ClearError -> {
|
||||
JoinRoomEvents.ClearActionStates -> {
|
||||
knockAction.value = AsyncAction.Uninitialized
|
||||
joinAction.value = AsyncAction.Uninitialized
|
||||
cancelKnockAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
is JoinRoomEvents.UpdateKnockMessage -> {
|
||||
knockMessage = event.message.take(MAX_KNOCK_MESSAGE_LENGTH)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,7 +150,9 @@ class JoinRoomPresenter @AssistedInject constructor(
|
||||
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||
joinAction = joinAction.value,
|
||||
knockAction = knockAction.value,
|
||||
cancelKnockAction = cancelKnockAction.value,
|
||||
applicationName = buildMeta.applicationName,
|
||||
knockMessage = knockMessage,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
@@ -153,9 +167,19 @@ class JoinRoomPresenter @AssistedInject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.knockRoom(knockAction: MutableState<AsyncAction<Unit>>) = launch {
|
||||
private fun CoroutineScope.knockRoom(knockAction: MutableState<AsyncAction<Unit>>, message: String) = launch {
|
||||
knockAction.runUpdatingState {
|
||||
knockRoom(roomId)
|
||||
knockRoom(roomIdOrAlias, message, serverNames)
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.cancelKnockRoom(requiresConfirmation: Boolean, cancelKnockAction: MutableState<AsyncAction<Unit>>) = launch {
|
||||
if (requiresConfirmation) {
|
||||
cancelKnockAction.value = AsyncAction.ConfirmingNoParams
|
||||
} else {
|
||||
cancelKnockAction.runUpdatingState {
|
||||
cancelKnockRoom(roomId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -206,7 +230,7 @@ internal fun MatrixRoomInfo.toContentState(): ContentState {
|
||||
name = name,
|
||||
topic = topic,
|
||||
alias = canonicalAlias,
|
||||
numberOfMembers = activeMembersCount.toLong(),
|
||||
numberOfMembers = activeMembersCount,
|
||||
isDm = isDm,
|
||||
roomType = if (isSpace) RoomType.Space else RoomType.Room,
|
||||
roomAvatarUrl = avatarUrl,
|
||||
@@ -214,6 +238,7 @@ internal fun MatrixRoomInfo.toContentState(): ContentState {
|
||||
currentUserMembership == CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited(
|
||||
inviteSender = inviter?.toInviteSender()
|
||||
)
|
||||
currentUserMembership == CurrentUserMembership.KNOCKED -> JoinAuthorisationStatus.IsKnocked
|
||||
isPublic -> JoinAuthorisationStatus.CanJoin
|
||||
else -> JoinAuthorisationStatus.Unknown
|
||||
}
|
||||
|
||||
@@ -24,7 +24,9 @@ data class JoinRoomState(
|
||||
val acceptDeclineInviteState: AcceptDeclineInviteState,
|
||||
val joinAction: AsyncAction<Unit>,
|
||||
val knockAction: AsyncAction<Unit>,
|
||||
val cancelKnockAction: AsyncAction<Unit>,
|
||||
val applicationName: String,
|
||||
val knockMessage: String,
|
||||
val eventSink: (JoinRoomEvents) -> Unit
|
||||
) {
|
||||
val joinAuthorisationStatus = when (contentState) {
|
||||
@@ -68,6 +70,7 @@ sealed interface ContentState {
|
||||
|
||||
sealed interface JoinAuthorisationStatus {
|
||||
data class IsInvited(val inviteSender: InviteSender?) : JoinAuthorisationStatus
|
||||
data object IsKnocked : JoinAuthorisationStatus
|
||||
data object CanKnock : JoinAuthorisationStatus
|
||||
data object CanJoin : JoinAuthorisationStatus
|
||||
data object Unknown : JoinAuthorisationStatus
|
||||
|
||||
@@ -81,6 +81,12 @@ open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
|
||||
isDm = true,
|
||||
)
|
||||
),
|
||||
aJoinRoomState(
|
||||
contentState = aLoadedContentState(
|
||||
name = "A knocked Room",
|
||||
joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -124,13 +130,17 @@ fun aJoinRoomState(
|
||||
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
|
||||
joinAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
knockAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
cancelKnockAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
knockMessage: String = "",
|
||||
eventSink: (JoinRoomEvents) -> Unit = {}
|
||||
) = JoinRoomState(
|
||||
contentState = contentState,
|
||||
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||
joinAction = joinAction,
|
||||
knockAction = knockAction,
|
||||
cancelKnockAction = cancelKnockAction,
|
||||
applicationName = "AppName",
|
||||
knockMessage = knockMessage,
|
||||
eventSink = eventSink
|
||||
)
|
||||
|
||||
|
||||
@@ -9,21 +9,31 @@ package io.element.android.features.joinroom.impl
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
@@ -32,20 +42,25 @@ import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewDescrip
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewSubtitleAtom
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewTitleAtom
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.RoomPreviewMembersCountMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.organisms.RoomPreviewOrganism
|
||||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||
import io.element.android.libraries.designsystem.background.LightGradientBackground
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.components.button.SuperButton
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
import io.element.android.libraries.designsystem.theme.components.ButtonSize
|
||||
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
|
||||
import io.element.android.libraries.designsystem.theme.components.OutlinedTextField
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||
@@ -59,6 +74,7 @@ fun JoinRoomView(
|
||||
onBackClick: () -> Unit,
|
||||
onJoinSuccess: () -> Unit,
|
||||
onKnockSuccess: () -> Unit,
|
||||
onCancelKnockSuccess: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
@@ -69,12 +85,14 @@ fun JoinRoomView(
|
||||
containerColor = Color.Transparent,
|
||||
paddingValues = PaddingValues(16.dp),
|
||||
topBar = {
|
||||
JoinRoomTopBar(onBackClick = onBackClick)
|
||||
JoinRoomTopBar(contentState = state.contentState, onBackClick = onBackClick)
|
||||
},
|
||||
content = {
|
||||
JoinRoomContent(
|
||||
contentState = state.contentState,
|
||||
applicationName = state.applicationName,
|
||||
knockMessage = state.knockMessage,
|
||||
onKnockMessageUpdate = { state.eventSink(JoinRoomEvents.UpdateKnockMessage(it)) },
|
||||
)
|
||||
},
|
||||
footer = {
|
||||
@@ -92,6 +110,9 @@ fun JoinRoomView(
|
||||
onKnockRoom = {
|
||||
state.eventSink(JoinRoomEvents.KnockRoom)
|
||||
},
|
||||
onCancelKnock = {
|
||||
state.eventSink(JoinRoomEvents.CancelKnock(requiresConfirmation = true))
|
||||
},
|
||||
onRetry = {
|
||||
state.eventSink(JoinRoomEvents.RetryFetchingContent)
|
||||
},
|
||||
@@ -103,12 +124,30 @@ fun JoinRoomView(
|
||||
AsyncActionView(
|
||||
async = state.joinAction,
|
||||
onSuccess = { onJoinSuccess() },
|
||||
onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearError) },
|
||||
onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) },
|
||||
)
|
||||
AsyncActionView(
|
||||
async = state.knockAction,
|
||||
onSuccess = { onKnockSuccess() },
|
||||
onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearError) },
|
||||
onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) },
|
||||
)
|
||||
AsyncActionView(
|
||||
async = state.cancelKnockAction,
|
||||
onSuccess = { onCancelKnockSuccess() },
|
||||
onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) },
|
||||
errorMessage = {
|
||||
stringResource(CommonStrings.error_unknown)
|
||||
},
|
||||
confirmationDialog = {
|
||||
ConfirmationDialog(
|
||||
content = stringResource(R.string.screen_join_room_cancel_knock_alert_description),
|
||||
title = stringResource(R.string.screen_join_room_cancel_knock_alert_title),
|
||||
submitText = stringResource(R.string.screen_join_room_cancel_knock_alert_confirmation),
|
||||
cancelText = stringResource(CommonStrings.action_no),
|
||||
onSubmitClick = { state.eventSink(JoinRoomEvents.CancelKnock(requiresConfirmation = false)) },
|
||||
onDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) },
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -119,63 +158,81 @@ private fun JoinRoomFooter(
|
||||
onDeclineInvite: () -> Unit,
|
||||
onJoinRoom: () -> Unit,
|
||||
onKnockRoom: () -> Unit,
|
||||
onCancelKnock: () -> Unit,
|
||||
onRetry: () -> Unit,
|
||||
onGoBack: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (state.contentState is ContentState.Failure) {
|
||||
Button(
|
||||
text = stringResource(CommonStrings.action_retry),
|
||||
onClick = onRetry,
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
size = ButtonSize.Large,
|
||||
)
|
||||
} else if (state.contentState is ContentState.Loaded && state.contentState.roomType == RoomType.Space) {
|
||||
Button(
|
||||
text = stringResource(CommonStrings.action_go_back),
|
||||
onClick = onGoBack,
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
size = ButtonSize.Large,
|
||||
)
|
||||
} else {
|
||||
val joinAuthorisationStatus = state.joinAuthorisationStatus
|
||||
when (joinAuthorisationStatus) {
|
||||
is JoinAuthorisationStatus.IsInvited -> {
|
||||
ButtonRowMolecule(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(20.dp)) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp)
|
||||
) {
|
||||
if (state.contentState is ContentState.Failure) {
|
||||
Button(
|
||||
text = stringResource(CommonStrings.action_retry),
|
||||
onClick = onRetry,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
size = ButtonSize.Large,
|
||||
)
|
||||
} else if (state.contentState is ContentState.Loaded && state.contentState.roomType == RoomType.Space) {
|
||||
Button(
|
||||
text = stringResource(CommonStrings.action_go_back),
|
||||
onClick = onGoBack,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
size = ButtonSize.Large,
|
||||
)
|
||||
} else {
|
||||
val joinAuthorisationStatus = state.joinAuthorisationStatus
|
||||
when (joinAuthorisationStatus) {
|
||||
is JoinAuthorisationStatus.IsInvited -> {
|
||||
ButtonRowMolecule(horizontalArrangement = Arrangement.spacedBy(20.dp)) {
|
||||
OutlinedButton(
|
||||
text = stringResource(CommonStrings.action_decline),
|
||||
onClick = onDeclineInvite,
|
||||
modifier = Modifier.weight(1f),
|
||||
size = ButtonSize.LargeLowPadding,
|
||||
)
|
||||
Button(
|
||||
text = stringResource(CommonStrings.action_accept),
|
||||
onClick = onAcceptInvite,
|
||||
modifier = Modifier.weight(1f),
|
||||
size = ButtonSize.LargeLowPadding,
|
||||
)
|
||||
}
|
||||
}
|
||||
JoinAuthorisationStatus.CanJoin -> {
|
||||
SuperButton(
|
||||
onClick = onJoinRoom,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
buttonSize = ButtonSize.Large,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.screen_join_room_join_action),
|
||||
)
|
||||
}
|
||||
}
|
||||
JoinAuthorisationStatus.CanKnock -> {
|
||||
SuperButton(
|
||||
onClick = onKnockRoom,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
buttonSize = ButtonSize.Large,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.screen_join_room_knock_action),
|
||||
)
|
||||
}
|
||||
}
|
||||
JoinAuthorisationStatus.IsKnocked -> {
|
||||
OutlinedButton(
|
||||
text = stringResource(CommonStrings.action_decline),
|
||||
onClick = onDeclineInvite,
|
||||
modifier = Modifier.weight(1f),
|
||||
size = ButtonSize.LargeLowPadding,
|
||||
)
|
||||
Button(
|
||||
text = stringResource(CommonStrings.action_accept),
|
||||
onClick = onAcceptInvite,
|
||||
modifier = Modifier.weight(1f),
|
||||
size = ButtonSize.LargeLowPadding,
|
||||
text = stringResource(R.string.screen_join_room_cancel_knock_action),
|
||||
onClick = onCancelKnock,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
size = ButtonSize.Large,
|
||||
)
|
||||
}
|
||||
JoinAuthorisationStatus.Unknown -> Unit
|
||||
}
|
||||
JoinAuthorisationStatus.CanJoin -> {
|
||||
SuperButton(
|
||||
onClick = onJoinRoom,
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
buttonSize = ButtonSize.Large,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.screen_join_room_join_action),
|
||||
)
|
||||
}
|
||||
}
|
||||
JoinAuthorisationStatus.CanKnock -> {
|
||||
Button(
|
||||
text = stringResource(R.string.screen_join_room_knock_action),
|
||||
onClick = onKnockRoom,
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
size = ButtonSize.Large,
|
||||
)
|
||||
}
|
||||
JoinAuthorisationStatus.Unknown -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -184,132 +241,217 @@ private fun JoinRoomFooter(
|
||||
private fun JoinRoomContent(
|
||||
contentState: ContentState,
|
||||
applicationName: String,
|
||||
knockMessage: String,
|
||||
onKnockMessageUpdate: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
when (contentState) {
|
||||
is ContentState.Loaded -> {
|
||||
RoomPreviewOrganism(
|
||||
modifier = modifier,
|
||||
avatar = {
|
||||
Avatar(contentState.avatarData(AvatarSize.RoomHeader))
|
||||
},
|
||||
title = {
|
||||
if (contentState.name != null) {
|
||||
RoomPreviewTitleAtom(
|
||||
title = contentState.name,
|
||||
Box(modifier = modifier) {
|
||||
when (contentState) {
|
||||
is ContentState.Loaded -> {
|
||||
when (contentState.joinAuthorisationStatus) {
|
||||
is JoinAuthorisationStatus.IsKnocked -> {
|
||||
IsKnockedLoadedContent()
|
||||
}
|
||||
else -> {
|
||||
DefaultLoadedContent(
|
||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||
contentState = contentState,
|
||||
applicationName = applicationName,
|
||||
knockMessage = knockMessage,
|
||||
onKnockMessageUpdate = onKnockMessageUpdate
|
||||
)
|
||||
} else {
|
||||
RoomPreviewTitleAtom(
|
||||
title = stringResource(id = CommonStrings.common_no_room_name),
|
||||
fontStyle = FontStyle.Italic
|
||||
)
|
||||
}
|
||||
},
|
||||
subtitle = {
|
||||
if (contentState.alias != null) {
|
||||
RoomPreviewSubtitleAtom(contentState.alias.value)
|
||||
}
|
||||
},
|
||||
description = {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
val inviteSender = (contentState.joinAuthorisationStatus as? JoinAuthorisationStatus.IsInvited)?.inviteSender
|
||||
if (inviteSender != null) {
|
||||
InviteSenderView(inviteSender = inviteSender)
|
||||
}
|
||||
RoomPreviewDescriptionAtom(contentState.topic ?: "")
|
||||
if (contentState.roomType == RoomType.Space) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.screen_join_room_space_not_supported_title),
|
||||
textAlign = TextAlign.Center,
|
||||
style = ElementTheme.typography.fontBodyLgMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.screen_join_room_space_not_supported_description, applicationName),
|
||||
textAlign = TextAlign.Center,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
memberCount = {
|
||||
if (contentState.showMemberCount) {
|
||||
RoomPreviewMembersCountMolecule(memberCount = contentState.numberOfMembers ?: 0)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
is ContentState.UnknownRoom -> {
|
||||
RoomPreviewOrganism(
|
||||
modifier = modifier,
|
||||
avatar = {
|
||||
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
|
||||
},
|
||||
title = {
|
||||
RoomPreviewTitleAtom(stringResource(R.string.screen_join_room_title_no_preview))
|
||||
},
|
||||
subtitle = {
|
||||
RoomPreviewSubtitleAtom(stringResource(R.string.screen_join_room_subtitle_no_preview))
|
||||
},
|
||||
)
|
||||
}
|
||||
is ContentState.Loading -> {
|
||||
RoomPreviewOrganism(
|
||||
modifier = modifier,
|
||||
avatar = {
|
||||
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
|
||||
},
|
||||
title = {
|
||||
PlaceholderAtom(width = 200.dp, height = 22.dp)
|
||||
},
|
||||
subtitle = {
|
||||
PlaceholderAtom(width = 140.dp, height = 20.dp)
|
||||
},
|
||||
)
|
||||
}
|
||||
is ContentState.Failure -> {
|
||||
RoomPreviewOrganism(
|
||||
modifier = modifier,
|
||||
avatar = {
|
||||
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
|
||||
},
|
||||
title = {
|
||||
when (contentState.roomIdOrAlias) {
|
||||
is RoomIdOrAlias.Alias -> {
|
||||
RoomPreviewTitleAtom(contentState.roomIdOrAlias.identifier)
|
||||
}
|
||||
is ContentState.UnknownRoom -> {
|
||||
RoomPreviewOrganism(
|
||||
avatar = {
|
||||
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
|
||||
},
|
||||
title = {
|
||||
RoomPreviewTitleAtom(stringResource(R.string.screen_join_room_title_no_preview))
|
||||
},
|
||||
subtitle = {
|
||||
RoomPreviewSubtitleAtom(stringResource(R.string.screen_join_room_subtitle_no_preview))
|
||||
},
|
||||
)
|
||||
}
|
||||
is ContentState.Loading -> {
|
||||
RoomPreviewOrganism(
|
||||
avatar = {
|
||||
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
|
||||
},
|
||||
title = {
|
||||
PlaceholderAtom(width = 200.dp, height = 22.dp)
|
||||
},
|
||||
subtitle = {
|
||||
PlaceholderAtom(width = 140.dp, height = 20.dp)
|
||||
},
|
||||
)
|
||||
}
|
||||
is ContentState.Failure -> {
|
||||
RoomPreviewOrganism(
|
||||
avatar = {
|
||||
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
|
||||
},
|
||||
title = {
|
||||
when (contentState.roomIdOrAlias) {
|
||||
is RoomIdOrAlias.Alias -> {
|
||||
RoomPreviewTitleAtom(contentState.roomIdOrAlias.identifier)
|
||||
}
|
||||
is RoomIdOrAlias.Id -> {
|
||||
PlaceholderAtom(width = 200.dp, height = 22.dp)
|
||||
}
|
||||
}
|
||||
is RoomIdOrAlias.Id -> {
|
||||
PlaceholderAtom(width = 200.dp, height = 22.dp)
|
||||
}
|
||||
}
|
||||
},
|
||||
subtitle = {
|
||||
Text(
|
||||
text = stringResource(id = CommonStrings.error_unknown),
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
subtitle = {
|
||||
Text(
|
||||
text = stringResource(id = CommonStrings.error_unknown),
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun IsKnockedLoadedContent(modifier: Modifier = Modifier) {
|
||||
BoxWithConstraints(
|
||||
modifier = modifier
|
||||
.fillMaxHeight()
|
||||
.padding(horizontal = 16.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
IconTitleSubtitleMolecule(
|
||||
modifier = Modifier.sizeIn(minHeight = maxHeight * 0.7f),
|
||||
iconStyle = BigIcon.Style.SuccessSolid,
|
||||
title = stringResource(R.string.screen_join_room_knock_sent_title),
|
||||
subTitle = stringResource(R.string.screen_join_room_knock_sent_description),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DefaultLoadedContent(
|
||||
contentState: ContentState.Loaded,
|
||||
applicationName: String,
|
||||
knockMessage: String,
|
||||
onKnockMessageUpdate: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
RoomPreviewOrganism(
|
||||
modifier = modifier,
|
||||
avatar = {
|
||||
Avatar(contentState.avatarData(AvatarSize.RoomHeader))
|
||||
},
|
||||
title = {
|
||||
if (contentState.name != null) {
|
||||
RoomPreviewTitleAtom(
|
||||
title = contentState.name,
|
||||
)
|
||||
} else {
|
||||
RoomPreviewTitleAtom(
|
||||
title = stringResource(id = CommonStrings.common_no_room_name),
|
||||
fontStyle = FontStyle.Italic
|
||||
)
|
||||
}
|
||||
},
|
||||
subtitle = {
|
||||
if (contentState.alias != null) {
|
||||
RoomPreviewSubtitleAtom(contentState.alias.value)
|
||||
}
|
||||
},
|
||||
description = {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
val inviteSender = (contentState.joinAuthorisationStatus as? JoinAuthorisationStatus.IsInvited)?.inviteSender
|
||||
if (inviteSender != null) {
|
||||
InviteSenderView(inviteSender = inviteSender)
|
||||
}
|
||||
RoomPreviewDescriptionAtom(contentState.topic ?: "")
|
||||
if (contentState.roomType == RoomType.Space) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.screen_join_room_space_not_supported_title),
|
||||
textAlign = TextAlign.Center,
|
||||
style = ElementTheme.typography.fontBodyLgMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.screen_join_room_space_not_supported_description, applicationName),
|
||||
textAlign = TextAlign.Center,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
)
|
||||
} else if (contentState.joinAuthorisationStatus is JoinAuthorisationStatus.CanKnock) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
OutlinedTextField(
|
||||
value = knockMessage,
|
||||
onValueChange = onKnockMessageUpdate,
|
||||
maxLines = 3,
|
||||
minLines = 3,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.screen_join_room_knock_message_description),
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
color = ElementTheme.colors.textPlaceholder,
|
||||
textAlign = TextAlign.Start,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
memberCount = {
|
||||
if (contentState.showMemberCount) {
|
||||
RoomPreviewMembersCountMolecule(memberCount = contentState.numberOfMembers ?: 0)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun JoinRoomTopBar(
|
||||
contentState: ContentState,
|
||||
onBackClick: () -> Unit,
|
||||
) {
|
||||
TopAppBar(
|
||||
navigationIcon = {
|
||||
BackButton(onClick = onBackClick)
|
||||
},
|
||||
title = {},
|
||||
title = {
|
||||
if (contentState is ContentState.Loaded && contentState.joinAuthorisationStatus is JoinAuthorisationStatus.IsKnocked) {
|
||||
val roundedCornerShape = RoundedCornerShape(8.dp)
|
||||
val titleModifier = Modifier
|
||||
.clip(roundedCornerShape)
|
||||
if (contentState.name != null) {
|
||||
Row(
|
||||
modifier = titleModifier,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Avatar(avatarData = contentState.avatarData(AvatarSize.TimelineRoom))
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
text = contentState.name,
|
||||
style = ElementTheme.typography.fontBodyLgMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
} else {
|
||||
IconTitlePlaceholdersRowMolecule(
|
||||
iconSize = AvatarSize.TimelineRoom.dp,
|
||||
modifier = titleModifier
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -321,5 +463,6 @@ internal fun JoinRoomViewPreview(@PreviewParameter(JoinRoomStateProvider::class)
|
||||
onBackClick = { },
|
||||
onJoinSuccess = { },
|
||||
onKnockSuccess = { },
|
||||
onCancelKnockSuccess = { },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.joinroom.impl.di
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import javax.inject.Inject
|
||||
|
||||
interface CancelKnockRoom {
|
||||
suspend operator fun invoke(roomId: RoomId): Result<Unit>
|
||||
}
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class DefaultCancelKnockRoom @Inject constructor(private val client: MatrixClient) : CancelKnockRoom {
|
||||
override suspend fun invoke(roomId: RoomId): Result<Unit> {
|
||||
return client
|
||||
.getPendingRoom(roomId)
|
||||
?.leave()
|
||||
?: Result.failure(IllegalStateException("No pending room found"))
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ object JoinRoomModule {
|
||||
client: MatrixClient,
|
||||
joinRoom: JoinRoom,
|
||||
knockRoom: KnockRoom,
|
||||
cancelKnockRoom: CancelKnockRoom,
|
||||
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
||||
buildMeta: BuildMeta,
|
||||
): JoinRoomPresenter.Factory {
|
||||
@@ -51,6 +52,7 @@ object JoinRoomModule {
|
||||
matrixClient = client,
|
||||
joinRoom = joinRoom,
|
||||
knockRoom = knockRoom,
|
||||
cancelKnockRoom = cancelKnockRoom,
|
||||
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
|
||||
buildMeta = buildMeta,
|
||||
)
|
||||
|
||||
@@ -10,14 +10,26 @@ package io.element.android.features.joinroom.impl.di
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||
import javax.inject.Inject
|
||||
|
||||
interface KnockRoom {
|
||||
suspend operator fun invoke(roomId: RoomId): Result<Unit>
|
||||
suspend operator fun invoke(
|
||||
roomIdOrAlias: RoomIdOrAlias,
|
||||
message: String,
|
||||
serverNames: List<String>,
|
||||
): Result<Unit>
|
||||
}
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class DefaultKnockRoom @Inject constructor(private val client: MatrixClient) : KnockRoom {
|
||||
override suspend fun invoke(roomId: RoomId) = client.knockRoom(roomId)
|
||||
override suspend fun invoke(
|
||||
roomIdOrAlias: RoomIdOrAlias,
|
||||
message: String,
|
||||
serverNames: List<String>
|
||||
): Result<Unit> {
|
||||
return client
|
||||
.knockRoom(roomIdOrAlias, message, serverNames)
|
||||
.map { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_join_room_cancel_knock_action">"Zrušit žádost"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_confirmation">"Ano, zrušit"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_description">"Opravdu chcete zrušit svou žádost o vstup do této místnosti?"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_title">"Zrušit žádost o vstup"</string>
|
||||
<string name="screen_join_room_join_action">"Připojit se do místnosti"</string>
|
||||
<string name="screen_join_room_knock_action">"Zaklepejte a připojte se"</string>
|
||||
<string name="screen_join_room_knock_message_description">"Zpráva (nepovinné)"</string>
|
||||
<string name="screen_join_room_knock_sent_description">"Pokud bude váš požadavek přijat, obdržíte pozvánku na vstup do místnosti."</string>
|
||||
<string name="screen_join_room_knock_sent_title">"Žádost o vstup odeslána"</string>
|
||||
<string name="screen_join_room_space_not_supported_description">"%1$s zatím nepodporuje prostory. Prostory můžete používat na webu."</string>
|
||||
<string name="screen_join_room_space_not_supported_title">"Prostory zatím nejsou podporovány"</string>
|
||||
<string name="screen_join_room_subtitle_knock">"Klikněte na tlačítko níže a správce místnosti bude informován. Po schválení se budete moci připojit ke konverzaci."</string>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_join_room_cancel_knock_action">"Ακύρωση αιτήματος"</string>
|
||||
<string name="screen_join_room_join_action">"Συμμετοχή στο δωμάτιο"</string>
|
||||
<string name="screen_join_room_knock_action">"Χτύπα για συμμετοχή"</string>
|
||||
<string name="screen_join_room_knock_message_description">"Μήνυμα (προαιρετικό)"</string>
|
||||
<string name="screen_join_room_knock_sent_description">"Θα λάβεις πρόσκληση για συμμετοχή στο δωμάτιο εάν το αίτημά σου γίνει αποδεκτό."</string>
|
||||
<string name="screen_join_room_knock_sent_title">"Το αίτημα συμμετοχής στάλθηκε"</string>
|
||||
<string name="screen_join_room_space_not_supported_description">"Το %1$s δεν υποστηρίζει ακόμα χώρους. Μπορείς να έχεις πρόσβαση σε χώρους στον ιστό."</string>
|
||||
<string name="screen_join_room_space_not_supported_title">"Οι Χώροι δεν υποστηρίζονται ακόμα"</string>
|
||||
<string name="screen_join_room_subtitle_knock">"Κάνε κλικ στο παρακάτω κουμπί και ένας διαχειριστής δωματίου θα ειδοποιηθεί. Θα μπορείς να συμμετάσχεις στη συνομιλία μόλις εγκριθεί."</string>
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_join_room_cancel_knock_action">"Tühista liitumispalve"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_confirmation">"Jah, tühista"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_description">"Kas sa oled kindel, et soovid tühistada oma palve jututoaga liitumiseks?"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_title">"Tühista liitumispalve"</string>
|
||||
<string name="screen_join_room_join_action">"Liitu jututoaga"</string>
|
||||
<string name="screen_join_room_knock_action">"Liitumiseks koputa jututoa uksele"</string>
|
||||
<string name="screen_join_room_knock_message_description">"Selgitus (kui soovid lisada)"</string>
|
||||
<string name="screen_join_room_knock_sent_description">"Kui sinu liitumispalvega ollakse nõus, siis saad kutse jututoaga liitumiseks."</string>
|
||||
<string name="screen_join_room_knock_sent_title">"Liitumispalve on saadetud"</string>
|
||||
<string name="screen_join_room_space_not_supported_description">"%1$s veel ei toeta kogukondadega liitumise ja kasutamise võimalust. Vajadusel saad seda teha veebiliidese vahendusel."</string>
|
||||
<string name="screen_join_room_space_not_supported_title">"Kogukonnad pole veel toetatud"</string>
|
||||
<string name="screen_join_room_subtitle_knock">"Klõpsi allolevat nuppu ja jututoa haldaja saab asjakohase teate. Sa saad liituda, kui haldaja sinu soovi heaks kiidab."</string>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_join_room_cancel_knock_action">"Annuler la demande"</string>
|
||||
<string name="screen_join_room_join_action">"Rejoindre"</string>
|
||||
<string name="screen_join_room_knock_action">"Demander à joindre"</string>
|
||||
<string name="screen_join_room_knock_message_description">"Message (facultatif)"</string>
|
||||
<string name="screen_join_room_knock_sent_description">"Vous recevrez une invitation à rejoindre le salon si votre demande est acceptée."</string>
|
||||
<string name="screen_join_room_knock_sent_title">"Demande de rejoindre le salon envoyée"</string>
|
||||
<string name="screen_join_room_space_not_supported_description">"Les Spaces ne sont pas encore pris en charge par %1$s . Vous pouvez voir les Spaces sur le Web."</string>
|
||||
<string name="screen_join_room_space_not_supported_title">"Les Spaces ne sont pas encore pris en charge"</string>
|
||||
<string name="screen_join_room_subtitle_knock">"Cliquez ci-dessous et un administrateur sera prévenu. Une fois votre demande approuvée, pour pourrez rejoindre la discussion."</string>
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_join_room_cancel_knock_action">"Kérés visszavonása"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_confirmation">"Igen, visszavonás"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_description">"Biztos, hogy visszavonja a szobához való csatlakozási kérését?"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_title">"Csatlakozási kérés visszavonása"</string>
|
||||
<string name="screen_join_room_join_action">"Csatlakozás a szobához"</string>
|
||||
<string name="screen_join_room_knock_action">"Kopogtasson a csatlakozáshoz"</string>
|
||||
<string name="screen_join_room_knock_message_description">"Üzenet (nem kötelező)"</string>
|
||||
<string name="screen_join_room_knock_sent_description">"Ha a kérését elfogadják, meghívót kap a szobához való csatlakozáshoz."</string>
|
||||
<string name="screen_join_room_knock_sent_title">"Csatlakozási kérés elküldve"</string>
|
||||
<string name="screen_join_room_space_not_supported_description">"Az %1$s még nem támogatja a tereket. A tereket a weben érheti el."</string>
|
||||
<string name="screen_join_room_space_not_supported_title">"A terek még nem támogatottak"</string>
|
||||
<string name="screen_join_room_subtitle_knock">"Kattintson az alábbi gombra, és a szoba adminisztrátora értesítést kap. A jóváhagyást követően csatlakozhat a beszélgetéshez."</string>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_join_room_cancel_knock_action">"Cancelar pedido"</string>
|
||||
<string name="screen_join_room_join_action">"Entrar na sala"</string>
|
||||
<string name="screen_join_room_knock_action">"Bater à porta"</string>
|
||||
<string name="screen_join_room_knock_message_description">"Mensagem (opcional)"</string>
|
||||
<string name="screen_join_room_knock_sent_description">"Irá receber um convite para participar na sala se seu pedido for aceite."</string>
|
||||
<string name="screen_join_room_knock_sent_title">"Pedido de adesão enviado"</string>
|
||||
<string name="screen_join_room_space_not_supported_description">"A %1$s ainda não funciona com espaços. Podes usá-los na aplicação web."</string>
|
||||
<string name="screen_join_room_space_not_supported_title">"Os espaços ainda não estão implementados"</string>
|
||||
<string name="screen_join_room_subtitle_knock">"Carrega no botão abaixo para notificar um administrador da sala. Poderás entrar quando te aprovarem."</string>
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_join_room_cancel_knock_action">"Отменить запрос"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_confirmation">"Да, отменить"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_description">"Вы действительно хотите отменить заявку на вступление в эту комнату?"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_title">"Отменить запрос на присоединение"</string>
|
||||
<string name="screen_join_room_join_action">"Присоединиться к комнате"</string>
|
||||
<string name="screen_join_room_knock_action">"Постучите, чтобы присоединиться"</string>
|
||||
<string name="screen_join_room_knock_message_description">"Сообщение (опционально)"</string>
|
||||
<string name="screen_join_room_knock_sent_description">"Вы получите приглашение присоединиться к комнате, как только ваш запрос будет принят."</string>
|
||||
<string name="screen_join_room_knock_sent_title">"Запрос на присоединение отправлен"</string>
|
||||
<string name="screen_join_room_space_not_supported_description">"%1$s еще не поддерживает пространства. Вы можете получить к ним доступ в веб-версии."</string>
|
||||
<string name="screen_join_room_space_not_supported_title">"Пространства пока не поддерживаются"</string>
|
||||
<string name="screen_join_room_subtitle_knock">"Нажмите кнопку ниже и администратор комнаты получит уведомление. После одобрения вы сможете присоединиться к обсуждению."</string>
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_join_room_cancel_knock_action">"Zrušiť žiadosť"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_confirmation">"Áno, zrušiť"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_description">"Ste si istí, že chcete zrušiť svoju žiadosť o vstup do tejto miestnosti?"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_title">"Zrušiť žiadosť o pripojenie"</string>
|
||||
<string name="screen_join_room_join_action">"Pripojiť sa do miestnosti"</string>
|
||||
<string name="screen_join_room_knock_action">"Zaklopaním sa pripojíte"</string>
|
||||
<string name="screen_join_room_knock_message_description">"Správa (voliteľné)"</string>
|
||||
<string name="screen_join_room_knock_sent_description">"Ak bude vaša žiadosť prijatá, dostanete pozvánku na vstup do miestnosti."</string>
|
||||
<string name="screen_join_room_knock_sent_title">"Žiadosť o pripojenie bola odoslaná"</string>
|
||||
<string name="screen_join_room_space_not_supported_description">"%1$s zatiaľ nepodporuje priestory. K priestorom môžete pristupovať na webe."</string>
|
||||
<string name="screen_join_room_space_not_supported_title">"Priestory zatiaľ nie sú podporované"</string>
|
||||
<string name="screen_join_room_subtitle_knock">"Kliknite na tlačidlo nižšie a správca miestnosti bude informovaný. Po schválení sa budete môcť pripojiť ku konverzácii."</string>
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_join_room_cancel_knock_action">"Cancel request"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_confirmation">"Yes, cancel"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_description">"Are you sure that you want to cancel your request to join this room?"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_title">"Cancel request to join"</string>
|
||||
<string name="screen_join_room_join_action">"Join room"</string>
|
||||
<string name="screen_join_room_knock_action">"Send request to join"</string>
|
||||
<string name="screen_join_room_knock_message_description">"Message (optional)"</string>
|
||||
<string name="screen_join_room_knock_sent_description">"You will receive an invite to join the room if your request is accepted."</string>
|
||||
<string name="screen_join_room_knock_sent_title">"Request to join sent"</string>
|
||||
<string name="screen_join_room_space_not_supported_description">"%1$s does not support spaces yet. You can access spaces on web."</string>
|
||||
<string name="screen_join_room_space_not_supported_title">"Spaces are not supported yet"</string>
|
||||
<string name="screen_join_room_subtitle_knock">"Click the button below and a room administrator will be notified. You’ll be able to join the conversation once approved."</string>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.joinroom.impl
|
||||
|
||||
import io.element.android.features.joinroom.impl.di.CancelKnockRoom
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.tests.testutils.simulateLongTask
|
||||
|
||||
class FakeCancelKnockRoom(
|
||||
var lambda: (RoomId) -> Result<Unit> = { Result.success(Unit) }
|
||||
) : CancelKnockRoom {
|
||||
override suspend fun invoke(roomId: RoomId) = simulateLongTask {
|
||||
lambda(roomId)
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,13 @@
|
||||
package io.element.android.features.joinroom.impl
|
||||
|
||||
import io.element.android.features.joinroom.impl.di.KnockRoom
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||
import io.element.android.tests.testutils.simulateLongTask
|
||||
|
||||
class FakeKnockRoom(
|
||||
var lambda: (RoomId) -> Result<Unit> = { Result.success(Unit) }
|
||||
var lambda: (RoomIdOrAlias, String, List<String>) -> Result<Unit> = { _, _, _ -> Result.success(Unit) }
|
||||
) : KnockRoom {
|
||||
override suspend fun invoke(roomId: RoomId) = simulateLongTask {
|
||||
lambda(roomId)
|
||||
override suspend fun invoke(roomIdOrAlias: RoomIdOrAlias, message: String, serverNames: List<String>): Result<Unit> = simulateLongTask {
|
||||
lambda(roomIdOrAlias, message, serverNames)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import im.vector.app.features.analytics.plan.JoinedRoom
|
||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
|
||||
import io.element.android.features.joinroom.impl.di.CancelKnockRoom
|
||||
import io.element.android.features.joinroom.impl.di.KnockRoom
|
||||
import io.element.android.features.roomdirectory.api.RoomDescription
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
@@ -37,6 +38,7 @@ import io.element.android.libraries.matrix.test.room.aRoomSummary
|
||||
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
|
||||
import io.element.android.libraries.matrix.ui.model.toInviteSender
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.lambda.any
|
||||
import io.element.android.tests.testutils.lambda.assert
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
@@ -59,6 +61,8 @@ class JoinRoomPresenterTest {
|
||||
assertThat(state.contentState).isEqualTo(ContentState.Loading(A_ROOM_ID.toRoomIdOrAlias()))
|
||||
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Unknown)
|
||||
assertThat(state.acceptDeclineInviteState).isEqualTo(anAcceptDeclineInviteState())
|
||||
assertThat(state.cancelKnockAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
assertThat(state.knockAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
assertThat(state.applicationName).isEqualTo("AppName")
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
@@ -214,7 +218,7 @@ class JoinRoomPresenterTest {
|
||||
}
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.joinAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
|
||||
state.eventSink(JoinRoomEvents.ClearError)
|
||||
state.eventSink(JoinRoomEvents.ClearActionStates)
|
||||
}
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.joinAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
@@ -325,16 +329,20 @@ class JoinRoomPresenterTest {
|
||||
|
||||
@Test
|
||||
fun `present - emit knock room event`() = runTest {
|
||||
val knockRoomSuccess = lambdaRecorder { _: RoomId ->
|
||||
val knockMessage = "Knock message"
|
||||
val knockRoomSuccess = lambdaRecorder { _: RoomIdOrAlias, _: String, _: List<String> ->
|
||||
Result.success(Unit)
|
||||
}
|
||||
val knockRoomFailure = lambdaRecorder { roomId: RoomId ->
|
||||
Result.failure<Unit>(RuntimeException("Failed to knock room $roomId"))
|
||||
val knockRoomFailure = lambdaRecorder { roomIdOrAlias: RoomIdOrAlias, _: String, _: List<String> ->
|
||||
Result.failure<Unit>(RuntimeException("Failed to knock room $roomIdOrAlias"))
|
||||
}
|
||||
val fakeKnockRoom = FakeKnockRoom(knockRoomSuccess)
|
||||
val presenter = createJoinRoomPresenter(knockRoom = fakeKnockRoom)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
awaitItem().also { state ->
|
||||
state.eventSink(JoinRoomEvents.UpdateKnockMessage(knockMessage))
|
||||
}
|
||||
awaitItem().also { state ->
|
||||
state.eventSink(JoinRoomEvents.KnockRoom)
|
||||
}
|
||||
@@ -353,8 +361,46 @@ class JoinRoomPresenterTest {
|
||||
}
|
||||
assert(knockRoomSuccess)
|
||||
.isCalledOnce()
|
||||
.with(value(A_ROOM_ID))
|
||||
.with(value(A_ROOM_ID.toRoomIdOrAlias()), value(knockMessage), any())
|
||||
assert(knockRoomFailure)
|
||||
.isCalledOnce()
|
||||
.with(value(A_ROOM_ID.toRoomIdOrAlias()), value(knockMessage), any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - emit cancel knock room event`() = runTest {
|
||||
val cancelKnockRoomSuccess = lambdaRecorder { _: RoomId ->
|
||||
Result.success(Unit)
|
||||
}
|
||||
val cancelKnockRoomFailure = lambdaRecorder { roomId: RoomId ->
|
||||
Result.failure<Unit>(RuntimeException("Failed to knock room $roomId"))
|
||||
}
|
||||
val cancelKnockRoom = FakeCancelKnockRoom(cancelKnockRoomSuccess)
|
||||
val presenter = createJoinRoomPresenter(cancelKnockRoom = cancelKnockRoom)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
awaitItem().also { state ->
|
||||
state.eventSink(JoinRoomEvents.CancelKnock(true))
|
||||
}
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.cancelKnockAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||
state.eventSink(JoinRoomEvents.CancelKnock(false))
|
||||
}
|
||||
assertThat(awaitItem().cancelKnockAction).isEqualTo(AsyncAction.Loading)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.cancelKnockAction).isEqualTo(AsyncAction.Success(Unit))
|
||||
cancelKnockRoom.lambda = cancelKnockRoomFailure
|
||||
state.eventSink(JoinRoomEvents.CancelKnock(false))
|
||||
}
|
||||
assertThat(awaitItem().cancelKnockAction).isEqualTo(AsyncAction.Loading)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.cancelKnockAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||
}
|
||||
}
|
||||
assert(cancelKnockRoomFailure)
|
||||
.isCalledOnce()
|
||||
.with(value(A_ROOM_ID))
|
||||
assert(cancelKnockRoomSuccess)
|
||||
.isCalledOnce()
|
||||
.with(value(A_ROOM_ID))
|
||||
}
|
||||
@@ -474,6 +520,7 @@ class JoinRoomPresenterTest {
|
||||
Result.success(Unit)
|
||||
},
|
||||
knockRoom: KnockRoom = FakeKnockRoom(),
|
||||
cancelKnockRoom: CancelKnockRoom = FakeCancelKnockRoom(),
|
||||
buildMeta: BuildMeta = aBuildMeta(applicationName = "AppName"),
|
||||
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() }
|
||||
): JoinRoomPresenter {
|
||||
@@ -486,6 +533,7 @@ class JoinRoomPresenterTest {
|
||||
matrixClient = matrixClient,
|
||||
joinRoom = FakeJoinRoom(joinRoomLambda),
|
||||
knockRoom = knockRoom,
|
||||
cancelKnockRoom = cancelKnockRoom,
|
||||
buildMeta = buildMeta,
|
||||
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter
|
||||
)
|
||||
|
||||
@@ -61,6 +61,7 @@ class JoinRoomViewTest {
|
||||
rule.setJoinRoomView(
|
||||
aJoinRoomState(
|
||||
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock),
|
||||
knockMessage = "Knock knock",
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
@@ -79,7 +80,34 @@ class JoinRoomViewTest {
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_ok)
|
||||
eventsRecorder.assertSingle(JoinRoomEvents.ClearError)
|
||||
eventsRecorder.assertSingle(JoinRoomEvents.ClearActionStates)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on cancel knock request emit the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
|
||||
rule.setJoinRoomView(
|
||||
aJoinRoomState(
|
||||
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked),
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.clickOn(R.string.screen_join_room_cancel_knock_action)
|
||||
eventsRecorder.assertSingle(JoinRoomEvents.CancelKnock(true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on closing Cancel Knock error emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
|
||||
rule.setJoinRoomView(
|
||||
aJoinRoomState(
|
||||
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked),
|
||||
cancelKnockAction = AsyncAction.Failure(Exception("Error")),
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_ok)
|
||||
eventsRecorder.assertSingle(JoinRoomEvents.ClearActionStates)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -93,7 +121,7 @@ class JoinRoomViewTest {
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_ok)
|
||||
eventsRecorder.assertSingle(JoinRoomEvents.ClearError)
|
||||
eventsRecorder.assertSingle(JoinRoomEvents.ClearActionStates)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -170,6 +198,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setJoinR
|
||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||
onJoinSuccess: () -> Unit = EnsureNeverCalled(),
|
||||
onKnockSuccess: () -> Unit = EnsureNeverCalled(),
|
||||
onCancelKnockSuccess: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
setContent {
|
||||
JoinRoomView(
|
||||
@@ -177,6 +206,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setJoinR
|
||||
onBackClick = onBackClick,
|
||||
onJoinSuccess = onJoinSuccess,
|
||||
onKnockSuccess = onKnockSuccess,
|
||||
onCancelKnockSuccess = onCancelKnockSuccess
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import io.element.android.features.lockscreen.impl.R
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
@@ -51,7 +52,7 @@ fun SetupBiometricView(
|
||||
private fun SetupBiometricHeader() {
|
||||
val biometricAuth = stringResource(id = R.string.screen_app_lock_biometric_authentication)
|
||||
IconTitleSubtitleMolecule(
|
||||
iconImageVector = Icons.Default.Fingerprint,
|
||||
iconStyle = BigIcon.Style.Default(Icons.Default.Fingerprint),
|
||||
title = stringResource(id = R.string.screen_app_lock_settings_enable_biometric_unlock),
|
||||
subTitle = stringResource(id = R.string.screen_app_lock_setup_biometric_unlock_subtitle, biometricAuth),
|
||||
)
|
||||
|
||||
@@ -33,6 +33,7 @@ import io.element.android.features.lockscreen.impl.R
|
||||
import io.element.android.features.lockscreen.impl.components.PinEntryTextField
|
||||
import io.element.android.features.lockscreen.impl.setup.pin.validation.SetupPinFailure
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
@@ -88,7 +89,7 @@ private fun SetupPinHeader(
|
||||
stringResource(id = R.string.screen_app_lock_setup_choose_pin)
|
||||
},
|
||||
subTitle = stringResource(id = R.string.screen_app_lock_setup_pin_context, appName),
|
||||
iconImageVector = Icons.Filled.Lock,
|
||||
iconStyle = BigIcon.Style.Default(Icons.Filled.Lock),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
||||
import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypad
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||
@@ -299,7 +299,7 @@ private fun PinUnlockHeader(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
if (isInAppUnlock) {
|
||||
RoundedIconAtom(imageVector = Icons.Filled.Lock)
|
||||
BigIcon(style = BigIcon.Style.Default(Icons.Filled.Lock))
|
||||
} else {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
|
||||
@@ -22,7 +22,6 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -34,6 +33,7 @@ import io.element.android.features.login.impl.accountprovider.AccountProviderVie
|
||||
import io.element.android.features.login.impl.changeserver.ChangeServerEvents
|
||||
import io.element.android.features.login.impl.changeserver.ChangeServerView
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
@@ -74,8 +74,7 @@ fun ChangeAccountProviderView(
|
||||
) {
|
||||
IconTitleSubtitleMolecule(
|
||||
modifier = Modifier.padding(top = 16.dp, bottom = 32.dp, start = 16.dp, end = 16.dp),
|
||||
iconImageVector = Icons.Filled.Home,
|
||||
iconTint = MaterialTheme.colorScheme.primary,
|
||||
iconStyle = BigIcon.Style.Default(Icons.Filled.Home),
|
||||
title = stringResource(id = R.string.screen_change_account_provider_title),
|
||||
subTitle = stringResource(id = R.string.screen_change_account_provider_subtitle),
|
||||
)
|
||||
|
||||
@@ -27,6 +27,7 @@ import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
@@ -59,7 +60,7 @@ fun ConfirmAccountProviderView(
|
||||
header = {
|
||||
IconTitleSubtitleMolecule(
|
||||
modifier = Modifier.padding(top = 60.dp),
|
||||
iconImageVector = Icons.Filled.AccountCircle,
|
||||
iconStyle = BigIcon.Style.Default(Icons.Filled.AccountCircle),
|
||||
title = stringResource(
|
||||
id = if (state.isAccountCreation) {
|
||||
R.string.screen_account_provider_signup_title
|
||||
|
||||
@@ -48,6 +48,7 @@ import io.element.android.features.login.impl.error.loginError
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||
import io.element.android.libraries.designsystem.components.form.textFieldState
|
||||
@@ -110,7 +111,7 @@ fun LoginPasswordView(
|
||||
// Title
|
||||
IconTitleSubtitleMolecule(
|
||||
modifier = Modifier.padding(top = 20.dp, start = 16.dp, end = 16.dp),
|
||||
iconImageVector = Icons.Filled.AccountCircle,
|
||||
iconStyle = BigIcon.Style.Default(Icons.Filled.AccountCircle),
|
||||
title = stringResource(
|
||||
id = R.string.screen_account_provider_signin_title,
|
||||
state.accountProvider.title
|
||||
|
||||
@@ -48,6 +48,7 @@ import io.element.android.features.login.impl.changeserver.ChangeServerView
|
||||
import io.element.android.features.login.impl.resolver.HomeserverData
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.components.form.textFieldState
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
@@ -94,7 +95,7 @@ fun SearchAccountProviderView(
|
||||
item {
|
||||
IconTitleSubtitleMolecule(
|
||||
modifier = Modifier.padding(top = 16.dp, bottom = 40.dp, start = 16.dp, end = 16.dp),
|
||||
iconImageVector = CompoundIcons.Search(),
|
||||
iconStyle = BigIcon.Style.Default(CompoundIcons.Search()),
|
||||
title = stringResource(id = R.string.screen_account_provider_form_title),
|
||||
subTitle = stringResource(id = R.string.screen_account_provider_form_subtitle),
|
||||
)
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<string name="screen_change_server_subtitle">"Какой адрес у вашего сервера?"</string>
|
||||
<string name="screen_change_server_title">"Выберите свой сервер"</string>
|
||||
<string name="screen_create_account_title">"Создать учетную запись"</string>
|
||||
<string name="screen_login_error_deactivated_account">"Данная учетная запись была деактивирована."</string>
|
||||
<string name="screen_login_error_deactivated_account">"Данная учётная запись была отключена."</string>
|
||||
<string name="screen_login_error_invalid_credentials">"Неверное имя пользователя и/или пароль"</string>
|
||||
<string name="screen_login_error_invalid_user_id">"Это не корректный идентификатор пользователя. Ожидаемый формат: \'@user:homeserver.org\'"</string>
|
||||
<string name="screen_login_error_refresh_tokens">"Этот сервер настроен на использование токенов обновления. Они не поддерживаются при использовании входа на основе пароля."</string>
|
||||
|
||||
@@ -32,7 +32,5 @@ sealed class TimelineItemAction(
|
||||
data object ReportContent : TimelineItemAction(CommonStrings.action_report_content, CompoundDrawables.ic_compound_chat_problem, destructive = true)
|
||||
data object EndPoll : TimelineItemAction(CommonStrings.action_end_poll, CompoundDrawables.ic_compound_polls_end)
|
||||
data object Pin : TimelineItemAction(CommonStrings.action_pin, CompoundDrawables.ic_compound_pin)
|
||||
|
||||
// TODO use the Unpin compound icon when available.
|
||||
data object Unpin : TimelineItemAction(CommonStrings.action_unpin, CompoundDrawables.ic_compound_pin)
|
||||
data object Unpin : TimelineItemAction(CommonStrings.action_unpin, CompoundDrawables.ic_compound_unpin)
|
||||
}
|
||||
|
||||
@@ -15,5 +15,5 @@ import kotlinx.parcelize.Parcelize
|
||||
@Immutable
|
||||
sealed interface Attachment : Parcelable {
|
||||
@Parcelize
|
||||
data class Media(val localMedia: LocalMedia, val compressIfPossible: Boolean) : Attachment
|
||||
data class Media(val localMedia: LocalMedia) : Attachment
|
||||
}
|
||||
|
||||
@@ -96,7 +96,6 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
||||
mediaSender.sendMedia(
|
||||
uri = mediaAttachment.localMedia.uri,
|
||||
mimeType = mediaAttachment.localMedia.info.mimeType,
|
||||
compressIfPossible = mediaAttachment.compressIfPossible,
|
||||
progressCallback = progressCallback
|
||||
).getOrThrow()
|
||||
}.fold(
|
||||
|
||||
@@ -31,7 +31,6 @@ fun anAttachmentsPreviewState(
|
||||
) = AttachmentsPreviewState(
|
||||
attachment = Attachment.Media(
|
||||
localMedia = LocalMedia("file://path".toUri(), mediaInfo),
|
||||
compressIfPossible = true
|
||||
),
|
||||
sendActionState = sendActionState,
|
||||
eventSink = {}
|
||||
|
||||
@@ -24,11 +24,10 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
@@ -80,9 +79,7 @@ fun ResolveVerifiedUserSendFailureView(
|
||||
modifier = Modifier.padding(24.dp),
|
||||
title = state.verifiedUserSendFailure.title(),
|
||||
subTitle = state.verifiedUserSendFailure.subtitle(),
|
||||
iconImageVector = CompoundIcons.Error(),
|
||||
iconTint = ElementTheme.colors.iconCriticalPrimary,
|
||||
iconBackgroundTint = ElementTheme.colors.bgCriticalSubtle,
|
||||
iconStyle = BigIcon.Style.AlertSolid,
|
||||
)
|
||||
ButtonColumnMolecule(
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 16.dp),
|
||||
|
||||
@@ -169,7 +169,7 @@ class MessageComposerPresenter @Inject constructor(
|
||||
handlePickedMedia(attachmentsState, uri, mimeType)
|
||||
}
|
||||
val filesPicker = mediaPickerProvider.registerFilePicker(AnyMimeTypes) { uri ->
|
||||
handlePickedMedia(attachmentsState, uri, compressIfPossible = false)
|
||||
handlePickedMedia(attachmentsState, uri)
|
||||
}
|
||||
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker { uri ->
|
||||
handlePickedMedia(attachmentsState, uri, MimeTypes.IMAGE_JPEG)
|
||||
@@ -294,7 +294,6 @@ class MessageComposerPresenter @Inject constructor(
|
||||
name = null,
|
||||
formattedFileSize = null
|
||||
),
|
||||
compressIfPossible = true
|
||||
),
|
||||
attachmentState = attachmentsState,
|
||||
)
|
||||
@@ -493,7 +492,6 @@ class MessageComposerPresenter @Inject constructor(
|
||||
attachmentsState: MutableState<AttachmentsState>,
|
||||
uri: Uri?,
|
||||
mimeType: String? = null,
|
||||
compressIfPossible: Boolean = true,
|
||||
) {
|
||||
if (uri == null) {
|
||||
attachmentsState.value = AttachmentsState.None
|
||||
@@ -505,7 +503,7 @@ class MessageComposerPresenter @Inject constructor(
|
||||
name = null,
|
||||
formattedFileSize = null
|
||||
)
|
||||
val mediaAttachment = Attachment.Media(localMedia, compressIfPossible)
|
||||
val mediaAttachment = Attachment.Media(localMedia)
|
||||
val isPreviewable = when {
|
||||
MimeTypes.isImage(localMedia.info.mimeType) -> true
|
||||
MimeTypes.isVideo(localMedia.info.mimeType) -> true
|
||||
@@ -535,7 +533,6 @@ class MessageComposerPresenter @Inject constructor(
|
||||
mediaSender.sendMedia(
|
||||
uri = uri,
|
||||
mimeType = mimeType,
|
||||
compressIfPossible = false,
|
||||
progressCallback = progressCallback
|
||||
).getOrThrow()
|
||||
}
|
||||
|
||||
@@ -19,7 +19,11 @@ internal class PinnedMessagesBannerStateProvider : PreviewParameterProvider<Pinn
|
||||
aLoadingPinnedMessagesBannerState(knownPinnedMessagesCount = 1),
|
||||
aLoadingPinnedMessagesBannerState(knownPinnedMessagesCount = 5),
|
||||
aLoadedPinnedMessagesBannerState(knownPinnedMessagesCount = 1, currentPinnedMessageIndex = 0),
|
||||
aLoadedPinnedMessagesBannerState(knownPinnedMessagesCount = 2, currentPinnedMessageIndex = 0),
|
||||
aLoadedPinnedMessagesBannerState(
|
||||
knownPinnedMessagesCount = 2,
|
||||
currentPinnedMessageIndex = 0,
|
||||
message = "This is a pinned long message to check the wrapping behavior",
|
||||
),
|
||||
aLoadedPinnedMessagesBannerState(knownPinnedMessagesCount = 3, currentPinnedMessageIndex = 0),
|
||||
aLoadedPinnedMessagesBannerState(knownPinnedMessagesCount = 5, currentPinnedMessageIndex = 0),
|
||||
aLoadedPinnedMessagesBannerState(knownPinnedMessagesCount = 5, currentPinnedMessageIndex = 1),
|
||||
@@ -40,9 +44,10 @@ internal fun aLoadingPinnedMessagesBannerState(
|
||||
internal fun aLoadedPinnedMessagesBannerState(
|
||||
currentPinnedMessageIndex: Int = 0,
|
||||
knownPinnedMessagesCount: Int = 1,
|
||||
message: String = "This is a pinned message",
|
||||
currentPinnedMessage: PinnedMessagesBannerItem = PinnedMessagesBannerItem(
|
||||
eventId = EventId("\$" + Random.nextInt().toString()),
|
||||
formatted = AnnotatedString("This is a pinned message")
|
||||
formatted = AnnotatedString(message)
|
||||
),
|
||||
eventSink: (PinnedMessagesBannerEvents) -> Unit = {}
|
||||
) = PinnedMessagesBannerState.Loaded(
|
||||
|
||||
@@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -98,9 +99,8 @@ private fun PinnedMessagesBannerRow(
|
||||
}
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = spacedBy(10.dp)
|
||||
) {
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Spacer(modifier = Modifier.width(26.dp))
|
||||
PinIndicators(
|
||||
pinIndex = state.currentPinnedMessageIndex(),
|
||||
pinsCount = state.pinnedMessagesCount(),
|
||||
@@ -109,7 +109,9 @@ private fun PinnedMessagesBannerRow(
|
||||
imageVector = CompoundIcons.PinSolid(),
|
||||
contentDescription = null,
|
||||
tint = ElementTheme.materialColors.secondary,
|
||||
modifier = Modifier.size(20.dp)
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 10.dp)
|
||||
.size(20.dp)
|
||||
)
|
||||
PinnedMessageItem(
|
||||
index = state.currentPinnedMessageIndex(),
|
||||
|
||||
@@ -24,6 +24,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import im.vector.app.features.analytics.plan.Interaction
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListEvents
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListView
|
||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||
@@ -36,9 +37,9 @@ import io.element.android.features.messages.impl.timeline.protection.TimelinePro
|
||||
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
|
||||
import io.element.android.features.poll.api.pollcontent.PollTitleView
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||
import io.element.android.libraries.designsystem.icons.CompoundDrawables
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||
@@ -154,7 +155,7 @@ private fun PinnedMessagesListEmpty(
|
||||
IconTitleSubtitleMolecule(
|
||||
title = stringResource(id = CommonStrings.screen_pinned_timeline_empty_state_headline),
|
||||
subTitle = stringResource(id = CommonStrings.screen_pinned_timeline_empty_state_description, pinActionText),
|
||||
iconResourceId = CompoundDrawables.ic_compound_pin,
|
||||
iconStyle = BigIcon.Style.Default(CompoundIcons.Pin()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.impl.timeline.components
|
||||
|
||||
enum class ContentPadding {
|
||||
Textual,
|
||||
Media,
|
||||
CaptionedMedia
|
||||
}
|
||||
@@ -522,32 +522,33 @@ private fun MessageEventBubbleContent(
|
||||
fun CommonLayout(
|
||||
timestampPosition: TimestampPosition,
|
||||
showThreadDecoration: Boolean,
|
||||
paddingBehaviour: ContentPadding,
|
||||
inReplyToDetails: InReplyToDetails?,
|
||||
modifier: Modifier = Modifier,
|
||||
canShrinkContent: Boolean = false,
|
||||
) {
|
||||
val timestampLayoutModifier: Modifier
|
||||
val contentModifier: Modifier
|
||||
when {
|
||||
inReplyToDetails != null -> {
|
||||
if (timestampPosition == TimestampPosition.Overlay) {
|
||||
timestampLayoutModifier = Modifier.padding(start = 8.dp, end = 8.dp, bottom = 8.dp)
|
||||
contentModifier = Modifier.clip(RoundedCornerShape(12.dp))
|
||||
val timestampLayoutModifier =
|
||||
if (inReplyToDetails != null && timestampPosition == TimestampPosition.Overlay) {
|
||||
Modifier.padding(start = 8.dp, end = 8.dp, bottom = 8.dp)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
val topPadding = if (inReplyToDetails != null) 0.dp else 8.dp
|
||||
val contentModifier = when (paddingBehaviour) {
|
||||
ContentPadding.Textual ->
|
||||
Modifier.padding(start = 12.dp, end = 12.dp, top = topPadding, bottom = 8.dp)
|
||||
ContentPadding.Media -> {
|
||||
if (inReplyToDetails == null) {
|
||||
Modifier
|
||||
} else {
|
||||
contentModifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 0.dp, bottom = 8.dp)
|
||||
timestampLayoutModifier = Modifier
|
||||
Modifier.clip(RoundedCornerShape(10.dp))
|
||||
}
|
||||
}
|
||||
timestampPosition != TimestampPosition.Overlay -> {
|
||||
timestampLayoutModifier = Modifier
|
||||
contentModifier = Modifier
|
||||
.padding(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 8.dp)
|
||||
}
|
||||
else -> {
|
||||
timestampLayoutModifier = Modifier
|
||||
contentModifier = Modifier
|
||||
}
|
||||
ContentPadding.CaptionedMedia ->
|
||||
Modifier.padding(start = 8.dp, end = 8.dp, top = topPadding, bottom = 8.dp)
|
||||
}
|
||||
|
||||
val threadDecoration = @Composable {
|
||||
if (showThreadDecoration) {
|
||||
ThreadDecoration(modifier = Modifier.padding(top = 8.dp, start = 12.dp, end = 12.dp))
|
||||
@@ -601,9 +602,17 @@ private fun MessageEventBubbleContent(
|
||||
is TimelineItemPollContent -> TimestampPosition.Below
|
||||
else -> TimestampPosition.Default
|
||||
}
|
||||
val paddingBehaviour = when (event.content) {
|
||||
is TimelineItemImageContent -> if (event.content.showCaption) ContentPadding.CaptionedMedia else ContentPadding.Media
|
||||
is TimelineItemVideoContent -> if (event.content.showCaption) ContentPadding.CaptionedMedia else ContentPadding.Media
|
||||
is TimelineItemStickerContent,
|
||||
is TimelineItemLocationContent -> ContentPadding.Media
|
||||
else -> ContentPadding.Textual
|
||||
}
|
||||
CommonLayout(
|
||||
showThreadDecoration = event.isThreaded,
|
||||
timestampPosition = timestampPosition,
|
||||
paddingBehaviour = paddingBehaviour,
|
||||
inReplyToDetails = event.inReplyTo,
|
||||
canShrinkContent = event.content is TimelineItemVoiceContent,
|
||||
modifier = bubbleModifier.semantics(mergeDescendants = true) {
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.impl.timeline.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.Composable
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.UtdCause
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun TimelineItemEventRowUtdPreview() = ElementPreview {
|
||||
Column {
|
||||
ATimelineItemEventRow(
|
||||
event = aTimelineItemEvent(
|
||||
senderDisplayName = "Alice",
|
||||
isMine = false,
|
||||
content = TimelineItemEncryptedContent(
|
||||
data = UnableToDecryptContent.Data.MegolmV1AesSha2(
|
||||
sessionId = "sessionId",
|
||||
utdCause = UtdCause.UnsignedDevice,
|
||||
)
|
||||
),
|
||||
timelineItemReactions = aTimelineItemReactions(count = 0),
|
||||
groupPosition = TimelineItemGroupPosition.First,
|
||||
),
|
||||
)
|
||||
ATimelineItemEventRow(
|
||||
event = aTimelineItemEvent(
|
||||
senderDisplayName = "Bob",
|
||||
isMine = false,
|
||||
content = TimelineItemEncryptedContent(
|
||||
data = UnableToDecryptContent.Data.MegolmV1AesSha2(
|
||||
sessionId = "sessionId",
|
||||
utdCause = UtdCause.VerificationViolation,
|
||||
)
|
||||
),
|
||||
groupPosition = TimelineItemGroupPosition.First,
|
||||
timelineItemReactions = aTimelineItemReactions(count = 0)
|
||||
),
|
||||
)
|
||||
|
||||
ATimelineItemEventRow(
|
||||
event = aTimelineItemEvent(
|
||||
senderDisplayName = "Bob",
|
||||
isMine = false,
|
||||
content = TimelineItemEncryptedContent(
|
||||
data = UnableToDecryptContent.Data.MegolmV1AesSha2(
|
||||
sessionId = "sessionId",
|
||||
utdCause = UtdCause.SentBeforeWeJoined,
|
||||
)
|
||||
),
|
||||
groupPosition = TimelineItemGroupPosition.Last,
|
||||
timelineItemReactions = aTimelineItemReactions(count = 0)
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,9 @@ internal fun TimelineItemEventRowWithReplyContentToPreview(
|
||||
isMine = it,
|
||||
timelineItemReactions = aTimelineItemReactions(count = 0),
|
||||
content = aTimelineItemImageContent(
|
||||
aspectRatio = 2.5f
|
||||
aspectRatio = 2.5f,
|
||||
filename = "image.jpg",
|
||||
caption = "A reply with an image.",
|
||||
),
|
||||
inReplyTo = inReplyToDetails,
|
||||
displayNameAmbiguous = displayNameAmbiguous,
|
||||
|
||||
@@ -27,11 +27,28 @@ fun TimelineItemEncryptedView(
|
||||
onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val isMembershipUtd = (content.data as? UnableToDecryptContent.Data.MegolmV1AesSha2)?.utdCause == UtdCause.Membership
|
||||
val (textId, iconId) = if (isMembershipUtd) {
|
||||
CommonStrings.common_unable_to_decrypt_no_access to CompoundDrawables.ic_compound_block
|
||||
} else {
|
||||
CommonStrings.common_waiting_for_decryption_key to CompoundDrawables.ic_compound_time
|
||||
val (textId, iconId) = when (content.data) {
|
||||
is UnableToDecryptContent.Data.MegolmV1AesSha2 -> {
|
||||
when (content.data.utdCause) {
|
||||
UtdCause.SentBeforeWeJoined -> {
|
||||
CommonStrings.common_unable_to_decrypt_no_access to CompoundDrawables.ic_compound_block
|
||||
}
|
||||
UtdCause.VerificationViolation -> {
|
||||
CommonStrings.common_unable_to_decrypt_verification_violation to CompoundDrawables.ic_compound_block
|
||||
}
|
||||
UtdCause.UnsignedDevice,
|
||||
UtdCause.UnknownDevice -> {
|
||||
CommonStrings.common_unable_to_decrypt_insecure_device to CompoundDrawables.ic_compound_block
|
||||
}
|
||||
else -> {
|
||||
CommonStrings.common_waiting_for_decryption_key to CompoundDrawables.ic_compound_time
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// Should not happen, we only supports megolm in rooms
|
||||
CommonStrings.common_waiting_for_decryption_key to CompoundDrawables.ic_compound_time
|
||||
}
|
||||
}
|
||||
TimelineItemInformativeView(
|
||||
text = stringResource(id = textId),
|
||||
|
||||
@@ -69,9 +69,7 @@ fun TimelineItemImageView(
|
||||
modifier = modifier.semantics { contentDescription = description },
|
||||
) {
|
||||
val containerModifier = if (content.showCaption) {
|
||||
Modifier
|
||||
.padding(top = 6.dp)
|
||||
.clip(RoundedCornerShape(6.dp))
|
||||
Modifier.clip(RoundedCornerShape(10.dp))
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
@@ -119,6 +117,7 @@ fun TimelineItemImageView(
|
||||
val aspectRatio = content.aspectRatio ?: DEFAULT_ASPECT_RATIO
|
||||
EditorStyledText(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 4.dp) // This is (12.dp - 8.dp) contentPadding from CommonLayout
|
||||
.widthIn(min = MIN_HEIGHT_IN_DP.dp * aspectRatio, max = MAX_HEIGHT_IN_DP.dp * aspectRatio),
|
||||
text = caption,
|
||||
style = ElementRichTextEditorStyle.textStyle(),
|
||||
|
||||
@@ -137,6 +137,7 @@ fun TimelineItemVideoView(
|
||||
val aspectRatio = content.aspectRatio ?: DEFAULT_ASPECT_RATIO
|
||||
EditorStyledText(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 4.dp) // This is (12.dp - 8.dp) contentPadding from CommonLayout
|
||||
.widthIn(min = MIN_HEIGHT_IN_DP.dp * aspectRatio, max = MAX_HEIGHT_IN_DP.dp * aspectRatio),
|
||||
text = caption,
|
||||
style = ElementRichTextEditorStyle.textStyle(),
|
||||
|
||||
@@ -12,6 +12,7 @@ import io.element.android.libraries.matrix.api.core.UserId
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
import java.util.TimeZone
|
||||
|
||||
open class AggregatedReactionProvider : PreviewParameterProvider<AggregatedReaction> {
|
||||
override val values: Sequence<AggregatedReaction>
|
||||
@@ -29,7 +30,9 @@ fun anAggregatedReaction(
|
||||
count: Int = 1,
|
||||
isHighlighted: Boolean = false,
|
||||
): AggregatedReaction {
|
||||
val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT, java.util.Locale.US)
|
||||
val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT, java.util.Locale.US).apply {
|
||||
timeZone = TimeZone.getTimeZone("UTC")
|
||||
}
|
||||
val date = Date(1_689_061_264L)
|
||||
val senders = buildList {
|
||||
repeat(count) { index ->
|
||||
|
||||
@@ -18,7 +18,19 @@ open class TimelineItemEncryptedContentProvider : PreviewParameterProvider<Timel
|
||||
aTimelineItemEncryptedContent(
|
||||
data = UnableToDecryptContent.Data.MegolmV1AesSha2(
|
||||
sessionId = "sessionId",
|
||||
utdCause = UtdCause.Membership,
|
||||
utdCause = UtdCause.SentBeforeWeJoined,
|
||||
)
|
||||
),
|
||||
aTimelineItemEncryptedContent(
|
||||
data = UnableToDecryptContent.Data.MegolmV1AesSha2(
|
||||
sessionId = "sessionId",
|
||||
utdCause = UtdCause.VerificationViolation,
|
||||
)
|
||||
),
|
||||
aTimelineItemEncryptedContent(
|
||||
data = UnableToDecryptContent.Data.MegolmV1AesSha2(
|
||||
sessionId = "sessionId",
|
||||
utdCause = UtdCause.UnsignedDevice,
|
||||
)
|
||||
),
|
||||
aTimelineItemEncryptedContent(
|
||||
|
||||
@@ -17,6 +17,7 @@ import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewEvents
|
||||
import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewPresenter
|
||||
import io.element.android.features.messages.impl.attachments.preview.SendActionState
|
||||
import io.element.android.features.messages.impl.fixtures.aMediaAttachment
|
||||
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
|
||||
@@ -26,6 +27,7 @@ import io.element.android.libraries.mediaupload.api.MediaSender
|
||||
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
||||
import io.element.android.libraries.mediaviewer.test.viewer.aLocalMedia
|
||||
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.mockk.mockk
|
||||
@@ -120,8 +122,8 @@ class AttachmentsPreviewPresenterTest {
|
||||
room: MatrixRoom = FakeMatrixRoom()
|
||||
): AttachmentsPreviewPresenter {
|
||||
return AttachmentsPreviewPresenter(
|
||||
attachment = Attachment.Media(localMedia, compressIfPossible = false),
|
||||
mediaSender = MediaSender(mediaPreProcessor, room)
|
||||
attachment = aMediaAttachment(localMedia),
|
||||
mediaSender = MediaSender(mediaPreProcessor, room, InMemorySessionPreferencesStore())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ package io.element.android.features.messages.impl.fixtures
|
||||
import io.element.android.features.messages.impl.attachments.Attachment
|
||||
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
||||
|
||||
fun aMediaAttachment(localMedia: LocalMedia, compressIfPossible: Boolean = true) = Attachment.Media(
|
||||
fun aMediaAttachment(localMedia: LocalMedia) = Attachment.Media(
|
||||
localMedia = localMedia,
|
||||
compressIfPossible = compressIfPossible,
|
||||
)
|
||||
|
||||
@@ -1489,7 +1489,7 @@ class MessageComposerPresenterTest {
|
||||
featureFlagService,
|
||||
sessionPreferencesStore,
|
||||
localMediaFactory,
|
||||
MediaSender(mediaPreProcessor, room),
|
||||
MediaSender(mediaPreProcessor, room, InMemorySessionPreferencesStore()),
|
||||
snackbarDispatcher,
|
||||
analyticsService,
|
||||
DefaultMessageComposerContext(),
|
||||
|
||||
@@ -30,6 +30,7 @@ import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||
import io.element.android.libraries.permissions.api.aPermissionsState
|
||||
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
|
||||
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
|
||||
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
|
||||
import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent
|
||||
@@ -61,7 +62,7 @@ class VoiceMessageComposerPresenterTest {
|
||||
sendMediaResult = sendMediaResult
|
||||
)
|
||||
private val mediaPreProcessor = FakeMediaPreProcessor().apply { givenAudioResult() }
|
||||
private val mediaSender = MediaSender(mediaPreProcessor, matrixRoom)
|
||||
private val mediaSender = MediaSender(mediaPreProcessor, matrixRoom, InMemorySessionPreferencesStore())
|
||||
private val messageComposerContext = FakeMessageComposerContext()
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -55,6 +55,7 @@ dependencies {
|
||||
implementation(projects.features.deactivation.api)
|
||||
implementation(projects.features.roomlist.api)
|
||||
implementation(projects.services.analytics.api)
|
||||
implementation(projects.services.analytics.compose)
|
||||
implementation(projects.services.toolbox.api)
|
||||
implementation(libs.datetime)
|
||||
implementation(libs.coil.compose)
|
||||
|
||||
@@ -12,6 +12,7 @@ import io.element.android.compound.theme.Theme
|
||||
sealed interface AdvancedSettingsEvents {
|
||||
data class SetDeveloperModeEnabled(val enabled: Boolean) : AdvancedSettingsEvents
|
||||
data class SetSharePresenceEnabled(val enabled: Boolean) : AdvancedSettingsEvents
|
||||
data class SetCompressMedia(val compress: Boolean) : AdvancedSettingsEvents
|
||||
data object ChangeTheme : AdvancedSettingsEvents
|
||||
data object CancelChangeTheme : AdvancedSettingsEvents
|
||||
data class SetTheme(val theme: Theme) : AdvancedSettingsEvents
|
||||
|
||||
@@ -35,6 +35,9 @@ class AdvancedSettingsPresenter @Inject constructor(
|
||||
val isSharePresenceEnabled by sessionPreferencesStore
|
||||
.isSharePresenceEnabled()
|
||||
.collectAsState(initial = true)
|
||||
val doesCompressMedia by sessionPreferencesStore
|
||||
.doesCompressMedia()
|
||||
.collectAsState(initial = false)
|
||||
val theme by remember {
|
||||
appPreferencesStore.getThemeFlow().mapToTheme()
|
||||
}
|
||||
@@ -49,6 +52,9 @@ class AdvancedSettingsPresenter @Inject constructor(
|
||||
is AdvancedSettingsEvents.SetSharePresenceEnabled -> localCoroutineScope.launch {
|
||||
sessionPreferencesStore.setSharePresence(event.enabled)
|
||||
}
|
||||
is AdvancedSettingsEvents.SetCompressMedia -> localCoroutineScope.launch {
|
||||
sessionPreferencesStore.setCompressMedia(event.compress)
|
||||
}
|
||||
AdvancedSettingsEvents.CancelChangeTheme -> showChangeThemeDialog = false
|
||||
AdvancedSettingsEvents.ChangeTheme -> showChangeThemeDialog = true
|
||||
is AdvancedSettingsEvents.SetTheme -> localCoroutineScope.launch {
|
||||
@@ -61,6 +67,7 @@ class AdvancedSettingsPresenter @Inject constructor(
|
||||
return AdvancedSettingsState(
|
||||
isDeveloperModeEnabled = isDeveloperModeEnabled,
|
||||
isSharePresenceEnabled = isSharePresenceEnabled,
|
||||
doesCompressMedia = doesCompressMedia,
|
||||
theme = theme,
|
||||
showChangeThemeDialog = showChangeThemeDialog,
|
||||
eventSink = { handleEvents(it) }
|
||||
|
||||
@@ -12,6 +12,7 @@ import io.element.android.compound.theme.Theme
|
||||
data class AdvancedSettingsState(
|
||||
val isDeveloperModeEnabled: Boolean,
|
||||
val isSharePresenceEnabled: Boolean,
|
||||
val doesCompressMedia: Boolean,
|
||||
val theme: Theme,
|
||||
val showChangeThemeDialog: Boolean,
|
||||
val eventSink: (AdvancedSettingsEvents) -> Unit
|
||||
|
||||
@@ -16,18 +16,21 @@ open class AdvancedSettingsStateProvider : PreviewParameterProvider<AdvancedSett
|
||||
aAdvancedSettingsState(),
|
||||
aAdvancedSettingsState(isDeveloperModeEnabled = true),
|
||||
aAdvancedSettingsState(showChangeThemeDialog = true),
|
||||
aAdvancedSettingsState(isSendPublicReadReceiptsEnabled = true),
|
||||
aAdvancedSettingsState(isSharePresenceEnabled = true),
|
||||
aAdvancedSettingsState(doesCompressMedia = true),
|
||||
)
|
||||
}
|
||||
|
||||
fun aAdvancedSettingsState(
|
||||
isDeveloperModeEnabled: Boolean = false,
|
||||
isSendPublicReadReceiptsEnabled: Boolean = false,
|
||||
isSharePresenceEnabled: Boolean = false,
|
||||
doesCompressMedia: Boolean = false,
|
||||
showChangeThemeDialog: Boolean = false,
|
||||
eventSink: (AdvancedSettingsEvents) -> Unit = {},
|
||||
) = AdvancedSettingsState(
|
||||
isDeveloperModeEnabled = isDeveloperModeEnabled,
|
||||
isSharePresenceEnabled = isSendPublicReadReceiptsEnabled,
|
||||
isSharePresenceEnabled = isSharePresenceEnabled,
|
||||
doesCompressMedia = doesCompressMedia,
|
||||
theme = Theme.System,
|
||||
showChangeThemeDialog = showChangeThemeDialog,
|
||||
eventSink = eventSink
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import im.vector.app.features.analytics.plan.Interaction
|
||||
import io.element.android.compound.theme.Theme
|
||||
import io.element.android.compound.theme.themes
|
||||
import io.element.android.features.preferences.impl.R
|
||||
@@ -23,6 +24,8 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.analytics.compose.LocalAnalyticsService
|
||||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@@ -32,6 +35,7 @@ fun AdvancedSettingsView(
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val analyticsService = LocalAnalyticsService.current
|
||||
PreferencePage(
|
||||
modifier = modifier,
|
||||
onBackClick = onBackClick,
|
||||
@@ -72,6 +76,28 @@ fun AdvancedSettingsView(
|
||||
),
|
||||
onClick = { state.eventSink(AdvancedSettingsEvents.SetSharePresenceEnabled(!state.isSharePresenceEnabled)) }
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(text = stringResource(id = R.string.screen_advanced_settings_media_compression_title))
|
||||
},
|
||||
supportingContent = {
|
||||
Text(text = stringResource(id = R.string.screen_advanced_settings_media_compression_description))
|
||||
},
|
||||
trailingContent = ListItemContent.Switch(
|
||||
checked = state.doesCompressMedia,
|
||||
),
|
||||
onClick = {
|
||||
val newValue = !state.doesCompressMedia
|
||||
analyticsService.captureInteraction(
|
||||
if (newValue) {
|
||||
Interaction.Name.MobileSettingsOptimizeMediaUploadsEnabled
|
||||
} else {
|
||||
Interaction.Name.MobileSettingsOptimizeMediaUploadsDisabled
|
||||
}
|
||||
)
|
||||
state.eventSink(AdvancedSettingsEvents.SetCompressMedia(newValue))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (state.showChangeThemeDialog) {
|
||||
|
||||
@@ -138,8 +138,8 @@ private fun ColumnScope.ManageAppSection(
|
||||
}
|
||||
if (state.showSecureBackup) {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(id = CommonStrings.common_chat_backup)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.KeySolid())),
|
||||
headlineContent = { Text(stringResource(id = CommonStrings.common_encryption)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Key())),
|
||||
trailingContent = ListItemContent.Badge.takeIf { state.showSecureBackupBadge },
|
||||
onClick = onSecureBackupClick,
|
||||
)
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
<string name="screen_advanced_settings_element_call_base_url">"Element Calli kohandatud teenuseaadress"</string>
|
||||
<string name="screen_advanced_settings_element_call_base_url_description">"Seadista kohandatud teenuseaadress Element Calli jaoks."</string>
|
||||
<string name="screen_advanced_settings_element_call_base_url_validation_error">"Vigane url. Palun vaata, et url algaks protokolliga (http/https) ning aadress ise oleks ka õige."</string>
|
||||
<string name="screen_advanced_settings_media_compression_description">"Optimeeri üleslaadimiseks"</string>
|
||||
<string name="screen_advanced_settings_media_compression_title">"Meedia"</string>
|
||||
<string name="screen_advanced_settings_push_provider_android">"Tõuketeavituste pakkuja"</string>
|
||||
<string name="screen_advanced_settings_rich_text_editor_description">"Kui soovid Markdown-vormingut käsitsi lisada, siis lülita vormindatud teksti toimeti välja."</string>
|
||||
<string name="screen_advanced_settings_send_read_receipts">"Lugemisteatised"</string>
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
<string name="screen_advanced_settings_element_call_base_url">"Базовый URL сервера звонков Element"</string>
|
||||
<string name="screen_advanced_settings_element_call_base_url_description">"Задайте свой сервер Element Call."</string>
|
||||
<string name="screen_advanced_settings_element_call_base_url_validation_error">"Адрес указан неверно, удостоверьтесь, что вы указали протокол (http/https) и правильный адрес."</string>
|
||||
<string name="screen_advanced_settings_media_compression_description">"Оптимизировать для загрузки"</string>
|
||||
<string name="screen_advanced_settings_media_compression_title">"Медиа"</string>
|
||||
<string name="screen_advanced_settings_push_provider_android">"Поставщик push-уведомлений"</string>
|
||||
<string name="screen_advanced_settings_rich_text_editor_description">"Отключить редактор форматированного текста и включить Markdown."</string>
|
||||
<string name="screen_advanced_settings_send_read_receipts">"Уведомления о прочтении"</string>
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
<string name="screen_advanced_settings_element_call_base_url">"Custom Element Call base URL"</string>
|
||||
<string name="screen_advanced_settings_element_call_base_url_description">"Set a custom base URL for Element Call."</string>
|
||||
<string name="screen_advanced_settings_element_call_base_url_validation_error">"Invalid URL, please make sure you include the protocol (http/https) and the correct address."</string>
|
||||
<string name="screen_advanced_settings_media_compression_description">"Optimize for upload"</string>
|
||||
<string name="screen_advanced_settings_media_compression_title">"Media"</string>
|
||||
<string name="screen_advanced_settings_push_provider_android">"Push notification provider"</string>
|
||||
<string name="screen_advanced_settings_rich_text_editor_description">"Disable the rich text editor to type Markdown manually."</string>
|
||||
<string name="screen_advanced_settings_send_read_receipts">"Read receipts"</string>
|
||||
|
||||
@@ -34,6 +34,7 @@ class AdvancedSettingsPresenterTest {
|
||||
assertThat(initialState.isDeveloperModeEnabled).isFalse()
|
||||
assertThat(initialState.showChangeThemeDialog).isFalse()
|
||||
assertThat(initialState.isSharePresenceEnabled).isTrue()
|
||||
assertThat(initialState.doesCompressMedia).isFalse()
|
||||
assertThat(initialState.theme).isEqualTo(Theme.System)
|
||||
}
|
||||
}
|
||||
@@ -68,6 +69,21 @@ class AdvancedSettingsPresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - compress media off on`() = runTest {
|
||||
val presenter = createAdvancedSettingsPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitLastSequentialItem()
|
||||
assertThat(initialState.doesCompressMedia).isFalse()
|
||||
initialState.eventSink.invoke(AdvancedSettingsEvents.SetCompressMedia(true))
|
||||
assertThat(awaitItem().doesCompressMedia).isTrue()
|
||||
initialState.eventSink.invoke(AdvancedSettingsEvents.SetCompressMedia(false))
|
||||
assertThat(awaitItem().doesCompressMedia).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - change theme`() = runTest {
|
||||
val presenter = createAdvancedSettingsPresenter()
|
||||
|
||||
@@ -8,12 +8,18 @@
|
||||
package io.element.android.features.preferences.impl.advanced
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.Interaction
|
||||
import io.element.android.compound.theme.Theme
|
||||
import io.element.android.features.preferences.impl.R
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analytics.compose.LocalAnalyticsService
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||
import io.element.android.tests.testutils.EventsRecorder
|
||||
import io.element.android.tests.testutils.clickOn
|
||||
@@ -91,16 +97,64 @@ class AdvancedSettingsViewTest {
|
||||
rule.clickOn(R.string.screen_advanced_settings_share_presence)
|
||||
eventsRecorder.assertSingle(AdvancedSettingsEvents.SetSharePresenceEnabled(true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on media to enable compression emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<AdvancedSettingsEvents>()
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
rule.setAdvancedSettingsView(
|
||||
state = aAdvancedSettingsState(
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
analyticsService = analyticsService
|
||||
)
|
||||
rule.clickOn(R.string.screen_advanced_settings_media_compression_description)
|
||||
eventsRecorder.assertSingle(AdvancedSettingsEvents.SetCompressMedia(true))
|
||||
assertThat(analyticsService.capturedEvents).isEqualTo(
|
||||
listOf(
|
||||
Interaction(
|
||||
name = Interaction.Name.MobileSettingsOptimizeMediaUploadsEnabled
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on media to disable compression emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<AdvancedSettingsEvents>()
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
rule.setAdvancedSettingsView(
|
||||
state = aAdvancedSettingsState(
|
||||
doesCompressMedia = true,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
analyticsService = analyticsService
|
||||
)
|
||||
rule.clickOn(R.string.screen_advanced_settings_media_compression_description)
|
||||
eventsRecorder.assertSingle(AdvancedSettingsEvents.SetCompressMedia(false))
|
||||
assertThat(analyticsService.capturedEvents).isEqualTo(
|
||||
listOf(
|
||||
Interaction(
|
||||
name = Interaction.Name.MobileSettingsOptimizeMediaUploadsDisabled
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setAdvancedSettingsView(
|
||||
state: AdvancedSettingsState,
|
||||
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
setContent {
|
||||
AdvancedSettingsView(
|
||||
state = state,
|
||||
onBackClick = onBackClick,
|
||||
)
|
||||
CompositionLocalProvider(
|
||||
LocalAnalyticsService provides analyticsService,
|
||||
) {
|
||||
AdvancedSettingsView(
|
||||
state = state,
|
||||
onBackClick = onBackClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,9 @@ import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.awaitLastSequentialItem
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@@ -154,6 +155,7 @@ class DeveloperSettingsPresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `present - toggling simplified sliding sync changes the preferences and logs out the user`() = runTest {
|
||||
val logoutCallRecorder = lambdaRecorder<Boolean, String?> { "" }
|
||||
@@ -169,15 +171,13 @@ class DeveloperSettingsPresenterTest {
|
||||
initialState.eventSink(DeveloperSettingsEvents.SetSimplifiedSlidingSyncEnabled(true))
|
||||
assertThat(awaitItem().isSimpleSlidingSyncEnabled).isTrue()
|
||||
assertThat(preferences.isSimplifiedSlidingSyncEnabledFlow().first()).isTrue()
|
||||
// Give time for the logout to be called, but for some reason using runCurrent() is not enough
|
||||
delay(2)
|
||||
advanceUntilIdle()
|
||||
logoutCallRecorder.assertions().isCalledOnce()
|
||||
|
||||
initialState.eventSink(DeveloperSettingsEvents.SetSimplifiedSlidingSyncEnabled(false))
|
||||
assertThat(awaitItem().isSimpleSlidingSyncEnabled).isFalse()
|
||||
assertThat(preferences.isSimplifiedSlidingSyncEnabledFlow().first()).isFalse()
|
||||
// Give time for the logout to be called, but for some reason using runCurrent() is not enough
|
||||
delay(2)
|
||||
advanceUntilIdle()
|
||||
logoutCallRecorder.assertions().isCalledExactly(times = 2)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
data class RoomDetailsState(
|
||||
val roomId: RoomId,
|
||||
@@ -40,7 +41,20 @@ data class RoomDetailsState(
|
||||
val canShowPinnedMessages: Boolean,
|
||||
val pinnedMessagesCount: Int?,
|
||||
val eventSink: (RoomDetailsEvent) -> Unit
|
||||
)
|
||||
) {
|
||||
val roomBadges = buildList {
|
||||
if (isEncrypted || isPublic) {
|
||||
if (isEncrypted) {
|
||||
add(RoomBadge.ENCRYPTED)
|
||||
} else {
|
||||
add(RoomBadge.NOT_ENCRYPTED)
|
||||
}
|
||||
}
|
||||
if (isPublic) {
|
||||
add(RoomBadge.PUBLIC)
|
||||
}
|
||||
}.toPersistentList()
|
||||
}
|
||||
|
||||
@Immutable
|
||||
sealed interface RoomDetailsType {
|
||||
@@ -57,3 +71,9 @@ sealed interface RoomTopicState {
|
||||
data object CanAddTopic : RoomTopicState
|
||||
data class ExistingTopic(val topic: String) : RoomTopicState
|
||||
}
|
||||
|
||||
enum class RoomBadge {
|
||||
ENCRYPTED,
|
||||
NOT_ENCRYPTED,
|
||||
PUBLIC,
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import io.element.android.features.leaveroom.api.aLeaveRoomState
|
||||
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
||||
import io.element.android.features.userprofile.api.UserProfileState
|
||||
import io.element.android.features.userprofile.shared.aUserProfileState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
@@ -33,7 +34,7 @@ open class RoomDetailsStateProvider : PreviewParameterProvider<RoomDetailsState>
|
||||
aRoomDetailsState(isEncrypted = false),
|
||||
aRoomDetailsState(roomAlias = null),
|
||||
aDmRoomDetailsState(),
|
||||
aDmRoomDetailsState(isDmMemberIgnored = true),
|
||||
aDmRoomDetailsState(isDmMemberIgnored = true, roomName = "Daniel (ignored and clear)", isEncrypted = false),
|
||||
aRoomDetailsState(canInvite = true),
|
||||
aRoomDetailsState(isFavorite = true),
|
||||
aRoomDetailsState(
|
||||
@@ -136,12 +137,16 @@ fun aRoomNotificationSettings(
|
||||
fun aDmRoomDetailsState(
|
||||
isDmMemberIgnored: Boolean = false,
|
||||
roomName: String = "Daniel",
|
||||
isEncrypted: Boolean = true,
|
||||
) = aRoomDetailsState(
|
||||
roomName = roomName,
|
||||
isPublic = false,
|
||||
isEncrypted = isEncrypted,
|
||||
roomType = RoomDetailsType.Dm(
|
||||
aRoomMember(),
|
||||
aDmRoomMember(isIgnored = isDmMemberIgnored),
|
||||
me = aRoomMember(),
|
||||
otherMember = aDmRoomMember(isIgnored = isDmMemberIgnored),
|
||||
),
|
||||
roomMemberDetailsState = aUserProfileState()
|
||||
roomMemberDetailsState = aUserProfileState(
|
||||
isBlocked = AsyncData.Success(isDmMemberIgnored),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ package io.element.android.features.roomdetails.impl
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -41,10 +42,11 @@ import im.vector.app.features.analytics.plan.Interaction
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomView
|
||||
import io.element.android.features.roomdetails.impl.components.RoomBadge
|
||||
import io.element.android.features.userprofile.shared.blockuser.BlockUserDialogs
|
||||
import io.element.android.features.userprofile.shared.blockuser.BlockUserSection
|
||||
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.MatrixBadgeAtom
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.MatrixBadgeRowMolecule
|
||||
import io.element.android.libraries.designsystem.components.ClickableLinkText
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
@@ -84,6 +86,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.analytics.compose.LocalAnalyticsService
|
||||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
@Composable
|
||||
@@ -114,9 +117,9 @@ fun RoomDetailsView(
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.consumeWindowInsets(padding)
|
||||
.padding(padding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.consumeWindowInsets(padding)
|
||||
) {
|
||||
LeaveRoomView(state = state.leaveRoomState)
|
||||
|
||||
@@ -145,8 +148,7 @@ fun RoomDetailsView(
|
||||
}
|
||||
}
|
||||
BadgeList(
|
||||
isEncrypted = state.isEncrypted,
|
||||
isPublic = state.isPublic,
|
||||
roomBadge = state.roomBadges,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
)
|
||||
Spacer(Modifier.height(32.dp))
|
||||
@@ -273,8 +275,8 @@ private fun MainActionsSection(
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
val roomNotificationSettings = state.roomNotificationSettings
|
||||
@@ -333,8 +335,8 @@ private fun RoomHeaderSection(
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
CompositeAvatar(
|
||||
@@ -343,8 +345,8 @@ private fun RoomHeaderSection(
|
||||
user.getAvatarData(size = AvatarSize.RoomHeader)
|
||||
}.toPersistentList(),
|
||||
modifier = Modifier
|
||||
.clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) }
|
||||
.testTag(TestTags.roomDetailAvatar)
|
||||
.clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) }
|
||||
.testTag(TestTags.roomDetailAvatar)
|
||||
)
|
||||
TitleAndSubtitle(title = roomName, subtitle = roomAlias?.value)
|
||||
}
|
||||
@@ -360,8 +362,8 @@ private fun DmHeaderSection(
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
DmAvatars(
|
||||
@@ -401,36 +403,43 @@ private fun ColumnScope.TitleAndSubtitle(
|
||||
|
||||
@Composable
|
||||
private fun BadgeList(
|
||||
isEncrypted: Boolean,
|
||||
isPublic: Boolean,
|
||||
roomBadge: ImmutableList<RoomBadge>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (isEncrypted || isPublic) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.padding(start = 16.dp, end = 16.dp, top = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
if (isEncrypted) {
|
||||
RoomBadge.View(
|
||||
text = stringResource(R.string.screen_room_details_badge_encrypted),
|
||||
icon = CompoundIcons.LockSolid(),
|
||||
type = RoomBadge.Type.Positive,
|
||||
)
|
||||
} else {
|
||||
RoomBadge.View(
|
||||
text = stringResource(R.string.screen_room_details_badge_not_encrypted),
|
||||
icon = CompoundIcons.LockOff(),
|
||||
type = RoomBadge.Type.Neutral,
|
||||
)
|
||||
}
|
||||
if (isPublic) {
|
||||
RoomBadge.View(
|
||||
text = stringResource(R.string.screen_room_details_badge_public),
|
||||
icon = CompoundIcons.Public(),
|
||||
type = RoomBadge.Type.Neutral,
|
||||
)
|
||||
}
|
||||
Box(modifier = modifier) {
|
||||
if (roomBadge.isNotEmpty()) {
|
||||
MatrixBadgeRowMolecule(
|
||||
data = roomBadge.map {
|
||||
it.toMatrixBadgeData()
|
||||
}.toImmutableList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RoomBadge.toMatrixBadgeData(): MatrixBadgeAtom.MatrixBadgeData {
|
||||
return when (this) {
|
||||
RoomBadge.ENCRYPTED -> {
|
||||
MatrixBadgeAtom.MatrixBadgeData(
|
||||
text = stringResource(R.string.screen_room_details_badge_encrypted),
|
||||
icon = CompoundIcons.LockSolid(),
|
||||
type = MatrixBadgeAtom.Type.Positive,
|
||||
)
|
||||
}
|
||||
RoomBadge.NOT_ENCRYPTED -> {
|
||||
MatrixBadgeAtom.MatrixBadgeData(
|
||||
text = stringResource(R.string.screen_room_details_badge_not_encrypted),
|
||||
icon = CompoundIcons.LockOff(),
|
||||
type = MatrixBadgeAtom.Type.Neutral,
|
||||
)
|
||||
}
|
||||
RoomBadge.PUBLIC -> {
|
||||
MatrixBadgeAtom.MatrixBadgeData(
|
||||
text = stringResource(R.string.screen_room_details_badge_public),
|
||||
icon = CompoundIcons.Public(),
|
||||
type = MatrixBadgeAtom.Type.Neutral,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
<string name="screen_room_notification_settings_mentions_only_disclaimer">"Ваш домашний сервер не поддерживает эту опцию в зашифрованных комнатах, вы не будете получать уведомления в этой комнате."</string>
|
||||
<string name="screen_room_notification_settings_mode_all_messages">"О всех сообщениях"</string>
|
||||
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Только упоминания и ключевые слова"</string>
|
||||
<string name="screen_room_notification_settings_room_custom_settings_title">"В этой комнате уведомить меня о"</string>
|
||||
<string name="screen_room_notification_settings_room_custom_settings_title">"В этой комнате уведомлять меня"</string>
|
||||
<string name="screen_room_roles_and_permissions_admins">"Администраторы"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_my_role">"Изменить мою роль"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_role_demote_to_member">"Понизить до участника"</string>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomdetails
|
||||
package io.element.android.features.roomdetails.impl
|
||||
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import app.cash.molecule.RecompositionMode
|
||||
@@ -17,11 +17,7 @@ import im.vector.app.features.analytics.plan.Interaction
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.features.leaveroom.api.aLeaveRoomState
|
||||
import io.element.android.features.roomdetails.impl.RoomDetailsEvent
|
||||
import io.element.android.features.roomdetails.impl.RoomDetailsPresenter
|
||||
import io.element.android.features.roomdetails.impl.RoomDetailsState
|
||||
import io.element.android.features.roomdetails.impl.RoomDetailsType
|
||||
import io.element.android.features.roomdetails.impl.RoomTopicState
|
||||
import io.element.android.features.roomdetails.aMatrixRoom
|
||||
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
||||
import io.element.android.features.userprofile.shared.aUserProfileState
|
||||
@@ -125,6 +121,7 @@ class RoomDetailsPresenterTest {
|
||||
)
|
||||
val presenter = createRoomDetailsPresenter(room)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.roomId).isEqualTo(room.roomId)
|
||||
assertThat(initialState.roomName).isEqualTo(room.displayName)
|
||||
@@ -134,7 +131,6 @@ class RoomDetailsPresenterTest {
|
||||
assertThat(initialState.isEncrypted).isEqualTo(room.isEncrypted)
|
||||
assertThat(initialState.canShowPinnedMessages).isTrue()
|
||||
assertThat(initialState.pinnedMessagesCount).isNull()
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,6 +138,7 @@ class RoomDetailsPresenterTest {
|
||||
fun `present - initial state is updated with roomInfo if it exists`() = runTest {
|
||||
val roomInfo = aRoomInfo(
|
||||
name = A_ROOM_NAME,
|
||||
isPublic = true,
|
||||
topic = A_ROOM_TOPIC,
|
||||
avatarUrl = AN_AVATAR_URL,
|
||||
pinnedEventIds = listOf(AN_EVENT_ID),
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomdetails.impl
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.junit.Test
|
||||
|
||||
class RoomDetailsStateTest {
|
||||
@Test
|
||||
fun `room not public not encrypted should have no badges`() {
|
||||
val sut = aRoomDetailsState(
|
||||
isPublic = false,
|
||||
isEncrypted = false,
|
||||
)
|
||||
assertThat(sut.roomBadges).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `room public not encrypted should have not encrypted and public badges`() {
|
||||
val sut = aRoomDetailsState(
|
||||
isPublic = true,
|
||||
isEncrypted = false,
|
||||
)
|
||||
assertThat(sut.roomBadges).isEqualTo(
|
||||
persistentListOf(RoomBadge.NOT_ENCRYPTED, RoomBadge.PUBLIC)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `room public encrypted should have encrypted and public badges`() {
|
||||
val sut = aRoomDetailsState(
|
||||
isPublic = true,
|
||||
isEncrypted = true,
|
||||
)
|
||||
assertThat(sut.roomBadges).isEqualTo(
|
||||
persistentListOf(RoomBadge.ENCRYPTED, RoomBadge.PUBLIC)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `room not public encrypted should have encrypted badges`() {
|
||||
val sut = aRoomDetailsState(
|
||||
isPublic = false,
|
||||
isEncrypted = true,
|
||||
)
|
||||
assertThat(sut.roomBadges).isEqualTo(
|
||||
persistentListOf(RoomBadge.ENCRYPTED)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
@@ -38,6 +39,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.roomlist.impl.R
|
||||
import io.element.android.features.roomlist.impl.RoomListEvents
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryProvider
|
||||
@@ -72,54 +74,86 @@ internal fun RoomSummaryRow(
|
||||
eventSink: (RoomListEvents) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
when (room.displayType) {
|
||||
RoomSummaryDisplayType.PLACEHOLDER -> {
|
||||
RoomSummaryPlaceholderRow(modifier = modifier)
|
||||
}
|
||||
RoomSummaryDisplayType.INVITE -> {
|
||||
RoomSummaryScaffoldRow(
|
||||
room = room,
|
||||
onClick = onClick,
|
||||
onLongClick = {
|
||||
Timber.d("Long click on invite room")
|
||||
},
|
||||
modifier = modifier
|
||||
) {
|
||||
InviteNameAndIndicatorRow(name = room.name)
|
||||
InviteSubtitle(isDm = room.isDm, inviteSender = room.inviteSender, canonicalAlias = room.canonicalAlias)
|
||||
if (!room.isDm && room.inviteSender != null) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
InviteSenderView(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
inviteSender = room.inviteSender,
|
||||
Box(modifier = modifier) {
|
||||
when (room.displayType) {
|
||||
RoomSummaryDisplayType.PLACEHOLDER -> {
|
||||
RoomSummaryPlaceholderRow()
|
||||
}
|
||||
RoomSummaryDisplayType.INVITE -> {
|
||||
RoomSummaryScaffoldRow(
|
||||
room = room,
|
||||
onClick = onClick,
|
||||
onLongClick = {
|
||||
Timber.d("Long click on invite room")
|
||||
},
|
||||
) {
|
||||
InviteNameAndIndicatorRow(name = room.name)
|
||||
InviteSubtitle(isDm = room.isDm, inviteSender = room.inviteSender, canonicalAlias = room.canonicalAlias)
|
||||
if (!room.isDm && room.inviteSender != null) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
InviteSenderView(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
inviteSender = room.inviteSender,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
InviteButtonsRow(
|
||||
onAcceptClick = {
|
||||
eventSink(RoomListEvents.AcceptInvite(room))
|
||||
},
|
||||
onDeclineClick = {
|
||||
eventSink(RoomListEvents.DeclineInvite(room))
|
||||
}
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
InviteButtonsRow(
|
||||
onAcceptClick = {
|
||||
eventSink(RoomListEvents.AcceptInvite(room))
|
||||
},
|
||||
onDeclineClick = {
|
||||
eventSink(RoomListEvents.DeclineInvite(room))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
RoomSummaryDisplayType.ROOM -> {
|
||||
RoomSummaryScaffoldRow(
|
||||
room = room,
|
||||
onClick = onClick,
|
||||
onLongClick = {
|
||||
eventSink(RoomListEvents.ShowContextMenu(room))
|
||||
},
|
||||
modifier = modifier
|
||||
) {
|
||||
NameAndTimestampRow(
|
||||
name = room.name,
|
||||
timestamp = room.timestamp,
|
||||
isHighlighted = room.isHighlighted
|
||||
)
|
||||
LastMessageAndIndicatorRow(room = room)
|
||||
RoomSummaryDisplayType.ROOM -> {
|
||||
RoomSummaryScaffoldRow(
|
||||
room = room,
|
||||
onClick = onClick,
|
||||
onLongClick = {
|
||||
eventSink(RoomListEvents.ShowContextMenu(room))
|
||||
},
|
||||
) {
|
||||
NameAndTimestampRow(
|
||||
name = room.name,
|
||||
timestamp = room.timestamp,
|
||||
isHighlighted = room.isHighlighted
|
||||
)
|
||||
LastMessageAndIndicatorRow(room = room)
|
||||
}
|
||||
}
|
||||
RoomSummaryDisplayType.KNOCKED -> {
|
||||
RoomSummaryScaffoldRow(
|
||||
room = room,
|
||||
onClick = onClick,
|
||||
onLongClick = {
|
||||
Timber.d("Long click on knocked room")
|
||||
},
|
||||
) {
|
||||
NameAndTimestampRow(
|
||||
name = room.name,
|
||||
timestamp = null,
|
||||
isHighlighted = room.isHighlighted
|
||||
)
|
||||
if (room.canonicalAlias != null) {
|
||||
Text(
|
||||
text = room.canonicalAlias.value,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
}
|
||||
Text(
|
||||
text = stringResource(id = R.string.screen_join_room_knock_sent_title),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ internal fun SetUpRecoveryKeyBanner(
|
||||
modifier = modifier,
|
||||
title = stringResource(R.string.banner_set_up_recovery_title),
|
||||
content = stringResource(R.string.banner_set_up_recovery_content),
|
||||
actionText = stringResource(R.string.banner_set_up_recovery_submit),
|
||||
onSubmitClick = onContinueClick,
|
||||
onDismissClick = onDismissClick,
|
||||
)
|
||||
|
||||
@@ -48,10 +48,16 @@ class RoomListRoomSummaryFactory @Inject constructor(
|
||||
inviteSender = roomInfo.inviter?.toInviteSender(),
|
||||
isDm = roomInfo.isDm,
|
||||
canonicalAlias = roomInfo.canonicalAlias,
|
||||
displayType = if (roomInfo.currentUserMembership == CurrentUserMembership.INVITED) {
|
||||
RoomSummaryDisplayType.INVITE
|
||||
} else {
|
||||
RoomSummaryDisplayType.ROOM
|
||||
displayType = when (roomInfo.currentUserMembership) {
|
||||
CurrentUserMembership.INVITED -> {
|
||||
RoomSummaryDisplayType.INVITE
|
||||
}
|
||||
CurrentUserMembership.KNOCKED -> {
|
||||
RoomSummaryDisplayType.KNOCKED
|
||||
}
|
||||
else -> {
|
||||
RoomSummaryDisplayType.ROOM
|
||||
}
|
||||
},
|
||||
heroes = roomInfo.heroes.map { user ->
|
||||
user.getAvatarData(size = AvatarSize.RoomListItem)
|
||||
|
||||
@@ -102,6 +102,15 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSu
|
||||
displayName = "Bob",
|
||||
),
|
||||
),
|
||||
aRoomListRoomSummary(
|
||||
name = "A knocked room",
|
||||
displayType = RoomSummaryDisplayType.KNOCKED,
|
||||
),
|
||||
aRoomListRoomSummary(
|
||||
name = "A knocked room with alias",
|
||||
canonicalAlias = RoomAlias("#knockable:matrix.org"),
|
||||
displayType = RoomSummaryDisplayType.KNOCKED,
|
||||
)
|
||||
),
|
||||
).flatten()
|
||||
}
|
||||
|
||||
@@ -13,5 +13,6 @@ package io.element.android.features.roomlist.impl.model
|
||||
enum class RoomSummaryDisplayType {
|
||||
PLACEHOLDER,
|
||||
ROOM,
|
||||
INVITE
|
||||
INVITE,
|
||||
KNOCKED,
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<string name="screen_invites_decline_direct_chat_title">"Odmítnout chat"</string>
|
||||
<string name="screen_invites_empty_list">"Žádné pozvánky"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) vás pozval(a)"</string>
|
||||
<string name="screen_join_room_knock_sent_title">"Žádost o vstup odeslána"</string>
|
||||
<string name="screen_migration_message">"Jedná se o jednorázový proces, prosíme o strpení."</string>
|
||||
<string name="screen_migration_title">"Nastavení vašeho účtu"</string>
|
||||
<string name="screen_roomlist_a11y_create_message">"Vytvořte novou konverzaci nebo místnost"</string>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<string name="screen_invites_decline_direct_chat_title">"Απόρριψη συνομιλίας"</string>
|
||||
<string name="screen_invites_empty_list">"Χωρίς προσκλήσεις"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) σέ προσκάλεσε"</string>
|
||||
<string name="screen_join_room_knock_sent_title">"Το αίτημα συμμετοχής στάλθηκε"</string>
|
||||
<string name="screen_migration_message">"Αυτή είναι μια εφάπαξ διαδικασία, ευχαριστώ που περίμενες."</string>
|
||||
<string name="screen_migration_title">"Ρύθμιση του λογαριασμού σου."</string>
|
||||
<string name="screen_roomlist_a11y_create_message">"Δημιουργία νέας συνομιλίας ή δωματίου"</string>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<string name="screen_invites_decline_direct_chat_title">"Keeldu vestlusest"</string>
|
||||
<string name="screen_invites_empty_list">"Kutseid pole"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) saatis sulle kutse"</string>
|
||||
<string name="screen_join_room_knock_sent_title">"Liitumispalve on saadetud"</string>
|
||||
<string name="screen_migration_message">"Tänud, et ootad - seda toimingut on vaja teha vaid üks kord."</string>
|
||||
<string name="screen_migration_title">"Seadistame sinu kasutajakontot."</string>
|
||||
<string name="screen_roomlist_a11y_create_message">"Loo uus vestlus või jututuba"</string>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<string name="screen_invites_decline_direct_chat_title">"Refuser l’invitation"</string>
|
||||
<string name="screen_invites_empty_list">"Aucune invitation"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) vous a invité(e)"</string>
|
||||
<string name="screen_join_room_knock_sent_title">"Demande de rejoindre le salon envoyée"</string>
|
||||
<string name="screen_migration_message">"Il s’agit d’une opération ponctuelle, merci d’attendre quelques instants."</string>
|
||||
<string name="screen_migration_title">"Configuration de votre compte."</string>
|
||||
<string name="screen_roomlist_a11y_create_message">"Créer une nouvelle discussion ou un nouveau salon"</string>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<string name="screen_invites_decline_direct_chat_title">"Csevegés elutasítása"</string>
|
||||
<string name="screen_invites_empty_list">"Nincsenek meghívások"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) meghívta"</string>
|
||||
<string name="screen_join_room_knock_sent_title">"Csatlakozási kérés elküldve"</string>
|
||||
<string name="screen_migration_message">"Ez egy egyszeri folyamat, köszönjük a türelmét."</string>
|
||||
<string name="screen_migration_title">"A fiók beállítása."</string>
|
||||
<string name="screen_roomlist_a11y_create_message">"Új beszélgetés vagy szoba létrehozása"</string>
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="banner_migrate_to_native_sliding_sync_action">"Keluar & Tingkatkan"</string>
|
||||
<string name="banner_migrate_to_native_sliding_sync_description">"Server Anda kini mendukung protokol baru yang lebih cepat. Keluar dan masuk lagi untuk memperbarui sekarang. Melakukan hal ini sekarang akan membantu Anda menghindari keluar paksa saat protokol lama dihapus nantinya."</string>
|
||||
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Homeserver Anda tidak lagi mendukung protokol lama. Silakan keluar dan masuk kembali untuk terus menggunakan aplikasi."</string>
|
||||
<string name="banner_migrate_to_native_sliding_sync_title">"Peningkatan tersedia"</string>
|
||||
<string name="banner_set_up_recovery_content">"Buat kunci pemulihan baru yang dapat digunakan untuk memulihkan riwayat pesan terenkripsi Anda jika Anda kehilangan akses ke perangkat Anda."</string>
|
||||
<string name="banner_set_up_recovery_title">"Siapkan pemulihan"</string>
|
||||
<string name="confirm_recovery_key_banner_message">"Cadangan percakapan Anda saat ini tidak tersinkron. Anda perlu mengonfirmasi kunci pemulihan Anda untuk tetap memiliki akses ke cadangan percakapan Anda."</string>
|
||||
<string name="confirm_recovery_key_banner_title">"Konfirmasi kunci pemulihan Anda"</string>
|
||||
<string name="full_screen_intent_banner_message">"Untuk memastikan Anda tidak melewatkan panggilan penting, silakan ubah pengaturan Anda untuk memperbolehkan notifikasi layar penuh ketika ponsel Anda terkunci."</string>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<string name="screen_invites_decline_direct_chat_title">"Rejeitar conversa"</string>
|
||||
<string name="screen_invites_empty_list">"Sem convites"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) convidou-te"</string>
|
||||
<string name="screen_join_room_knock_sent_title">"Pedido de adesão enviado"</string>
|
||||
<string name="screen_migration_message">"Este processo só acontece uma única vez, obrigado por esperares."</string>
|
||||
<string name="screen_migration_title">"A configurar a tua conta…"</string>
|
||||
<string name="screen_roomlist_a11y_create_message">"Criar uma nova conversa ou sala"</string>
|
||||
|
||||
@@ -7,10 +7,7 @@
|
||||
<string name="banner_set_up_recovery_content">"Создайте новый ключ восстановления, который можно использовать для восстановления зашифрованной истории сообщений в случае потери доступа к своим устройствам."</string>
|
||||
<string name="banner_set_up_recovery_title">"Настроить восстановление"</string>
|
||||
<string name="confirm_recovery_key_banner_message">"В настоящее время резервная копия ваших чатов не синхронизирована. Вам потребуется ввести свой ключ восстановления, чтобы сохранить доступ к резервной копии чатов."</string>
|
||||
<string name="confirm_recovery_key_banner_title">
|
||||
"Введите "
|
||||
<b>"ключ восстановления"</b>
|
||||
</string>
|
||||
<string name="confirm_recovery_key_banner_title">"Введите ключ восстановления"</string>
|
||||
<string name="full_screen_intent_banner_message">"Чтобы больше не пропускать важные звонки, разрешите приложению показывать полноэкранные уведомления на заблокированном экране телефона."</string>
|
||||
<string name="full_screen_intent_banner_title">"Улучшите качество звонков"</string>
|
||||
<string name="screen_invites_decline_chat_message">"Вы уверены, что хотите отклонить приглашение в %1$s?"</string>
|
||||
@@ -19,6 +16,7 @@
|
||||
<string name="screen_invites_decline_direct_chat_title">"Отклонить чат"</string>
|
||||
<string name="screen_invites_empty_list">"Нет приглашений"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) пригласил вас"</string>
|
||||
<string name="screen_join_room_knock_sent_title">"Запрос на присоединение отправлен"</string>
|
||||
<string name="screen_migration_message">"Это одноразовый процесс, спасибо, что подождали."</string>
|
||||
<string name="screen_migration_title">"Настройка учетной записи."</string>
|
||||
<string name="screen_roomlist_a11y_create_message">"Создайте новую беседу или комнату"</string>
|
||||
@@ -33,7 +31,7 @@
|
||||
<string name="screen_roomlist_filter_low_priority">"Низкий приоритет"</string>
|
||||
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Вы можете убрать фильтры, чтобы увидеть другие ваши чаты."</string>
|
||||
<string name="screen_roomlist_filter_mixed_empty_state_title">"У вас нет чатов для этой подборки"</string>
|
||||
<string name="screen_roomlist_filter_people">"Люди"</string>
|
||||
<string name="screen_roomlist_filter_people">"Пользователи"</string>
|
||||
<string name="screen_roomlist_filter_people_empty_state_title">"У вас пока нет личных сообщений"</string>
|
||||
<string name="screen_roomlist_filter_rooms">"Комнаты"</string>
|
||||
<string name="screen_roomlist_filter_rooms_empty_state_title">"Вас пока нет ни в одной комнате"</string>
|
||||
@@ -42,8 +40,8 @@
|
||||
У вас нет непрочитанных сообщений!"</string>
|
||||
<string name="screen_roomlist_main_space_title">"Все чаты"</string>
|
||||
<string name="screen_roomlist_mark_as_read">"Пометить как прочитанное"</string>
|
||||
<string name="screen_roomlist_mark_as_unread">"Пометить как непрочитанное"</string>
|
||||
<string name="screen_roomlist_mark_as_unread">"Отметить как непрочитанное"</string>
|
||||
<string name="screen_roomlist_room_directory_button_title">"Просмотреть все комнаты"</string>
|
||||
<string name="session_verification_banner_message">"Похоже, вы используете новое устройство. Чтобы получить доступ к зашифрованным сообщениям пройдите верификацию с другим устройством."</string>
|
||||
<string name="session_verification_banner_message">"Похоже, вы используете новое устройство. Чтобы получить доступ к зашифрованным сообщениям пройдите подтверждение с другим устройством."</string>
|
||||
<string name="session_verification_banner_title">"Подтвердите, что это вы"</string>
|
||||
</resources>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user