Merge branch 'release/25.10.1'

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

View file

@ -69,7 +69,7 @@ jobs:
retention-days: 5 retention-days: 5
overwrite: true overwrite: true
if-no-files-found: error 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 id: diawi
# Do not fail the whole build if Diawi upload fails # Do not fail the whole build if Diawi upload fails
continue-on-error: true continue-on-error: true

View file

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

View file

@ -37,7 +37,7 @@ jobs:
- name: Set up Python 3.12 - name: Set up Python 3.12
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
python-version: 3.13 python-version: 3.14
- name: Search for invalid screenshot files - name: Search for invalid screenshot files
run: ./tools/test/checkInvalidScreenshots.py run: ./tools/test/checkInvalidScreenshots.py
@ -58,7 +58,7 @@ jobs:
- name: Set up Python 3.12 - name: Set up Python 3.12
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
python-version: 3.13 python-version: 3.14
- name: Search for invalid dependencies - name: Search for invalid dependencies
run: ./tools/dependencies/checkDependencies.py run: ./tools/dependencies/checkDependencies.py
@ -103,6 +103,39 @@ jobs:
path: | path: |
**/build/reports/**/*.* **/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: lint:
name: Android lint check name: Android lint check
runs-on: ubuntu-latest runs-on: ubuntu-latest

View file

@ -24,7 +24,7 @@ jobs:
- name: Set up Python 3.12 - name: Set up Python 3.12
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
python-version: 3.13 python-version: 3.14
- name: Setup Localazy - name: Setup Localazy
run: | run: |
curl -sS https://dist.localazy.com/debian/pubkey.gpg | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/localazy.gpg 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 - name: Set up Python 3.12
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
python-version: 3.13 python-version: 3.14
- name: Install Prerequisite dependencies - name: Install Prerequisite dependencies
run: | run: |
pip install requests 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 Changes in Element X v25.09.2
============================= =============================

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,12 +7,10 @@
package io.element.android.appnav.root 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.crash.CrashDetectionState
import io.element.android.features.rageshake.api.detection.RageshakeDetectionState import io.element.android.features.rageshake.api.detection.RageshakeDetectionState
import io.element.android.services.apperror.api.AppErrorState import io.element.android.services.apperror.api.AppErrorState
@Immutable
data class RootState( data class RootState(
val rageshakeDetectionState: RageshakeDetectionState, val rageshakeDetectionState: RageshakeDetectionState,
val crashDetectionState: CrashDetectionState, val crashDetectionState: CrashDetectionState,

@ -1 +1 @@
Subproject commit ffc02b8d0f35188c3ef8a876dc1532bfe3e533da Subproject commit c5465c95792004409e0eaa7342171e1cd652914a

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"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_space_announcement_item1">"Se klynger, du har oprettet eller tilmeldt dig"</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 klynger"</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 klynger"</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 klynger"</string> <string name="screen_space_announcement_item4">"Deltage i offentlige grupper"</string>
<string name="screen_space_announcement_item5">"Forlade de klynger, du har tilsluttet dig"</string> <string name="screen_space_announcement_item5">"Forlade de grupper, du har tilsluttet dig"</string>
<string name="screen_space_announcement_notice">"Oprettelse og administration af klynger kommer snart."</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 Klynger! Med denne første version kan du:"</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 Klynger"</string> <string name="screen_space_announcement_title">"Introduktion til Grupper"</string>
</resources> </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_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_item4">"Öffentlichen Spaces beitreten"</string>
<string name="screen_space_announcement_item5">"Spaces verlassen, bei denen du Mitglied bist"</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_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> <string name="screen_space_announcement_title">"Einführung in Spaces"</string>
</resources> </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_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_item4">"Liittyä julkisiin tiloihin"</string>
<string name="screen_space_announcement_item5">"Poistua mistä tahansa tilasta, johon olet liittynyt"</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_subtitle">"Tervetuloa tilojen beetaversioon! Tämän ensimmäisen version avulla voit:"</string>
<string name="screen_space_announcement_title">"Esittelyssä tilat"</string> <string name="screen_space_announcement_title">"Esittelyssä tilat"</string>
</resources> </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_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_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_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_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> <string name="screen_space_announcement_title">"Ajout des espaces"</string>
</resources> </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_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_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_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_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> <string name="screen_space_announcement_title">"Vi introduserer Områder"</string>
</resources> </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, private val appForegroundStateService: AppForegroundStateService,
@AppCoroutineScope @AppCoroutineScope
private val appCoroutineScope: CoroutineScope, private val appCoroutineScope: CoroutineScope,
private val widgetMessageSerializer: WidgetMessageSerializer,
) : Presenter<CallScreenState> { ) : Presenter<CallScreenState> {
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
@ -258,7 +259,7 @@ class CallScreenPresenter(
} }
private fun parseMessage(message: String): WidgetMessage? { private fun parseMessage(message: String): WidgetMessage? {
return WidgetMessageSerializer.deserialize(message).getOrNull() return widgetMessageSerializer.deserialize(message).getOrNull()
} }
private fun sendHangupMessage(widgetId: String, messageInterceptor: WidgetMessageInterceptor) { private fun sendHangupMessage(widgetId: String, messageInterceptor: WidgetMessageInterceptor) {
@ -269,7 +270,7 @@ class CallScreenPresenter(
action = WidgetMessage.Action.HangUp, action = WidgetMessage.Action.HangUp,
data = null, data = null,
) )
messageInterceptor.sendMessage(WidgetMessageSerializer.serialize(message)) messageInterceptor.sendMessage(widgetMessageSerializer.serialize(message))
} }
private fun CoroutineScope.close(widgetDriver: MatrixWidgetDriver?, navigator: CallScreenNavigator) = launch(dispatchers.io) { 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 package io.element.android.features.call.impl.ui
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.util.Log
import android.view.ViewGroup import android.view.ViewGroup
import android.webkit.ConsoleMessage import android.webkit.ConsoleMessage
import android.webkit.JavascriptInterface import android.webkit.JavascriptInterface
@ -60,6 +59,7 @@ interface CallScreenNavigator {
internal fun CallScreenView( internal fun CallScreenView(
state: CallScreenState, state: CallScreenState,
pipState: PictureInPictureState, pipState: PictureInPictureState,
onConsoleMessage: (ConsoleMessage) -> Unit,
requestPermissions: (Array<String>, RequestPermissionCallback) -> Unit, requestPermissions: (Array<String>, RequestPermissionCallback) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@ -108,6 +108,7 @@ internal fun CallScreenView(
val callback: RequestPermissionCallback = { request.grant(it) } val callback: RequestPermissionCallback = { request.grant(it) }
requestPermissions(androidPermissions.toTypedArray(), callback) requestPermissions(androidPermissions.toTypedArray(), callback)
}, },
onConsoleMessage = onConsoleMessage,
onCreateWebView = { webView -> onCreateWebView = { webView ->
webView.addBackHandler(onBackPressed = ::handleBack) webView.addBackHandler(onBackPressed = ::handleBack)
val interceptor = WebViewWidgetMessageInterceptor( val interceptor = WebViewWidgetMessageInterceptor(
@ -174,6 +175,7 @@ private fun CallWebView(
url: AsyncData<String>, url: AsyncData<String>,
userAgent: String, userAgent: String,
onPermissionsRequest: (PermissionRequest) -> Unit, onPermissionsRequest: (PermissionRequest) -> Unit,
onConsoleMessage: (ConsoleMessage) -> Unit,
onCreateWebView: (WebView) -> Unit, onCreateWebView: (WebView) -> Unit,
onDestroyWebView: (WebView) -> Unit, onDestroyWebView: (WebView) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -188,7 +190,11 @@ private fun CallWebView(
factory = { context -> factory = { context ->
WebView(context).apply { WebView(context).apply {
onCreateWebView(this) onCreateWebView(this)
setup(userAgent, onPermissionsRequest) setup(
userAgent = userAgent,
onPermissionsRequested = onPermissionsRequest,
onConsoleMessage = onConsoleMessage,
)
} }
}, },
update = { webView -> update = { webView ->
@ -208,6 +214,7 @@ private fun CallWebView(
private fun WebView.setup( private fun WebView.setup(
userAgent: String, userAgent: String,
onPermissionsRequested: (PermissionRequest) -> Unit, onPermissionsRequested: (PermissionRequest) -> Unit,
onConsoleMessage: (ConsoleMessage) -> Unit,
) { ) {
layoutParams = ViewGroup.LayoutParams( layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
@ -232,35 +239,7 @@ private fun WebView.setup(
} }
override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean { override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
val priority = when (consoleMessage.messageLevel()) { onConsoleMessage(consoleMessage)
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())
},
)
return true return true
} }
} }
@ -286,6 +265,7 @@ internal fun CallScreenViewPreview(
state = state, state = state,
pipState = aPictureInPictureState(), pipState = aPictureInPictureState(),
requestPermissions = { _, _ -> }, 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.services.CallForegroundService
import io.element.android.features.call.impl.utils.CallIntentDataParser import io.element.android.features.call.impl.utils.CallIntentDataParser
import io.element.android.features.enterprise.api.EnterpriseService 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.Presenter
import io.element.android.libraries.architecture.bindings import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.audio.api.AudioFocus import io.element.android.libraries.audio.api.AudioFocus
@ -65,6 +66,7 @@ class ElementCallActivity :
@Inject lateinit var pictureInPicturePresenter: PictureInPicturePresenter @Inject lateinit var pictureInPicturePresenter: PictureInPicturePresenter
@Inject lateinit var buildMeta: BuildMeta @Inject lateinit var buildMeta: BuildMeta
@Inject lateinit var audioFocus: AudioFocus @Inject lateinit var audioFocus: AudioFocus
@Inject lateinit var consoleMessageLogger: ConsoleMessageLogger
private lateinit var presenter: Presenter<CallScreenState> private lateinit var presenter: Presenter<CallScreenState>
@ -119,6 +121,9 @@ class ElementCallActivity :
CallScreenView( CallScreenView(
state = state, state = state,
pipState = pipState, pipState = pipState,
onConsoleMessage = {
consoleMessageLogger.log("ElementCall", it)
},
requestPermissions = { permissions, callback -> requestPermissions = { permissions, callback ->
requestPermissionCallback = callback requestPermissionCallback = callback
requestPermissionsLauncher.launch(permissions) 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.AppCoroutineScope
import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.matrix.api.MatrixClientProvider 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.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.matrix.ui.media.ImageLoaderHolder
import io.element.android.libraries.push.api.notifications.ForegroundServiceType import io.element.android.libraries.push.api.notifications.ForegroundServiceType
import io.element.android.libraries.push.api.notifications.NotificationIdProvider import io.element.android.libraries.push.api.notifications.NotificationIdProvider
@ -39,16 +41,17 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
@ -180,13 +183,7 @@ class DefaultActiveCallManager(
val previousActiveCall = activeCall.value ?: return val previousActiveCall = activeCall.value ?: return
val notificationData = (previousActiveCall.callState as? CallState.Ringing)?.notificationData ?: return val notificationData = (previousActiveCall.callState as? CallState.Ringing)?.notificationData ?: return
activeCall.value = null removeCurrentCall()
if (activeWakeLock?.isHeld == true) {
Timber.tag(tag).d("Releasing partial wakelock after timeout")
activeWakeLock.release()
}
cancelIncomingCallNotification()
if (displayMissedCallNotification) { if (displayMissedCallNotification) {
displayMissedCallNotification(notificationData) displayMissedCallNotification(notificationData)
@ -211,24 +208,16 @@ class DefaultActiveCallManager(
?.declineCall(notificationData.eventId) ?.declineCall(notificationData.eventId)
} }
cancelIncomingCallNotification() removeCurrentCall()
if (activeWakeLock?.isHeld == true) {
Timber.tag(tag).d("Releasing partial wakelock after hang up")
activeWakeLock.release()
}
timedOutCallJob?.cancel()
activeCall.value = null
} }
/**
* Removes the current active call and any associated UI, cancelling the timeouts and wakelocks.
*/
override suspend fun joinedCall(callType: CallType) = mutex.withLock { override suspend fun joinedCall(callType: CallType) = mutex.withLock {
Timber.tag(tag).d("Joined call: $callType") Timber.tag(tag).d("Joined call: $callType")
cancelIncomingCallNotification() removeCurrentCall()
if (activeWakeLock?.isHeld == true) {
Timber.tag(tag).d("Releasing partial wakelock after joining call")
activeWakeLock.release()
}
timedOutCallJob?.cancel()
activeCall.value = ActiveCall( activeCall.value = ActiveCall(
callType = callType, 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") @SuppressLint("MissingPermission")
private suspend fun showIncomingCallNotification(notificationData: CallNotificationData) { private suspend fun showIncomingCallNotification(notificationData: CallNotificationData) {
Timber.tag(tag).d("Displaying ringing call notification") Timber.tag(tag).d("Displaying ringing call notification")
@ -281,73 +287,75 @@ class DefaultActiveCallManager(
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
private fun observeRingingCall() { private fun observeRingingCall() {
activeCall val roomForActiveCallFlow: Flow<Pair<BaseRoom, EventId>?> = activeCall.mapLatest { activeCall ->
.filterNotNull() val callType = activeCall?.callType as? CallType.RoomCall ?: return@mapLatest null
.filter { it.callState is CallState.Ringing && it.callType is CallType.RoomCall } val ringingInfo = activeCall.callState as? CallState.Ringing ?: return@mapLatest null
.flatMapLatest { activeCall -> val client = matrixClientProvider.getOrRestore(callType.sessionId).getOrNull() ?: run {
val callType = activeCall.callType as CallType.RoomCall Timber.tag(tag).d("Couldn't find session for incoming call: $activeCall")
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") val room = client.getRoom(callType.roomId) ?: run {
return@flatMapLatest flowOf() Timber.tag(tag).d("Couldn't find room 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@flatMapLatest flowOf()
}
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. // If we have declined from another phone we want to stop ringing.
room.subscribeToCallDecline(ringingInfo.notificationData.eventId) room.subscribeToCallDecline(eventId)
.filter { decliner -> .filter { decliner ->
Timber.tag(tag).d("Call: $activeCall was declined by $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, // only want to listen if the call was declined from another of my sessions,
// (we are ringing for an incoming call in a DM) // (we are ringing for an incoming call in a DM)
decliner == client.sessionId decliner == room.sessionId
} }
} }
.onEach { decliner -> .onEach { decliner ->
Timber.tag(tag).d("Call: $activeCall was declined by user from another session") Timber.tag(tag).d("Call: $activeCall was declined by user from another session")
// Remove the active call and cancel the notification removeCurrentCall()
activeCall.value = null
if (activeWakeLock?.isHeld == true) {
Timber.tag(tag).d("Releasing partial wakelock after call declined from another session")
activeWakeLock.release()
}
cancelIncomingCallNotification()
} }
.launchIn(coroutineScope) .launchIn(coroutineScope)
// This will observe ringing calls and ensure they're terminated if the room call is cancelled or if the user // 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. // has joined the call from another session.
activeCall roomForActiveCallFlow
.filterNotNull() .flatMapLatest { pair ->
.filter { it.callState is CallState.Ringing && it.callType is CallType.RoomCall } val (room, _) = pair
.flatMapLatest { activeCall -> // This will cancel the previous iteration of flatMapLatest if the active call is now null
val callType = activeCall.callType as CallType.RoomCall ?: return@flatMapLatest flowOf()
// Get a flow of updated `hasRoomCall` and `activeRoomCallParticipants` values for the room
val room = matrixClientProvider.getOrRestore(callType.sessionId).getOrNull()?.getRoom(callType.roomId) ?: run { // We now observe the room info for changes to the active call state and the call participants
Timber.tag(tag).d("Couldn't find room for incoming call: $activeCall")
return@flatMapLatest flowOf()
}
room.roomInfoFlow.map { room.roomInfoFlow.map {
Timber.tag(tag).d("Has room call status changed for ringing call: ${it.hasRoomCall}") val participants = it.activeRoomCallParticipants
it.hasRoomCall to (callType.sessionId in 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() .distinctUntilChanged()
// Skip the first one, we're not interested in it (if the check below passes, it had to be active anyway) // Skip the first one, we're not interested in it (if the check below passes, it had to be active anyway)
.drop(1) .drop(1)
.onEach { (roomHasActiveCall, userIsInTheCall) -> .onEach { (roomHasActiveCall, userIsInTheCall) ->
if (!roomHasActiveCall) { if (!roomHasActiveCall) {
// The call was cancelled val notificationData = (activeCall.value?.callState as? CallState.Ringing)?.notificationData
timedOutCallJob?.cancel() removeCurrentCall()
incomingCallTimedOut(displayMissedCallNotification = true)
if (notificationData != null) {
displayMissedCallNotification(notificationData)
}
} else if (userIsInTheCall) { } else if (userIsInTheCall) {
// The user joined the call from another session removeCurrentCall()
timedOutCallJob?.cancel()
incomingCallTimedOut(displayMissedCallNotification = false)
} }
} }
.launchIn(coroutineScope) .launchIn(coroutineScope)

View file

@ -44,6 +44,13 @@ class WebViewAudioManager(
private val coroutineScope: CoroutineScope, private val coroutineScope: CoroutineScope,
private val onInvalidAudioDeviceAdded: (InvalidAudioDeviceReason) -> Unit, 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, * 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. * 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), devices: List<SerializableAudioDevice> = listAudioDevices().map(SerializableAudioDevice::fromAudioDeviceInfo),
) { ) {
Timber.d("Updating available audio devices") Timber.d("Updating available audio devices")
val jsonSerializer = Json { val deviceList = json.encodeToString(devices)
encodeDefaults = true
explicitNulls = false
}
val deviceList = jsonSerializer.encodeToString(devices)
webView.evaluateJavascript("controls.setAvailableOutputDevices($deviceList);", { webView.evaluateJavascript("controls.setAvailableOutputDevices($deviceList);", {
Timber.d("Audio: setAvailableOutputDevices result: $it") Timber.d("Audio: setAvailableOutputDevices result: $it")
}) })

View file

@ -7,18 +7,20 @@
package io.element.android.features.call.impl.utils 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.features.call.impl.data.WidgetMessage
import io.element.android.libraries.androidutils.json.JsonProvider
import io.element.android.libraries.core.extensions.runCatchingExceptions 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> { 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 { 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.CallScreenEvents
import io.element.android.features.call.impl.ui.CallScreenNavigator 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.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.FakeActiveCallManager
import io.element.android.features.call.utils.FakeCallWidgetProvider import io.element.android.features.call.utils.FakeCallWidgetProvider
import io.element.android.features.call.utils.FakeWidgetMessageInterceptor 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.architecture.AsyncData
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.api.sync.SyncState
@ -50,7 +52,8 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@OptIn(ExperimentalCoroutinesApi::class) class CallScreenPresenterTest { @OptIn(ExperimentalCoroutinesApi::class)
class CallScreenPresenterTest {
@get:Rule @get:Rule
val warmUpRule = WarmUpRule() val warmUpRule = WarmUpRule()
@ -409,6 +412,7 @@ import kotlin.time.Duration.Companion.seconds
languageTagProvider = FakeLanguageTagProvider("en-US"), languageTagProvider = FakeLanguageTagProvider("en-US"),
appForegroundStateService = appForegroundStateService, appForegroundStateService = appForegroundStateService,
appCoroutineScope = backgroundScope, 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
import io.element.android.libraries.matrix.test.A_ROOM_ID_2 import io.element.android.libraries.matrix.test.A_ROOM_ID_2
import io.element.android.libraries.matrix.test.A_SESSION_ID 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.FakeMatrixClient
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
import io.element.android.libraries.matrix.test.room.FakeBaseRoom 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.element.android.tests.testutils.plantTestTimber
import io.mockk.coVerify import io.mockk.coVerify
import io.mockk.mockk import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
@ -331,6 +333,49 @@ class DefaultActiveCallManagerTest {
assertThat(manager.activeCall.value).isNull() 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) @OptIn(ExperimentalCoroutinesApi::class)
@Test @Test
fun `observeRingingCalls - will do nothing if either the session or the room are not found`() = runTest { 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_administrators_title">"ویرایش مدیران"</string>
<string name="screen_room_change_role_confirm_add_admin_description">"قادر نخواهید بود این کنش را بازکردانید. داردید کاربر را به سطح قدرت خودتان ارتقا می‌دهید."</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_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_action">"تنزل بده"</string>
<string name="screen_room_change_role_confirm_demote_self_description">"شما نمی‌توانید این تغییر را بازگردانید زیرا در حال تنزل نقش خود در اتاق هستید، اگر آخرین کاربر ممتاز در اتاق باشید، امکان دستیابی مجدد به دسترسی‌های سطح بالای اتاق غیرممکن است."</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_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">"%1$s (منتظر)"</string>
<string name="screen_room_change_role_invited_member_name_android">"(منتظر)"</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_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_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_administrators">"مدیران"</string>
<string name="screen_room_change_role_section_moderators">"ناظم‌ها"</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_section_users">"اعضا"</string>
<string name="screen_room_change_role_unsaved_changes_description">"تغییراتی ذخیره نشده دارید."</string> <string name="screen_room_change_role_unsaved_changes_description">"تغییراتی ذخیره نشده دارید."</string>
<string name="screen_room_change_role_unsaved_changes_title">"ذخیرهٔ تغییرات؟"</string> <string name="screen_room_change_role_unsaved_changes_title">"ذخیرهٔ تغییرات؟"</string>
<plurals name="screen_room_member_list_header_title"> <string name="screen_room_member_list_banned_empty">"هیچ کاربر محرومی در این اتاق نیست."</string>
<item quantity="one">"%1$d نفر"</item>
<item quantity="other">"%1$d نفر"</item>
</plurals>
<string name="screen_room_member_list_manage_member_remove_confirmation_ban">"برداشت و تحریم عضو"</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_remove_confirmation_kick">"تنها برداشتن عضو"</string>
<string name="screen_room_member_list_manage_member_unban_action">"رفع انسداد"</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_room_members_header_title">"اعضای اتاق"</string>
<string name="screen_room_member_list_unbanning_user">"رفع تحریم %1$s"</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">"مدیران"</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_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_member">"تنزّل به عضو"</string>
<string name="screen_room_roles_and_permissions_change_role_demote_to_moderator">"تنزّل به ناظم"</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_member_moderation">"نظارت اعضا"</string>
<string name="screen_room_roles_and_permissions_messages_and_content">"پیام‌ها و محتوا"</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_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_permissions_header">"اجازه‌ها"</string>
<string name="screen_room_roles_and_permissions_reset">"بازنشانی اجازه‌ها"</string> <string name="screen_room_roles_and_permissions_reset">"بازنشانی اجازه‌ها"</string>
<string name="screen_room_roles_and_permissions_reset_confirm_title">"بازنشانی اجازه‌ها؟"</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"> <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_administrators">"Tylko administratorzy"</string>
<string name="screen_room_change_permissions_ban_people">"Banowanie osób"</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_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_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> <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.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.core.net.toUri
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Inject
import im.vector.app.features.analytics.plan.CreatedRoom import im.vector.app.features.analytics.plan.CreatedRoom
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
@ -157,7 +158,7 @@ class ConfigureRoomPresenter(
createRoomAction: MutableState<AsyncAction<RoomId>> createRoomAction: MutableState<AsyncAction<RoomId>>
) = launch { ) = launch {
suspend { suspend {
val avatarUrl = config.avatarUri?.let { uploadAvatar(it) } val avatarUrl = config.avatarUri?.let { uploadAvatar(it.toUri()) }
val params = if (config.roomVisibility is RoomVisibilityState.Public) { val params = if (config.roomVisibility is RoomVisibilityState.Public) {
CreateRoomParameters( CreateRoomParameters(
name = config.roomName, name = config.roomName,

View file

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

View file

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

View file

@ -62,7 +62,7 @@ class CreateRoomConfigStore(
fun setAvatarUri(uri: Uri?, cached: Boolean = false) { fun setAvatarUri(uri: Uri?, cached: Boolean = false) {
cachedAvatarUri = uri.takeIf { cached } cachedAvatarUri = uri.takeIf { cached }
createRoomConfigFlow.getAndUpdate { config -> 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 package io.element.android.features.startchat.impl.configureroom
import android.net.Uri import android.net.Uri
import androidx.core.net.toUri
import app.cash.turbine.TurbineTestContext import app.cash.turbine.TurbineTestContext
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.CreatedRoom import im.vector.app.features.analytics.plan.CreatedRoom
@ -155,15 +156,15 @@ class ConfigureRoomPresenterTest {
// Pick avatar // Pick avatar
pickerProvider.givenResult(null) pickerProvider.givenResult(null)
// From gallery // From gallery
val uriFromGallery = Uri.parse(AN_URI_FROM_GALLERY) val uriFromGallery = AN_URI_FROM_GALLERY
pickerProvider.givenResult(uriFromGallery) pickerProvider.givenResult(uriFromGallery.toUri())
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
newState = awaitItem() newState = awaitItem()
expectedConfig = expectedConfig.copy(avatarUri = uriFromGallery) expectedConfig = expectedConfig.copy(avatarUri = uriFromGallery)
assertThat(newState.config).isEqualTo(expectedConfig) assertThat(newState.config).isEqualTo(expectedConfig)
// From camera // From camera
val uriFromCamera = Uri.parse(AN_URI_FROM_CAMERA) val uriFromCamera = AN_URI_FROM_CAMERA
pickerProvider.givenResult(uriFromCamera) pickerProvider.givenResult(uriFromCamera.toUri())
assertThat(newState.cameraPermissionState.permissionGranted).isFalse() assertThat(newState.cameraPermissionState.permissionGranted).isFalse()
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto)) newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto))
newState = awaitItem() newState = awaitItem()
@ -175,8 +176,8 @@ class ConfigureRoomPresenterTest {
expectedConfig = expectedConfig.copy(avatarUri = uriFromCamera) expectedConfig = expectedConfig.copy(avatarUri = uriFromCamera)
assertThat(newState.config).isEqualTo(expectedConfig) assertThat(newState.config).isEqualTo(expectedConfig)
// Do it again, no permission is requested // Do it again, no permission is requested
val uriFromCamera2 = Uri.parse(AN_URI_FROM_CAMERA_2) val uriFromCamera2 = AN_URI_FROM_CAMERA_2
pickerProvider.givenResult(uriFromCamera2) pickerProvider.givenResult(uriFromCamera2.toUri())
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto)) newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto))
newState = awaitItem() newState = awaitItem()
expectedConfig = expectedConfig.copy(avatarUri = uriFromCamera2) 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. * Please see LICENSE files in the repository root for full details.
*/ */
plugins { plugins {
id("io.element.android-library") id("io.element.android-compose-library")
} }
android { android {

View file

@ -7,6 +7,8 @@
package io.element.android.features.enterprise.api 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.compound.tokens.generated.SemanticColors
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -17,8 +19,17 @@ interface EnterpriseService {
fun defaultHomeserverList(): List<String> fun defaultHomeserverList(): List<String>
suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean 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 firebasePushGateway(): String?
fun unifiedPushDefaultPushGateway(): String? fun unifiedPushDefaultPushGateway(): String?

View file

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

View file

@ -7,6 +7,10 @@
package io.element.android.features.enterprise.impl 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.AppScope
import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Inject
@ -28,9 +32,17 @@ class DefaultEnterpriseService : EnterpriseService {
override fun defaultHomeserverList(): List<String> = emptyList() override fun defaultHomeserverList(): List<String> = emptyList()
override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true 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 firebasePushGateway(): String? = null
override fun unifiedPushDefaultPushGateway(): String? = null override fun unifiedPushDefaultPushGateway(): String? = null

View file

@ -7,7 +7,12 @@
package io.element.android.features.enterprise.impl 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 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_HOMESERVER_URL
import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -37,4 +42,30 @@ class DefaultEnterpriseServiceTest {
val defaultEnterpriseService = DefaultEnterpriseService() val defaultEnterpriseService = DefaultEnterpriseService()
assertThat(defaultEnterpriseService.isEnterpriseUser(A_SESSION_ID)).isFalse() 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 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.compound.tokens.generated.SemanticColors
import io.element.android.features.enterprise.api.BugReportUrl import io.element.android.features.enterprise.api.BugReportUrl
import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.api.EnterpriseService
@ -22,8 +24,9 @@ class FakeEnterpriseService(
private val isEnterpriseUserResult: (SessionId) -> Boolean = { lambdaError() }, private val isEnterpriseUserResult: (SessionId) -> Boolean = { lambdaError() },
private val defaultHomeserverListResult: () -> List<String> = { emptyList() }, private val defaultHomeserverListResult: () -> List<String> = { emptyList() },
private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() }, private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() },
private val semanticColorsLightResult: () -> SemanticColors = { lambdaError() }, private val semanticColorsLightResult: () -> State<SemanticColors> = { lambdaError() },
private val semanticColorsDarkResult: () -> SemanticColors = { lambdaError() }, private val semanticColorsDarkResult: () -> State<SemanticColors> = { lambdaError() },
private val overrideBrandColorResult: (String?) -> Unit = { lambdaError() },
private val firebasePushGatewayResult: () -> String? = { lambdaError() }, private val firebasePushGatewayResult: () -> String? = { lambdaError() },
private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() }, private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() },
) : EnterpriseService { ) : EnterpriseService {
@ -39,11 +42,17 @@ class FakeEnterpriseService(
isAllowedToConnectToHomeserverResult(homeserverUrl) isAllowedToConnectToHomeserverResult(homeserverUrl)
} }
override fun semanticColorsLight(): SemanticColors { override fun overrideBrandColor(brandColor: String?) {
overrideBrandColorResult(brandColor)
}
@Composable
override fun semanticColorsLight(): State<SemanticColors> {
return semanticColorsLightResult() return semanticColorsLightResult()
} }
override fun semanticColorsDark(): SemanticColors { @Composable
override fun semanticColorsDark(): State<SemanticColors> {
return semanticColorsDarkResult() 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 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.roomlist.RoomListState
import io.element.android.features.home.impl.spaces.HomeSpacesState import io.element.android.features.home.impl.spaces.HomeSpacesState
import io.element.android.features.logout.api.direct.DirectLogoutState 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 io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@Immutable
data class HomeState( data class HomeState(
/** /**
* The current user of this session, in case of multiple accounts, will contains 3 items, with the * 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.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
@ -34,6 +35,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -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.TestTags
import io.element.android.libraries.testtags.testTag import io.element.android.libraries.testtags.testTag
/**
* Ref: https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=2191-606
*/
@Composable @Composable
fun RoomListFiltersView( fun RoomListFiltersView(
state: RoomListFiltersState, state: RoomListFiltersState,
@ -143,9 +148,12 @@ private fun RoomListClearFiltersButton(
.clip(CircleShape) .clip(CircleShape)
.background(ElementTheme.colors.bgActionPrimaryRest) .background(ElementTheme.colors.bgActionPrimaryRest)
.clickable(onClick = onClick) .clickable(onClick = onClick)
.padding(4.dp)
) { ) {
Icon( Icon(
modifier = Modifier.align(Alignment.Center), modifier = Modifier
.align(Alignment.Center)
.size(16.dp),
imageVector = CompoundIcons.Close(), imageVector = CompoundIcons.Close(),
tint = ElementTheme.colors.iconOnSolidPrimary, tint = ElementTheme.colors.iconOnSolidPrimary,
contentDescription = stringResource(id = R.string.screen_roomlist_clear_filters), contentDescription = stringResource(id = R.string.screen_roomlist_clear_filters),
@ -170,21 +178,34 @@ private fun RoomListFilterView(
animationSpec = spring(stiffness = Spring.StiffnessMediumLow), animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
label = "chip text colour", 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( FilterChip(
selected = selected, selected = selected,
onClick = { onClick(roomListFilter) }, onClick = { onClick(roomListFilter) },
modifier = modifier.height(36.dp), modifier = modifier.height(32.dp),
shape = CircleShape, shape = CircleShape,
colors = FilterChipDefaults.filterChipColors( colors = FilterChipDefaults.filterChipColors(
containerColor = background.value, containerColor = background.value,
selectedContainerColor = background.value, selectedContainerColor = background.value,
labelColor = textColour.value, labelColor = textColour.value,
selectedLabelColor = textColour.value selectedLabelColor = textColour.value,
), ),
label = { 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 @Composable
internal fun RoomListFiltersViewPreview(@PreviewParameter(RoomListFiltersStateProvider::class) state: RoomListFiltersState) = ElementPreview { internal fun RoomListFiltersViewPreview(@PreviewParameter(RoomListFiltersStateProvider::class) state: RoomListFiltersState) = ElementPreview {
RoomListFiltersView( RoomListFiltersView(
modifier = Modifier.padding(vertical = 4.dp),
state = state, 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.ImmutableList
import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.ImmutableSet
@Immutable
data class RoomListState( data class RoomListState(
val contextMenu: ContextMenu, val contextMenu: ContextMenu,
val declineInviteMenu: DeclineInviteMenu, val declineInviteMenu: DeclineInviteMenu,

View file

@ -24,6 +24,7 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier 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.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource 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.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.tokens.generated.CompoundIcons 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.model.RoomListRoomSummary
import io.element.android.features.home.impl.roomlist.RoomListEvents 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.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.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.FilledTextField import io.element.android.libraries.designsystem.theme.components.FilledTextField
@ -111,18 +113,18 @@ private fun RoomListSearchContent(
}, },
navigationIcon = { BackButton(onClick = ::onBackButtonClick) }, navigationIcon = { BackButton(onClick = ::onBackButtonClick) },
title = { title = {
var filter by textFieldState(state.query) var value by remember { mutableStateOf(TextFieldValue(state.query)) }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
FilledTextField( FilledTextField(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.focusRequester(focusRequester), .focusRequester(focusRequester),
value = filter, value = value,
singleLine = true, singleLine = true,
onValueChange = { onValueChange = {
filter = it value = it
state.eventSink(RoomListSearchEvents.QueryChanged(it)) state.eventSink(RoomListSearchEvents.QueryChanged(it.text))
}, },
colors = TextFieldDefaults.colors( colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent, focusedContainerColor = Color.Transparent,
@ -134,7 +136,7 @@ private fun RoomListSearchContent(
errorIndicatorColor = Color.Transparent, errorIndicatorColor = Color.Transparent,
), ),
trailingIcon = { trailingIcon = {
if (filter.isNotEmpty()) { if (value.text.isNotEmpty()) {
IconButton(onClick = { IconButton(onClick = {
state.eventSink(RoomListSearchEvents.ClearQuery) state.eventSink(RoomListSearchEvents.ClearQuery)
}) { }) {
@ -148,7 +150,11 @@ private fun RoomListSearchContent(
) )
LaunchedEffect(Unit) { 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) 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.architecture.Presenter
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@ -28,7 +30,10 @@ class HomeSpacesPresenter(
@Composable @Composable
override fun present(): HomeSpacesState { override fun present(): HomeSpacesState {
val hideInvitesAvatar by client.rememberHideInvitesAvatar() 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 { val seenSpaceInvites by remember {
seenInvitesStore.seenRoomIds().map { it.toImmutableSet() } seenInvitesStore.seenRoomIds().map { it.toImmutableSet() }
}.collectAsState(persistentSetOf()) }.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.core.RoomId
import io.element.android.libraries.matrix.api.spaces.SpaceRoom import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.ImmutableSet
data class HomeSpacesState( data class HomeSpacesState(
val space: CurrentSpace, val space: CurrentSpace,
val spaceRooms: List<SpaceRoom>, val spaceRooms: ImmutableList<SpaceRoom>,
val seenSpaceInvites: ImmutableSet<RoomId>, val seenSpaceInvites: ImmutableSet<RoomId>,
val hideInvitesAvatar: Boolean, val hideInvitesAvatar: Boolean,
val eventSink: (HomeSpacesEvents) -> Unit, 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.core.RoomId
import io.element.android.libraries.matrix.api.spaces.SpaceRoom import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.previewutils.room.aSpaceRoom import io.element.android.libraries.previewutils.room.aSpaceRoom
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet import kotlinx.collections.immutable.toImmutableSet
open class HomeSpacesStateProvider : PreviewParameterProvider<HomeSpacesState> { open class HomeSpacesStateProvider : PreviewParameterProvider<HomeSpacesState> {
@ -39,7 +40,7 @@ internal fun aHomeSpacesState(
eventSink: (HomeSpacesEvents) -> Unit = {}, eventSink: (HomeSpacesEvents) -> Unit = {},
) = HomeSpacesState( ) = HomeSpacesState(
space = space, space = space,
spaceRooms = spaceRooms, spaceRooms = spaceRooms.toImmutableList(),
seenSpaceInvites = seenSpaceInvites.toImmutableSet(), seenSpaceInvites = seenSpaceInvites.toImmutableSet(),
hideInvitesAvatar = hideInvitesAvatar, hideInvitesAvatar = hideInvitesAvatar,
eventSink = eventSink, 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.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.ui.components.SpaceHeaderRootView import io.element.android.libraries.matrix.ui.components.SpaceHeaderRootView
@ -47,6 +48,9 @@ fun HomeSpacesView(
) )
} }
} }
item {
HorizontalDivider()
}
state.spaceRooms.forEach { spaceRoom -> state.spaceRooms.forEach { spaceRoom ->
item(spaceRoom.roomId) { item(spaceRoom.roomId) {
val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED 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_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="full_screen_intent_banner_title">"Gør din opkaldsoplevelse bedre"</string>
<string name="screen_home_tab_chats">"Samtaler"</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_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_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> <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_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_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_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_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_submit">"Seadista andmete taastamine"</string>
<string name="banner_set_up_recovery_title">"Seadista 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="confirm_recovery_key_banner_title">"ذخیره‌ساز کلیدتان از هم‌گام بودن در آمده"</string>
<string name="full_screen_intent_banner_title">"بهبود تجریهٔ تماستان"</string> <string name="full_screen_intent_banner_title">"بهبود تجریهٔ تماستان"</string>
<string name="screen_home_tab_chats">"گپ‌ها"</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_message">"مطمئنید که می‌خواهید دعوت پیوستن به %1$s را رد کنید؟"</string>
<string name="screen_invites_decline_chat_title">"رد دعوت"</string> <string name="screen_invites_decline_chat_title">"رد دعوت"</string>
<string name="screen_invites_decline_direct_chat_message">"مطمئنید که می‌خواهید این گپ خصوصی با %1$s را رد کنید؟"</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_message">"فرایندی یک باره است. ممنون از شکیباییتان."</string>
<string name="screen_migration_title">"برپایی حسابتان."</string> <string name="screen_migration_title">"برپایی حسابتان."</string>
<string name="screen_roomlist_a11y_create_message">"ایجاد اتاق یا گفت‌وگویی جدید"</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_message">"آغاز با پیام دادن به کسی."</string>
<string name="screen_roomlist_empty_title">"هنوز گپی وجود ندارد."</string> <string name="screen_roomlist_empty_title">"هنوز گپی وجود ندارد."</string>
<string name="screen_roomlist_filter_favourites">"علاقه‌مندی‌ها"</string> <string name="screen_roomlist_filter_favourites">"علاقه‌مندی‌ها"</string>
@ -39,6 +41,7 @@
<string name="screen_roomlist_main_space_title">"گپ‌ها"</string> <string name="screen_roomlist_main_space_title">"گپ‌ها"</string>
<string name="screen_roomlist_mark_as_read">"علامت‌گذاری به عنوان خوانده شده"</string> <string name="screen_roomlist_mark_as_read">"علامت‌گذاری به عنوان خوانده شده"</string>
<string name="screen_roomlist_mark_as_unread">"نشان به ناخوانده"</string> <string name="screen_roomlist_mark_as_unread">"نشان به ناخوانده"</string>
<string name="screen_roomlist_tombstoned_room_description">"این اتاق ارتقا یافته"</string>
<string name="session_verification_banner_message">"گویا از افزاره‌ای جدید استفاده می‌کنید. تأیید با افزاره‌ای دیگر برای دسترسی به پیام‌های رمزنگاری شده‌تان."</string> <string name="session_verification_banner_message">"گویا از افزاره‌ای جدید استفاده می‌کنید. تأیید با افزاره‌ای دیگر برای دسترسی به پیام‌های رمزنگاری شده‌تان."</string>
<string name="session_verification_banner_title">"تأیید کنید که خودتانید"</string> <string name="session_verification_banner_title">"تأیید کنید که خودتانید"</string>
</resources> </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_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_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_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_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_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> <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_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_submit_android">"Wyłącz optymalizację"</string>
<string name="banner_battery_optimization_title_android">"Powiadomienia nie dochodzą?"</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_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_submit">"Skonfiguruj przywracanie"</string>
<string name="banner_set_up_recovery_title">"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_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="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_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_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_chat_title">"Odrzuć zaproszenie"</string>
<string name="screen_invites_decline_direct_chat_message">"Czy na pewno chcesz odrzucić rozmowę prywatną z %1$s?"</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">"Zaproszenia"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"Nie masz żadnych oczekujących zaproszeń."</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">"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_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_mixed_empty_state_title">"Brak czatów dla podanych kryteriów"</string>
<string name="screen_roomlist_filter_people">"Osoby"</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_content_android">"停用此應用程式的電池最佳化,才能確保收到所有通知。"</string>
<string name="banner_battery_optimization_submit_android">"停用最佳化"</string> <string name="banner_battery_optimization_submit_android">"停用最佳化"</string>
<string name="banner_battery_optimization_title_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_content">"若您遺失了所有現有裝置,則請使用復原金鑰以救援您的密碼學身份與訊息歷史紀錄。"</string>
<string name="banner_set_up_recovery_submit">"設定復原"</string> <string name="banner_set_up_recovery_submit">"設定復原"</string>
<string name="banner_set_up_recovery_title">"設定備援以保護您的帳號"</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.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias 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.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomInfo import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.room.RoomMembershipDetails import io.element.android.libraries.matrix.api.room.RoomMembershipDetails
@ -141,11 +139,7 @@ class JoinRoomPresenter(
preview.previewInfo.toContentState(membershipDetails) preview.previewInfo.toContentState(membershipDetails)
}, },
onFailure = { throwable -> onFailure = { throwable ->
if (throwable is ClientException.MatrixApi && (throwable.kind == ErrorKind.NotFound || throwable.kind == ErrorKind.Forbidden)) { ContentState.UnknownRoom
ContentState.UnknownRoom
} else {
ContentState.Failure(throwable)
}
} }
) )
} }

View file

@ -24,7 +24,6 @@ import kotlinx.collections.immutable.ImmutableList
internal const val MAX_KNOCK_MESSAGE_LENGTH = 500 internal const val MAX_KNOCK_MESSAGE_LENGTH = 500
@Immutable
data class JoinRoomState( data class JoinRoomState(
val roomIdOrAlias: RoomIdOrAlias, val roomIdOrAlias: RoomIdOrAlias,
val contentState: ContentState, 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_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_invited_by">"Inviteret af"</string>
<string name="screen_join_room_join_action">"Deltag"</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_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_characters_count">"Tilladte tegn %1$d af %2$d"</string>
<string name="screen_join_room_knock_message_description">"Besked (valgfrit)"</string> <string name="screen_join_room_knock_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_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_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_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_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">"Klynger er ikke understøttet endnu"</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_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_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> <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_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_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_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_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_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_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_invite_required_message">"Selle jututoaga liitumiseks vajad sa kutset"</string>
<string name="screen_join_room_invited_by">"Kutsuja"</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_confirmation">"بله. رد و انسداد"</string>
<string name="screen_join_room_decline_and_block_alert_title">"رد دعوت و انسداد"</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_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_forget_action">"فراموشی این اتاق"</string>
<string name="screen_join_room_invite_required_message">"برای پیوستن به این اتاق نیاز به دعوت دارید"</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_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_action">"در زدن برای پیوستن"</string>
<string name="screen_join_room_knock_message_description">"پیام (اختیاری)"</string> <string name="screen_join_room_knock_message_description">"پیام (اختیاری)"</string>
<string name="screen_join_room_knock_sent_title">"درخواست پیوستن فرستاده شد"</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_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_forget_action">"Unohda"</string>
<string name="screen_join_room_invite_required_message">"Tarvitset kutsun liittyäksesi"</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_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_join_restricted_message">"Saatat tarvita kutsun tai olla tilan jäsen, jotta voit liittyä."</string>
<string name="screen_join_room_knock_action">"Lähetä liittymispyyntö"</string> <string name="screen_join_room_knock_action">"Lähetä liittymispyyntö"</string>

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_ban_by_message">"%1$s kitiltotta a szobából."</string> <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_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_action">"Kérés visszavonása"</string>
<string name="screen_join_room_cancel_knock_alert_confirmation">"Igen, visszavonás"</string> <string name="screen_join_room_cancel_knock_alert_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_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_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_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_message">"A 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_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">"Szoba elfelejtése"</string> <string name="screen_join_room_forget_action">"Elfelejt"</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_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_invited_by">"Meghívta:"</string>
<string name="screen_join_room_join_action">"Csatlakozás"</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> <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"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_ban_by_message">"Zostałeś zbanowany z tego pokoju przez %1$s."</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 z tego pokoju"</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_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_action">"Anuluj prośbę"</string>
<string name="screen_join_room_cancel_knock_alert_confirmation">"Tak, anuluj"</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_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_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_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_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_fail_reason">"Ten pokój wymaga zaproszenia lub dołączanie zostało ograniczone."</string>
<string name="screen_join_room_forget_action">"Zapomnij o tym pokoju"</string> <string name="screen_join_room_forget_action">"Zapomnij"</string>
<string name="screen_join_room_invite_required_message">"Potrzebujesz zaproszenia, aby dołączyć do tego pokoju"</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_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_join_restricted_message">"Aby dołączyć, musisz uzyskać zaproszenie lub być członkiem danej przestrzeni."</string>
<string name="screen_join_room_knock_action">"Wyślij prośbę o dołączenie"</string> <string name="screen_join_room_knock_action">"Wyślij prośbę o dołączenie"</string>

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_ban_by_message">"Foste banido desta sala por %1$s."</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 desta sala."</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_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_action">"Cancelar pedido"</string>
<string name="screen_join_room_cancel_knock_alert_confirmation">"Sim, cancelar"</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_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_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_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_message">"Falha ao entrar"</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_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 esta sala"</string> <string name="screen_join_room_forget_action">"Esquecer"</string>
<string name="screen_join_room_invite_required_message">"Precisas de um convite para entrares nesta sala"</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_invited_by">"Convidado por"</string>
<string name="screen_join_room_join_action">"Entrar"</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> <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"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_ban_by_message">"您被 %1$s 禁止進入此聊天室。"</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_message">"您被禁止"</string>
<string name="screen_join_room_ban_reason">"理由:%1$s。"</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_action">"取消請求"</string>
<string name="screen_join_room_cancel_knock_alert_confirmation">"是的,取消"</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_message">"您確定要拒絕加入此聊天室的邀請嗎?這也會防止 %1$s 聯絡您或邀請您加入聊天室。"</string>
<string name="screen_join_room_decline_and_block_alert_title">"拒絕邀請並封鎖"</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_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_fail_reason">"此聊天室僅有受邀者才能進入,或是在空間層級有存取限制。"</string> <string name="screen_join_room_fail_reason">"您必須獲得邀請才能加入,或者可能存在存取限制。"</string>
<string name="screen_join_room_forget_action">"忘記此聊天室"</string> <string name="screen_join_room_forget_action">"忘記"</string>
<string name="screen_join_room_invite_required_message">"您需要獲得邀請才能加入此聊天室"</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_action">"加入"</string>
<string name="screen_join_room_join_restricted_message">"您可能需要被邀請成為空間的成員才能加入。"</string> <string name="screen_join_room_join_restricted_message">"您可能需要被邀請成為空間的成員才能加入。"</string>
<string name="screen_join_room_knock_action">"傳送加入請求"</string> <string name="screen_join_room_knock_action">"傳送加入請求"</string>

View file

@ -1193,46 +1193,8 @@ class JoinRoomPresenterTest {
skipItems(1) skipItems(1)
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.contentState).isEqualTo( 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 package io.element.android.features.leaveroom.api
import androidx.compose.runtime.Immutable
@Immutable
interface LeaveRoomState { interface LeaveRoomState {
val eventSink: (LeaveRoomEvent) -> Unit val eventSink: (LeaveRoomEvent) -> Unit
} }

View file

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

View file

@ -24,14 +24,6 @@
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"پین‌ها مطابق نیستند"</string> <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_message">"برای ادامه باید دوباره وارد شده و پینی جدید ایجاد کنید"</string>
<string name="screen_app_lock_signout_alert_title">"دارید خارج می‌شوید"</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_biometric_android">"استفاده از زیست‌سنجی"</string>
<string name="screen_app_lock_use_pin_android">"استفاده از پین"</string> <string name="screen_app_lock_use_pin_android">"استفاده از پین"</string>
<string name="screen_signout_in_progress_dialog_content">"خارج شدن…"</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.features.login.impl.changeserver.ChangeServerState
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.uri.ensureProtocol import io.element.android.libraries.core.uri.ensureProtocol
import kotlinx.collections.immutable.toImmutableList
@Inject @Inject
class ChangeAccountProviderPresenter( class ChangeAccountProviderPresenter(
@ -39,6 +40,7 @@ class ChangeAccountProviderPresenter(
isValid = true, isValid = true,
) )
} }
.toImmutableList()
} }
val canSearchForAccountProviders = remember { 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.accountprovider.AccountProvider
import io.element.android.features.login.impl.changeserver.ChangeServerState 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( data class ChangeAccountProviderState(
val accountProviders: List<AccountProvider>, val accountProviders: ImmutableList<AccountProvider>,
val canSearchForAccountProviders: Boolean, val canSearchForAccountProviders: Boolean,
val changeServerState: ChangeServerState, 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.accountprovider.anAccountProvider
import io.element.android.features.login.impl.changeserver.ChangeServerState import io.element.android.features.login.impl.changeserver.ChangeServerState
import io.element.android.features.login.impl.changeserver.aChangeServerState import io.element.android.features.login.impl.changeserver.aChangeServerState
import kotlinx.collections.immutable.toImmutableList
open class ChangeAccountProviderStateProvider : PreviewParameterProvider<ChangeAccountProviderState> { open class ChangeAccountProviderStateProvider : PreviewParameterProvider<ChangeAccountProviderState> {
override val values: Sequence<ChangeAccountProviderState> override val values: Sequence<ChangeAccountProviderState>
@ -29,7 +30,7 @@ fun aChangeAccountProviderState(
canSearchForAccountProviders: Boolean = true, canSearchForAccountProviders: Boolean = true,
changeServerState: ChangeServerState = aChangeServerState(), changeServerState: ChangeServerState = aChangeServerState(),
) = ChangeAccountProviderState( ) = ChangeAccountProviderState(
accountProviders = accountProviders, accountProviders = accountProviders.toImmutableList(),
canSearchForAccountProviders = canSearchForAccountProviders, canSearchForAccountProviders = canSearchForAccountProviders,
changeServerState = changeServerState, 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.AsyncData
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.uri.ensureProtocol import io.element.android.libraries.core.uri.ensureProtocol
import kotlinx.collections.immutable.toImmutableList
@Inject @Inject
class ChooseAccountProviderPresenter( class ChooseAccountProviderPresenter(
@ -69,6 +70,7 @@ class ChooseAccountProviderPresenter(
isValid = true, isValid = true,
) )
} }
.toImmutableList()
} }
return ChooseAccountProviderState( 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.accountprovider.AccountProvider
import io.element.android.features.login.impl.login.LoginMode import io.element.android.features.login.impl.login.LoginMode
import io.element.android.libraries.architecture.AsyncData 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( data class ChooseAccountProviderState(
val accountProviders: List<AccountProvider>, val accountProviders: ImmutableList<AccountProvider>,
val selectedAccountProvider: AccountProvider?, val selectedAccountProvider: AccountProvider?,
val loginMode: AsyncData<LoginMode>, val loginMode: AsyncData<LoginMode>,
val eventSink: (ChooseAccountProviderEvents) -> Unit, 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.accountprovider.anAccountProvider
import io.element.android.features.login.impl.login.LoginMode import io.element.android.features.login.impl.login.LoginMode
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import kotlinx.collections.immutable.toImmutableList
open class ChooseAccountProviderStateProvider : PreviewParameterProvider<ChooseAccountProviderState> { open class ChooseAccountProviderStateProvider : PreviewParameterProvider<ChooseAccountProviderState> {
private val server1 = anAccountProvider( private val server1 = anAccountProvider(
@ -70,7 +71,7 @@ fun aChooseAccountProviderState(
loginMode: AsyncData<LoginMode> = AsyncData.Uninitialized, loginMode: AsyncData<LoginMode> = AsyncData.Uninitialized,
eventSink: (ChooseAccountProviderEvents) -> Unit = {}, eventSink: (ChooseAccountProviderEvents) -> Unit = {},
) = ChooseAccountProviderState( ) = ChooseAccountProviderState(
accountProviders = accountProviders, accountProviders = accountProviders.toImmutableList(),
selectedAccountProvider = selectedAccountProvider, selectedAccountProvider = selectedAccountProvider,
loginMode = loginMode, loginMode = loginMode,
eventSink = eventSink, 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.features.login.impl.login.LoginMode
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
// Do not use default value, so no member get forgotten in the presenters.
data class ConfirmAccountProviderState( data class ConfirmAccountProviderState(
val accountProvider: AccountProvider, val accountProvider: AccountProvider,
val isAccountCreation: Boolean, val isAccountCreation: Boolean,

View file

@ -11,8 +11,8 @@ import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Inject
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource 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 io.element.android.libraries.matrix.api.auth.external.ExternalSession
import kotlinx.serialization.json.Json
interface MessageParser { interface MessageParser {
/** /**
@ -26,10 +26,10 @@ interface MessageParser {
@Inject @Inject
class DefaultMessageParser( class DefaultMessageParser(
private val accountProviderDataSource: AccountProviderDataSource, private val accountProviderDataSource: AccountProviderDataSource,
private val json: JsonProvider,
) : MessageParser { ) : MessageParser {
override fun parse(message: String): ExternalSession { override fun parse(message: String): ExternalSession {
val parser = Json { ignoreUnknownKeys = true } val response = json().decodeFromString(MobileRegistrationResponse.serializer(), message)
val response = parser.decodeFromString(MobileRegistrationResponse.serializer(), message)
val userId = response.userId ?: error("No user ID in response") val userId = response.userId ?: error("No user ID in response")
val homeServer = response.homeServer ?: accountProviderDataSource.flow.value.url val homeServer = response.homeServer ?: accountProviderDataSource.flow.value.url
val accessToken = response.accessToken ?: error("No access token in response") 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.features.login.impl.resolver.HomeserverData
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
// Do not use default value, so no member get forgotten in the presenters.
data class SearchAccountProviderState( data class SearchAccountProviderState(
val userInput: String, val userInput: String,
val userInputResult: AsyncData<List<HomeserverData>>, 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_other">"دیگر"</string>
<string name="screen_change_account_provider_subtitle">"استفاده از فراهم کنندهٔ حسابی دیگر چون کارساز خصوصی خوتان یا حسابی کاری."</string> <string name="screen_change_account_provider_subtitle">"استفاده از فراهم کنندهٔ حسابی دیگر چون کارساز خصوصی خوتان یا حسابی کاری."</string>
<string name="screen_change_account_provider_title">"تغییر فراهم کنندهٔ حساب"</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_error_invalid_homeserver">"ما نتوانستیم به این کارساز خانگی برسیم. لطفاً بررسی کنید که URL کارساز اصلی را به درستی وارد کرده اید. اگر URL صحیح است، برای کمک بیشتر با مدیر کارساز خانگی خود تماس بگیرید."</string>
<string name="screen_change_server_form_header">"نشانی کارساز خانگی"</string> <string name="screen_change_server_form_header">"نشانی کارساز خانگی"</string>
<string name="screen_change_server_form_notice">"ورود نشانی دامنه."</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_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_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_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_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_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> <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.appconfig.AuthenticationConfig
import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource 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 io.element.android.libraries.matrix.api.auth.external.ExternalSession
import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationException
import org.junit.Assert.assertThrows import org.junit.Assert.assertThrows
@ -68,7 +69,8 @@ class DefaultMessageParserTest {
private fun createDefaultMessageParser(): DefaultMessageParser { private fun createDefaultMessageParser(): DefaultMessageParser {
return DefaultMessageParser( return DefaultMessageParser(
AccountProviderDataSource(FakeEnterpriseService()) accountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
json = DefaultJsonProvider(),
) )
} }
} }

View file

@ -34,10 +34,12 @@ dependencies {
implementation(projects.libraries.testtags) implementation(projects.libraries.testtags)
implementation(projects.libraries.uiStrings) implementation(projects.libraries.uiStrings)
implementation(projects.libraries.dateformatter.api) implementation(projects.libraries.dateformatter.api)
implementation(projects.libraries.workmanager.api)
api(projects.features.logout.api) api(projects.features.logout.api)
testCommonDependencies(libs, true) testCommonDependencies(libs, true)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.sessionStorage.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.BackupState
import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.BackupUploadState
import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.workmanager.api.WorkManagerScheduler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -34,6 +35,7 @@ import kotlinx.coroutines.launch
class LogoutPresenter( class LogoutPresenter(
private val matrixClient: MatrixClient, private val matrixClient: MatrixClient,
private val encryptionService: EncryptionService, private val encryptionService: EncryptionService,
private val workManagerScheduler: WorkManagerScheduler,
) : Presenter<LogoutState> { ) : Presenter<LogoutState> {
@Composable @Composable
override fun present(): LogoutState { override fun present(): LogoutState {
@ -109,6 +111,9 @@ class LogoutPresenter(
ignoreSdkError: Boolean, ignoreSdkError: Boolean,
) = launch { ) = launch {
suspend { suspend {
// Cancel any pending work (e.g. notification sync)
workManagerScheduler.cancel(matrixClient.sessionId)
matrixClient.logout(userInitiated = true, ignoreSdkError) matrixClient.logout(userInitiated = true, ignoreSdkError)
}.runCatchingUpdatingState(logoutAction) }.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 com.google.common.truth.Truth.assertThat
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.MatrixClient 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.BackupState
import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.BackupUploadState
import io.element.android.libraries.matrix.api.encryption.EncryptionService 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.AN_EXCEPTION
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService 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.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -145,7 +148,9 @@ class LogoutPresenterTest {
@Test @Test
fun `present - logout then confirm`() = runTest { 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) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -158,6 +163,8 @@ class LogoutPresenterTest {
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java) assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
val successState = awaitItem() val successState = awaitItem()
assertThat(successState.logoutAction).isInstanceOf(AsyncAction.Success::class.java) assertThat(successState.logoutAction).isInstanceOf(AsyncAction.Success::class.java)
cancelWorkManagerJobsLambda.assertions().isCalledOnce()
} }
} }
@ -230,7 +237,9 @@ class LogoutPresenterTest {
internal fun createLogoutPresenter( internal fun createLogoutPresenter(
matrixClient: MatrixClient = FakeMatrixClient(), matrixClient: MatrixClient = FakeMatrixClient(),
encryptionService: EncryptionService = FakeEncryptionService(), encryptionService: EncryptionService = FakeEncryptionService(),
workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(cancelLambda = {}),
): LogoutPresenter = LogoutPresenter( ): LogoutPresenter = LogoutPresenter(
matrixClient = matrixClient, matrixClient = matrixClient,
encryptionService = encryptionService, encryptionService = encryptionService,
workManagerScheduler = workManagerScheduler,
) )

View file

@ -8,15 +8,14 @@
package io.element.android.features.messages.api.timeline.voicemessages.composer package io.element.android.features.messages.api.timeline.voicemessages.composer
import androidx.compose.ui.tooling.preview.PreviewParameterProvider 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 io.element.android.libraries.textcomposer.model.VoiceMessageState
import kotlinx.collections.immutable.toImmutableList
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
open class VoiceMessageComposerStateProvider : PreviewParameterProvider<VoiceMessageComposerState> { open class VoiceMessageComposerStateProvider : PreviewParameterProvider<VoiceMessageComposerState> {
override val values: Sequence<VoiceMessageComposerState> override val values: Sequence<VoiceMessageComposerState>
get() = sequenceOf( 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, showCursor = false,
playbackProgress = 0f, playbackProgress = 0f,
time = 10.seconds, 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 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.api.timeline.voicemessages.composer.VoiceMessageComposerState
import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.actionlist.ActionListState
import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState 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 io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@Immutable
data class MessagesState( data class MessagesState(
val roomId: RoomId, val roomId: RoomId,
val roomName: String?, 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.BottomSheetDragHandle
import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.HideKeyboardWhenDisposed
import io.element.android.libraries.designsystem.utils.KeepScreenOn import io.element.android.libraries.designsystem.utils.KeepScreenOn
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
@ -124,8 +123,6 @@ fun MessagesView(
KeepScreenOn(state.voiceMessageComposerState.keepScreenOn) KeepScreenOn(state.voiceMessageComposerState.keepScreenOn)
HideKeyboardWhenDisposed()
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) 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 // 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 io.element.android.features.messages.impl.timeline.model.TimelineItem
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@Immutable
data class ActionListState( data class ActionListState(
val target: Target, val target: Target,
val eventSink: (ActionListEvents) -> Unit, 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.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.runtime.Immutable
import io.element.android.libraries.designsystem.icons.CompoundDrawables import io.element.android.libraries.designsystem.icons.CompoundDrawables
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@Immutable
enum class TimelineItemAction( enum class TimelineItemAction(
@StringRes val titleRes: Int, @StringRes val titleRes: Int,
@DrawableRes val icon: Int, @DrawableRes val icon: Int,

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