Merge branch 'develop' into renovate/kotlin

This commit is contained in:
Benoit Marty
2024-10-30 11:14:29 +01:00
committed by GitHub
1157 changed files with 7158 additions and 4277 deletions

View File

@@ -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)
========================================

View File

@@ -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()
}
}
}

View File

@@ -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

View 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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 cant 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>

View File

@@ -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>

View File

@@ -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)

View File

@@ -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

View File

@@ -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,

View File

@@ -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
}

View File

@@ -43,7 +43,8 @@ class JoinRoomNode @AssistedInject constructor(
state = state,
onBackClick = ::navigateUp,
onJoinSuccess = ::navigateUp,
onKnockSuccess = ::navigateUp,
onCancelKnockSuccess = ::navigateUp,
onKnockSuccess = { },
modifier = modifier
)
acceptDeclineInviteView.Render(

View File

@@ -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
}

View File

@@ -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

View File

@@ -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
)

View File

@@ -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 = { },
)
}

View File

@@ -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"))
}
}

View File

@@ -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,
)

View File

@@ -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 { }
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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. Youll be able to join the conversation once approved."</string>

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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
)

View File

@@ -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
)
}
}

View File

@@ -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),
)

View File

@@ -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),
)
}
}

View File

@@ -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

View File

@@ -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),
)

View File

@@ -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

View File

@@ -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

View File

@@ -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),
)

View File

@@ -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>

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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(

View File

@@ -31,7 +31,6 @@ fun anAttachmentsPreviewState(
) = AttachmentsPreviewState(
attachment = Attachment.Media(
localMedia = LocalMedia("file://path".toUri(), mediaInfo),
compressIfPossible = true
),
sendActionState = sendActionState,
eventSink = {}

View File

@@ -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),

View File

@@ -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()
}

View File

@@ -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(

View File

@@ -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(),

View File

@@ -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()),
)
}
}

View File

@@ -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
}

View File

@@ -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) {

View File

@@ -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)
),
)
}
}

View File

@@ -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,

View File

@@ -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),

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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 ->

View File

@@ -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(

View File

@@ -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())
)
}
}

View File

@@ -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,
)

View File

@@ -1489,7 +1489,7 @@ class MessageComposerPresenterTest {
featureFlagService,
sessionPreferencesStore,
localMediaFactory,
MediaSender(mediaPreProcessor, room),
MediaSender(mediaPreProcessor, room, InMemorySessionPreferencesStore()),
snackbarDispatcher,
analyticsService,
DefaultMessageComposerContext(),

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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

View File

@@ -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) }

View File

@@ -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

View File

@@ -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

View File

@@ -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) {

View File

@@ -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,
)

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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()

View File

@@ -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,
)
}
}
}

View File

@@ -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)
}
}

View File

@@ -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,
}

View File

@@ -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),
)
)

View File

@@ -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,
)
}
}
}

View File

@@ -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>

View File

@@ -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),

View File

@@ -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)
)
}
}

View File

@@ -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,
)
}
}
}
}

View File

@@ -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,
)

View File

@@ -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)

View File

@@ -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()
}

View File

@@ -13,5 +13,6 @@ package io.element.android.features.roomlist.impl.model
enum class RoomSummaryDisplayType {
PLACEHOLDER,
ROOM,
INVITE
INVITE,
KNOCKED,
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -16,6 +16,7 @@
<string name="screen_invites_decline_direct_chat_title">"Refuser linvitation"</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 sagit dune opération ponctuelle, merci dattendre 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>

View File

@@ -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>

View File

@@ -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 &amp; 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>

View File

@@ -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>

View File

@@ -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