Merge branch 'release/25.11.2' into main
This commit is contained in:
commit
9507ae3551
1157 changed files with 9536 additions and 7182 deletions
|
|
@ -26,7 +26,11 @@ ktlint_standard_annotation = disabled
|
|||
ktlint_standard_parameter-list-wrapping = disabled
|
||||
ktlint_standard_indent = disabled
|
||||
ktlint_standard_blank-line-before-declaration = disabled
|
||||
ktlint_function_naming_ignore_when_annotated_with=Composable
|
||||
ktlint_function_naming_ignore_when_annotated_with = Composable
|
||||
# Added when upgrading to 1.7.1
|
||||
ktlint_standard_function-expression-body = disabled
|
||||
ktlint_standard_chain-method-continuation = disabled
|
||||
ktlint_standard_class-signature = disabled
|
||||
|
||||
[*.java]
|
||||
ij_java_align_consecutive_assignments = false
|
||||
|
|
|
|||
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
|
|
@ -53,7 +53,7 @@ jobs:
|
|||
run: ./gradlew :app:assembleGplayDebug app:assembleFDroidDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
||||
- name: Upload debug APKs
|
||||
if: ${{ matrix.variant == 'debug' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: elementx-debug
|
||||
path: |
|
||||
|
|
@ -61,7 +61,7 @@ jobs:
|
|||
app/build/outputs/apk/fdroid/debug/*-universal-debug.apk
|
||||
- name: Upload x86_64 APK for Maestro
|
||||
if: ${{ matrix.variant == 'debug' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: elementx-apk-maestro
|
||||
path: |
|
||||
|
|
|
|||
2
.github/workflows/build_enterprise.yml
vendored
2
.github/workflows/build_enterprise.yml
vendored
|
|
@ -61,7 +61,7 @@ jobs:
|
|||
run: ./gradlew :app:assembleGplayDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
||||
- name: Upload debug Enterprise APKs
|
||||
if: ${{ matrix.variant == 'debug' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: elementx-enterprise-debug
|
||||
path: |
|
||||
|
|
|
|||
2
.github/workflows/danger.yml
vendored
2
.github/workflows/danger.yml
vendored
|
|
@ -20,7 +20,7 @@ jobs:
|
|||
- run: |
|
||||
npm install --save-dev @babel/plugin-transform-flow-strip-types
|
||||
- name: Danger
|
||||
uses: danger/danger-js@bdccecb77e0144055fbaea9224f10cf8b1229b68 # 13.0.4
|
||||
uses: danger/danger-js@67ed2c1f42fd2fc198cc3c14b43c8f83351f4fe9 # 13.0.5
|
||||
with:
|
||||
args: "--dangerfile ./tools/danger/dangerfile.js"
|
||||
env:
|
||||
|
|
|
|||
6
.github/workflows/maestro-local.yml
vendored
6
.github/workflows/maestro-local.yml
vendored
|
|
@ -44,7 +44,7 @@ jobs:
|
|||
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
||||
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
||||
- name: Upload APK as artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: elementx-apk-maestro
|
||||
path: |
|
||||
|
|
@ -69,7 +69,7 @@ jobs:
|
|||
# https://github.com/actions/checkout/issues/881
|
||||
ref: ${{ github.ref }}
|
||||
- name: Download APK artifact from previous job
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: elementx-apk-maestro
|
||||
- name: Enable KVM group perms
|
||||
|
|
@ -102,7 +102,7 @@ jobs:
|
|||
script: |
|
||||
.github/workflows/scripts/maestro/maestro-local-with-screen-recording.sh app-gplay-x86_64-debug.apk
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: test-results
|
||||
path: |
|
||||
|
|
|
|||
4
.github/workflows/nightlyReports.yml
vendored
4
.github/workflows/nightlyReports.yml
vendored
|
|
@ -42,7 +42,7 @@ jobs:
|
|||
|
||||
- name: ✅ Upload kover report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: kover-results
|
||||
path: |
|
||||
|
|
@ -74,7 +74,7 @@ jobs:
|
|||
run: ./gradlew dependencyCheckAnalyze $CI_GRADLE_ARG_PROPERTIES
|
||||
- name: Upload dependency analysis
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: dependency-analysis
|
||||
path: build/reports/dependency-check-report.html
|
||||
|
|
|
|||
12
.github/workflows/quality.yml
vendored
12
.github/workflows/quality.yml
vendored
|
|
@ -97,7 +97,7 @@ jobs:
|
|||
run: ./gradlew :tests:konsist:testDebugUnitTest $CI_GRADLE_ARG_PROPERTIES --no-daemon
|
||||
- name: Upload reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: konsist-report
|
||||
path: |
|
||||
|
|
@ -174,7 +174,7 @@ jobs:
|
|||
run: ./gradlew :app:lintGplayDebug :app:lintFdroidDebug lintDebug $CI_GRADLE_ARG_PROPERTIES --continue
|
||||
- name: Upload reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: linting-report
|
||||
path: |
|
||||
|
|
@ -214,7 +214,7 @@ jobs:
|
|||
run: ./gradlew detekt $CI_GRADLE_ARG_PROPERTIES --no-daemon
|
||||
- name: Upload reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: detekt-report
|
||||
path: |
|
||||
|
|
@ -254,7 +254,7 @@ jobs:
|
|||
run: ./gradlew ktlintCheck $CI_GRADLE_ARG_PROPERTIES
|
||||
- name: Upload reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: ktlint-report
|
||||
path: |
|
||||
|
|
@ -317,7 +317,7 @@ jobs:
|
|||
# https://github.com/actions/checkout/issues/881
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||
- name: Download reports from previous jobs
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
- name: Prepare Danger
|
||||
if: always()
|
||||
run: |
|
||||
|
|
@ -326,7 +326,7 @@ jobs:
|
|||
yarn add danger-plugin-lint-report --dev
|
||||
- name: Danger lint
|
||||
if: always()
|
||||
uses: danger/danger-js@bdccecb77e0144055fbaea9224f10cf8b1229b68 # 13.0.4
|
||||
uses: danger/danger-js@67ed2c1f42fd2fc198cc3c14b43c8f83351f4fe9 # 13.0.5
|
||||
with:
|
||||
args: "--dangerfile ./tools/danger/dangerfile-lint.js"
|
||||
env:
|
||||
|
|
|
|||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
|
|
@ -38,7 +38,7 @@ jobs:
|
|||
ELEMENT_CALL_RAGESHAKE_URL: ${{ secrets.ELEMENT_CALL_RAGESHAKE_URL }}
|
||||
run: ./gradlew bundleGplayRelease $CI_GRADLE_ARG_PROPERTIES
|
||||
- name: Upload bundle as artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: elementx-app-gplay-bundle-unsigned
|
||||
path: |
|
||||
|
|
@ -74,7 +74,7 @@ jobs:
|
|||
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
||||
run: ./gradlew bundleGplayRelease $CI_GRADLE_ARG_PROPERTIES
|
||||
- name: Upload bundle as artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: elementx-enterprise-app-gplay-bundle-unsigned
|
||||
path: |
|
||||
|
|
@ -102,7 +102,7 @@ jobs:
|
|||
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
||||
run: ./gradlew assembleFdroidRelease $CI_GRADLE_ARG_PROPERTIES
|
||||
- name: Upload apks as artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: elementx-app-fdroid-apks-unsigned
|
||||
path: |
|
||||
|
|
|
|||
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
|
|
@ -61,7 +61,7 @@ jobs:
|
|||
|
||||
- name: 🚫 Upload kover failed coverage reports
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: kover-error-report
|
||||
path: |
|
||||
|
|
@ -73,7 +73,7 @@ jobs:
|
|||
|
||||
- name: 🚫 Upload test results on error
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: tests-and-screenshot-tests-results
|
||||
path: |
|
||||
|
|
|
|||
69
CHANGES.md
69
CHANGES.md
|
|
@ -1,3 +1,72 @@
|
|||
Changes in Element X v25.11.0
|
||||
=============================
|
||||
|
||||
Hotfix release.
|
||||
|
||||
Includes https://github.com/element-hq/element-x-android/pull/5615, which fixes an issue that prevented Element Call notifications from being displayed sometimes.
|
||||
|
||||
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.10.1...v25.11.0
|
||||
|
||||
Changes in Element X v25.10.1
|
||||
=============================
|
||||
|
||||
<!-- Release notes generated using configuration in .github/release.yml at v25.10.1 -->
|
||||
|
||||
## What's Changed
|
||||
### ✨ Features
|
||||
* Sync notifications using WorkManager by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5545
|
||||
### 🙌 Improvements
|
||||
* Sort feature flags by @bmarty in https://github.com/element-hq/element-x-android/pull/5557
|
||||
### 🐛 Bugfixes
|
||||
* Makes sure images are loaded when cancelling multiaccount flow by @ganfra in https://github.com/element-hq/element-x-android/pull/5502
|
||||
* Fix 'test push loop back' notification check by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5541
|
||||
* Display 'join anyway' button on room preview when the state can't be loaded by @ShadowRZ in https://github.com/element-hq/element-x-android/pull/5514
|
||||
* Fix media viewer not being dismissed with reduced motion enabled by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5555
|
||||
* Keep the cursor position in room list search when going back by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5570
|
||||
* Make sure declining a call stops observing the ringing call state by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5563
|
||||
### 🗣 Translations
|
||||
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5515
|
||||
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5562
|
||||
### 🧱 Build
|
||||
* Do some cleanup on our immutable annotation usage by @bmarty in https://github.com/element-hq/element-x-android/pull/5503
|
||||
* `interface TestParameterValuesProvider` is deprecated. by @bmarty in https://github.com/element-hq/element-x-android/pull/5568
|
||||
### Dependency upgrades
|
||||
* fix(deps): update metro to v0.6.9 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5480
|
||||
* fix(deps): update dependency org.unifiedpush.android:connector to v3.1.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5443
|
||||
* fix(deps): update wysiwyg to v2.40.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5400
|
||||
* fix(deps): update dependency io.github.sergio-sastre.composablepreviewscanner:android to v0.7.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5510
|
||||
* fix(deps): update camera to v1.5.1 - autoclosed by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5509
|
||||
* chore(deps): update plugin dependencycheck to v12.1.7 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5518
|
||||
* chore(deps): update plugin licensee to v1.14.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5477
|
||||
* chore(deps): update dependency python to 3.14 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5475
|
||||
* fix(deps): update metro to v0.6.10 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5520
|
||||
* fix(deps): update dependency org.unifiedpush.android:connector to v3.1.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5519
|
||||
* chore(deps): update plugin gms_google_services to v4.4.4 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5507
|
||||
* fix(deps): update dependency com.google.firebase:firebase-bom to v34.4.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5522
|
||||
* fix(deps): update dependency com.squareup.okhttp3:okhttp-bom to v5.2.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5524
|
||||
* fix(deps): update dependency net.zetetic:sqlcipher-android to v4.11.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5525
|
||||
* fix(deps): update dependencyanalysis to v3.1.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5523
|
||||
* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.10.13 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5527
|
||||
* chore(deps): update plugin dependencycheck to v12.1.8 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5531
|
||||
* chore(deps): update rnkdsh/action-upload-diawi action to v1.5.12 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5533
|
||||
* fix(deps): update dependency org.maplibre.gl:android-sdk to v12.0.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5548
|
||||
* fix(deps): update metro to v0.7.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5554
|
||||
* fix(deps): update dependency com.posthog:posthog-android to v3.24.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5564
|
||||
* chore(deps): update plugin sonarqube to v7 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5535
|
||||
### Others
|
||||
* Import Compound tokens - fixed icons by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5506
|
||||
* Replace Uri by String in States that are used in Composable function. by @bmarty in https://github.com/element-hq/element-x-android/pull/5508
|
||||
* Let room filters follow the design. by @bmarty in https://github.com/element-hq/element-x-android/pull/5526
|
||||
* Allow uploading notification push rules in bug reports by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5538
|
||||
* Add number of accounts info in the rageshake data. by @bmarty in https://github.com/element-hq/element-x-android/pull/5532
|
||||
* design(space): match figma for Space views by @ganfra in https://github.com/element-hq/element-x-android/pull/5540
|
||||
* Extract console message logger and mutualize instance of Json by @bmarty in https://github.com/element-hq/element-x-android/pull/5552
|
||||
* Improve colors customization by @bmarty in https://github.com/element-hq/element-x-android/pull/5542
|
||||
* Fix test warning by @bmarty in https://github.com/element-hq/element-x-android/pull/5558
|
||||
|
||||
|
||||
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.10.0...v25.10.1
|
||||
|
||||
Changes in Element X v25.10.0
|
||||
=============================
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ import androidx.compose.foundation.layout.Box
|
|||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
|
|
@ -25,6 +28,7 @@ import androidx.lifecycle.repeatOnLifecycle
|
|||
import com.bumble.appyx.core.integration.NodeHost
|
||||
import com.bumble.appyx.core.integrationpoint.NodeActivity
|
||||
import com.bumble.appyx.core.plugin.NodeReadyObserver
|
||||
import io.element.android.compound.colors.SemanticColorsLightDark
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
||||
import io.element.android.features.lockscreen.api.LockScreenLockState
|
||||
|
|
@ -61,9 +65,13 @@ class MainActivity : NodeActivity() {
|
|||
@Composable
|
||||
private fun MainContent(appBindings: AppBindings) {
|
||||
val migrationState = appBindings.migrationEntryPoint().present()
|
||||
val colors by remember {
|
||||
appBindings.enterpriseService().semanticColorsFlow(sessionId = null)
|
||||
}.collectAsState(SemanticColorsLightDark.default)
|
||||
ElementThemeApp(
|
||||
appPreferencesStore = appBindings.preferencesStore(),
|
||||
enterpriseService = appBindings.enterpriseService(),
|
||||
compoundLight = colors.light,
|
||||
compoundDark = colors.dark,
|
||||
buildMeta = appBindings.buildMeta()
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
|
|
|
|||
|
|
@ -18,8 +18,6 @@ import dev.zacsweers.metro.Provides
|
|||
import dev.zacsweers.metro.SingleIn
|
||||
import io.element.android.appconfig.ApplicationConfig
|
||||
import io.element.android.features.enterprise.api.EnterpriseService
|
||||
import io.element.android.features.messages.impl.timeline.components.customreaction.DefaultEmojibaseProvider
|
||||
import io.element.android.features.messages.impl.timeline.components.customreaction.EmojibaseProvider
|
||||
import io.element.android.libraries.androidutils.system.getVersionCodeFromManifest
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
|
|
@ -29,6 +27,8 @@ import io.element.android.libraries.di.BaseDirectory
|
|||
import io.element.android.libraries.di.CacheDirectory
|
||||
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
import io.element.android.libraries.recentemojis.api.EmojibaseProvider
|
||||
import io.element.android.libraries.recentemojis.impl.DefaultEmojibaseProvider
|
||||
import io.element.android.x.BuildConfig
|
||||
import io.element.android.x.R
|
||||
import kotlinx.coroutines.CoroutineName
|
||||
|
|
|
|||
|
|
@ -8,13 +8,11 @@
|
|||
package io.element.android.x.di
|
||||
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.appnav.di.RoomGraphFactory
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
@Inject
|
||||
class DefaultRoomGraphFactory(
|
||||
private val sessionGraph: SessionGraph,
|
||||
) : RoomGraphFactory {
|
||||
|
|
|
|||
|
|
@ -9,12 +9,10 @@ package io.element.android.x.di
|
|||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.appnav.di.SessionGraphFactory
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@Inject
|
||||
class DefaultSessionGraphFactory(
|
||||
private val appGraph: AppGraph
|
||||
) : SessionGraphFactory {
|
||||
|
|
|
|||
|
|
@ -9,13 +9,14 @@ package io.element.android.x.di
|
|||
|
||||
import dev.zacsweers.metro.GraphExtension
|
||||
import dev.zacsweers.metro.Provides
|
||||
import io.element.android.appnav.di.TimelineBindings
|
||||
import io.element.android.libraries.architecture.NodeFactoriesBindings
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.room.BaseRoom
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
|
||||
@GraphExtension(RoomScope::class)
|
||||
interface RoomGraph : NodeFactoriesBindings {
|
||||
interface RoomGraph : NodeFactoriesBindings, TimelineBindings {
|
||||
@GraphExtension.Factory
|
||||
interface Factory {
|
||||
fun create(
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ import android.content.Intent
|
|||
import androidx.core.net.toUri
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.deeplink.api.DeepLinkCreator
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
|
|
@ -22,7 +22,6 @@ import io.element.android.libraries.push.impl.intent.IntentProvider
|
|||
import io.element.android.x.MainActivity
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@Inject
|
||||
class DefaultIntentProvider(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val deepLinkCreator: DeepLinkCreator,
|
||||
|
|
@ -31,10 +30,11 @@ class DefaultIntentProvider(
|
|||
sessionId: SessionId,
|
||||
roomId: RoomId?,
|
||||
threadId: ThreadId?,
|
||||
eventId: EventId?,
|
||||
): Intent {
|
||||
return Intent(context, MainActivity::class.java).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
data = deepLinkCreator.create(sessionId, roomId, threadId).toUri()
|
||||
data = deepLinkCreator.create(sessionId, roomId, threadId, eventId).toUri()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,13 +9,11 @@ package io.element.android.x.oidc
|
|||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.matrix.api.auth.OidcRedirectUrlProvider
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import io.element.android.x.R
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@Inject
|
||||
class DefaultOidcRedirectUrlProvider(
|
||||
private val stringProvider: StringProvider,
|
||||
) : OidcRedirectUrlProvider {
|
||||
|
|
|
|||
|
|
@ -13,9 +13,11 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.deeplink.api.DeepLinkCreator
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_THREAD_ID
|
||||
|
|
@ -31,14 +33,15 @@ import org.robolectric.RuntimeEnvironment
|
|||
class DefaultIntentProviderTest {
|
||||
@Test
|
||||
fun `test getViewRoomIntent with data`() {
|
||||
val deepLinkCreator = lambdaRecorder<SessionId, RoomId?, ThreadId?, String> { _, _, _ -> "deepLinkCreatorResult" }
|
||||
val deepLinkCreator = lambdaRecorder<SessionId, RoomId?, ThreadId?, EventId?, String> { _, _, _, _ -> "deepLinkCreatorResult" }
|
||||
val sut = createDefaultIntentProvider(
|
||||
deepLinkCreator = { sessionId, roomId, threadId -> deepLinkCreator.invoke(sessionId, roomId, threadId) },
|
||||
deepLinkCreator = { sessionId, roomId, threadId, eventId -> deepLinkCreator.invoke(sessionId, roomId, threadId, eventId) },
|
||||
)
|
||||
val result = sut.getViewRoomIntent(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
threadId = A_THREAD_ID,
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
result.commonAssertions()
|
||||
assertThat(result.data.toString()).isEqualTo("deepLinkCreatorResult")
|
||||
|
|
@ -46,11 +49,12 @@ class DefaultIntentProviderTest {
|
|||
value(A_SESSION_ID),
|
||||
value(A_ROOM_ID),
|
||||
value(A_THREAD_ID),
|
||||
value(AN_EVENT_ID),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createDefaultIntentProvider(
|
||||
deepLinkCreator: DeepLinkCreator = DeepLinkCreator { _, _, _ -> "" },
|
||||
deepLinkCreator: DeepLinkCreator = DeepLinkCreator { _, _, _, _ -> "" },
|
||||
): DefaultIntentProvider {
|
||||
return DefaultIntentProvider(
|
||||
context = RuntimeEnvironment.getApplication() as Context,
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@
|
|||
|
||||
package io.element.android.appconfig
|
||||
|
||||
import android.graphics.Color
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.core.graphics.toColorInt
|
||||
|
||||
object NotificationConfig {
|
||||
/**
|
||||
|
|
@ -27,5 +27,5 @@ object NotificationConfig {
|
|||
const val SHOW_QUICK_REPLY_ACTION = true
|
||||
|
||||
@ColorInt
|
||||
val NOTIFICATION_ACCENT_COLOR: Int = Color.parseColor("#FF0DBD8B")
|
||||
val NOTIFICATION_ACCENT_COLOR: Int = "#FF0DBD8B".toColorInt()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ dependencies {
|
|||
testImplementation(projects.libraries.preferences.test)
|
||||
testImplementation(projects.libraries.push.test)
|
||||
testImplementation(projects.libraries.pushproviders.test)
|
||||
testImplementation(projects.features.forward.test)
|
||||
testImplementation(projects.features.networkmonitor.test)
|
||||
testImplementation(projects.features.rageshake.test)
|
||||
testImplementation(projects.services.appnavstate.test)
|
||||
|
|
|
|||
|
|
@ -21,13 +21,13 @@ import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel
|
|||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.node.ParentNode
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.appnav.di.SessionGraphFactory
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.di.DependencyInjectionGraphOwner
|
||||
|
|
@ -56,10 +56,12 @@ class LoggedInAppScopeFlowNode(
|
|||
plugins = plugins
|
||||
), DependencyInjectionGraphOwner {
|
||||
interface Callback : Plugin {
|
||||
fun onOpenBugReport()
|
||||
fun onAddAccount()
|
||||
fun navigateToBugReport()
|
||||
fun navigateToAddAccount()
|
||||
}
|
||||
|
||||
private val callback: Callback = callback()
|
||||
|
||||
@Parcelize
|
||||
object NavTarget : Parcelable
|
||||
|
||||
|
|
@ -81,12 +83,12 @@ class LoggedInAppScopeFlowNode(
|
|||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
val callback = object : LoggedInFlowNode.Callback {
|
||||
override fun onOpenBugReport() {
|
||||
plugins<Callback>().forEach { it.onOpenBugReport() }
|
||||
override fun navigateToBugReport() {
|
||||
callback.navigateToBugReport()
|
||||
}
|
||||
|
||||
override fun onAddAccount() {
|
||||
plugins<Callback>().forEach { it.onAddAccount() }
|
||||
override fun navigateToAddAccount() {
|
||||
callback.navigateToAddAccount()
|
||||
}
|
||||
}
|
||||
return createNode<LoggedInFlowNode>(buildContext, listOf(callback))
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.Box
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.bumble.appyx.core.composable.PermanentChild
|
||||
|
|
@ -23,7 +24,6 @@ import com.bumble.appyx.core.navigation.NavKey
|
|||
import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack.State.ACTIVE
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack.State.CREATED
|
||||
|
|
@ -46,6 +46,8 @@ import io.element.android.appnav.loggedin.SendQueues
|
|||
import io.element.android.appnav.room.RoomFlowNode
|
||||
import io.element.android.appnav.room.RoomNavigationTarget
|
||||
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
|
||||
import io.element.android.compound.colors.SemanticColorsLightDark
|
||||
import io.element.android.features.enterprise.api.EnterpriseService
|
||||
import io.element.android.features.enterprise.api.SessionEnterpriseService
|
||||
import io.element.android.features.ftue.api.FtueEntryPoint
|
||||
import io.element.android.features.ftue.api.state.FtueService
|
||||
|
|
@ -53,19 +55,23 @@ import io.element.android.features.ftue.api.state.FtueState
|
|||
import io.element.android.features.home.api.HomeEntryPoint
|
||||
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorContainer
|
||||
import io.element.android.features.preferences.api.PreferencesEntryPoint
|
||||
import io.element.android.features.roomdirectory.api.RoomDescription
|
||||
import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint
|
||||
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
|
||||
import io.element.android.features.share.api.ShareEntryPoint
|
||||
import io.element.android.features.space.api.SpaceEntryPoint
|
||||
import io.element.android.features.startchat.api.StartChatEntryPoint
|
||||
import io.element.android.features.userprofile.api.UserProfileEntryPoint
|
||||
import io.element.android.features.verifysession.api.IncomingVerificationEntryPoint
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.architecture.waitForChildAttached
|
||||
import io.element.android.libraries.architecture.waitForNavTargetAttached
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.designsystem.theme.ElementThemeApp
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
||||
|
|
@ -77,8 +83,10 @@ import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
|||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener
|
||||
import io.element.android.libraries.matrix.api.verification.VerificationRequest
|
||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||
import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService
|
||||
import io.element.android.libraries.ui.common.nodes.emptyNode
|
||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||
|
|
@ -122,6 +130,10 @@ class LoggedInFlowNode(
|
|||
private val sessionEnterpriseService: SessionEnterpriseService,
|
||||
private val networkMonitor: NetworkMonitor,
|
||||
private val notificationConversationService: NotificationConversationService,
|
||||
private val syncService: SyncService,
|
||||
private val enterpriseService: EnterpriseService,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val buildMeta: BuildMeta,
|
||||
snackbarDispatcher: SnackbarDispatcher,
|
||||
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
|
|
@ -136,10 +148,11 @@ class LoggedInFlowNode(
|
|||
plugins = plugins
|
||||
) {
|
||||
interface Callback : Plugin {
|
||||
fun onOpenBugReport()
|
||||
fun onAddAccount()
|
||||
fun navigateToBugReport()
|
||||
fun navigateToAddAccount()
|
||||
}
|
||||
|
||||
private val callback: Callback = callback()
|
||||
private val loggedInFlowProcessor = LoggedInEventProcessor(
|
||||
snackbarDispatcher = snackbarDispatcher,
|
||||
roomMembershipObserver = matrixClient.roomMembershipObserver,
|
||||
|
|
@ -244,7 +257,7 @@ class LoggedInFlowNode(
|
|||
val serverNames: List<String> = emptyList(),
|
||||
val trigger: JoinedRoom.Trigger? = null,
|
||||
val roomDescription: RoomDescription? = null,
|
||||
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages(),
|
||||
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Root(),
|
||||
val targetId: UUID = UUID.randomUUID(),
|
||||
) : NavTarget
|
||||
|
||||
|
|
@ -270,7 +283,7 @@ class LoggedInFlowNode(
|
|||
data object Ftue : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object RoomDirectorySearch : NavTarget
|
||||
data object RoomDirectory : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class IncomingShare(val intent: Intent) : NavTarget
|
||||
|
|
@ -292,50 +305,47 @@ class LoggedInFlowNode(
|
|||
}
|
||||
NavTarget.Home -> {
|
||||
val callback = object : HomeEntryPoint.Callback {
|
||||
override fun onRoomClick(roomId: RoomId) {
|
||||
override fun navigateToRoom(roomId: RoomId) {
|
||||
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
|
||||
}
|
||||
|
||||
override fun onSettingsClick() {
|
||||
override fun navigateToSettings() {
|
||||
backstack.push(NavTarget.Settings())
|
||||
}
|
||||
|
||||
override fun onStartChatClick() {
|
||||
override fun navigateToCreateRoom() {
|
||||
backstack.push(NavTarget.CreateRoom)
|
||||
}
|
||||
|
||||
override fun onSetUpRecoveryClick() {
|
||||
override fun navigateToSetUpRecovery() {
|
||||
backstack.push(NavTarget.SecureBackup(initialElement = SecureBackupEntryPoint.InitialTarget.Root))
|
||||
}
|
||||
|
||||
override fun onSessionConfirmRecoveryKeyClick() {
|
||||
override fun navigateToEnterRecoveryKey() {
|
||||
backstack.push(NavTarget.SecureBackup(initialElement = SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey))
|
||||
}
|
||||
|
||||
override fun onRoomSettingsClick(roomId: RoomId) {
|
||||
override fun navigateToRoomSettings(roomId: RoomId) {
|
||||
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.Details))
|
||||
}
|
||||
|
||||
override fun onReportBugClick() {
|
||||
plugins<Callback>().forEach { it.onOpenBugReport() }
|
||||
override fun navigateToBugReport() {
|
||||
callback.navigateToBugReport()
|
||||
}
|
||||
}
|
||||
homeEntryPoint
|
||||
.nodeBuilder(this, buildContext)
|
||||
.callback(callback)
|
||||
.build()
|
||||
homeEntryPoint.createNode(
|
||||
parentNode = this,
|
||||
buildContext = buildContext,
|
||||
callback = callback,
|
||||
)
|
||||
}
|
||||
is NavTarget.Room -> {
|
||||
val joinedRoomCallback = object : JoinedRoomLoadedFlowNode.Callback {
|
||||
override fun onOpenRoom(roomId: RoomId, serverNames: List<String>) {
|
||||
override fun navigateToRoom(roomId: RoomId, serverNames: List<String>) {
|
||||
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), serverNames))
|
||||
}
|
||||
|
||||
override fun onForwardedToSingleRoom(roomId: RoomId) {
|
||||
sessionCoroutineScope.launch { attachRoom(roomId.toRoomIdOrAlias(), clearBackstack = false) }
|
||||
}
|
||||
|
||||
override fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) {
|
||||
override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) {
|
||||
when (data) {
|
||||
is PermalinkData.UserLink -> {
|
||||
// Should not happen (handled by MessagesNode)
|
||||
|
|
@ -346,7 +356,7 @@ class LoggedInFlowNode(
|
|||
roomIdOrAlias = data.roomIdOrAlias,
|
||||
serverNames = data.viaParameters,
|
||||
trigger = JoinedRoom.Trigger.Timeline,
|
||||
initialElement = RoomNavigationTarget.Messages(data.eventId),
|
||||
initialElement = RoomNavigationTarget.Root(data.eventId),
|
||||
)
|
||||
if (pushToBackstack) {
|
||||
backstack.push(target)
|
||||
|
|
@ -361,15 +371,10 @@ class LoggedInFlowNode(
|
|||
}
|
||||
}
|
||||
|
||||
override fun onOpenGlobalNotificationSettings() {
|
||||
override fun navigateToGlobalNotificationSettings() {
|
||||
backstack.push(NavTarget.Settings(PreferencesEntryPoint.InitialTarget.NotificationSettings))
|
||||
}
|
||||
}
|
||||
val spaceCallback = object : SpaceEntryPoint.Callback {
|
||||
override fun onOpenRoom(roomId: RoomId, viaParameters: List<String>) {
|
||||
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), serverNames = viaParameters))
|
||||
}
|
||||
}
|
||||
val inputs = RoomFlowNode.Inputs(
|
||||
roomIdOrAlias = navTarget.roomIdOrAlias,
|
||||
roomDescription = Optional.ofNullable(navTarget.roomDescription),
|
||||
|
|
@ -377,80 +382,89 @@ class LoggedInFlowNode(
|
|||
trigger = Optional.ofNullable(navTarget.trigger),
|
||||
initialElement = navTarget.initialElement
|
||||
)
|
||||
createNode<RoomFlowNode>(buildContext, plugins = listOf(inputs, joinedRoomCallback, spaceCallback))
|
||||
createNode<RoomFlowNode>(buildContext, plugins = listOf(inputs, joinedRoomCallback))
|
||||
}
|
||||
is NavTarget.UserProfile -> {
|
||||
val callback = object : UserProfileEntryPoint.Callback {
|
||||
override fun onOpenRoom(roomId: RoomId) {
|
||||
override fun navigateToRoom(roomId: RoomId) {
|
||||
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
|
||||
}
|
||||
}
|
||||
userProfileEntryPoint.nodeBuilder(this, buildContext)
|
||||
.params(UserProfileEntryPoint.Params(userId = navTarget.userId))
|
||||
.callback(callback)
|
||||
.build()
|
||||
userProfileEntryPoint.createNode(
|
||||
parentNode = this,
|
||||
buildContext = buildContext,
|
||||
params = UserProfileEntryPoint.Params(userId = navTarget.userId),
|
||||
callback = callback,
|
||||
)
|
||||
}
|
||||
is NavTarget.Settings -> {
|
||||
val callback = object : PreferencesEntryPoint.Callback {
|
||||
override fun onAddAccount() {
|
||||
plugins<Callback>().forEach { it.onAddAccount() }
|
||||
override fun navigateToAddAccount() {
|
||||
callback.navigateToAddAccount()
|
||||
}
|
||||
|
||||
override fun onOpenBugReport() {
|
||||
plugins<Callback>().forEach { it.onOpenBugReport() }
|
||||
override fun navigateToBugReport() {
|
||||
callback.navigateToBugReport()
|
||||
}
|
||||
|
||||
override fun onSecureBackupClick() {
|
||||
override fun navigateToSecureBackup() {
|
||||
backstack.push(NavTarget.SecureBackup())
|
||||
}
|
||||
|
||||
override fun onOpenRoomNotificationSettings(roomId: RoomId) {
|
||||
override fun navigateToRoomNotificationSettings(roomId: RoomId) {
|
||||
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.NotificationSettings))
|
||||
}
|
||||
|
||||
override fun navigateTo(roomId: RoomId, eventId: EventId) {
|
||||
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.Messages(eventId)))
|
||||
override fun navigateToEvent(roomId: RoomId, eventId: EventId) {
|
||||
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.Root(eventId)))
|
||||
}
|
||||
}
|
||||
val inputs = PreferencesEntryPoint.Params(navTarget.initialElement)
|
||||
preferencesEntryPoint.nodeBuilder(this, buildContext)
|
||||
.params(inputs)
|
||||
.callback(callback)
|
||||
.build()
|
||||
preferencesEntryPoint.createNode(
|
||||
parentNode = this,
|
||||
buildContext = buildContext,
|
||||
params = inputs,
|
||||
callback = callback,
|
||||
)
|
||||
}
|
||||
NavTarget.CreateRoom -> {
|
||||
val callback = object : StartChatEntryPoint.Callback {
|
||||
override fun onOpenRoom(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>) {
|
||||
override fun onRoomCreated(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>) {
|
||||
backstack.replace(NavTarget.Room(roomIdOrAlias = roomIdOrAlias, serverNames = serverNames))
|
||||
}
|
||||
|
||||
override fun onOpenRoomDirectory() {
|
||||
backstack.push(NavTarget.RoomDirectorySearch)
|
||||
override fun navigateToRoomDirectory() {
|
||||
backstack.push(NavTarget.RoomDirectory)
|
||||
}
|
||||
}
|
||||
|
||||
startChatEntryPoint
|
||||
.nodeBuilder(this, buildContext)
|
||||
.callback(callback)
|
||||
.build()
|
||||
startChatEntryPoint.createNode(
|
||||
parentNode = this,
|
||||
buildContext = buildContext,
|
||||
callback = callback,
|
||||
)
|
||||
}
|
||||
is NavTarget.SecureBackup -> {
|
||||
secureBackupEntryPoint.nodeBuilder(this, buildContext)
|
||||
.params(SecureBackupEntryPoint.Params(initialElement = navTarget.initialElement))
|
||||
.callback(object : SecureBackupEntryPoint.Callback {
|
||||
secureBackupEntryPoint.createNode(
|
||||
parentNode = this,
|
||||
buildContext = buildContext,
|
||||
params = SecureBackupEntryPoint.Params(initialElement = navTarget.initialElement),
|
||||
callback = object : SecureBackupEntryPoint.Callback {
|
||||
override fun onDone() {
|
||||
backstack.pop()
|
||||
}
|
||||
})
|
||||
.build()
|
||||
},
|
||||
)
|
||||
}
|
||||
NavTarget.Ftue -> {
|
||||
ftueEntryPoint.createNode(this, buildContext)
|
||||
}
|
||||
NavTarget.RoomDirectorySearch -> {
|
||||
roomDirectoryEntryPoint.nodeBuilder(this, buildContext)
|
||||
.callback(object : RoomDirectoryEntryPoint.Callback {
|
||||
override fun onResultClick(roomDescription: RoomDescription) {
|
||||
NavTarget.RoomDirectory -> {
|
||||
roomDirectoryEntryPoint.createNode(
|
||||
parentNode = this,
|
||||
buildContext = buildContext,
|
||||
callback = object : RoomDirectoryEntryPoint.Callback {
|
||||
override fun navigateToRoom(roomDescription: RoomDescription) {
|
||||
backstack.push(
|
||||
NavTarget.Room(
|
||||
roomIdOrAlias = roomDescription.roomId.toRoomIdOrAlias(),
|
||||
|
|
@ -459,32 +473,35 @@ class LoggedInFlowNode(
|
|||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
.build()
|
||||
},
|
||||
)
|
||||
}
|
||||
is NavTarget.IncomingShare -> {
|
||||
shareEntryPoint.nodeBuilder(this, buildContext)
|
||||
.callback(object : ShareEntryPoint.Callback {
|
||||
shareEntryPoint.createNode(
|
||||
parentNode = this,
|
||||
buildContext = buildContext,
|
||||
params = ShareEntryPoint.Params(intent = navTarget.intent),
|
||||
callback = object : ShareEntryPoint.Callback {
|
||||
override fun onDone(roomIds: List<RoomId>) {
|
||||
navigateUp()
|
||||
if (roomIds.size == 1) {
|
||||
val targetRoomId = roomIds.first()
|
||||
backstack.push(NavTarget.Room(targetRoomId.toRoomIdOrAlias()))
|
||||
roomIds.singleOrNull()?.let { roomId ->
|
||||
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
|
||||
}
|
||||
}
|
||||
})
|
||||
.params(ShareEntryPoint.Params(intent = navTarget.intent))
|
||||
.build()
|
||||
},
|
||||
)
|
||||
}
|
||||
is NavTarget.IncomingVerificationRequest -> {
|
||||
incomingVerificationEntryPoint.nodeBuilder(this, buildContext)
|
||||
.params(IncomingVerificationEntryPoint.Params(navTarget.data))
|
||||
.callback(object : IncomingVerificationEntryPoint.Callback {
|
||||
incomingVerificationEntryPoint.createNode(
|
||||
parentNode = this,
|
||||
buildContext = buildContext,
|
||||
params = IncomingVerificationEntryPoint.Params(navTarget.data),
|
||||
callback = object : IncomingVerificationEntryPoint.Callback {
|
||||
override fun onDone() {
|
||||
backstack.pop()
|
||||
}
|
||||
})
|
||||
.build()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -495,7 +512,7 @@ class LoggedInFlowNode(
|
|||
trigger: JoinedRoom.Trigger? = null,
|
||||
eventId: EventId? = null,
|
||||
clearBackstack: Boolean,
|
||||
) {
|
||||
): RoomFlowNode {
|
||||
waitForNavTargetAttached { navTarget ->
|
||||
navTarget is NavTarget.Home
|
||||
}
|
||||
|
|
@ -504,12 +521,19 @@ class LoggedInFlowNode(
|
|||
roomIdOrAlias = roomIdOrAlias,
|
||||
serverNames = serverNames,
|
||||
trigger = trigger,
|
||||
initialElement = RoomNavigationTarget.Messages(
|
||||
focusedEventId = eventId
|
||||
)
|
||||
initialElement = RoomNavigationTarget.Root(eventId = eventId)
|
||||
)
|
||||
backstack.accept(AttachRoomOperation(roomNavTarget, clearBackstack))
|
||||
}
|
||||
|
||||
// If we don't do this check, we might be returning while a previous node with the same type is still displayed
|
||||
// This means we may attach some new nodes to that one, which will be quickly replaced by the one instantiated above
|
||||
return waitForChildAttached<RoomFlowNode, NavTarget> {
|
||||
it is NavTarget.Room &&
|
||||
it.roomIdOrAlias == roomIdOrAlias &&
|
||||
it.initialElement is RoomNavigationTarget.Root &&
|
||||
it.initialElement.eventId == eventId
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun attachUser(userId: UserId) {
|
||||
|
|
@ -538,11 +562,27 @@ class LoggedInFlowNode(
|
|||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
Box(modifier = modifier) {
|
||||
val ftueState by ftueService.state.collectAsState()
|
||||
BackstackView()
|
||||
if (ftueState is FtueState.Complete) {
|
||||
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
|
||||
val colors by remember {
|
||||
enterpriseService.semanticColorsFlow(sessionId = matrixClient.sessionId)
|
||||
}.collectAsState(SemanticColorsLightDark.default)
|
||||
ElementThemeApp(
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
compoundLight = colors.light,
|
||||
compoundDark = colors.dark,
|
||||
buildMeta = buildMeta,
|
||||
) {
|
||||
val isOnline by syncService.isOnline.collectAsState()
|
||||
ConnectivityIndicatorContainer(
|
||||
isOnline = isOnline,
|
||||
modifier = modifier,
|
||||
) { contentModifier ->
|
||||
Box(modifier = contentModifier) {
|
||||
val ftueState by ftueService.state.collectAsState()
|
||||
BackstackView()
|
||||
if (ftueState is FtueState.Complete) {
|
||||
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import com.bumble.appyx.core.lifecycle.subscribe
|
|||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
|
|
@ -29,6 +28,7 @@ import io.element.android.features.login.api.LoginParams
|
|||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.designsystem.utils.ForceOrientationInMobileDevices
|
||||
import io.element.android.libraries.designsystem.utils.ScreenOrientation
|
||||
|
|
@ -55,9 +55,10 @@ class NotLoggedInFlowNode(
|
|||
) : NodeInputs
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun onOpenBugReport()
|
||||
fun navigateToBugReport()
|
||||
}
|
||||
|
||||
private val callback: Callback = callback()
|
||||
private val inputs = inputs<Params>()
|
||||
|
||||
override fun onBuilt() {
|
||||
|
|
@ -78,20 +79,19 @@ class NotLoggedInFlowNode(
|
|||
return when (navTarget) {
|
||||
NavTarget.Root -> {
|
||||
val callback = object : LoginEntryPoint.Callback {
|
||||
override fun onReportProblem() {
|
||||
plugins<Callback>().forEach { it.onOpenBugReport() }
|
||||
override fun navigateToBugReport() {
|
||||
callback.navigateToBugReport()
|
||||
}
|
||||
}
|
||||
loginEntryPoint
|
||||
.nodeBuilder(this, buildContext)
|
||||
.params(
|
||||
LoginEntryPoint.Params(
|
||||
accountProvider = inputs.loginParams?.accountProvider,
|
||||
loginHint = inputs.loginParams?.loginHint,
|
||||
)
|
||||
)
|
||||
.callback(callback)
|
||||
.build()
|
||||
loginEntryPoint.createNode(
|
||||
parentNode = this,
|
||||
buildContext = buildContext,
|
||||
params = LoginEntryPoint.Params(
|
||||
accountProvider = inputs.loginParams?.accountProvider,
|
||||
loginHint = inputs.loginParams?.loginHint,
|
||||
),
|
||||
callback = callback,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import io.element.android.annotations.ContributesNode
|
|||
import io.element.android.appnav.di.MatrixSessionCache
|
||||
import io.element.android.appnav.intent.IntentResolver
|
||||
import io.element.android.appnav.intent.ResolvedIntent
|
||||
import io.element.android.appnav.room.RoomFlowNode
|
||||
import io.element.android.appnav.root.RootNavStateFlowFactory
|
||||
import io.element.android.appnav.root.RootPresenter
|
||||
import io.element.android.appnav.root.RootView
|
||||
|
|
@ -38,7 +39,6 @@ import io.element.android.features.announcement.api.AnnouncementService
|
|||
import io.element.android.features.login.api.LoginParams
|
||||
import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl
|
||||
import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint
|
||||
import io.element.android.features.rageshake.api.reporter.BugReporter
|
||||
import io.element.android.features.signedout.api.SignedOutEntryPoint
|
||||
import io.element.android.libraries.accountselect.api.AccountSelectEntryPoint
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
|
|
@ -50,7 +50,10 @@ import io.element.android.libraries.core.uri.ensureProtocol
|
|||
import io.element.android.libraries.deeplink.api.DeeplinkData
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
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.ThreadId
|
||||
import io.element.android.libraries.matrix.api.core.asEventId
|
||||
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||
import io.element.android.libraries.oidc.api.OidcAction
|
||||
|
|
@ -80,7 +83,6 @@ class RootFlowNode(
|
|||
private val accountSelectEntryPoint: AccountSelectEntryPoint,
|
||||
private val intentResolver: IntentResolver,
|
||||
private val oidcActionFlow: OidcActionFlow,
|
||||
private val bugReporter: BugReporter,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
private val announcementService: AnnouncementService,
|
||||
) : BaseFlowNode<RootFlowNode.NavTarget>(
|
||||
|
|
@ -130,7 +132,6 @@ class RootFlowNode(
|
|||
|
||||
private fun switchToNotLoggedInFlow(params: LoginParams?) {
|
||||
matrixSessionCache.removeAll()
|
||||
bugReporter.setLogDirectorySubfolder(null)
|
||||
backstack.safeRoot(NavTarget.NotLoggedInFlow(params))
|
||||
}
|
||||
|
||||
|
|
@ -226,11 +227,11 @@ class RootFlowNode(
|
|||
}
|
||||
val inputs = LoggedInAppScopeFlowNode.Inputs(matrixClient)
|
||||
val callback = object : LoggedInAppScopeFlowNode.Callback {
|
||||
override fun onOpenBugReport() {
|
||||
override fun navigateToBugReport() {
|
||||
backstack.push(NavTarget.BugReport)
|
||||
}
|
||||
|
||||
override fun onAddAccount() {
|
||||
override fun navigateToAddAccount() {
|
||||
backstack.push(NavTarget.NotLoggedInFlow(null))
|
||||
}
|
||||
}
|
||||
|
|
@ -238,7 +239,7 @@ class RootFlowNode(
|
|||
}
|
||||
is NavTarget.NotLoggedInFlow -> {
|
||||
val callback = object : NotLoggedInFlowNode.Callback {
|
||||
override fun onOpenBugReport() {
|
||||
override fun navigateToBugReport() {
|
||||
backstack.push(NavTarget.BugReport)
|
||||
}
|
||||
}
|
||||
|
|
@ -248,11 +249,13 @@ class RootFlowNode(
|
|||
createNode<NotLoggedInFlowNode>(buildContext, plugins = listOf(params, callback))
|
||||
}
|
||||
is NavTarget.SignedOutFlow -> {
|
||||
signedOutEntryPoint.nodeBuilder(this, buildContext).params(
|
||||
SignedOutEntryPoint.Params(
|
||||
sessionId = navTarget.sessionId
|
||||
)
|
||||
).build()
|
||||
signedOutEntryPoint.createNode(
|
||||
parentNode = this,
|
||||
buildContext = buildContext,
|
||||
params = SignedOutEntryPoint.Params(
|
||||
sessionId = navTarget.sessionId,
|
||||
),
|
||||
)
|
||||
}
|
||||
NavTarget.SplashScreen -> emptyNode(buildContext)
|
||||
NavTarget.BugReport -> {
|
||||
|
|
@ -261,11 +264,15 @@ class RootFlowNode(
|
|||
backstack.pop()
|
||||
}
|
||||
}
|
||||
bugReportEntryPoint.nodeBuilder(this, buildContext).callback(callback).build()
|
||||
bugReportEntryPoint.createNode(
|
||||
parentNode = this,
|
||||
buildContext = buildContext,
|
||||
callback = callback,
|
||||
)
|
||||
}
|
||||
is NavTarget.AccountSelect -> {
|
||||
val callback: AccountSelectEntryPoint.Callback = object : AccountSelectEntryPoint.Callback {
|
||||
override fun onSelectAccount(sessionId: SessionId) {
|
||||
override fun onAccountSelected(sessionId: SessionId) {
|
||||
lifecycleScope.launch {
|
||||
if (sessionId == navTarget.currentSessionId) {
|
||||
// Ensure that the account selection Node is removed from the backstack
|
||||
|
|
@ -286,7 +293,11 @@ class RootFlowNode(
|
|||
backstack.pop()
|
||||
}
|
||||
}
|
||||
accountSelectEntryPoint.nodeBuilder(this, buildContext).callback(callback).build()
|
||||
accountSelectEntryPoint.createNode(
|
||||
parentNode = this,
|
||||
buildContext = buildContext,
|
||||
callback = callback,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -338,7 +349,7 @@ class RootFlowNode(
|
|||
} else {
|
||||
// wait for the current session to be restored
|
||||
val loggedInFlowNode = attachSession(latestSessionId)
|
||||
if (sessionStore.getAllSessions().size > 1) {
|
||||
if (sessionStore.numberOfSessions() > 1) {
|
||||
// Several accounts, let the user choose which one to use
|
||||
backstack.push(
|
||||
NavTarget.AccountSelect(
|
||||
|
|
@ -368,7 +379,7 @@ class RootFlowNode(
|
|||
is PermalinkData.FallbackLink -> Unit
|
||||
is PermalinkData.RoomEmailInviteLink -> Unit
|
||||
else -> {
|
||||
if (sessionStore.getAllSessions().size > 1) {
|
||||
if (sessionStore.numberOfSessions() > 1) {
|
||||
// Several accounts, let the user choose which one to use
|
||||
backstack.push(
|
||||
NavTarget.AccountSelect(
|
||||
|
|
@ -391,13 +402,19 @@ class RootFlowNode(
|
|||
is PermalinkData.FallbackLink -> Unit
|
||||
is PermalinkData.RoomEmailInviteLink -> Unit
|
||||
is PermalinkData.RoomLink -> {
|
||||
// If there is a thread id, focus on it in the main timeline
|
||||
val focusedEventId = if (permalinkData.threadId != null) {
|
||||
permalinkData.threadId?.asEventId()
|
||||
} else {
|
||||
permalinkData.eventId
|
||||
}
|
||||
attachRoom(
|
||||
roomIdOrAlias = permalinkData.roomIdOrAlias,
|
||||
trigger = JoinedRoom.Trigger.MobilePermalink,
|
||||
serverNames = permalinkData.viaParameters,
|
||||
eventId = permalinkData.eventId,
|
||||
eventId = focusedEventId,
|
||||
clearBackstack = true
|
||||
)
|
||||
).maybeAttachThread(permalinkData.threadId, permalinkData.eventId)
|
||||
}
|
||||
is PermalinkData.UserLink -> {
|
||||
attachUser(permalinkData.userId)
|
||||
|
|
@ -405,12 +422,24 @@ class RootFlowNode(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun RoomFlowNode.maybeAttachThread(threadId: ThreadId?, focusedEventId: EventId?) {
|
||||
if (threadId != null) {
|
||||
attachThread(threadId, focusedEventId)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun navigateTo(deeplinkData: DeeplinkData) {
|
||||
Timber.d("Navigating to $deeplinkData")
|
||||
attachSession(deeplinkData.sessionId).apply {
|
||||
attachSession(deeplinkData.sessionId).let { loggedInFlowNode ->
|
||||
when (deeplinkData) {
|
||||
is DeeplinkData.Root -> Unit // The room list will always be shown, observing FtueState
|
||||
is DeeplinkData.Room -> attachRoom(deeplinkData.roomId.toRoomIdOrAlias(), clearBackstack = true)
|
||||
is DeeplinkData.Room -> {
|
||||
loggedInFlowNode.attachRoom(
|
||||
roomIdOrAlias = deeplinkData.roomId.toRoomIdOrAlias(),
|
||||
eventId = if (deeplinkData.threadId != null) deeplinkData.threadId?.asEventId() else deeplinkData.eventId,
|
||||
clearBackstack = true,
|
||||
).maybeAttachThread(deeplinkData.threadId, deeplinkData.eventId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import com.bumble.appyx.core.state.MutableSavedStateMap
|
|||
import com.bumble.appyx.core.state.SavedStateMap
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||
|
|
@ -33,7 +32,6 @@ private const val SAVE_INSTANCE_KEY = "io.element.android.x.di.MatrixClientsHold
|
|||
*/
|
||||
@SingleIn(AppScope::class)
|
||||
@ContributesBinding(AppScope::class)
|
||||
@Inject
|
||||
class MatrixSessionCache(
|
||||
private val authenticationService: MatrixAuthenticationService,
|
||||
private val syncOrchestratorFactory: SyncOrchestrator.Factory,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.appnav.di
|
||||
|
||||
import io.element.android.features.messages.api.pinned.PinnedEventsTimelineProvider
|
||||
import io.element.android.libraries.matrix.api.timeline.TimelineProvider
|
||||
|
||||
interface TimelineBindings {
|
||||
val timelineProvider: TimelineProvider
|
||||
val pinnedEventsTimelineProvider: PinnedEventsTimelineProvider
|
||||
}
|
||||
|
|
@ -12,10 +12,10 @@ import androidx.compose.ui.Modifier
|
|||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
|
|
@ -32,18 +32,14 @@ class LoggedInNode(
|
|||
fun navigateToNotificationTroubleshoot()
|
||||
}
|
||||
|
||||
private fun navigateToNotificationTroubleshoot() {
|
||||
plugins<Callback>().forEach {
|
||||
it.navigateToNotificationTroubleshoot()
|
||||
}
|
||||
}
|
||||
private val callback: Callback = callback()
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val loggedInState = loggedInPresenter.present()
|
||||
LoggedInView(
|
||||
state = loggedInState,
|
||||
navigateToNotificationTroubleshoot = ::navigateToNotificationTroubleshoot,
|
||||
navigateToNotificationTroubleshoot = callback::navigateToNotificationTroubleshoot,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ class LoggedInPresenter(
|
|||
|
||||
private suspend fun ensurePusherIsRegistered(pusherRegistrationState: MutableState<AsyncData<Unit>>) {
|
||||
Timber.tag(pusherTag.value).d("Ensure pusher is registered")
|
||||
val currentPushProvider = pushService.getCurrentPushProvider()
|
||||
val currentPushProvider = pushService.getCurrentPushProvider(matrixClient.sessionId)
|
||||
val result = if (currentPushProvider == null) {
|
||||
Timber.tag(pusherTag.value).d("Register with the first available push provider with at least one distributor")
|
||||
val pushProvider = pushService.getAvailablePushProviders()
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@ package io.element.android.appnav.room
|
|||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
|
|
@ -38,24 +36,24 @@ import io.element.android.libraries.architecture.BaseFlowNode
|
|||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.core.bool.orFalse
|
||||
import io.element.android.libraries.core.coroutine.withPreviousValue
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||
import io.element.android.libraries.matrix.ui.room.LoadingRoomState
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
|
@ -71,7 +69,6 @@ class RoomFlowNode(
|
|||
private val client: MatrixClient,
|
||||
private val joinRoomEntryPoint: JoinRoomEntryPoint,
|
||||
private val roomAliasResolverEntryPoint: RoomAliasResolverEntryPoint,
|
||||
private val syncService: SyncService,
|
||||
private val membershipObserver: RoomMembershipObserver,
|
||||
private val spaceEntryPoint: SpaceEntryPoint,
|
||||
) : BaseFlowNode<RoomFlowNode.NavTarget>(
|
||||
|
|
@ -133,7 +130,6 @@ class RoomFlowNode(
|
|||
|
||||
private fun subscribeToRoomInfoFlow(roomId: RoomId, serverNames: List<String>) {
|
||||
val roomInfoFlow = client.getRoomInfoFlow(roomId)
|
||||
val isSpaceFlow = roomInfoFlow.map { it.getOrNull()?.isSpace.orFalse() }.distinctUntilChanged()
|
||||
|
||||
// This observes the local membership changes for the room
|
||||
val membershipUpdateFlow = membershipObserver.updates
|
||||
|
|
@ -146,14 +142,10 @@ class RoomFlowNode(
|
|||
.map { it.getOrNull()?.currentUserMembership }
|
||||
.distinctUntilChanged()
|
||||
.withPreviousValue()
|
||||
combine(currentMembershipFlow, isSpaceFlow) { (previousMembership, membership), isSpace ->
|
||||
currentMembershipFlow.onEach { (previousMembership, membership) ->
|
||||
Timber.d("Room membership: $membership")
|
||||
if (membership == CurrentUserMembership.JOINED) {
|
||||
if (isSpace) {
|
||||
backstack.newRoot(NavTarget.JoinedSpace(spaceId = roomId))
|
||||
} else {
|
||||
backstack.newRoot(NavTarget.JoinedRoom(roomId))
|
||||
}
|
||||
backstack.newRoot(NavTarget.JoinedRoom(roomId))
|
||||
} else {
|
||||
val leavingFromCurrentDevice =
|
||||
membership == CurrentUserMembership.LEFT &&
|
||||
|
|
@ -188,10 +180,12 @@ class RoomFlowNode(
|
|||
}
|
||||
}
|
||||
val params = Params(navTarget.roomAlias)
|
||||
roomAliasResolverEntryPoint.nodeBuilder(this, buildContext)
|
||||
.callback(callback)
|
||||
.params(params)
|
||||
.build()
|
||||
roomAliasResolverEntryPoint.createNode(
|
||||
parentNode = this,
|
||||
buildContext = buildContext,
|
||||
params = params,
|
||||
callback = callback,
|
||||
)
|
||||
}
|
||||
is NavTarget.JoinRoom -> {
|
||||
val inputs = JoinRoomEntryPoint.Inputs(
|
||||
|
|
@ -201,7 +195,11 @@ class RoomFlowNode(
|
|||
serverNames = navTarget.serverNames,
|
||||
trigger = navTarget.trigger,
|
||||
)
|
||||
joinRoomEntryPoint.createNode(this, buildContext, inputs)
|
||||
joinRoomEntryPoint.createNode(
|
||||
parentNode = this,
|
||||
buildContext = buildContext,
|
||||
inputs = inputs,
|
||||
)
|
||||
}
|
||||
is NavTarget.JoinedRoom -> {
|
||||
val roomFlowNodeCallback = plugins<JoinedRoomLoadedFlowNode.Callback>()
|
||||
|
|
@ -213,19 +211,24 @@ class RoomFlowNode(
|
|||
}
|
||||
is NavTarget.JoinedSpace -> {
|
||||
val spaceCallback = plugins<SpaceEntryPoint.Callback>().single()
|
||||
spaceEntryPoint.nodeBuilder(this, buildContext)
|
||||
.inputs(SpaceEntryPoint.Inputs(roomId = navTarget.spaceId))
|
||||
.callback(spaceCallback)
|
||||
.build()
|
||||
spaceEntryPoint.createNode(
|
||||
parentNode = this,
|
||||
buildContext = buildContext,
|
||||
inputs = SpaceEntryPoint.Inputs(roomId = navTarget.spaceId),
|
||||
callback = spaceCallback,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun attachThread(threadId: ThreadId, focusedEventId: EventId?) {
|
||||
waitForChildAttached<JoinedRoomFlowNode>()
|
||||
.attachThread(threadId, focusedEventId)
|
||||
}
|
||||
|
||||
private fun loadingNode(buildContext: BuildContext) = node(buildContext) { modifier ->
|
||||
val isOnline by syncService.isOnline.collectAsState()
|
||||
LoadingRoomNodeView(
|
||||
state = LoadingRoomState.Loading,
|
||||
hasNetworkConnection = isOnline,
|
||||
onBackClick = { navigateUp() },
|
||||
modifier = modifier,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ import kotlinx.parcelize.Parcelize
|
|||
|
||||
sealed interface RoomNavigationTarget : Parcelable {
|
||||
@Parcelize
|
||||
data class Messages(val focusedEventId: EventId? = null) : RoomNavigationTarget
|
||||
data class Root(
|
||||
val eventId: EventId? = null,
|
||||
) : RoomNavigationTarget
|
||||
|
||||
@Parcelize
|
||||
data object Details : RoomNavigationTarget
|
||||
|
|
|
|||
|
|
@ -34,8 +34,9 @@ import io.element.android.libraries.architecture.NodeInputs
|
|||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.matrix.ui.room.LoadingRoomState
|
||||
import io.element.android.libraries.matrix.ui.room.LoadingRoomStateFlowFactory
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
|
@ -50,7 +51,6 @@ class JoinedRoomFlowNode(
|
|||
@Assisted val buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
loadingRoomStateFlowFactory: LoadingRoomStateFlowFactory,
|
||||
private val syncService: SyncService,
|
||||
) :
|
||||
BaseFlowNode<JoinedRoomFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
|
|
@ -116,15 +116,18 @@ class JoinedRoomFlowNode(
|
|||
|
||||
private fun loadingNode(buildContext: BuildContext, onBackClick: () -> Unit) = node(buildContext) { modifier ->
|
||||
val loadingRoomState by loadingRoomStateStateFlow.collectAsState()
|
||||
val isOnline by syncService.isOnline.collectAsState()
|
||||
LoadingRoomNodeView(
|
||||
state = loadingRoomState,
|
||||
hasNetworkConnection = isOnline,
|
||||
modifier = modifier,
|
||||
onBackClick = onBackClick
|
||||
onBackClick = onBackClick,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun attachThread(threadId: ThreadId, focusedEventId: EventId?) {
|
||||
waitForChildAttached<JoinedRoomLoadedFlowNode>()
|
||||
.attachThread(threadId, focusedEventId)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
BackstackView(
|
||||
|
|
|
|||
|
|
@ -16,24 +16,31 @@ import com.bumble.appyx.core.modality.BuildContext
|
|||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.pop
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.appnav.di.RoomGraphFactory
|
||||
import io.element.android.appnav.di.TimelineBindings
|
||||
import io.element.android.appnav.room.RoomNavigationTarget
|
||||
import io.element.android.features.forward.api.ForwardEntryPoint
|
||||
import io.element.android.features.messages.api.MessagesEntryPoint
|
||||
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
||||
import io.element.android.features.space.api.SpaceEntryPoint
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.architecture.waitForChildAttached
|
||||
import io.element.android.libraries.di.DependencyInjectionGraphOwner
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
|
|
@ -51,6 +58,8 @@ class JoinedRoomLoadedFlowNode(
|
|||
@Assisted plugins: List<Plugin>,
|
||||
private val messagesEntryPoint: MessagesEntryPoint,
|
||||
private val roomDetailsEntryPoint: RoomDetailsEntryPoint,
|
||||
private val spaceEntryPoint: SpaceEntryPoint,
|
||||
private val forwardEntryPoint: ForwardEntryPoint,
|
||||
private val appNavigationStateService: AppNavigationStateService,
|
||||
@SessionCoroutineScope
|
||||
private val sessionCoroutineScope: CoroutineScope,
|
||||
|
|
@ -59,21 +68,16 @@ class JoinedRoomLoadedFlowNode(
|
|||
roomGraphFactory: RoomGraphFactory,
|
||||
) : BaseFlowNode<JoinedRoomLoadedFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = when (val input = plugins.filterIsInstance<Inputs>().first().initialElement) {
|
||||
is RoomNavigationTarget.Messages -> NavTarget.Messages(input.focusedEventId)
|
||||
RoomNavigationTarget.Details -> NavTarget.RoomDetails
|
||||
RoomNavigationTarget.NotificationSettings -> NavTarget.RoomNotificationSettings
|
||||
},
|
||||
initialElement = initialElement(plugins),
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
), DependencyInjectionGraphOwner {
|
||||
interface Callback : Plugin {
|
||||
fun onOpenRoom(roomId: RoomId, serverNames: List<String>)
|
||||
fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean)
|
||||
fun onForwardedToSingleRoom(roomId: RoomId)
|
||||
fun onOpenGlobalNotificationSettings()
|
||||
fun navigateToRoom(roomId: RoomId, serverNames: List<String>)
|
||||
fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean)
|
||||
fun navigateToGlobalNotificationSettings()
|
||||
}
|
||||
|
||||
data class Inputs(
|
||||
|
|
@ -82,7 +86,7 @@ class JoinedRoomLoadedFlowNode(
|
|||
) : NodeInputs
|
||||
|
||||
private val inputs: Inputs = inputs()
|
||||
private val callbacks = plugins.filterIsInstance<Callback>()
|
||||
private val callback: Callback = callback()
|
||||
override val graph = roomGraphFactory.create(inputs.room)
|
||||
|
||||
init {
|
||||
|
|
@ -118,26 +122,28 @@ class JoinedRoomLoadedFlowNode(
|
|||
|
||||
private fun createRoomDetailsNode(buildContext: BuildContext, initialTarget: RoomDetailsEntryPoint.InitialTarget): Node {
|
||||
val callback = object : RoomDetailsEntryPoint.Callback {
|
||||
override fun onOpenGlobalNotificationSettings() {
|
||||
callbacks.forEach { it.onOpenGlobalNotificationSettings() }
|
||||
override fun navigateToGlobalNotificationSettings() {
|
||||
callback.navigateToGlobalNotificationSettings()
|
||||
}
|
||||
|
||||
override fun onOpenRoom(roomId: RoomId, serverNames: List<String>) {
|
||||
callbacks.forEach { it.onOpenRoom(roomId, serverNames) }
|
||||
override fun navigateToRoom(roomId: RoomId, serverNames: List<String>) {
|
||||
callback.navigateToRoom(roomId, serverNames)
|
||||
}
|
||||
|
||||
override fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) {
|
||||
callbacks.forEach { it.onPermalinkClick(data, pushToBackstack) }
|
||||
override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) {
|
||||
callback.handlePermalinkClick(data, pushToBackstack)
|
||||
}
|
||||
|
||||
override fun onForwardedToSingleRoom(roomId: RoomId) {
|
||||
callbacks.forEach { it.onForwardedToSingleRoom(roomId) }
|
||||
override fun startForwardEventFlow(eventId: EventId, fromPinnedEvents: Boolean) {
|
||||
backstack.push(NavTarget.ForwardEvent(eventId, fromPinnedEvents))
|
||||
}
|
||||
}
|
||||
return roomDetailsEntryPoint.nodeBuilder(this, buildContext)
|
||||
.params(RoomDetailsEntryPoint.Params(initialTarget))
|
||||
.callback(callback)
|
||||
.build()
|
||||
return roomDetailsEntryPoint.createNode(
|
||||
parentNode = this,
|
||||
buildContext = buildContext,
|
||||
params = RoomDetailsEntryPoint.Params(initialTarget),
|
||||
callback = callback,
|
||||
)
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
|
|
@ -154,55 +160,144 @@ class JoinedRoomLoadedFlowNode(
|
|||
NavTarget.RoomNotificationSettings -> {
|
||||
createRoomDetailsNode(buildContext, RoomDetailsEntryPoint.InitialTarget.RoomNotificationSettings)
|
||||
}
|
||||
NavTarget.RoomMemberList -> {
|
||||
createRoomDetailsNode(buildContext, RoomDetailsEntryPoint.InitialTarget.RoomMemberList)
|
||||
}
|
||||
NavTarget.Space -> {
|
||||
createSpaceNode(buildContext)
|
||||
}
|
||||
is NavTarget.ForwardEvent -> {
|
||||
val timelineProvider = if (navTarget.fromPinnedEvents) {
|
||||
(graph as TimelineBindings).pinnedEventsTimelineProvider
|
||||
} else {
|
||||
(graph as TimelineBindings).timelineProvider
|
||||
}
|
||||
val params = ForwardEntryPoint.Params(navTarget.eventId, timelineProvider)
|
||||
val callback = object : ForwardEntryPoint.Callback {
|
||||
override fun onDone(roomIds: List<RoomId>) {
|
||||
backstack.pop()
|
||||
roomIds.singleOrNull()?.let { roomId ->
|
||||
callback.navigateToRoom(roomId, emptyList())
|
||||
}
|
||||
}
|
||||
}
|
||||
forwardEntryPoint.createNode(
|
||||
parentNode = this,
|
||||
buildContext = buildContext,
|
||||
params = params,
|
||||
callback = callback,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSpaceNode(buildContext: BuildContext): Node {
|
||||
val callback = object : SpaceEntryPoint.Callback {
|
||||
override fun navigateToRoom(roomId: RoomId, viaParameters: List<String>) {
|
||||
callback.navigateToRoom(roomId, viaParameters)
|
||||
}
|
||||
|
||||
override fun navigateToRoomDetails() {
|
||||
backstack.push(NavTarget.RoomDetails)
|
||||
}
|
||||
|
||||
override fun navigateToRoomMemberList() {
|
||||
backstack.push(NavTarget.RoomMemberList)
|
||||
}
|
||||
}
|
||||
return spaceEntryPoint.createNode(
|
||||
parentNode = this,
|
||||
buildContext = buildContext,
|
||||
inputs = SpaceEntryPoint.Inputs(roomId = inputs.room.roomId),
|
||||
callback = callback,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createMessagesNode(
|
||||
buildContext: BuildContext,
|
||||
navTarget: NavTarget.Messages,
|
||||
): Node {
|
||||
val callback = object : MessagesEntryPoint.Callback {
|
||||
override fun onRoomDetailsClick() {
|
||||
override fun navigateToRoomDetails() {
|
||||
backstack.push(NavTarget.RoomDetails)
|
||||
}
|
||||
|
||||
override fun onUserDataClick(userId: UserId) {
|
||||
override fun navigateToRoomMemberDetails(userId: UserId) {
|
||||
backstack.push(NavTarget.RoomMemberDetails(userId))
|
||||
}
|
||||
|
||||
override fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) {
|
||||
callbacks.forEach { it.onPermalinkClick(data, pushToBackstack) }
|
||||
override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) {
|
||||
callback.handlePermalinkClick(data, pushToBackstack)
|
||||
}
|
||||
|
||||
override fun onForwardedToSingleRoom(roomId: RoomId) {
|
||||
callbacks.forEach { it.onForwardedToSingleRoom(roomId) }
|
||||
override fun forwardEvent(eventId: EventId, fromPinnedEvents: Boolean) {
|
||||
backstack.push(NavTarget.ForwardEvent(eventId, fromPinnedEvents))
|
||||
}
|
||||
|
||||
override fun navigateToRoom(roomId: RoomId) {
|
||||
callback.navigateToRoom(roomId, emptyList())
|
||||
}
|
||||
}
|
||||
val params = MessagesEntryPoint.Params(
|
||||
MessagesEntryPoint.InitialTarget.Messages(navTarget.focusedEventId)
|
||||
)
|
||||
return messagesEntryPoint.nodeBuilder(this, buildContext)
|
||||
.params(params)
|
||||
.callback(callback)
|
||||
.build()
|
||||
return messagesEntryPoint.createNode(
|
||||
parentNode = this,
|
||||
buildContext = buildContext,
|
||||
params = params,
|
||||
callback = callback,
|
||||
)
|
||||
}
|
||||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
data class Messages(val focusedEventId: EventId? = null) : NavTarget
|
||||
data object Space : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class Messages(
|
||||
val focusedEventId: EventId? = null,
|
||||
) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object RoomDetails : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object RoomMemberList : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class RoomMemberDetails(val userId: UserId) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class ForwardEvent(val eventId: EventId, val fromPinnedEvents: Boolean) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object RoomNotificationSettings : NavTarget
|
||||
}
|
||||
|
||||
suspend fun attachThread(threadId: ThreadId, focusedEventId: EventId?) {
|
||||
val messageNode = waitForChildAttached<Node, NavTarget> { navTarget ->
|
||||
navTarget is NavTarget.Messages
|
||||
}
|
||||
(messageNode as? MessagesEntryPoint.NodeProxy)?.attachThread(threadId, focusedEventId)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
BackstackView()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initialElement(plugins: List<Plugin>): JoinedRoomLoadedFlowNode.NavTarget {
|
||||
val input = plugins.filterIsInstance<JoinedRoomLoadedFlowNode.Inputs>().single()
|
||||
return when (input.initialElement) {
|
||||
is RoomNavigationTarget.Root -> {
|
||||
if (input.room.roomInfoFlow.value.isSpace) {
|
||||
JoinedRoomLoadedFlowNode.NavTarget.Space
|
||||
} else {
|
||||
JoinedRoomLoadedFlowNode.NavTarget.Messages(input.initialElement.eventId)
|
||||
}
|
||||
}
|
||||
RoomNavigationTarget.Details -> JoinedRoomLoadedFlowNode.NavTarget.RoomDetails
|
||||
RoomNavigationTarget.NotificationSettings -> JoinedRoomLoadedFlowNode.NavTarget.RoomNotificationSettings
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@
|
|||
package io.element.android.appnav.room.joined
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
|
|
@ -21,9 +19,6 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
|
|
@ -31,6 +26,7 @@ import io.element.android.libraries.designsystem.theme.components.CircularProgre
|
|||
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.TopAppBar
|
||||
import io.element.android.libraries.designsystem.utils.DelayedVisibility
|
||||
import io.element.android.libraries.matrix.ui.room.LoadingRoomState
|
||||
import io.element.android.libraries.matrix.ui.room.LoadingRoomStateProvider
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
|
@ -38,17 +34,13 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
|||
@Composable
|
||||
fun LoadingRoomNodeView(
|
||||
state: LoadingRoomState,
|
||||
hasNetworkConnection: Boolean,
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
Column {
|
||||
ConnectivityIndicatorView(isOnline = hasNetworkConnection)
|
||||
LoadingRoomTopBar(onBackClick)
|
||||
}
|
||||
LoadingRoomTopBar(onBackClick)
|
||||
},
|
||||
content = { padding ->
|
||||
Box(
|
||||
|
|
@ -66,7 +58,9 @@ fun LoadingRoomNodeView(
|
|||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
)
|
||||
} else {
|
||||
CircularProgressIndicator()
|
||||
DelayedVisibility {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -83,9 +77,7 @@ private fun LoadingRoomTopBar(
|
|||
BackButton(onClick = onBackClick)
|
||||
},
|
||||
title = {
|
||||
IconTitlePlaceholdersRowMolecule(iconSize = AvatarSize.TimelineRoom.dp)
|
||||
},
|
||||
windowInsets = WindowInsets(0.dp),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -94,7 +86,6 @@ private fun LoadingRoomTopBar(
|
|||
internal fun LoadingRoomNodeViewPreview(@PreviewParameter(LoadingRoomStateProvider::class) state: LoadingRoomState) = ElementPreview {
|
||||
LoadingRoomNodeView(
|
||||
state = state,
|
||||
onBackClick = {},
|
||||
hasNetworkConnection = false
|
||||
onBackClick = {}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,22 +19,30 @@ import com.bumble.appyx.testing.unit.common.helper.parentNodeTestHelper
|
|||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.appnav.di.RoomGraphFactory
|
||||
import io.element.android.appnav.room.RoomNavigationTarget
|
||||
import io.element.android.appnav.room.joined.FakeJoinedRoomLoadedFlowNodeCallback
|
||||
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
|
||||
import io.element.android.features.forward.api.ForwardEntryPoint
|
||||
import io.element.android.features.forward.test.FakeForwardEntryPoint
|
||||
import io.element.android.features.messages.api.MessagesEntryPoint
|
||||
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
||||
import io.element.android.features.space.api.SpaceEntryPoint
|
||||
import io.element.android.libraries.architecture.childNode
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class JoinedRoomLoadedFlowNodeTest {
|
||||
@get:Rule
|
||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
|
@ -42,29 +50,20 @@ class JoinedRoomLoadedFlowNodeTest {
|
|||
@get:Rule
|
||||
val mainDispatcherRule = MainDispatcherRule()
|
||||
|
||||
private class FakeMessagesEntryPoint : MessagesEntryPoint, MessagesEntryPoint.NodeBuilder {
|
||||
var buildContext: BuildContext? = null
|
||||
private class FakeMessagesEntryPoint : MessagesEntryPoint {
|
||||
var nodeId: String? = null
|
||||
var parameters: MessagesEntryPoint.Params? = null
|
||||
var callback: MessagesEntryPoint.Callback? = null
|
||||
|
||||
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): MessagesEntryPoint.NodeBuilder {
|
||||
this.buildContext = buildContext
|
||||
return this
|
||||
}
|
||||
|
||||
override fun params(params: MessagesEntryPoint.Params): MessagesEntryPoint.NodeBuilder {
|
||||
override fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
params: MessagesEntryPoint.Params,
|
||||
callback: MessagesEntryPoint.Callback,
|
||||
): Node {
|
||||
parameters = params
|
||||
return this
|
||||
}
|
||||
|
||||
override fun callback(callback: MessagesEntryPoint.Callback): MessagesEntryPoint.NodeBuilder {
|
||||
this.callback = callback
|
||||
return this
|
||||
}
|
||||
|
||||
override fun build(): Node {
|
||||
return node(buildContext!!) {}.also {
|
||||
return node(buildContext) {}.also {
|
||||
nodeId = it.id
|
||||
}
|
||||
}
|
||||
|
|
@ -79,22 +78,26 @@ class JoinedRoomLoadedFlowNodeTest {
|
|||
private class FakeRoomDetailsEntryPoint : RoomDetailsEntryPoint {
|
||||
var nodeId: String? = null
|
||||
|
||||
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): RoomDetailsEntryPoint.NodeBuilder {
|
||||
return object : RoomDetailsEntryPoint.NodeBuilder {
|
||||
override fun params(params: RoomDetailsEntryPoint.Params): RoomDetailsEntryPoint.NodeBuilder {
|
||||
return this
|
||||
}
|
||||
override fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
params: RoomDetailsEntryPoint.Params,
|
||||
callback: RoomDetailsEntryPoint.Callback,
|
||||
) = node(buildContext) {}.also {
|
||||
nodeId = it.id
|
||||
}
|
||||
}
|
||||
|
||||
override fun callback(callback: RoomDetailsEntryPoint.Callback): RoomDetailsEntryPoint.NodeBuilder {
|
||||
return this
|
||||
}
|
||||
private class FakeSpaceEntryPoint : SpaceEntryPoint {
|
||||
var nodeId: String? = null
|
||||
|
||||
override fun build(): Node {
|
||||
return node(buildContext) {}.also {
|
||||
nodeId = it.id
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
inputs: SpaceEntryPoint.Inputs,
|
||||
callback: SpaceEntryPoint.Callback,
|
||||
) = node(buildContext) {}.also {
|
||||
nodeId = it.id
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -102,27 +105,32 @@ class JoinedRoomLoadedFlowNodeTest {
|
|||
plugins: List<Plugin>,
|
||||
messagesEntryPoint: MessagesEntryPoint = FakeMessagesEntryPoint(),
|
||||
roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(),
|
||||
spaceEntryPoint: SpaceEntryPoint = FakeSpaceEntryPoint(),
|
||||
forwardEntryPoint: ForwardEntryPoint = FakeForwardEntryPoint(),
|
||||
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
|
||||
matrixClient: FakeMatrixClient = FakeMatrixClient(),
|
||||
) = JoinedRoomLoadedFlowNode(
|
||||
buildContext = BuildContext.root(savedStateMap = null),
|
||||
plugins = plugins,
|
||||
messagesEntryPoint = messagesEntryPoint,
|
||||
roomDetailsEntryPoint = roomDetailsEntryPoint,
|
||||
spaceEntryPoint = spaceEntryPoint,
|
||||
forwardEntryPoint = forwardEntryPoint,
|
||||
appNavigationStateService = FakeAppNavigationStateService(),
|
||||
sessionCoroutineScope = this,
|
||||
sessionCoroutineScope = backgroundScope,
|
||||
roomGraphFactory = FakeRoomGraphFactory(),
|
||||
matrixClient = FakeMatrixClient(),
|
||||
matrixClient = matrixClient,
|
||||
activeRoomsHolder = activeRoomsHolder,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `given a room flow node when initialized then it loads messages entry point`() = runTest {
|
||||
fun `given a room flow node when initialized then it loads messages entry point if room is not space`() = runTest {
|
||||
// GIVEN
|
||||
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {}))
|
||||
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {}, initialRoomInfo = aRoomInfo(isSpace = false)))
|
||||
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
||||
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages())
|
||||
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Root())
|
||||
val roomFlowNode = createJoinedRoomLoadedFlowNode(
|
||||
plugins = listOf(inputs),
|
||||
plugins = listOf(inputs, FakeJoinedRoomLoadedFlowNodeCallback()),
|
||||
messagesEntryPoint = fakeMessagesEntryPoint,
|
||||
)
|
||||
// WHEN
|
||||
|
|
@ -135,21 +143,41 @@ class JoinedRoomLoadedFlowNodeTest {
|
|||
assertThat(messagesNode.id).isEqualTo(fakeMessagesEntryPoint.nodeId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a room flow node when initialized then it loads space entry point if room is space`() = runTest {
|
||||
// GIVEN
|
||||
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {}, initialRoomInfo = aRoomInfo(isSpace = true)))
|
||||
val spaceEntryPoint = FakeSpaceEntryPoint()
|
||||
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Root())
|
||||
val roomFlowNode = createJoinedRoomLoadedFlowNode(
|
||||
plugins = listOf(inputs, FakeJoinedRoomLoadedFlowNodeCallback()),
|
||||
spaceEntryPoint = spaceEntryPoint,
|
||||
)
|
||||
// WHEN
|
||||
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
|
||||
|
||||
// THEN
|
||||
assertThat(roomFlowNode.backstack.activeElement).isEqualTo(JoinedRoomLoadedFlowNode.NavTarget.Space)
|
||||
roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.Space, Lifecycle.State.CREATED)
|
||||
val spaceNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.Space)!!
|
||||
assertThat(spaceNode.id).isEqualTo(spaceEntryPoint.nodeId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a room flow node when callback on room details is triggered then it loads room details entry point`() = runTest {
|
||||
// GIVEN
|
||||
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {}))
|
||||
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
||||
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
|
||||
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages())
|
||||
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Root())
|
||||
val roomFlowNode = createJoinedRoomLoadedFlowNode(
|
||||
plugins = listOf(inputs),
|
||||
plugins = listOf(inputs, FakeJoinedRoomLoadedFlowNodeCallback()),
|
||||
messagesEntryPoint = fakeMessagesEntryPoint,
|
||||
roomDetailsEntryPoint = fakeRoomDetailsEntryPoint,
|
||||
)
|
||||
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
|
||||
// WHEN
|
||||
fakeMessagesEntryPoint.callback?.onRoomDetailsClick()
|
||||
fakeMessagesEntryPoint.callback?.navigateToRoomDetails()
|
||||
// THEN
|
||||
roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.RoomDetails, Lifecycle.State.CREATED)
|
||||
val roomDetailsNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.RoomDetails)!!
|
||||
|
|
@ -162,10 +190,10 @@ class JoinedRoomLoadedFlowNodeTest {
|
|||
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {}))
|
||||
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
||||
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
|
||||
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages())
|
||||
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Root())
|
||||
val activeRoomsHolder = ActiveRoomsHolder()
|
||||
val roomFlowNode = createJoinedRoomLoadedFlowNode(
|
||||
plugins = listOf(inputs),
|
||||
plugins = listOf(inputs, FakeJoinedRoomLoadedFlowNodeCallback()),
|
||||
messagesEntryPoint = fakeMessagesEntryPoint,
|
||||
roomDetailsEntryPoint = fakeRoomDetailsEntryPoint,
|
||||
activeRoomsHolder = activeRoomsHolder,
|
||||
|
|
@ -185,12 +213,12 @@ class JoinedRoomLoadedFlowNodeTest {
|
|||
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {}))
|
||||
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
||||
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
|
||||
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages())
|
||||
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Root())
|
||||
val activeRoomsHolder = ActiveRoomsHolder().apply {
|
||||
addRoom(room)
|
||||
}
|
||||
val roomFlowNode = createJoinedRoomLoadedFlowNode(
|
||||
plugins = listOf(inputs),
|
||||
plugins = listOf(inputs, FakeJoinedRoomLoadedFlowNodeCallback()),
|
||||
messagesEntryPoint = fakeMessagesEntryPoint,
|
||||
roomDetailsEntryPoint = fakeRoomDetailsEntryPoint,
|
||||
activeRoomsHolder = activeRoomsHolder,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import io.element.android.features.login.test.FakeLoginIntentResolver
|
|||
import io.element.android.libraries.deeplink.api.DeeplinkData
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_THREAD_ID
|
||||
|
|
@ -67,6 +68,7 @@ class IntentResolverTest {
|
|||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
threadId = null,
|
||||
eventId = null,
|
||||
)
|
||||
)
|
||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||
|
|
@ -79,6 +81,7 @@ class IntentResolverTest {
|
|||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
threadId = null,
|
||||
eventId = null,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -91,6 +94,7 @@ class IntentResolverTest {
|
|||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
threadId = A_THREAD_ID,
|
||||
eventId = null,
|
||||
)
|
||||
)
|
||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||
|
|
@ -103,6 +107,59 @@ class IntentResolverTest {
|
|||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
threadId = A_THREAD_ID,
|
||||
eventId = null,
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test resolve navigation intent event`() {
|
||||
val sut = createIntentResolver(
|
||||
deeplinkParserResult = DeeplinkData.Room(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
threadId = null,
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
)
|
||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
}
|
||||
val result = sut.resolve(intent)
|
||||
assertThat(result).isEqualTo(
|
||||
ResolvedIntent.Navigation(
|
||||
deeplinkData = DeeplinkData.Room(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
threadId = null,
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test resolve navigation intent thread and event`() {
|
||||
val sut = createIntentResolver(
|
||||
deeplinkParserResult = DeeplinkData.Room(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
threadId = A_THREAD_ID,
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
)
|
||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
}
|
||||
val result = sut.resolve(intent)
|
||||
assertThat(result).isEqualTo(
|
||||
ResolvedIntent.Navigation(
|
||||
deeplinkData = DeeplinkData.Room(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
threadId = A_THREAD_ID,
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -487,7 +487,7 @@ class LoggedInPresenterTest {
|
|||
Result.success(Unit)
|
||||
},
|
||||
selectPushProviderLambda: (SessionId, PushProvider) -> Unit = { _, _ -> lambdaError() },
|
||||
currentPushProvider: () -> PushProvider? = { null },
|
||||
currentPushProvider: (SessionId) -> PushProvider? = { null },
|
||||
setIgnoreRegistrationErrorLambda: (SessionId, Boolean) -> Unit = { _, _ -> lambdaError() },
|
||||
): PushService {
|
||||
return FakePushService(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.appnav.room.joined
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeJoinedRoomLoadedFlowNodeCallback : JoinedRoomLoadedFlowNode.Callback {
|
||||
override fun navigateToRoom(roomId: RoomId, serverNames: List<String>) = lambdaError()
|
||||
override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) = lambdaError()
|
||||
override fun navigateToGlobalNotificationSettings() = lambdaError()
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import org.gradle.accessors.dm.LibrariesForLibs
|
||||
|
||||
/*
|
||||
* Copyright 2022-2024 New Vector Ltd.
|
||||
*
|
||||
|
|
@ -27,6 +29,8 @@ tasks.register<Delete>("clean").configure {
|
|||
delete(rootProject.layout.buildDirectory)
|
||||
}
|
||||
|
||||
private val ktLintVersion = the<LibrariesForLibs>().versions.ktlint.get()
|
||||
|
||||
allprojects {
|
||||
// Detekt
|
||||
apply {
|
||||
|
|
@ -56,14 +60,12 @@ allprojects {
|
|||
|
||||
// See https://github.com/JLLeitschuh/ktlint-gradle#configuration
|
||||
configure<org.jlleitschuh.gradle.ktlint.KtlintExtension> {
|
||||
// See https://github.com/pinterest/ktlint/releases/
|
||||
// TODO Regularly check for new version here ^
|
||||
version.set("1.1.1")
|
||||
android.set(true)
|
||||
ignoreFailures.set(false)
|
||||
enableExperimentalRules.set(true)
|
||||
version = ktLintVersion
|
||||
android = true
|
||||
ignoreFailures = false
|
||||
enableExperimentalRules = true
|
||||
// display the corresponding rule
|
||||
verbose.set(true)
|
||||
verbose = true
|
||||
reporters {
|
||||
reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.PLAIN)
|
||||
// To have XML report for Danger
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit c5465c95792004409e0eaa7342171e1cd652914a
|
||||
Subproject commit 19d78b589dfbca08b1e8306bff1a236fa2cdf528
|
||||
2
fastlane/metadata/android/en-US/changelogs/202511000.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/202511000.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
Main changes in this version: fixes an issue that prevented Element Call notifications from being displayed sometimes.
|
||||
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||
2
fastlane/metadata/android/en-US/changelogs/202511020.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/202511020.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
Main changes in this version: bug fixes and improvements.
|
||||
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||
|
|
@ -11,12 +11,10 @@ import com.bumble.appyx.core.modality.BuildContext
|
|||
import com.bumble.appyx.core.node.Node
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.features.analytics.api.AnalyticsEntryPoint
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@Inject
|
||||
class DefaultAnalyticsEntryPoint : AnalyticsEntryPoint {
|
||||
override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
|
||||
return parentNode.createNode<AnalyticsOptInNode>(buildContext)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.features.announcement.api.Announcement
|
||||
import io.element.android.features.announcement.api.AnnouncementService
|
||||
import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementState
|
||||
|
|
@ -29,7 +28,6 @@ import kotlinx.coroutines.flow.combine
|
|||
import kotlinx.coroutines.flow.first
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@Inject
|
||||
class DefaultAnnouncementService(
|
||||
private val announcementStore: AnnouncementStore,
|
||||
private val announcementPresenter: Presenter<AnnouncementState>,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import androidx.datastore.preferences.core.edit
|
|||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.features.announcement.api.Announcement
|
||||
import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
|
@ -21,7 +20,6 @@ private val spaceAnnouncementKey = intPreferencesKey("spaceAnnouncement")
|
|||
private val newNotificationSoundKey = intPreferencesKey("newNotificationSound")
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@Inject
|
||||
class DefaultAnnouncementStore(
|
||||
preferenceDataStoreFactory: PreferenceDataStoreFactory,
|
||||
) : AnnouncementStore {
|
||||
|
|
|
|||
|
|
@ -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">"Zobrazit prostory, které jste vytvořili nebo ke kterým jste se připojili"</string>
|
||||
<string name="screen_space_announcement_item2">"Přijmout nebo odmítnout pozvánky do prostorů"</string>
|
||||
<string name="screen_space_announcement_item3">"Objevte všechny místnosti, do kterých můžete vstoupit ve svých prostorech"</string>
|
||||
<string name="screen_space_announcement_item4">"Připojit se k veřejným prostorům"</string>
|
||||
<string name="screen_space_announcement_item5">"Opustit všechny prostory, ke kterým jste se připojili"</string>
|
||||
<string name="screen_space_announcement_notice">"Filtrování, vytváření a správa prostorů bude brzy k dispozici."</string>
|
||||
<string name="screen_space_announcement_subtitle">"Vítejte v beta verzi prostorů! S touto první verzí můžete:"</string>
|
||||
<string name="screen_space_announcement_title">"Představujeme prostory"</string>
|
||||
</resources>
|
||||
|
|
@ -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">"Zobraziť priestory, ktoré ste vytvorili alebo ku ktorým ste sa pripojili"</string>
|
||||
<string name="screen_space_announcement_item2">"Prijímať alebo odmietať pozvánky do priestorov"</string>
|
||||
<string name="screen_space_announcement_item3">"Objaviť všetky miestnosti, do ktorých sa môžete pripojiť vo svojich priestoroch"</string>
|
||||
<string name="screen_space_announcement_item4">"Pripojiť sa k verejnému priestoru"</string>
|
||||
<string name="screen_space_announcement_item5">"Opustiť akékoľvek priestory, ku ktorým ste sa pridali"</string>
|
||||
<string name="screen_space_announcement_notice">"Filtrovanie, vytváranie a správa priestorov bude čoskoro k dispozícii."</string>
|
||||
<string name="screen_space_announcement_subtitle">"Vitajte v beta verzii priestorov! S touto prvou verziou môžete:"</string>
|
||||
<string name="screen_space_announcement_title">"Predstavujeme priestory"</string>
|
||||
</resources>
|
||||
|
|
@ -9,7 +9,6 @@ package io.element.android.features.cachecleaner.impl
|
|||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.features.cachecleaner.api.CacheCleaner
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
|
|
@ -24,7 +23,6 @@ import java.io.File
|
|||
* Default implementation of [CacheCleaner].
|
||||
*/
|
||||
@ContributesBinding(AppScope::class)
|
||||
@Inject
|
||||
class DefaultCacheCleaner(
|
||||
@AppCoroutineScope
|
||||
private val coroutineScope: CoroutineScope,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ package io.element.android.features.call.impl
|
|||
import android.content.Context
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.features.call.api.CallType
|
||||
import io.element.android.features.call.api.ElementCallEntryPoint
|
||||
import io.element.android.features.call.impl.notifications.CallNotificationData
|
||||
|
|
@ -21,7 +20,6 @@ import io.element.android.libraries.matrix.api.core.EventId
|
|||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@Inject
|
||||
class DefaultElementCallEntryPoint(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val activeCallManager: ActiveCallManager,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import android.os.Build
|
|||
import androidx.annotation.ChecksSdkIntAtLeast
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.core.bool.orFalse
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
|
||||
|
|
@ -23,7 +22,6 @@ interface PipSupportProvider {
|
|||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@Inject
|
||||
class DefaultPipSupportProvider(
|
||||
@ApplicationContext private val context: Context,
|
||||
) : PipSupportProvider {
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ class CallScreenPresenter(
|
|||
userAgent = userAgent,
|
||||
isCallActive = isWidgetLoaded,
|
||||
isInWidgetMode = isInWidgetMode,
|
||||
eventSink = { handleEvents(it) },
|
||||
eventSink = ::handleEvents,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.call.impl.ui
|
||||
|
||||
import io.element.android.features.call.api.CallType
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
fun CallType.getSessionId(): SessionId? {
|
||||
return when (this) {
|
||||
is CallType.ExternalUrl -> null
|
||||
is CallType.RoomCall -> sessionId
|
||||
}
|
||||
}
|
||||
|
|
@ -23,14 +23,17 @@ import androidx.appcompat.app.AppCompatActivity
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.core.app.PictureInPictureModeChangedInfo
|
||||
import androidx.core.content.IntentCompat
|
||||
import androidx.core.util.Consumer
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.compound.colors.SemanticColorsLightDark
|
||||
import io.element.android.features.call.api.CallType
|
||||
import io.element.android.features.call.api.CallType.ExternalUrl
|
||||
import io.element.android.features.call.impl.DefaultElementCallEntryPoint
|
||||
|
|
@ -105,9 +108,13 @@ class ElementCallActivity :
|
|||
setContent {
|
||||
val pipState = pictureInPicturePresenter.present()
|
||||
ListenToAndroidEvents(pipState)
|
||||
val colors by remember(webViewTarget.value?.getSessionId()) {
|
||||
enterpriseService.semanticColorsFlow(sessionId = webViewTarget.value?.getSessionId())
|
||||
}.collectAsState(SemanticColorsLightDark.default)
|
||||
ElementThemeApp(
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
enterpriseService = enterpriseService,
|
||||
compoundLight = colors.light,
|
||||
compoundDark = colors.dark,
|
||||
buildMeta = buildMeta,
|
||||
) {
|
||||
val state = presenter.present()
|
||||
|
|
|
|||
|
|
@ -11,9 +11,13 @@ import android.os.Bundle
|
|||
import android.view.WindowManager
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.core.content.IntentCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.compound.colors.SemanticColorsLightDark
|
||||
import io.element.android.features.call.api.CallType
|
||||
import io.element.android.features.call.api.ElementCallEntryPoint
|
||||
import io.element.android.features.call.impl.di.CallBindings
|
||||
|
|
@ -78,9 +82,13 @@ class IncomingCallActivity : AppCompatActivity() {
|
|||
val notificationData = intent?.let { IntentCompat.getParcelableExtra(it, EXTRA_NOTIFICATION_DATA, CallNotificationData::class.java) }
|
||||
if (notificationData != null) {
|
||||
setContent {
|
||||
val colors by remember {
|
||||
enterpriseService.semanticColorsFlow(sessionId = notificationData.sessionId)
|
||||
}.collectAsState(SemanticColorsLightDark.default)
|
||||
ElementThemeApp(
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
enterpriseService = enterpriseService,
|
||||
compoundLight = colors.light,
|
||||
compoundDark = colors.dark,
|
||||
buildMeta = buildMeta,
|
||||
) {
|
||||
IncomingCallScreen(
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
|
||||
interface LanguageTagProvider {
|
||||
@Composable
|
||||
|
|
@ -19,7 +18,6 @@ interface LanguageTagProvider {
|
|||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@Inject
|
||||
class DefaultLanguageTagProvider : LanguageTagProvider {
|
||||
@Composable
|
||||
override fun provideLanguageTag(): String? {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import coil3.SingletonImageLoader
|
|||
import coil3.annotation.DelicateCoilApi
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import io.element.android.appconfig.ElementCallConfig
|
||||
import io.element.android.features.call.api.CallType
|
||||
|
|
@ -87,7 +86,6 @@ interface ActiveCallManager {
|
|||
|
||||
@SingleIn(AppScope::class)
|
||||
@ContributesBinding(AppScope::class)
|
||||
@Inject
|
||||
class DefaultActiveCallManager(
|
||||
@ApplicationContext context: Context,
|
||||
@AppCoroutineScope
|
||||
|
|
|
|||
|
|
@ -9,12 +9,10 @@ package io.element.android.features.call.impl.utils
|
|||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.features.call.impl.BuildConfig
|
||||
import io.element.android.libraries.matrix.api.widget.CallAnalyticCredentialsProvider
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@Inject
|
||||
class DefaultCallAnalyticCredentialsProvider : CallAnalyticCredentialsProvider {
|
||||
override val posthogUserId: String? = BuildConfig.POSTHOG_USER_ID.takeIf { it.isNotBlank() }
|
||||
override val posthogApiHost: String? = BuildConfig.POSTHOG_API_HOST.takeIf { it.isNotBlank() }
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ package io.element.android.features.call.impl.utils
|
|||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
|
@ -23,7 +22,6 @@ import kotlinx.coroutines.flow.firstOrNull
|
|||
private const val EMBEDDED_CALL_WIDGET_BASE_URL = "https://appassets.androidplatform.net/element-call/index.html"
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@Inject
|
||||
class DefaultCallWidgetProvider(
|
||||
private val matrixClientsProvider: MatrixClientProvider,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ package io.element.android.features.call.impl.utils
|
|||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import io.element.android.features.call.api.CurrentCall
|
||||
import io.element.android.features.call.api.CurrentCallService
|
||||
|
|
@ -17,7 +16,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
|
||||
@SingleIn(AppScope::class)
|
||||
@ContributesBinding(AppScope::class)
|
||||
@Inject
|
||||
class DefaultCurrentCallService : CurrentCallService {
|
||||
override val currentCall = MutableStateFlow<CurrentCall>(CurrentCall.None)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.call.ui
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.call.api.CallType
|
||||
import io.element.android.features.call.impl.ui.getSessionId
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import org.junit.Test
|
||||
|
||||
class CallTypeTest {
|
||||
@Test
|
||||
fun `getSessionId returns null for ExternalUrl`() {
|
||||
assertThat(CallType.ExternalUrl("aURL").getSessionId()).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getSessionId returns the sessionId for RoomCall`() {
|
||||
assertThat(
|
||||
CallType.RoomCall(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
).getSessionId()
|
||||
).isEqualTo(A_SESSION_ID)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ExternalUrl stringification does not contain the URL`() {
|
||||
assertThat(CallType.ExternalUrl("aURL").toString()).isEqualTo("ExternalUrl")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `RoomCall stringification does not contain the URL`() {
|
||||
assertThat(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID).toString())
|
||||
.isEqualTo("RoomCall(sessionId=$A_SESSION_ID, roomId=$A_ROOM_ID)")
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.changeroommemberroes.api
|
||||
package io.element.android.features.changeroommemberroles.api
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
|
|
@ -15,13 +15,12 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
|||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
|
||||
fun interface ChangeRoomMemberRolesEntryPoint : FeatureEntryPoint {
|
||||
fun builder(parentNode: Node, buildContext: BuildContext): Builder
|
||||
|
||||
interface Builder {
|
||||
fun room(room: JoinedRoom): Builder
|
||||
fun listType(changeRoomMemberRolesListType: ChangeRoomMemberRolesListType): Builder
|
||||
fun build(): Node
|
||||
}
|
||||
fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
room: JoinedRoom,
|
||||
listType: ChangeRoomMemberRolesListType,
|
||||
): Node
|
||||
|
||||
interface NodeProxy {
|
||||
val roomId: RoomId
|
||||
|
|
@ -15,7 +15,5 @@ sealed interface ChangeRolesEvent {
|
|||
data class UserSelectionToggled(val matrixUser: MatrixUser) : ChangeRolesEvent
|
||||
data object Save : ChangeRolesEvent
|
||||
data object Exit : ChangeRolesEvent
|
||||
data object CancelExit : ChangeRolesEvent
|
||||
data object ClearError : ChangeRolesEvent
|
||||
data object CancelSave : ChangeRolesEvent
|
||||
data object CloseDialog : ChangeRolesEvent
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import com.bumble.appyx.core.plugin.Plugin
|
|||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesListType
|
||||
import io.element.android.features.changeroommemberroles.api.ChangeRoomMemberRolesListType
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.appyx.launchMolecule
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
|
|
|
|||
|
|
@ -69,20 +69,19 @@ class ChangeRolesPresenter(
|
|||
val selectedUsers = remember {
|
||||
mutableStateOf<ImmutableList<MatrixUser>>(persistentListOf())
|
||||
}
|
||||
val exitState: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||
val saveState: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||
val saveState: MutableState<AsyncAction<Boolean>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||
val usersWithRole = produceState<ImmutableList<MatrixUser>>(initialValue = persistentListOf()) {
|
||||
room.usersWithRole(role).map { members -> members.map { it.toMatrixUser() } }
|
||||
.onEach { users ->
|
||||
val previous = value
|
||||
value = users.toImmutableList()
|
||||
// Users who were selected but didn't have the role, so their role change was pending
|
||||
val toAdd = selectedUsers.value.filter { user -> users.none { it.userId == user.userId } && previous.none { it.userId == user.userId } }
|
||||
// Users who no longer have the role
|
||||
val toRemove = previous.filter { user -> users.none { it.userId == user.userId } }.toSet()
|
||||
selectedUsers.value = (users + toAdd - toRemove).toImmutableList()
|
||||
}
|
||||
.launchIn(this)
|
||||
.onEach { users ->
|
||||
val previous = value
|
||||
value = users.toImmutableList()
|
||||
// Users who were selected but didn't have the role, so their role change was pending
|
||||
val toAdd = selectedUsers.value.filter { user -> users.none { it.userId == user.userId } && previous.none { it.userId == user.userId } }
|
||||
// Users who no longer have the role
|
||||
val toRemove = previous.filter { user -> users.none { it.userId == user.userId } }.toSet()
|
||||
selectedUsers.value = (users + toAdd - toRemove).toImmutableList()
|
||||
}
|
||||
.launchIn(this)
|
||||
}
|
||||
|
||||
val roomMemberState by room.membersStateFlow.collectAsState()
|
||||
|
|
@ -147,22 +146,16 @@ class ChangeRolesPresenter(
|
|||
}
|
||||
}
|
||||
}
|
||||
is ChangeRolesEvent.ClearError -> {
|
||||
saveState.value = AsyncAction.Uninitialized
|
||||
}
|
||||
is ChangeRolesEvent.Exit -> {
|
||||
exitState.value = if (exitState.value.isUninitialized() && hasPendingChanges) {
|
||||
saveState.value = if (saveState.value.isUninitialized() && hasPendingChanges) {
|
||||
// Has pending changes, confirm exit
|
||||
AsyncAction.ConfirmingNoParams
|
||||
AsyncAction.ConfirmingCancellation
|
||||
} else {
|
||||
// No pending changes, exit immediately
|
||||
AsyncAction.Success(Unit)
|
||||
AsyncAction.Success(false)
|
||||
}
|
||||
}
|
||||
is ChangeRolesEvent.CancelExit -> {
|
||||
exitState.value = AsyncAction.Uninitialized
|
||||
}
|
||||
is ChangeRolesEvent.CancelSave -> {
|
||||
is ChangeRolesEvent.CloseDialog -> {
|
||||
saveState.value = AsyncAction.Uninitialized
|
||||
}
|
||||
}
|
||||
|
|
@ -174,7 +167,6 @@ class ChangeRolesPresenter(
|
|||
searchResults = searchResults,
|
||||
selectedUsers = selectedUsers.value,
|
||||
hasPendingChanges = hasPendingChanges,
|
||||
exitState = exitState.value,
|
||||
savingState = saveState.value,
|
||||
canChangeMemberRole = ::canChangeMemberRole,
|
||||
eventSink = ::handleEvent,
|
||||
|
|
@ -198,7 +190,7 @@ class ChangeRolesPresenter(
|
|||
private fun CoroutineScope.save(
|
||||
usersWithRole: ImmutableList<MatrixUser>,
|
||||
selectedUsers: MutableState<ImmutableList<MatrixUser>>,
|
||||
saveState: MutableState<AsyncAction<Unit>>,
|
||||
saveState: MutableState<AsyncAction<Boolean>>,
|
||||
) = launch {
|
||||
saveState.value = AsyncAction.Loading
|
||||
|
||||
|
|
@ -221,7 +213,7 @@ class ChangeRolesPresenter(
|
|||
saveState.value = AsyncAction.Failure(it)
|
||||
}
|
||||
.onSuccess {
|
||||
saveState.value = AsyncAction.Success(Unit)
|
||||
saveState.value = AsyncAction.Success(true)
|
||||
// Asynchronously reload the room members
|
||||
launch { room.updateMembers() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,8 +23,7 @@ data class ChangeRolesState(
|
|||
val searchResults: SearchBarResultState<MembersByRole>,
|
||||
val selectedUsers: ImmutableList<MatrixUser>,
|
||||
val hasPendingChanges: Boolean,
|
||||
val exitState: AsyncAction<Unit>,
|
||||
val savingState: AsyncAction<Unit>,
|
||||
val savingState: AsyncAction<Boolean>,
|
||||
val canChangeMemberRole: (UserId) -> Boolean,
|
||||
val eventSink: (ChangeRolesEvent) -> Unit,
|
||||
)
|
||||
|
|
@ -36,10 +35,10 @@ data class MembersByRole(
|
|||
val members: ImmutableList<RoomMember>,
|
||||
) {
|
||||
constructor(members: List<RoomMember>) : this(
|
||||
owners = members.filter { it.role is RoomMember.Role.Owner }.sorted(),
|
||||
admins = members.filter { it.role == RoomMember.Role.Admin }.sorted(),
|
||||
moderators = members.filter { it.role == RoomMember.Role.Moderator }.sorted(),
|
||||
members = members.filter { it.role == RoomMember.Role.User }.sorted(),
|
||||
owners = members.filter { it.role is RoomMember.Role.Owner }.sorted(),
|
||||
admins = members.filter { it.role == RoomMember.Role.Admin }.sorted(),
|
||||
moderators = members.filter { it.role == RoomMember.Role.Moderator }.sorted(),
|
||||
members = members.filter { it.role == RoomMember.Role.User }.sorted(),
|
||||
)
|
||||
|
||||
fun isEmpty() = owners.isEmpty() && admins.isEmpty() && moderators.isEmpty() && members.isEmpty()
|
||||
|
|
|
|||
|
|
@ -38,10 +38,10 @@ class ChangeRolesStateProvider : PreviewParameterProvider<ChangeRolesState> {
|
|||
searchResults = SearchBarResultState.Results(MembersByRole(aRoomMemberList().take(1).toImmutableList())),
|
||||
selectedUsers = aMatrixUserList().take(1).toImmutableList(),
|
||||
),
|
||||
aChangeRolesStateWithSelectedUsers().copy(exitState = AsyncAction.ConfirmingNoParams),
|
||||
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.ConfirmingCancellation),
|
||||
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.ConfirmingNoParams),
|
||||
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.Loading),
|
||||
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.Success(Unit)),
|
||||
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.Success(true)),
|
||||
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.Failure(Exception("boom"))),
|
||||
aChangeRolesStateWithOwners(),
|
||||
aChangeRolesStateWithOwners().copy(role = RoomMember.Role.Owner(isCreator = false)),
|
||||
|
|
@ -55,8 +55,7 @@ internal fun aChangeRolesState(
|
|||
searchResults: SearchBarResultState<MembersByRole> = SearchBarResultState.NoResultsFound(),
|
||||
selectedUsers: ImmutableList<MatrixUser> = persistentListOf(),
|
||||
hasPendingChanges: Boolean = false,
|
||||
exitState: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
savingState: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
savingState: AsyncAction<Boolean> = AsyncAction.Uninitialized,
|
||||
canRemoveMember: (UserId) -> Boolean = { true },
|
||||
eventSink: (ChangeRolesEvent) -> Unit = {},
|
||||
) = ChangeRolesState(
|
||||
|
|
@ -66,7 +65,6 @@ internal fun aChangeRolesState(
|
|||
searchResults = searchResults,
|
||||
selectedUsers = selectedUsers,
|
||||
hasPendingChanges = hasPendingChanges,
|
||||
exitState = exitState,
|
||||
savingState = savingState,
|
||||
canChangeMemberRole = canRemoveMember,
|
||||
eventSink = eventSink,
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ import androidx.compose.foundation.lazy.items
|
|||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -41,7 +40,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncIndicator
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncIndicatorHost
|
||||
|
|
@ -52,7 +50,6 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
|||
import io.element.android.libraries.designsystem.components.avatar.AvatarType
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Checkbox
|
||||
|
|
@ -172,61 +169,59 @@ fun ChangeRolesView(
|
|||
AsyncIndicatorHost(modifier = Modifier.statusBarsPadding(), asyncIndicatorState)
|
||||
|
||||
AsyncActionView(
|
||||
async = state.exitState,
|
||||
onSuccess = { latestNavigateUp() },
|
||||
confirmationDialog = {
|
||||
ConfirmationDialog(
|
||||
title = stringResource(CommonStrings.dialog_unsaved_changes_title),
|
||||
content = stringResource(CommonStrings.dialog_unsaved_changes_description_android),
|
||||
onSubmitClick = { state.eventSink(ChangeRolesEvent.Exit) },
|
||||
onDismiss = { state.eventSink(ChangeRolesEvent.CancelExit) }
|
||||
)
|
||||
},
|
||||
onErrorDismiss = { /* Cannot happen */ },
|
||||
)
|
||||
|
||||
when (state.savingState) {
|
||||
is AsyncAction.Confirming -> {
|
||||
when (state.role) {
|
||||
is RoomMember.Role.Owner -> {
|
||||
ConfirmationDialog(
|
||||
title = stringResource(R.string.screen_room_change_role_confirm_change_owners_title),
|
||||
content = stringResource(R.string.screen_room_change_role_confirm_change_owners_description),
|
||||
submitText = stringResource(CommonStrings.action_continue),
|
||||
onSubmitClick = { state.eventSink(ChangeRolesEvent.Save) },
|
||||
onDismiss = { state.eventSink(ChangeRolesEvent.ClearError) },
|
||||
destructiveSubmit = true,
|
||||
)
|
||||
}
|
||||
is RoomMember.Role.Admin -> {
|
||||
ConfirmationDialog(
|
||||
title = stringResource(R.string.screen_room_change_role_confirm_add_admin_title),
|
||||
content = stringResource(R.string.screen_room_change_role_confirm_add_admin_description),
|
||||
onSubmitClick = { state.eventSink(ChangeRolesEvent.Save) },
|
||||
onDismiss = { state.eventSink(ChangeRolesEvent.ClearError) }
|
||||
)
|
||||
}
|
||||
else -> Unit // No confirmation needed for Moderator or User roles
|
||||
}
|
||||
}
|
||||
is AsyncAction.Loading -> {
|
||||
ProgressDialog()
|
||||
}
|
||||
is AsyncAction.Failure -> {
|
||||
ErrorDialog(
|
||||
content = stringResource(CommonStrings.error_unknown),
|
||||
onSubmit = { state.eventSink(ChangeRolesEvent.ClearError) }
|
||||
)
|
||||
}
|
||||
is AsyncAction.Success -> {
|
||||
LaunchedEffect(state.savingState) {
|
||||
async = state.savingState,
|
||||
onSuccess = { changeSaved ->
|
||||
if (changeSaved) {
|
||||
asyncIndicatorState.enqueue(durationMs = AsyncIndicator.DURATION_SHORT) {
|
||||
AsyncIndicator.Custom(text = stringResource(CommonStrings.common_saved_changes))
|
||||
}
|
||||
} else {
|
||||
latestNavigateUp()
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
},
|
||||
confirmationDialog = { confirming ->
|
||||
when (confirming) {
|
||||
is AsyncAction.ConfirmingCancellation -> {
|
||||
ConfirmationDialog(
|
||||
title = stringResource(CommonStrings.dialog_unsaved_changes_title),
|
||||
content = stringResource(CommonStrings.dialog_unsaved_changes_description_android),
|
||||
onSubmitClick = { state.eventSink(ChangeRolesEvent.Exit) },
|
||||
onDismiss = { state.eventSink(ChangeRolesEvent.CloseDialog) }
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
when (state.role) {
|
||||
is RoomMember.Role.Owner -> {
|
||||
ConfirmationDialog(
|
||||
title = stringResource(R.string.screen_room_change_role_confirm_change_owners_title),
|
||||
content = stringResource(R.string.screen_room_change_role_confirm_change_owners_description),
|
||||
submitText = stringResource(CommonStrings.action_continue),
|
||||
onSubmitClick = { state.eventSink(ChangeRolesEvent.Save) },
|
||||
onDismiss = { state.eventSink(ChangeRolesEvent.CloseDialog) },
|
||||
destructiveSubmit = true,
|
||||
)
|
||||
}
|
||||
is RoomMember.Role.Admin -> {
|
||||
ConfirmationDialog(
|
||||
title = stringResource(R.string.screen_room_change_role_confirm_add_admin_title),
|
||||
content = stringResource(R.string.screen_room_change_role_confirm_add_admin_description),
|
||||
onSubmitClick = { state.eventSink(ChangeRolesEvent.Save) },
|
||||
onDismiss = { state.eventSink(ChangeRolesEvent.CloseDialog) }
|
||||
)
|
||||
}
|
||||
// No confirmation needed for Moderator or User roles
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
errorMessage = {
|
||||
stringResource(CommonStrings.error_unknown)
|
||||
},
|
||||
onErrorDismiss = {
|
||||
state.eventSink(ChangeRolesEvent.CloseDialog)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ import dev.zacsweers.metro.Assisted
|
|||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.appnav.di.RoomGraphFactory
|
||||
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesEntryPoint
|
||||
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesListType
|
||||
import io.element.android.features.changeroommemberroles.api.ChangeRoomMemberRolesEntryPoint
|
||||
import io.element.android.features.changeroommemberroles.api.ChangeRoomMemberRolesListType
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
|
|
|
|||
|
|
@ -10,39 +10,25 @@ package io.element.android.features.changeroommemberroles.impl
|
|||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesEntryPoint
|
||||
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesListType
|
||||
import io.element.android.features.changeroommemberroles.api.ChangeRoomMemberRolesEntryPoint
|
||||
import io.element.android.features.changeroommemberroles.api.ChangeRoomMemberRolesListType
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
@Inject
|
||||
class DefaultChangeRoomMemberRolesEntyPoint : ChangeRoomMemberRolesEntryPoint {
|
||||
override fun builder(parentNode: Node, buildContext: BuildContext): ChangeRoomMemberRolesEntryPoint.Builder {
|
||||
return object : ChangeRoomMemberRolesEntryPoint.Builder {
|
||||
private lateinit var changeRoomMemberRolesListType: ChangeRoomMemberRolesListType
|
||||
private lateinit var room: JoinedRoom
|
||||
|
||||
override fun room(room: JoinedRoom): ChangeRoomMemberRolesEntryPoint.Builder {
|
||||
this.room = room
|
||||
return this
|
||||
}
|
||||
|
||||
override fun listType(changeRoomMemberRolesListType: ChangeRoomMemberRolesListType): ChangeRoomMemberRolesEntryPoint.Builder {
|
||||
this.changeRoomMemberRolesListType = changeRoomMemberRolesListType
|
||||
return this
|
||||
}
|
||||
|
||||
override fun build(): Node {
|
||||
return parentNode.createNode<ChangeRoomMemberRolesRootNode>(
|
||||
buildContext = buildContext,
|
||||
plugins = listOf(
|
||||
ChangeRoomMemberRolesRootNode.Inputs(joinedRoom = room, listType = changeRoomMemberRolesListType),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
override fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
room: JoinedRoom,
|
||||
listType: ChangeRoomMemberRolesListType,
|
||||
): Node {
|
||||
return parentNode.createNode<ChangeRoomMemberRolesRootNode>(
|
||||
buildContext = buildContext,
|
||||
plugins = listOf(
|
||||
ChangeRoomMemberRolesRootNode.Inputs(joinedRoom = room, listType = listType),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
<string name="screen_room_change_role_section_users">"Mitglieder"</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_description">"Du hast nicht gespeicherte Änderungen."</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_title">"Änderungen speichern?"</string>
|
||||
<string name="screen_room_member_list_banned_empty">"In diesem Chat gibt es keine gesperrten Nutzer."</string>
|
||||
<string name="screen_room_member_list_banned_empty">"Es gibt keine gesperrten Nutzer."</string>
|
||||
<plurals name="screen_room_member_list_header_title">
|
||||
<item quantity="one">"%1$d Person"</item>
|
||||
<item quantity="other">"%1$d Personen"</item>
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
<string name="screen_room_change_role_section_users">"Liikmed"</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_description">"Sul on salvestamata muudatusi"</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_title">"Kas salvestame muudatused?"</string>
|
||||
<string name="screen_room_member_list_banned_empty">"Jututoas pole suhtluskeeluga kasutajaid"</string>
|
||||
<string name="screen_room_member_list_banned_empty">"Suhtluskeeluga kasutajaid pole"</string>
|
||||
<plurals name="screen_room_member_list_header_title">
|
||||
<item quantity="one">"%1$d osaleja"</item>
|
||||
<item quantity="other">"%1$d osalejat"</item>
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
<string name="screen_room_change_role_section_users">"Členovia"</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_description">"Máte neuložené zmeny."</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_title">"Uložiť zmeny?"</string>
|
||||
<string name="screen_room_member_list_banned_empty">"V tejto miestnosti nie sú žiadni zakázaní používatelia."</string>
|
||||
<string name="screen_room_member_list_banned_empty">"Neexistujú žiadni zablokovaní používatelia."</string>
|
||||
<plurals name="screen_room_member_list_header_title">
|
||||
<item quantity="one">"%1$d osoba"</item>
|
||||
<item quantity="few">"%1$d osoby"</item>
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
<string name="screen_room_change_role_section_users">"成員"</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_description">"您有尚未儲存的變更"</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_title">"是否儲存變更?"</string>
|
||||
<string name="screen_room_member_list_banned_empty">"此聊天室沒有黑名單。"</string>
|
||||
<string name="screen_room_member_list_banned_empty">"沒有被封鎖的使用者。"</string>
|
||||
<plurals name="screen_room_member_list_header_title">
|
||||
<item quantity="other">"%1$d 位夥伴"</item>
|
||||
</plurals>
|
||||
|
|
|
|||
|
|
@ -33,12 +33,12 @@
|
|||
<string name="screen_room_change_role_section_users">"Members"</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_description">"You have unsaved changes."</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_title">"Save changes?"</string>
|
||||
<string name="screen_room_member_list_banned_empty">"There are no banned users in this room."</string>
|
||||
<string name="screen_room_member_list_banned_empty">"There are no banned users."</string>
|
||||
<plurals name="screen_room_member_list_header_title">
|
||||
<item quantity="one">"%1$d person"</item>
|
||||
<item quantity="other">"%1$d people"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_member_list_manage_member_remove_confirmation_ban">"Ban from room"</string>
|
||||
<string name="screen_room_member_list_manage_member_remove_confirmation_ban">"Ban user"</string>
|
||||
<string name="screen_room_member_list_manage_member_remove_confirmation_kick">"Only remove member"</string>
|
||||
<string name="screen_room_member_list_manage_member_unban_action">"Unban"</string>
|
||||
<string name="screen_room_member_list_manage_member_unban_message">"They will be able to join this room again if invited."</string>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
package io.element.android.features.changeroommemberroles.impl
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesListType
|
||||
import io.element.android.features.changeroommemberroles.api.ChangeRoomMemberRolesListType
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import org.junit.Test
|
||||
|
||||
|
|
|
|||
|
|
@ -52,7 +52,6 @@ class ChangeRolesPresenterTest {
|
|||
assertThat(searchResults).isInstanceOf(SearchBarResultState.Initial::class.java)
|
||||
assertThat(selectedUsers).isEmpty()
|
||||
assertThat(hasPendingChanges).isFalse()
|
||||
assertThat(exitState).isEqualTo(AsyncAction.Uninitialized)
|
||||
assertThat(savingState).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
|
|
@ -266,7 +265,7 @@ class ChangeRolesPresenterTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `present - Exit will display success if no pending changes`() = runTest {
|
||||
fun `present - Exit will display success false if no pending changes`() = runTest {
|
||||
val room = FakeJoinedRoom().apply {
|
||||
givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList()))
|
||||
givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.Admin)))
|
||||
|
|
@ -278,15 +277,15 @@ class ChangeRolesPresenterTest {
|
|||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.hasPendingChanges).isFalse()
|
||||
assertThat(initialState.exitState).isEqualTo(AsyncAction.Uninitialized)
|
||||
assertThat(initialState.savingState).isEqualTo(AsyncAction.Uninitialized)
|
||||
|
||||
initialState.eventSink(ChangeRolesEvent.Exit)
|
||||
assertThat(awaitItem().exitState).isEqualTo(AsyncAction.Success(Unit))
|
||||
assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Success(false))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - CancelExit will remove exit confirmation`() = runTest {
|
||||
fun `present - CloseDialog will remove exit confirmation`() = runTest {
|
||||
val room = FakeJoinedRoom().apply {
|
||||
givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList()))
|
||||
givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.Admin)))
|
||||
|
|
@ -298,16 +297,16 @@ class ChangeRolesPresenterTest {
|
|||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.hasPendingChanges).isFalse()
|
||||
assertThat(initialState.exitState).isEqualTo(AsyncAction.Uninitialized)
|
||||
assertThat(initialState.savingState).isEqualTo(AsyncAction.Uninitialized)
|
||||
|
||||
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(MatrixUser(A_USER_ID_2)))
|
||||
|
||||
awaitItem().eventSink(ChangeRolesEvent.Exit)
|
||||
val confirmingState = awaitItem()
|
||||
assertThat(confirmingState.exitState).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||
assertThat(confirmingState.savingState).isEqualTo(AsyncAction.ConfirmingCancellation)
|
||||
|
||||
confirmingState.eventSink(ChangeRolesEvent.CancelExit)
|
||||
assertThat(awaitItem().exitState).isEqualTo(AsyncAction.Uninitialized)
|
||||
confirmingState.eventSink(ChangeRolesEvent.CloseDialog)
|
||||
assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -324,7 +323,7 @@ class ChangeRolesPresenterTest {
|
|||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.hasPendingChanges).isFalse()
|
||||
assertThat(initialState.exitState).isEqualTo(AsyncAction.Uninitialized)
|
||||
assertThat(initialState.savingState).isEqualTo(AsyncAction.Uninitialized)
|
||||
|
||||
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(MatrixUser(A_USER_ID_2)))
|
||||
val updatedState = awaitItem()
|
||||
|
|
@ -332,10 +331,10 @@ class ChangeRolesPresenterTest {
|
|||
skipItems(1)
|
||||
|
||||
updatedState.eventSink(ChangeRolesEvent.Exit)
|
||||
assertThat(awaitItem().exitState).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||
assertThat(awaitItem().savingState).isEqualTo(AsyncAction.ConfirmingCancellation)
|
||||
|
||||
updatedState.eventSink(ChangeRolesEvent.Exit)
|
||||
assertThat(awaitItem().exitState).isEqualTo(AsyncAction.Success(Unit))
|
||||
assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Success(false))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -367,12 +366,12 @@ class ChangeRolesPresenterTest {
|
|||
assertThat(loadingState.savingState).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
skipItems(1)
|
||||
|
||||
assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Success(Unit))
|
||||
assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Success(true))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - CancelSave will remove the confirmation dialog`() = runTest {
|
||||
fun `present - CloseDialog will remove the confirmation dialog`() = runTest {
|
||||
val room = FakeJoinedRoom().apply {
|
||||
givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList()))
|
||||
givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.Admin)))
|
||||
|
|
@ -391,7 +390,7 @@ class ChangeRolesPresenterTest {
|
|||
val confirmingState = awaitItem()
|
||||
assertThat(confirmingState.savingState).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||
|
||||
confirmingState.eventSink(ChangeRolesEvent.CancelSave)
|
||||
confirmingState.eventSink(ChangeRolesEvent.CloseDialog)
|
||||
assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
|
@ -426,7 +425,7 @@ class ChangeRolesPresenterTest {
|
|||
assertThat(loadingState.savingState).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
skipItems(1)
|
||||
|
||||
assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Success(Unit))
|
||||
assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Success(true))
|
||||
assertThat(analyticsService.capturedEvents.last()).isEqualTo(RoomModeration(RoomModeration.Action.ChangeMemberRole, RoomModeration.Role.Moderator))
|
||||
}
|
||||
}
|
||||
|
|
@ -504,13 +503,13 @@ class ChangeRolesPresenterTest {
|
|||
assertThat(loadingState.savingState).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
skipItems(1)
|
||||
|
||||
assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Success(Unit))
|
||||
assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Success(true))
|
||||
assertThat(analyticsService.capturedEvents.last()).isEqualTo(RoomModeration(RoomModeration.Action.ChangeMemberRole, RoomModeration.Role.User))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - Save can handle failures and ClearError clears them`() = runTest {
|
||||
fun `present - Save can handle failures and CloseDialog clears them`() = runTest {
|
||||
val room = FakeJoinedRoom(
|
||||
updateUserRoleResult = { Result.failure(IllegalStateException("Failed")) }
|
||||
).apply {
|
||||
|
|
@ -534,7 +533,7 @@ class ChangeRolesPresenterTest {
|
|||
val failedState = awaitItem()
|
||||
assertThat(failedState.savingState).isInstanceOf(AsyncAction.Failure::class.java)
|
||||
|
||||
failedState.eventSink(ChangeRolesEvent.ClearError)
|
||||
failedState.eventSink(ChangeRolesEvent.CloseDialog)
|
||||
assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ class ChangeRolesViewTest {
|
|||
rule.setChangeRolesContent(
|
||||
state = aChangeRolesState(
|
||||
isSearchActive = true,
|
||||
exitState = AsyncAction.ConfirmingNoParams,
|
||||
savingState = AsyncAction.ConfirmingCancellation,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
|
|
@ -151,14 +151,14 @@ class ChangeRolesViewTest {
|
|||
rule.setChangeRolesContent(
|
||||
state = aChangeRolesState(
|
||||
isSearchActive = true,
|
||||
exitState = AsyncAction.ConfirmingNoParams,
|
||||
savingState = AsyncAction.ConfirmingCancellation,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
|
||||
rule.clickOn(CommonStrings.action_cancel)
|
||||
|
||||
eventsRecorder.assertSingle(ChangeRolesEvent.CancelExit)
|
||||
eventsRecorder.assertSingle(ChangeRolesEvent.CloseDialog)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -209,7 +209,7 @@ class ChangeRolesViewTest {
|
|||
|
||||
rule.clickOn(CommonStrings.action_cancel)
|
||||
|
||||
eventsRecorder.assertSingle(ChangeRolesEvent.ClearError)
|
||||
eventsRecorder.assertSingle(ChangeRolesEvent.CloseDialog)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -225,7 +225,7 @@ class ChangeRolesViewTest {
|
|||
|
||||
rule.clickOn(CommonStrings.action_ok)
|
||||
|
||||
eventsRecorder.assertSingle(ChangeRolesEvent.ClearError)
|
||||
eventsRecorder.assertSingle(ChangeRolesEvent.CloseDialog)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ package io.element.android.features.changeroommemberroles.impl
|
|||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesListType
|
||||
import io.element.android.features.changeroommemberroles.api.ChangeRoomMemberRolesListType
|
||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||
import io.element.android.tests.testutils.node.TestParentNode
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
|
@ -31,10 +31,12 @@ class DefaultChangeRoomMemberRolesEntyPointTest {
|
|||
}
|
||||
val room = FakeJoinedRoom()
|
||||
val listType = ChangeRoomMemberRolesListType.Admins
|
||||
val result = entryPoint.builder(parentNode, BuildContext.root(null))
|
||||
.room(FakeJoinedRoom())
|
||||
.listType(listType)
|
||||
.build()
|
||||
val result = entryPoint.createNode(
|
||||
parentNode = parentNode,
|
||||
buildContext = BuildContext.root(null),
|
||||
room = FakeJoinedRoom(),
|
||||
listType = listType,
|
||||
)
|
||||
assertThat(result).isInstanceOf(ChangeRoomMemberRolesRootNode::class.java)
|
||||
// Search for the Inputs plugin
|
||||
val input = result.plugins.filterIsInstance<ChangeRoomMemberRolesRootNode.Inputs>().single()
|
||||
|
|
|
|||
21
features/changeroommemberroles/test/build.gradle.kts
Normal file
21
features/changeroommemberroles/test/build.gradle.kts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.changeroommemberroles.test"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.features.changeroommemberroles.api)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.tests.testutils)
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.changeroommemberroles.test
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import io.element.android.features.changeroommemberroles.api.ChangeRoomMemberRolesEntryPoint
|
||||
import io.element.android.features.changeroommemberroles.api.ChangeRoomMemberRolesListType
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeChangeRoomMemberRolesEntryPoint : ChangeRoomMemberRolesEntryPoint {
|
||||
override fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
room: JoinedRoom,
|
||||
listType: ChangeRoomMemberRolesListType,
|
||||
): Node {
|
||||
lambdaError()
|
||||
}
|
||||
}
|
||||
|
|
@ -14,12 +14,11 @@ import io.element.android.libraries.architecture.FeatureEntryPoint
|
|||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
interface CreateRoomEntryPoint : FeatureEntryPoint {
|
||||
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||
|
||||
interface NodeBuilder {
|
||||
fun callback(callback: Callback): NodeBuilder
|
||||
fun build(): Node
|
||||
}
|
||||
fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
callback: Callback,
|
||||
): Node
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun onRoomCreated(roomId: RoomId)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import androidx.compose.ui.Modifier
|
|||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.replace
|
||||
import dev.zacsweers.metro.Assisted
|
||||
|
|
@ -24,6 +23,7 @@ import io.element.android.features.createroom.impl.addpeople.AddPeopleNode
|
|||
import io.element.android.features.createroom.impl.configureroom.ConfigureRoomNode
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
|
@ -42,9 +42,7 @@ class CreateRoomFlowNode(
|
|||
buildContext = buildContext,
|
||||
plugins = plugins
|
||||
) {
|
||||
private fun onRoomCreated(roomId: RoomId) {
|
||||
plugins<CreateRoomEntryPoint.Callback>().forEach { it.onRoomCreated(roomId) }
|
||||
}
|
||||
private val callback: CreateRoomEntryPoint.Callback = callback()
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
|
|
@ -60,7 +58,7 @@ class CreateRoomFlowNode(
|
|||
val inputs = AddPeopleNode.Inputs(navTarget.roomId)
|
||||
val callback: AddPeopleNode.Callback = object : AddPeopleNode.Callback {
|
||||
override fun onFinish() {
|
||||
onRoomCreated(navTarget.roomId)
|
||||
callback.onRoomCreated(navTarget.roomId)
|
||||
}
|
||||
}
|
||||
createNode<AddPeopleNode>(buildContext, plugins = listOf(inputs, callback))
|
||||
|
|
|
|||
|
|
@ -9,28 +9,18 @@ package io.element.android.features.createroom.impl
|
|||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
@Inject
|
||||
class DefaultCreateRoomEntryPoint : CreateRoomEntryPoint {
|
||||
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): CreateRoomEntryPoint.NodeBuilder {
|
||||
val plugins = ArrayList<Plugin>()
|
||||
|
||||
return object : CreateRoomEntryPoint.NodeBuilder {
|
||||
override fun callback(callback: CreateRoomEntryPoint.Callback): CreateRoomEntryPoint.NodeBuilder {
|
||||
plugins += callback
|
||||
return this
|
||||
}
|
||||
|
||||
override fun build(): Node {
|
||||
return parentNode.createNode<CreateRoomFlowNode>(buildContext, plugins)
|
||||
}
|
||||
}
|
||||
override fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
callback: CreateRoomEntryPoint.Callback,
|
||||
): Node {
|
||||
return parentNode.createNode<CreateRoomFlowNode>(buildContext, listOf(callback))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,13 +12,13 @@ import androidx.compose.ui.Modifier
|
|||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.invitepeople.api.InvitePeoplePresenter
|
||||
import io.element.android.features.invitepeople.api.InvitePeopleRenderer
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
|
@ -39,10 +39,7 @@ class AddPeopleNode(
|
|||
fun onFinish()
|
||||
}
|
||||
|
||||
private fun onFinish() {
|
||||
plugins<Callback>().forEach { it.onFinish() }
|
||||
}
|
||||
|
||||
private val callback: Callback = callback()
|
||||
private val roomId = inputs<Inputs>().roomId
|
||||
private val invitePeoplePresenter = invitePeoplePresenterFactory.create(
|
||||
joinedRoom = null,
|
||||
|
|
@ -54,7 +51,7 @@ class AddPeopleNode(
|
|||
val state = invitePeoplePresenter.present()
|
||||
AddPeopleView(
|
||||
state = state,
|
||||
onFinish = ::onFinish,
|
||||
onFinish = callback::onFinish,
|
||||
) {
|
||||
invitePeopleRenderer.Render(state, Modifier)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,11 +13,11 @@ import com.bumble.appyx.core.lifecycle.subscribe
|
|||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.MobileScreen
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
|
|
@ -42,9 +42,7 @@ class ConfigureRoomNode(
|
|||
)
|
||||
}
|
||||
|
||||
private fun onCreateRoomSuccess(roomId: RoomId) {
|
||||
plugins<Callback>().forEach { it.onCreateRoomSuccess(roomId) }
|
||||
}
|
||||
private val callback: Callback = callback()
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
|
|
@ -53,7 +51,7 @@ class ConfigureRoomNode(
|
|||
state = state,
|
||||
modifier = modifier,
|
||||
onBackClick = this::navigateUp,
|
||||
onCreateRoomSuccess = ::onCreateRoomSuccess,
|
||||
onCreateRoomSuccess = callback::onCreateRoomSuccess,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,9 +38,11 @@ class DefaultCreateRoomEntryPointTest {
|
|||
val callback = object : CreateRoomEntryPoint.Callback {
|
||||
override fun onRoomCreated(roomId: RoomId) = lambdaError()
|
||||
}
|
||||
val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null))
|
||||
.callback(callback)
|
||||
.build()
|
||||
val result = entryPoint.createNode(
|
||||
parentNode = parentNode,
|
||||
buildContext = BuildContext.root(null),
|
||||
callback = callback,
|
||||
)
|
||||
assertThat(result.plugins).contains(callback)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
19
features/createroom/test/build.gradle.kts
Normal file
19
features/createroom/test/build.gradle.kts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.createroom.test"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.features.createroom.api)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.tests.testutils)
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.createroom.api
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeCreateRoomEntryPoint : CreateRoomEntryPoint {
|
||||
override fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
callback: CreateRoomEntryPoint.Callback,
|
||||
): Node = lambdaError()
|
||||
}
|
||||
|
|
@ -11,12 +11,10 @@ import com.bumble.appyx.core.modality.BuildContext
|
|||
import com.bumble.appyx.core.node.Node
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.features.deactivation.api.AccountDeactivationEntryPoint
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@Inject
|
||||
class DefaultAccountDeactivationEntryPoint : AccountDeactivationEntryPoint {
|
||||
override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
|
||||
return parentNode.createNode<AccountDeactivationNode>(buildContext)
|
||||
|
|
|
|||
19
features/deactivation/test/build.gradle.kts
Normal file
19
features/deactivation/test/build.gradle.kts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.deactivation.test"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.features.deactivation.api)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.tests.testutils)
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.deactivation.test
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import io.element.android.features.deactivation.api.AccountDeactivationEntryPoint
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeAccountDeactivationEntryPoint : AccountDeactivationEntryPoint {
|
||||
override fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
): Node {
|
||||
lambdaError()
|
||||
}
|
||||
}
|
||||
|
|
@ -7,9 +7,8 @@
|
|||
|
||||
package io.element.android.features.enterprise.api
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import io.element.android.compound.tokens.generated.SemanticColors
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import io.element.android.compound.colors.SemanticColorsLightDark
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
|
|
@ -21,20 +20,19 @@ interface EnterpriseService {
|
|||
|
||||
/**
|
||||
* Override the brand color.
|
||||
* @param sessionId the session to override the brand color for, or null to set the brand color to use when there is no session.
|
||||
* @param brandColor the color in hex format (#RRGGBBAA or #RRGGBB), or null to reset to default.
|
||||
*/
|
||||
fun overrideBrandColor(brandColor: String?)
|
||||
suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?)
|
||||
|
||||
@Composable
|
||||
fun semanticColorsLight(): State<SemanticColors>
|
||||
fun brandColorsFlow(sessionId: SessionId?): Flow<Color?>
|
||||
|
||||
@Composable
|
||||
fun semanticColorsDark(): State<SemanticColors>
|
||||
fun semanticColorsFlow(sessionId: SessionId?): Flow<SemanticColorsLightDark>
|
||||
|
||||
fun firebasePushGateway(): String?
|
||||
fun unifiedPushDefaultPushGateway(): String?
|
||||
|
||||
val bugReportUrlFlow: Flow<BugReportUrl>
|
||||
fun bugReportUrlFlow(sessionId: SessionId?): Flow<BugReportUrl>
|
||||
|
||||
companion object {
|
||||
const val ANY_ACCOUNT_PROVIDER = "*"
|
||||
|
|
|
|||
|
|
@ -7,23 +7,17 @@
|
|||
|
||||
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 androidx.compose.ui.graphics.Color
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.compound.tokens.generated.SemanticColors
|
||||
import io.element.android.compound.tokens.generated.compoundColorsDark
|
||||
import io.element.android.compound.tokens.generated.compoundColorsLight
|
||||
import io.element.android.compound.colors.SemanticColorsLightDark
|
||||
import io.element.android.features.enterprise.api.BugReportUrl
|
||||
import io.element.android.features.enterprise.api.EnterpriseService
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@Inject
|
||||
class DefaultEnterpriseService : EnterpriseService {
|
||||
override val isEnterpriseBuild = false
|
||||
|
||||
|
|
@ -32,20 +26,20 @@ class DefaultEnterpriseService : EnterpriseService {
|
|||
override fun defaultHomeserverList(): List<String> = emptyList()
|
||||
override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true
|
||||
|
||||
override fun overrideBrandColor(brandColor: String?) = Unit
|
||||
override suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?) = Unit
|
||||
|
||||
@Composable
|
||||
override fun semanticColorsLight(): State<SemanticColors> {
|
||||
return remember { derivedStateOf { compoundColorsLight } }
|
||||
override fun brandColorsFlow(sessionId: SessionId?): Flow<Color?> {
|
||||
return flowOf(null)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun semanticColorsDark(): State<SemanticColors> {
|
||||
return remember { derivedStateOf { compoundColorsDark } }
|
||||
override fun semanticColorsFlow(sessionId: SessionId?): Flow<SemanticColorsLightDark> {
|
||||
return flowOf(SemanticColorsLightDark.default)
|
||||
}
|
||||
|
||||
override fun firebasePushGateway(): String? = null
|
||||
override fun unifiedPushDefaultPushGateway(): String? = null
|
||||
|
||||
override val bugReportUrlFlow = flowOf(BugReportUrl.UseDefault)
|
||||
override fun bugReportUrlFlow(sessionId: SessionId?): Flow<BugReportUrl> {
|
||||
return flowOf(BugReportUrl.UseDefault)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,12 +8,10 @@
|
|||
package io.element.android.features.enterprise.impl
|
||||
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.features.enterprise.api.SessionEnterpriseService
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
@Inject
|
||||
class DefaultSessionEnterpriseService : SessionEnterpriseService {
|
||||
override suspend fun init() = Unit
|
||||
override suspend fun isElementCallAvailable(): Boolean = true
|
||||
|
|
|
|||
|
|
@ -7,12 +7,10 @@
|
|||
|
||||
package io.element.android.features.enterprise.impl
|
||||
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.compound.tokens.generated.compoundColorsDark
|
||||
import io.element.android.compound.tokens.generated.compoundColorsLight
|
||||
import io.element.android.compound.colors.SemanticColorsLightDark
|
||||
import io.element.android.features.enterprise.api.BugReportUrl
|
||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
|
@ -44,28 +42,59 @@ class DefaultEnterpriseServiceTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `semanticColorsLight always emits the same value`() = runTest {
|
||||
fun `semanticColorsFlow always emits the same value`() = runTest {
|
||||
val defaultEnterpriseService = DefaultEnterpriseService()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
defaultEnterpriseService.semanticColorsLight().value
|
||||
}.test {
|
||||
defaultEnterpriseService.semanticColorsFlow(null).test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState).isEqualTo(compoundColorsLight)
|
||||
defaultEnterpriseService.overrideBrandColor("#87654321")
|
||||
expectNoEvents()
|
||||
assertThat(initialState).isEqualTo(SemanticColorsLightDark.default)
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `semanticColorsDark always emits the same value`() = runTest {
|
||||
fun `brandColorsFlow always emits null`() = runTest {
|
||||
val defaultEnterpriseService = DefaultEnterpriseService()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
defaultEnterpriseService.semanticColorsDark().value
|
||||
}.test {
|
||||
defaultEnterpriseService.brandColorsFlow(null).test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState).isEqualTo(compoundColorsDark)
|
||||
defaultEnterpriseService.overrideBrandColor("#87654321")
|
||||
expectNoEvents()
|
||||
assertThat(initialState).isNull()
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `semanticColorsFlow always emits the same value for a session`() = runTest {
|
||||
val defaultEnterpriseService = DefaultEnterpriseService()
|
||||
defaultEnterpriseService.semanticColorsFlow(A_SESSION_ID).test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState).isEqualTo(SemanticColorsLightDark.default)
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `overrideBrandColor has no effect`() = runTest {
|
||||
val defaultEnterpriseService = DefaultEnterpriseService()
|
||||
defaultEnterpriseService.overrideBrandColor(A_SESSION_ID, "aColor")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `firebasePushGateway returns null`() = runTest {
|
||||
val defaultEnterpriseService = DefaultEnterpriseService()
|
||||
assertThat(defaultEnterpriseService.firebasePushGateway()).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unifiedPushDefaultPushGateway returns null`() = runTest {
|
||||
val defaultEnterpriseService = DefaultEnterpriseService()
|
||||
assertThat(defaultEnterpriseService.unifiedPushDefaultPushGateway()).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `bugReportUrlFlow only emits UseDefault`() = runTest {
|
||||
val defaultEnterpriseService = DefaultEnterpriseService()
|
||||
defaultEnterpriseService.bugReportUrlFlow(A_SESSION_ID).test {
|
||||
assertThat(awaitItem()).isEqualTo(BugReportUrl.UseDefault)
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,8 @@
|
|||
|
||||
package io.element.android.features.enterprise.test
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import io.element.android.compound.tokens.generated.SemanticColors
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import io.element.android.compound.colors.SemanticColorsLightDark
|
||||
import io.element.android.features.enterprise.api.BugReportUrl
|
||||
import io.element.android.features.enterprise.api.EnterpriseService
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
|
@ -24,12 +23,14 @@ class FakeEnterpriseService(
|
|||
private val isEnterpriseUserResult: (SessionId) -> Boolean = { lambdaError() },
|
||||
private val defaultHomeserverListResult: () -> List<String> = { emptyList() },
|
||||
private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() },
|
||||
private val semanticColorsLightResult: () -> State<SemanticColors> = { lambdaError() },
|
||||
private val semanticColorsDarkResult: () -> State<SemanticColors> = { lambdaError() },
|
||||
private val overrideBrandColorResult: (String?) -> Unit = { lambdaError() },
|
||||
initialSemanticColors: SemanticColorsLightDark = SemanticColorsLightDark.default,
|
||||
private val overrideBrandColorResult: (SessionId?, String?) -> Unit = { _, _ -> lambdaError() },
|
||||
private val firebasePushGatewayResult: () -> String? = { lambdaError() },
|
||||
private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() },
|
||||
) : EnterpriseService {
|
||||
private val brandColorState = MutableStateFlow<Color?>(null)
|
||||
private val semanticColorsState = MutableStateFlow(initialSemanticColors)
|
||||
|
||||
override suspend fun isEnterpriseUser(sessionId: SessionId): Boolean = simulateLongTask {
|
||||
isEnterpriseUserResult(sessionId)
|
||||
}
|
||||
|
|
@ -42,18 +43,16 @@ class FakeEnterpriseService(
|
|||
isAllowedToConnectToHomeserverResult(homeserverUrl)
|
||||
}
|
||||
|
||||
override fun overrideBrandColor(brandColor: String?) {
|
||||
overrideBrandColorResult(brandColor)
|
||||
override suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?) = simulateLongTask {
|
||||
overrideBrandColorResult(sessionId, brandColor)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun semanticColorsLight(): State<SemanticColors> {
|
||||
return semanticColorsLightResult()
|
||||
override fun brandColorsFlow(sessionId: SessionId?): Flow<Color?> {
|
||||
return brandColorState.asStateFlow()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun semanticColorsDark(): State<SemanticColors> {
|
||||
return semanticColorsDarkResult()
|
||||
override fun semanticColorsFlow(sessionId: SessionId?): Flow<SemanticColorsLightDark> {
|
||||
return semanticColorsState.asStateFlow()
|
||||
}
|
||||
|
||||
override fun firebasePushGateway(): String? {
|
||||
|
|
@ -65,5 +64,7 @@ class FakeEnterpriseService(
|
|||
}
|
||||
|
||||
val bugReportUrlMutableFlow = MutableStateFlow<BugReportUrl>(BugReportUrl.UseDefault)
|
||||
override val bugReportUrlFlow: Flow<BugReportUrl> = bugReportUrlMutableFlow.asStateFlow()
|
||||
override fun bugReportUrlFlow(sessionId: SessionId?): Flow<BugReportUrl> {
|
||||
return bugReportUrlMutableFlow.asStateFlow()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
19
features/forward/api/build.gradle.kts
Normal file
19
features/forward/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.forward.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.forward.api
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.timeline.TimelineProvider
|
||||
|
||||
interface ForwardEntryPoint : FeatureEntryPoint {
|
||||
interface Callback : Plugin {
|
||||
fun onDone(roomIds: List<RoomId>)
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val eventId: EventId,
|
||||
val timelineProvider: TimelineProvider,
|
||||
) : NodeInputs
|
||||
|
||||
fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
params: Params,
|
||||
callback: Callback,
|
||||
): Node
|
||||
}
|
||||
40
features/forward/impl/build.gradle.kts
Normal file
40
features/forward/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
id("kotlin-parcelize")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.forward.impl"
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
isIncludeAndroidResources = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setupDependencyInjection()
|
||||
|
||||
dependencies {
|
||||
api(projects.features.forward.api)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.roomselect.api)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
|
||||
testCommonDependencies(libs, true)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.roomselect.test)
|
||||
testImplementation(projects.libraries.testtags)
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.forward.impl
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.features.forward.api.ForwardEntryPoint
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class DefaultForwardEntryPoint : ForwardEntryPoint {
|
||||
override fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
params: ForwardEntryPoint.Params,
|
||||
callback: ForwardEntryPoint.Callback,
|
||||
): Node {
|
||||
return parentNode.createNode<ForwardMessagesNode>(
|
||||
buildContext = buildContext,
|
||||
plugins = listOf(
|
||||
ForwardMessagesNode.Inputs(
|
||||
eventId = params.eventId,
|
||||
timelineProvider = params.timelineProvider,
|
||||
),
|
||||
callback,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.impl.forward
|
||||
package io.element.android.features.forward.impl
|
||||
|
||||
sealed interface ForwardMessagesEvents {
|
||||
data object ClearError : ForwardMessagesEvents
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue