Merge branch 'release/25.10.1'

This commit is contained in:
Jorge Martín
2025-10-21 14:15:12 +02:00
3116 changed files with 9836 additions and 8175 deletions

View File

@@ -69,7 +69,7 @@ jobs:
retention-days: 5
overwrite: true
if-no-files-found: error
- uses: rnkdsh/action-upload-diawi@26292a7b424bdc9f4ab4ccea6202fc513f571370 # v1.5.11
- uses: rnkdsh/action-upload-diawi@4e1421305be7cfc510d05f47850262eeaf345108 # v1.5.12
id: diawi
# Do not fail the whole build if Diawi upload fails
continue-on-error: true

View File

@@ -25,7 +25,7 @@ jobs:
- name: Set up Python 3.12
uses: actions/setup-python@v6
with:
python-version: 3.13
python-version: 3.14
- name: Run World screenshots generation script
run: |
./tools/test/generateWorldScreenshots.py

View File

@@ -37,7 +37,7 @@ jobs:
- name: Set up Python 3.12
uses: actions/setup-python@v6
with:
python-version: 3.13
python-version: 3.14
- name: Search for invalid screenshot files
run: ./tools/test/checkInvalidScreenshots.py
@@ -58,7 +58,7 @@ jobs:
- name: Set up Python 3.12
uses: actions/setup-python@v6
with:
python-version: 3.13
python-version: 3.14
- name: Search for invalid dependencies
run: ./tools/dependencies/checkDependencies.py
@@ -103,6 +103,39 @@ jobs:
path: |
**/build/reports/**/*.*
compose:
name: Compose tests
runs-on: ubuntu-latest
# Allow all jobs on main and develop. Just one per PR.
concurrency:
group: ${{ github.ref == 'refs/heads/main' && format('check-compose-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-compose-develop-{0}', github.sha) || format('check-compose-{0}', github.ref) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v5
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
- name: Add SSH private keys for submodule repositories
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
with:
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
- name: Clone submodules
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: Use JDK 21
uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run compose tests
run: ./tools/compose/check_stability.sh
lint:
name: Android lint check
runs-on: ubuntu-latest

View File

@@ -24,7 +24,7 @@ jobs:
- name: Set up Python 3.12
uses: actions/setup-python@v6
with:
python-version: 3.13
python-version: 3.14
- name: Setup Localazy
run: |
curl -sS https://dist.localazy.com/debian/pubkey.gpg | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/localazy.gpg

View File

@@ -16,7 +16,7 @@ jobs:
- name: Set up Python 3.12
uses: actions/setup-python@v6
with:
python-version: 3.13
python-version: 3.14
- name: Install Prerequisite dependencies
run: |
pip install requests

View File

@@ -1,3 +1,83 @@
Changes in Element X v25.10.0
=============================
<!-- Release notes generated using configuration in .github/release.yml at v25.10.0 -->
## What's Changed
### ✨ Features
* Use shared recent emoji reactions from account data by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5402
* Follow permalinks to and from threads by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5414
* Add support for Spaces by @bmarty in https://github.com/element-hq/element-x-android/pull/5462
* Add Labs screen for beta testing of public features by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5465
### 🙌 Improvements
* Update the strings for the device verification flow by @andybalaam in https://github.com/element-hq/element-x-android/pull/5419
* Set a notification sound by @bmarty in https://github.com/element-hq/element-x-android/pull/5469
* Improve current push provider test: give info about the distributor. by @bmarty in https://github.com/element-hq/element-x-android/pull/5471
* Improve AnnouncementService. by @bmarty in https://github.com/element-hq/element-x-android/pull/5482
### 🐛 Bugfixes
* Improvement and bugfix on incoming verification request screen by @bmarty in https://github.com/element-hq/element-x-android/pull/5426
* Space : makes sure to use room heroes for avatar by @ganfra in https://github.com/element-hq/element-x-android/pull/5488
* Filter out direct room in the leave space screen. by @bmarty in https://github.com/element-hq/element-x-android/pull/5498
### 🗣 Translations
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5427
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5460
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5486
### 🧱 Build
* Remove unused dependency on `javax.inject:javax.inject` by @bmarty in https://github.com/element-hq/element-x-android/pull/5445
* Internalize compound-android by @bmarty in https://github.com/element-hq/element-x-android/pull/5457
### 🚧 In development 🚧
* Sdk : use latest apis for space by @ganfra in https://github.com/element-hq/element-x-android/pull/5404
* Multi accounts - experimental first implementation by @bmarty in https://github.com/element-hq/element-x-android/pull/5285
* Leave space - UI by @bmarty in https://github.com/element-hq/element-x-android/pull/5354
* Leave spave: iteration on string value. by @bmarty in https://github.com/element-hq/element-x-android/pull/5425
* Feature : space list join action by @ganfra in https://github.com/element-hq/element-x-android/pull/5431
* Room list space invite by @ganfra in https://github.com/element-hq/element-x-android/pull/5449
* Leave space: use SDK API. by @bmarty in https://github.com/element-hq/element-x-android/pull/5432
* Space annoucement by @bmarty in https://github.com/element-hq/element-x-android/pull/5451
* feature(space) : keep space children in the presenter by @ganfra in https://github.com/element-hq/element-x-android/pull/5456
* Spaces : some tweaks around ui by @ganfra in https://github.com/element-hq/element-x-android/pull/5468
* Use "BETA" word from Localazy and ensure layout is correct by @bmarty in https://github.com/element-hq/element-x-android/pull/5470
* Disable avatar cluster for now by @bmarty in https://github.com/element-hq/element-x-android/pull/5492
### Dependency upgrades
* Update dependency com.posthog:posthog-android to v3.21.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5360
* Update dependency io.element.android:element-call-embedded to v0.16.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5408
* Update dependency net.java.dev.jna:jna to v5.18.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5398
* Update plugin dependencycheck to v12.1.6 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5405
* Update dependency org.matrix.rustcomponents:sdk-android to v25.9.25 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5412
* Update dependency androidx.sqlite:sqlite-ktx to v2.6.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5409
* Update kotlin by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5317
* Update metro to v0.6.7 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5416
* Update dependency app.cash.molecule:molecule-runtime to v2.2.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5413
* Update dependency com.posthog:posthog-android to v3.22.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5415
* Update metro to v0.6.8 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5422
* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.10.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5438
* fix(deps): update dependency net.java.dev.jna:jna to v5.18.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5437
* fix(deps): update dependency io.mockk:mockk to v1.14.6 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5441
* Update gradle/actions action to v5 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5444
* fix(deps): update dependency io.sentry:sentry-android to v8.23.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5442
* fix(deps): update dependency org.maplibre.gl:android-sdk to v12 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5455
* fix(deps): update dependency com.posthog:posthog-android to v3.23.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5463
* fix(deps): update roborazzi to v1.50.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5464
* fix(deps): update telephoto to v0.18.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5459
### Others
* Ensure Metro `@AssistedInject` is used. by @bmarty in https://github.com/element-hq/element-x-android/pull/5420
* Misc : destroy SpaceRoomList by @ganfra in https://github.com/element-hq/element-x-android/pull/5436
* Remove CurrentSessionIdHolder and inject SessionId instead. by @bmarty in https://github.com/element-hq/element-x-android/pull/5440
* Only offer to verify if a cross-signed device is available by @uhoreg in https://github.com/element-hq/element-x-android/pull/5433
* Replace fun by val in MatrixClient by @bmarty in https://github.com/element-hq/element-x-android/pull/5466
* Space : makes sure to use SpaceRoom.displayName from sdk by @ganfra in https://github.com/element-hq/element-x-android/pull/5476
* Add preview with all icons in the Showkase browser by @bmarty in https://github.com/element-hq/element-x-android/pull/5485
* Ensure that we are using Immutable instead of Persistent by @bmarty in https://github.com/element-hq/element-x-android/pull/5490
* Reduce number of Previews for Avatar. by @bmarty in https://github.com/element-hq/element-x-android/pull/5495
* Fix error when attempting to verify with recovery key with missing backup key by @uhoreg in https://github.com/element-hq/element-x-android/pull/5314
* Sync strings by @bmarty in https://github.com/element-hq/element-x-android/pull/5499
* feature(space): make sure to handle topic properly by @ganfra in https://github.com/element-hq/element-x-android/pull/5493
## New Contributors
* @uhoreg made their first contribution in https://github.com/element-hq/element-x-android/pull/5433
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.09.2...v25.10.0
Changes in Element X v25.09.2
=============================

View File

@@ -197,6 +197,12 @@ android {
buildConfigFieldStr("FLAVOR_DESCRIPTION", "FDroid")
}
}
packaging {
resources.pickFirsts += setOf(
"META-INF/versions/9/OSGI-INF/MANIFEST.MF",
)
}
}
androidComponents {
@@ -318,6 +324,7 @@ licensee {
allowUrl("https://jsoup.org/license")
allowUrl("https://asm.ow2.io/license.html")
allowUrl("https://www.gnu.org/licenses/agpl-3.0.txt")
allowUrl("https://github.com/mhssn95/compose-color-picker/blob/main/LICENSE")
ignoreDependencies("com.github.matrix-org", "matrix-analytics-events")
// Ignore dependency that are not third-party licenses to us.
ignoreDependencies(groupId = "io.element.android")

View File

@@ -34,10 +34,17 @@
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false">
android:exported="false"
tools:node="merge">
<meta-data
android:name='androidx.lifecycle.ProcessLifecycleInitializer'
android:value='androidx.startup' />
<!-- Remove to use Application workManagerConfiguration -->
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
<!--
@@ -175,7 +182,6 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_providers" />
</provider>
</application>
</manifest>

View File

@@ -9,17 +9,23 @@ package io.element.android.x
import android.app.Application
import androidx.startup.AppInitializer
import androidx.work.Configuration
import dev.zacsweers.metro.createGraphFactory
import io.element.android.features.cachecleaner.api.CacheCleanerInitializer
import io.element.android.libraries.di.DependencyInjectionGraphOwner
import io.element.android.libraries.workmanager.api.di.MetroWorkerFactory
import io.element.android.x.di.AppGraph
import io.element.android.x.info.logApplicationInfo
import io.element.android.x.initializer.CrashInitializer
import io.element.android.x.initializer.PlatformInitializer
class ElementXApplication : Application(), DependencyInjectionGraphOwner {
class ElementXApplication : Application(), DependencyInjectionGraphOwner, Configuration.Provider {
override val graph: AppGraph = createGraphFactory<AppGraph.Factory>().create(this)
override val workManagerConfiguration: Configuration = Configuration.Builder()
.setWorkerFactory(MetroWorkerFactory(graph.workerProviders))
.build()
override fun onCreate() {
super.onCreate()
AppInitializer.getInstance(this).apply {
@@ -27,6 +33,7 @@ class ElementXApplication : Application(), DependencyInjectionGraphOwner {
initializeComponent(PlatformInitializer::class.java)
initializeComponent(CacheCleanerInitializer::class.java)
}
logApplicationInfo(this)
}
}

View File

@@ -8,16 +8,24 @@
package io.element.android.x.di
import android.content.Context
import androidx.work.ListenableWorker
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.DependencyGraph
import dev.zacsweers.metro.Multibinds
import dev.zacsweers.metro.Provides
import io.element.android.libraries.architecture.NodeFactoriesBindings
import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.workmanager.api.di.MetroWorkerFactory
import kotlin.reflect.KClass
@DependencyGraph(AppScope::class)
interface AppGraph : NodeFactoriesBindings {
val sessionGraphFactory: SessionGraph.Factory
@Multibinds
val workerProviders:
Map<KClass<out ListenableWorker>, MetroWorkerFactory.WorkerInstanceFactory<*>>
@DependencyGraph.Factory
interface Factory {
fun create(

View File

@@ -9,7 +9,6 @@
<locale android:name="el"/>
<locale android:name="en"/>
<locale android:name="en_US"/>
<locale android:name="eo"/>
<locale android:name="es"/>
<locale android:name="et"/>
<locale android:name="eu"/>

View File

@@ -73,7 +73,7 @@ class LoggedInAppScopeFlowNode(
override fun onBuilt() {
super.onBuilt()
lifecycle.subscribe(
onCreate = {
onResume = {
SingletonImageLoader.setUnsafe(imageLoaderHolder.get(inputs.matrixClient))
},
)

View File

@@ -63,7 +63,7 @@ class NotLoggedInFlowNode(
override fun onBuilt() {
super.onBuilt()
lifecycle.subscribe(
onCreate = {
onResume = {
SingletonImageLoader.setUnsafe(notLoggedInImageLoaderFactory.newImageLoader())
},
)

View File

@@ -7,12 +7,10 @@
package io.element.android.appnav.root
import androidx.compose.runtime.Immutable
import io.element.android.features.rageshake.api.crash.CrashDetectionState
import io.element.android.features.rageshake.api.detection.RageshakeDetectionState
import io.element.android.services.apperror.api.AppErrorState
@Immutable
data class RootState(
val rageshakeDetectionState: RageshakeDetectionState,
val crashDetectionState: CrashDetectionState,

View File

@@ -0,0 +1,2 @@
Main changes in this version: bug fixes around notifications and UX improvements.
Full changelog: https://github.com/element-hq/element-x-android/releases

View File

@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_space_announcement_item1">"Se klynger, du har oprettet eller tilmeldt dig"</string>
<string name="screen_space_announcement_item2">"Acceptere eller afvise invitationer til klynger"</string>
<string name="screen_space_announcement_item3">"Finde alle rum, du kan deltage i, i dine klynger"</string>
<string name="screen_space_announcement_item4">"Deltage i offentlige klynger"</string>
<string name="screen_space_announcement_item5">"Forlade de klynger, du har tilsluttet dig"</string>
<string name="screen_space_announcement_notice">"Oprettelse og administration af klynger kommer snart."</string>
<string name="screen_space_announcement_subtitle">"Velkommen til betaversionen af Klynger! Med denne første version kan du:"</string>
<string name="screen_space_announcement_title">"Introduktion til Klynger"</string>
<string name="screen_space_announcement_item1">"Se grupper, du har oprettet eller tilmeldt dig"</string>
<string name="screen_space_announcement_item2">"Acceptere eller afvise invitationer til grupper"</string>
<string name="screen_space_announcement_item3">"Finde alle rum, du kan deltage i, i dine grupper"</string>
<string name="screen_space_announcement_item4">"Deltage i offentlige grupper"</string>
<string name="screen_space_announcement_item5">"Forlade de grupper, du har tilsluttet dig"</string>
<string name="screen_space_announcement_notice">"Filtrering, oprettelse og administration af grupper kommer snart."</string>
<string name="screen_space_announcement_subtitle">"Velkommen til betaversionen af Grupper! Med denne første version kan du:"</string>
<string name="screen_space_announcement_title">"Introduktion til Grupper"</string>
</resources>

View File

@@ -5,7 +5,7 @@
<string name="screen_space_announcement_item3">"Chats innerhalb deiner Spaces entdecken, um ihnen beizutreten"</string>
<string name="screen_space_announcement_item4">"Öffentlichen Spaces beitreten"</string>
<string name="screen_space_announcement_item5">"Spaces verlassen, bei denen du Mitglied bist"</string>
<string name="screen_space_announcement_notice">"Das Erstellen und Verwalten von Spaces ist bald verfügbar."</string>
<string name="screen_space_announcement_notice">"Das Filtern, Erstellen und Verwalten von Spaces ist bald verfügbar."</string>
<string name="screen_space_announcement_subtitle">"Willkommen bei der Beta-Version von Spaces! Mit dieser ersten Version kannst du:"</string>
<string name="screen_space_announcement_title">"Einführung in Spaces"</string>
</resources>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_space_announcement_item1">"Vaadata kogukondi, mille oled loonud või millega oled liitunud"</string>
<string name="screen_space_announcement_item2">"Nõustuda kutsetega liitumiseks kogukonnaga või sellest keelduda"</string>
<string name="screen_space_announcement_item3">"Uurida neis kogukondades leiduvaid jututube ning nendega liituda"</string>
<string name="screen_space_announcement_item4">"Liituda avalike kogukondadega"</string>
<string name="screen_space_announcement_item5">"Lahkuda kogukonnast, millega oled liitunud"</string>
<string name="screen_space_announcement_notice">"Kogukondade filtreerimine, loomine ja haldamine lisandub peagi"</string>
<string name="screen_space_announcement_subtitle">"Tere tulemast kasutama kogukondade beetaversiooni! Selles esimeses versioonis saad sa:"</string>
<string name="screen_space_announcement_title">"Võtame kasutusele kogukonnad"</string>
</resources>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_space_announcement_item1">"دیدن فضاهایی که ساخته یا پیوسته‌اید"</string>
<string name="screen_space_announcement_item2">"پذیرش یا رد دعوت‌ها به فضاها"</string>
<string name="screen_space_announcement_item3">"کشف تمامی اتاق‌هایی که می‌توانید در فضاهایتان بپیوندید"</string>
<string name="screen_space_announcement_item4">"پیوستن به فضاهای عمومی"</string>
<string name="screen_space_announcement_item5">"ترک هر فضایی که پیوسته‌اید"</string>
<string name="screen_space_announcement_notice">"پالایش، ایجاد و مدیریت کردن فضاها به زودی."</string>
<string name="screen_space_announcement_subtitle">"به نگارش آزمایشی فضاها خوش آمدید! در این نگارش می‌توانید:"</string>
<string name="screen_space_announcement_title">"معرّفی فضاها"</string>
</resources>

View File

@@ -5,7 +5,7 @@
<string name="screen_space_announcement_item3">"Löytää kaikki huoneet, joihin voit liittyä tiloissasi"</string>
<string name="screen_space_announcement_item4">"Liittyä julkisiin tiloihin"</string>
<string name="screen_space_announcement_item5">"Poistua mistä tahansa tilasta, johon olet liittynyt"</string>
<string name="screen_space_announcement_notice">"Tilojen luominen ja hallinta on tulossa pian."</string>
<string name="screen_space_announcement_notice">"Tilojen suodatus, luominen ja hallinta on tulossa pian."</string>
<string name="screen_space_announcement_subtitle">"Tervetuloa tilojen beetaversioon! Tämän ensimmäisen version avulla voit:"</string>
<string name="screen_space_announcement_title">"Esittelyssä tilat"</string>
</resources>

View File

@@ -5,7 +5,7 @@
<string name="screen_space_announcement_item3">"Découvrir les salons que vous pouvez joindre depuis vos espaces"</string>
<string name="screen_space_announcement_item4">"Rejoindre les espaces publics"</string>
<string name="screen_space_announcement_item5">"Quitter les espaces dont vous êtes membre."</string>
<string name="screen_space_announcement_notice">"La création et la gestion des espaces seront bientôt disponibles."</string>
<string name="screen_space_announcement_notice">"Le filtrage, la création et la gestion des espaces seront bientôt disponibles."</string>
<string name="screen_space_announcement_subtitle">"Bienvenue dans la version bêta des espaces! Avec cette première version, vous pourrez :"</string>
<string name="screen_space_announcement_title">"Ajout des espaces"</string>
</resources>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_space_announcement_item1">"Az Ön által létrehozott vagy csatlakozott térek megtekintése"</string>
<string name="screen_space_announcement_item2">"A meghívások elfogadására vagy elutasítására a terekhez"</string>
<string name="screen_space_announcement_item3">"Szobák felfedezése a terekben, amelyekhez csatlakozhat"</string>
<string name="screen_space_announcement_item4">"Csatlakozás nyilvános terekhez"</string>
<string name="screen_space_announcement_item5">"Terek elhagyása"</string>
<string name="screen_space_announcement_notice">"Terek szűrése, készítése és kezelése hamarosan érkezik."</string>
<string name="screen_space_announcement_subtitle">"Üdvözöljük a tér béta verziójában! Ezzel az első verzióval a következőket teheti:"</string>
</resources>

View File

@@ -5,7 +5,7 @@
<string name="screen_space_announcement_item3">"Oppdag alle rom du kan bli med i i dine områder"</string>
<string name="screen_space_announcement_item4">"Bli med i offentlige områder"</string>
<string name="screen_space_announcement_item5">"Forlat områder du har blitt med i"</string>
<string name="screen_space_announcement_notice">"Oppretting og administrasjon av områder kommer snart."</string>
<string name="screen_space_announcement_notice">"Oppretting, filtrering og administrasjon av områder kommer snart."</string>
<string name="screen_space_announcement_subtitle">"Velkommen til betaversjonen av Områder! Med denne første versjonen kan du:"</string>
<string name="screen_space_announcement_title">"Vi introduserer Områder"</string>
</resources>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_space_announcement_item1">"Wyświetlić przestrzenie, które stworzyłeś lub do których dołączyłeś"</string>
<string name="screen_space_announcement_item2">"Akceptować lub odrzucać zaproszenia"</string>
<string name="screen_space_announcement_item3">"Odkrywać wszystkie pokoje, do których możesz dołączyć w swoich przestrzeniach"</string>
<string name="screen_space_announcement_item4">"Dołączać do przestrzeni publicznych"</string>
<string name="screen_space_announcement_item5">"Opuszczać jakąkolwiek przestrzeń, do której dołączyłeś"</string>
<string name="screen_space_announcement_notice">"Filtrowanie, tworzenie i zarządzanie przestrzeniami pojawi się wkrótce."</string>
<string name="screen_space_announcement_subtitle">"Witamy w wersji beta przestrzeni! W tej wersji możesz:"</string>
<string name="screen_space_announcement_title">"Przedstawiamy przestrzenie"</string>
</resources>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_space_announcement_item1">"檢視您建立或加入的空間"</string>
<string name="screen_space_announcement_item2">"接受或拒絕空間邀請"</string>
<string name="screen_space_announcement_item3">"探索空間內您可以加入的任何聊天室"</string>
<string name="screen_space_announcement_item4">"加入公開空間"</string>
<string name="screen_space_announcement_item5">"離開任何您已加入的空間"</string>
<string name="screen_space_announcement_notice">"篩選、建立與管理空間功能即將推出。"</string>
<string name="screen_space_announcement_subtitle">"歡迎使用空間的測試版!此初始版本可讓您:"</string>
<string name="screen_space_announcement_title">"介紹空間"</string>
</resources>

View File

@@ -64,6 +64,7 @@ class CallScreenPresenter(
private val appForegroundStateService: AppForegroundStateService,
@AppCoroutineScope
private val appCoroutineScope: CoroutineScope,
private val widgetMessageSerializer: WidgetMessageSerializer,
) : Presenter<CallScreenState> {
@AssistedFactory
interface Factory {
@@ -258,7 +259,7 @@ class CallScreenPresenter(
}
private fun parseMessage(message: String): WidgetMessage? {
return WidgetMessageSerializer.deserialize(message).getOrNull()
return widgetMessageSerializer.deserialize(message).getOrNull()
}
private fun sendHangupMessage(widgetId: String, messageInterceptor: WidgetMessageInterceptor) {
@@ -269,7 +270,7 @@ class CallScreenPresenter(
action = WidgetMessage.Action.HangUp,
data = null,
)
messageInterceptor.sendMessage(WidgetMessageSerializer.serialize(message))
messageInterceptor.sendMessage(widgetMessageSerializer.serialize(message))
}
private fun CoroutineScope.close(widgetDriver: MatrixWidgetDriver?, navigator: CallScreenNavigator) = launch(dispatchers.io) {

View File

@@ -8,7 +8,6 @@
package io.element.android.features.call.impl.ui
import android.annotation.SuppressLint
import android.util.Log
import android.view.ViewGroup
import android.webkit.ConsoleMessage
import android.webkit.JavascriptInterface
@@ -60,6 +59,7 @@ interface CallScreenNavigator {
internal fun CallScreenView(
state: CallScreenState,
pipState: PictureInPictureState,
onConsoleMessage: (ConsoleMessage) -> Unit,
requestPermissions: (Array<String>, RequestPermissionCallback) -> Unit,
modifier: Modifier = Modifier,
) {
@@ -108,6 +108,7 @@ internal fun CallScreenView(
val callback: RequestPermissionCallback = { request.grant(it) }
requestPermissions(androidPermissions.toTypedArray(), callback)
},
onConsoleMessage = onConsoleMessage,
onCreateWebView = { webView ->
webView.addBackHandler(onBackPressed = ::handleBack)
val interceptor = WebViewWidgetMessageInterceptor(
@@ -174,6 +175,7 @@ private fun CallWebView(
url: AsyncData<String>,
userAgent: String,
onPermissionsRequest: (PermissionRequest) -> Unit,
onConsoleMessage: (ConsoleMessage) -> Unit,
onCreateWebView: (WebView) -> Unit,
onDestroyWebView: (WebView) -> Unit,
modifier: Modifier = Modifier,
@@ -188,7 +190,11 @@ private fun CallWebView(
factory = { context ->
WebView(context).apply {
onCreateWebView(this)
setup(userAgent, onPermissionsRequest)
setup(
userAgent = userAgent,
onPermissionsRequested = onPermissionsRequest,
onConsoleMessage = onConsoleMessage,
)
}
},
update = { webView ->
@@ -208,6 +214,7 @@ private fun CallWebView(
private fun WebView.setup(
userAgent: String,
onPermissionsRequested: (PermissionRequest) -> Unit,
onConsoleMessage: (ConsoleMessage) -> Unit,
) {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
@@ -232,35 +239,7 @@ private fun WebView.setup(
}
override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
val priority = when (consoleMessage.messageLevel()) {
ConsoleMessage.MessageLevel.ERROR -> Log.ERROR
ConsoleMessage.MessageLevel.WARNING -> Log.WARN
else -> Log.DEBUG
}
val message = buildString {
append(consoleMessage.sourceId())
append(":")
append(consoleMessage.lineNumber())
append(" ")
append(consoleMessage.message())
}
if (message.contains("password=")) {
// Avoid logging any messages that contain "password" to prevent leaking sensitive information
return true
}
Timber.tag("WebView").log(
priority = priority,
message = buildString {
append(consoleMessage.sourceId())
append(":")
append(consoleMessage.lineNumber())
append(" ")
append(consoleMessage.message())
},
)
onConsoleMessage(consoleMessage)
return true
}
}
@@ -286,6 +265,7 @@ internal fun CallScreenViewPreview(
state = state,
pipState = aPictureInPictureState(),
requestPermissions = { _, _ -> },
onConsoleMessage = {},
)
}

View File

@@ -42,6 +42,7 @@ import io.element.android.features.call.impl.pip.PipView
import io.element.android.features.call.impl.services.CallForegroundService
import io.element.android.features.call.impl.utils.CallIntentDataParser
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.androidutils.browser.ConsoleMessageLogger
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.audio.api.AudioFocus
@@ -65,6 +66,7 @@ class ElementCallActivity :
@Inject lateinit var pictureInPicturePresenter: PictureInPicturePresenter
@Inject lateinit var buildMeta: BuildMeta
@Inject lateinit var audioFocus: AudioFocus
@Inject lateinit var consoleMessageLogger: ConsoleMessageLogger
private lateinit var presenter: Presenter<CallScreenState>
@@ -119,6 +121,9 @@ class ElementCallActivity :
CallScreenView(
state = state,
pipState = pipState,
onConsoleMessage = {
consoleMessageLogger.log("ElementCall", it)
},
requestPermissions = { permissions, callback ->
requestPermissionCallback = callback
requestPermissionsLauncher.launch(permissions)

View File

@@ -28,7 +28,9 @@ import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
import io.element.android.libraries.push.api.notifications.ForegroundServiceType
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
@@ -39,16 +41,17 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
@@ -180,13 +183,7 @@ class DefaultActiveCallManager(
val previousActiveCall = activeCall.value ?: return
val notificationData = (previousActiveCall.callState as? CallState.Ringing)?.notificationData ?: return
activeCall.value = null
if (activeWakeLock?.isHeld == true) {
Timber.tag(tag).d("Releasing partial wakelock after timeout")
activeWakeLock.release()
}
cancelIncomingCallNotification()
removeCurrentCall()
if (displayMissedCallNotification) {
displayMissedCallNotification(notificationData)
@@ -211,24 +208,16 @@ class DefaultActiveCallManager(
?.declineCall(notificationData.eventId)
}
cancelIncomingCallNotification()
if (activeWakeLock?.isHeld == true) {
Timber.tag(tag).d("Releasing partial wakelock after hang up")
activeWakeLock.release()
}
timedOutCallJob?.cancel()
activeCall.value = null
removeCurrentCall()
}
/**
* Removes the current active call and any associated UI, cancelling the timeouts and wakelocks.
*/
override suspend fun joinedCall(callType: CallType) = mutex.withLock {
Timber.tag(tag).d("Joined call: $callType")
cancelIncomingCallNotification()
if (activeWakeLock?.isHeld == true) {
Timber.tag(tag).d("Releasing partial wakelock after joining call")
activeWakeLock.release()
}
timedOutCallJob?.cancel()
removeCurrentCall()
activeCall.value = ActiveCall(
callType = callType,
@@ -236,6 +225,23 @@ class DefaultActiveCallManager(
)
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun removeCurrentCall() {
// Cancel and remove the timeout call job, if any
timedOutCallJob?.cancel()
timedOutCallJob = null
// Remove the active call and cancel the notification
activeCall.value = null
cancelIncomingCallNotification()
// Also remove any wake locks that may be held
if (activeWakeLock?.isHeld == true) {
Timber.tag(tag).d("Releasing partial wakelock after call declined from another session")
activeWakeLock.release()
}
}
@SuppressLint("MissingPermission")
private suspend fun showIncomingCallNotification(notificationData: CallNotificationData) {
Timber.tag(tag).d("Displaying ringing call notification")
@@ -281,73 +287,75 @@ class DefaultActiveCallManager(
@OptIn(ExperimentalCoroutinesApi::class)
private fun observeRingingCall() {
activeCall
.filterNotNull()
.filter { it.callState is CallState.Ringing && it.callType is CallType.RoomCall }
.flatMapLatest { activeCall ->
val callType = activeCall.callType as CallType.RoomCall
val ringingInfo = activeCall.callState as CallState.Ringing
val client = matrixClientProvider.getOrRestore(callType.sessionId).getOrNull() ?: run {
Timber.tag(tag).d("Couldn't find session for incoming call: $activeCall")
return@flatMapLatest flowOf()
}
val room = client.getRoom(callType.roomId) ?: run {
Timber.tag(tag).d("Couldn't find room for incoming call: $activeCall")
return@flatMapLatest flowOf()
}
val roomForActiveCallFlow: Flow<Pair<BaseRoom, EventId>?> = activeCall.mapLatest { activeCall ->
val callType = activeCall?.callType as? CallType.RoomCall ?: return@mapLatest null
val ringingInfo = activeCall.callState as? CallState.Ringing ?: return@mapLatest null
val client = matrixClientProvider.getOrRestore(callType.sessionId).getOrNull() ?: run {
Timber.tag(tag).d("Couldn't find session for incoming call: $activeCall")
return@mapLatest null
}
val room = client.getRoom(callType.roomId) ?: run {
Timber.tag(tag).d("Couldn't find room for incoming call: $activeCall")
return@mapLatest null
}
Timber.tag(tag).d("Found room for ringing call: ${room.roomId}")
Timber.tag(tag).d("Found room for ringing call: ${room.roomId}")
val eventId = ringingInfo.notificationData.eventId
room to eventId
}
roomForActiveCallFlow
.flatMapLatest { pair ->
val (room, eventId) = pair
// This will cancel the previous iteration of flatMapLatest if the active call is now null
?: return@flatMapLatest flowOf()
// If we have declined from another phone we want to stop ringing.
room.subscribeToCallDecline(ringingInfo.notificationData.eventId)
room.subscribeToCallDecline(eventId)
.filter { decliner ->
Timber.tag(tag).d("Call: $activeCall was declined by $decliner")
// only want to listen if the call was declined from another of my sessions,
// (we are ringing for an incoming call in a DM)
decliner == client.sessionId
decliner == room.sessionId
}
}
.onEach { decliner ->
Timber.tag(tag).d("Call: $activeCall was declined by user from another session")
// Remove the active call and cancel the notification
activeCall.value = null
if (activeWakeLock?.isHeld == true) {
Timber.tag(tag).d("Releasing partial wakelock after call declined from another session")
activeWakeLock.release()
}
cancelIncomingCallNotification()
removeCurrentCall()
}
.launchIn(coroutineScope)
// This will observe ringing calls and ensure they're terminated if the room call is cancelled or if the user
// has joined the call from another session.
activeCall
.filterNotNull()
.filter { it.callState is CallState.Ringing && it.callType is CallType.RoomCall }
.flatMapLatest { activeCall ->
val callType = activeCall.callType as CallType.RoomCall
// Get a flow of updated `hasRoomCall` and `activeRoomCallParticipants` values for the room
val room = matrixClientProvider.getOrRestore(callType.sessionId).getOrNull()?.getRoom(callType.roomId) ?: run {
Timber.tag(tag).d("Couldn't find room for incoming call: $activeCall")
return@flatMapLatest flowOf()
}
roomForActiveCallFlow
.flatMapLatest { pair ->
val (room, _) = pair
// This will cancel the previous iteration of flatMapLatest if the active call is now null
?: return@flatMapLatest flowOf()
// We now observe the room info for changes to the active call state and the call participants
room.roomInfoFlow.map {
Timber.tag(tag).d("Has room call status changed for ringing call: ${it.hasRoomCall}")
it.hasRoomCall to (callType.sessionId in it.activeRoomCallParticipants)
val participants = it.activeRoomCallParticipants
Timber.tag(tag).d("Room call status changed for ringing call | hasRoomCall: ${it.hasRoomCall} | participants: $participants")
val userIsInTheCall = room.sessionId in participants
it.hasRoomCall to userIsInTheCall
}
}
// We only want to check if the room active call status changes
// Filter out duplicate values
.distinctUntilChanged()
// Skip the first one, we're not interested in it (if the check below passes, it had to be active anyway)
.drop(1)
.onEach { (roomHasActiveCall, userIsInTheCall) ->
if (!roomHasActiveCall) {
// The call was cancelled
timedOutCallJob?.cancel()
incomingCallTimedOut(displayMissedCallNotification = true)
val notificationData = (activeCall.value?.callState as? CallState.Ringing)?.notificationData
removeCurrentCall()
if (notificationData != null) {
displayMissedCallNotification(notificationData)
}
} else if (userIsInTheCall) {
// The user joined the call from another session
timedOutCallJob?.cancel()
incomingCallTimedOut(displayMissedCallNotification = false)
removeCurrentCall()
}
}
.launchIn(coroutineScope)

View File

@@ -44,6 +44,13 @@ class WebViewAudioManager(
private val coroutineScope: CoroutineScope,
private val onInvalidAudioDeviceAdded: (InvalidAudioDeviceReason) -> Unit,
) {
private val json by lazy {
Json {
encodeDefaults = true
explicitNulls = false
}
}
/**
* Whether to disable bluetooth audio devices. This must be done on Android versions lower than Android 12,
* since the WebView approach breaks when using the legacy Bluetooth audio APIs.
@@ -308,11 +315,7 @@ class WebViewAudioManager(
devices: List<SerializableAudioDevice> = listAudioDevices().map(SerializableAudioDevice::fromAudioDeviceInfo),
) {
Timber.d("Updating available audio devices")
val jsonSerializer = Json {
encodeDefaults = true
explicitNulls = false
}
val deviceList = jsonSerializer.encodeToString(devices)
val deviceList = json.encodeToString(devices)
webView.evaluateJavascript("controls.setAvailableOutputDevices($deviceList);", {
Timber.d("Audio: setAvailableOutputDevices result: $it")
})

View File

@@ -7,18 +7,20 @@
package io.element.android.features.call.impl.utils
import dev.zacsweers.metro.Inject
import io.element.android.features.call.impl.data.WidgetMessage
import io.element.android.libraries.androidutils.json.JsonProvider
import io.element.android.libraries.core.extensions.runCatchingExceptions
import kotlinx.serialization.json.Json
object WidgetMessageSerializer {
private val coder = Json { ignoreUnknownKeys = true }
@Inject
class WidgetMessageSerializer(
private val json: JsonProvider,
) {
fun deserialize(message: String): Result<WidgetMessage> {
return runCatchingExceptions { coder.decodeFromString(WidgetMessage.serializer(), message) }
return runCatchingExceptions { json().decodeFromString(WidgetMessage.serializer(), message) }
}
fun serialize(message: WidgetMessage): String {
return coder.encodeToString(WidgetMessage.serializer(), message)
return json().encodeToString(WidgetMessage.serializer(), message)
}
}

View File

@@ -16,9 +16,11 @@ import io.element.android.features.call.api.CallType
import io.element.android.features.call.impl.ui.CallScreenEvents
import io.element.android.features.call.impl.ui.CallScreenNavigator
import io.element.android.features.call.impl.ui.CallScreenPresenter
import io.element.android.features.call.impl.utils.WidgetMessageSerializer
import io.element.android.features.call.utils.FakeActiveCallManager
import io.element.android.features.call.utils.FakeCallWidgetProvider
import io.element.android.features.call.utils.FakeWidgetMessageInterceptor
import io.element.android.libraries.androidutils.json.DefaultJsonProvider
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.sync.SyncState
@@ -50,7 +52,8 @@ import org.junit.Rule
import org.junit.Test
import kotlin.time.Duration.Companion.seconds
@OptIn(ExperimentalCoroutinesApi::class) class CallScreenPresenterTest {
@OptIn(ExperimentalCoroutinesApi::class)
class CallScreenPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@@ -409,6 +412,7 @@ import kotlin.time.Duration.Companion.seconds
languageTagProvider = FakeLanguageTagProvider("en-US"),
appForegroundStateService = appForegroundStateService,
appCoroutineScope = backgroundScope,
widgetMessageSerializer = WidgetMessageSerializer(DefaultJsonProvider()),
)
}
}

View File

@@ -28,6 +28,7 @@ import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
@@ -46,6 +47,7 @@ import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.plantTestTimber
import io.mockk.coVerify
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
@@ -331,6 +333,49 @@ class DefaultActiveCallManagerTest {
assertThat(manager.activeCall.value).isNull()
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `observeRingingCalls - declining won't do anything if the call was already cancelled`() = runTest {
val room = FakeBaseRoom().apply {
givenRoomInfo(aRoomInfo())
}
val client = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, room)
}
val matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(client) })
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
val manager = spyk<DefaultActiveCallManager>(
createActiveCallManager(
matrixClientProvider = matrixClientProvider,
notificationManagerCompat = notificationManagerCompat,
)
)
manager.registerIncomingCall(aCallNotificationData())
// Call is active (the other user join the call)
room.givenRoomInfo(aRoomInfo(hasRoomCall = true))
advanceTimeBy(1)
// Call is cancelled by us, hanging up
manager.hungUpCall(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID))
advanceTimeBy(1)
verify(exactly = 1) { notificationManagerCompat.cancel(any()) }
verify(exactly = 1) { manager.removeCurrentCall() }
assertThat(manager.activeCall.value).isNull()
assertThat(manager.activeWakeLock?.isHeld).isNull()
// Simulate that another user declined the call
room.givenDecliner(A_USER_ID_2, AN_EVENT_ID)
advanceTimeBy(1)
// Check everything stays the same, no extra call to cancelling notifications
verify(exactly = 1) { notificationManagerCompat.cancel(any()) }
verify(exactly = 1) { manager.removeCurrentCall() }
assertThat(manager.activeWakeLock?.isHeld).isNull()
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `observeRingingCalls - will do nothing if either the session or the room are not found`() = runTest {

View File

@@ -17,22 +17,22 @@
<string name="screen_room_change_role_administrators_title">"ویرایش مدیران"</string>
<string name="screen_room_change_role_confirm_add_admin_description">"قادر نخواهید بود این کنش را بازکردانید. داردید کاربر را به سطح قدرت خودتان ارتقا می‌دهید."</string>
<string name="screen_room_change_role_confirm_add_admin_title">"افزودن مدیر؟"</string>
<string name="screen_room_change_role_confirm_change_owners_title">"انتقال مالکیت؟"</string>
<string name="screen_room_change_role_confirm_demote_self_action">"تنزل بده"</string>
<string name="screen_room_change_role_confirm_demote_self_description">"شما نمی‌توانید این تغییر را بازگردانید زیرا در حال تنزل نقش خود در اتاق هستید، اگر آخرین کاربر ممتاز در اتاق باشید، امکان دستیابی مجدد به دسترسی‌های سطح بالای اتاق غیرممکن است."</string>
<string name="screen_room_change_role_confirm_demote_self_title">"تنزل نقش شما در اتاق؟"</string>
<string name="screen_room_change_role_invited_member_name">"%1$s (منتظر)"</string>
<string name="screen_room_change_role_invited_member_name_android">"(منتظر)"</string>
<string name="screen_room_change_role_moderators_admin_section_footer">"مدیران به صورت خودکار اجازه‌های نظارتی را دارند"</string>
<string name="screen_room_change_role_moderators_owner_section_footer">"ماکان به صورت خودکار اجازه‌های مدیریتی را دارند."</string>
<string name="screen_room_change_role_moderators_title">"ویرایش ناظران"</string>
<string name="screen_room_change_role_owners_title">"گزینش مالکان"</string>
<string name="screen_room_change_role_section_administrators">"مدیران"</string>
<string name="screen_room_change_role_section_moderators">"ناظم‌ها"</string>
<string name="screen_room_change_role_section_users">"اعضا"</string>
<string name="screen_room_change_role_unsaved_changes_description">"تغییراتی ذخیره نشده دارید."</string>
<string name="screen_room_change_role_unsaved_changes_title">"ذخیرهٔ تغییرات؟"</string>
<plurals name="screen_room_member_list_header_title">
<item quantity="one">"%1$d نفر"</item>
<item quantity="other">"%1$d نفر"</item>
</plurals>
<string name="screen_room_member_list_banned_empty">"هیچ کاربر محرومی در این اتاق نیست."</string>
<string name="screen_room_member_list_manage_member_remove_confirmation_ban">"برداشت و تحریم عضو"</string>
<string name="screen_room_member_list_manage_member_remove_confirmation_kick">"تنها برداشتن عضو"</string>
<string name="screen_room_member_list_manage_member_unban_action">"رفع انسداد"</string>
@@ -47,12 +47,14 @@
<string name="screen_room_member_list_room_members_header_title">"اعضای اتاق"</string>
<string name="screen_room_member_list_unbanning_user">"رفع تحریم %1$s"</string>
<string name="screen_room_roles_and_permissions_admins">"مدیران"</string>
<string name="screen_room_roles_and_permissions_admins_and_owners">"مدیران و مالکان"</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>
<string name="screen_room_roles_and_permissions_change_role_demote_to_moderator">"تنزّل به ناظم"</string>
<string name="screen_room_roles_and_permissions_member_moderation">"نظارت اعضا"</string>
<string name="screen_room_roles_and_permissions_messages_and_content">"پیام‌ها و محتوا"</string>
<string name="screen_room_roles_and_permissions_moderators">"ناظم‌ها"</string>
<string name="screen_room_roles_and_permissions_owners">"مالکان"</string>
<string name="screen_room_roles_and_permissions_permissions_header">"اجازه‌ها"</string>
<string name="screen_room_roles_and_permissions_reset">"بازنشانی اجازه‌ها"</string>
<string name="screen_room_roles_and_permissions_reset_confirm_title">"بازنشانی اجازه‌ها؟"</string>

View File

@@ -2,7 +2,7 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_room_change_permissions_administrators">"Tylko administratorzy"</string>
<string name="screen_room_change_permissions_ban_people">"Banowanie osób"</string>
<string name="screen_room_change_permissions_delete_messages">"Usuwanie wiadomości"</string>
<string name="screen_room_change_permissions_delete_messages">"Usuń wiadomości"</string>
<string name="screen_room_change_permissions_everyone">"Wszyscy"</string>
<string name="screen_room_change_permissions_invite_people">"Zapraszanie osób i akceptowanie próśb o dołączenie"</string>
<string name="screen_room_change_permissions_member_moderation">"Moderacja członków"</string>

View File

@@ -17,6 +17,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.core.net.toUri
import dev.zacsweers.metro.Inject
import im.vector.app.features.analytics.plan.CreatedRoom
import io.element.android.libraries.architecture.AsyncAction
@@ -157,7 +158,7 @@ class ConfigureRoomPresenter(
createRoomAction: MutableState<AsyncAction<RoomId>>
) = launch {
suspend {
val avatarUrl = config.avatarUri?.let { uploadAvatar(it) }
val avatarUrl = config.avatarUri?.let { uploadAvatar(it.toUri()) }
val params = if (config.roomVisibility is RoomVisibilityState.Public) {
CreateRoomParameters(
name = config.roomName,

View File

@@ -7,7 +7,6 @@
package io.element.android.features.createroom.impl.configureroom
import android.net.Uri
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -192,7 +191,7 @@ private fun ConfigureRoomToolbar(
@Composable
private fun RoomNameWithAvatar(
avatarUri: Uri?,
avatarUri: String?,
roomName: String,
onAvatarClick: () -> Unit,
onChangeRoomName: (String) -> Unit,

View File

@@ -7,7 +7,6 @@
package io.element.android.features.createroom.impl.configureroom
import android.net.Uri
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@@ -15,7 +14,7 @@ import kotlinx.collections.immutable.persistentListOf
data class CreateRoomConfig(
val roomName: String? = null,
val topic: String? = null,
val avatarUri: Uri? = null,
val avatarUri: String? = null,
val invites: ImmutableList<MatrixUser> = persistentListOf(),
val roomVisibility: RoomVisibilityState = RoomVisibilityState.Private,
)

View File

@@ -62,7 +62,7 @@ class CreateRoomConfigStore(
fun setAvatarUri(uri: Uri?, cached: Boolean = false) {
cachedAvatarUri = uri.takeIf { cached }
createRoomConfigFlow.getAndUpdate { config ->
config.copy(avatarUri = uri)
config.copy(avatarUri = uri?.toString())
}
}

View File

@@ -8,6 +8,7 @@
package io.element.android.features.startchat.impl.configureroom
import android.net.Uri
import androidx.core.net.toUri
import app.cash.turbine.TurbineTestContext
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.CreatedRoom
@@ -155,15 +156,15 @@ class ConfigureRoomPresenterTest {
// Pick avatar
pickerProvider.givenResult(null)
// From gallery
val uriFromGallery = Uri.parse(AN_URI_FROM_GALLERY)
pickerProvider.givenResult(uriFromGallery)
val uriFromGallery = AN_URI_FROM_GALLERY
pickerProvider.givenResult(uriFromGallery.toUri())
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
newState = awaitItem()
expectedConfig = expectedConfig.copy(avatarUri = uriFromGallery)
assertThat(newState.config).isEqualTo(expectedConfig)
// From camera
val uriFromCamera = Uri.parse(AN_URI_FROM_CAMERA)
pickerProvider.givenResult(uriFromCamera)
val uriFromCamera = AN_URI_FROM_CAMERA
pickerProvider.givenResult(uriFromCamera.toUri())
assertThat(newState.cameraPermissionState.permissionGranted).isFalse()
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto))
newState = awaitItem()
@@ -175,8 +176,8 @@ class ConfigureRoomPresenterTest {
expectedConfig = expectedConfig.copy(avatarUri = uriFromCamera)
assertThat(newState.config).isEqualTo(expectedConfig)
// Do it again, no permission is requested
val uriFromCamera2 = Uri.parse(AN_URI_FROM_CAMERA_2)
pickerProvider.givenResult(uriFromCamera2)
val uriFromCamera2 = AN_URI_FROM_CAMERA_2
pickerProvider.givenResult(uriFromCamera2.toUri())
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto))
newState = awaitItem()
expectedConfig = expectedConfig.copy(avatarUri = uriFromCamera2)

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_deactivate_account_list_item_3">"Delete your account information from our server."</string>
</resources>

View File

@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
plugins {
id("io.element.android-library")
id("io.element.android-compose-library")
}
android {

View File

@@ -7,6 +7,8 @@
package io.element.android.features.enterprise.api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import io.element.android.compound.tokens.generated.SemanticColors
import io.element.android.libraries.matrix.api.core.SessionId
import kotlinx.coroutines.flow.Flow
@@ -17,8 +19,17 @@ interface EnterpriseService {
fun defaultHomeserverList(): List<String>
suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean
fun semanticColorsLight(): SemanticColors
fun semanticColorsDark(): SemanticColors
/**
* Override the brand color.
* @param brandColor the color in hex format (#RRGGBBAA or #RRGGBB), or null to reset to default.
*/
fun overrideBrandColor(brandColor: String?)
@Composable
fun semanticColorsLight(): State<SemanticColors>
@Composable
fun semanticColorsDark(): State<SemanticColors>
fun firebasePushGateway(): String?
fun unifiedPushDefaultPushGateway(): String?

View File

@@ -8,7 +8,7 @@ import extension.testCommonDependencies
* Please see LICENSE files in the repository root for full details.
*/
plugins {
id("io.element.android-library")
id("io.element.android-compose-library")
}
android {

View File

@@ -7,6 +7,10 @@
package io.element.android.features.enterprise.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.remember
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
@@ -28,9 +32,17 @@ class DefaultEnterpriseService : EnterpriseService {
override fun defaultHomeserverList(): List<String> = emptyList()
override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true
override fun semanticColorsLight(): SemanticColors = compoundColorsLight
override fun overrideBrandColor(brandColor: String?) = Unit
override fun semanticColorsDark(): SemanticColors = compoundColorsDark
@Composable
override fun semanticColorsLight(): State<SemanticColors> {
return remember { derivedStateOf { compoundColorsLight } }
}
@Composable
override fun semanticColorsDark(): State<SemanticColors> {
return remember { derivedStateOf { compoundColorsDark } }
}
override fun firebasePushGateway(): String? = null
override fun unifiedPushDefaultPushGateway(): String? = null

View File

@@ -7,7 +7,12 @@
package io.element.android.features.enterprise.impl
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.compound.tokens.generated.compoundColorsDark
import io.element.android.compound.tokens.generated.compoundColorsLight
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
import io.element.android.libraries.matrix.test.A_SESSION_ID
import kotlinx.coroutines.test.runTest
@@ -37,4 +42,30 @@ class DefaultEnterpriseServiceTest {
val defaultEnterpriseService = DefaultEnterpriseService()
assertThat(defaultEnterpriseService.isEnterpriseUser(A_SESSION_ID)).isFalse()
}
@Test
fun `semanticColorsLight always emits the same value`() = runTest {
val defaultEnterpriseService = DefaultEnterpriseService()
moleculeFlow(RecompositionMode.Immediate) {
defaultEnterpriseService.semanticColorsLight().value
}.test {
val initialState = awaitItem()
assertThat(initialState).isEqualTo(compoundColorsLight)
defaultEnterpriseService.overrideBrandColor("#87654321")
expectNoEvents()
}
}
@Test
fun `semanticColorsDark always emits the same value`() = runTest {
val defaultEnterpriseService = DefaultEnterpriseService()
moleculeFlow(RecompositionMode.Immediate) {
defaultEnterpriseService.semanticColorsDark().value
}.test {
val initialState = awaitItem()
assertThat(initialState).isEqualTo(compoundColorsDark)
defaultEnterpriseService.overrideBrandColor("#87654321")
expectNoEvents()
}
}
}

View File

@@ -7,6 +7,8 @@
package io.element.android.features.enterprise.test
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import io.element.android.compound.tokens.generated.SemanticColors
import io.element.android.features.enterprise.api.BugReportUrl
import io.element.android.features.enterprise.api.EnterpriseService
@@ -22,8 +24,9 @@ class FakeEnterpriseService(
private val isEnterpriseUserResult: (SessionId) -> Boolean = { lambdaError() },
private val defaultHomeserverListResult: () -> List<String> = { emptyList() },
private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() },
private val semanticColorsLightResult: () -> SemanticColors = { lambdaError() },
private val semanticColorsDarkResult: () -> SemanticColors = { lambdaError() },
private val semanticColorsLightResult: () -> State<SemanticColors> = { lambdaError() },
private val semanticColorsDarkResult: () -> State<SemanticColors> = { lambdaError() },
private val overrideBrandColorResult: (String?) -> Unit = { lambdaError() },
private val firebasePushGatewayResult: () -> String? = { lambdaError() },
private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() },
) : EnterpriseService {
@@ -39,11 +42,17 @@ class FakeEnterpriseService(
isAllowedToConnectToHomeserverResult(homeserverUrl)
}
override fun semanticColorsLight(): SemanticColors {
override fun overrideBrandColor(brandColor: String?) {
overrideBrandColorResult(brandColor)
}
@Composable
override fun semanticColorsLight(): State<SemanticColors> {
return semanticColorsLightResult()
}
override fun semanticColorsDark(): SemanticColors {
@Composable
override fun semanticColorsDark(): State<SemanticColors> {
return semanticColorsDarkResult()
}

View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_create_new_recovery_key">"Create a new backup password"</string>
<string name="screen_identity_confirmation_subtitle">"Confirm this device to set up secure messaging."</string>
<string name="screen_identity_confirmation_title">"Confirm it\'s you"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Use backup password"</string>
<string name="screen_identity_confirmed_title">"Device confirmed"</string>
<string name="screen_session_verification_enter_recovery_key">"Enter backup password"</string>
</resources>

View File

@@ -7,7 +7,6 @@
package io.element.android.features.home.impl
import androidx.compose.runtime.Immutable
import io.element.android.features.home.impl.roomlist.RoomListState
import io.element.android.features.home.impl.spaces.HomeSpacesState
import io.element.android.features.logout.api.direct.DirectLogoutState
@@ -15,7 +14,6 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.ImmutableList
@Immutable
data class HomeState(
/**
* The current user of this session, in case of multiple accounts, will contains 3 items, with the

View File

@@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
@@ -34,6 +35,7 @@ import androidx.compose.runtime.setValue
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.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
@@ -48,6 +50,9 @@ import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
/**
* Ref: https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=2191-606
*/
@Composable
fun RoomListFiltersView(
state: RoomListFiltersState,
@@ -143,9 +148,12 @@ private fun RoomListClearFiltersButton(
.clip(CircleShape)
.background(ElementTheme.colors.bgActionPrimaryRest)
.clickable(onClick = onClick)
.padding(4.dp)
) {
Icon(
modifier = Modifier.align(Alignment.Center),
modifier = Modifier
.align(Alignment.Center)
.size(16.dp),
imageVector = CompoundIcons.Close(),
tint = ElementTheme.colors.iconOnSolidPrimary,
contentDescription = stringResource(id = R.string.screen_roomlist_clear_filters),
@@ -170,21 +178,34 @@ private fun RoomListFilterView(
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
label = "chip text colour",
)
val borderColour = animateColorAsState(
targetValue = if (selected) Color.Transparent else ElementTheme.colors.borderInteractiveSecondary,
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
label = "chip border colour",
)
FilterChip(
selected = selected,
onClick = { onClick(roomListFilter) },
modifier = modifier.height(36.dp),
modifier = modifier.height(32.dp),
shape = CircleShape,
colors = FilterChipDefaults.filterChipColors(
containerColor = background.value,
selectedContainerColor = background.value,
labelColor = textColour.value,
selectedLabelColor = textColour.value
selectedLabelColor = textColour.value,
),
label = {
Text(text = stringResource(id = roomListFilter.stringResource))
}
Text(
text = stringResource(id = roomListFilter.stringResource),
style = ElementTheme.typography.fontBodyMdRegular,
)
},
border = FilterChipDefaults.filterChipBorder(
enabled = true,
selected = selected,
borderColor = borderColour.value,
),
)
}
@@ -192,6 +213,7 @@ private fun RoomListFilterView(
@Composable
internal fun RoomListFiltersViewPreview(@PreviewParameter(RoomListFiltersStateProvider::class) state: RoomListFiltersState) = ElementPreview {
RoomListFiltersView(
modifier = Modifier.padding(vertical = 4.dp),
state = state,
)
}

View File

@@ -19,7 +19,6 @@ import io.element.android.libraries.push.api.battery.BatteryOptimizationState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet
@Immutable
data class RoomListState(
val contextMenu: ContextMenu,
val declineInviteMenu: DeclineInviteMenu,

View File

@@ -24,6 +24,7 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
@@ -33,6 +34,8 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.tokens.generated.CompoundIcons
@@ -41,7 +44,6 @@ import io.element.android.features.home.impl.contentType
import io.element.android.features.home.impl.model.RoomListRoomSummary
import io.element.android.features.home.impl.roomlist.RoomListEvents
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
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.FilledTextField
@@ -111,18 +113,18 @@ private fun RoomListSearchContent(
},
navigationIcon = { BackButton(onClick = ::onBackButtonClick) },
title = {
var filter by textFieldState(state.query)
var value by remember { mutableStateOf(TextFieldValue(state.query)) }
val focusRequester = remember { FocusRequester() }
FilledTextField(
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
value = filter,
value = value,
singleLine = true,
onValueChange = {
filter = it
state.eventSink(RoomListSearchEvents.QueryChanged(it))
value = it
state.eventSink(RoomListSearchEvents.QueryChanged(it.text))
},
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
@@ -134,7 +136,7 @@ private fun RoomListSearchContent(
errorIndicatorColor = Color.Transparent,
),
trailingIcon = {
if (filter.isNotEmpty()) {
if (value.text.isNotEmpty()) {
IconButton(onClick = {
state.eventSink(RoomListSearchEvents.ClearQuery)
}) {
@@ -148,7 +150,11 @@ private fun RoomListSearchContent(
)
LaunchedEffect(Unit) {
focusRequester.requestFocus()
value = value.copy(selection = TextRange(value.text.length))
if (!focusRequester.restoreFocusedChild()) {
focusRequester.requestFocus()
}
focusRequester.saveFocusedChild()
}
},
windowInsets = TopAppBarDefaults.windowInsets.copy(top = 0)

View File

@@ -16,7 +16,9 @@ import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.flow.map
@@ -28,7 +30,10 @@ class HomeSpacesPresenter(
@Composable
override fun present(): HomeSpacesState {
val hideInvitesAvatar by client.rememberHideInvitesAvatar()
val spaceRooms by client.spaceService.spaceRoomsFlow.collectAsState(emptyList())
val spaceRooms by remember {
client.spaceService.spaceRoomsFlow.map { it.toImmutableList() }
}.collectAsState(persistentListOf())
val seenSpaceInvites by remember {
seenInvitesStore.seenRoomIds().map { it.toImmutableSet() }
}.collectAsState(persistentSetOf())

View File

@@ -9,11 +9,12 @@ package io.element.android.features.home.impl.spaces
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet
data class HomeSpacesState(
val space: CurrentSpace,
val spaceRooms: List<SpaceRoom>,
val spaceRooms: ImmutableList<SpaceRoom>,
val seenSpaceInvites: ImmutableSet<RoomId>,
val hideInvitesAvatar: Boolean,
val eventSink: (HomeSpacesEvents) -> Unit,

View File

@@ -11,6 +11,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.previewutils.room.aSpaceRoom
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet
open class HomeSpacesStateProvider : PreviewParameterProvider<HomeSpacesState> {
@@ -39,7 +40,7 @@ internal fun aHomeSpacesState(
eventSink: (HomeSpacesEvents) -> Unit = {},
) = HomeSpacesState(
space = space,
spaceRooms = spaceRooms,
spaceRooms = spaceRooms.toImmutableList(),
seenSpaceInvites = seenSpaceInvites.toImmutableSet(),
hideInvitesAvatar = hideInvitesAvatar,
eventSink = eventSink,

View File

@@ -14,6 +14,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.ui.components.SpaceHeaderRootView
@@ -47,6 +48,9 @@ fun HomeSpacesView(
)
}
}
item {
HorizontalDivider()
}
state.spaceRooms.forEach { spaceRoom ->
item(spaceRoom.roomId) {
val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED

View File

@@ -15,7 +15,7 @@
<string name="full_screen_intent_banner_message">"For at sikre, at du aldrig går glip af et vigtigt opkald, skal du ændre dine indstillinger til at tillade underretninger i fuld skærm, når din telefon er låst."</string>
<string name="full_screen_intent_banner_title">"Gør din opkaldsoplevelse bedre"</string>
<string name="screen_home_tab_chats">"Samtaler"</string>
<string name="screen_home_tab_spaces">"Klynger"</string>
<string name="screen_home_tab_spaces">"Grupper"</string>
<string name="screen_invites_decline_chat_message">"Er du sikker på, at du vil afvise invitationen til at deltage i %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Afvis invitation"</string>
<string name="screen_invites_decline_direct_chat_message">"Er du sikker på, at du vil afvise denne private samtale med %1$s?"</string>

View File

@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_set_up_recovery_content">"Restore your account security and message history with a backup password if you have lost all your existing devices."</string>
<string name="banner_set_up_recovery_submit">"Set up backup"</string>
<string name="banner_set_up_recovery_title">"Set up backup to protect your account"</string>
<string name="confirm_recovery_key_banner_message">"Confirm your backup password to maintain access to your message backup and message history."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Enter your backup password"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"Forgot your backup password?"</string>
<string name="confirm_recovery_key_banner_title">"Your message backup is out of sync"</string>
<string name="session_verification_banner_message">"Looks like you\'re using a new device. Confirm it with another linked device to access your encrypted messages."</string>
</resources>

View File

@@ -3,6 +3,8 @@
<string name="banner_battery_optimization_content_android">"Kui tahad olla kindel, et näed õigel ajal kõiki teavitusi, siis palun lülita akukasutuse optimeerimine välja."</string>
<string name="banner_battery_optimization_submit_android">"Lülita akukasutuse optimeerimine välja"</string>
<string name="banner_battery_optimization_title_android">"Sa ei näe kõiki teavitusi?"</string>
<string name="banner_new_sound_message">"Sinu nutiseadme teavituste heli on uuenenud - see on nüüd selgem, kiirem ja vähem häiriv."</string>
<string name="banner_new_sound_title">"Oleme sinu helisid värskendanud"</string>
<string name="banner_set_up_recovery_content">"Loo uus taastevõti, mida saad kasutada oma krüptitud sõnumite ajaloo taastamisel olukorras, kus kaotad ligipääsu oma seadmetele."</string>
<string name="banner_set_up_recovery_submit">"Seadista andmete taastamine"</string>
<string name="banner_set_up_recovery_title">"Seadista taastamine"</string>

View File

@@ -10,6 +10,7 @@
<string name="confirm_recovery_key_banner_title">"ذخیره‌ساز کلیدتان از هم‌گام بودن در آمده"</string>
<string name="full_screen_intent_banner_title">"بهبود تجریهٔ تماستان"</string>
<string name="screen_home_tab_chats">"گپ‌ها"</string>
<string name="screen_home_tab_spaces">"فضاها"</string>
<string name="screen_invites_decline_chat_message">"مطمئنید که می‌خواهید دعوت پیوستن به %1$s را رد کنید؟"</string>
<string name="screen_invites_decline_chat_title">"رد دعوت"</string>
<string name="screen_invites_decline_direct_chat_message">"مطمئنید که می‌خواهید این گپ خصوصی با %1$s را رد کنید؟"</string>
@@ -19,6 +20,7 @@
<string name="screen_migration_message">"فرایندی یک باره است. ممنون از شکیباییتان."</string>
<string name="screen_migration_title">"برپایی حسابتان."</string>
<string name="screen_roomlist_a11y_create_message">"ایجاد اتاق یا گفت‌وگویی جدید"</string>
<string name="screen_roomlist_clear_filters">"پاک کردن پالایه‌ها"</string>
<string name="screen_roomlist_empty_message">"آغاز با پیام دادن به کسی."</string>
<string name="screen_roomlist_empty_title">"هنوز گپی وجود ندارد."</string>
<string name="screen_roomlist_filter_favourites">"علاقه‌مندی‌ها"</string>
@@ -39,6 +41,7 @@
<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_tombstoned_room_description">"این اتاق ارتقا یافته"</string>
<string name="session_verification_banner_message">"گویا از افزاره‌ای جدید استفاده می‌کنید. تأیید با افزاره‌ای دیگر برای دسترسی به پیام‌های رمزنگاری شده‌تان."</string>
<string name="session_verification_banner_title">"تأیید کنید که خودتانید"</string>
</resources>

View File

@@ -3,6 +3,7 @@
<string name="banner_battery_optimization_content_android">"Kapcsolja ki az alkalmazás akkumulátoroptimalizálását, hogy biztosan megkapja az összes értesítést."</string>
<string name="banner_battery_optimization_submit_android">"Optimalizálás letiltása"</string>
<string name="banner_battery_optimization_title_android">"Nem érkeznek meg az értesítések?"</string>
<string name="banner_new_sound_message">"Értesítési hangja frissült tisztább, gyorsabb és kevésbé zavaró lett."</string>
<string name="banner_set_up_recovery_content">"Hozzon létre egy új helyreállítási kulcsot, amellyel visszaállíthatja a titkosított üzenetek előzményeit, ha elveszíti az eszközökhöz való hozzáférést."</string>
<string name="banner_set_up_recovery_submit">"Helyreállítás beállítása"</string>
<string name="banner_set_up_recovery_title">"Helyreállítás beállítása a fiókja védelméhez"</string>

View File

@@ -3,6 +3,8 @@
<string name="banner_battery_optimization_content_android">"Wyłącz optymalizację baterii dla tej aplikacji, aby upewnić się, że wszystkie powiadomienia są odbierane."</string>
<string name="banner_battery_optimization_submit_android">"Wyłącz optymalizację"</string>
<string name="banner_battery_optimization_title_android">"Powiadomienia nie dochodzą?"</string>
<string name="banner_new_sound_message">"Sygnał powiadomień został zaktualizowany — jest wyraźniejszy, szybszy i mniej uciążliwy."</string>
<string name="banner_new_sound_title">"Odświeżyliśmy Twoje dźwięki"</string>
<string name="banner_set_up_recovery_content">"Wygeneruj nowy klucz przywracania, którego można użyć do przywrócenia historii wiadomości szyfrowanych w przypadku utraty dostępu do swoich urządzeń."</string>
<string name="banner_set_up_recovery_submit">"Skonfiguruj przywracanie"</string>
<string name="banner_set_up_recovery_title">"Skonfiguruj przywracanie"</string>
@@ -13,6 +15,7 @@
<string name="full_screen_intent_banner_message">"Upewnij się, że nie pominiesz żadnego połączenia. Zmień swoje ustawienia i zezwól na powiadomienia na blokadzie ekranu."</string>
<string name="full_screen_intent_banner_title">"Popraw jakość swoich rozmów"</string>
<string name="screen_home_tab_chats">"Wszystkie czaty"</string>
<string name="screen_home_tab_spaces">"Przestrzenie"</string>
<string name="screen_invites_decline_chat_message">"Czy na pewno chcesz odrzucić zaproszenie dołączenia do %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Odrzuć zaproszenie"</string>
<string name="screen_invites_decline_direct_chat_message">"Czy na pewno chcesz odrzucić rozmowę prywatną z %1$s?"</string>
@@ -32,6 +35,7 @@ Na razie możesz wyczyścić filtry, aby zobaczyć pozostałe czaty"</string>
<string name="screen_roomlist_filter_invites">"Zaproszenia"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"Nie masz żadnych oczekujących zaproszeń."</string>
<string name="screen_roomlist_filter_low_priority">"Niski priorytet"</string>
<string name="screen_roomlist_filter_low_priority_empty_state_title">"Nie masz jeszcze żadnych czatów o niskim priorytecie"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Wyczyść filtry, aby zobaczyć pozostałe czaty"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"Brak czatów dla podanych kryteriów"</string>
<string name="screen_roomlist_filter_people">"Osoby"</string>

View File

@@ -3,6 +3,8 @@
<string name="banner_battery_optimization_content_android">"停用此應用程式的電池最佳化,才能確保收到所有通知。"</string>
<string name="banner_battery_optimization_submit_android">"停用最佳化"</string>
<string name="banner_battery_optimization_title_android">"沒收到通知?"</string>
<string name="banner_new_sound_message">"您的通知提示音已更新,更清晰、更快、更不易分心。"</string>
<string name="banner_new_sound_title">"我們已更新您的音效設定"</string>
<string name="banner_set_up_recovery_content">"若您遺失了所有現有裝置,則請使用復原金鑰以救援您的密碼學身份與訊息歷史紀錄。"</string>
<string name="banner_set_up_recovery_submit">"設定復原"</string>
<string name="banner_set_up_recovery_title">"設定備援以保護您的帳號"</string>

View File

@@ -39,8 +39,6 @@ import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.exception.ClientException
import io.element.android.libraries.matrix.api.exception.ErrorKind
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.room.RoomMembershipDetails
@@ -141,11 +139,7 @@ class JoinRoomPresenter(
preview.previewInfo.toContentState(membershipDetails)
},
onFailure = { throwable ->
if (throwable is ClientException.MatrixApi && (throwable.kind == ErrorKind.NotFound || throwable.kind == ErrorKind.Forbidden)) {
ContentState.UnknownRoom
} else {
ContentState.Failure(throwable)
}
ContentState.UnknownRoom
}
)
}

View File

@@ -24,7 +24,6 @@ import kotlinx.collections.immutable.ImmutableList
internal const val MAX_KNOCK_MESSAGE_LENGTH = 500
@Immutable
data class JoinRoomState(
val roomIdOrAlias: RoomIdOrAlias,
val contentState: ContentState,

View File

@@ -17,7 +17,7 @@
<string name="screen_join_room_invite_required_message">"Du har brug for en invitation for at deltage"</string>
<string name="screen_join_room_invited_by">"Inviteret af"</string>
<string name="screen_join_room_join_action">"Deltag"</string>
<string name="screen_join_room_join_restricted_message">"Du skal muligvis være inviteret eller være medlem af en klynge for at deltage."</string>
<string name="screen_join_room_join_restricted_message">"Du skal muligvis være inviteret eller være medlem af en gruppe for at deltage."</string>
<string name="screen_join_room_knock_action">"Send anmodning om at deltage"</string>
<string name="screen_join_room_knock_message_characters_count">"Tilladte tegn %1$d af %2$d"</string>
<string name="screen_join_room_knock_message_description">"Besked (valgfrit)"</string>
@@ -25,8 +25,8 @@
<string name="screen_join_room_knock_sent_title">"Anmodning om at deltage sendt"</string>
<string name="screen_join_room_loading_alert_message">"Vi kunne ikke forhåndsvise rummet. Dette kan skyldes netværks- eller serverproblemer."</string>
<string name="screen_join_room_loading_alert_title">"Vi kunne ikke forhåndsvise rummet"</string>
<string name="screen_join_room_space_not_supported_description">"%1$s understøtter ikke klynger endnu. Du kan få adgang til klynger på nettet."</string>
<string name="screen_join_room_space_not_supported_title">"Klynger er ikke understøttet endnu"</string>
<string name="screen_join_room_space_not_supported_description">"%1$s understøtter ikke grupper endnu. Du kan få adgang til grupper på nettet."</string>
<string name="screen_join_room_space_not_supported_title">"Grupper er ikke understøttet endnu"</string>
<string name="screen_join_room_subtitle_knock">"Klik på knappen nedenfor, og en rumadministrator vil blive underrettet. Du kan deltage i samtalen, når din anmodning er godkendt."</string>
<string name="screen_join_room_subtitle_no_preview">"Du skal være medlem af dette rum for at kunne se meddelelseshistorikken."</string>
<string name="screen_join_room_title_knock">"Vil du deltage i dette rum?"</string>

View File

@@ -11,8 +11,8 @@
<string name="screen_join_room_decline_and_block_alert_message">"Kas sa oled kindel, et soovid keelduda kutsest sellesse jututuppa? Samaga kaob kasutajal %1$s võimalus sinuga suhelda ja saata sulle jututubade kutseid."</string>
<string name="screen_join_room_decline_and_block_alert_title">"Keeldu kutsest ja blokeeri"</string>
<string name="screen_join_room_decline_and_block_button_title">"Keeldu ja blokeeri"</string>
<string name="screen_join_room_fail_message">"Jututoaga liitumine ei õnnestunud."</string>
<string name="screen_join_room_fail_reason">"Ligipääs siia jututuppa on võimalik vaid kutse alusel või kehtivad siin kogukonnakohased piirangud."</string>
<string name="screen_join_room_fail_message">"Jututoaga liitumine ei õnnestunud"</string>
<string name="screen_join_room_fail_reason">"Ligipääs siia on võimalik vaid kutse alusel või siin kehtivad ligipääsupiirangud."</string>
<string name="screen_join_room_forget_action">"Unusta see jututuba"</string>
<string name="screen_join_room_invite_required_message">"Selle jututoaga liitumiseks vajad sa kutset"</string>
<string name="screen_join_room_invited_by">"Kutsuja"</string>

View File

@@ -9,10 +9,12 @@
<string name="screen_join_room_decline_and_block_alert_confirmation">"بله. رد و انسداد"</string>
<string name="screen_join_room_decline_and_block_alert_title">"رد دعوت و انسداد"</string>
<string name="screen_join_room_decline_and_block_button_title">"رد و انسداد"</string>
<string name="screen_join_room_fail_message">"پیوستن به اتاق شکست خورد."</string>
<string name="screen_join_room_fail_message">"پیوستن شکست خورد"</string>
<string name="screen_join_room_forget_action">"فراموشی این اتاق"</string>
<string name="screen_join_room_invite_required_message">"برای پیوستن به این اتاق نیاز به دعوت دارید"</string>
<string name="screen_join_room_invited_by">"دعوت شده از سوی"</string>
<string name="screen_join_room_join_action">"پیوستن"</string>
<string name="screen_join_room_join_restricted_message">"برای پیوستن به فضا باید دعوت شده باشید."</string>
<string name="screen_join_room_knock_action">"در زدن برای پیوستن"</string>
<string name="screen_join_room_knock_message_description">"پیام (اختیاری)"</string>
<string name="screen_join_room_knock_sent_title">"درخواست پیوستن فرستاده شد"</string>

View File

@@ -15,6 +15,7 @@
<string name="screen_join_room_fail_reason">"Sinun on joko saatava kutsu liittyäksesi tai pääsyyn voi olla rajoituksia."</string>
<string name="screen_join_room_forget_action">"Unohda"</string>
<string name="screen_join_room_invite_required_message">"Tarvitset kutsun liittyäksesi"</string>
<string name="screen_join_room_invited_by">"Kutsuja"</string>
<string name="screen_join_room_join_action">"Liity"</string>
<string name="screen_join_room_join_restricted_message">"Saatat tarvita kutsun tai olla tilan jäsen, jotta voit liittyä."</string>
<string name="screen_join_room_knock_action">"Lähetä liittymispyyntö"</string>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_ban_by_message">"%1$s kitiltotta a szobából."</string>
<string name="screen_join_room_ban_message">"Kitiltották ebből a szobából"</string>
<string name="screen_join_room_ban_message">"Kitiltották"</string>
<string name="screen_join_room_ban_reason">"Ok: %1$s."</string>
<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>
@@ -11,10 +11,10 @@
<string name="screen_join_room_decline_and_block_alert_message">"Biztos, hogy elutasítja a meghívást, hogy csatlakozzon ehhez a szobához? Ez azt is megakadályozza, hogy %1$s kapcsolatba lépjen Önnel, vagy szobákba hívja."</string>
<string name="screen_join_room_decline_and_block_alert_title">"Meghívó elutasítása és blokkolás"</string>
<string name="screen_join_room_decline_and_block_button_title">"Elutasítás és letiltás"</string>
<string name="screen_join_room_fail_message">"A szobához való csatlakozás sikertelen."</string>
<string name="screen_join_room_fail_reason">"Ebbe a szobába csak meghívóval vagy tértagsággal lehet belépni."</string>
<string name="screen_join_room_forget_action">"Szoba elfelejtése"</string>
<string name="screen_join_room_invite_required_message">"Meghívóra van szüksége ahhoz, hogy csatlakozzon ehhez a szobához"</string>
<string name="screen_join_room_fail_message">"A csatlakozás sikertelen"</string>
<string name="screen_join_room_fail_reason">"Csatlakozáshoz meghívóra van szükség, vagy lehet, hogy korlátozva van a hozzáférés."</string>
<string name="screen_join_room_forget_action">"Elfelejt"</string>
<string name="screen_join_room_invite_required_message">"A csatlakozáshoz meghívóra van szükség."</string>
<string name="screen_join_room_invited_by">"Meghívta:"</string>
<string name="screen_join_room_join_action">"Csatlakozás"</string>
<string name="screen_join_room_join_restricted_message">"A csatlakozáshoz meghívásra vagy tértagságra lehet szüksége."</string>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_ban_by_message">"Zostałeś zbanowany z tego pokoju przez %1$s."</string>
<string name="screen_join_room_ban_message">"Zostałeś zbanowany z tego pokoju"</string>
<string name="screen_join_room_ban_by_message">"Zostałeś zbanowany przez %1$s ."</string>
<string name="screen_join_room_ban_message">"Zostałeś zbanowany"</string>
<string name="screen_join_room_ban_reason">"Powód: %1$s."</string>
<string name="screen_join_room_cancel_knock_action">"Anuluj prośbę"</string>
<string name="screen_join_room_cancel_knock_alert_confirmation">"Tak, anuluj"</string>
@@ -11,10 +11,11 @@
<string name="screen_join_room_decline_and_block_alert_message">"Czy na pewno chcesz odrzucić zaproszenie dołączenia do tego pokoju? %1$s nie będzie mógł się również z Tobą skontaktować, ani zaprosić Cię do pokoju."</string>
<string name="screen_join_room_decline_and_block_alert_title">"Odrzuć zaproszenie i zablokuj"</string>
<string name="screen_join_room_decline_and_block_button_title">"Odrzuć i zablokuj"</string>
<string name="screen_join_room_fail_message">"Nie udało się dołączyć do pokoju."</string>
<string name="screen_join_room_fail_reason">"Ten pokój wymaga zaproszenia lub jest ograniczony z poziomu przestrzeni."</string>
<string name="screen_join_room_forget_action">"Zapomnij o tym pokoju"</string>
<string name="screen_join_room_invite_required_message">"Potrzebujesz zaproszenia, aby dołączyć do tego pokoju"</string>
<string name="screen_join_room_fail_message">"Nie udało się dołączyć do pokoju"</string>
<string name="screen_join_room_fail_reason">"Ten pokój wymaga zaproszenia lub dołączanie zostało ograniczone."</string>
<string name="screen_join_room_forget_action">"Zapomnij"</string>
<string name="screen_join_room_invite_required_message">"Aby dołączyć, potrzebujesz zaproszenia"</string>
<string name="screen_join_room_invited_by">"Zaproszony przez"</string>
<string name="screen_join_room_join_action">"Dołącz"</string>
<string name="screen_join_room_join_restricted_message">"Aby dołączyć, musisz uzyskać zaproszenie lub być członkiem danej przestrzeni."</string>
<string name="screen_join_room_knock_action">"Wyślij prośbę o dołączenie"</string>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_ban_by_message">"Foste banido desta sala por %1$s."</string>
<string name="screen_join_room_ban_message">"Foste banido desta sala."</string>
<string name="screen_join_room_ban_by_message">"Foste banido(a) por %1$s."</string>
<string name="screen_join_room_ban_message">"Foste banido(a)"</string>
<string name="screen_join_room_ban_reason">"Razão: %1$s."</string>
<string name="screen_join_room_cancel_knock_action">"Cancelar pedido"</string>
<string name="screen_join_room_cancel_knock_alert_confirmation">"Sim, cancelar"</string>
@@ -11,10 +11,10 @@
<string name="screen_join_room_decline_and_block_alert_message">"Tens a certeza de que queres recusar o convite para entrar nesta sala? Isto também evitará que %1$s te contacte ou te convide para salas."</string>
<string name="screen_join_room_decline_and_block_alert_title">"Recusar convite &amp; bloquear"</string>
<string name="screen_join_room_decline_and_block_button_title">"Recusar e bloquear"</string>
<string name="screen_join_room_fail_message">"Falha ao entrar na sala."</string>
<string name="screen_join_room_fail_reason">"A entrada nesta sala ou está limitada a convites ou a alguma configuração de espaço."</string>
<string name="screen_join_room_forget_action">"Esquecer esta sala"</string>
<string name="screen_join_room_invite_required_message">"Precisas de um convite para entrares nesta sala"</string>
<string name="screen_join_room_fail_message">"Falha ao entrar"</string>
<string name="screen_join_room_fail_reason">"A entrada pode estar limitada a convites ou pode haver uma outra limitação de acesso."</string>
<string name="screen_join_room_forget_action">"Esquecer"</string>
<string name="screen_join_room_invite_required_message">"Precisas de um convite para entrares"</string>
<string name="screen_join_room_invited_by">"Convidado por"</string>
<string name="screen_join_room_join_action">"Entrar"</string>
<string name="screen_join_room_join_restricted_message">"Podes ter que ser convidado ou pertenceres a um espaço para poderes entrar."</string>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_ban_by_message">"您被 %1$s 禁止進入此聊天室。"</string>
<string name="screen_join_room_ban_message">"您被禁止進入此聊天室"</string>
<string name="screen_join_room_ban_by_message">"您被 %1$s 禁止。"</string>
<string name="screen_join_room_ban_message">"您被禁止"</string>
<string name="screen_join_room_ban_reason">"理由:%1$s。"</string>
<string name="screen_join_room_cancel_knock_action">"取消請求"</string>
<string name="screen_join_room_cancel_knock_alert_confirmation">"是的,取消"</string>
@@ -11,10 +11,11 @@
<string name="screen_join_room_decline_and_block_alert_message">"您確定要拒絕加入此聊天室的邀請嗎?這也會防止 %1$s 聯絡您或邀請您加入聊天室。"</string>
<string name="screen_join_room_decline_and_block_alert_title">"拒絕邀請並封鎖"</string>
<string name="screen_join_room_decline_and_block_button_title">"拒絕並封鎖"</string>
<string name="screen_join_room_fail_message">"加入聊天室失敗。"</string>
<string name="screen_join_room_fail_reason">"此聊天室僅有受邀者才能入,或是在空間層級有存取限制。"</string>
<string name="screen_join_room_forget_action">"忘記此聊天室"</string>
<string name="screen_join_room_invite_required_message">"您需要獲得邀請才能加入此聊天室"</string>
<string name="screen_join_room_fail_message">"加入失敗。"</string>
<string name="screen_join_room_fail_reason">"您必須獲得邀請才能入,或者可能存在存取限制。"</string>
<string name="screen_join_room_forget_action">"忘記"</string>
<string name="screen_join_room_invite_required_message">"您需要獲得邀請才能加入"</string>
<string name="screen_join_room_invited_by">"邀請者"</string>
<string name="screen_join_room_join_action">"加入"</string>
<string name="screen_join_room_join_restricted_message">"您可能需要被邀請成為空間的成員才能加入。"</string>
<string name="screen_join_room_knock_action">"傳送加入請求"</string>

View File

@@ -1193,46 +1193,8 @@ class JoinRoomPresenterTest {
skipItems(1)
awaitItem().also { state ->
assertThat(state.contentState).isEqualTo(
ContentState.Failure(error = AN_EXCEPTION)
ContentState.UnknownRoom
)
state.eventSink(JoinRoomEvents.RetryFetchingContent)
}
skipItems(1)
awaitItem().also { state ->
assertThat(state.contentState).isEqualTo(ContentState.Loading)
}
awaitItem().also { state ->
assertThat(state.contentState).isEqualTo(
ContentState.Failure(error = AN_EXCEPTION)
)
}
}
}
@Test
fun `present - when room is not known RoomPreview is loaded with error - dismiss`() = runTest {
val client = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ ->
Result.failure(AN_EXCEPTION)
},
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
)
val presenter = createJoinRoomPresenter(
matrixClient = client
)
presenter.test {
skipItems(1)
awaitItem().also { state ->
assertThat(state.contentState).isEqualTo(
ContentState.Failure(error = AN_EXCEPTION)
)
state.eventSink(JoinRoomEvents.DismissErrorAndHideContent)
}
skipItems(1)
awaitItem().also { state ->
assertThat(state.contentState).isEqualTo(ContentState.Dismissing)
}
}
}

View File

@@ -7,6 +7,9 @@
package io.element.android.features.leaveroom.api
import androidx.compose.runtime.Immutable
@Immutable
interface LeaveRoomState {
val eventSink: (LeaveRoomEvent) -> Unit
}

View File

@@ -2,5 +2,7 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="leave_room_alert_empty_subtitle">"مطمئنید که می‌خواهید این اتاق را ترک کنید؟ تنها فرد این‌جا هستید. در صورت ترک، هیچ‌کسی از جمله خودتان در آینده نخواهد توانست به آن بپیوندد."</string>
<string name="leave_room_alert_private_subtitle">"مطمئنید که می‌خواهید این اتاق را ترک کنید؟ این اتاق عمومی نبوده قادر نخواهید بود بدون دعوت دوباره بپیوندید."</string>
<string name="leave_room_alert_select_new_owner_action">"گزینش مالکان"</string>
<string name="leave_room_alert_select_new_owner_title">"انتقال مالکیت"</string>
<string name="leave_room_alert_subtitle">"مطمئنید که می‌خواهید این اتاق را ترک کنید؟"</string>
</resources>

View File

@@ -24,14 +24,6 @@
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"پین‌ها مطابق نیستند"</string>
<string name="screen_app_lock_signout_alert_message">"برای ادامه باید دوباره وارد شده و پینی جدید ایجاد کنید"</string>
<string name="screen_app_lock_signout_alert_title">"دارید خارج می‌شوید"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"شما %1$d تلاش برای باز کردن قفل دارید"</item>
<item quantity="other">"شما %1$d تلاش برای باز کردن قفل دارید"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"پین اشتباه است. شما %1$d شانس دیگر دارید"</item>
<item quantity="other">"پین اشتباه است. شما %1$d شانس دیگر دارید"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"استفاده از زیست‌سنجی"</string>
<string name="screen_app_lock_use_pin_android">"استفاده از پین"</string>
<string name="screen_signout_in_progress_dialog_content">"خارج شدن…"</string>

View File

@@ -17,6 +17,7 @@ import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.changeserver.ChangeServerState
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.uri.ensureProtocol
import kotlinx.collections.immutable.toImmutableList
@Inject
class ChangeAccountProviderPresenter(
@@ -39,6 +40,7 @@ class ChangeAccountProviderPresenter(
isValid = true,
)
}
.toImmutableList()
}
val canSearchForAccountProviders = remember {

View File

@@ -9,10 +9,10 @@ package io.element.android.features.login.impl.screens.changeaccountprovider
import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.changeserver.ChangeServerState
import kotlinx.collections.immutable.ImmutableList
// Do not use default value, so no member get forgotten in the presenters.
data class ChangeAccountProviderState(
val accountProviders: List<AccountProvider>,
val accountProviders: ImmutableList<AccountProvider>,
val canSearchForAccountProviders: Boolean,
val changeServerState: ChangeServerState,
)

View File

@@ -12,6 +12,7 @@ import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.accountprovider.anAccountProvider
import io.element.android.features.login.impl.changeserver.ChangeServerState
import io.element.android.features.login.impl.changeserver.aChangeServerState
import kotlinx.collections.immutable.toImmutableList
open class ChangeAccountProviderStateProvider : PreviewParameterProvider<ChangeAccountProviderState> {
override val values: Sequence<ChangeAccountProviderState>
@@ -29,7 +30,7 @@ fun aChangeAccountProviderState(
canSearchForAccountProviders: Boolean = true,
changeServerState: ChangeServerState = aChangeServerState(),
) = ChangeAccountProviderState(
accountProviders = accountProviders,
accountProviders = accountProviders.toImmutableList(),
canSearchForAccountProviders = canSearchForAccountProviders,
changeServerState = changeServerState,
)

View File

@@ -21,6 +21,7 @@ import io.element.android.features.login.impl.login.LoginHelper
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.uri.ensureProtocol
import kotlinx.collections.immutable.toImmutableList
@Inject
class ChooseAccountProviderPresenter(
@@ -69,6 +70,7 @@ class ChooseAccountProviderPresenter(
isValid = true,
)
}
.toImmutableList()
}
return ChooseAccountProviderState(

View File

@@ -10,10 +10,10 @@ package io.element.android.features.login.impl.screens.chooseaccountprovider
import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.login.LoginMode
import io.element.android.libraries.architecture.AsyncData
import kotlinx.collections.immutable.ImmutableList
// Do not use default value, so no member get forgotten in the presenters.
data class ChooseAccountProviderState(
val accountProviders: List<AccountProvider>,
val accountProviders: ImmutableList<AccountProvider>,
val selectedAccountProvider: AccountProvider?,
val loginMode: AsyncData<LoginMode>,
val eventSink: (ChooseAccountProviderEvents) -> Unit,

View File

@@ -12,6 +12,7 @@ import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.accountprovider.anAccountProvider
import io.element.android.features.login.impl.login.LoginMode
import io.element.android.libraries.architecture.AsyncData
import kotlinx.collections.immutable.toImmutableList
open class ChooseAccountProviderStateProvider : PreviewParameterProvider<ChooseAccountProviderState> {
private val server1 = anAccountProvider(
@@ -70,7 +71,7 @@ fun aChooseAccountProviderState(
loginMode: AsyncData<LoginMode> = AsyncData.Uninitialized,
eventSink: (ChooseAccountProviderEvents) -> Unit = {},
) = ChooseAccountProviderState(
accountProviders = accountProviders,
accountProviders = accountProviders.toImmutableList(),
selectedAccountProvider = selectedAccountProvider,
loginMode = loginMode,
eventSink = eventSink,

View File

@@ -11,7 +11,6 @@ import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.login.LoginMode
import io.element.android.libraries.architecture.AsyncData
// Do not use default value, so no member get forgotten in the presenters.
data class ConfirmAccountProviderState(
val accountProvider: AccountProvider,
val isAccountCreation: Boolean,

View File

@@ -11,8 +11,8 @@ import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.libraries.androidutils.json.JsonProvider
import io.element.android.libraries.matrix.api.auth.external.ExternalSession
import kotlinx.serialization.json.Json
interface MessageParser {
/**
@@ -26,10 +26,10 @@ interface MessageParser {
@Inject
class DefaultMessageParser(
private val accountProviderDataSource: AccountProviderDataSource,
private val json: JsonProvider,
) : MessageParser {
override fun parse(message: String): ExternalSession {
val parser = Json { ignoreUnknownKeys = true }
val response = parser.decodeFromString(MobileRegistrationResponse.serializer(), message)
val response = json().decodeFromString(MobileRegistrationResponse.serializer(), message)
val userId = response.userId ?: error("No user ID in response")
val homeServer = response.homeServer ?: accountProviderDataSource.flow.value.url
val accessToken = response.accessToken ?: error("No access token in response")

View File

@@ -11,7 +11,6 @@ import io.element.android.features.login.impl.changeserver.ChangeServerState
import io.element.android.features.login.impl.resolver.HomeserverData
import io.element.android.libraries.architecture.AsyncData
// Do not use default value, so no member get forgotten in the presenters.
data class SearchAccountProviderState(
val userInput: String,
val userInputResult: AsyncData<List<HomeserverData>>,

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_qr_code_login_connection_note_secure_state_description">"A secure connection could not be made to the new device. Your existing linked devices are still safe and you don\'t need to worry about them."</string>
</resources>

View File

@@ -13,6 +13,7 @@
<string name="screen_change_account_provider_other">"دیگر"</string>
<string name="screen_change_account_provider_subtitle">"استفاده از فراهم کنندهٔ حسابی دیگر چون کارساز خصوصی خوتان یا حسابی کاری."</string>
<string name="screen_change_account_provider_title">"تغییر فراهم کنندهٔ حساب"</string>
<string name="screen_change_server_error_element_pro_required_action_android">"پلی گپگل"</string>
<string name="screen_change_server_error_invalid_homeserver">"ما نتوانستیم به این کارساز خانگی برسیم. لطفاً بررسی کنید که URL کارساز اصلی را به درستی وارد کرده اید. اگر URL صحیح است، برای کمک بیشتر با مدیر کارساز خانگی خود تماس بگیرید."</string>
<string name="screen_change_server_form_header">"نشانی کارساز خانگی"</string>
<string name="screen_change_server_form_notice">"ورود نشانی دامنه."</string>

View File

@@ -13,6 +13,7 @@
<string name="screen_change_account_provider_other">"Inne"</string>
<string name="screen_change_account_provider_subtitle">"Użyj innego dostawcy konta, takiego jak własny serwer lub konta służbowego."</string>
<string name="screen_change_account_provider_title">"Zmień dostawcę konta"</string>
<string name="screen_change_server_error_element_pro_required_action_android">"Google Play"</string>
<string name="screen_change_server_error_element_pro_required_message">"Wymagana jest aplikacja Element Pro na %1$s. Znajdziesz ją w sklepie z aplikacjami."</string>
<string name="screen_change_server_error_element_pro_required_title">"Wymagany jest Element Pro"</string>
<string name="screen_change_server_error_invalid_homeserver">"Nie mogliśmy połączyć się z tym serwerem domowym. Sprawdź, czy adres URL serwera został wprowadzony poprawnie. Jeśli adres URL jest poprawny, skontaktuj się z administratorem serwera w celu uzyskania dalszej pomocy."</string>

View File

@@ -11,6 +11,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.appconfig.AuthenticationConfig
import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.libraries.androidutils.json.DefaultJsonProvider
import io.element.android.libraries.matrix.api.auth.external.ExternalSession
import kotlinx.serialization.SerializationException
import org.junit.Assert.assertThrows
@@ -68,7 +69,8 @@ class DefaultMessageParserTest {
private fun createDefaultMessageParser(): DefaultMessageParser {
return DefaultMessageParser(
AccountProviderDataSource(FakeEnterpriseService())
accountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
json = DefaultJsonProvider(),
)
}
}

View File

@@ -34,10 +34,12 @@ dependencies {
implementation(projects.libraries.testtags)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.dateformatter.api)
implementation(projects.libraries.workmanager.api)
api(projects.features.logout.api)
testCommonDependencies(libs, true)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.sessionStorage.test)
testImplementation(projects.libraries.workmanager.test)
}

View File

@@ -26,6 +26,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.encryption.BackupState
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.workmanager.api.WorkManagerScheduler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -34,6 +35,7 @@ import kotlinx.coroutines.launch
class LogoutPresenter(
private val matrixClient: MatrixClient,
private val encryptionService: EncryptionService,
private val workManagerScheduler: WorkManagerScheduler,
) : Presenter<LogoutState> {
@Composable
override fun present(): LogoutState {
@@ -109,6 +111,9 @@ class LogoutPresenter(
ignoreSdkError: Boolean,
) = launch {
suspend {
// Cancel any pending work (e.g. notification sync)
workManagerScheduler.cancel(matrixClient.sessionId)
matrixClient.logout(userInitiated = true, ignoreSdkError)
}.runCatchingUpdatingState(logoutAction)
}

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_key_backup_offline_subtitle">"Your messages were still being backed up when you went offline. Reconnect so that your messages can be backed up before signing out."</string>
<string name="screen_signout_key_backup_offline_title">"Your messages are still being backed up"</string>
<string name="screen_signout_key_backup_ongoing_title">"Your messages are still being backed up"</string>
<string name="screen_signout_recovery_disabled_title">"Backup not set up"</string>
<string name="screen_signout_save_recovery_key_title">"Have you saved your backup password?"</string>
</resources>

View File

@@ -14,6 +14,7 @@ import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.encryption.BackupState
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
import io.element.android.libraries.matrix.api.encryption.EncryptionService
@@ -21,7 +22,9 @@ import io.element.android.libraries.matrix.api.encryption.RecoveryState
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.libraries.workmanager.test.FakeWorkManagerScheduler
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.test.runTest
@@ -145,7 +148,9 @@ class LogoutPresenterTest {
@Test
fun `present - logout then confirm`() = runTest {
val presenter = createLogoutPresenter()
val cancelWorkManagerJobsLambda = lambdaRecorder<SessionId, Unit> {}
val workManagerScheduler = FakeWorkManagerScheduler(cancelLambda = cancelWorkManagerJobsLambda)
val presenter = createLogoutPresenter(workManagerScheduler = workManagerScheduler)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -158,6 +163,8 @@ class LogoutPresenterTest {
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
val successState = awaitItem()
assertThat(successState.logoutAction).isInstanceOf(AsyncAction.Success::class.java)
cancelWorkManagerJobsLambda.assertions().isCalledOnce()
}
}
@@ -230,7 +237,9 @@ class LogoutPresenterTest {
internal fun createLogoutPresenter(
matrixClient: MatrixClient = FakeMatrixClient(),
encryptionService: EncryptionService = FakeEncryptionService(),
workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(cancelLambda = {}),
): LogoutPresenter = LogoutPresenter(
matrixClient = matrixClient,
encryptionService = encryptionService,
workManagerScheduler = workManagerScheduler,
)

View File

@@ -8,15 +8,14 @@
package io.element.android.features.messages.api.timeline.voicemessages.composer
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.designsystem.components.media.createFakeWaveform
import io.element.android.libraries.designsystem.components.media.WaveFormSamples
import io.element.android.libraries.textcomposer.model.VoiceMessageState
import kotlinx.collections.immutable.toImmutableList
import kotlin.time.Duration.Companion.seconds
open class VoiceMessageComposerStateProvider : PreviewParameterProvider<VoiceMessageComposerState> {
override val values: Sequence<VoiceMessageComposerState>
get() = sequenceOf(
aVoiceMessageComposerState(voiceMessageState = VoiceMessageState.Recording(duration = 61.seconds, levels = aWaveformLevels)),
aVoiceMessageComposerState(voiceMessageState = VoiceMessageState.Recording(duration = 61.seconds, levels = WaveFormSamples.allRangeWaveForm)),
)
}
@@ -39,7 +38,5 @@ fun aVoiceMessagePreviewState() = VoiceMessageState.Preview(
showCursor = false,
playbackProgress = 0f,
time = 10.seconds,
waveform = createFakeWaveform(),
waveform = WaveFormSamples.realisticWaveForm,
)
internal var aWaveformLevels = List(100) { it.toFloat() / 100 }.toImmutableList()

View File

@@ -7,7 +7,6 @@
package io.element.android.features.messages.impl
import androidx.compose.runtime.Immutable
import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerState
import io.element.android.features.messages.impl.actionlist.ActionListState
import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState
@@ -29,7 +28,6 @@ import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
import kotlinx.collections.immutable.ImmutableList
@Immutable
data class MessagesState(
val roomId: RoomId,
val roomName: String?,

View File

@@ -84,7 +84,6 @@ import io.element.android.libraries.designsystem.text.toAnnotatedString
import io.element.android.libraries.designsystem.theme.components.BottomSheetDragHandle
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.HideKeyboardWhenDisposed
import io.element.android.libraries.designsystem.utils.KeepScreenOn
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
@@ -124,8 +123,6 @@ fun MessagesView(
KeepScreenOn(state.voiceMessageComposerState.keepScreenOn)
HideKeyboardWhenDisposed()
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
// This is needed because the composer is inside an AndroidView that can't be affected by the FocusManager in Compose

View File

@@ -13,7 +13,6 @@ import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUser
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import kotlinx.collections.immutable.ImmutableList
@Immutable
data class ActionListState(
val target: Target,
val eventSink: (ActionListEvents) -> Unit,

View File

@@ -9,11 +9,9 @@ package io.element.android.features.messages.impl.actionlist.model
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.runtime.Immutable
import io.element.android.libraries.designsystem.icons.CompoundDrawables
import io.element.android.libraries.ui.strings.CommonStrings
@Immutable
enum class TimelineItemAction(
@StringRes val titleRes: Int,
@DrawableRes val icon: Int,

Some files were not shown because too many files have changed in this diff Show More