diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7e5f16200a..c60a89e2f6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -56,7 +56,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Assemble debug APKs diff --git a/.github/workflows/build_enterprise.yml b/.github/workflows/build_enterprise.yml index 71195ff163..aa00b74c44 100644 --- a/.github/workflows/build_enterprise.yml +++ b/.github/workflows/build_enterprise.yml @@ -61,7 +61,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Assemble debug Gplay Enterprise APK diff --git a/.github/workflows/generate_github_pages.yml b/.github/workflows/generate_github_pages.yml index 8780cd7c7b..55a300dd88 100644 --- a/.github/workflows/generate_github_pages.yml +++ b/.github/workflows/generate_github_pages.yml @@ -16,14 +16,14 @@ jobs: contents: write steps: - name: ⏬ Checkout with LFS - uses: nschloe/action-cached-lfs-checkout@1c185ad576953eab13e35ffe1bffef437d97e9d2 # v1.2.4 + uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5 - name: Use JDK 21 uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Set up Python 3.12 diff --git a/.github/workflows/maestro-local.yml b/.github/workflows/maestro-local.yml index c5587f40c1..8903ed0e57 100644 --- a/.github/workflows/maestro-local.yml +++ b/.github/workflows/maestro-local.yml @@ -50,7 +50,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Assemble debug APK diff --git a/.github/workflows/nightlyReports.yml b/.github/workflows/nightlyReports.yml index 10d41f5d1a..314929d801 100644 --- a/.github/workflows/nightlyReports.yml +++ b/.github/workflows/nightlyReports.yml @@ -34,7 +34,7 @@ jobs: swap-storage: false - name: ⏬ Checkout with LFS - uses: nschloe/action-cached-lfs-checkout@1c185ad576953eab13e35ffe1bffef437d97e9d2 # v1.2.4 + uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5 - name: Use JDK 21 uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 @@ -43,7 +43,7 @@ jobs: java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: cache-read-only: false @@ -85,7 +85,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Dependency analysis diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index a1c7bd3c70..9a191ba15b 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -74,7 +74,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Set up Python 3.12 @@ -113,7 +113,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Run Konsist tests @@ -154,7 +154,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Run compose tests @@ -188,7 +188,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Build Gplay Debug @@ -233,7 +233,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Run Detekt @@ -274,7 +274,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Run Ktlint check diff --git a/.github/workflows/recordScreenshots.yml b/.github/workflows/recordScreenshots.yml index 4bec9643c5..0f4c8ee581 100644 --- a/.github/workflows/recordScreenshots.yml +++ b/.github/workflows/recordScreenshots.yml @@ -43,13 +43,13 @@ jobs: labels: Record-Screenshots - name: ⏬ Checkout with LFS (PR) if: github.event.label.name == 'Record-Screenshots' - uses: nschloe/action-cached-lfs-checkout@1c185ad576953eab13e35ffe1bffef437d97e9d2 # v1.2.4 + uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5 with: persist-credentials: false ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref }} - name: ⏬ Checkout with LFS (Branch) if: github.event_name == 'workflow_dispatch' - uses: nschloe/action-cached-lfs-checkout@1c185ad576953eab13e35ffe1bffef437d97e9d2 # v1.2.4 + uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5 with: persist-credentials: false - name: ☕️ Use JDK 21 @@ -59,7 +59,7 @@ jobs: java-version: '21' # Add gradle cache, this should speed up the process - name: Configure gradle - uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Record screenshots diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b5514a72f0..73cba6c8f7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -43,7 +43,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 - name: Create app bundle env: ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }} @@ -87,7 +87,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 - name: Create Enterprise app bundle env: ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }} @@ -131,7 +131,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 - name: Create APKs env: ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }} diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 347d99c0f7..40cf9d0058 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -50,7 +50,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Build debug code and test fixtures diff --git a/.github/workflows/sync-localazy.yml b/.github/workflows/sync-localazy.yml index e37076b9de..f45a926814 100644 --- a/.github/workflows/sync-localazy.yml +++ b/.github/workflows/sync-localazy.yml @@ -22,7 +22,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Set up Python 3.12 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f4205758c1..0ce66df478 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -49,7 +49,7 @@ jobs: sudo swapon /mnt/swapfile sudo swapon --show - name: ⏬ Checkout with LFS - uses: nschloe/action-cached-lfs-checkout@1c185ad576953eab13e35ffe1bffef437d97e9d2 # v1.2.4 + uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5 with: # Ensure we are building the branch and not the branch after being merged on develop # https://github.com/actions/checkout/issues/881 @@ -68,7 +68,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} @@ -108,7 +108,7 @@ jobs: # https://github.com/codecov/codecov-action - name: ☂️ Upload coverage reports to codecov - uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 + uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 with: fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/validate-lfs.yml b/.github/workflows/validate-lfs.yml index c3158291c3..027c7d68e9 100644 --- a/.github/workflows/validate-lfs.yml +++ b/.github/workflows/validate-lfs.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest name: Validate steps: - - uses: nschloe/action-cached-lfs-checkout@1c185ad576953eab13e35ffe1bffef437d97e9d2 # v1.2.4 + - uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5 - run: | ./tools/git/validate_lfs.sh diff --git a/CHANGES.md b/CHANGES.md index 830edde763..a3d7870288 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,14 @@ +Changes in Element X v26.04.2 +============================= + +## What's Changed +### 🐛 Bugfixes +* Restore enterprise submodule. by @bmarty in https://github.com/element-hq/element-x-android/pull/6541 +### Dependency upgrades +* fix(deps): update dependency io.element.android:element-call-embedded to v0.19.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6538 + +**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.04.1...v26.04.2 + Changes in Element X v26.04.1 ============================= diff --git a/app/src/main/kotlin/io/element/android/x/MainActivity.kt b/app/src/main/kotlin/io/element/android/x/MainActivity.kt index b522edd137..502518bf3c 100644 --- a/app/src/main/kotlin/io/element/android/x/MainActivity.kt +++ b/app/src/main/kotlin/io/element/android/x/MainActivity.kt @@ -52,7 +52,7 @@ class MainActivity : NodeActivity() { private lateinit var appBindings: AppBindings override fun onCreate(savedInstanceState: Bundle?) { - Timber.tag(loggerTag.value).w("onCreate, with savedInstanceState: ${savedInstanceState != null}") + Timber.tag(loggerTag.value).d("onCreate, with savedInstanceState: ${savedInstanceState != null}") installSplashScreen() super.onCreate(savedInstanceState) appBindings = bindings() @@ -108,7 +108,7 @@ class MainActivity : NodeActivity() { plugins = listOf( object : NodeReadyObserver { override fun init(node: MainNode) { - Timber.tag(loggerTag.value).w("onMainNodeInit") + Timber.tag(loggerTag.value).d("onMainNodeInit") mainNode = node mainNode.handleIntent(intent) } @@ -144,7 +144,7 @@ class MainActivity : NodeActivity() { */ override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) - Timber.tag(loggerTag.value).w("onNewIntent") + Timber.tag(loggerTag.value).d("onNewIntent") // If the mainNode is not init yet, keep the intent for later. // It can happen when the activity is killed by the system. The methods are called in this order : // onCreate(savedInstanceState=true) -> onNewIntent -> onResume -> onMainNodeInit @@ -157,16 +157,16 @@ class MainActivity : NodeActivity() { override fun onPause() { super.onPause() - Timber.tag(loggerTag.value).w("onPause") + Timber.tag(loggerTag.value).d("onPause") } override fun onResume() { super.onResume() - Timber.tag(loggerTag.value).w("onResume") + Timber.tag(loggerTag.value).d("onResume") } override fun onDestroy() { super.onDestroy() - Timber.tag(loggerTag.value).w("onDestroy") + Timber.tag(loggerTag.value).d("onDestroy") } } diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml index a171646bad..c95b3a5cc0 100644 --- a/app/src/main/res/xml/locales_config.xml +++ b/app/src/main/res/xml/locales_config.xml @@ -19,6 +19,7 @@ + @@ -35,6 +36,7 @@ + diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/TimelineConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/TimelineConfig.kt index d4fe7d1fc5..7e5f064ef4 100644 --- a/appconfig/src/main/kotlin/io/element/android/appconfig/TimelineConfig.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/TimelineConfig.kt @@ -18,7 +18,6 @@ object TimelineConfig { */ val excludedEvents = listOf( StateEventType.CallMember, - StateEventType.RoomAliases, StateEventType.RoomCanonicalAlias, StateEventType.RoomGuestAccess, StateEventType.RoomHistoryVisibility, diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts index ecac391216..24a0355b3f 100644 --- a/appnav/build.gradle.kts +++ b/appnav/build.gradle.kts @@ -51,7 +51,7 @@ dependencies { implementation(projects.features.linknewdevice.api) implementation(projects.features.share.api) - implementation(projects.services.apperror.impl) + implementation(projects.services.apperror.api) implementation(projects.services.appnavstate.api) implementation(projects.services.analytics.api) @@ -67,8 +67,7 @@ dependencies { testImplementation(projects.features.messages.test) testImplementation(projects.features.networkmonitor.test) testImplementation(projects.features.rageshake.test) - testImplementation(projects.services.appnavstate.impl) + testImplementation(projects.services.apperror.test) testImplementation(projects.services.appnavstate.test) testImplementation(projects.services.analytics.test) - testImplementation(projects.services.toolbox.test) } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt index 745ab390b2..0e458d3b9c 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -252,7 +252,8 @@ class RootFlowNode( val transitionHandler = rememberDelegateTransitionHandler { navTarget -> when (navTarget) { is NavTarget.SplashScreen, - is NavTarget.LoggedInFlow -> backstackFader + is NavTarget.LoggedInFlow, + is NavTarget.NotLoggedInFlow -> backstackFader else -> backstackSlider } } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt index 757dd73395..8dc2de5e4e 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt @@ -21,6 +21,8 @@ import androidx.compose.runtime.setValue import dev.zacsweers.metro.Inject import im.vector.app.features.analytics.plan.CryptoSessionStateChange import im.vector.app.features.analytics.plan.UserProperties +import io.element.android.features.networkmonitor.api.NetworkMonitor +import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.extensions.runCatchingExceptions @@ -56,6 +58,7 @@ class LoggedInPresenter( private val analyticsService: AnalyticsService, private val encryptionService: EncryptionService, private val buildMeta: BuildMeta, + private val networkMonitor: NetworkMonitor, ) : Presenter { @Composable override fun present(): LoggedInState { @@ -107,6 +110,14 @@ class LoggedInPresenter( }.launchIn(this) } + val networkConnectivity by networkMonitor.connectivity.collectAsState() + LaunchedEffect(networkConnectivity) { + if (networkConnectivity == NetworkStatus.Connected) { + // Refresh homeserver capabilities when the network is back + matrixClient.homeserverCapabilities().refresh() + } + } + fun handleEvent(event: LoggedInEvents) { when (event) { is LoggedInEvents.CloseErrorDialog -> { diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootView.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootView.kt index 32c8e52084..19aa820433 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootView.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootView.kt @@ -22,7 +22,7 @@ import io.element.android.features.rageshake.api.detection.RageshakeDetectionVie import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.services.apperror.impl.AppErrorView +import io.element.android.services.apperror.api.AppErrorView @Composable fun RootView( diff --git a/appnav/src/main/res/values-ja/translations.xml b/appnav/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..3405cc1d68 --- /dev/null +++ b/appnav/src/main/res/values-ja/translations.xml @@ -0,0 +1,6 @@ + + + "ログアウトしてアップグレード" + "%1$s は古いプロトコルに非対応になりました。アプリを引き続き使用するには、ログアウトしてから再度ログインしてください。" + "使用しているホームサーバーは古いプロトコルに非対応になりました。アプリケーションを引き続き使用するには、ログアウトしてから再度ログインしてください。" + diff --git a/appnav/src/main/res/values-vi/translations.xml b/appnav/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..8fa3a7ee30 --- /dev/null +++ b/appnav/src/main/res/values-vi/translations.xml @@ -0,0 +1,6 @@ + + + "Đăng xuất & Nâng cấp" + "%1$s không còn hỗ trợ giao thức cũ. Vui lòng đăng xuất và đăng nhập lại để tiếp tục sử dụng ứng dụng." + "Homeserver của bạn không còn hỗ trợ giao thức cũ. Vui lòng đăng xuất và đăng nhập lại để tiếp tục sử dụng ứng dụng." + diff --git a/appnav/src/test/kotlin/io/element/android/appnav/JoinedRoomLoadedFlowNodeTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/JoinedRoomLoadedFlowNodeTest.kt index 8d514a2c0f..6dffda9e42 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/JoinedRoomLoadedFlowNodeTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/JoinedRoomLoadedFlowNodeTest.kt @@ -43,7 +43,7 @@ import io.element.android.services.analytics.api.watchers.AnalyticsSendMessageWa import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.analytics.test.watchers.FakeAnalyticsSendMessageWatcher import io.element.android.services.appnavstate.api.ActiveRoomsHolder -import io.element.android.services.appnavstate.impl.DefaultActiveRoomsHolder +import io.element.android.services.appnavstate.test.FakeActiveRoomsHolder import io.element.android.services.appnavstate.test.FakeAppNavigationStateService import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest @@ -128,7 +128,7 @@ class JoinedRoomLoadedFlowNodeTest { roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(), spaceEntryPoint: SpaceEntryPoint = FakeSpaceEntryPoint(), forwardEntryPoint: ForwardEntryPoint = FakeForwardEntryPoint(), - activeRoomsHolder: ActiveRoomsHolder = DefaultActiveRoomsHolder(), + activeRoomsHolder: ActiveRoomsHolder = FakeActiveRoomsHolder(), matrixClient: FakeMatrixClient = FakeMatrixClient(), ) = JoinedRoomLoadedFlowNode( buildContext = BuildContext.root(savedStateMap = null), @@ -213,7 +213,7 @@ class JoinedRoomLoadedFlowNodeTest { val fakeMessagesEntryPoint = FakeMessagesEntryPoint() val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint() val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Root()) - val activeRoomsHolder = DefaultActiveRoomsHolder() + val activeRoomsHolder = FakeActiveRoomsHolder() val roomFlowNode = createJoinedRoomLoadedFlowNode( plugins = listOf(inputs, FakeJoinedRoomLoadedFlowNodeCallback()), messagesEntryPoint = fakeMessagesEntryPoint, @@ -236,7 +236,7 @@ class JoinedRoomLoadedFlowNodeTest { val fakeMessagesEntryPoint = FakeMessagesEntryPoint() val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint() val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Root()) - val activeRoomsHolder = DefaultActiveRoomsHolder().apply { + val activeRoomsHolder = FakeActiveRoomsHolder().apply { addRoom(room) } val roomFlowNode = createJoinedRoomLoadedFlowNode( diff --git a/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt index 73d55135fb..9ba98a3f72 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt @@ -19,8 +19,7 @@ import io.element.android.libraries.matrix.test.FakeSdkMetadata import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.apperror.api.AppErrorState import io.element.android.services.apperror.api.AppErrorStateService -import io.element.android.services.apperror.impl.DefaultAppErrorStateService -import io.element.android.services.toolbox.test.strings.FakeStringProvider +import io.element.android.services.apperror.test.FakeAppErrorStateService import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -44,10 +43,16 @@ class RootPresenterTest { @Test fun `present - passes app error state`() = runTest { val presenter = createRootPresenter( - appErrorService = DefaultAppErrorStateService( - stringProvider = FakeStringProvider(), - ).apply { - showError("Bad news", "Something bad happened") + appErrorService = FakeAppErrorStateService().apply { + setAppErrorState( + AppErrorState.Error( + title = "Bad news", + body = "Something bad happened", + dismiss = { + setAppErrorState(AppErrorState.NoError) + } + ) + ) } ) moleculeFlow(RecompositionMode.Immediate) { @@ -65,9 +70,7 @@ class RootPresenterTest { } private fun createRootPresenter( - appErrorService: AppErrorStateService = DefaultAppErrorStateService( - stringProvider = FakeStringProvider(), - ), + appErrorService: AppErrorStateService = FakeAppErrorStateService(), ): RootPresenter { return RootPresenter( crashDetectionPresenter = { aCrashDetectionState() }, diff --git a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt index f1759eab3e..d147a4ed68 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt @@ -14,6 +14,8 @@ import app.cash.turbine.ReceiveTurbine import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.CryptoSessionStateChange import im.vector.app.features.analytics.plan.UserProperties +import io.element.android.features.networkmonitor.api.NetworkStatus +import io.element.android.features.networkmonitor.test.FakeNetworkMonitor import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.SessionId @@ -27,6 +29,7 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationS import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.FakeHomeserverCapabilitiesProvider import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService @@ -109,6 +112,7 @@ class LoggedInPresenterTest { val verificationService = FakeSessionVerificationService() val encryptionService = FakeEncryptionService() val buildMeta = aBuildMeta() + val networkMonitor = FakeNetworkMonitor() LoggedInPresenter( matrixClient = FakeMatrixClient( roomListService = roomListService, @@ -122,6 +126,7 @@ class LoggedInPresenterTest { analyticsService = analyticsService, encryptionService = encryptionService, buildMeta = buildMeta, + networkMonitor = networkMonitor, ).test { encryptionService.emitRecoveryState(RecoveryState.UNKNOWN) encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE) @@ -319,6 +324,27 @@ class LoggedInPresenterTest { } } + @Test + fun `present - refreshes homeserver capabilities when network is back`() = runTest { + val refreshLambda = lambdaRecorder> { Result.success(Unit) } + val matrixClient = FakeMatrixClient( + homeserverCapabilitiesProvider = FakeHomeserverCapabilitiesProvider(refresh = refreshLambda), + accountManagementUrlResult = { Result.success(null) }, + ) + val networkMonitor = FakeNetworkMonitor() + createLoggedInPresenter( + matrixClient = matrixClient, + networkMonitor = networkMonitor, + ).test { + awaitItem() + networkMonitor.connectivity.value = NetworkStatus.Connected + + advanceUntilIdle() + + refreshLambda.assertions().isCalledOnce() + } + } + private suspend fun ReceiveTurbine.awaitFirstItem(): T { skipItems(1) return awaitItem() @@ -334,6 +360,7 @@ class LoggedInPresenterTest { accountManagementUrlResult = { Result.success(null) }, ), buildMeta: BuildMeta = aBuildMeta(), + networkMonitor: FakeNetworkMonitor = FakeNetworkMonitor(), ): LoggedInPresenter { return LoggedInPresenter( matrixClient = matrixClient, @@ -343,6 +370,7 @@ class LoggedInPresenterTest { analyticsService = analyticsService, encryptionService = encryptionService, buildMeta = buildMeta, + networkMonitor = networkMonitor, ) } } diff --git a/fastlane/metadata/android/en-US/changelogs/202604030.txt b/fastlane/metadata/android/en-US/changelogs/202604030.txt new file mode 100644 index 0000000000..a4b397f1bb --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202604030.txt @@ -0,0 +1,2 @@ +Main changes in this version: bug fixes and improvements. +Full changelog: https://github.com/element-hq/element-x-android/releases \ No newline at end of file diff --git a/features/analytics/api/src/main/res/values-ja/translations.xml b/features/analytics/api/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..5554ee162f --- /dev/null +++ b/features/analytics/api/src/main/res/values-ja/translations.xml @@ -0,0 +1,7 @@ + + + "問題発見のため、匿名の使用データの共有にご協力ください。" + "利用規約の全文を%1$sから確認することができます。" + "こちら" + "使用データを共有" + diff --git a/features/analytics/api/src/main/res/values-lt/translations.xml b/features/analytics/api/src/main/res/values-lt/translations.xml index 08dd155332..004b20d79d 100644 --- a/features/analytics/api/src/main/res/values-lt/translations.xml +++ b/features/analytics/api/src/main/res/values-lt/translations.xml @@ -1,7 +1,7 @@ - "Dalinkitės anoniminiais naudojimo duomenimis ir padėkite mums nustatyti problemas." + "Bendrinkite anoniminius naudojimo duomenis, kad padėtumėte mums nustatyti problemas." "Galite perskaityti visas mūsų sąlygas %1$s." "čia" - "Dalytis analitiniais duomenimis" + "Bendrinti analitinius duomenis" diff --git a/features/analytics/api/src/main/res/values-vi/translations.xml b/features/analytics/api/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..0dc02d79d6 --- /dev/null +++ b/features/analytics/api/src/main/res/values-vi/translations.xml @@ -0,0 +1,7 @@ + + + "Chia sẻ dữ liệu sử dụng ẩn danh để giúp chúng tôi xác định vấn đề." + "Bạn có thể xem tất cả điều khoản của chúng tôi tại %1$s" + "tại đây" + "Chia sẻ dữ liệu phân tích" + diff --git a/features/analytics/impl/src/main/res/values-ja/translations.xml b/features/analytics/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..2cee69962c --- /dev/null +++ b/features/analytics/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,10 @@ + + + "いかなる個人情報も記録, 分析されることはありません" + "問題発見のため、匿名の使用データの共有にご協力ください。" + "利用規約の全文を%1$sから確認することができます。" + "こちら" + "いつでも設定は変更できます" + "情報が第三者に共有されることはありません" + "%1$s の改善にご協力ください" + diff --git a/features/analytics/impl/src/main/res/values-lt/translations.xml b/features/analytics/impl/src/main/res/values-lt/translations.xml index 11f2cbc74e..4f7c70dd6d 100644 --- a/features/analytics/impl/src/main/res/values-lt/translations.xml +++ b/features/analytics/impl/src/main/res/values-lt/translations.xml @@ -1,10 +1,10 @@ - "Mes nekaupsime ir neprofiliuosime jokių asmens duomenų" - "Dalinkitės anoniminiais naudojimo duomenimis ir padėkite mums nustatyti problemas." + "Mes neįrašysime ar neprofiliuosime jokių asmeninių duomenų." + "Bendrinkite anoniminius naudojimo duomenis, kad padėtumėte mums nustatyti problemas." "Galite perskaityti visas mūsų sąlygas %1$s." "čia" - "Tai galite bet kada išjungti" - "Mes nesidalinsime Jūsų duomenimis su trečiosiomis šalimis" - "Padėkite pagerinti %1$s" + "Tai galite išjungti bet kuriuo metu." + "Mes nebendrinsime jūsų duomenų su trečiosiomis šalimis." + "Padėkite patobulinti „%1$s“" diff --git a/features/analytics/impl/src/main/res/values-vi/translations.xml b/features/analytics/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..205a52c296 --- /dev/null +++ b/features/analytics/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,10 @@ + + + "Chúng tôi sẽ không ghi lại hoặc lập hồ sơ bất kỳ dữ liệu cá nhân nào." + "Chia sẻ dữ liệu sử dụng ẩn danh để giúp chúng tôi xác định vấn đề." + "Bạn có thể xem tất cả điều khoản của chúng tôi tại %1$s" + "tại đây" + "Bạn có thể tắt tính năng này bất cứ lúc nào" + "Chúng tôi sẽ không chia sẻ dữ liệu của bạn với bên thứ ba." + "Giúp cải thiện %1$s" + diff --git a/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/Announcement.kt b/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/Announcement.kt index 0bf35650a0..d743ae4cd6 100644 --- a/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/Announcement.kt +++ b/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/Announcement.kt @@ -8,7 +8,13 @@ package io.element.android.features.announcement.api -enum class Announcement { - Space, - NewNotificationSound, +import androidx.compose.runtime.Immutable + +@Immutable +sealed interface Announcement { + enum class Fullscreen : Announcement { + Space, + } + + data object NewNotificationSound : Announcement } diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementEvent.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementEvent.kt new file mode 100644 index 0000000000..947a3ceeba --- /dev/null +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementEvent.kt @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * 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.announcement.impl + +import io.element.android.features.announcement.api.Announcement + +sealed interface AnnouncementEvent { + data class Continue( + val announcement: Announcement.Fullscreen, + ) : AnnouncementEvent +} diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenter.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenter.kt index 508f1e44a0..bd45ddb956 100644 --- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenter.kt +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenter.kt @@ -12,12 +12,16 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import dev.zacsweers.metro.Inject import io.element.android.features.announcement.api.Announcement import io.element.android.features.announcement.impl.store.AnnouncementStatus import io.element.android.features.announcement.impl.store.AnnouncementStore import io.element.android.libraries.architecture.Presenter +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch @Inject class AnnouncementPresenter( @@ -25,13 +29,39 @@ class AnnouncementPresenter( ) : Presenter { @Composable override fun present(): AnnouncementState { - val showSpaceAnnouncement by remember { - announcementStore.announcementStatusFlow(Announcement.Space).map { - it == AnnouncementStatus.Show + val coroutineScope = rememberCoroutineScope() + + val fullscreenAnnouncementToShow by remember { + combine( + flowOf(Unit), + announcementStore.announcementStatusFlow(Announcement.Fullscreen.Space).map { + it == AnnouncementStatus.Show + }, + // Add other announcements here when needed + ) { _, showFullscreenSpace -> + when { + showFullscreenSpace -> Announcement.Fullscreen.Space + else -> { + null + } + } } - }.collectAsState(false) + }.collectAsState(null) + + fun handle(event: AnnouncementEvent) { + when (event) { + is AnnouncementEvent.Continue -> coroutineScope.launch { + announcementStore.setAnnouncementStatus( + announcement = event.announcement, + status = AnnouncementStatus.Shown, + ) + } + } + } + return AnnouncementState( - showSpaceAnnouncement = showSpaceAnnouncement, + announcement = fullscreenAnnouncementToShow, + eventSink = ::handle, ) } } diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementState.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementState.kt index e762dd607f..fb0732450d 100644 --- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementState.kt +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementState.kt @@ -8,12 +8,9 @@ package io.element.android.features.announcement.impl -data class AnnouncementState( - val showSpaceAnnouncement: Boolean, -) +import io.element.android.features.announcement.api.Announcement -fun anAnnouncementState( - showSpaceAnnouncement: Boolean = false, -) = AnnouncementState( - showSpaceAnnouncement = showSpaceAnnouncement, +data class AnnouncementState( + val announcement: Announcement.Fullscreen?, + val eventSink: (AnnouncementEvent) -> Unit, ) diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementStateProvider.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementStateProvider.kt new file mode 100644 index 0000000000..2412fee167 --- /dev/null +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementStateProvider.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * 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.announcement.impl + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.announcement.api.Announcement + +open class AnnouncementStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + anAnnouncementState(), + anAnnouncementState( + announcement = Announcement.Fullscreen.Space, + ), + ) +} + +fun anAnnouncementState( + announcement: Announcement.Fullscreen? = null, + eventSink: (AnnouncementEvent) -> Unit = {}, +) = AnnouncementState( + announcement = announcement, + eventSink = eventSink, +) diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementService.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementService.kt index 0e5c30178c..adb81db61a 100644 --- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementService.kt +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementService.kt @@ -8,35 +8,28 @@ package io.element.android.features.announcement.impl -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding 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 -import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementView +import io.element.android.features.announcement.impl.fullscreen.FullscreenAnnouncementView import io.element.android.features.announcement.impl.store.AnnouncementStatus import io.element.android.features.announcement.impl.store.AnnouncementStore -import io.element.android.libraries.architecture.Presenter import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf @ContributesBinding(AppScope::class) class DefaultAnnouncementService( private val announcementStore: AnnouncementStore, - private val announcementPresenter: Presenter, - private val spaceAnnouncementPresenter: Presenter, + private val announcementPresenter: AnnouncementPresenter, ) : AnnouncementService { override suspend fun showAnnouncement(announcement: Announcement) { when (announcement) { - Announcement.Space -> showSpaceAnnouncement() + is Announcement.Fullscreen -> showFullscreenAnnouncement(announcement) Announcement.NewNotificationSound -> { announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStatus.Show) } @@ -49,13 +42,10 @@ class DefaultAnnouncementService( override fun announcementsToShowFlow(): Flow> { return combine( - announcementStore.announcementStatusFlow(Announcement.Space), + flowOf(Unit), announcementStore.announcementStatusFlow(Announcement.NewNotificationSound), - ) { spaceAnnouncementStatus, newNotificationSoundStatus -> + ) { _, newNotificationSoundStatus -> buildList { - if (spaceAnnouncementStatus == AnnouncementStatus.Show) { - add(Announcement.Space) - } if (newNotificationSoundStatus == AnnouncementStatus.Show) { add(Announcement.NewNotificationSound) } @@ -63,27 +53,19 @@ class DefaultAnnouncementService( } } - private suspend fun showSpaceAnnouncement() { - val currentValue = announcementStore.announcementStatusFlow(Announcement.Space).first() + private suspend fun showFullscreenAnnouncement(announcement: Announcement.Fullscreen) { + val currentValue = announcementStore.announcementStatusFlow(announcement).first() if (currentValue == AnnouncementStatus.NeverShown) { - announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Show) + announcementStore.setAnnouncementStatus(announcement, AnnouncementStatus.Show) } } @Composable override fun Render(modifier: Modifier) { val announcementState = announcementPresenter.present() - Box(modifier = modifier.fillMaxSize()) { - AnimatedVisibility( - visible = announcementState.showSpaceAnnouncement, - enter = fadeIn(), - exit = fadeOut(), - ) { - val spaceAnnouncementState = spaceAnnouncementPresenter.present() - SpaceAnnouncementView( - state = spaceAnnouncementState, - ) - } - } + FullscreenAnnouncementView( + state = announcementState, + modifier = modifier, + ) } } diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/di/AnnouncementModule.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/di/AnnouncementModule.kt deleted file mode 100644 index 4cfc073271..0000000000 --- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/di/AnnouncementModule.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * 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.announcement.impl.di - -import dev.zacsweers.metro.AppScope -import dev.zacsweers.metro.BindingContainer -import dev.zacsweers.metro.Binds -import dev.zacsweers.metro.ContributesTo -import io.element.android.features.announcement.impl.AnnouncementPresenter -import io.element.android.features.announcement.impl.AnnouncementState -import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementPresenter -import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementState -import io.element.android.libraries.architecture.Presenter - -@ContributesTo(AppScope::class) -@BindingContainer -interface AnnouncementModule { - @Binds - fun bindAnnouncementPresenter(presenter: AnnouncementPresenter): Presenter - - @Binds - fun bindSpaceAnnouncementPresenter(presenter: SpaceAnnouncementPresenter): Presenter -} diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/fullscreen/FullscreenAnnouncementView.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/fullscreen/FullscreenAnnouncementView.kt new file mode 100644 index 0000000000..c544fd4914 --- /dev/null +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/fullscreen/FullscreenAnnouncementView.kt @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * 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.announcement.impl.fullscreen + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.announcement.api.Announcement +import io.element.android.features.announcement.impl.AnnouncementEvent +import io.element.android.features.announcement.impl.AnnouncementState +import io.element.android.features.announcement.impl.AnnouncementStateProvider +import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule +import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule +import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem +import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism +import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage +import io.element.android.libraries.designsystem.components.BigIcon +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +/** + * Ref: https://www.figma.com/design/kcnHxunG1LDWXsJhaNuiHz/ER-145--Spaces-on-Element-X?node-id=4593-40181 + */ +@Composable +fun FullscreenAnnouncementView( + state: AnnouncementState, + modifier: Modifier = Modifier, +) { + // Ensure that the content stays visible during the exit animation + var fullscreenAnnouncement by remember { mutableStateOf(null) } + if (state.announcement != null) { + fullscreenAnnouncement = state.announcement + } + Box(modifier = modifier.fillMaxSize()) { + AnimatedVisibility( + visible = state.announcement != null, + enter = fadeIn(), + exit = fadeOut(), + ) { + fullscreenAnnouncement?.let { + FullscreenAnnouncementView( + announcement = it, + eventSink = state.eventSink, + ) + } + } + } +} + +@Composable +private fun FullscreenAnnouncementView( + announcement: Announcement.Fullscreen, + eventSink: (AnnouncementEvent) -> Unit, + modifier: Modifier = Modifier +) { + fun onContinue() { + eventSink(AnnouncementEvent.Continue(announcement)) + } + + BackHandler(onBack = ::onContinue) + HeaderFooterPage( + modifier = modifier, + isScrollable = true, + contentPadding = PaddingValues(top = 24.dp, start = 16.dp, end = 16.dp, bottom = 24.dp), + header = { + FullscreenAnnouncementHeader(announcement) + }, + content = { + FullscreenAnnouncementContent( + modifier = Modifier.padding(horizontal = 8.dp), + announcement = announcement, + ) + }, + footer = { + FullscreenAnnouncementFooter( + onContinue = ::onContinue, + ) + } + ) +} + +@Composable +private fun FullscreenAnnouncementHeader( + announcement: Announcement.Fullscreen, + modifier: Modifier = Modifier, +) { + IconTitleSubtitleMolecule( + modifier = modifier.padding(top = 16.dp, bottom = 16.dp), + title = announcement.title(), + showBetaLabel = true, + subTitle = announcement.subtitle(), + iconStyle = BigIcon.Style.Default( + vectorIcon = announcement.icon(), + usePrimaryTint = true, + ), + ) +} + +@Composable +private fun FullscreenAnnouncementContent( + announcement: Announcement.Fullscreen, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.fillMaxSize(), + ) { + InfoListOrganism( + modifier = Modifier.fillMaxWidth(), + items = announcement.items(), + textStyle = ElementTheme.typography.fontBodyLgMedium, + iconTint = ElementTheme.colors.iconSecondary, + iconSize = 24.dp + ) + announcement.notice()?.let { notice -> + Text( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + text = notice, + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textSecondary, + textAlign = TextAlign.Center, + ) + } + } +} + +@Composable +private fun FullscreenAnnouncementFooter( + onContinue: () -> Unit, +) { + ButtonColumnMolecule( + modifier = Modifier.padding(bottom = 8.dp) + ) { + Button( + text = stringResource(id = CommonStrings.action_continue), + onClick = onContinue, + modifier = Modifier.fillMaxWidth(), + ) + } +} + +@Composable +private fun Announcement.Fullscreen.title() = when (this) { + Announcement.Fullscreen.Space -> "Introducing Spaces" +} + +@Composable +private fun Announcement.Fullscreen.subtitle() = when (this) { + Announcement.Fullscreen.Space -> "Welcome to the beta version of Spaces! With this first version you can:" +} + +@Composable +private fun Announcement.Fullscreen.icon() = when (this) { + Announcement.Fullscreen.Space -> CompoundIcons.SpaceSolid() +} + +@Composable +private fun Announcement.Fullscreen.items(): ImmutableList = when (this) { + Announcement.Fullscreen.Space -> persistentListOf( + InfoListItem( + message = "View spaces you\'ve created or joined", + iconVector = CompoundIcons.VisibilityOn(), + ), + InfoListItem( + message = "Accept or decline invites to spaces", + iconVector = CompoundIcons.Email(), + ), + InfoListItem( + message = "Discover any rooms you can join in your spaces", + iconVector = CompoundIcons.Search(), + ), + InfoListItem( + message = "Join public spaces", + iconVector = CompoundIcons.Explore(), + ), + InfoListItem( + message = "Leave any spaces you’ve joined", + iconVector = CompoundIcons.Leave(), + ), + ) +} + +@Composable +private fun Announcement.Fullscreen.notice(): String? = when (this) { + Announcement.Fullscreen.Space -> "Filtering, creating and managing spaces is coming soon." +} + +@PreviewsDayNight +@Composable +internal fun FullscreenAnnouncementViewPreview(@PreviewParameter(AnnouncementStateProvider::class) state: AnnouncementState) = ElementPreview { + FullscreenAnnouncementView( + state = state, + ) +} diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementEvents.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementEvents.kt deleted file mode 100644 index 3b968d09a6..0000000000 --- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementEvents.kt +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * 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.announcement.impl.spaces - -sealed interface SpaceAnnouncementEvents { - data object Continue : SpaceAnnouncementEvents -} diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenter.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenter.kt deleted file mode 100644 index 7c4bc7b5eb..0000000000 --- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenter.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * 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.announcement.impl.spaces - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope -import dev.zacsweers.metro.Inject -import io.element.android.features.announcement.api.Announcement -import io.element.android.features.announcement.impl.store.AnnouncementStatus -import io.element.android.features.announcement.impl.store.AnnouncementStore -import io.element.android.libraries.architecture.Presenter -import kotlinx.coroutines.launch - -@Inject -class SpaceAnnouncementPresenter( - private val announcementStore: AnnouncementStore, -) : Presenter { - @Composable - override fun present(): SpaceAnnouncementState { - val localCoroutineScope = rememberCoroutineScope() - - fun handleEvent(event: SpaceAnnouncementEvents) { - when (event) { - SpaceAnnouncementEvents.Continue -> localCoroutineScope.launch { - announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Shown) - } - } - } - - return SpaceAnnouncementState( - eventSink = ::handleEvent, - ) - } -} diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementState.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementState.kt deleted file mode 100644 index 9407fad872..0000000000 --- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementState.kt +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * 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.announcement.impl.spaces - -data class SpaceAnnouncementState( - val eventSink: (SpaceAnnouncementEvents) -> Unit -) diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementStateProvider.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementStateProvider.kt deleted file mode 100644 index 27f48cc7ed..0000000000 --- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementStateProvider.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * 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.announcement.impl.spaces - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider - -open class SpaceAnnouncementStateProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - aSpaceAnnouncementState(), - ) -} - -fun aSpaceAnnouncementState( - eventSink: (SpaceAnnouncementEvents) -> Unit = {}, -) = SpaceAnnouncementState( - eventSink = eventSink, -) diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementView.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementView.kt deleted file mode 100644 index 3fe6ec4456..0000000000 --- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementView.kt +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * 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.announcement.impl.spaces - -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp -import io.element.android.compound.theme.ElementTheme -import io.element.android.compound.tokens.generated.CompoundIcons -import io.element.android.features.announcement.impl.R -import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule -import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule -import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem -import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism -import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage -import io.element.android.libraries.designsystem.components.BigIcon -import io.element.android.libraries.designsystem.preview.ElementPreview -import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.theme.components.Button -import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.ui.strings.CommonStrings -import kotlinx.collections.immutable.persistentListOf - -/** - * Ref: https://www.figma.com/design/kcnHxunG1LDWXsJhaNuiHz/ER-145--Spaces-on-Element-X?node-id=4593-40181 - */ -@Composable -fun SpaceAnnouncementView( - state: SpaceAnnouncementState, - modifier: Modifier = Modifier, -) { - val eventSink = state.eventSink - - fun onContinue() { - eventSink(SpaceAnnouncementEvents.Continue) - } - - BackHandler(onBack = ::onContinue) - HeaderFooterPage( - modifier = modifier, - isScrollable = true, - contentPadding = PaddingValues(top = 24.dp, start = 16.dp, end = 16.dp, bottom = 24.dp), - header = { - SpaceAnnouncementHeader() - }, - content = { - SpaceAnnouncementContent( - modifier = Modifier.padding(horizontal = 8.dp), - ) - }, - footer = { - SpaceAnnouncementFooter( - onContinue = ::onContinue, - ) - } - ) -} - -@Composable -private fun SpaceAnnouncementHeader( - modifier: Modifier = Modifier, -) { - IconTitleSubtitleMolecule( - modifier = modifier.padding(top = 16.dp, bottom = 16.dp), - title = stringResource(id = R.string.screen_space_announcement_title), - showBetaLabel = true, - subTitle = stringResource(id = R.string.screen_space_announcement_subtitle), - iconStyle = BigIcon.Style.Default( - vectorIcon = CompoundIcons.SpaceSolid(), - usePrimaryTint = true, - ), - ) -} - -@Composable -private fun SpaceAnnouncementContent( - modifier: Modifier = Modifier, -) { - Column( - modifier = modifier.fillMaxSize(), - ) { - InfoListOrganism( - modifier = Modifier.fillMaxWidth(), - items = persistentListOf( - InfoListItem( - message = stringResource(id = R.string.screen_space_announcement_item1), - iconVector = CompoundIcons.VisibilityOn(), - ), - InfoListItem( - message = stringResource(id = R.string.screen_space_announcement_item2), - iconVector = CompoundIcons.Email(), - ), - InfoListItem( - message = stringResource(id = R.string.screen_space_announcement_item3), - iconVector = CompoundIcons.Search(), - ), - InfoListItem( - message = stringResource(id = R.string.screen_space_announcement_item4), - iconVector = CompoundIcons.Explore(), - ), - InfoListItem( - message = stringResource(id = R.string.screen_space_announcement_item5), - iconVector = CompoundIcons.Leave(), - ), - ), - textStyle = ElementTheme.typography.fontBodyLgMedium, - iconTint = ElementTheme.colors.iconSecondary, - iconSize = 24.dp - ) - Text( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp), - text = stringResource(id = R.string.screen_space_announcement_notice), - style = ElementTheme.typography.fontBodyMdRegular, - color = ElementTheme.colors.textSecondary, - textAlign = TextAlign.Center, - ) - } -} - -@Composable -private fun SpaceAnnouncementFooter( - onContinue: () -> Unit, -) { - ButtonColumnMolecule( - modifier = Modifier.padding(bottom = 8.dp) - ) { - Button( - text = stringResource(id = CommonStrings.action_continue), - onClick = onContinue, - modifier = Modifier.fillMaxWidth(), - ) - } -} - -@PreviewsDayNight -@Composable -internal fun SpaceAnnouncementViewPreview(@PreviewParameter(SpaceAnnouncementStateProvider::class) state: SpaceAnnouncementState) = ElementPreview { - SpaceAnnouncementView( - state = state, - ) -} diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/DefaultAnnouncementStore.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/DefaultAnnouncementStore.kt index ad166e4ef5..5a6c135247 100644 --- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/DefaultAnnouncementStore.kt +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/DefaultAnnouncementStore.kt @@ -35,9 +35,10 @@ class DefaultAnnouncementStore( override fun announcementStatusFlow(announcement: Announcement): Flow { val key = announcement.toKey() + // Announcement.Fullscreen.Space is disabled, consider it's shown // For NewNotificationSound, a migration will set it to Show on application upgrade (see AppMigration08) val defaultStatus = when (announcement) { - Announcement.Space -> AnnouncementStatus.NeverShown + Announcement.Fullscreen.Space -> AnnouncementStatus.Shown Announcement.NewNotificationSound -> AnnouncementStatus.Shown } return store.data.map { prefs -> @@ -52,6 +53,6 @@ class DefaultAnnouncementStore( } private fun Announcement.toKey() = when (this) { - Announcement.Space -> spaceAnnouncementKey + Announcement.Fullscreen.Space -> spaceAnnouncementKey Announcement.NewNotificationSound -> newNotificationSoundKey } diff --git a/features/announcement/impl/src/main/res/values-ja/translations.xml b/features/announcement/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..47273cc12d --- /dev/null +++ b/features/announcement/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,11 @@ + + + "作成または参加したスペースを表示できます" + "スペースへの招待を受諾または拒否できます" + "スペース内の参加可能なルームを検索できます" + "公開スペースに参加できます" + "参加したスペースを退出できます" + "スペースの作成や管理, フィルター検索は近日実装予定です。" + "ベータ版のスペースにようこそ。この最新のバージョンでは:" + "スペースの紹介" + diff --git a/features/announcement/impl/src/main/res/values-zh/translations.xml b/features/announcement/impl/src/main/res/values-zh/translations.xml index e01e63b2ae..70d86638ea 100644 --- a/features/announcement/impl/src/main/res/values-zh/translations.xml +++ b/features/announcement/impl/src/main/res/values-zh/translations.xml @@ -6,6 +6,6 @@ "加入公共空间" "离开你加入的所有空间" "筛选、创建及管理空间功能即将上线。" - "欢迎使用 Spaces 测试版!使用首个版本,您可以:" - "Spaces 简介" + "欢迎使用空间测试版!使用首个版本,您可以:" + "空间简介" diff --git a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenterTest.kt b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenterTest.kt index 18deb8b2fd..c37f49fad8 100644 --- a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenterTest.kt +++ b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenterTest.kt @@ -14,6 +14,7 @@ import io.element.android.features.announcement.impl.store.AnnouncementStatus import io.element.android.features.announcement.impl.store.AnnouncementStore import io.element.android.features.announcement.impl.store.InMemoryAnnouncementStore import io.element.android.tests.testutils.test +import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import org.junit.Test @@ -23,25 +24,47 @@ class AnnouncementPresenterTest { val presenter = createAnnouncementPresenter() presenter.test { val state = awaitItem() - assertThat(state.showSpaceAnnouncement).isFalse() + assertThat(state.announcement).isNull() } } @Test - fun `present - showSpaceAnnouncement value depends on the value in the store`() = runTest { + fun `present - showFullscreen value depends on the value in the store`() = runTest { val store = InMemoryAnnouncementStore() val presenter = createAnnouncementPresenter( announcementStore = store, ) presenter.test { val state = awaitItem() - assertThat(state.showSpaceAnnouncement).isFalse() - store.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Show) + assertThat(state.announcement).isNull() + store.setAnnouncementStatus(Announcement.Fullscreen.Space, AnnouncementStatus.Show) val updatedState = awaitItem() - assertThat(updatedState.showSpaceAnnouncement).isTrue() - store.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Shown) + assertThat(updatedState.announcement).isEqualTo(Announcement.Fullscreen.Space) + store.setAnnouncementStatus(Announcement.Fullscreen.Space, AnnouncementStatus.Shown) val finalState = awaitItem() - assertThat(finalState.showSpaceAnnouncement).isFalse() + assertThat(finalState.announcement).isNull() + } + } + + @Test + fun `present - continue event will mark the announcement as Shown`() = runTest { + val store = InMemoryAnnouncementStore() + val presenter = createAnnouncementPresenter( + announcementStore = store, + ) + presenter.test { + val state = awaitItem() + assertThat(state.announcement).isNull() + store.setAnnouncementStatus(Announcement.Fullscreen.Space, AnnouncementStatus.Show) + val statusShow = store.announcementStatusFlow(Announcement.Fullscreen.Space).first() + assertThat(statusShow).isEqualTo(AnnouncementStatus.Show) + val updatedState = awaitItem() + assertThat(updatedState.announcement).isEqualTo(Announcement.Fullscreen.Space) + updatedState.eventSink(AnnouncementEvent.Continue(Announcement.Fullscreen.Space)) + val statusShown = store.announcementStatusFlow(Announcement.Fullscreen.Space).first() + assertThat(statusShown).isEqualTo(AnnouncementStatus.Shown) + val finalState = awaitItem() + assertThat(finalState.announcement).isNull() } } } diff --git a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementServiceTest.kt b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementServiceTest.kt index e16619129c..c72d147c1d 100644 --- a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementServiceTest.kt +++ b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementServiceTest.kt @@ -11,31 +11,28 @@ package io.element.android.features.announcement.impl import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.announcement.api.Announcement -import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementState -import io.element.android.features.announcement.impl.spaces.aSpaceAnnouncementState import io.element.android.features.announcement.impl.store.AnnouncementStatus import io.element.android.features.announcement.impl.store.AnnouncementStore import io.element.android.features.announcement.impl.store.InMemoryAnnouncementStore -import io.element.android.libraries.architecture.Presenter import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import org.junit.Test class DefaultAnnouncementServiceTest { @Test - fun `when showing Space announcement, space announcement is set to show only if it was never shown`() = runTest { + fun `when showing Fullscreen announcement, Fullscreen announcement is set to show only if it was never shown`() = runTest { val announcementStore = InMemoryAnnouncementStore() val sut = createDefaultAnnouncementService( announcementStore = announcementStore, ) - assertThat(announcementStore.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.NeverShown) - sut.showAnnouncement(Announcement.Space) - assertThat(announcementStore.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.Show) + assertThat(announcementStore.announcementStatusFlow(Announcement.Fullscreen.Space).first()).isEqualTo(AnnouncementStatus.NeverShown) + sut.showAnnouncement(Announcement.Fullscreen.Space) + assertThat(announcementStore.announcementStatusFlow(Announcement.Fullscreen.Space).first()).isEqualTo(AnnouncementStatus.Show) // Simulate user close the announcement - sut.onAnnouncementDismissed(Announcement.Space) + sut.onAnnouncementDismissed(Announcement.Fullscreen.Space) // Entering again the space tab should not change the value - sut.showAnnouncement(Announcement.Space) - assertThat(announcementStore.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.Shown) + sut.showAnnouncement(Announcement.Fullscreen.Space) + assertThat(announcementStore.announcementStatusFlow(Announcement.Fullscreen.Space).first()).isEqualTo(AnnouncementStatus.Shown) } @Test @@ -62,11 +59,7 @@ class DefaultAnnouncementServiceTest { ) sut.announcementsToShowFlow().test { assertThat(awaitItem()).isEmpty() - announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Show) - assertThat(awaitItem()).containsExactly(Announcement.Space) announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStatus.Show) - assertThat(awaitItem()).containsExactly(Announcement.Space, Announcement.NewNotificationSound) - announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Shown) assertThat(awaitItem()).containsExactly(Announcement.NewNotificationSound) announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStatus.Shown) assertThat(awaitItem()).isEmpty() @@ -75,11 +68,9 @@ class DefaultAnnouncementServiceTest { private fun createDefaultAnnouncementService( announcementStore: AnnouncementStore = InMemoryAnnouncementStore(), - announcementPresenter: Presenter = Presenter { anAnnouncementState() }, - spaceAnnouncementPresenter: Presenter = Presenter { aSpaceAnnouncementState() }, + announcementPresenter: AnnouncementPresenter = AnnouncementPresenter(announcementStore), ) = DefaultAnnouncementService( announcementStore = announcementStore, announcementPresenter = announcementPresenter, - spaceAnnouncementPresenter = spaceAnnouncementPresenter, ) } diff --git a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementViewTest.kt b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/fullscreen/FullscreenAnnouncementViewTest.kt similarity index 51% rename from features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementViewTest.kt rename to features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/fullscreen/FullscreenAnnouncementViewTest.kt index ad3d83f1b5..b69037e61a 100644 --- a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementViewTest.kt +++ b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/fullscreen/FullscreenAnnouncementViewTest.kt @@ -6,12 +6,16 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.announcement.impl.spaces +package io.element.android.features.announcement.impl.fullscreen import androidx.activity.ComponentActivity import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.announcement.api.Announcement +import io.element.android.features.announcement.impl.AnnouncementEvent +import io.element.android.features.announcement.impl.AnnouncementState +import io.element.android.features.announcement.impl.anAnnouncementState import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn @@ -22,39 +26,41 @@ import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class SpaceAnnouncementViewTest { +class FullscreenAnnouncementViewTest { @get:Rule val rule = createAndroidComposeRule() @Test - fun `clicking on back sends a SpaceAnnouncementEvents`() { - val eventsRecorder = EventsRecorder() - rule.setSpaceAnnouncementView( - aSpaceAnnouncementState( + fun `clicking on back sends a AnnouncementEvent`() { + val eventsRecorder = EventsRecorder() + rule.setFullscreenAnnouncementView( + anAnnouncementState( + announcement = Announcement.Fullscreen.Space, eventSink = eventsRecorder, ), ) rule.pressBackKey() - eventsRecorder.assertSingle(SpaceAnnouncementEvents.Continue) + eventsRecorder.assertSingle(AnnouncementEvent.Continue(Announcement.Fullscreen.Space)) } @Test - fun `clicking on Continue sends a SpaceAnnouncementEvents`() { - val eventsRecorder = EventsRecorder() - rule.setSpaceAnnouncementView( - aSpaceAnnouncementState( + fun `clicking on Continue sends a AnnouncementEvent`() { + val eventsRecorder = EventsRecorder() + rule.setFullscreenAnnouncementView( + anAnnouncementState( + announcement = Announcement.Fullscreen.Space, eventSink = eventsRecorder, ), ) rule.clickOn(CommonStrings.action_continue) - eventsRecorder.assertSingle(SpaceAnnouncementEvents.Continue) + eventsRecorder.assertSingle(AnnouncementEvent.Continue(Announcement.Fullscreen.Space)) } } -private fun AndroidComposeTestRule.setSpaceAnnouncementView( - state: SpaceAnnouncementState, +private fun AndroidComposeTestRule.setFullscreenAnnouncementView( + state: AnnouncementState, ) { setContent { - SpaceAnnouncementView( + FullscreenAnnouncementView( state = state, ) } diff --git a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenterTest.kt b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenterTest.kt deleted file mode 100644 index 672f677407..0000000000 --- a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenterTest.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * 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.announcement.impl.spaces - -import com.google.common.truth.Truth.assertThat -import io.element.android.features.announcement.api.Announcement -import io.element.android.features.announcement.impl.store.AnnouncementStatus -import io.element.android.features.announcement.impl.store.AnnouncementStore -import io.element.android.features.announcement.impl.store.InMemoryAnnouncementStore -import io.element.android.tests.testutils.test -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.test.runTest -import org.junit.Test - -class SpaceAnnouncementPresenterTest { - @Test - fun `present - when user continues, the store is updated`() = runTest { - val store = InMemoryAnnouncementStore() - val presenter = createSpaceAnnouncementPresenter( - announcementStore = store, - ) - presenter.test { - assertThat(store.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.NeverShown) - val state = awaitItem() - state.eventSink(SpaceAnnouncementEvents.Continue) - assertThat(store.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.Shown) - } - } -} - -private fun createSpaceAnnouncementPresenter( - announcementStore: AnnouncementStore = InMemoryAnnouncementStore(), -) = SpaceAnnouncementPresenter( - announcementStore = announcementStore, -) diff --git a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/store/InMemoryAnnouncementStore.kt b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/store/InMemoryAnnouncementStore.kt index ab3e85124f..ed6dfec850 100644 --- a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/store/InMemoryAnnouncementStore.kt +++ b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/store/InMemoryAnnouncementStore.kt @@ -14,10 +14,10 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow class InMemoryAnnouncementStore( - initialSpaceAnnouncementStatus: AnnouncementStatus = AnnouncementStatus.NeverShown, + initialFullscreenAnnouncementStatus: AnnouncementStatus = AnnouncementStatus.NeverShown, initialNewNotificationSoundAnnouncementStatus: AnnouncementStatus = AnnouncementStatus.NeverShown, ) : AnnouncementStore { - private val spaceAnnouncement = MutableStateFlow(initialSpaceAnnouncementStatus) + private val fullScreenAnnouncement = MutableStateFlow(initialFullscreenAnnouncementStatus) private val newNotificationSoundAnnouncement = MutableStateFlow(initialNewNotificationSoundAnnouncementStatus) override suspend fun setAnnouncementStatus(announcement: Announcement, status: AnnouncementStatus) { @@ -29,12 +29,12 @@ class InMemoryAnnouncementStore( } override suspend fun reset() { - spaceAnnouncement.value = AnnouncementStatus.NeverShown + fullScreenAnnouncement.value = AnnouncementStatus.NeverShown newNotificationSoundAnnouncement.value = AnnouncementStatus.NeverShown } private fun Announcement.toMutableStateFlow() = when (this) { - Announcement.Space -> spaceAnnouncement + is Announcement.Fullscreen -> fullScreenAnnouncement Announcement.NewNotificationSound -> newNotificationSoundAnnouncement } } diff --git a/features/call/impl/build.gradle.kts b/features/call/impl/build.gradle.kts index e77c09e19a..2843e47870 100644 --- a/features/call/impl/build.gradle.kts +++ b/features/call/impl/build.gradle.kts @@ -73,7 +73,7 @@ dependencies { implementation(projects.libraries.core) implementation(projects.libraries.designsystem) implementation(projects.libraries.featureflag.api) - implementation(projects.libraries.matrix.impl) + implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixmedia.api) implementation(projects.libraries.network) implementation(projects.libraries.preferences.api) diff --git a/features/call/impl/src/main/res/values-ja/translations.xml b/features/call/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..18ee195d0f --- /dev/null +++ b/features/call/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,8 @@ + + + "通話中" + "タップして通話に戻る" + "☎️ 通話中" + "Element CallはこのAndroidバージョンにおいて、Bluetoothオーディオデバイスの使用をサポートしていません。別のオーディオデバイスを選択してください。" + "Element Call の着信" + diff --git a/features/call/impl/src/main/res/values-vi/translations.xml b/features/call/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..c88cf651e6 --- /dev/null +++ b/features/call/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,8 @@ + + + "Cuộc gọi đang diễn ra" + "Nhấn để quay lại cuộc gọi." + "☎️ Cuộc gọi đang diễn ra" + "Ứng dụng Element Call không hỗ trợ sử dụng thiết bị âm thanh Bluetooth trên phiên bản Android này. Vui lòng chọn thiết bị âm thanh khác." + "Cuộc gọi Element đến" + diff --git a/features/call/test/src/main/kotlin/io/element/android/features/call/test/CallNotificationData.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/notifications/CallNotificationData.kt similarity index 93% rename from features/call/test/src/main/kotlin/io/element/android/features/call/test/CallNotificationData.kt rename to features/call/impl/src/test/kotlin/io/element/android/features/call/impl/notifications/CallNotificationData.kt index 387a77ef8b..1906aba551 100644 --- a/features/call/test/src/main/kotlin/io/element/android/features/call/test/CallNotificationData.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/notifications/CallNotificationData.kt @@ -6,9 +6,8 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.call.test +package io.element.android.features.call.impl.notifications -import io.element.android.features.call.impl.notifications.CallNotificationData 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 diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt index 5650eaa47f..f9f6206ec7 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt @@ -15,11 +15,11 @@ import androidx.test.platform.app.InstrumentationRegistry import com.google.common.truth.Truth.assertThat import io.element.android.features.call.api.CallType import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator +import io.element.android.features.call.impl.notifications.aCallNotificationData import io.element.android.features.call.impl.utils.ActiveCall import io.element.android.features.call.impl.utils.CallState import io.element.android.features.call.impl.utils.DefaultActiveCallManager import io.element.android.features.call.impl.utils.DefaultCurrentCallService -import io.element.android.features.call.test.aCallNotificationData 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 diff --git a/features/call/test/build.gradle.kts b/features/call/test/build.gradle.kts index 76fbf9915e..f06c5ed16e 100644 --- a/features/call/test/build.gradle.kts +++ b/features/call/test/build.gradle.kts @@ -20,7 +20,6 @@ dependencies { implementation(projects.libraries.core) api(projects.features.call.api) - implementation(projects.features.call.impl) implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.test) implementation(projects.tests.testutils) diff --git a/features/createroom/impl/build.gradle.kts b/features/createroom/impl/build.gradle.kts index 7201f7dc9c..13a195e940 100644 --- a/features/createroom/impl/build.gradle.kts +++ b/features/createroom/impl/build.gradle.kts @@ -39,7 +39,6 @@ dependencies { implementation(projects.libraries.mediaupload.api) implementation(projects.libraries.permissions.api) implementation(projects.libraries.previewutils) - implementation(projects.libraries.usersearch.impl) implementation(projects.services.analytics.api) implementation(libs.coil.compose) implementation(projects.libraries.featureflag.api) @@ -52,7 +51,6 @@ dependencies { testImplementation(projects.libraries.mediapickers.test) testImplementation(projects.libraries.mediaupload.test) testImplementation(projects.libraries.permissions.test) - testImplementation(projects.libraries.usersearch.test) testImplementation(projects.features.startchat.test) testImplementation(projects.libraries.featureflag.test) } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index 38d6132e14..0d74ca05bf 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -84,7 +84,6 @@ class ConfigureRoomPresenter( @Composable override fun present(): ConfigureRoomState { - val canAddRoomToSpace by featureFlagService.isFeatureEnabledFlow(FeatureFlags.CreateSpaces).collectAsState(false) val cameraPermissionState = cameraPermissionPresenter.present() val createRoomConfig by dataStore.getCreateRoomConfigFlow().collectAsState() val homeserverName = remember { matrixClient.userIdServerName() } @@ -113,12 +112,8 @@ class ConfigureRoomPresenter( } var spaces by remember { mutableStateOf>(persistentListOf()) } - LaunchedEffect(canAddRoomToSpace) { - spaces = if (canAddRoomToSpace) { - matrixClient.spaceService.editableSpaces().getOrElse { emptyList() }.toImmutableList() - } else { - persistentListOf() - } + LaunchedEffect(Unit) { + spaces = matrixClient.spaceService.editableSpaces().getOrElse { emptyList() }.toImmutableList() val parentSpace = spaces.find { it.roomId == initialParentSpaceId } parentSpace?.let { dataStore.setParentSpace(parentSpace = parentSpace, updateVisibility = true) diff --git a/features/createroom/impl/src/main/res/values-it/translations.xml b/features/createroom/impl/src/main/res/values-it/translations.xml index ce8bca20e0..7a936f41a8 100644 --- a/features/createroom/impl/src/main/res/values-it/translations.xml +++ b/features/createroom/impl/src/main/res/values-it/translations.xml @@ -3,14 +3,34 @@ "Nuova stanza" "Invita persone" "Si è verificato un errore durante la creazione della stanza" - "Solo le persone invitate possono accedere a questa stanza. Tutti i messaggi sono cifrati end-to-end." + "Non è stato possibile creare lo spazio a causa di un errore sconosciuto. Riprova più tardi." + "Aggiungi nome…" + "Nuova stanza" + "Nuovo spazio" + "Possono partecipare solo le persone invitate." + "Privato" "Chiunque può trovare questa stanza. Puoi modificarlo in qualsiasi momento nelle impostazioni della stanza." - "Chiunque può chiedere di entrare nella stanza, ma un amministratore o un moderatore dovrà accettare la richiesta" - "Chiedi di entrare" - "Chiunque può entrare in questa stanza" - "Affinché questa stanza sia visibile nell\'elenco delle stanze pubbliche, è necessario un indirizzo della stanza." - "Indirizzo della stanza" + "Chiunque può partecipare." + "Pubblico" + "Chiunque può chiedere di partecipare, ma un amministratore o un moderatore deve accettare la richiesta." + "Consenti di chiedere di partecipare" + "Chiunque sia membro di %1$s può partecipare, mentre tutti gli altri devono richiedere l\'accesso." + "Richiedi accesso" + "Possono partecipare solo le persone invitate." + "Privato" + "Chiunque può partecipare." + "Pubblico" + "Chiunque in %1$s può unirsi." + "Standard" + "Chi ha accesso" + "Avrai bisogno di un indirizzo per renderlo visibile nella directory pubblica." + "Indirizzo" "Visibilità della stanza" + "(nessuno spazio)" + "Non aggiungere a uno spazio" + "Nessuno spazio selezionato" + "Aggiungi allo spazio" "Argomento (facoltativo)" + "Aggiungi descrizione…" diff --git a/features/createroom/impl/src/main/res/values-ja/translations.xml b/features/createroom/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..f6f7c71390 --- /dev/null +++ b/features/createroom/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,36 @@ + + + "新しいルーム" + "ユーザーを招待" + "ルームの作成中に問題が発生しました" + "不明な問題のためスペースを作成できませんでした。再度お試しください。" + "名前を追加…" + "新しいルーム" + "新しいスペース" + "招待されたユーザーのみ参加できます。" + "非公開" + "ルームは全世界に公開されます。 +ルーム設定でいつでも変更できます。" + "誰でも参加できます。" + "公開" + "誰でも参加できますが、管理者またはモデレーターの承認が必要です。" + "参加の要求を許可" + "%1$s にいる全員が参加することができますが、事前に参加の要求をする必要があります。" + "参加を要求" + "招待されたユーザーのみが参加できます。" + "非公開" + "誰でも参加できます。" + "公開" + "%1$s にいる全員が参加することができます。" + "スタンダード" + "参加できるユーザー" + "公開ディレクトリで自分を見つけられるようにするには、アドレスが必要です。" + "アドレス" + "ルームの公開度" + "(スペースなし)" + "スペースに追加しない" + "スペースが選択されていません" + "スペースに追加" + "トピック (任意)" + "説明を追加…" + diff --git a/features/createroom/impl/src/main/res/values-vi/translations.xml b/features/createroom/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..b10d15e077 --- /dev/null +++ b/features/createroom/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,11 @@ + + + "Phòng mới" + "Mời ai đó" + "Đã xảy ra lỗi khi tạo phòng." + "Chỉ những người được mời mới có thể tham gia." + "Bất kỳ ai cũng có thể tìm thấy phòng này. +Bạn có thể thay đổi cài đặt phòng bất cứ lúc nào." + "Chủ đề (tùy chọn)" + "Thêm mô tả…" + diff --git a/features/deactivation/impl/src/main/res/values-ja/translations.xml b/features/deactivation/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..873b1308ec --- /dev/null +++ b/features/deactivation/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,14 @@ + + + "アカウントを無効化することを再度確認します。この操作は元に戻せません。" + "メッセージをすべて削除" + "注意: 新しいユーザーには断片的な会話が表示されます" + "アカウントを無効化することは %1$s であり、次の変化が生じます:" + "不可逆" + "アカウントを %1$s (再度ログイン不可, 同一のIDを再利用不可)" + "恒久的に無効化する" + "すべてのチャットルームから退出します。" + "アカウント提供元サーバーからアカウント情報を削除します。" + "あなたの会話は、既存ユーザーには引き続き表示されますが、新規ユーザーには表示されなくなります。" + "アカウントを無効化" + diff --git a/features/deactivation/impl/src/main/res/values-vi/translations.xml b/features/deactivation/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..22bc0a6d6e --- /dev/null +++ b/features/deactivation/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,7 @@ + + + "Xóa tất cả tin nhắn của tôi" + "Cảnh báo: Người dùng sau này có thể thấy các cuộc trò chuyện chưa hoàn chỉnh." + "Tin nhắn của bạn vẫn sẽ hiển thị cho người dùng đã đăng ký nhưng sẽ không hiển thị cho người dùng mới hoặc chưa đăng ký nếu bạn chọn xóa chúng." + "Vô hiệu hóa tài khoản" + diff --git a/features/ftue/impl/src/main/res/values-cs/translations.xml b/features/ftue/impl/src/main/res/values-cs/translations.xml index 30d6998ac6..0937e7ed14 100644 --- a/features/ftue/impl/src/main/res/values-cs/translations.xml +++ b/features/ftue/impl/src/main/res/values-cs/translations.xml @@ -2,8 +2,8 @@ "Nemůžete potvrdit?" "Vytvoření nového klíče pro obnovení" - "Ověřte toto zařízení a nastavte zabezpečené zasílání zpráv." - "Potvrďte, že jste to vy" + "Vyberte způsob ověření pro nastavení zabezpečeného zasílání zpráv." + "Potvrďte svou digitální identitu" "Použít jiné zařízení" "Použít klíč pro obnovení" "Nyní můžete bezpečně číst nebo odesílat zprávy, a kdokoli, s kým chatujete, může tomuto zařízení důvěřovat." diff --git a/features/ftue/impl/src/main/res/values-it/translations.xml b/features/ftue/impl/src/main/res/values-it/translations.xml index 25ba544d00..8aa9132732 100644 --- a/features/ftue/impl/src/main/res/values-it/translations.xml +++ b/features/ftue/impl/src/main/res/values-it/translations.xml @@ -2,8 +2,8 @@ "Non puoi confermare?" "Crea una nuova chiave di recupero" - "Verifica questo dispositivo per segnare i tuoi messaggi come sicuri." - "Conferma la tua identità" + "Scegli come effettuare la verifica per configurare la messaggistica sicura." + "Conferma la tua identità digitale" "Usa un altro dispositivo" "Usa la chiave di recupero" "Ora puoi leggere o inviare messaggi in tutta sicurezza e anche chi chatta con te può fidarsi di questo dispositivo." diff --git a/features/ftue/impl/src/main/res/values-ja/translations.xml b/features/ftue/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..68b69079ef --- /dev/null +++ b/features/ftue/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,15 @@ + + + "認証できませんか?" + "回復鍵を新規作成します" + "安全なメッセージを設定するための検証方法を選択してください。" + "デジタルIDの認証" + "他の端末を使用" + "回復鍵を使用" + "メッセージのやり取りを安全に行えるようになりました。他のユーザーはこの端末を信頼できます。" + "検証済みの端末" + "他の端末を使用" + "一方の端末を待機中…" + "設定は後で変更することができます。" + "メッセージを見逃さないため通知を許可" + diff --git a/features/ftue/impl/src/main/res/values-ru/translations.xml b/features/ftue/impl/src/main/res/values-ru/translations.xml index b2f0925813..34ac77ae3f 100644 --- a/features/ftue/impl/src/main/res/values-ru/translations.xml +++ b/features/ftue/impl/src/main/res/values-ru/translations.xml @@ -2,7 +2,7 @@ "Не можете подтвердить?" "Создайте новый ключ восстановления" - "Подтвердите это устройство, чтобы настроить безопасный обмен сообщениями." + "Выберите способ подтверждения для настройки защищенного обмена сообщениями." "Подтвердите личность" "Использовать другое устройство" "Использовать ключ восстановления" diff --git a/features/ftue/impl/src/main/res/values-vi/translations.xml b/features/ftue/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..c70d9be0fb --- /dev/null +++ b/features/ftue/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,10 @@ + + + "Chọn phương thức xác minh để bật nhắn tin bảo mật." + "Xác nhận danh tính kỹ thuật số của bạn" + "Giờ đây bạn có thể đọc và gửi tin nhắn một cách an toàn, và những người bạn trò chuyện cũng có thể tin tưởng thiết bị này." + "Thiết bị được xác thực" + "Đang chờ trên thiết bị khác…" + "Bạn có thể thay đổi cài đặt sau." + "Cho phép thông báo để không bỏ lỡ bất kỳ tin nhắn nào" + diff --git a/features/ftue/impl/src/main/res/values-zh/translations.xml b/features/ftue/impl/src/main/res/values-zh/translations.xml index 669a316243..68a48831e0 100644 --- a/features/ftue/impl/src/main/res/values-zh/translations.xml +++ b/features/ftue/impl/src/main/res/values-zh/translations.xml @@ -2,8 +2,8 @@ "无法确认?" "创建新的恢复密钥" - "验证此设备以开始安全地收发消息。" - "确认这是你" + "选择验证方式以设置安全的消息传输。" + "确认您的数字身份" "使用其他设备" "使用恢复密钥" "现在,您可以安全地阅读或发送消息,与您聊天的人也会信任此设备。" diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt index 6878f4d53c..1e49b1aa70 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt @@ -19,8 +19,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue 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.home.impl.roomlist.RoomListState import io.element.android.features.home.impl.spaces.HomeSpacesState import io.element.android.features.logout.api.direct.DirectLogoutState @@ -47,7 +45,6 @@ class HomePresenter( private val logoutPresenter: Presenter, private val rageshakeFeatureAvailability: RageshakeFeatureAvailability, private val sessionStore: SessionStore, - private val announcementService: AnnouncementService, ) : Presenter { private val currentUserWithNeighborsBuilder = CurrentUserWithNeighborsBuilder() @@ -82,10 +79,7 @@ class HomePresenter( fun handleEvent(event: HomeEvent) { when (event) { - is HomeEvent.SelectHomeNavigationBarItem -> coroutineState.launch { - if (event.item == HomeNavigationBarItem.Spaces) { - announcementService.showAnnouncement(Announcement.Space) - } + is HomeEvent.SelectHomeNavigationBarItem -> { currentHomeNavigationBarItemOrdinal = event.item.ordinal } is HomeEvent.SwitchToAccount -> coroutineState.launch { @@ -94,12 +88,6 @@ class HomePresenter( } } - LaunchedEffect(homeSpacesState.canCreateSpaces, homeSpacesState.spaceRooms.isEmpty()) { - // If the flag to create spaces is disabled and the last space is left, ensure that the Chat view is rendered. - if (!homeSpacesState.canCreateSpaces && homeSpacesState.spaceRooms.isEmpty()) { - currentHomeNavigationBarItemOrdinal = HomeNavigationBarItem.Chats.ordinal - } - } val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState() return HomeState( currentUserAndNeighbors = currentUserAndNeighbors, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt index 934dac831e..ae59ef8eb9 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt @@ -34,5 +34,4 @@ data class HomeState( ) { val isBackHandlerEnabled = currentHomeNavigationBarItem != HomeNavigationBarItem.Chats || roomListState.spaceFiltersState is SpaceFiltersState.Selected val displayRoomListFilters = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats && roomListState.displayFilters - val showNavigationBar = homeSpacesState.canCreateSpaces || homeSpacesState.spaceRooms.isNotEmpty() } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt index ddf8b1c499..6956ba6ba5 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt @@ -199,50 +199,41 @@ private fun HomeScaffold( ) }, floatingActionButton = { - if (state.showNavigationBar) { - val coroutineScope = rememberCoroutineScope() - HomeBottomBar( - currentHomeNavigationBarItem = state.currentHomeNavigationBarItem, - onItemClick = { item -> - // scroll to top if selecting the same item - if (item == state.currentHomeNavigationBarItem) { - val lazyListStateTarget = when (item) { - HomeNavigationBarItem.Chats -> roomsLazyListState - HomeNavigationBarItem.Spaces -> spacesLazyListState - } - coroutineScope.launch { - if (lazyListStateTarget.firstVisibleItemIndex > 10) { - lazyListStateTarget.scrollToItem(10) - } - // Also reset the scrollBehavior height offset as it's not triggered by programmatic scrolls - scrollBehavior.state.heightOffset = 0f - lazyListStateTarget.animateScrollToItem(0) - } - } else { - state.eventSink(HomeEvent.SelectHomeNavigationBarItem(item)) + val coroutineScope = rememberCoroutineScope() + HomeBottomBar( + currentHomeNavigationBarItem = state.currentHomeNavigationBarItem, + onItemClick = { item -> + // scroll to top if selecting the same item + if (item == state.currentHomeNavigationBarItem) { + val lazyListStateTarget = when (item) { + HomeNavigationBarItem.Chats -> roomsLazyListState + HomeNavigationBarItem.Spaces -> spacesLazyListState } - }, - floatingActionButton = when (state.currentHomeNavigationBarItem) { + coroutineScope.launch { + if (lazyListStateTarget.firstVisibleItemIndex > 10) { + lazyListStateTarget.scrollToItem(10) + } + // Also reset the scrollBehavior height offset as it's not triggered by programmatic scrolls + scrollBehavior.state.heightOffset = 0f + lazyListStateTarget.animateScrollToItem(0) + } + } else { + state.eventSink(HomeEvent.SelectHomeNavigationBarItem(item)) + } + }, + floatingActionButton = { + when (state.currentHomeNavigationBarItem) { HomeNavigationBarItem.Chats -> { - { - HomeFloatingActionButton(onStartChatClick, CommonStrings.action_create_room) - } + HomeFloatingActionButton(onStartChatClick, CommonStrings.action_create_room) } - HomeNavigationBarItem.Spaces -> if (state.homeSpacesState.canCreateSpaces) { - { - HomeFloatingActionButton(onCreateSpaceClick, CommonStrings.action_create_space) - } - } else { - // No FAB for spaces if we cannot create spaces - null + HomeNavigationBarItem.Spaces -> { + HomeFloatingActionButton(onCreateSpaceClick, CommonStrings.action_create_space) } - }, - ) - } else { - HomeFloatingActionButton(onStartChatClick, CommonStrings.action_create_room) - } + } + }, + ) }, - floatingActionButtonPosition = if (state.showNavigationBar) FabPosition.Center else FabPosition.End, + floatingActionButtonPosition = FabPosition.Center, content = { padding -> val contentPadding = PaddingValues( bottom = 96.dp, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt index e2598a9e1c..f541417104 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt @@ -60,6 +60,7 @@ import io.element.android.libraries.designsystem.theme.roomListRoomMessage import io.element.android.libraries.designsystem.theme.roomListRoomMessageDate import io.element.android.libraries.designsystem.theme.roomListRoomName import io.element.android.libraries.designsystem.theme.unreadIndicator +import io.element.android.libraries.matrix.api.notification.CallIntent import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.ui.components.InviteSenderView import io.element.android.libraries.matrix.ui.model.InviteSender @@ -349,6 +350,7 @@ private fun MessagePreviewAndIndicatorRow( if (room.hasRoomCall) { OnGoingCallIcon( color = tint, + isAudio = room.activeCallIntent == CallIntent.AUDIO ) } if (room.userDefinedNotificationMode == RoomNotificationMode.MUTE) { @@ -398,10 +400,11 @@ private fun InviteNameAndIndicatorRow( @Composable private fun OnGoingCallIcon( color: Color, + isAudio: Boolean ) { Icon( modifier = Modifier.size(16.dp), - imageVector = CompoundIcons.VideoCallSolid(), + imageVector = if (isAudio) CompoundIcons.VoiceCallSolid() else CompoundIcons.VideoCallSolid(), contentDescription = stringResource(CommonStrings.a11y_notifications_ongoing_call), tint = color, ) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListRoomSummaryFactory.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListRoomSummaryFactory.kt index d723d1a424..26054d7e56 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListRoomSummaryFactory.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListRoomSummaryFactory.kt @@ -17,6 +17,7 @@ import io.element.android.libraries.dateformatter.api.DateFormatter import io.element.android.libraries.dateformatter.api.DateFormatterMode import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.eventformatter.api.RoomLatestEventFormatter +import io.element.android.libraries.matrix.api.room.CallIntentConsensus import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.isDm import io.element.android.libraries.matrix.api.roomlist.LatestEventValue @@ -50,6 +51,11 @@ class RoomListRoomSummaryFactory( avatarData = avatarData, userDefinedNotificationMode = roomInfo.userDefinedNotificationMode, hasRoomCall = roomInfo.hasRoomCall, + activeCallIntent = when (val consensus = roomInfo.activeCallIntentConsensus) { + is CallIntentConsensus.Full -> consensus.callIntent + is CallIntentConsensus.Partial -> consensus.callIntent + CallIntentConsensus.None -> null + }, isDirect = roomInfo.isDirect, isFavorite = roomInfo.isFavorite, inviteSender = roomInfo.inviter?.toInviteSender(), diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummary.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummary.kt index a59e444455..628d0d0a9b 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummary.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummary.kt @@ -13,6 +13,7 @@ import io.element.android.features.invite.api.InviteData import io.element.android.libraries.designsystem.components.avatar.AvatarData 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.notification.CallIntent import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.ui.model.InviteSender import kotlinx.collections.immutable.ImmutableList @@ -33,6 +34,7 @@ data class RoomListRoomSummary( val avatarData: AvatarData, val userDefinedNotificationMode: RoomNotificationMode?, val hasRoomCall: Boolean, + val activeCallIntent: CallIntent?, val isDirect: Boolean, val isDm: Boolean, val isFavorite: Boolean, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryProvider.kt index 400decff6f..eefb2d6484 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryProvider.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryProvider.kt @@ -14,6 +14,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.notification.CallIntent import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.ui.model.InviteSender import kotlinx.collections.immutable.toImmutableList @@ -132,6 +133,14 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider { @Composable override fun present(): SpaceFiltersState { - val isFeatureEnabled by featureFlagService - .isFeatureEnabledFlow(FeatureFlags.RoomListSpaceFilters) - .collectAsState(initial = false) - val availableFilters by remember { matrixClient.spaceService.spaceFiltersFlow.map { it.toImmutableList() } }.collectAsState(initial = persistentListOf()) - if (!isFeatureEnabled || availableFilters.isEmpty()) { + if (availableFilters.isEmpty()) { return SpaceFiltersState.Disabled } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.kt index 707ac73261..b758a1e593 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.kt @@ -15,8 +15,6 @@ import androidx.compose.runtime.remember import dev.zacsweers.metro.Inject import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.featureflag.api.FeatureFlagService -import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar import kotlinx.collections.immutable.persistentListOf @@ -29,11 +27,9 @@ import kotlinx.coroutines.flow.map class HomeSpacesPresenter( private val client: MatrixClient, private val seenInvitesStore: SeenInvitesStore, - private val featureFlagsService: FeatureFlagService, ) : Presenter { @Composable override fun present(): HomeSpacesState { - val canCreateSpaces by featureFlagsService.isFeatureEnabledFlow(FeatureFlags.CreateSpaces).collectAsState(false) val hideInvitesAvatar by client.rememberHideInvitesAvatar() val spaceRooms by remember { client.spaceService.topLevelSpacesFlow.map { it.toImmutableList() } @@ -52,7 +48,6 @@ class HomeSpacesPresenter( spaceRooms = spaceRooms, seenSpaceInvites = seenSpaceInvites, hideInvitesAvatar = hideInvitesAvatar, - canCreateSpaces = canCreateSpaces, // TODO enable once we can link to the screen to explore public spaces canExploreSpaces = false, eventSink = ::handleEvent, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesState.kt index 84b2dc7f52..e93f04291e 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesState.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesState.kt @@ -18,7 +18,6 @@ data class HomeSpacesState( val spaceRooms: ImmutableList, val seenSpaceInvites: ImmutableSet, val hideInvitesAvatar: Boolean, - val canCreateSpaces: Boolean, val canExploreSpaces: Boolean, val eventSink: (HomeSpacesEvents) -> Unit, ) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesStateProvider.kt index a65f29cc2f..17f2cbad31 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesStateProvider.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesStateProvider.kt @@ -30,17 +30,9 @@ open class HomeSpacesStateProvider : PreviewParameterProvider { ), spaceRooms = aListOfSpaceRooms(), ), - aHomeSpacesState( - space = CurrentSpace.Space( - spaceRoom = aSpaceRoom(roomId = RoomId("!mySpace:example.com")) - ), - spaceRooms = aListOfSpaceRooms(), - canCreateSpaces = false, - ), aHomeSpacesState( space = CurrentSpace.Root, spaceRooms = emptyList(), - canCreateSpaces = true, ), ) } @@ -50,7 +42,6 @@ internal fun aHomeSpacesState( spaceRooms: List = aListOfSpaceRooms(), seenSpaceInvites: Set = emptySet(), hideInvitesAvatar: Boolean = false, - canCreateSpaces: Boolean = true, canExploreSpaces: Boolean = true, eventSink: (HomeSpacesEvents) -> Unit = {}, ) = HomeSpacesState( @@ -58,7 +49,6 @@ internal fun aHomeSpacesState( spaceRooms = spaceRooms.toImmutableList(), seenSpaceInvites = seenSpaceInvites.toImmutableSet(), hideInvitesAvatar = hideInvitesAvatar, - canCreateSpaces = canCreateSpaces, canExploreSpaces = canExploreSpaces, eventSink = eventSink, ) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesView.kt index c563e6eb26..9125912bf0 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesView.kt @@ -55,7 +55,7 @@ fun HomeSpacesView( onExploreClick: () -> Unit, modifier: Modifier = Modifier, ) { - if (state.canCreateSpaces && state.spaceRooms.isEmpty()) { + if (state.spaceRooms.isEmpty()) { EmptySpaceHomeView( modifier = modifier.padding(contentPadding), onCreateSpaceClick = onCreateSpaceClick, diff --git a/features/home/impl/src/main/res/values-cs/translations.xml b/features/home/impl/src/main/res/values-cs/translations.xml index 16f9e6b801..0276db1fbe 100644 --- a/features/home/impl/src/main/res/values-cs/translations.xml +++ b/features/home/impl/src/main/res/values-cs/translations.xml @@ -5,9 +5,9 @@ "Nepřicházejí vám oznámení?" "Váš zvuk oznámení byl aktualizován – je jasnější, rychlejší a méně rušivý." "Aktualizovali jsme vaše zvuky" - "Vygenerujte nový klíč pro obnovení, který lze použít k obnovení historie šifrovaných zpráv v případě, že ztratíte přístup ke svým zařízením." - "Nastavení obnovy" - "Nastavení obnovy" + "Vaše chaty jsou automaticky zálohovány pomocí koncového šifrování. Chcete-li tuto zálohu obnovit a zachovat si svou digitální identitu v případě, že ztratíte přístup ke všem svým zařízením, budete potřebovat svůj klíč pro obnovení." + "Získat klíč pro obnovení" + "Zálohujte své chaty" "Potvrďte klíč pro obnovení, abyste zachovali přístup k úložišti klíčů a historii zpráv." "Zadejte klíč pro obnovení" "Zapomněli jste klíč pro obnovení?" diff --git a/features/home/impl/src/main/res/values-it/translations.xml b/features/home/impl/src/main/res/values-it/translations.xml index 932545c7b0..2c5461bfa8 100644 --- a/features/home/impl/src/main/res/values-it/translations.xml +++ b/features/home/impl/src/main/res/values-it/translations.xml @@ -5,9 +5,9 @@ "Le notifiche non arrivano?" "Il ping delle notifiche è stato aggiornato: ora è più chiaro, più rapido e meno fastidioso." "Abbiamo rinnovato i tuoi suoni" - "Recupera la tua identità crittografica e la cronologia dei messaggi con una chiave di recupero se hai perso tutti i tuoi dispositivi." - "Configura il recupero" - "Configura il ripristino" + "Le tue conversazioni vengono automaticamente salvate con crittografia end-to-end. Per ripristinare questo backup e conservare la tua identità digitale quando perdi l\'accesso a tutti i tuoi dispositivi, avrai bisogno della tua chiave di recupero." + "Ottieni la chiave di recupero" + "Esegui il backup delle tue conversazioni" "Conferma la chiave di recupero per mantenere l\'accesso all\'archiviazione delle chiavi e alla cronologia dei messaggi." "Inserisci la tua chiave di recupero" "Hai dimenticato la chiave di recupero?" @@ -50,6 +50,7 @@ Non hai messaggi non letti!" "Segna come letto" "Segna come non letto" "Questa stanza è stata aggiornata" + "I tuoi spazi" "Sembra che tu stia usando un nuovo dispositivo. Verificati con un altro dispositivo per accedere ai tuoi messaggi cifrati." "Verifica che sei tu" diff --git a/features/home/impl/src/main/res/values-ja/translations.xml b/features/home/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..3e4b16b682 --- /dev/null +++ b/features/home/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,56 @@ + + + "すべての通知を確実に受信するために、このアプリのバッテリー最適化を無効にしてください。" + "最適化を無効にする" + "通知が届いていませんか?" + "通知音が更新され、より明確で速く、そして邪魔にならなくなりました。" + "サウンドを刷新しました" + "あなたのチャットはエンドツーエンド暗号化を使用して自動的にバックアップされています。すべての端末を使用できない状況で、このバックアップからデジタルIDを復元するには、回復鍵が必要となります。" + "回復鍵を作成" + "チャットをバックアップ" + "鍵の保管庫と過去のメッセージにアクセスするために、回復鍵を認証してください。" + "回復鍵を入力してください" + "回復鍵を忘れましたか?" + "鍵の保管庫を同期できません。" + "重要な電話を確実に受け取るため、端末がロックされている状態での全画面通知を、設定から許可してください。" + "通話品質を高める" + "チャット" + "スペース" + "%1$sへの招待を本当に破棄しますか?" + "招待を破棄" + "%1$sとのチャットを本当に拒否しますか?" + "チャットを拒否" + "招待はありません" + "%1$s (%2$s) があなたを招待しました" + "一度限りの工程です。お待ちください。" + "アカウントを設定しています。" + "新しい会話またはルームを作成" + "フィルターを解除" + "誰かにメッセージを送信しましょう。" + "まだチャットがありません。" + "お気に入り" + "チャットの設定からお気に入りに追加できます。 +現在は、フィルターの選択を解除することで他のチャットを表示できます。" + "お気に入りのチャットはまだありません" + "招待" + "承認待ちの招待はありません" + "低い優先度" + "低い優先度のチャットはまだありません" + "フィルターを解除して他のチャットを表示できます" + "この選択中にチャットがありません" + "人" + "まだダイレクトメッセージは届いていません" + "ルーム" + "まだルームに参加していません" + "未読" + "やった! +未読メッセージはありません。" + "参加リクエストを送信しました" + "チャット" + "既読にする" + "未読にする" + "このルームはアップグレードされました" + "あなたのスペース" + "新しいデバイスをご利用のようです。暗号化されたメッセージにアクセスするには、別のデバイスで検証してください。" + "本人確認" + diff --git a/features/home/impl/src/main/res/values-ru/translations.xml b/features/home/impl/src/main/res/values-ru/translations.xml index d06c9a3854..233e8749b4 100644 --- a/features/home/impl/src/main/res/values-ru/translations.xml +++ b/features/home/impl/src/main/res/values-ru/translations.xml @@ -5,7 +5,7 @@ "Уведомления не приходят?" "Ваши уведомления были обновлены — теперь они понятнее, быстрее и менее отвлекающие." "Мы обновили ваши звуки" - "Создайте новый ключ восстановления, который можно использовать для восстановления зашифрованной истории сообщений в случае потери доступа к своим устройствам." + "Ваши чаты автоматически резервируются с использованием сквозного шифрования. Для восстановления этой резервной копии и сохранения вашей цифровой личности в случае потери доступа ко всем вашим устройствам вам потребуется ключ восстановления." "Получить ключ восстановления" "Сделайте резервную копию своих чатов." "Подтвердите ключ восстановления, чтобы сохранить доступ к хранилищу ключей и истории сообщений." diff --git a/features/home/impl/src/main/res/values-vi/translations.xml b/features/home/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..bc62880fff --- /dev/null +++ b/features/home/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,48 @@ + + + "Tắt tính năng tối ưu hóa pin cho ứng dụng này để đảm bảo nhận được mọi thông báo." + "Tắt tối ưu hóa" + "Có nhận được thông báo không?" + "Thông báo của bạn đã được cập nhật — rõ ràng hơn, nhanh hơn và ít gây khó chịu hơn." + "Chúng tôi đã làm mới âm thanh của bạn." + "Các cuộc trò chuyện của bạn được tự động sao lưu bằng mã hóa đầu cuối. Để khôi phục bản sao lưu này và giữ lại danh tính kỹ thuật số của bạn khi bạn mất quyền truy cập vào tất cả các thiết bị, bạn sẽ cần khóa khôi phục." + "Lấy khóa khôi phục." + "Sao lưu tin nhắn của bạn" + "Xác nhận khóa khôi phục để không bị mất quyền truy cập vào tin nhắn." + "Nhập khóa khôi phục của bạn." + "Bạn quên khóa khôi phục?”" + "Dữ liệu khóa của bạn không còn đồng bộ" + "Cuộc trò chuyện" + "Bạn có chắc muốn từ chối lời mời tham gia %1$s không?" + "Từ chối lời mời" + "Bạn có chắc muốn từ chối cuộc trò chuyện riêng với %1$s không?" + "Từ chối trò chuyện" + "Không có lời mời" + "%1$s(%2$s ) đã mời bạn" + "Quá trình này chỉ thực hiện một lần, cảm ơn bạn đã kiên nhẫn." + "Đang thiết lập tài khoản của bạn." + "Tạo một cuộc trò chuyện hoặc phòng mới" + "Bắt đầu bằng cách nhắn tin cho ai đó." + "Chưa có cuộc trò chuyện nào." + "Yêu thích" + "Bạn có thể thêm cuộc trò chuyện vào mục yêu thích trong cài đặt chat. +Hiện tại, bạn có thể bỏ chọn bộ lọc để xem các cuộc trò chuyện khác." + "Bạn chưa có cuộc trò chuyện yêu thích nào." + "Lời mời" + "Ưu tiên thấp" + "Bạn có thể bỏ chọn bộ lọc để xem các cuộc trò chuyện khác" + "Bạn không có cuộc trò chuyện nào cho lựa chọn này" + "Danh bạ" + "Bạn chưa có tin nhắn riêng nào cả" + "Phòng" + "Bạn chưa tham gia phòng nào" + "Chưa đọc" + "Chúc mừng! +Bạn không còn tin nhắn nào chưa đọc nữa!" + "Yêu cầu tham gia đã được gửi" + "Cuộc trò chuyện" + "Đánh dấu đã đọc" + "Đánh dấu chưa đọc" + "Có vẻ như bạn đang sử dụng thiết bị mới. Hãy xác minh bằng một thiết bị khác để truy cập tin nhắn được mã hóa của bạn." + "Xác thực danh tính của bạn" + diff --git a/features/home/impl/src/main/res/values-zh/translations.xml b/features/home/impl/src/main/res/values-zh/translations.xml index 1705569a49..58e5b1eb91 100644 --- a/features/home/impl/src/main/res/values-zh/translations.xml +++ b/features/home/impl/src/main/res/values-zh/translations.xml @@ -6,7 +6,7 @@ "您的通知提示音已升级 - 更清晰、更快速、干扰更少。" "我们已更新您的声音" "生成新的恢复密钥,该密钥可用于在您无法访问设备时恢复加密的消息历史记录。" - "设置恢复" + "获取恢复密钥" "设置恢复" "确认恢复密钥,以保持对密钥存储和消息历史的访问。" "输入恢复密钥" diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt index 4002844947..371a718523 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt @@ -9,14 +9,11 @@ package io.element.android.features.home.impl import com.google.common.truth.Truth.assertThat -import io.element.android.features.announcement.api.Announcement -import io.element.android.features.announcement.api.AnnouncementService import io.element.android.features.home.impl.roomlist.aRoomListState import io.element.android.features.home.impl.spaces.HomeSpacesState import io.element.android.features.home.impl.spaces.aHomeSpacesState import io.element.android.features.logout.api.direct.aDirectLogoutState import io.element.android.features.rageshake.api.RageshakeFeatureAvailability -import io.element.android.features.rageshake.test.logs.FakeAnnouncementService import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.indicator.api.IndicatorService @@ -33,10 +30,7 @@ import io.element.android.libraries.matrix.test.sync.FakeSyncService import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.libraries.sessionstorage.test.aSessionData -import io.element.android.tests.testutils.MutablePresenter import io.element.android.tests.testutils.WarmUpRule -import io.element.android.tests.testutils.lambda.lambdaRecorder -import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.test import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest @@ -79,7 +73,6 @@ class HomePresenterTest { MatrixUser(A_USER_ID, A_USER_NAME, AN_AVATAR_URL) ) assertThat(withUserState.showAvatarIndicator).isFalse() - assertThat(withUserState.showNavigationBar).isTrue() } } @@ -139,14 +132,10 @@ class HomePresenterTest { @Test fun `present - NavigationBar change`() = runTest { - val showAnnouncementResult = lambdaRecorder { } val presenter = createHomePresenter( sessionStore = InMemorySessionStore( updateUserProfileResult = { _, _, _ -> }, ), - announcementService = FakeAnnouncementService( - showAnnouncementResult = showAnnouncementResult, - ) ) presenter.test { val initialState = awaitItem() @@ -154,38 +143,6 @@ class HomePresenterTest { initialState.eventSink(HomeEvent.SelectHomeNavigationBarItem(HomeNavigationBarItem.Spaces)) val finalState = awaitItem() assertThat(finalState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Spaces) - showAnnouncementResult.assertions().isCalledOnce() - .with(value(Announcement.Space)) - } - } - - @Test - fun `present - NavigationBar is hidden when the last space is left when the user can't create new spaces`() = runTest { - val homeSpacesPresenter = MutablePresenter(aHomeSpacesState()) - val presenter = createHomePresenter( - sessionStore = InMemorySessionStore( - updateUserProfileResult = { _, _, _ -> }, - ), - homeSpacesPresenter = homeSpacesPresenter, - announcementService = FakeAnnouncementService( - showAnnouncementResult = {}, - ) - ) - presenter.test { - val initialState = awaitItem() - assertThat(initialState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Chats) - assertThat(initialState.showNavigationBar).isTrue() - // User navigate to Spaces - initialState.eventSink(HomeEvent.SelectHomeNavigationBarItem(HomeNavigationBarItem.Spaces)) - val spaceState = awaitItem() - assertThat(spaceState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Spaces) - // The last space is left - homeSpacesPresenter.updateState(aHomeSpacesState(spaceRooms = emptyList(), canCreateSpaces = false)) - skipItems(1) - val finalState = awaitItem() - // We are back to Chats - assertThat(finalState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Chats) - assertThat(finalState.showNavigationBar).isFalse() } } } @@ -198,7 +155,6 @@ internal fun createHomePresenter( indicatorService: IndicatorService = FakeIndicatorService(), homeSpacesPresenter: Presenter = Presenter { aHomeSpacesState() }, sessionStore: SessionStore = InMemorySessionStore(), - announcementService: AnnouncementService = FakeAnnouncementService(), ) = HomePresenter( client = client, syncService = syncService, @@ -209,5 +165,4 @@ internal fun createHomePresenter( logoutPresenter = { aDirectLogoutState() }, rageshakeFeatureAvailability = rageshakeFeatureAvailability, sessionStore = sessionStore, - announcementService = announcementService, ) diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/model/RoomListBaseRoomSummaryTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/model/RoomListBaseRoomSummaryTest.kt index 28e7051a55..63f1ecebd7 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/model/RoomListBaseRoomSummaryTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/model/RoomListBaseRoomSummaryTest.kt @@ -101,6 +101,7 @@ internal fun createRoomListRoomSummary( displayType = displayType, userDefinedNotificationMode = userDefinedNotificationMode, hasRoomCall = false, + activeCallIntent = null, isDirect = false, isFavorite = isFavorite, canonicalAlias = null, diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenterTest.kt index 278a268864..31a47d830f 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenterTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenterTest.kt @@ -8,8 +8,6 @@ package io.element.android.features.home.impl.spacefilters import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.featureflag.api.FeatureFlags -import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.spaces.FakeSpaceService @@ -21,26 +19,9 @@ import org.junit.Test @OptIn(ExperimentalCoroutinesApi::class) class SpaceFiltersPresenterTest { - @Test - fun `present - when feature flag is disabled returns Disabled state`() = runTest { - val presenter = createSpaceFiltersPresenter( - featureFlagService = FakeFeatureFlagService( - initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to false) - ) - ) - presenter.test { - val state = awaitItem() - assertThat(state).isEqualTo(SpaceFiltersState.Disabled) - } - } - @Test fun `present - when available filters is empty returns Disabled state`() = runTest { - val presenter = createSpaceFiltersPresenter( - featureFlagService = FakeFeatureFlagService( - initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) - ) - ) + val presenter = createSpaceFiltersPresenter() presenter.test { val state = awaitLastSequentialItem() assertThat(state).isEqualTo(SpaceFiltersState.Disabled) @@ -48,15 +29,12 @@ class SpaceFiltersPresenterTest { } @Test - fun `present - when feature flag is enabled and filters exist returns Unselected state`() = runTest { + fun `present - when filters exist returns Unselected state`() = runTest { val spaceFilter = aSpaceServiceFilter(displayName = "Test Space") val spaceService = FakeSpaceService() val matrixClient = FakeMatrixClient(spaceService = spaceService) val presenter = createSpaceFiltersPresenter( - featureFlagService = FakeFeatureFlagService( - initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) - ), matrixClient = matrixClient, ) presenter.test { @@ -75,9 +53,6 @@ class SpaceFiltersPresenterTest { val matrixClient = FakeMatrixClient(spaceService = spaceService) val presenter = createSpaceFiltersPresenter( - featureFlagService = FakeFeatureFlagService( - initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) - ), matrixClient = matrixClient, ) presenter.test { @@ -99,9 +74,6 @@ class SpaceFiltersPresenterTest { val matrixClient = FakeMatrixClient(spaceService = spaceService) val presenter = createSpaceFiltersPresenter( - featureFlagService = FakeFeatureFlagService( - initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) - ), matrixClient = matrixClient, ) presenter.test { @@ -129,9 +101,6 @@ class SpaceFiltersPresenterTest { val matrixClient = FakeMatrixClient(spaceService = spaceService) val presenter = createSpaceFiltersPresenter( - featureFlagService = FakeFeatureFlagService( - initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) - ), matrixClient = matrixClient, ) presenter.test { @@ -159,9 +128,6 @@ class SpaceFiltersPresenterTest { val matrixClient = FakeMatrixClient(spaceService = spaceService) val presenter = createSpaceFiltersPresenter( - featureFlagService = FakeFeatureFlagService( - initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) - ), matrixClient = matrixClient, ) presenter.test { @@ -196,9 +162,6 @@ class SpaceFiltersPresenterTest { val matrixClient = FakeMatrixClient(spaceService = spaceService) val presenter = createSpaceFiltersPresenter( - featureFlagService = FakeFeatureFlagService( - initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) - ), matrixClient = matrixClient, ) presenter.test { @@ -224,9 +187,6 @@ class SpaceFiltersPresenterTest { val matrixClient = FakeMatrixClient(spaceService = spaceService) val presenter = createSpaceFiltersPresenter( - featureFlagService = FakeFeatureFlagService( - initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) - ), matrixClient = matrixClient, ) presenter.test { @@ -271,9 +231,6 @@ class SpaceFiltersPresenterTest { val matrixClient = FakeMatrixClient(spaceService = spaceService) val presenter = createSpaceFiltersPresenter( - featureFlagService = FakeFeatureFlagService( - initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) - ), matrixClient = matrixClient, ) presenter.test { @@ -302,11 +259,9 @@ class SpaceFiltersPresenterTest { } private fun createSpaceFiltersPresenter( - featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(), matrixClient: FakeMatrixClient = FakeMatrixClient(), ): SpaceFiltersPresenter { return SpaceFiltersPresenter( - featureFlagService = featureFlagService, matrixClient = matrixClient, ) } diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenterTest.kt index 43d3a8896d..c7608833ac 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenterTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenterTest.kt @@ -11,9 +11,6 @@ package io.element.android.features.home.impl.spaces import com.google.common.truth.Truth.assertThat import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.features.invite.test.InMemorySeenInvitesStore -import io.element.android.libraries.featureflag.api.FeatureFlagService -import io.element.android.libraries.featureflag.api.FeatureFlags -import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.tests.testutils.test @@ -26,25 +23,18 @@ class HomeSpacesPresenterTest { val presenter = createPresenter() presenter.test { val state = awaitItem() - // canCreateSpaces is initially false - assertThat(state.canCreateSpaces).isFalse() assertThat(state.space).isEqualTo(CurrentSpace.Root) assertThat(state.spaceRooms).isEmpty() assertThat(state.hideInvitesAvatar).isFalse() assertThat(state.seenSpaceInvites).isEmpty() - - // It'll eventually be true - assertThat(awaitItem().canCreateSpaces).isTrue() } } private fun createPresenter( client: MatrixClient = FakeMatrixClient(), seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(), - featureFlagsService: FeatureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.CreateSpaces.key to true)), ) = HomeSpacesPresenter( client = client, seenInvitesStore = seenInvitesStore, - featureFlagsService = featureFlagsService, ) } diff --git a/features/invite/impl/src/main/res/values-be/translations.xml b/features/invite/impl/src/main/res/values-be/translations.xml index fca38b796a..3c17aa0cdb 100644 --- a/features/invite/impl/src/main/res/values-be/translations.xml +++ b/features/invite/impl/src/main/res/values-be/translations.xml @@ -1,10 +1,12 @@ "Заблакіраваць карыстальніка" + "Адхіліць і заблакіраваць" "Вы ўпэўненыя, што хочаце адхіліць запрашэнне ў %1$s?" "Адхіліць запрашэнне" "Вы ўпэўненыя, што хочаце адмовіцца ад прыватных зносін з %1$s?" "Адхіліць чат" "Няма запрашэнняў" "%1$s (%2$s) запрасіў(-ла) вас" + "Адхіліць і заблакіраваць" diff --git a/features/invite/impl/src/main/res/values-ja/translations.xml b/features/invite/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..5e1b527dd3 --- /dev/null +++ b/features/invite/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,18 @@ + + + "このユーザーからのメッセージと招待を非表示します" + "ユーザーをブロック" + "アカウント提供元にこのルームを報告" + "報告の理由を説明してください…" + "拒否してブロック" + "%1$sへの招待を本当に破棄しますか?" + "招待を破棄" + "%1$sとのチャットを本当に拒否しますか?" + "チャットを拒否" + "招待はありません" + "%1$s (%2$s) があなたを招待しました" + "拒否してブロックする" + "本当にこのルームへの参加の招待を拒否しますか?%1$s は、あなたと会話することやルームに招待することができなくなります。" + "招待を拒否してブロック" + "拒否してブロック" + diff --git a/features/invite/impl/src/main/res/values-vi/translations.xml b/features/invite/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..9b7784c25d --- /dev/null +++ b/features/invite/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,17 @@ + + + "Bạn sẽ không nhận được bất kỳ tin nhắn hoặc lời mời tham gia phòng nào từ người dùng này." + "Chặn người dùng" + "Báo cáo phòng này cho nhà cung cấp tài khoản của bạn." + "Từ chối và chặn" + "Bạn có chắc muốn từ chối lời mời tham gia %1$s không?" + "Từ chối lời mời" + "Bạn có chắc muốn từ chối cuộc trò chuyện riêng với %1$s không?" + "Từ chối trò chuyện" + "Không có lời mời" + "%1$s(%2$s ) đã mời bạn" + "Có, từ chối & chặn" + "Bạn có chắc muốn từ chối lời mời tham gia phòng này không? Điều này cũng sẽ ngăn %1$s liên hệ với bạn hoặc mời bạn vào các phòng." + "Từ chối lời mời và chặn" + "Từ chối và chặn" + diff --git a/features/invite/impl/src/main/res/values-zh/translations.xml b/features/invite/impl/src/main/res/values-zh/translations.xml index e7cf39a9c3..8c27397225 100644 --- a/features/invite/impl/src/main/res/values-zh/translations.xml +++ b/features/invite/impl/src/main/res/values-zh/translations.xml @@ -1,7 +1,7 @@ - "您不会看到来自该用户的任何信息或房间邀请" - "封禁用户" + "您将不会看到来自该用户的任何信息或房间邀请" + "屏蔽用户" "向您的帐户提供商举报此房间。" "描述举报的原因…" "拒绝并屏蔽" @@ -12,7 +12,7 @@ "没有邀请" "%1$s (%2$s)邀请了你" "是的,拒绝并屏蔽" - "您确定要拒绝加入此房间的邀请吗?这也将阻止%1$s 与您联系或邀请您加入房间。" + "您确定要拒绝加入此房间的邀请吗?这也将阻止 %1$s 与您联系或邀请您加入房间。" "拒绝邀请并屏蔽" "拒绝并屏蔽" diff --git a/features/invitepeople/impl/build.gradle.kts b/features/invitepeople/impl/build.gradle.kts index e2025405ff..390ccce7b9 100644 --- a/features/invitepeople/impl/build.gradle.kts +++ b/features/invitepeople/impl/build.gradle.kts @@ -34,13 +34,15 @@ dependencies { implementation(projects.libraries.designsystem) implementation(projects.libraries.uiStrings) implementation(projects.libraries.androidutils) - implementation(projects.libraries.usersearch.impl) + implementation(projects.libraries.usersearch.api) implementation(libs.coil.compose) implementation(projects.services.apperror.api) + implementation(projects.libraries.featureflag.api) api(projects.features.invitepeople.api) testCommonDependencies(libs, true) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.usersearch.test) testImplementation(projects.services.apperror.test) + testImplementation(projects.libraries.featureflag.test) } diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/ConfirmingUnknownUserInvitation.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/ConfirmingUnknownUserInvitation.kt new file mode 100644 index 0000000000..8f4d5e1510 --- /dev/null +++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/ConfirmingUnknownUserInvitation.kt @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2026 Element Creations 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.invitepeople.impl + +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.user.MatrixUser +import kotlinx.collections.immutable.ImmutableList + +data class ConfirmingUnknownUserInvitation( + val users: ImmutableList +) : AsyncAction.Confirming diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleEvents.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleEvents.kt index b1f18b1df9..449d0ce6ac 100644 --- a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleEvents.kt +++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleEvents.kt @@ -14,4 +14,6 @@ import io.element.android.libraries.matrix.api.user.MatrixUser sealed interface DefaultInvitePeopleEvents : InvitePeopleEvents { data class ToggleUser(val user: MatrixUser) : DefaultInvitePeopleEvents data class OnSearchActiveChanged(val active: Boolean) : DefaultInvitePeopleEvents + data object DismissUnknownUsersModal : DefaultInvitePeopleEvents + data object RemoveUnknownUsers : DefaultInvitePeopleEvents } diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt index 3450587e82..58b3fb67f6 100644 --- a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt +++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -36,8 +37,11 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.annotations.SessionCoroutineScope +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMembershipState @@ -50,6 +54,7 @@ import io.element.android.services.apperror.api.AppErrorStateService import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toImmutableMap import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.launchIn @@ -69,6 +74,7 @@ class DefaultInvitePeoplePresenter( private val coroutineDispatchers: CoroutineDispatchers, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, private val appErrorStateService: AppErrorStateService, + private val featureFlagService: FeatureFlagService, private val matrixClient: MatrixClient, ) : InvitePeoplePresenter { @AssistedFactory @@ -87,6 +93,8 @@ class DefaultInvitePeoplePresenter( val showSearchLoader = rememberSaveable { mutableStateOf(false) } val sendInvitesAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } + val enableKeyShareOnInvite by featureFlagService.isFeatureEnabledFlow(FeatureFlags.EnableKeyShareOnInvite).collectAsState(initial = false) + val recentDirectRooms by produceState(emptyList(), roomMembers.value) { if (roomMembers.value.isSuccess()) { val activeMemberIds = roomMembers.value.dataOrNull().orEmpty() @@ -126,6 +134,40 @@ class DefaultInvitePeoplePresenter( } } + val selectedUserIdentities = produceState( + emptyMap().toImmutableMap(), + selectedUsers.value, + enableKeyShareOnInvite, + ) { + if (!enableKeyShareOnInvite) { + return@produceState + } + + val selected = selectedUsers.value + + val cached = value + .filterKeys { it in selected } + + val uncached = selected + .filterNot(cached::containsKey) + .associateWith { user -> + matrixClient.encryptionService + .getUserIdentity(user.userId, fallbackToServer = false) + .getOrNull() + } + + value = (cached + uncached).toImmutableMap() + } + + val unknownUsers by remember { + derivedStateOf { + selectedUserIdentities.value + .filterValues { it == null } + .keys + .toImmutableList() + } + } + LaunchedEffect(room.isSuccess()) { room.dataOrNull()?.let { fetchMembers(it, roomMembers) @@ -144,21 +186,41 @@ class DefaultInvitePeoplePresenter( fun handleEvent(event: InvitePeopleEvents) { when (event) { - is DefaultInvitePeopleEvents.OnSearchActiveChanged -> { - searchActive = event.active - if (!event.active) { - queryState.clearText() + // Dedicated `when` for exhaustivity. + is DefaultInvitePeopleEvents -> when (event) { + is DefaultInvitePeopleEvents.OnSearchActiveChanged -> { + searchActive = event.active + if (!event.active) { + queryState.clearText() + } + } + + is DefaultInvitePeopleEvents.ToggleUser -> { + selectedUsers.toggleUser(event.user) + searchResults.toggleUser(event.user) + // suggestions will automatically update via derivedStateOf when selectedUsers changes + } + is DefaultInvitePeopleEvents.DismissUnknownUsersModal -> { + sendInvitesAction.value = AsyncAction.Uninitialized + } + is DefaultInvitePeopleEvents.RemoveUnknownUsers -> { + val usersToRemove = selectedUsers.value.filter { it in unknownUsers } + usersToRemove.forEach { user -> + selectedUsers.toggleUser(user) + searchResults.toggleUser(user) + } + sendInvitesAction.value = AsyncAction.Uninitialized } } - - is DefaultInvitePeopleEvents.ToggleUser -> { - selectedUsers.toggleUser(event.user) - searchResults.toggleUser(event.user) - // suggestions will automatically update via derivedStateOf when selectedUsers changes - } is InvitePeopleEvents.SendInvites -> { - room.dataOrNull()?.let { - sessionCoroutineScope.sendInvites(it, selectedUsers.value, sendInvitesAction) + if (enableKeyShareOnInvite && unknownUsers.isNotEmpty() && sendInvitesAction.value !is ConfirmingUnknownUserInvitation) { + sendInvitesAction.value = ConfirmingUnknownUserInvitation( + unknownUsers + ) + } else { + room.dataOrNull()?.let { + sessionCoroutineScope.sendInvites(it, selectedUsers.value, sendInvitesAction) + } } } is InvitePeopleEvents.CloseSearch -> { diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStateProvider.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStateProvider.kt index 15ded2ae3f..c26b8de254 100644 --- a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStateProvider.kt +++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStateProvider.kt @@ -76,6 +76,16 @@ internal class DefaultInvitePeopleStateProvider : PreviewParameterProvider, + onDismiss: () -> Unit, + onInvite: () -> Unit, + onRemove: () -> Unit +) { + ModalBottomSheet( + onDismissRequest = onDismiss, + dragHandle = null, + ) { + IconTitleSubtitleMolecule( + title = pluralStringResource(R.plurals.screen_invite_users_confirm_dialog_title, users.size), + subTitle = pluralStringResource(R.plurals.screen_invite_users_confirm_dialog_subtitle, users.size), + iconStyle = BigIcon.Style.Default(CompoundIcons.UserAddSolid()), + modifier = Modifier.padding( + top = 32.dp, + bottom = 16.dp, + start = 16.dp, + end = 16.dp, + ) + ) + + LazyColumn { + items(users) { user -> + MatrixUserRow(user) + } + } + + ButtonRowMolecule( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.padding(16.dp) + ) { + OutlinedButton( + text = stringResource(CommonStrings.action_remove), + onClick = onRemove, + leadingIcon = IconSource.Vector(CompoundIcons.Close()), + modifier = Modifier.weight(1f) + ) + Button( + text = stringResource(CommonStrings.action_invite), + onClick = onInvite, + leadingIcon = IconSource.Vector(CompoundIcons.Check()), + modifier = Modifier.weight(1f) + ) + } + } +} + @PreviewsDayNight @Composable internal fun InvitePeopleViewPreview(@PreviewParameter(DefaultInvitePeopleStateProvider::class) state: DefaultInvitePeopleState) = diff --git a/features/invitepeople/impl/src/main/res/values-ja/translations.xml b/features/invitepeople/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..6aa1fb0370 --- /dev/null +++ b/features/invitepeople/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,11 @@ + + + "既に参加しています" + "既に招待しています" + + "この連絡先とのチャットがありません。続行する前に、このルームに招待してください。" + + + "このルームに新しい連絡先を追加しますか?" + + diff --git a/features/invitepeople/impl/src/main/res/values-vi/translations.xml b/features/invitepeople/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..934cb7d24f --- /dev/null +++ b/features/invitepeople/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,5 @@ + + + "Đã là thành viên" + "Đã được mời" + diff --git a/features/invitepeople/impl/src/main/res/values/localazy.xml b/features/invitepeople/impl/src/main/res/values/localazy.xml index d89ae92e75..0515121428 100644 --- a/features/invitepeople/impl/src/main/res/values/localazy.xml +++ b/features/invitepeople/impl/src/main/res/values/localazy.xml @@ -2,4 +2,12 @@ "Already a member" "Already invited" + + "You currently don’t have any chats with this contact. Confirm inviting them to this room before continuing." + "You currently don’t have any chats with these contacts. Confirm inviting them to this room before continuing." + + + "Invite a new contact to this room?" + "Invite new contacts to this room?" + diff --git a/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt b/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt index ab9e20437e..a1d72010f6 100644 --- a/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt +++ b/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt @@ -15,9 +15,13 @@ import io.element.android.features.invitepeople.api.InvitePeopleEvents import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.RoomMembersState @@ -28,6 +32,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService 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 @@ -43,6 +48,7 @@ import io.element.android.libraries.usersearch.test.FakeUserRepository import io.element.android.services.apperror.api.AppErrorStateService import io.element.android.services.apperror.test.FakeAppErrorStateService import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.awaitLastSequentialItem import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value @@ -56,6 +62,7 @@ import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test +@Suppress("LargeClass") internal class DefaultInvitePeoplePresenterTest { @get:Rule val warmUpRule = WarmUpRule() @@ -605,6 +612,231 @@ internal class DefaultInvitePeoplePresenterTest { } } + @Test + fun `present - users are prompted for confirmation if they attempt to invite unknown users`() = runTest { + val alice = aMatrixUser("@alice:example.com") + val bob = aMatrixUser("@bob:example.com") + val charlie = aMatrixUser("@charlie:example.com") + + val getUserIdentityResult = lambdaRecorder> { userId -> + when (userId.value) { + alice.userId.value -> Result.success(IdentityState.Pinned) + bob.userId.value -> Result.success(null) + else -> Result.failure(AN_EXCEPTION) + } + } + + val inviteUserResult = lambdaRecorder> { userId: UserId -> + Result.success(Unit) + } + val encryptionService = FakeEncryptionService( + getUserIdentityResult = getUserIdentityResult + ) + val featureFlagService = FakeFeatureFlagService().apply { + setFeatureEnabled(FeatureFlags.EnableKeyShareOnInvite, true) + } + + val presenter = createDefaultInvitePeoplePresenter( + coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), + inviteUserResult = inviteUserResult, + matrixClient = FakeMatrixClient(encryptionService = encryptionService), + featureFlagService = featureFlagService + ) + presenter.test { + val initialState = awaitItem() + skipItems(1) + + // When we toggle a user not in the list, they are added, and we fetch their identity. + initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(alice)) + delay(100) + awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(bob)) + delay(100) + awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(charlie)) + delay(100) + + // If we do not have their identity cached, or fail to fetch it, we should mark them as unknown. + awaitItemAsDefault().run { + assertThat(selectedUsers).containsExactly(alice, bob, charlie) + eventSink(InvitePeopleEvents.SendInvites) + } + + getUserIdentityResult.assertions().isCalledExactly(3).withSequence( + listOf(value(alice.userId)), + listOf(value(bob.userId)), + listOf(value(charlie.userId)) + ) + + // When we then try to invite these users, we should prompt for confirmation first. + awaitItemAsDefault().run { + assertThat(sendInvitesAction).isInstanceOf(ConfirmingUnknownUserInvitation::class.java) + assertThat(canInvite).isTrue() + eventSink(InvitePeopleEvents.SendInvites) + } + + delay(1_000) + inviteUserResult.assertions().isCalledExactly(3).withSequence( + listOf(value(alice.userId)), + listOf(value(bob.userId)), + listOf(value(charlie.userId)) + ) + + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - selecting remove on confirmation prompt unselects unknown users`() = runTest { + val alice = aMatrixUser("@alice:example.com") + val bob = aMatrixUser("@bob:example.com") + val charlie = aMatrixUser("@charlie:example.com") + + val repository = FakeUserRepository() + + val getUserIdentityResult = lambdaRecorder> { userId -> + when (userId.value) { + alice.userId.value -> Result.success(IdentityState.Pinned) + bob.userId.value -> Result.success(null) + else -> Result.failure(AN_EXCEPTION) + } + } + + val encryptionService = FakeEncryptionService( + getUserIdentityResult = getUserIdentityResult + ) + val featureFlagService = FakeFeatureFlagService().apply { + setFeatureEnabled(FeatureFlags.EnableKeyShareOnInvite, true) + } + + val presenter = createDefaultInvitePeoplePresenter( + userRepository = repository, + coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), + matrixClient = FakeMatrixClient(encryptionService = encryptionService), + featureFlagService = featureFlagService + ) + presenter.test { + val initialState = awaitItemAsDefault() + skipItems(1) + + // When we toggle a user not in the list, they are added, and we fetch their identity. + initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(alice)) + delay(100) + awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(bob)) + delay(100) + awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(charlie)) + delay(100) + + // And the search is matching Alice and Bob + initialState.searchQuery.setTextAndPlaceCursorAtEnd("some query") + assertThat(repository.providedQuery).isEqualTo("some query") + repository.emitState( + UserSearchResultState( + results = listOf(UserSearchResult(alice), UserSearchResult(bob)), + isSearching = true + ) + ) + skipItems(3) + + awaitItemAsDefault().run { + assertThat(selectedUsers).containsExactly(alice, bob, charlie) + + // Both Alice and Bob are selected in searchResults + assertThat( + searchResults.users().map { Pair(it.matrixUser, it.isSelected) } + ).containsExactly(Pair(alice, true), Pair(bob, true)) + + eventSink(InvitePeopleEvents.SendInvites) + } + + getUserIdentityResult.assertions().isCalledExactly(3).withSequence( + listOf(value(alice.userId)), + listOf(value(bob.userId)), + listOf(value(charlie.userId)) + ) + + // When we then try to invite these user, we should prompt for confirmation first. + awaitItemAsDefault().run { + assertThat(sendInvitesAction).isInstanceOf(ConfirmingUnknownUserInvitation::class.java) + assertThat(canInvite).isTrue() + eventSink(DefaultInvitePeopleEvents.RemoveUnknownUsers) + } + + // Selecting "remove" should remove all unknown users, but keeps those who are known. + (awaitLastSequentialItem() as DefaultInvitePeopleState).run { + assertThat(sendInvitesAction.isUninitialized()).isTrue() + assertThat(selectedUsers).containsExactly(alice) + + // Bob is no longer selected in searchResults + assertThat( + searchResults.users().map { Pair(it.matrixUser, it.isSelected) } + ).containsExactly(Pair(alice, true), Pair(bob, false)) + } + } + } + + @Test + fun `present - dismissing confirmation prompt does not affect selection`() = runTest { + val alice = aMatrixUser("@alice:example.com") + val bob = aMatrixUser("@bob:example.com") + val charlie = aMatrixUser("@charlie:example.com") + + val getUserIdentityResult = lambdaRecorder> { userId -> + when (userId.value) { + alice.userId.value -> Result.success(IdentityState.Pinned) + bob.userId.value -> Result.success(null) + else -> Result.failure(AN_EXCEPTION) + } + } + + val encryptionService = FakeEncryptionService( + getUserIdentityResult = getUserIdentityResult + ) + val featureFlagService = FakeFeatureFlagService().apply { + setFeatureEnabled(FeatureFlags.EnableKeyShareOnInvite, true) + } + + val presenter = createDefaultInvitePeoplePresenter( + coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), + matrixClient = FakeMatrixClient(encryptionService = encryptionService), + featureFlagService = featureFlagService + ) + presenter.test { + val initialState = awaitItem() + skipItems(1) + + // When we toggle a user not in the list, they are added, and we fetch their identity. + initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(alice)) + delay(100) + awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(bob)) + delay(100) + awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(charlie)) + delay(100) + + awaitItemAsDefault().run { + assertThat(selectedUsers).containsExactly(alice, bob, charlie) + eventSink(InvitePeopleEvents.SendInvites) + } + + getUserIdentityResult.assertions().isCalledExactly(3).withSequence( + listOf(value(alice.userId)), + listOf(value(bob.userId)), + listOf(value(charlie.userId)) + ) + + // When we then try to invite these user, we should prompt for confirmation first. + awaitItemAsDefault().run { + assertThat(sendInvitesAction).isInstanceOf(ConfirmingUnknownUserInvitation::class.java) + assertThat(canInvite).isTrue() + eventSink(DefaultInvitePeopleEvents.DismissUnknownUsersModal) + } + + // Dismissing should not modify the selection at all + (awaitLastSequentialItem() as DefaultInvitePeopleState).run { + assertThat(sendInvitesAction.isUninitialized()).isTrue() + assertThat(selectedUsers).containsExactly(alice, bob, charlie) + } + } + } + private suspend fun FakeUserRepository.emitStateWithUsers( users: List, isSearching: Boolean = false @@ -646,6 +878,7 @@ fun TestScope.createDefaultInvitePeoplePresenter( userRepository: UserRepository = FakeUserRepository(), coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(), appErrorStateService: AppErrorStateService = FakeAppErrorStateService(), + featureFlagService: FeatureFlagService = FakeFeatureFlagService(), matrixClient: MatrixClient = FakeMatrixClient(), ): DefaultInvitePeoplePresenter { return DefaultInvitePeoplePresenter( @@ -655,6 +888,7 @@ fun TestScope.createDefaultInvitePeoplePresenter( coroutineDispatchers = coroutineDispatchers, sessionCoroutineScope = backgroundScope, appErrorStateService = appErrorStateService, + featureFlagService = featureFlagService, matrixClient = matrixClient, ) } diff --git a/features/joinroom/impl/src/main/res/values-be/translations.xml b/features/joinroom/impl/src/main/res/values-be/translations.xml index 1986a761b0..f166186a1b 100644 --- a/features/joinroom/impl/src/main/res/values-be/translations.xml +++ b/features/joinroom/impl/src/main/res/values-be/translations.xml @@ -1,5 +1,6 @@ + "Адхіліць і заблакіраваць" "Далучыцца" "Націсніце, каб далучыцца" "%1$s пакуль не падтрымлівае прасторы. Вы можаце атрымаць доступ да прастор праз вэб-старонку." diff --git a/features/joinroom/impl/src/main/res/values-ja/translations.xml b/features/joinroom/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..f5bb2e3b66 --- /dev/null +++ b/features/joinroom/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,34 @@ + + + "%1$s があなたを追放しました。" + "追放されました" + "理由: %1$s" + "リクエストをキャンセル" + "キャンセルします" + "このルームへの参加のリクエストを本当にキャンセルしますか?" + "参加のリクエストをキャンセル" + "拒否してブロックする" + "本当にこのルームへの参加の招待を拒否しますか?%1$s は、あなたと会話することやルームに招待することができなくなります。" + "招待を拒否してブロック" + "拒否してブロック" + "参加に失敗" + "制限付きアクセスまたは招待制です。" + "忘れる" + "参加するには招待が必要です" + "以下のユーザーからの招待" + "参加" + "参加するには、招待またはスペースのメンバーである必要があります。" + "参加をリクエスト" + "文字数制限 %1$d/%2$d 字" + "メッセージ (任意)" + "リクエストが承認された場合はルームへの招待が届きます。" + "参加リクエストを送信しました" + "ルームのプレビューを表示できません。サーバーまたはネットワークの問題の可能性があります。" + "ルームのプレビューを表示できません" + "%1$s はスペースに対応していません。Webからアクセスすることができます。" + "まだスペースに対応していません" + "下のボタンを押すとルーム管理者に通知が届きます。承認の後、会話に参加することができます。" + "過去のメッセージを表示するには、このルームのメンバーである必要があります。" + "ルームに参加しますか?" + "プレビューは利用できません" + diff --git a/features/joinroom/impl/src/main/res/values-vi/translations.xml b/features/joinroom/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..cb9258b308 --- /dev/null +++ b/features/joinroom/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,19 @@ + + + "Hủy yêu cầu" + "Có, hủy" + "Bạn có chắc chắn muốn hủy yêu cầu tham gia phòng này không?" + "Hủy yêu cầu tham gia" + "Có, từ chối & chặn" + "Bạn có chắc muốn từ chối lời mời tham gia phòng này không? Điều này cũng sẽ ngăn %1$s liên hệ với bạn hoặc mời bạn vào các phòng." + "Từ chối lời mời và chặn" + "Từ chối và chặn" + "Được mời bởi" + "Tham gia" + "Số ký tự cho phép: %1$d / %2$d" + "Lời nhắn (tùy chọn)" + "Bạn sẽ nhận được lời mời tham gia phòng nếu yêu cầu của bạn được chấp nhận." + "Yêu cầu tham gia đã được gửi" + "Không thể hiển thị bản xem trước của phòng. Có thể do lỗi mạng hoặc máy chủ." + "Không thể hiển thị bản xem trước của phòng này" + diff --git a/features/joinroom/impl/src/main/res/values-zh/translations.xml b/features/joinroom/impl/src/main/res/values-zh/translations.xml index 7bea362c81..d38f9d3427 100644 --- a/features/joinroom/impl/src/main/res/values-zh/translations.xml +++ b/features/joinroom/impl/src/main/res/values-zh/translations.xml @@ -1,14 +1,14 @@ - "您已被禁止访问%1$s。" - "你已被禁止访问" + "您已被 %1$s 封禁。" + "你已被此房间封禁" "理由:%1$s。" "取消请求" "是的,取消" "您确定要取消加入此房间的请求吗?" "取消加入申请" "是的,拒绝并屏蔽" - "您确定要拒绝加入此房间的邀请吗?这也将阻止%1$s 与您联系或邀请您加入房间。" + "您确定要拒绝加入此房间的邀请吗?这也将阻止 %1$s 与您联系或邀请您加入房间。" "拒绝邀请并屏蔽" "拒绝并屏蔽" "加入失败" @@ -29,6 +29,6 @@ "空间尚不支持" "点击下面的按钮,系统将通知聊天室管理员。获得批准后将能够加入对话。" "只有聊天室成员才能查看消息历史记录。" - "想加入这个聊天室吗?" + "想加入此聊天室吗?" "预览不可用" diff --git a/features/knockrequests/impl/src/main/res/values-ja/translations.xml b/features/knockrequests/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..d08d4f3c20 --- /dev/null +++ b/features/knockrequests/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,35 @@ + + + "すべて承認" + "本当にすべての参加リクエストを承認しますか?" + "すべてのリクエストを承認" + "すべて承認" + "リクエストの一部を承認できませんでした。もう一度試しますか?" + "リクエストの承認に一部失敗" + "すべてのリクエストを承認中" + "リクエストを承認できませんでした。もう一度試しますか?" + "リクエストの承認に失敗" + "リクエストを承認中" + "拒否して追放する" + "本当に %1$s を拒否して追放しますか?このユーザーが再度リクエストを送信することはできなくなります。" + "拒否してアクセスから追放" + "拒否してアクセスから追放中" + "拒否する" + "本当に %1$s の参加リクエストを拒否しますか?" + "アクセスを拒否" + "拒否と追放" + "このリクエストを拒否できません。もう一度試しますか?" + "リクエストの拒否に失敗" + "参加リクエストを拒否中" + "ルームへの参加リクエストがある場合は、ここに表示されます。" + "参加リクエストがありません" + "参加リクエストを読み込み中" + "参加のリクエスト" + + "%1$s 他 %2$d 人がルーム参加を希望" + + "すべて表示" + "承諾" + "%1$s がこのルームの参加を要求しています" + "表示" + diff --git a/features/knockrequests/impl/src/main/res/values-vi/translations.xml b/features/knockrequests/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..a80aa6fbb8 --- /dev/null +++ b/features/knockrequests/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,29 @@ + + + "Có, chấp nhận tất cả" + "Bạn có chắc chắn muốn chấp nhận tất cả các yêu cầu tham gia không?" + "Chấp nhận tất cả các yêu cầu" + "Chấp nhận tất cả" + "Không thể chấp nhận tất cả yêu cầu. Bạn có muốn thử lại không?" + "Chấp nhận tất cả yêu cầu thất bại" + "Đang duyệt tất cả yêu cầu tham gia" + "Không thể chấp nhận yêu cầu này. Bạn có muốn thử lại không?" + "Chấp nhận yêu cầu thất bại" + "Đang duyệt yêu cầu tham gia" + "Có, từ chối và cấm" + "Bạn có chắc muốn từ chối và cấm %1$s không? Người dùng này sẽ không thể yêu cầu tham gia phòng này nữa" + "Từ chối và cấm truy cập" + "Đang từ chối và chặn truy cập" + "Có, từ chối" + "Bạn có chắc muốn từ chối yêu cầu tham gia phòng của %1$s không?" + "Từ chối truy cập" + "Từ chối và chặn" + "Không thể từ chối yêu cầu. Bạn có muốn thử lại không?" + "Từ chối yêu cầu thất bại" + "Đang từ chối yêu cầu tham gia" + "Khi ai đó xin vào phòng, bạn sẽ thấy yêu cầu ở đây." + "Không có yêu cầu tham gia nào đang chờ xử lý" + "Đang tải các yêu cầu tham gia…" + "Đồng ý" + "Xem" + diff --git a/features/knockrequests/impl/src/main/res/values-zh/translations.xml b/features/knockrequests/impl/src/main/res/values-zh/translations.xml index f568ee1120..08718dfc5a 100644 --- a/features/knockrequests/impl/src/main/res/values-zh/translations.xml +++ b/features/knockrequests/impl/src/main/res/values-zh/translations.xml @@ -30,6 +30,6 @@ "查看全部" "接受" - "%1$s想加入这个房间" + "%1$s 想加入此房间" "查看" diff --git a/features/leaveroom/api/src/main/res/values-ja/translations.xml b/features/leaveroom/api/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..73c1508bd2 --- /dev/null +++ b/features/leaveroom/api/src/main/res/values-ja/translations.xml @@ -0,0 +1,10 @@ + + + "本当にこの会話を退出しますか?この会話は非公開で、再度参加するには招待が必要です。" + "本当にこのルームを退出しますか?あなたが最後の一人であり、このルームには誰も参加することができなくなります。" + "ルームから退出してもよいですか? このルームは非公開のため、参加しなおすには改めて招待される必要があります。" + "所有者を選択" + "あなたがこのルームの唯一の所有者です。退出する前に所有権を他のユーザーへ譲与する必要があります。" + "所有権の譲与" + "本当にこのルームを退出しますか?" + diff --git a/features/leaveroom/api/src/main/res/values-vi/translations.xml b/features/leaveroom/api/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..d25a7e34b2 --- /dev/null +++ b/features/leaveroom/api/src/main/res/values-vi/translations.xml @@ -0,0 +1,7 @@ + + + "Bạn có chắc chắn muốn rời khỏi cuộc trò chuyện này không? Cuộc trò chuyện này không công khai và bạn sẽ không thể tham gia lại nếu không được mời." + "Bạn có chắc chắn muốn rời khỏi phòng này không? Bạn là người duy nhất ở đây. Nếu bạn rời đi, sẽ không ai có thể tham gia nữa, kể cả bạn." + "Bạn có chắc chắn muốn rời khỏi phòng này không? Phòng này không công khai và bạn sẽ không thể tham gia lại nếu không có lời mời." + "Bạn có chắc chắn muốn rời khỏi phòng không?" + diff --git a/features/linknewdevice/impl/src/main/res/values-it/translations.xml b/features/linknewdevice/impl/src/main/res/values-it/translations.xml index 0cf548c19b..9a6476823a 100644 --- a/features/linknewdevice/impl/src/main/res/values-it/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-it/translations.xml @@ -1,16 +1,33 @@ "Scansiona il codice QR" + "Apri %1$s su un laptop o un computer desktop" "Scansiona il codice QR con questo dispositivo" "Pronto per la scansione" + "Apri %1$s su un computer desktop per ottenere il codice QR" + "I numeri non corrispondono" + "Inserisci il codice a 2 cifre" + "Questo verificherà che la connessione con l\'altro dispositivo sia sicura." + "Inserisci il numero visualizzato sull\'altro dispositivo" "Il tuo fornitore di account non supporta %1$s." "%1$s non supportato" + "Il tuo provider di account non supporta l\'accesso a un nuovo dispositivo tramite codice QR." "Codice QR non supportato" "L\'accesso è stato annullato sull\'altro dispositivo." "Richiesta di accesso annullata" "L\'accesso è scaduto. Riprova." "L\'accesso non è stato completato in tempo" + "Apri %1$s sull\'altro dispositivo" "Seleziona %1$s" + "“Accedi con codice QR”" + "Scansiona il codice QR qui riportato con l\'altro dispositivo" + "Apri %1$s sull\'altro dispositivo" + "Computer desktop" + "Caricamento codice QR in corso…" + "Dispositivo mobile" + "Che tipo di dispositivo desideri collegare?" + "Prova di nuovo e assicurati di aver inserito correttamente il codice a 2 cifre. Se i numeri continuano a non corrispondere, contatta il gestore del tuo account." + "I numeri non corrispondono" "Non è stato possibile stabilire una connessione sicura con il nuovo dispositivo. I tuoi dispositivi esistenti sono ancora al sicuro e non devi preoccuparti di loro." "E adesso?" "Prova ad accedere di nuovo con un codice QR nel caso si sia verificato un problema di rete." @@ -21,6 +38,8 @@ "Richiesta di accesso annullata" "L\'accesso è stato rifiutato sull\'altro dispositivo." "Accesso rifiutato" + "Non devi fare altro." + "L\'altro tuo dispositivo è già connesso" "L\'accesso è scaduto. Riprova." "L\'accesso non è stato completato in tempo" "L\'altro dispositivo non supporta l\'accesso a %s con un codice QR. diff --git a/features/linknewdevice/impl/src/main/res/values-ja/translations.xml b/features/linknewdevice/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..6cfd5baf84 --- /dev/null +++ b/features/linknewdevice/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,57 @@ + + + "QRコードを読み取り" + "%1$s をコンピュータで開いてください" + "この端末でQRコードを読み取る" + "読み取る" + "%1$s をコンピュータで開き、QRコードを表示してください" + "数字が一致しません" + "2桁の数字を入力してください" + "他の端末との接続が安全であることを確認します。" + "一方の端末で表示される数字を入力してください" + "アカウント提供元が %1$s に対応していません。" + "%1$s に非対応" + "あなたのアカウント提供元は、QRコードによる追加のサインインに対応していません。" + "QRコードに非対応" + "もう一方の端末がサインインをキャンセルしました" + "サインインのリクエストがキャンセルされました" + "サインインが無効です。もう一度試してください。" + "サインインが時間内に完了しませんでした" + "%1$s を他の端末で開いてください" + "%1$s を選択してください" + "\"QRコードでサインイン\"" + "表示されているQRコードを一方の端末で読み取ってください" + "%1$s を他の端末で開いてください" + "コンピュータ" + "QRコードを読み込み中…" + "モバイル端末" + "どのような端末を使用してサインインしますか?" + "入力した2桁の数字が正しいことを確認し、再度試してください。問題が継続する場合はアカウント提供元に問い合わせてください。" + "数字が一致しません" + "新しい端末で安全な通信を確立できませんでした。既存の端末は安全な状態を維持しています。" + "どうしますか?" + "ネットワークの問題の可能性があるため、再度QRコードでログインを試してください。" + "同様の問題が発生する場合は、異なるWi-Fiやモバイルデータ通信を試してください" + "問題が解決しない場合は、手動でサインインしてください" + "接続が安全ではありません" + "もう一方の端末がサインインをキャンセルしました" + "サインインのリクエストがキャンセルされました" + "もう一方の端末でサインインを拒否されました" + "サインインを拒否" + "他には何もする必要はありません。" + "他の端末で既にサインインしています" + "サインインが無効です。もう一度試してください。" + "サインインが時間内に完了しませんでした" + "QRコードを使用した %s へのサインインに他の端末が対応していません。 + +異なる端末でQRコードを読み取るか、手動でサインインしてください。" + "QRコードに非対応" + "アカウント提供元が %1$s に対応していません。" + "%1$s に非対応" + "もう一方の端末に表示されているQRコードを使用してください" + "もう一度やり直してください" + "QRコードが間違っています" + "続行するには、%1$s にカメラの使用を許可する必要があります。" + "QRコードを読み取るため、カメラへのアクセスを許可" + "予期せぬ問題が発生しました。もう一度試してください。" + diff --git a/features/linknewdevice/impl/src/main/res/values-vi/translations.xml b/features/linknewdevice/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..5e7709f814 --- /dev/null +++ b/features/linknewdevice/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,4 @@ + + + "Thử lại" + diff --git a/features/linknewdevice/impl/src/main/res/values-zh/translations.xml b/features/linknewdevice/impl/src/main/res/values-zh/translations.xml index 843395fab7..10ae1ae7a7 100644 --- a/features/linknewdevice/impl/src/main/res/values-zh/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-zh/translations.xml @@ -23,7 +23,7 @@ "请用另一台设备扫描此处显示的二维码" "在另一台设备上打开 %1$s" "台式计算机" - "正在加载 QR 码…" + "正在加载二维码…" "移动设备" "您想连接哪种类型的设备?" "请重试,并确保您已正确输入两位验证码。如果验证码仍然不匹配,请联系您的账户提供商。" diff --git a/features/location/impl/src/main/res/values-cs/translations.xml b/features/location/impl/src/main/res/values-cs/translations.xml new file mode 100644 index 0000000000..99deeba029 --- /dev/null +++ b/features/location/impl/src/main/res/values-cs/translations.xml @@ -0,0 +1,4 @@ + + + "Zvolte, jak dlouho chcete sdílet svou aktuální polohu." + diff --git a/features/location/impl/src/main/res/values-fr/translations.xml b/features/location/impl/src/main/res/values-fr/translations.xml index 46689488e1..043c180dbc 100644 --- a/features/location/impl/src/main/res/values-fr/translations.xml +++ b/features/location/impl/src/main/res/values-fr/translations.xml @@ -1,4 +1,5 @@ + "Votre historique de localisation en direct sera enregistré dans le salon et visible par les membres après la fin de la session." "Choisissez la durée pendant laquelle vous partagerez votre position en direct." diff --git a/features/location/impl/src/main/res/values-hu/translations.xml b/features/location/impl/src/main/res/values-hu/translations.xml new file mode 100644 index 0000000000..b89965485f --- /dev/null +++ b/features/location/impl/src/main/res/values-hu/translations.xml @@ -0,0 +1,5 @@ + + + "Az élő helymeghatározás története a szobában lesz tárolva, és a munkamenet befejezése után is látható marad a tagok számára." + "Válassza ki, mennyi ideig szeretné megosztani az aktuális tartózkodási helyét." + diff --git a/features/location/impl/src/main/res/values-it/translations.xml b/features/location/impl/src/main/res/values-it/translations.xml new file mode 100644 index 0000000000..235a9eba4a --- /dev/null +++ b/features/location/impl/src/main/res/values-it/translations.xml @@ -0,0 +1,5 @@ + + + "La cronologia delle tue posizioni in tempo reale verrà archiviata nella stanza e sarà visibile ai membri al termine della sessione." + "Scegli per quanto tempo condividere la tua posizione in tempo reale." + diff --git a/features/location/impl/src/main/res/values-ja/translations.xml b/features/location/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..971693ef11 --- /dev/null +++ b/features/location/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,5 @@ + + + "ライブ位置情報の履歴はルームに保管され、メンバーは後から確認することもできます。" + "ライブ位置情報を共有する期間を選択してください。" + diff --git a/features/location/impl/src/main/res/values-ko/translations.xml b/features/location/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..e283d20b9c --- /dev/null +++ b/features/location/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,5 @@ + + + "실시간 위치 기록은 대화방에 저장되며, 공유 종료 후에도 멤버들이 확인할 수 있습니다." + "실시간 위치를 공유할 시간을 선택해 주세요." + diff --git a/features/location/impl/src/main/res/values-ru/translations.xml b/features/location/impl/src/main/res/values-ru/translations.xml new file mode 100644 index 0000000000..3c496f1d39 --- /dev/null +++ b/features/location/impl/src/main/res/values-ru/translations.xml @@ -0,0 +1,5 @@ + + + "История вашего местоположения в режиме реального времени будет сохранена в комнате и станет доступна участникам после окончания сессии." + "Выберите, как долго вы будете делиться своим местоположением в режиме реального времени." + diff --git a/features/location/impl/src/main/res/values/localazy.xml b/features/location/impl/src/main/res/values/localazy.xml index 04538049db..ac2ff4b2a0 100644 --- a/features/location/impl/src/main/res/values/localazy.xml +++ b/features/location/impl/src/main/res/values/localazy.xml @@ -1,4 +1,5 @@ + "Your live location history will be stored in the room and visible to members after the session ends." "Choose how long to share your live location." diff --git a/features/lockscreen/impl/src/main/res/values-cs/translations.xml b/features/lockscreen/impl/src/main/res/values-cs/translations.xml index fce1142f2c..f54d16f42b 100644 --- a/features/lockscreen/impl/src/main/res/values-cs/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-cs/translations.xml @@ -23,7 +23,7 @@ Vyberte si něco zapamatovatelného. Pokud tento kód PIN zapomenete, budete z a "Zadejte stejný PIN dvakrát" "PIN kódy se neshodují." "Abyste mohli pokračovat, budete se muset znovu přihlásit a vytvořit nový PIN" - "Jste odhlášeni" + "Toto zařízení se odstraňuje" "Máte %1$d pokus pro odemknutí" "Máte %1$d pokusy pro odemknutí" @@ -36,5 +36,5 @@ Vyberte si něco zapamatovatelného. Pokud tento kód PIN zapomenete, budete z a "Použijte biometrické údaje" "Použít PIN" - "Odhlašování…" + "Odebírání zařízení…" diff --git a/features/lockscreen/impl/src/main/res/values-hu/translations.xml b/features/lockscreen/impl/src/main/res/values-hu/translations.xml index 3a65239697..470324b2ba 100644 --- a/features/lockscreen/impl/src/main/res/values-hu/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-hu/translations.xml @@ -23,7 +23,7 @@ Válasszon valami megjegyezhetőt. Ha elfelejti a PIN-kódot, akkor ki lesz jele "Adja meg a PIN-kódját kétszer" "A PIN-kódok nem egyeznek" "A folytatáshoz újra be kell jelentkeznie, és létre kell hoznia egy új PIN-kódot" - "Kijelentkeztetésre kerül" + "Ez az eszköz eltávolításra kerül" "%1$d próbálkozása van a feloldáshoz" "%1$d próbálkozása van a feloldáshoz" @@ -34,5 +34,5 @@ Válasszon valami megjegyezhetőt. Ha elfelejti a PIN-kódot, akkor ki lesz jele "Biometrikus adatok használata" "PIN-kód használata" - "Kijelentkezés…" + "Eszköz eltávolítása…" diff --git a/features/lockscreen/impl/src/main/res/values-it/translations.xml b/features/lockscreen/impl/src/main/res/values-it/translations.xml index 514d2461a2..5f9c82a34a 100644 --- a/features/lockscreen/impl/src/main/res/values-it/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-it/translations.xml @@ -23,7 +23,7 @@ Scegli un PIN facile da ricordare. Se lo dimentichi, verrai disconnesso dall’a "Inserisci lo stesso PIN due volte" "I PIN non corrispondono" "Dovrai effettuare nuovamente l\'accesso e creare un nuovo PIN per procedere" - "Stai per essere disconnesso" + "Questo dispositivo verrà rimosso" "Hai %1$d tentativo di sblocco" "Hai %1$d tentativi di sblocco" @@ -34,5 +34,5 @@ Scegli un PIN facile da ricordare. Se lo dimentichi, verrai disconnesso dall’a "Usa la biometria" "Usa il PIN" - "Disconnessione in corso…" + "Rimozione del dispositivo…" diff --git a/features/lockscreen/impl/src/main/res/values-ja/translations.xml b/features/lockscreen/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..982c4574c8 --- /dev/null +++ b/features/lockscreen/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,36 @@ + + + "生体認証" + "生体認証で解除" + "生体認証を使用" + "生体認証を使用しますか?" + "PINをお忘れですか?" + "PINを変更" + "生体認証を使用" + "PINを削除" + "本当にPINを削除しますか?" + "PINを削除しますか?" + "%1$sを使用" + "PINを使用する" + "素早い認証のために %1$s を常に使用" + "PINを選択" + "PINの確認" + "チャットのセキュリティを強化するため、%1$s を保護しましょう。 + +覚えやすいPINを設定してください。PINを忘れると、アプリにログインできなくなります。" + "セキュリティ上の理由により、入力された内容をPINとして使用できません。" + "別のPINを使用してください" + "同一のPINを2回入力してください" + "PINが一致しません" + "再度ログインし、PINを再設定する必要があります" + "端末を削除しようとしています" + + "%1$d 回試すことができます" + + + "PINが間違っています。あと %1$d 回試すことができます。" + + "生体認証を使用" + "PINを使用" + "削除中…" + diff --git a/features/lockscreen/impl/src/main/res/values-ru/translations.xml b/features/lockscreen/impl/src/main/res/values-ru/translations.xml index d0441fb253..cb71dfc88c 100644 --- a/features/lockscreen/impl/src/main/res/values-ru/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-ru/translations.xml @@ -36,5 +36,5 @@ "Использовать биометрию" "Использовать PIN-код" - "Выполняется выход…" + "Удаление устройства…" diff --git a/features/lockscreen/impl/src/main/res/values-sv/translations.xml b/features/lockscreen/impl/src/main/res/values-sv/translations.xml index a559e4c909..021fa67fb3 100644 --- a/features/lockscreen/impl/src/main/res/values-sv/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-sv/translations.xml @@ -34,5 +34,5 @@ Välj något minnesvärt. Om du glömmer den här PIN-koden loggas du ut från a "Använd biometri" "Använd PIN-kod" - "Loggar ut …" + "Tar bort enhet …" diff --git a/features/lockscreen/impl/src/main/res/values-vi/translations.xml b/features/lockscreen/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..2a177b843b --- /dev/null +++ b/features/lockscreen/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,35 @@ + + + "xác thực sinh trắc học" + "mở khóa sinh trắc học" + "Mở khóa bằng sinh trắc học" + "Quên mã PIN rồi à?" + "Thay đổi mã PIN" + "Cho phép mở khóa bằng sinh trắc học" + "Xóa mã PIN" + "Bạn có chắc chắn muốn xóa mã PIN không?" + "Xóa mã PIN?" + "Cho phép %1$s" + "Tôi thích dùng mã PIN hơn." + "Dùng %1$s để mở khóa ứng dụng nhanh hơn." + "Chọn mã PIN" + "Xác nhận mã PIN" + "Khóa %1$s để tăng cường bảo mật cho các cuộc trò chuyện của bạn. + +Chọn một mã dễ nhớ. Nếu quên PIN này, bạn sẽ bị đăng xuất khỏi ứng dụng." + "Vì lý do bảo mật, bạn không thể chọn mã này làm mã PIN của mình." + "Chọn mã PIN khác" + "Vui lòng nhập cùng một mã PIN hai lần." + "Mã PIN không khớp" + "Đăng nhập lại và tạo PIN mới để tiếp tục." + "Thiết bị này đang được gỡ bỏ" + + "Bạn còn %1$d lần thử để mở khóa" + + + "PIN không đúng. Còn %1$d lần thử" + + "Sử dụng sinh trắc học" + "Sử dụng mã PIN" + "Đang gỡ thiết bị…" + diff --git a/features/lockscreen/impl/src/main/res/values-zh/translations.xml b/features/lockscreen/impl/src/main/res/values-zh/translations.xml index d3633e4af5..defe7a0e32 100644 --- a/features/lockscreen/impl/src/main/res/values-zh/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-zh/translations.xml @@ -32,5 +32,5 @@ "使用生物识别" "使用 PIN 码" - "正在登出…" + "正在删除设备……" diff --git a/features/login/impl/build.gradle.kts b/features/login/impl/build.gradle.kts index f19ba61783..12af922cbe 100644 --- a/features/login/impl/build.gradle.kts +++ b/features/login/impl/build.gradle.kts @@ -55,6 +55,7 @@ setupDependencyInjection() dependencies { implementation(projects.appconfig) implementation(projects.features.enterprise.api) + implementation(projects.features.preferences.api) implementation(projects.features.rageshake.api) implementation(projects.libraries.core) implementation(projects.libraries.androidutils) @@ -79,6 +80,7 @@ dependencies { testCommonDependencies(libs, true) testImplementation(projects.features.login.test) testImplementation(projects.features.enterprise.test) + testImplementation(projects.features.preferences.test) testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.oidc.test) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt index 928d98c244..fb384d505a 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt @@ -22,6 +22,7 @@ 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 com.bumble.appyx.navmodel.backstack.operation.replace import com.bumble.appyx.navmodel.backstack.operation.singleTop import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.Assisted @@ -30,14 +31,17 @@ import io.element.android.annotations.ContributesNode import io.element.android.compound.theme.ElementTheme import io.element.android.features.login.api.LoginEntryPoint import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource +import io.element.android.features.login.impl.classic.ElementClassicConnection import io.element.android.features.login.impl.qrcode.QrCodeLoginFlowNode import io.element.android.features.login.impl.screens.changeaccountprovider.ChangeAccountProviderNode import io.element.android.features.login.impl.screens.chooseaccountprovider.ChooseAccountProviderNode +import io.element.android.features.login.impl.screens.classic.ClassicFlowNode import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderNode import io.element.android.features.login.impl.screens.createaccount.CreateAccountNode import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordNode import io.element.android.features.login.impl.screens.onboarding.OnBoardingNode import io.element.android.features.login.impl.screens.searchaccountprovider.SearchAccountProviderNode +import io.element.android.features.preferences.api.PreferencesEntryPoint import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode @@ -63,9 +67,11 @@ class LoginFlowNode( private val oidcActionFlow: OidcActionFlow, @AppCoroutineScope private val appCoroutineScope: CoroutineScope, + private val elementClassicConnection: ElementClassicConnection, + private val preferencesEntryPoint: PreferencesEntryPoint, ) : BaseFlowNode( backstack = BackStack( - initialElement = NavTarget.OnBoarding, + initialElement = NavTarget.CheckClassicFlow, savedStateMap = buildContext.savedStateMap, ), buildContext = buildContext, @@ -103,11 +109,19 @@ class LoginFlowNode( sealed interface NavTarget : Parcelable { @Parcelize - data object OnBoarding : NavTarget + data object CheckClassicFlow : NavTarget + + @Parcelize + data class OnBoarding( + val showBackButton: Boolean, + ) : NavTarget @Parcelize data object QrCode : NavTarget + @Parcelize + data object AppDeveloperSettings : NavTarget + @Parcelize data class ConfirmAccountProvider( val isAccountCreation: Boolean, @@ -123,7 +137,9 @@ class LoginFlowNode( data object SearchAccountProvider : NavTarget @Parcelize - data object LoginPassword : NavTarget + data class LoginPassword( + val initialLogin: String = "", + ) : NavTarget @Parcelize data class CreateAccount(val url: String) : NavTarget @@ -131,7 +147,31 @@ class LoginFlowNode( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { - NavTarget.OnBoarding -> { + NavTarget.CheckClassicFlow -> { + val callback = object : ClassicFlowNode.Callback { + override fun navigateToOnBoarding(allowBackNavigation: Boolean) { + if (allowBackNavigation) { + backstack.push(NavTarget.OnBoarding(showBackButton = true)) + } else { + backstack.replace(NavTarget.OnBoarding(showBackButton = false)) + } + } + + override fun navigateToLoginPassword() { + backstack.push(NavTarget.LoginPassword()) + } + + override fun navigateToOidc(oidcDetails: OidcDetails) { + navigateToMas(oidcDetails) + } + + override fun navigateToCreateAccount(url: String) { + backstack.push(NavTarget.CreateAccount(url)) + } + } + createNode(buildContext, listOf(callback)) + } + is NavTarget.OnBoarding -> { val callback = object : OnBoardingNode.Callback { override fun navigateToSignUpFlow() { backstack.push( @@ -165,21 +205,42 @@ class LoginFlowNode( backstack.push(NavTarget.CreateAccount(url)) } + override fun navigateToDeveloperSettings() { + backstack.push(NavTarget.AppDeveloperSettings) + } + override fun navigateToLoginPassword() { - backstack.push(NavTarget.LoginPassword) + backstack.push(NavTarget.LoginPassword()) } override fun onDone() { - callback.onDone() + if (navTarget.showBackButton) { + backstack.pop() + } else { + callback.onDone() + } } } val params = inputs() val inputs = OnBoardingNode.Params( accountProvider = params.accountProvider, loginHint = params.loginHint, + showBackButton = navTarget.showBackButton, ) createNode(buildContext, listOf(callback, inputs)) } + NavTarget.AppDeveloperSettings -> { + val callback = object : PreferencesEntryPoint.DeveloperSettingsCallback { + override fun onDone() { + backstack.pop() + } + } + preferencesEntryPoint.createAppDeveloperSettingsNode( + parentNode = this, + buildContext = buildContext, + callback = callback, + ) + } NavTarget.ChooseAccountProvider -> { val callback = object : ChooseAccountProviderNode.Callback { override fun navigateToOidc(oidcDetails: OidcDetails) { @@ -191,7 +252,7 @@ class LoginFlowNode( } override fun navigateToLoginPassword() { - backstack.push(NavTarget.LoginPassword) + backstack.push(NavTarget.LoginPassword()) } } createNode(buildContext, listOf(callback)) @@ -218,7 +279,7 @@ class LoginFlowNode( } override fun navigateToLoginPassword() { - backstack.push(NavTarget.LoginPassword) + backstack.push(NavTarget.LoginPassword()) } override fun navigateToChangeAccountProvider() { @@ -257,8 +318,11 @@ class LoginFlowNode( createNode(buildContext, plugins = listOf(callback)) } - NavTarget.LoginPassword -> { - createNode(buildContext) + is NavTarget.LoginPassword -> { + val inputs = LoginPasswordNode.Inputs( + initialLogin = navTarget.initialLogin, + ) + createNode(buildContext, plugins = listOf(inputs)) } is NavTarget.CreateAccount -> { val inputs = CreateAccountNode.Inputs( @@ -280,6 +344,14 @@ class LoginFlowNode( override fun View(modifier: Modifier) { activity = requireNotNull(LocalActivity.current) darkTheme = !ElementTheme.isLightTheme + + DisposableEffect(Unit) { + elementClassicConnection.start() + onDispose { + elementClassicConnection.stop() + } + } + DisposableEffect(Unit) { onDispose { activity = null @@ -288,6 +360,6 @@ class LoginFlowNode( } } } - BackstackView() + BackstackView(transitionHandler = rememberLoginFlowTransitionHandler()) } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowTransitionHandler.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowTransitionHandler.kt new file mode 100644 index 0000000000..5486619e5d --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowTransitionHandler.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2026 Element Creations 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.login.impl + +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.Transition +import androidx.compose.animation.core.spring +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.navigation.transition.ModifierTransitionHandler +import com.bumble.appyx.core.navigation.transition.TransitionDescriptor +import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.Replace +import com.bumble.appyx.navmodel.backstack.transitionhandler.rememberBackstackFader +import com.bumble.appyx.navmodel.backstack.transitionhandler.rememberBackstackSlider +import io.element.android.libraries.architecture.appyx.rememberDelegateTransitionHandler + +/** + * A TransitionHandler that uses fade transition when OnBoarding is replacing the current screen, + * and slide transition for all other cases. + */ +private class LoginFlowTransitionHandler( + private val slider: ModifierTransitionHandler, + private val fader: ModifierTransitionHandler, +) : ModifierTransitionHandler() { + override fun createModifier( + modifier: Modifier, + transition: Transition, + descriptor: TransitionDescriptor + ): Modifier { + val useFader = descriptor.element is LoginFlowNode.NavTarget.OnBoarding && + descriptor.operation is Replace + val handler = if (useFader) fader else slider + return handler.createModifier(modifier, transition, descriptor) + } +} + +@Composable +fun rememberLoginFlowTransitionHandler(): ModifierTransitionHandler { + val slider = rememberBackstackSlider( + transitionSpec = { spring(stiffness = Spring.StiffnessMediumLow) }, + ) + val fader = rememberBackstackFader( + transitionSpec = { spring(stiffness = Spring.StiffnessMediumLow) }, + ) + return rememberDelegateTransitionHandler { + LoginFlowTransitionHandler(slider, fader) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/classic/ElementClassicConnection.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/classic/ElementClassicConnection.kt new file mode 100644 index 0000000000..c928c05239 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/classic/ElementClassicConnection.kt @@ -0,0 +1,423 @@ +/* + * Copyright (c) 2026 Element Creations 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.login.impl.classic + +import android.content.ComponentName +import android.content.Context.BIND_AUTO_CREATE +import android.content.Intent +import android.content.ServiceConnection +import android.graphics.Bitmap +import android.os.Bundle +import android.os.Handler +import android.os.IBinder +import android.os.Message +import android.os.Messenger +import android.os.RemoteException +import androidx.annotation.VisibleForTesting +import androidx.core.os.BundleCompat +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.SingleIn +import io.element.android.features.login.impl.BuildConfig +import io.element.android.libraries.androidutils.service.ServiceBinder +import io.element.android.libraries.core.log.logger.LoggerTag +import io.element.android.libraries.core.uri.ensureProtocol +import io.element.android.libraries.di.annotations.AppCoroutineScope +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.matrix.api.auth.ElementClassicSession +import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker +import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import io.element.android.libraries.matrix.api.core.UserId +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import timber.log.Timber + +interface ElementClassicConnection { + fun start() + fun stop() + fun requestSession() + val stateFlow: StateFlow +} + +sealed interface ElementClassicConnectionState { + object Idle : ElementClassicConnectionState + object ElementClassicNotFound : ElementClassicConnectionState + object ElementClassicReadyNoSession : ElementClassicConnectionState + data class ElementClassicReady( + val elementClassicSession: ElementClassicSession, + val displayName: String?, + val avatar: Bitmap?, + ) : ElementClassicConnectionState + + data class Error(val error: String) : ElementClassicConnectionState +} + +private val loggerTag = LoggerTag("ECConnection") + +@ContributesBinding(AppScope::class) +@SingleIn(AppScope::class) +class DefaultElementClassicConnection( + private val serviceBinder: ServiceBinder, + @AppCoroutineScope + private val coroutineScope: CoroutineScope, + private val matrixAuthenticationService: MatrixAuthenticationService, + private val homeServerLoginCompatibilityChecker: HomeServerLoginCompatibilityChecker, + private val featureFlagService: FeatureFlagService, +) : ElementClassicConnection { + // Messenger for communicating with the service. + private var messenger: Messenger? = null + + // Target we publish for external service to send messages to IncomingHandler. + private val incomingMessenger: Messenger = Messenger(IncomingHandler()) + + // Flag indicating whether we have called bind on the service. + private var bound: Boolean = false + + private val mutableStateFlow = MutableStateFlow(ElementClassicConnectionState.Idle) + override val stateFlow = mutableStateFlow.asStateFlow() + + private val elementClassicComponent = ComponentName( + BuildConfig.elementClassicPackage, + ELEMENT_CLASSIC_SERVICE_FULL_CLASS_NAME, + ) + + /** + * Class for interacting with the main interface of the service. + */ + private val serviceConnection = object : ServiceConnection { + override fun onServiceConnected(className: ComponentName, service: IBinder) { + Timber.tag(loggerTag.value).d("onServiceConnected") + // This is called when the connection with the service has been + // established, giving us the object we can use to + // interact with the service. We are communicating with the + // service using a Messenger, so here we get a client-side + // representation of that from the raw IBinder object. + messenger = Messenger(service) + bound = true + // Request the data as soon as possible + requestSession() + } + + override fun onServiceDisconnected(className: ComponentName) { + Timber.tag(loggerTag.value).d("onServiceDisconnected") + // This is called when the connection with the service has been + // unexpectedly disconnected—that is, its process crashed. + messenger = null + bound = false + } + } + + override fun start() { + Timber.tag(loggerTag.value).d("start()") + coroutineScope.launch { + if (!featureFlagService.isFeatureEnabled(FeatureFlags.SignInWithClassic)) { + Timber.tag(loggerTag.value).d("Login with Element Classic is disabled, not starting connection") + return@launch + } + // Establish a connection with the service. We use an explicit + // class name because there is no reason to be able to let other + // applications replace our component. + try { + val intentService = Intent() + intentService.setComponent(elementClassicComponent) + if (serviceBinder.bindService(intentService, serviceConnection, BIND_AUTO_CREATE)) { + Timber.tag(loggerTag.value).d("Binding returned true") + } else { + // This happens when the app is not installed + Timber.tag(loggerTag.value).d("Binding returned false") + emitState(ElementClassicConnectionState.ElementClassicNotFound) + } + } catch (e: SecurityException) { + Timber.tag(loggerTag.value).e(e, "Can't bind to Service") + emitState(ElementClassicConnectionState.Error(e.localizedMessage.orEmpty())) + } + } + } + + override fun stop() { + Timber.tag(loggerTag.value).d("stop(): Unbinding (bound=$bound)") + if (bound) { + // Detach our existing connection. + serviceBinder.unbindService(serviceConnection) + bound = false + } + coroutineScope.launch { + emitState(ElementClassicConnectionState.Idle) + } + } + + override fun requestSession() { + Timber.tag(loggerTag.value).d("requestSession()") + coroutineScope.launch { + if (!featureFlagService.isFeatureEnabled(FeatureFlags.SignInWithClassic)) { + Timber.tag(loggerTag.value).d("Login with Element Classic is disabled") + emitState(ElementClassicConnectionState.Error("The feature is disabled")) + return@launch + } + val finalMessenger = messenger + if (finalMessenger == null) { + Timber.tag(loggerTag.value).d("The messenger is null, can't request data") + // Do not emit error, else the regular on boarding flow will be displayed + } else { + try { + // Get the data + val msg = Message.obtain(null, MSG_GET_SESSION) + msg.replyTo = incomingMessenger + finalMessenger.send(msg) + } catch (e: RemoteException) { + // In this case the service has crashed before we could even + // do anything with it; we can count on soon being + // disconnected (and then reconnected if it can be restarted) + // so there is no need to do anything here. + Timber.tag(loggerTag.value).e(e, "RemoteException") + emitState(ElementClassicConnectionState.Error(e.localizedMessage.orEmpty())) + } + } + } + } + + private fun requestAvatar(userId: UserId) { + Timber.tag(loggerTag.value).d("requestAvatar()") + coroutineScope.launch { + val finalMessenger = messenger + if (finalMessenger == null) { + Timber.tag(loggerTag.value).w("The messenger is null, can't request extra data") + } else { + try { + // Get the data + val msg = Message.obtain(null, MSG_GET_AVATAR) + msg.data = Bundle().apply { + putString(KEY_USER_ID_STR, userId.value) + } + msg.replyTo = incomingMessenger + finalMessenger.send(msg) + } catch (e: RemoteException) { + // In this case the service has crashed before we could even + // do anything with it; we can count on soon being + // disconnected (and then reconnected if it can be restarted) + // so there is no need to do anything here. + Timber.tag(loggerTag.value).e(e, "RemoteException") + } + } + } + } + + /** + * Handler of incoming messages from service. + */ + @Suppress("DEPRECATION") + inner class IncomingHandler : Handler() { + override fun handleMessage(msg: Message) { + Timber.tag(loggerTag.value).d("IncomingHandler handling message ${msg.what}") + when (msg.what) { + MSG_GET_SESSION -> onSessionReceived(msg.data) + MSG_GET_AVATAR -> onAvatarReceived(msg.data) + else -> { + Timber.tag(loggerTag.value).w("Received unknown message ${msg.what}") + super.handleMessage(msg) + } + } + } + } + + @VisibleForTesting + fun onSessionReceived(data: Bundle) { + // The data must be extracted from the bundle before we launch the coroutine, else the bundle will be emptied + val state = data.toElementClassicConnectionState() + coroutineScope.launch { + val updatedState = ensureHomeserverIsSupported(state) + emitState(updatedState) + val userId = (updatedState as? ElementClassicConnectionState.ElementClassicReady)?.elementClassicSession?.userId + if (userId != null) { + // Step 2, request the avatar + requestAvatar(userId) + } + } + } + + @VisibleForTesting + fun onAvatarReceived(data: Bundle) { + val currentState = stateFlow.value + if (currentState is ElementClassicConnectionState.ElementClassicReady) { + // Check that the userId is still the same + val userId = data.getString(KEY_USER_ID_STR) + if (userId != currentState.elementClassicSession.userId.value) { + Timber.tag(loggerTag.value).w( + "Received profile data for userId $userId but current" + + " userId is ${currentState.elementClassicSession.userId}, ignoring" + ) + } else { + val avatar = BundleCompat.getParcelable(data, KEY_USER_AVATAR_PARCELABLE, Bitmap::class.java) + // If the avatar is identical to the current one, do not emit a new state to avoid unnecessary recompositions + // and blink on the avatar image + if (avatar == null || !avatar.sameAs(currentState.avatar)) { + val updatedState = currentState.copy( + avatar = avatar, + ) + coroutineScope.launch { + emitState(updatedState) + } + } + } + } else { + Timber.tag(loggerTag.value).w("Received profile data but current state is not ElementClassicReady: %s", currentState) + } + } + + private suspend fun ensureHomeserverIsSupported(state: ElementClassicConnectionState): ElementClassicConnectionState { + return if (state is ElementClassicConnectionState.ElementClassicReady) { + val elementXCanConnect = setOfNotNull( + // Try with the domain name first + state.elementClassicSession.userId.domainName?.ensureProtocol(), + // Then try with the resolved homeserver URL, if provided and distinct + state.elementClassicSession.homeserverUrl, + ).any { url -> + val isCompatible = homeServerLoginCompatibilityChecker.check(url) + .onFailure { + Timber.tag(loggerTag.value).w(it, "Failed to check compatibility with homeserver: $url") + } + .getOrNull() == true + if (isCompatible) { + Timber.tag(loggerTag.value).d("Found compatible homeserver URL: %s", url) + } else { + Timber.tag(loggerTag.value).d("Homeserver URL is not compatible: %s", url) + } + isCompatible + } + if (elementXCanConnect) { + state + } else { + Timber.tag(loggerTag.value).w("Cannot import session because the homeserver is not compatible with Element X") + ElementClassicConnectionState.Error("The homeserver is not compatible with Element X") + } + } else { + state + } + } + + private suspend fun emitState(state: ElementClassicConnectionState) { + when (state) { + is ElementClassicConnectionState.Error -> { + Timber.tag(loggerTag.value).w("Error: %s", state.error) + } + is ElementClassicConnectionState.ElementClassicReady -> { + Timber.tag(loggerTag.value).d("Ready state for user: %s", state.elementClassicSession.userId) + } + ElementClassicConnectionState.ElementClassicReadyNoSession -> { + Timber.tag(loggerTag.value).d("No session from Element Classic") + } + ElementClassicConnectionState.ElementClassicNotFound -> { + Timber.tag(loggerTag.value).d("Element Classic not found") + } + ElementClassicConnectionState.Idle -> { + Timber.tag(loggerTag.value).d("Idle") + } + } + // Also give the Element Classic session info to the MatrixAuthenticationService + matrixAuthenticationService.setElementClassicSession( + session = (state as? ElementClassicConnectionState.ElementClassicReady)?.elementClassicSession + ) + mutableStateFlow.emit(state) + } + + private fun Bundle.toElementClassicConnectionState(): ElementClassicConnectionState { + val error = getString(KEY_ERROR_STR) + return if (error != null) { + ElementClassicConnectionState.Error(error) + } else { + val userId = getString(KEY_USER_ID_STR)?.takeIf { it.isNotEmpty() }?.let(::UserId) + if (userId == null) { + ElementClassicConnectionState.ElementClassicReadyNoSession + } else { + var secrets = getString(KEY_SECRETS_STR)?.takeIf { it.isNotEmpty() } + val roomKeysVersion = getString(KEY_ROOM_KEYS_VERSION_STR) + .also { + if (secrets != null && it == null) { + Timber.tag(loggerTag.value).w("Room keys version is null, outdated version of Element Classic, ignore secrets") + // In this case, just ignore the secrets, the SDK will not accept them anyway + secrets = null + } + } + ?.takeIf { it.isNotEmpty() } + val homeserverUrl = getString(KEY_HOMESERVER_URL_STR)?.takeIf { it.isNotEmpty() } + val displayName = getString(KEY_USER_DISPLAY_NAME_STR)?.takeIf { it.isNotEmpty() } + val doesContainBackupKey = secrets != null && + roomKeysVersion != null && + matrixAuthenticationService.doSecretsContainBackupKey(userId, secrets, roomKeysVersion) + Timber.tag(loggerTag.value).d( + buildString { + append("Receiving session $userId ($displayName) from Element Classic, with secrets: ") + append(secrets != null) + append(", with roomKeysVersion: ") + append(roomKeysVersion != null) + append(", with valid backup key: ") + append(doesContainBackupKey) + } + ) + // Ensure avatar is not lost when refreshing the data + val currentAvatar = (stateFlow.value as? ElementClassicConnectionState.ElementClassicReady) + ?.takeIf { it.elementClassicSession.userId == userId } + ?.avatar + ElementClassicConnectionState.ElementClassicReady( + elementClassicSession = ElementClassicSession( + userId = userId, + homeserverUrl = homeserverUrl, + secrets = secrets, + roomKeysVersion = roomKeysVersion, + doesContainBackupKey = doesContainBackupKey, + ), + displayName = displayName, + avatar = currentAvatar, + ) + } + } + } + + // Everything in this companion object must match what is defined in Element Classic + companion object { + const val ELEMENT_CLASSIC_SERVICE_FULL_CLASS_NAME = "im.vector.app.features.importer.ImporterService" + + // Command to the service to get the userId/displayName/secrets of a verified session. + const val MSG_GET_SESSION = 1 + + // Command to the service to get the avatar oor the session. + const val MSG_GET_AVATAR = 2 + + // Keys for the bundle returned from the service + const val KEY_ERROR_STR = "error" + const val KEY_USER_ID_STR = "userId" + const val KEY_HOMESERVER_URL_STR = "homeserverUrl" + const val KEY_USER_DISPLAY_NAME_STR = "displayName" + + /** + * Key to extract the secrets from the bundle, as a Json string. + * Json will have this format: + * { + * "cross_signing" : { + * "master_key" : "z8RUxnaAGu___REDACTED___k+BQL9o", + * "user_signing_key" : "baJHzA___REDACTED___xMLbSUAXw9QUzqms", + * "self_signing_key" : "DU0CvLtR2G/___REDACTED___dV/MONNq4nsQhM" + * }, + * "backup" : { + * "algorithm" : "m.megolm_backup.v1.curve25519-aes-sha2", + * "key" : "VzncmQ+UOV___REDACTED___patxDz7m0Nc", + * "backup_version" : "1" + * } + * } + */ + const val KEY_SECRETS_STR = "secrets" + const val KEY_ROOM_KEYS_VERSION_STR = "roomKeysVersion" + + // For the avatar + const val KEY_USER_AVATAR_PARCELABLE = "avatar" + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginModule.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginModule.kt index 12b9106b71..4523e6f45e 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginModule.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginModule.kt @@ -14,8 +14,6 @@ import dev.zacsweers.metro.Binds import dev.zacsweers.metro.ContributesTo import io.element.android.features.login.impl.changeserver.ChangeServerPresenter import io.element.android.features.login.impl.changeserver.ChangeServerState -import io.element.android.features.login.impl.screens.onboarding.classic.LoginWithClassicPresenter -import io.element.android.features.login.impl.screens.onboarding.classic.LoginWithClassicState import io.element.android.libraries.architecture.Presenter @ContributesTo(AppScope::class) @@ -23,7 +21,4 @@ import io.element.android.libraries.architecture.Presenter interface LoginModule { @Binds fun bindChangeServerPresenter(presenter: ChangeServerPresenter): Presenter - - @Binds - fun bindLoginWithClassicPresenter(presenter: LoginWithClassicPresenter): Presenter } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt index a62919e705..78be770bfc 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt @@ -60,10 +60,19 @@ class LoginHelper( suspend fun submit( isAccountCreation: Boolean, homeserverUrl: String, + resolvedHomeserverUrl: String?, loginHint: String?, ) { suspend { - authenticationService.setHomeserver(homeserverUrl).map { matrixHomeServerDetails -> + authenticationService.setHomeserver(homeserverUrl).recoverCatching { + // No .well-known file? + // If the homeserver is not reachable, try using resolvedHomeserverUrl. + if (resolvedHomeserverUrl != null && resolvedHomeserverUrl != homeserverUrl) { + authenticationService.setHomeserver(resolvedHomeserverUrl).getOrThrow() + } else { + throw it + } + }.map { matrixHomeServerDetails -> if (matrixHomeServerDetails.supportsOidcLogin) { // Retrieve the details right now val oidcPrompt = if (isAccountCreation) OidcPrompt.Create else OidcPrompt.Login diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt index 87010a4a30..c6d6d76486 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt @@ -44,6 +44,7 @@ class ChooseAccountProviderPresenter( loginHelper.submit( isAccountCreation = false, homeserverUrl = it.url, + resolvedHomeserverUrl = null, loginHint = null, ) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/ClassicFlowNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/ClassicFlowNode.kt new file mode 100644 index 0000000000..f2ff998652 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/ClassicFlowNode.kt @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2026 Element Creations 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.login.impl.screens.classic + +import android.os.Parcelable +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.lifecycle.lifecycleScope +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.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.newRoot +import com.bumble.appyx.navmodel.backstack.operation.pop +import com.bumble.appyx.navmodel.backstack.operation.push +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.features.login.impl.screens.classic.loginwithclassic.LoginWithClassicNode +import io.element.android.features.login.impl.screens.classic.missingkeybackup.MissingKeyBackupNode +import io.element.android.features.login.impl.screens.classic.root.RootNode +import io.element.android.libraries.architecture.BackstackView +import io.element.android.libraries.architecture.BaseFlowNode +import io.element.android.libraries.architecture.appyx.rememberFaderOrSliderTransitionHandler +import io.element.android.libraries.architecture.callback +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.matrix.api.auth.OidcDetails +import io.element.android.libraries.matrix.api.core.UserId +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.parcelize.Parcelize + +@ContributesNode(AppScope::class) +@AssistedInject +class ClassicFlowNode( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val classicFlowNodeHelper: ClassicFlowNodeHelper, +) : BaseFlowNode( + backstack = BackStack( + initialElement = NavTarget.Root, + savedStateMap = buildContext.savedStateMap, + ), + buildContext = buildContext, + plugins = plugins, +) { + interface Callback : Plugin { + fun navigateToOnBoarding(allowBackNavigation: Boolean) + fun navigateToLoginPassword() + fun navigateToOidc(oidcDetails: OidcDetails) + fun navigateToCreateAccount(url: String) + } + + sealed interface NavTarget : Parcelable { + @Parcelize + data object Root : NavTarget + + @Parcelize + data class LoginWithClassic( + val userId: UserId, + ) : NavTarget + + @Parcelize + data object MissingKeyBackup : NavTarget + } + + private val callback: Callback = callback() + + override fun onBuilt() { + super.onBuilt() + observeElementClassicConnection() + lifecycle.subscribe( + onResume = { + classicFlowNodeHelper.onResume() + }, + ) + } + + private fun observeElementClassicConnection() { + classicFlowNodeHelper.navigationEventFlow().onEach { navigationEvent -> + when (navigationEvent) { + is NavigationEvent.Idle -> Unit + is NavigationEvent.NavigateToOnBoarding -> callback.navigateToOnBoarding(allowBackNavigation = false) + is NavigationEvent.NavigateToLoginWithClassic -> backstack.newRoot(NavTarget.LoginWithClassic(navigationEvent.userId)) + } + }.launchIn(lifecycleScope) + } + + override fun resolve( + navTarget: NavTarget, + buildContext: BuildContext, + ): Node { + return when (navTarget) { + NavTarget.Root -> { + createNode(buildContext) + } + is NavTarget.LoginWithClassic -> { + val callback = object : LoginWithClassicNode.Callback { + override fun navigateToOtherOptions() { + callback.navigateToOnBoarding(allowBackNavigation = true) + } + + override fun navigateToLoginPassword() { + callback.navigateToLoginPassword() + } + + override fun navigateToOidc(oidcDetails: OidcDetails) { + callback.navigateToOidc(oidcDetails) + } + + override fun navigateToCreateAccount(url: String) { + callback.navigateToCreateAccount(url) + } + + override fun navigateToMissingKeyBackup() { + backstack.push(NavTarget.MissingKeyBackup) + } + } + val inputs = LoginWithClassicNode.Inputs( + userId = navTarget.userId, + ) + createNode(buildContext, plugins = listOf(inputs, callback)) + } + NavTarget.MissingKeyBackup -> { + val callback = object : MissingKeyBackupNode.Callback { + override fun navigateBack() { + backstack.pop() + } + } + createNode(buildContext, listOf(callback)) + } + } + } + + @Composable + override fun View(modifier: Modifier) { + BackstackView( + modifier = modifier, + transitionHandler = rememberFaderOrSliderTransitionHandler(), + ) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/ClassicFlowNodeHelper.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/ClassicFlowNodeHelper.kt new file mode 100644 index 0000000000..a5bc74c5e4 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/ClassicFlowNodeHelper.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2026 Element Creations 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.login.impl.screens.classic + +import dev.zacsweers.metro.Inject +import io.element.android.features.login.impl.classic.ElementClassicConnection +import io.element.android.features.login.impl.classic.ElementClassicConnectionState +import io.element.android.libraries.sessionstorage.api.SessionStore +import io.element.android.libraries.sessionstorage.api.toUserListFlow +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf + +@Inject +class ClassicFlowNodeHelper( + private val elementClassicConnection: ElementClassicConnection, + private val sessionStore: SessionStore, +) { + fun onResume() { + elementClassicConnection.requestSession() + } + + @OptIn(ExperimentalCoroutinesApi::class) + fun navigationEventFlow(): Flow { + return elementClassicConnection.stateFlow + .distinctUntilChangedBy { + // Ignore change on ElementClassicConnectionState.ElementClassicReady.avatar + if (it is ElementClassicConnectionState.ElementClassicReady) { + it.copy(avatar = null) + } else { + it + } + } + .flatMapLatest { elementClassicConnectionState -> + when (elementClassicConnectionState) { + ElementClassicConnectionState.Idle -> { + // Ensure user is not stuck on the loading screen. + // If Element Classic is taking too long to communicate (or crashes), unblock the user after a few seconds. + flow { + emit(NavigationEvent.Idle) + delay(5_000) + emit(NavigationEvent.NavigateToOnBoarding) + } + } + ElementClassicConnectionState.ElementClassicNotFound, + ElementClassicConnectionState.ElementClassicReadyNoSession, + is ElementClassicConnectionState.Error -> { + flowOf(NavigationEvent.NavigateToOnBoarding) + } + is ElementClassicConnectionState.ElementClassicReady -> { + val existingSessions = sessionStore.sessionsFlow().toUserListFlow().first() + if (elementClassicConnectionState.elementClassicSession.userId.value in existingSessions) { + flowOf(NavigationEvent.NavigateToOnBoarding) + } else { + // 2 cases when this can be run: + // First time this screen will be displayed + // Missing key backup screen was displayed, but the data has changed (user set up the key backup on Classic), + // and the app is resuming. + flowOf(NavigationEvent.NavigateToLoginWithClassic(elementClassicConnectionState.elementClassicSession.userId)) + } + } + } + } + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/NavigationEvent.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/NavigationEvent.kt new file mode 100644 index 0000000000..cddca0015b --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/NavigationEvent.kt @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2026 Element Creations 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.login.impl.screens.classic + +import io.element.android.libraries.matrix.api.core.UserId + +sealed interface NavigationEvent { + data object Idle : NavigationEvent + data object NavigateToOnBoarding : NavigationEvent + data class NavigateToLoginWithClassic( + val userId: UserId, + ) : NavigationEvent +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicEvent.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicEvent.kt new file mode 100644 index 0000000000..6ba9b2142a --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicEvent.kt @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2026 Element Creations 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.login.impl.screens.classic.loginwithclassic + +sealed interface LoginWithClassicEvent { + data object Submit : LoginWithClassicEvent + data object ClearError : LoginWithClassicEvent +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicNavigator.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicNavigator.kt new file mode 100644 index 0000000000..55716c2cf7 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicNavigator.kt @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2026 Element Creations 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.login.impl.screens.classic.loginwithclassic + +interface LoginWithClassicNavigator { + fun navigateToMissingKeyBackup() +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicNode.kt new file mode 100644 index 0000000000..c42248a3f8 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicNode.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2026 Element Creations 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.login.impl.screens.classic.loginwithclassic + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +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.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode +import io.element.android.features.login.impl.util.openLearnMorePage +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.matrix.api.auth.OidcDetails +import io.element.android.libraries.matrix.api.core.UserId + +@ContributesNode(AppScope::class) +@AssistedInject +class LoginWithClassicNode( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + presenterFactory: LoginWithClassicPresenter.Factory, +) : Node(buildContext, plugins = plugins), + LoginWithClassicNavigator { + interface Callback : Plugin { + fun navigateToOtherOptions() + fun navigateToLoginPassword() + fun navigateToOidc(oidcDetails: OidcDetails) + fun navigateToCreateAccount(url: String) + fun navigateToMissingKeyBackup() + } + + data class Inputs( + val userId: UserId, + ) : NodeInputs + + private val inputs: Inputs = inputs() + val presenter = presenterFactory.create(inputs.userId, this) + private val callback: Callback = callback() + + override fun navigateToMissingKeyBackup() { + callback.navigateToMissingKeyBackup() + } + + @Composable + override fun View(modifier: Modifier) { + val context = LocalContext.current + val state = presenter.present() + LoginWithClassicView( + state = state, + modifier = modifier, + onOtherOptionsClick = callback::navigateToOtherOptions, + onOidcDetails = callback::navigateToOidc, + onNeedLoginPassword = callback::navigateToLoginPassword, + onLearnMoreClick = { openLearnMorePage(context) }, + onCreateAccountContinue = callback::navigateToCreateAccount, + ) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenter.kt new file mode 100644 index 0000000000..90a528c3ae --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenter.kt @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2026 Element Creations 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.login.impl.screens.classic.loginwithclassic + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject +import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource +import io.element.android.features.login.impl.classic.ElementClassicConnection +import io.element.android.features.login.impl.classic.ElementClassicConnectionState +import io.element.android.features.login.impl.login.LoginHelper +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.core.uri.ensureProtocol +import io.element.android.libraries.matrix.api.core.UserId +import kotlinx.coroutines.launch + +@AssistedInject +class LoginWithClassicPresenter( + @Assisted private val userId: UserId, + @Assisted private val navigator: LoginWithClassicNavigator, + private val loginHelper: LoginHelper, + private val elementClassicConnection: ElementClassicConnection, + private val accountProviderDataSource: AccountProviderDataSource, + private val buildMeta: BuildMeta, +) : Presenter { + @AssistedFactory + interface Factory { + fun create( + userId: UserId, + navigator: LoginWithClassicNavigator, + ): LoginWithClassicPresenter + } + + @Composable + override fun present(): LoginWithClassicState { + val coroutineScope = rememberCoroutineScope() + var loginWithClassicAction by remember { + mutableStateOf>(AsyncAction.Uninitialized) + } + val loginMode by loginHelper.collectLoginMode() + val elementClassicConnectionState by elementClassicConnection.stateFlow.collectAsState() + + fun handleEvent(event: LoginWithClassicEvent) { + when (event) { + LoginWithClassicEvent.Submit -> { + val currentState = elementClassicConnection.stateFlow.value + if (currentState is ElementClassicConnectionState.ElementClassicReady) { + if (currentState.elementClassicSession.secrets != null && + !currentState.elementClassicSession.doesContainBackupKey) { + navigator.navigateToMissingKeyBackup() + } else { + coroutineScope.launch { + loginWithClassicAction = AsyncAction.Loading + // Ensure that the current account provider is set + val elementClassicUserId = currentState.elementClassicSession.userId + val accountProvider = elementClassicUserId.domainName.orEmpty().ensureProtocol() + accountProviderDataSource.setUrl(accountProvider) + loginHelper.submit( + isAccountCreation = false, + homeserverUrl = accountProvider, + resolvedHomeserverUrl = currentState.elementClassicSession.homeserverUrl, + loginHint = "mxid:" + elementClassicUserId.value, + ) + } + } + } else { + loginWithClassicAction = AsyncAction.Failure(IllegalStateException("Element Classic is not ready")) + } + } + LoginWithClassicEvent.ClearError -> { + loginWithClassicAction = AsyncAction.Uninitialized + loginHelper.clearError() + } + } + } + + val elementClassicReady = elementClassicConnectionState as? ElementClassicConnectionState.ElementClassicReady + return LoginWithClassicState( + isElementPro = buildMeta.isEnterpriseBuild, + userId = userId, + displayName = elementClassicReady?.displayName, + avatar = elementClassicReady?.avatar, + loginMode = loginMode, + loginWithClassicAction = loginWithClassicAction, + eventSink = ::handleEvent, + ) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicState.kt new file mode 100644 index 0000000000..275a444768 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicState.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2026 Element Creations 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.login.impl.screens.classic.loginwithclassic + +import android.graphics.Bitmap +import androidx.compose.runtime.Stable +import io.element.android.features.login.impl.login.LoginMode +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.core.UserId + +@Stable +data class LoginWithClassicState( + val isElementPro: Boolean, + val userId: UserId, + val displayName: String?, + val avatar: Bitmap?, + val loginWithClassicAction: AsyncAction, + val loginMode: AsyncData, + val eventSink: (LoginWithClassicEvent) -> Unit, +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicStateProvider.kt new file mode 100644 index 0000000000..d8dcfeb072 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicStateProvider.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2026 Element Creations 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.login.impl.screens.classic.loginwithclassic + +import android.graphics.Bitmap +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.login.impl.login.LoginMode +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.core.UserId + +open class LoginWithClassicStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aLoginWithClassicState(), + aLoginWithClassicState(isElementPro = true, displayName = "Alice"), + ) +} + +fun aLoginWithClassicState( + isElementPro: Boolean = false, + userId: UserId = UserId("@alice:matrix.org"), + displayName: String? = null, + avatar: Bitmap? = null, + loginWithClassicAction: AsyncAction = AsyncAction.Uninitialized, + loginMode: AsyncData = AsyncData.Uninitialized, + eventSink: (LoginWithClassicEvent) -> Unit = {}, +) = LoginWithClassicState( + isElementPro = isElementPro, + userId = userId, + displayName = displayName, + avatar = avatar, + loginWithClassicAction = loginWithClassicAction, + loginMode = loginMode, + eventSink = eventSink, +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicView.kt new file mode 100644 index 0000000000..6b5c48f1ec --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicView.kt @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2026 Element Creations 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.login.impl.screens.classic.loginwithclassic + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +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.login.impl.R +import io.element.android.features.login.impl.login.LoginModeView +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule +import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage +import io.element.android.libraries.designsystem.background.OnboardingBackground +import io.element.android.libraries.designsystem.components.async.AsyncActionView +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.components.avatar.BitmapAvatar +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.OutlinedButton +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.matrix.api.auth.OidcDetails +import io.element.android.libraries.testtags.TestTags +import io.element.android.libraries.testtags.testTag +import io.element.android.libraries.ui.strings.CommonStrings + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LoginWithClassicView( + state: LoginWithClassicState, + onOtherOptionsClick: () -> Unit, + onOidcDetails: (OidcDetails) -> Unit, + onNeedLoginPassword: () -> Unit, + onLearnMoreClick: () -> Unit, + onCreateAccountContinue: (url: String) -> Unit, + modifier: Modifier = Modifier, +) { + val isLoading by remember(state.loginMode) { + derivedStateOf { + state.loginMode is AsyncData.Loading + } + } + + HeaderFooterPage( + modifier = modifier + .fillMaxSize() + .systemBarsPadding() + .imePadding(), + background = { OnboardingBackground() }, + isScrollable = true, + header = { + Column( + modifier = Modifier + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(Modifier.height(40.dp)) + Box( + modifier = Modifier + .size(54.dp) + .shadow(elevation = 10.dp, shape = RoundedCornerShape(15.dp)) + .background(ElementTheme.colors.bgCanvasDefault, shape = RoundedCornerShape(15.dp)), + contentAlignment = Alignment.Center, + ) { + val resId = if (state.isElementPro) { + R.drawable.element_pro_logo + } else { + R.drawable.element_foss_logo + } + Image( + modifier = Modifier.size(37.5.dp), + painter = painterResource(id = resId), + contentDescription = null, + ) + } + Spacer(Modifier.height(8.dp)) + Text( + text = stringResource(id = R.string.screen_onboarding_welcome_title), + color = ElementTheme.colors.textPrimary, + style = ElementTheme.typography.fontHeadingMdBold, + textAlign = TextAlign.Center, + ) + Spacer(Modifier.height(10.dp)) + } + }, + content = { + Column( + modifier = Modifier + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(Modifier.height(40.dp)) + BitmapAvatar( + avatarData = AvatarData( + id = state.userId.value, + name = state.displayName, + // Not used here + url = null, + size = AvatarSize.UserHeader, + ), + bitmap = state.avatar, + ) + Spacer(Modifier.height(24.dp)) + Text( + modifier = Modifier.padding(horizontal = 32.dp), + text = stringResource(R.string.screen_onboarding_welcome_back), + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textSecondary, + textAlign = TextAlign.Center, + ) + // User display name + if (state.displayName != null) { + Text( + text = state.displayName, + style = ElementTheme.typography.fontHeadingLgBold, + color = ElementTheme.colors.textPrimary, + textAlign = TextAlign.Center, + ) + Spacer(Modifier.height(16.dp)) + } + // UserId + Text( + text = state.userId.value, + style = if (state.displayName == null) ElementTheme.typography.fontHeadingLgBold else ElementTheme.typography.fontBodyLgRegular, + color = ElementTheme.colors.textPrimary, + textAlign = TextAlign.Center, + ) + // Min spacing + Spacer(Modifier.height(45.dp)) + ButtonColumnMolecule { + Button( + text = stringResource(CommonStrings.action_continue), + showProgress = isLoading, + onClick = { + state.eventSink(LoginWithClassicEvent.Submit) + }, + modifier = Modifier + .fillMaxWidth() + .testTag(TestTags.loginContinue) + ) + OutlinedButton( + text = stringResource(CommonStrings.common_other_options), + onClick = onOtherOptionsClick, + enabled = !isLoading, + modifier = Modifier + .fillMaxWidth() + .testTag(TestTags.loginContinue) + ) + } + } + }, + footer = {}, + ) + + AsyncActionView( + async = state.loginWithClassicAction, + onErrorDismiss = { + state.eventSink(LoginWithClassicEvent.ClearError) + }, + onSuccess = { + // noop, the view will be closed + }, + progressDialog = { + // The button is showing the progress + } + ) + LoginModeView( + loginMode = state.loginMode, + onClearError = { + state.eventSink(LoginWithClassicEvent.ClearError) + }, + onLearnMoreClick = onLearnMoreClick, + onOidcDetails = onOidcDetails, + onNeedLoginPassword = onNeedLoginPassword, + onCreateAccountContinue = onCreateAccountContinue, + ) +} + +@PreviewsDayNight +@Composable +internal fun LoginWithClassicViewPreview(@PreviewParameter(LoginWithClassicStateProvider::class) state: LoginWithClassicState) = ElementPreview { + LoginWithClassicView( + state = state, + onOtherOptionsClick = {}, + onOidcDetails = {}, + onNeedLoginPassword = {}, + onLearnMoreClick = {}, + onCreateAccountContinue = {}, + ) +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupNode.kt new file mode 100644 index 0000000000..45c16e7cde --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupNode.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2026 Element Creations 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.login.impl.screens.classic.missingkeybackup + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +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.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode +import io.element.android.features.login.impl.BuildConfig +import io.element.android.libraries.architecture.callback +import timber.log.Timber + +@ContributesNode(AppScope::class) +@AssistedInject +class MissingKeyBackupNode( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: MissingKeyBackupPresenter, +) : Node(buildContext, plugins = plugins) { + interface Callback : Plugin { + fun navigateBack() + } + + private val callback: Callback = callback() + + /** + * Open Element Classic application. + */ + private fun openClassic(context: Context) { + context.packageManager.getLaunchIntentForPackage( + BuildConfig.elementClassicPackage, + )?.let { intent -> + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + try { + context.startActivity(intent) + } catch (e: ActivityNotFoundException) { + // Should not happen, Element Classic must be installed for this screen to be displayed. + Timber.e(e, "Element Classic app not found, cannot open it.") + } + } + } + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + val context = LocalContext.current + MissingKeyBackupView( + state = state, + onBackClick = callback::navigateBack, + onOpenClassicClick = { + openClassic(context) + }, + modifier = modifier, + ) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupPresenter.kt new file mode 100644 index 0000000000..593c50dcb5 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupPresenter.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2026 Element Creations 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.login.impl.screens.classic.missingkeybackup + +import androidx.compose.runtime.Composable +import dev.zacsweers.metro.Inject +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.meta.BuildMeta + +@Inject +class MissingKeyBackupPresenter( + private val buildMeta: BuildMeta, +) : Presenter { + @Composable + override fun present(): MissingKeyBackupState { + return MissingKeyBackupState( + appName = buildMeta.applicationName, + ) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupState.kt new file mode 100644 index 0000000000..31eaf015a0 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupState.kt @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2026 Element Creations 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.login.impl.screens.classic.missingkeybackup + +data class MissingKeyBackupState( + val appName: String, +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupStateProvider.kt new file mode 100644 index 0000000000..2c6a09b3ed --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupStateProvider.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2026 Element Creations 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.login.impl.screens.classic.missingkeybackup + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +open class MissingKeyBackupStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aMissingKeyBackupState(), + // Add other state here + ) +} + +fun aMissingKeyBackupState( + appName: String = "AppName", +) = MissingKeyBackupState( + appName = appName, +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupView.kt new file mode 100644 index 0000000000..c4c9c5f286 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupView.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2026 Element Creations 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.login.impl.screens.classic.missingkeybackup + +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.login.impl.R +import io.element.android.libraries.designsystem.atomic.organisms.NumberedListOrganism +import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage +import io.element.android.libraries.designsystem.components.BigIcon +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Button +import kotlinx.collections.immutable.persistentListOf + +@Composable +fun MissingKeyBackupView( + state: MissingKeyBackupState, + onBackClick: () -> Unit, + onOpenClassicClick: () -> Unit, + modifier: Modifier = Modifier, +) { + FlowStepPage( + modifier = modifier, + onBackClick = onBackClick, + iconStyle = BigIcon.Style.Default(CompoundIcons.KeySolid()), + title = stringResource(id = R.string.screen_missing_key_backup_title, state.appName), + content = { Content(state) }, + buttons = { + Buttons( + onOpenClassicClick = onOpenClassicClick, + ) + } + ) +} + +@Composable +private fun Content( + state: MissingKeyBackupState, +) { + NumberedListOrganism( + modifier = Modifier.padding(top = 50.dp, start = 20.dp, end = 20.dp), + items = persistentListOf( + AnnotatedString(stringResource(R.string.screen_missing_key_backup_step_1)), + AnnotatedString(stringResource(R.string.screen_missing_key_backup_step_2_android)), + AnnotatedString(stringResource(R.string.screen_missing_key_backup_step_3_android)), + AnnotatedString(stringResource(R.string.screen_missing_key_backup_step_4)), + AnnotatedString(stringResource(R.string.screen_missing_key_backup_step_5, state.appName)), + ), + ) +} + +@Composable +private fun ColumnScope.Buttons( + onOpenClassicClick: () -> Unit, +) { + Button( + text = stringResource(id = R.string.screen_missing_key_backup_open_element_classic), + modifier = Modifier.fillMaxWidth(), + onClick = onOpenClassicClick, + ) +} + +@PreviewsDayNight +@Composable +internal fun MissingKeyBackupViewPreview(@PreviewParameter(MissingKeyBackupStateProvider::class) state: MissingKeyBackupState) = ElementPreview { + MissingKeyBackupView( + state = state, + onBackClick = {}, + onOpenClassicClick = {}, + ) +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/root/RootNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/root/RootNode.kt new file mode 100644 index 0000000000..adb8c2d728 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/root/RootNode.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026 Element Creations 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.login.impl.screens.classic.root + +import androidx.compose.runtime.Composable +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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode + +@ContributesNode(AppScope::class) +@AssistedInject +class RootNode( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, +) : Node(buildContext, plugins = plugins) { + @Composable + override fun View(modifier: Modifier) { + RootView(modifier) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/root/RootView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/root/RootView.kt new file mode 100644 index 0000000000..f1ca4b048a --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/root/RootView.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2026 Element Creations 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.login.impl.screens.classic.root + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator +import io.element.android.libraries.designsystem.utils.DelayedVisibility +import kotlin.time.Duration.Companion.milliseconds + +@Composable +fun RootView( + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + DelayedVisibility( + duration = 100.milliseconds, + ) { + CircularProgressIndicator() + } + } +} + +@PreviewsDayNight +@Composable +internal fun RootViewPreview() = ElementPreview { + RootView() +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt index c38da7b11c..bf06613830 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt @@ -48,6 +48,7 @@ class ConfirmAccountProviderPresenter( loginHelper.submit( isAccountCreation = params.isAccountCreation, homeserverUrl = accountProvider.url, + resolvedHomeserverUrl = null, loginHint = null, ) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordNode.kt index c6ce16141d..853b8a7423 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordNode.kt @@ -17,14 +17,23 @@ 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.libraries.architecture.NodeInputs +import io.element.android.libraries.architecture.inputs @ContributesNode(AppScope::class) @AssistedInject class LoginPasswordNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val presenter: LoginPasswordPresenter, + presenterFactory: LoginPasswordPresenter.Factory, ) : Node(buildContext, plugins = plugins) { + data class Inputs( + val initialLogin: String, + ) : NodeInputs + + private val inputs: Inputs = inputs() + private val presenter = presenterFactory.create(inputs.initialLogin) + @Composable override fun View(modifier: Modifier) { val state = presenter.present() diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenter.kt index b1ddc6e5b8..f26f342a42 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenter.kt @@ -16,7 +16,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable -import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter @@ -25,11 +27,18 @@ import io.element.android.libraries.matrix.api.core.SessionId import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -@Inject +@AssistedInject class LoginPasswordPresenter( + @Assisted + private val initialLogin: String, private val authenticationService: MatrixAuthenticationService, private val accountProviderDataSource: AccountProviderDataSource, ) : Presenter { + @AssistedFactory + interface Factory { + fun create(initialLogin: String): LoginPasswordPresenter + } + @Composable override fun present(): LoginPasswordState { val localCoroutineScope = rememberCoroutineScope() @@ -38,7 +47,12 @@ class LoginPasswordPresenter( } val formState = rememberSaveable { - mutableStateOf(LoginFormState.Default) + mutableStateOf( + LoginFormState( + login = initialLogin, + password = "", + ) + ) } val accountProvider by accountProviderDataSource.flow.collectAsState() diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingNode.kt index 1ded677c13..5572c412a0 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingNode.kt @@ -42,12 +42,14 @@ class OnBoardingNode( fun navigateToLoginPassword() fun navigateToOidc(oidcDetails: OidcDetails) fun navigateToCreateAccount(url: String) + fun navigateToDeveloperSettings() fun onDone() } data class Params( val accountProvider: String?, val loginHint: String?, + val showBackButton: Boolean, ) : NodeInputs private val callback: Callback = callback() @@ -61,6 +63,7 @@ class OnBoardingNode( override fun View(modifier: Modifier) { val state = presenter.present() val context = LocalContext.current + OnBoardingView( state = state, modifier = modifier, @@ -73,6 +76,7 @@ class OnBoardingNode( onLearnMoreClick = { openLearnMorePage(context) }, onCreateAccountContinue = callback::navigateToCreateAccount, onBackClick = callback::onDone, + onDeveloperSettingsClick = callback::navigateToDeveloperSettings, ) } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt index 741f65234e..306549d11b 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt @@ -26,10 +26,10 @@ import io.element.android.features.enterprise.api.canConnectToAnyHomeserver import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.login.LoginHelper -import io.element.android.features.login.impl.screens.onboarding.classic.LoginWithClassicState import io.element.android.features.rageshake.api.RageshakeFeatureAvailability import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.core.meta.BuildType import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.ui.utils.MultipleTapToUnlock import kotlinx.coroutines.launch @@ -45,7 +45,6 @@ class OnBoardingPresenter( private val onBoardingLogoResIdProvider: OnBoardingLogoResIdProvider, private val sessionStore: SessionStore, private val accountProviderDataSource: AccountProviderDataSource, - private val loginWithClassicPresenter: Presenter, ) : Presenter { @AssistedFactory interface Factory { @@ -101,8 +100,6 @@ class OnBoardingPresenter( val loginMode by loginHelper.collectLoginMode() - val loginWithClassicState = loginWithClassicPresenter.present() - fun handleEvent(event: OnBoardingEvents) { when (event) { is OnBoardingEvents.OnSignIn -> localCoroutineScope.launch { @@ -111,6 +108,7 @@ class OnBoardingPresenter( loginHelper.submit( isAccountCreation = false, homeserverUrl = event.defaultAccountProvider, + resolvedHomeserverUrl = null, loginHint = params.loginHint?.takeIf { forcedAccountProvider == null }, ) } @@ -127,6 +125,8 @@ class OnBoardingPresenter( return OnBoardingState( isAddingAccount = isAddingAccount, + showBackButton = params.showBackButton, + showDeveloperSettings = buildMeta.buildType != BuildType.RELEASE, productionApplicationName = buildMeta.productionApplicationName, defaultAccountProvider = defaultAccountProvider, mustChooseAccountProvider = mustChooseAccountProvider, @@ -136,7 +136,6 @@ class OnBoardingPresenter( loginMode = loginMode, version = buildMeta.versionName, onBoardingLogoResId = onBoardingLogoResId, - loginWithClassicState = loginWithClassicState, eventSink = ::handleEvent, ) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt index 703120b260..316efb03ef 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt @@ -10,11 +10,12 @@ package io.element.android.features.login.impl.screens.onboarding import androidx.annotation.DrawableRes import io.element.android.features.login.impl.login.LoginMode -import io.element.android.features.login.impl.screens.onboarding.classic.LoginWithClassicState import io.element.android.libraries.architecture.AsyncData data class OnBoardingState( val isAddingAccount: Boolean, + val showBackButton: Boolean, + val showDeveloperSettings: Boolean, val productionApplicationName: String, val defaultAccountProvider: String?, val mustChooseAccountProvider: Boolean, @@ -25,7 +26,6 @@ data class OnBoardingState( @DrawableRes val onBoardingLogoResId: Int?, val loginMode: AsyncData, - val loginWithClassicState: LoginWithClassicState, val eventSink: (OnBoardingEvents) -> Unit, ) { val submitEnabled: Boolean diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt index 76f8eb3513..249a904dc3 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt @@ -11,8 +11,6 @@ package io.element.android.features.login.impl.screens.onboarding import androidx.annotation.DrawableRes import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.login.impl.login.LoginMode -import io.element.android.features.login.impl.screens.onboarding.classic.LoginWithClassicState -import io.element.android.features.login.impl.screens.onboarding.classic.aLoginWithClassicState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.R @@ -31,11 +29,17 @@ open class OnBoardingStateProvider : PreviewParameterProvider { canLoginWithQrCode = true, canCreateAccount = true, ), + anOnBoardingState( + showBackButton = true, + showDeveloperSettings = true, + ), ) } fun anOnBoardingState( isAddingAccount: Boolean = false, + showBackButton: Boolean = false, + showDeveloperSettings: Boolean = false, productionApplicationName: String = "Element", defaultAccountProvider: String? = null, mustChooseAccountProvider: Boolean = false, @@ -46,10 +50,11 @@ fun anOnBoardingState( @DrawableRes customLogoResId: Int? = null, loginMode: AsyncData = AsyncData.Uninitialized, - loginWithClassicState: LoginWithClassicState = aLoginWithClassicState(), eventSink: (OnBoardingEvents) -> Unit = {}, ) = OnBoardingState( isAddingAccount = isAddingAccount, + showBackButton = showBackButton, + showDeveloperSettings = showDeveloperSettings, productionApplicationName = productionApplicationName, defaultAccountProvider = defaultAccountProvider, mustChooseAccountProvider = mustChooseAccountProvider, @@ -59,6 +64,5 @@ fun anOnBoardingState( version = version, loginMode = loginMode, onBoardingLogoResId = customLogoResId, - loginWithClassicState = loginWithClassicState, eventSink = eventSink, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt index d590f1fec8..5ee7ab6ac4 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt @@ -31,15 +31,10 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.compose.LifecycleEventEffect import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.login.impl.R import io.element.android.features.login.impl.login.LoginModeView -import io.element.android.features.login.impl.screens.onboarding.classic.ConfirmingLoginWithElementClassic -import io.element.android.features.login.impl.screens.onboarding.classic.LoginWithClassicEvent -import io.element.android.features.login.impl.screens.onboarding.classic.LoginWithClassicState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtom import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtomSize @@ -47,11 +42,11 @@ import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMo import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage import io.element.android.libraries.designsystem.atomic.pages.OnBoardingPage import io.element.android.libraries.designsystem.components.BigIcon -import io.element.android.libraries.designsystem.components.async.AsyncActionView -import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.IconSource import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextButton @@ -69,6 +64,7 @@ import io.element.android.libraries.ui.strings.CommonStrings fun OnBoardingView( state: OnBoardingState, onBackClick: () -> Unit, + onDeveloperSettingsClick: () -> Unit, onSignInWithQrCode: () -> Unit, onSignIn: (mustChooseAccountProvider: Boolean) -> Unit, onCreateAccount: () -> Unit, @@ -114,45 +110,10 @@ fun OnBoardingView( state = state, loginView = loginView, buttons = buttons, + onBackClick = onBackClick, + onDeveloperSettingsClick = onDeveloperSettingsClick, ) } - - LoginWithElementClassicView( - state = state.loginWithClassicState, - ) -} - -@Composable -private fun LoginWithElementClassicView( - state: LoginWithClassicState, -) { - LifecycleEventEffect(Lifecycle.Event.ON_RESUME) { - state.eventSink(LoginWithClassicEvent.RefreshData) - } - AsyncActionView( - async = state.loginWithClassicAction, - confirmationDialog = { confirming -> - when (confirming) { - is ConfirmingLoginWithElementClassic -> { - // TODO i18n - ConfirmationDialog( - title = "Sign in with Element Classic", - content = "You are signing in as ${confirming.userId} on Element Classic." + - " Your existing session on Element Classic will not be signed out. Do you want to continue?", - submitText = stringResource(CommonStrings.action_continue), - onSubmitClick = { state.eventSink(LoginWithClassicEvent.DoLoginWithClassic) }, - onDismiss = { state.eventSink(LoginWithClassicEvent.CloseDialog) }, - ) - } - } - }, - onErrorDismiss = { - state.eventSink(LoginWithClassicEvent.CloseDialog) - }, - onSuccess = { - // noop, the view will be closed - } - ) } @Composable @@ -160,18 +121,49 @@ private fun AddFirstAccountScaffold( state: OnBoardingState, loginView: @Composable () -> Unit, buttons: @Composable () -> Unit, + onBackClick: () -> Unit, + onDeveloperSettingsClick: () -> Unit, modifier: Modifier = Modifier, ) { OnBoardingPage( modifier = modifier, renderBackground = state.onBoardingLogoResId == null, content = { - if (state.onBoardingLogoResId != null) { - OnBoardingLogo( - onBoardingLogoResId = state.onBoardingLogoResId, - ) - } else { - OnBoardingContent(state = state) + Box( + modifier = Modifier.fillMaxSize(), + ) { + if (state.onBoardingLogoResId != null) { + OnBoardingLogo( + onBoardingLogoResId = state.onBoardingLogoResId, + ) + } else { + OnBoardingContent(state = state) + } + if (state.showDeveloperSettings) { + IconButton( + onClick = onDeveloperSettingsClick, + modifier = Modifier + .align(Alignment.TopStart), + ) { + Icon( + imageVector = CompoundIcons.SettingsSolid(), + contentDescription = stringResource(CommonStrings.common_developer_options), + ) + } + } + if (state.showBackButton) { + // Add icon button to "navigate back" + IconButton( + onClick = onBackClick, + modifier = Modifier + .align(Alignment.TopEnd), + ) { + Icon( + imageVector = CompoundIcons.Close(), + contentDescription = stringResource(CommonStrings.action_cancel), + ) + } + } } loginView() }, @@ -283,18 +275,6 @@ private fun OnBoardingButtons( } else { CommonStrings.action_continue } - if (state.loginWithClassicState.canLoginWithClassic) { - Button( - text = "Sign in with Element Classic", - leadingIcon = IconSource.Vector(CompoundIcons.Mobile()), - onClick = { - state.loginWithClassicState.eventSink( - LoginWithClassicEvent.StartLoginWithClassic - ) - }, - modifier = Modifier.fillMaxWidth(), - ) - } if (state.canLoginWithQrCode) { Button( text = stringResource(id = R.string.screen_onboarding_sign_in_with_qr_code), @@ -369,6 +349,7 @@ internal fun OnBoardingViewPreview( OnBoardingView( state = state, onBackClick = {}, + onDeveloperSettingsClick = {}, onSignInWithQrCode = {}, onSignIn = {}, onCreateAccount = {}, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/ElementClassicConnection.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/ElementClassicConnection.kt deleted file mode 100644 index f895dd781e..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/ElementClassicConnection.kt +++ /dev/null @@ -1,260 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations 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.login.impl.screens.onboarding.classic - -import android.content.ComponentName -import android.content.Context -import android.content.Context.BIND_AUTO_CREATE -import android.content.Intent -import android.content.ServiceConnection -import android.os.Bundle -import android.os.Handler -import android.os.IBinder -import android.os.Message -import android.os.Messenger -import android.os.RemoteException -import dev.zacsweers.metro.AppScope -import dev.zacsweers.metro.ContributesBinding -import io.element.android.features.login.impl.BuildConfig -import io.element.android.libraries.core.log.logger.LoggerTag -import io.element.android.libraries.di.annotations.AppCoroutineScope -import io.element.android.libraries.di.annotations.ApplicationContext -import io.element.android.libraries.matrix.api.core.UserId -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch -import timber.log.Timber - -interface ElementClassicConnection { - fun start() - fun stop() - fun requestData() - val stateFlow: StateFlow -} - -sealed interface ElementClassicConnectionState { - object Idle : ElementClassicConnectionState - object ElementClassicNotFound : ElementClassicConnectionState - object ElementClassicReadyNoSession : ElementClassicConnectionState - data class ElementClassicReady( - val userId: UserId, - val secrets: String, - ) : ElementClassicConnectionState - - data class Error(val error: String) : ElementClassicConnectionState -} - -private val loggerTag = LoggerTag("ECConnection") - -@ContributesBinding(AppScope::class) -class DefaultElementClassicConnection( - @ApplicationContext - private val context: Context, - @AppCoroutineScope - private val coroutineScope: CoroutineScope, -) : ElementClassicConnection { - // Messenger for communicating with the service. - private var messenger: Messenger? = null - - // Target we publish for external service to send messages to IncomingHandler. - private val incomingMessenger: Messenger = Messenger(IncomingHandler()) - - // Flag indicating whether we have called bind on the service. - private var bound: Boolean = false - - /** - * Class for interacting with the main interface of the service. - */ - private val serviceConnection = object : ServiceConnection { - override fun onServiceConnected(className: ComponentName, service: IBinder) { - Timber.tag(loggerTag.value).d("onServiceConnected") - // This is called when the connection with the service has been - // established, giving us the object we can use to - // interact with the service. We are communicating with the - // service using a Messenger, so here we get a client-side - // representation of that from the raw IBinder object. - messenger = Messenger(service) - bound = true - // Request the data as soon as possible - requestData() - } - - override fun onServiceDisconnected(className: ComponentName) { - Timber.tag(loggerTag.value).d("onServiceDisconnected") - // This is called when the connection with the service has been - // unexpectedly disconnected—that is, its process crashed. - messenger = null - bound = false - } - } - - override fun start() { - Timber.tag(loggerTag.value).w("start()") - coroutineScope.launch { - // Establish a connection with the service. We use an explicit - // class name because there is no reason to be able to let other - // applications replace our component. - try { - val intentService = Intent() - intentService.setComponent(getElementClassicComponent()) - if (context.bindService(intentService, serviceConnection, BIND_AUTO_CREATE)) { - Timber.tag(loggerTag.value).d("Binding returned true") - } else { - // This happen when the app is not installed - Timber.tag(loggerTag.value).d("Binding returned false") - mutableStateFlow.emit(ElementClassicConnectionState.ElementClassicNotFound) - } - } catch (e: SecurityException) { - Timber.tag(loggerTag.value).e(e, "Can't bind to Service") - mutableStateFlow.emit(ElementClassicConnectionState.Error(e.localizedMessage.orEmpty())) - } - } - } - - override fun stop() { - Timber.tag(loggerTag.value).w("stop(): Unbinding (bound=$bound)") - if (bound) { - // Detach our existing connection. - context.unbindService(serviceConnection) - bound = false - } - coroutineScope.launch { - mutableStateFlow.emit(ElementClassicConnectionState.Idle) - } - } - - override fun requestData() { - Timber.tag(loggerTag.value).w("requestData()") - coroutineScope.launch { - val finalMessenger = messenger - if (finalMessenger == null) { - Timber.tag(loggerTag.value).w("The messenger is null, can't request data") - mutableStateFlow.emit(ElementClassicConnectionState.Error("The messenger is null, can't request data")) - } else { - try { - // Get the data - val msg = Message.obtain(null, MSG_GET_DATA) - msg.replyTo = incomingMessenger - finalMessenger.send(msg) - } catch (e: RemoteException) { - // In this case the service has crashed before we could even - // do anything with it; we can count on soon being - // disconnected (and then reconnected if it can be restarted) - // so there is no need to do anything here. - Timber.tag(loggerTag.value).e(e, "RemoteException") - mutableStateFlow.emit(ElementClassicConnectionState.Error(e.localizedMessage.orEmpty())) - } - } - } - } - - private val mutableStateFlow = MutableStateFlow(ElementClassicConnectionState.Idle) - override val stateFlow = mutableStateFlow.asStateFlow() - - /** - * Handler of incoming messages from service. - */ - @Suppress("DEPRECATION") - inner class IncomingHandler : Handler() { - override fun handleMessage(msg: Message) { - Timber.tag(loggerTag.value).d("IncomingHandler handling message ${msg.what}") - when (msg.what) { - MSG_GET_DATA -> { - // The data must be extracted from the bundle before we launch the coroutine, else the bundle will be emptied - val state = msg.data.toElementClassicConnectionState() - emitElementClassicState(state) - } - else -> { - super.handleMessage(msg) - } - } - } - } - - private fun emitElementClassicState(state: ElementClassicConnectionState) = coroutineScope.launch { - when (state) { - is ElementClassicConnectionState.Error -> { - Timber.tag(loggerTag.value).w("Received error from Element Classic: %s", state.error) - mutableStateFlow.emit(state) - } - is ElementClassicConnectionState.ElementClassicReady -> { - Timber.tag(loggerTag.value).d("Received userId from Element Classic: %s", state.userId) - mutableStateFlow.emit(state) - } - ElementClassicConnectionState.ElementClassicReadyNoSession -> { - Timber.tag(loggerTag.value).d("Received no session from Element Classic") - mutableStateFlow.emit(state) - } - else -> { - // Should not happen - Timber.tag(loggerTag.value).w("Received unexpected state from Element Classic: %s", state) - mutableStateFlow.emit(ElementClassicConnectionState.Idle) - } - } - } - - private fun getElementClassicComponent() = ComponentName( - BuildConfig.elementClassicPackage, - ELEMENT_CLASSIC_SERVICE_FULL_CLASS_NAME, - ) - - private fun Bundle?.toElementClassicConnectionState(): ElementClassicConnectionState { - return if (this == null) { - ElementClassicConnectionState.Error("No data received from Element Classic") - } else { - val error = getString(KEY_ERROR_STR) - if (error != null) { - ElementClassicConnectionState.Error(error) - } else { - val userId = getString(KEY_USER_ID_STR)?.takeIf { it.isNotEmpty() }?.let(::UserId) - if (userId != null) { - val secrets = getString(KEY_SECRETS_STR)?.takeIf { it.isNotEmpty() } - if (secrets == null) { - ElementClassicConnectionState.Error("No secrets received from Element Classic") - } else { - ElementClassicConnectionState.ElementClassicReady(userId, secrets) - } - } else { - ElementClassicConnectionState.ElementClassicReadyNoSession - } - } - } - } - - // Everything in this companion object must match what is defined in Element Classic - private companion object { - const val ELEMENT_CLASSIC_SERVICE_FULL_CLASS_NAME = "im.vector.app.features.importer.ImporterService" - - // Command to the service to get the data. - const val MSG_GET_DATA = 1 - - // Keys for the bundle returned from the service - const val KEY_ERROR_STR = "error" - const val KEY_USER_ID_STR = "userId" - - /** - * Key to extract the secrets from the bundle, as a Json string. - * Json will have this format: - * { - * "cross_signing" : { - * "master_key" : "z8RUxnaAGu___REDACTED___k+BQL9o", - * "user_signing_key" : "baJHzA___REDACTED___xMLbSUAXw9QUzqms", - * "self_signing_key" : "DU0CvLtR2G/___REDACTED___dV/MONNq4nsQhM" - * }, - * "backup" : { - * "algorithm" : "m.megolm_backup.v1.curve25519-aes-sha2", - * "key" : "VzncmQ+UOV___REDACTED___patxDz7m0Nc", - * "backup_version" : "1" - * } - * } - */ - const val KEY_SECRETS_STR = "secrets" - } -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/LoginWithClassicEvent.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/LoginWithClassicEvent.kt deleted file mode 100644 index 75a9496a02..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/LoginWithClassicEvent.kt +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations 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.login.impl.screens.onboarding.classic - -sealed interface LoginWithClassicEvent { - data object RefreshData : LoginWithClassicEvent - data object StartLoginWithClassic : LoginWithClassicEvent - data object DoLoginWithClassic : LoginWithClassicEvent - data object CloseDialog : LoginWithClassicEvent -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/LoginWithClassicPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/LoginWithClassicPresenter.kt deleted file mode 100644 index ef352794cb..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/LoginWithClassicPresenter.kt +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations 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.login.impl.screens.onboarding.classic - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import dev.zacsweers.metro.Inject -import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.featureflag.api.FeatureFlagService -import io.element.android.libraries.featureflag.api.FeatureFlags -import io.element.android.libraries.sessionstorage.api.SessionStore -import io.element.android.libraries.sessionstorage.api.toUserListFlow -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch - -@Inject -class LoginWithClassicPresenter( - private val elementClassicConnection: ElementClassicConnection, - private val sessionStore: SessionStore, - private val featureFlagService: FeatureFlagService, -) : Presenter { - @Composable - override fun present(): LoginWithClassicState { - val coroutineScope = rememberCoroutineScope() - - val isSignInWithClassicEnabled by remember { - featureFlagService.isFeatureEnabledFlow(FeatureFlags.SignInWithClassic) - }.collectAsState(initial = false) - - if (isSignInWithClassicEnabled) { - DisposableEffect(Unit) { - elementClassicConnection.start() - onDispose { - elementClassicConnection.stop() - } - } - } - - val state by elementClassicConnection.stateFlow.collectAsState() - val loginWithClassicAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } - - val existingSession by remember { - sessionStore.sessionsFlow().toUserListFlow() - }.collectAsState(emptyList()) - - val canLoginWithClassic by remember { - derivedStateOf { - when (val finalState = state) { - is ElementClassicConnectionState.ElementClassicReady -> { - // Ensure there is no existing session with the same Id. - finalState.userId.value !in existingSession && isSignInWithClassicEnabled - } - else -> false - } - } - } - - fun handleEvent(event: LoginWithClassicEvent) { - when (event) { - LoginWithClassicEvent.RefreshData -> { - elementClassicConnection.requestData() - } - LoginWithClassicEvent.StartLoginWithClassic -> { - val currentState = elementClassicConnection.stateFlow.value - if (currentState is ElementClassicConnectionState.ElementClassicReady) { - loginWithClassicAction.value = ConfirmingLoginWithElementClassic( - userId = currentState.userId, - ) - } else { - loginWithClassicAction.value = AsyncAction.Failure(IllegalStateException("Element Classic is not ready")) - } - } - LoginWithClassicEvent.DoLoginWithClassic -> coroutineScope.launch { - // TODO Implement real login logic here - loginWithClassicAction.value = AsyncAction.Loading - delay(1000) - loginWithClassicAction.value = AsyncAction.Success(Unit) - } - LoginWithClassicEvent.CloseDialog -> { - loginWithClassicAction.value = AsyncAction.Uninitialized - } - } - } - - return LoginWithClassicState( - canLoginWithClassic = canLoginWithClassic, - loginWithClassicAction = loginWithClassicAction.value, - eventSink = ::handleEvent, - ) - } -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/LoginWithClassicState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/LoginWithClassicState.kt deleted file mode 100644 index d2706fc24a..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/LoginWithClassicState.kt +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations 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.login.impl.screens.onboarding.classic - -import io.element.android.libraries.architecture.AsyncAction - -data class LoginWithClassicState( - val canLoginWithClassic: Boolean, - val loginWithClassicAction: AsyncAction, - val eventSink: (LoginWithClassicEvent) -> Unit, -) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/LoginWithClassicStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/LoginWithClassicStateProvider.kt deleted file mode 100644 index 73f68e5d61..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/LoginWithClassicStateProvider.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations 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.login.impl.screens.onboarding.classic - -import io.element.android.libraries.architecture.AsyncAction - -fun aLoginWithClassicState( - canLoginWithClassic: Boolean = false, - loginWithClassicAction: AsyncAction = AsyncAction.Uninitialized, - eventSink: (LoginWithClassicEvent) -> Unit = {}, -) = LoginWithClassicState( - canLoginWithClassic = canLoginWithClassic, - loginWithClassicAction = loginWithClassicAction, - eventSink = eventSink, -) diff --git a/features/login/impl/src/main/res/drawable-xxhdpi/element_foss_logo.png b/features/login/impl/src/main/res/drawable-xxhdpi/element_foss_logo.png new file mode 100644 index 0000000000..67684ee944 Binary files /dev/null and b/features/login/impl/src/main/res/drawable-xxhdpi/element_foss_logo.png differ diff --git a/features/login/impl/src/main/res/drawable-xxhdpi/element_pro_logo.png b/features/login/impl/src/main/res/drawable-xxhdpi/element_pro_logo.png new file mode 100644 index 0000000000..45d11b5f2e Binary files /dev/null and b/features/login/impl/src/main/res/drawable-xxhdpi/element_pro_logo.png differ diff --git a/features/login/impl/src/main/res/values-it/translations.xml b/features/login/impl/src/main/res/values-it/translations.xml index 310aa22906..0de1cc0690 100644 --- a/features/login/impl/src/main/res/values-it/translations.xml +++ b/features/login/impl/src/main/res/values-it/translations.xml @@ -60,6 +60,8 @@ "Richiesta di accesso annullata" "L\'accesso è stato rifiutato sull\'altro dispositivo." "Accesso rifiutato" + "Non devi fare altro." + "L\'altro tuo dispositivo è già connesso" "L\'accesso è scaduto. Riprova." "L\'accesso non è stato completato in tempo" "L\'altro dispositivo non supporta l\'accesso a %s con un codice QR. diff --git a/features/login/impl/src/main/res/values-ja/translations.xml b/features/login/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..e2c89ba26e --- /dev/null +++ b/features/login/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,99 @@ + + + "アカウントの提供元を変更" + "ホームサーバーのアドレス" + "検索用のキーワードまたはドメインのアドレスを入力してください。" + "会社やコミュニティ, 個人のサーバーなどを検索します。" + "アカウントの提供元を検索" + "メールアプリのように、あなたの会話はここに保管されています。" + "%s にサインインを試みています" + "メールアプリのように、あなたの会話はここに保管されています。" + "%s にアカウントの作成を試みています" + "Matrix.org は Matrix.org Foundation が運営する、大規模で安全な分散型コミュニケーションを実現する無償のサーバーです。" + "その他" + "自身のサーバーや仕事用のアカウントにサインインするには、アカウント提供元のサーバーを指定してください。" + "アカウントの提供元を変更" + "Google Play" + "%1$s では Element Pro を使用する必要があります。アプリストアよりダウンロードしてください。" + "Element Pro が必要です" + "このホームサーバーに接続できませんでした。正しいURLを入力したことを確認し、問題が継続する場合は、ホームサーバーの管理者に問い合わせてください。" + ".well-knownファイルに問題があるためサーバーを使用できません: %1$s" + "このアカウント提供元は、スライド同期に対応していません。%1$s を使用するにはサーバーのアップグレードが必要です。" + "%2$s は %1$s からの接続を許可していません。" + "このアプリは次のサーバーを許可します: %1$s" + "アカウント提供元 %1$s は許可されていません。" + "ホームサーバーURL" + "ドメイン名を入力してください" + "サーバーのアドレスは何ですか?" + "サーバーを選択" + "アカウントを作成" + "このアカウントは無効化されています。" + "ユーザー名またはパスワードが違います" + "無効なユーザーIDです。正しい形式は \"@ユーザー:ホームサーバー\" です。" + "このサーバーはリフレッシュトークンを使用します。パスワードを使用したログインとは併用できません。" + "指定したホームサーバはパスワードまたはOIDCによるログインに対応していません。管理者に問い合わせるか、異なるホームサーバーを使用してください。" + "詳細を入力" + "Matrix は安全で分散型のオープンなネットワークです。" + "お待ちしておりました。" + "%1$s にサインイン" + "バージョン %1$s" + "手動で指定してサインイン" + "%1$s にサインイン" + "QRコードでサインイン" + "アカウントを作成" + "最速の %1$s にようこそ。機能性と利便性を極限まで追求しました。" + "機敏と利便を追求した %1$s へようこそ。" + "Be in your element" + "安全な通信を確立しています" + "新しい端末で安全な通信を確立できませんでした。既存の端末は安全な状態を維持しています。" + "どうしますか?" + "ネットワークの問題の可能性があるため、再度QRコードでログインを試してください。" + "同様の問題が発生する場合は、異なるWi-Fiやモバイルデータ通信を試してください" + "問題が解決しない場合は、手動でサインインしてください" + "接続が安全ではありません" + "この端末に表示される2つの数字の入力を要求されます" + "もう一方に表示される数字を入力してください" + "他の端末にサインインしてからもう一度試すか、既にサインインしてある端末を使用してください" + "他の端末でサインインしていません" + "もう一方の端末がサインインをキャンセルしました" + "サインインのリクエストがキャンセルされました" + "もう一方の端末でサインインを拒否されました" + "サインインを拒否" + "他には何もする必要はありません。" + "他の端末で既にサインインしています" + "サインインが無効です。もう一度試してください。" + "サインインが時間内に完了しませんでした" + "QRコードを使用した %s へのサインインに他の端末が対応していません。 + +異なる端末でQRコードを読み取るか、手動でサインインしてください。" + "QRコードに非対応" + "アカウント提供元が %1$s に対応していません。" + "%1$s に非対応" + "読み取る" + "コンピュータで %1$s を開く" + "アバターをタップしてください" + "%1$s を選択してください" + "\"新しい端末を追加\"" + "この端末でQRコードを読み取る" + "アカウント提供元が対応する場合にのみ使用できます。" + "他の端末の %1$s でQRコードを表示" + "もう一方の端末に表示されているQRコードを使用してください" + "もう一度やり直してください" + "QRコードが間違っています" + "カメラの設定を開く" + "続行するには、%1$s にカメラの使用を許可する必要があります。" + "QRコードを読み取るため、カメラへのアクセスを許可" + "QRコードを読み取り" + "やり直す" + "予期せぬ問題が発生しました。もう一度試してください。" + "一方の端末を待機しています" + "アカウント提供元が、サインインを検証するために以下の文字列を要求することがあります。" + "検証コード" + "アカウントの提供元を変更" + "Element 開発者用の非公開のサーバーです。" + "Matrix は安全で分散型のオープンなネットワークです。" + "メールアプリのように、あなたの会話はここに保管されています。" + "%1$s にサインインを試みています" + "アカウント提供元を選択" + "%1$s 上にアカウントの作成を試みています" + diff --git a/features/login/impl/src/main/res/values-lt/translations.xml b/features/login/impl/src/main/res/values-lt/translations.xml index 35e82d5a58..f8f243f29d 100644 --- a/features/login/impl/src/main/res/values-lt/translations.xml +++ b/features/login/impl/src/main/res/values-lt/translations.xml @@ -4,17 +4,29 @@ "Pagrindinio serverio adresas" "Įveskite paieškos terminą arba domeno adresą." "Ieškokite bendrovės, bendruomenės arba privataus serverio." - "Rasti paskyros teikėją" + "Raskite paskyros teikėją" "Čia bus saugomi Jūsų pokalbiai - panašiai kaip el. pašto paslaugų teikėjas saugo Jūsų el. laiškus." "Ketinate prisijungti prie %s" "Čia bus saugomi Jūsų pokalbiai - panašiai kaip el. pašto paslaugų teikėjas saugo Jūsų el. laiškus." "Ketinate sukurti paskyrą teikėjoje %s" - "Kita" - "Naudokite skirtingą paskyros teikėją, pavyzdžiui, savo privatų serverį arba darbo paskyrą." + "Matrix.org – tai didelis nemokamas serveris viešajame „Matrix“ tinkle saugiam ir decentralizuotam bendravimui, kurį valdo „Matrix.org“ fondas." + "Kitas" + "Naudokite kitą paskyros teikėją, pavyzdžiui, savo privatų serverį arba darbo paskyrą." "Keisti paskyros teikėją" - "Nepavyko pasiekti šio serverio. Patikrinkite, ar teisingai įvedėte serverio URL. Jei URL yra teisingas, susisiekite su serverio administracija dėl tolimesnės pagalbos." - "Serverio URL" - "Koks yra Jūsų serverio adresas?" + "„Google Play“" + "„Element Pro“ programa privaloma teikėjoje %1$s. Atsisiųskite ją iš parduotuvės." + "„Element Pro“ privaloma" + "Nepavyko pasiekti šio pagrindinio serverio. Patikrinkite, ar teisingai įvedėte serverio URL. Jei URL yra teisingas, susisiekite su serverio administratoriumi dėl tolimesnės pagalbos." + "Serveris nepasiekiamas dėl problemos .labai-zinomame faile: +%1$s" + "Pasirinktas paskyros teikėjas nepalaiko slankiojo sinchronizavimo. Norint naudoti „%1$s“, reikia atnaujinti serverį." + "„%1$s“ neleidžiama prisijungti prie %2$s." + "Ši programa sukonfigūruota, kad leistų %1$s." + "Paskyros teikėjas %1$s neleidžiamas." + "Pagrindinio serverio URL" + "Įveskite domeno adresą." + "Koks yra jūsų serverio adresas?" + "Pasirinkite savo serverį" "Kurti paskyrą" "Ši paskyra buvo išjungta." "Neteisingas vartotojo vardas ir (arba) slaptažodis" @@ -31,7 +43,7 @@ "Kurti paskyrą" "Sveiki atvykę į sparčiausią „%1$s“ kada nors. Pagerintas spartai ir paprastumui." "Sveiki atvykę į „%1$s“. Pagerintas spartai ir paprastumui." - "Būkite savo elemente" + "Būkite savo stichijoje" "Keisti paskyros teikėją" "Privatus serveris “Element” darbuotojams." "Matrix yra atviras tinklas, skirtas saugiam, decentralizuotam bendravimui." diff --git a/features/login/impl/src/main/res/values-vi/translations.xml b/features/login/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..b22ba5de7c --- /dev/null +++ b/features/login/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,47 @@ + + + "Thay đổi nhà cung cấp tài khoản" + "Địa chỉ Homeserver" + "Nhập từ khóa tìm kiếm hoặc địa chỉ tên miền." + "Tìm kiếm công ty, cộng đồng hoặc máy chủ riêng." + "Tìm nhà cung cấp tài khoản" + "Đây là nơi các cuộc trò chuyện của bạn sẽ được lưu — giống như bạn dùng nhà cung cấp email để giữ email của mình." + "Bạn sắp đăng nhập vào %s" + "Đây là nơi các cuộc trò chuyện của bạn sẽ được lưu — giống như bạn dùng nhà cung cấp email để giữ email của mình." + "Bạn sắp tạo tài khoản trên %s" + "Matrix.org là một máy chủ lớn, miễn phí trên mạng Matrix công cộng, cung cấp liên lạc an toàn và phi tập trung, được điều hành bởi Quỹ Matrix.org." + "Khác" + "Sử dụng nhà cung cấp tài khoản khác, ví dụ như máy chủ riêng của bạn hoặc tài khoản công việc." + "Thay đổi nhà cung cấp tài khoản" + "Chúng tớ không thể kết nối với homeserver này. Vui lòng kiểm tra xem cậu đã nhập URL homeserver chính xác chưa. Nếu URL chính xác, hãy liên hệ với quản trị viên homeserver để được hỗ trợ thêm." + "Máy chủ không khả dụng do sự cố trong tệp .well-known: +%1$s" + "URL homeserver" + "Địa chỉ máy chủ của bạn là gì?" + "Chọn máy chủ của bạn" + "Tạo tài khoản" + "Tài khoản này đã bị vô hiệu hóa." + "Tên người dùng và/hoặc mật khẩu không chính xác" + "Đây không phải là mã nhận dạng người dùng hợp lệ. Định dạng mong đợi: ‘@user:homeserver.org’" + "Máy chủ này được cấu hình sử dụng refresh token. Điều này không được hỗ trợ khi đăng nhập bằng mật khẩu." + "Homeserver đã chọn không hỗ trợ đăng nhập bằng mật khẩu hoặc OIDC. Vui lòng liên hệ với quản trị viên của cậu hoặc chọn một homeserver khác." + "Nhập thông tin chi tiết của bạn." + "Matrix là một mạng mở cho việc liên lạc an toàn và phi tập trung." + "Chào mừng bạn quay trở lại!" + "Đăng nhập vào %1$s" + "Đăng nhập thủ công" + "Đăng nhập vào %1$s" + "Đăng nhập bằng mã QR" + "Tạo tài khoản" + "Chào mừng đến với %1$s nhanh nhất từ trước đến nay. Tối ưu cho tốc độ và sự đơn giản." + "Chào mừng đến với %1$s. Tối ưu hóa cho tốc độ và sự đơn giản." + "Hãy ở trong thế mạnh (element) của mình" + "Thử lại" + "Bắt đầu lại" + "Thay đổi nhà cung cấp tài khoản" + "Máy chủ riêng dành cho nhân viên của Element." + "Matrix là một mạng mở cho việc liên lạc an toàn và phi tập trung." + "Đây là nơi các cuộc trò chuyện của bạn sẽ được lưu — giống như bạn dùng nhà cung cấp email để giữ email của mình." + "Bạn sắp đăng nhập vào %1$s" + "Bạn sắp tạo tài khoản trên %1$s" + diff --git a/features/login/impl/src/main/res/values/localazy.xml b/features/login/impl/src/main/res/values/localazy.xml index 832c3b7f71..b4dee32721 100644 --- a/features/login/impl/src/main/res/values/localazy.xml +++ b/features/login/impl/src/main/res/values/localazy.xml @@ -37,11 +37,19 @@ "Matrix is an open network for secure, decentralised communication." "Welcome back!" "Sign in to %1$s" + "Open Element Classic" + "Open Element Classic on your device" + "Go to Settings > Security & Privacy" + "In Cryptography keys management, select Encrypted message recovery" + "Follow the instructions to enable your key storage" + "Come back to %1$s" + "Enable your key storage before proceeding to %1$s" "Version %1$s" "Sign in manually" "Sign in to %1$s" "Sign in with QR code" "Create account" + "Welcome back" "Welcome to the fastest %1$s ever. Supercharged for speed and simplicity." "Welcome to %1$s. Supercharged, for speed and simplicity." "Be in your element" diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/DefaultLoginEntryPointTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/DefaultLoginEntryPointTest.kt index 953693b40d..86a629270f 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/DefaultLoginEntryPointTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/DefaultLoginEntryPointTest.kt @@ -15,6 +15,8 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.login.api.LoginEntryPoint import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource +import io.element.android.features.login.impl.classic.FakeElementClassicConnection +import io.element.android.features.preferences.test.FakePreferencesEntryPoint import io.element.android.libraries.oidc.test.customtab.FakeOidcActionFlow import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.node.TestParentNode @@ -39,6 +41,8 @@ class DefaultLoginEntryPointTest { accountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()), oidcActionFlow = FakeOidcActionFlow(), appCoroutineScope = backgroundScope, + elementClassicConnection = FakeElementClassicConnection(), + preferencesEntryPoint = FakePreferencesEntryPoint(), ) } val callback = object : LoginEntryPoint.Callback { diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/classic/DefaultElementClassicConnectionTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/classic/DefaultElementClassicConnectionTest.kt new file mode 100644 index 0000000000..5da3c97f3c --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/classic/DefaultElementClassicConnectionTest.kt @@ -0,0 +1,530 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.login.impl.classic + +import android.graphics.Bitmap +import android.os.Bundle +import androidx.core.graphics.createBitmap +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.androidutils.service.ServiceBinder +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +import io.element.android.libraries.matrix.api.auth.ElementClassicSession +import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker +import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import io.element.android.libraries.matrix.test.A_FAILURE_REASON +import io.element.android.libraries.matrix.test.A_HOMESERVER_URL +import io.element.android.libraries.matrix.test.A_SECRET +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.A_USER_ID_2 +import io.element.android.libraries.matrix.test.A_USER_NAME +import io.element.android.libraries.matrix.test.auth.FakeHomeServerLoginCompatibilityChecker +import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class DefaultElementClassicConnectionTest { + @Test + fun `connection can be started Element Classic service can be bound`() = runTest { + val connection = createDefaultElementClassicConnection( + serviceBinder = FakeServiceBinder( + bindServiceResult = { + // Element Classic is found + true + }, + ), + ) + connection.stateFlow.test { + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.Idle) + connection.start() + runCurrent() + expectNoEvents() + } + } + + @Test + fun `connection can be started Element Classic service cannot be bound`() = runTest { + val setElementClassicSessionResult = lambdaRecorder { } + val connection = createDefaultElementClassicConnection( + serviceBinder = FakeServiceBinder( + bindServiceResult = { + // Element Classic not found + false + }, + ), + matrixAuthenticationService = FakeMatrixAuthenticationService( + setElementClassicSessionResult = setElementClassicSessionResult, + ), + ) + connection.stateFlow.test { + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.Idle) + connection.start() + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.ElementClassicNotFound) + setElementClassicSessionResult.assertions().isCalledOnce().with(value(null)) + } + } + + @Test + fun `connection cannot be started in case of security error`() = runTest { + val setElementClassicSessionResult = lambdaRecorder { } + val connection = createDefaultElementClassicConnection( + serviceBinder = FakeServiceBinder( + bindServiceResult = { throw SecurityException(A_FAILURE_REASON) }, + ), + matrixAuthenticationService = FakeMatrixAuthenticationService( + setElementClassicSessionResult = setElementClassicSessionResult, + ), + ) + connection.stateFlow.test { + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.Idle) + connection.start() + assertThat(awaitItem()).isInstanceOf(ElementClassicConnectionState.Error::class.java) + setElementClassicSessionResult.assertions().isCalledOnce().with(value(null)) + } + } + + @Test + fun `requestSession when messenger is not ready has no effect`() = runTest { + val connection = createDefaultElementClassicConnection() + connection.stateFlow.test { + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.Idle) + connection.requestSession() + runCurrent() + expectNoEvents() + } + } + + @Test + fun `requestSession when the feature is disabled emits an error`() = runTest { + val connection = createDefaultElementClassicConnection( + matrixAuthenticationService = FakeMatrixAuthenticationService( + setElementClassicSessionResult = {}, + ), + isFeatureEnabled = false, + ) + connection.stateFlow.test { + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.Idle) + connection.requestSession() + assertThat(awaitItem()).isInstanceOf(ElementClassicConnectionState.Error::class.java) + } + } + + @Test + fun `when an error is received, an error is emitted`() = runTest { + val connection = createDefaultElementClassicConnection( + matrixAuthenticationService = FakeMatrixAuthenticationService( + setElementClassicSessionResult = {}, + ), + ) + connection.stateFlow.test { + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.Idle) + // Simulate receiving a session from Element Classic + connection.onSessionReceived( + Bundle().apply { + putString(DefaultElementClassicConnection.KEY_ERROR_STR, A_FAILURE_REASON) + } + ) + assertThat(awaitItem()).isInstanceOf(ElementClassicConnectionState.Error::class.java) + } + } + + @Test + fun `when there is no Element Classic session, ElementClassicReadyNoSession is emitted`() = runTest { + val connection = createDefaultElementClassicConnection( + matrixAuthenticationService = FakeMatrixAuthenticationService( + setElementClassicSessionResult = {}, + ), + ) + connection.stateFlow.test { + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.Idle) + // Simulate receiving no session from Element Classic + connection.onSessionReceived(Bundle()) + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.ElementClassicReadyNoSession) + } + } + + @Test + fun `when there is Element Classic session with empty userId, ElementClassicReadyNoSession is emitted`() = runTest { + val connection = createDefaultElementClassicConnection( + matrixAuthenticationService = FakeMatrixAuthenticationService( + setElementClassicSessionResult = {}, + ), + ) + connection.stateFlow.test { + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.Idle) + // Simulate receiving empty userId from Element Classic + connection.onSessionReceived(Bundle().apply { + putString(DefaultElementClassicConnection.KEY_USER_ID_STR, "") + }) + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.ElementClassicReadyNoSession) + } + } + + @Test + fun `when session is received, but homeserver is not supported, an error is emitted`() = runTest { + val connection = createDefaultElementClassicConnection( + homeServerLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker( + checkResult = { Result.success(false) } + ), + matrixAuthenticationService = FakeMatrixAuthenticationService( + setElementClassicSessionResult = {}, + ), + ) + connection.stateFlow.test { + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.Idle) + // Simulate receiving a session from Element Classic + connection.onSessionReceived( + Bundle().apply { + putString(DefaultElementClassicConnection.KEY_USER_ID_STR, A_USER_ID.value) + } + ) + assertThat(awaitItem()).isInstanceOf(ElementClassicConnectionState.Error::class.java) + } + } + + @Test + fun `when session is received without secrets, and homeserver is supported, ElementClassicReady is emitted`() = runTest { + val connection = createDefaultElementClassicConnection( + homeServerLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker( + checkResult = { Result.success(true) } + ), + matrixAuthenticationService = FakeMatrixAuthenticationService( + setElementClassicSessionResult = {}, + ), + ) + connection.stateFlow.test { + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.Idle) + // Simulate receiving a session from Element Classic + connection.onSessionReceived( + Bundle().apply { + putString(DefaultElementClassicConnection.KEY_USER_ID_STR, A_USER_ID.value) + } + ) + assertThat(awaitItem()).isEqualTo( + ElementClassicConnectionState.ElementClassicReady( + elementClassicSession = ElementClassicSession( + userId = A_USER_ID, + homeserverUrl = null, + secrets = null, + roomKeysVersion = null, + doesContainBackupKey = false, + ), + displayName = null, + avatar = null, + ) + ) + } + } + + @Test + fun `when session is received with all data including key backup, and homeserver is supported, ElementClassicReady is emitted`() { + `when session is received with all data, and homeserver is supported, ElementClassicReady is emitted`( + withKeyBackup = true, + ) + } + + @Test + fun `when session is received with all data without key backup, and homeserver is supported, ElementClassicReady is emitted - backup key is missing`() { + `when session is received with all data, and homeserver is supported, ElementClassicReady is emitted`( + withKeyBackup = false, + ) + } + + private fun `when session is received with all data, and homeserver is supported, ElementClassicReady is emitted`( + withKeyBackup: Boolean, + ) = runTest { + val connection = createDefaultElementClassicConnection( + homeServerLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker( + checkResult = { Result.success(true) } + ), + matrixAuthenticationService = FakeMatrixAuthenticationService( + setElementClassicSessionResult = {}, + doSecretsContainBackupKeyResult = { _, _, _ -> withKeyBackup }, + ), + ) + connection.stateFlow.test { + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.Idle) + // Simulate receiving a session from Element Classic + connection.onSessionReceived( + Bundle().apply { + putString(DefaultElementClassicConnection.KEY_USER_ID_STR, A_USER_ID.value) + putString(DefaultElementClassicConnection.KEY_HOMESERVER_URL_STR, A_HOMESERVER_URL) + putString(DefaultElementClassicConnection.KEY_SECRETS_STR, A_SECRET) + putString(DefaultElementClassicConnection.KEY_ROOM_KEYS_VERSION_STR, ROOM_KEYS_VERSION) + putString(DefaultElementClassicConnection.KEY_USER_DISPLAY_NAME_STR, A_USER_NAME) + } + ) + assertThat(awaitItem()).isEqualTo( + ElementClassicConnectionState.ElementClassicReady( + elementClassicSession = ElementClassicSession( + userId = A_USER_ID, + homeserverUrl = A_HOMESERVER_URL, + secrets = A_SECRET, + roomKeysVersion = ROOM_KEYS_VERSION, + doesContainBackupKey = withKeyBackup, + ), + displayName = A_USER_NAME, + avatar = null, + ) + ) + } + } + + @Test + fun `when session is received with secret but without room keys version Element Classic is outdated and the secret is ignored`() = runTest { + val connection = createDefaultElementClassicConnection( + homeServerLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker( + checkResult = { Result.success(true) } + ), + matrixAuthenticationService = FakeMatrixAuthenticationService( + setElementClassicSessionResult = {}, + ), + ) + connection.stateFlow.test { + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.Idle) + // Simulate receiving a session from Element Classic + connection.onSessionReceived( + Bundle().apply { + putString(DefaultElementClassicConnection.KEY_USER_ID_STR, A_USER_ID.value) + putString(DefaultElementClassicConnection.KEY_HOMESERVER_URL_STR, A_HOMESERVER_URL) + putString(DefaultElementClassicConnection.KEY_SECRETS_STR, A_SECRET) + putString(DefaultElementClassicConnection.KEY_ROOM_KEYS_VERSION_STR, null) + putString(DefaultElementClassicConnection.KEY_USER_DISPLAY_NAME_STR, A_USER_NAME) + } + ) + assertThat(awaitItem()).isEqualTo( + ElementClassicConnectionState.ElementClassicReady( + elementClassicSession = ElementClassicSession( + userId = A_USER_ID, + homeserverUrl = A_HOMESERVER_URL, + secrets = null, + roomKeysVersion = null, + doesContainBackupKey = false, + ), + displayName = A_USER_NAME, + avatar = null, + ) + ) + } + } + + @Test + fun `when session is received with secret but with empty room keys version, doesContainBackupKey is false`() = runTest { + val connection = createDefaultElementClassicConnection( + homeServerLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker( + checkResult = { Result.success(true) } + ), + matrixAuthenticationService = FakeMatrixAuthenticationService( + setElementClassicSessionResult = {}, + ), + ) + connection.stateFlow.test { + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.Idle) + // Simulate receiving a session from Element Classic + connection.onSessionReceived( + Bundle().apply { + putString(DefaultElementClassicConnection.KEY_USER_ID_STR, A_USER_ID.value) + putString(DefaultElementClassicConnection.KEY_HOMESERVER_URL_STR, A_HOMESERVER_URL) + putString(DefaultElementClassicConnection.KEY_SECRETS_STR, A_SECRET) + putString(DefaultElementClassicConnection.KEY_ROOM_KEYS_VERSION_STR, "") + putString(DefaultElementClassicConnection.KEY_USER_DISPLAY_NAME_STR, A_USER_NAME) + } + ) + assertThat(awaitItem()).isEqualTo( + ElementClassicConnectionState.ElementClassicReady( + elementClassicSession = ElementClassicSession( + userId = A_USER_ID, + homeserverUrl = A_HOMESERVER_URL, + secrets = A_SECRET, + roomKeysVersion = null, + doesContainBackupKey = false, + ), + displayName = A_USER_NAME, + avatar = null, + ) + ) + } + } + + @Test + fun `when session is received with empty data, and homeserver is supported, ElementClassicReady is emitted`() = runTest { + val connection = createDefaultElementClassicConnection( + homeServerLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker( + checkResult = { Result.success(true) } + ), + matrixAuthenticationService = FakeMatrixAuthenticationService( + setElementClassicSessionResult = {}, + ), + ) + connection.stateFlow.test { + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.Idle) + // Simulate receiving a session from Element Classic + connection.onSessionReceived( + Bundle().apply { + putString(DefaultElementClassicConnection.KEY_USER_ID_STR, A_USER_ID.value) + putString(DefaultElementClassicConnection.KEY_HOMESERVER_URL_STR, "") + putString(DefaultElementClassicConnection.KEY_SECRETS_STR, "") + putString(DefaultElementClassicConnection.KEY_USER_DISPLAY_NAME_STR, "") + } + ) + assertThat(awaitItem()).isEqualTo( + ElementClassicConnectionState.ElementClassicReady( + elementClassicSession = ElementClassicSession( + userId = A_USER_ID, + homeserverUrl = null, + secrets = null, + roomKeysVersion = null, + doesContainBackupKey = false, + ), + displayName = null, + avatar = null, + ) + ) + } + } + + @Test + fun `when avatar is received when the state is not ElementClassicReady, nothing happen`() = runTest { + val connection = createDefaultElementClassicConnection( + homeServerLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker( + checkResult = { Result.success(true) } + ), + matrixAuthenticationService = FakeMatrixAuthenticationService( + setElementClassicSessionResult = {}, + ), + ) + connection.stateFlow.test { + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.Idle) + // Simulate receiving an avatar from Element Classic + connection.onAvatarReceived(Bundle().apply { + putString(DefaultElementClassicConnection.KEY_USER_ID_STR, A_USER_ID.value) + putParcelable(DefaultElementClassicConnection.KEY_USER_AVATAR_PARCELABLE, createBitmap(1, 1, Bitmap.Config.ARGB_8888)) + }) + runCurrent() + expectNoEvents() + } + } + + @Test + fun `when avatar is received when the state is ElementClassicReady with a different user, nothing happen`() = runTest { + val connection = createDefaultElementClassicConnection( + homeServerLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker( + checkResult = { Result.success(true) } + ), + matrixAuthenticationService = FakeMatrixAuthenticationService( + setElementClassicSessionResult = {}, + ), + ) + connection.stateFlow.test { + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.Idle) + // Simulate receiving a session from Element Classic + connection.onSessionReceived( + Bundle().apply { + putString(DefaultElementClassicConnection.KEY_USER_ID_STR, A_USER_ID.value) + } + ) + assertThat(awaitItem()).isEqualTo( + ElementClassicConnectionState.ElementClassicReady( + elementClassicSession = ElementClassicSession( + userId = A_USER_ID, + homeserverUrl = null, + secrets = null, + roomKeysVersion = null, + doesContainBackupKey = false, + ), + displayName = null, + avatar = null, + ) + ) + // Simulate receiving an avatar for another user from Element Classic + connection.onAvatarReceived(Bundle().apply { + putString(DefaultElementClassicConnection.KEY_USER_ID_STR, A_USER_ID_2.value) + putParcelable(DefaultElementClassicConnection.KEY_USER_AVATAR_PARCELABLE, createBitmap(1, 1, Bitmap.Config.ARGB_8888)) + }) + runCurrent() + expectNoEvents() + } + } + + @Test + fun `when avatar is received, the state is updated`() = runTest { + val connection = createDefaultElementClassicConnection( + homeServerLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker( + checkResult = { Result.success(true) } + ), + matrixAuthenticationService = FakeMatrixAuthenticationService( + setElementClassicSessionResult = {}, + ), + ) + connection.stateFlow.test { + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.Idle) + // Simulate receiving a session from Element Classic + connection.onSessionReceived( + Bundle().apply { + putString(DefaultElementClassicConnection.KEY_USER_ID_STR, A_USER_ID.value) + } + ) + assertThat(awaitItem()).isEqualTo( + ElementClassicConnectionState.ElementClassicReady( + elementClassicSession = ElementClassicSession( + userId = A_USER_ID, + homeserverUrl = null, + secrets = null, + roomKeysVersion = null, + doesContainBackupKey = false, + ), + displayName = null, + avatar = null, + ) + ) + // Simulate receiving an avatar from Element Classic + connection.onAvatarReceived(Bundle().apply { + putString(DefaultElementClassicConnection.KEY_USER_ID_STR, A_USER_ID.value) + putParcelable(DefaultElementClassicConnection.KEY_USER_AVATAR_PARCELABLE, createBitmap(1, 1, Bitmap.Config.ARGB_8888)) + }) + assertThat((awaitItem() as? ElementClassicConnectionState.ElementClassicReady)?.avatar).isNotNull() + } + } + + private fun TestScope.createDefaultElementClassicConnection( + serviceBinder: ServiceBinder = FakeServiceBinder( + bindServiceResult = { true }, + unbindServiceResult = { }, + ), + coroutineScope: CoroutineScope = backgroundScope, + matrixAuthenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(), + homeServerLoginCompatibilityChecker: HomeServerLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker( + checkResult = { Result.success(true) } + ), + isFeatureEnabled: Boolean = true, + featureFlagService: FeatureFlagService = FakeFeatureFlagService( + initialState = mapOf( + FeatureFlags.SignInWithClassic.key to isFeatureEnabled, + ) + ), + ) = DefaultElementClassicConnection( + serviceBinder = serviceBinder, + coroutineScope = coroutineScope, + matrixAuthenticationService = matrixAuthenticationService, + homeServerLoginCompatibilityChecker = homeServerLoginCompatibilityChecker, + featureFlagService = featureFlagService, + ) +} diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/FakeElementClassicConnection.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/classic/FakeElementClassicConnection.kt similarity index 84% rename from features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/FakeElementClassicConnection.kt rename to features/login/impl/src/test/kotlin/io/element/android/features/login/impl/classic/FakeElementClassicConnection.kt index 2c41d2ed0f..227aa514b3 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/FakeElementClassicConnection.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/classic/FakeElementClassicConnection.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.login.impl.screens.onboarding.classic +package io.element.android.features.login.impl.classic import io.element.android.tests.testutils.lambda.lambdaError import kotlinx.coroutines.flow.MutableStateFlow @@ -15,12 +15,12 @@ import kotlinx.coroutines.flow.asStateFlow class FakeElementClassicConnection( private val startResult: () -> Unit = { lambdaError() }, private val stopResult: () -> Unit = { lambdaError() }, - private val requestDataResult: () -> Unit = { lambdaError() }, + private val requestSessionResult: () -> Unit = { lambdaError() }, initialState: ElementClassicConnectionState = ElementClassicConnectionState.Idle ) : ElementClassicConnection { override fun start() = startResult() override fun stop() = stopResult() - override fun requestData() = requestDataResult() + override fun requestSession() = requestSessionResult() private val mutableStateFlow = MutableStateFlow(initialState) override val stateFlow: StateFlow = mutableStateFlow.asStateFlow() suspend fun emitState(state: ElementClassicConnectionState) { diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/classic/FakeServiceBinder.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/classic/FakeServiceBinder.kt new file mode 100644 index 0000000000..0a24f13fa7 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/classic/FakeServiceBinder.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2026 Element Creations 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.login.impl.classic + +import android.content.Intent +import android.content.ServiceConnection +import io.element.android.libraries.androidutils.service.ServiceBinder +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeServiceBinder( + private val bindServiceResult: () -> Boolean = { lambdaError() }, + private val unbindServiceResult: () -> Unit = { lambdaError() }, +) : ServiceBinder { + override fun bindService(service: Intent, conn: ServiceConnection, flags: Int): Boolean { + return bindServiceResult() + } + + override fun unbindService(conn: ServiceConnection) { + unbindServiceResult() + } +} diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/classic/Fixtures.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/classic/Fixtures.kt new file mode 100644 index 0000000000..9039743fd5 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/classic/Fixtures.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2026 Element Creations 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.login.impl.classic + +import android.graphics.Bitmap +import io.element.android.libraries.matrix.api.auth.ElementClassicSession +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.test.A_USER_ID + +internal const val ROOM_KEYS_VERSION = "roomKeysVersion as Json data" + +fun anElementClassicReady( + elementClassicSession: ElementClassicSession = anElementClassicSession(), + displayName: String? = null, + avatar: Bitmap? = null, +) = ElementClassicConnectionState.ElementClassicReady( + elementClassicSession = elementClassicSession, + displayName = displayName, + avatar = avatar, +) + +fun anElementClassicSession( + userId: UserId = A_USER_ID, + homeserverUrl: String? = null, + secrets: String? = null, + roomKeysVersion: String? = null, + doesContainBackupKey: Boolean = false, +) = ElementClassicSession( + userId = userId, + homeserverUrl = homeserverUrl, + secrets = secrets, + roomKeysVersion = roomKeysVersion, + doesContainBackupKey = doesContainBackupKey, +) diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/ClassicFlowNodeHelperTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/ClassicFlowNodeHelperTest.kt new file mode 100644 index 0000000000..017fd1b633 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/ClassicFlowNodeHelperTest.kt @@ -0,0 +1,287 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.login.impl.screens.classic + +import androidx.core.graphics.createBitmap +import androidx.test.ext.junit.runners.AndroidJUnit4 +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.features.login.impl.classic.ElementClassicConnection +import io.element.android.features.login.impl.classic.ElementClassicConnectionState +import io.element.android.features.login.impl.classic.FakeElementClassicConnection +import io.element.android.features.login.impl.classic.anElementClassicReady +import io.element.android.features.login.impl.classic.anElementClassicSession +import io.element.android.libraries.matrix.test.A_FAILURE_REASON +import io.element.android.libraries.matrix.test.A_SECRET +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.A_USER_ID_2 +import io.element.android.libraries.sessionstorage.api.SessionStore +import io.element.android.libraries.sessionstorage.test.InMemorySessionStore +import io.element.android.libraries.sessionstorage.test.aSessionData +import io.element.android.tests.testutils.WarmUpRule +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +// Use AndroidJUnit4 for the test with the Bitmap. +@RunWith(AndroidJUnit4::class) +class ClassicFlowNodeHelperTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `after a few seconds in Idle, NavigateToOnBoarding is emitted`() = runTest { + createHelper() + .navigationEventFlow() + .test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo(NavigationEvent.Idle) + val finalState = awaitItem() + assertThat(finalState).isEqualTo(NavigationEvent.NavigateToOnBoarding) + advanceTimeBy(10_000) + expectNoEvents() + } + } + + @Test + fun `navigate to onboarding if a session with the same account already exists`() = runTest { + val elementClassicConnection = FakeElementClassicConnection() + createHelper( + elementClassicConnection = elementClassicConnection, + sessionStore = InMemorySessionStore( + initialList = listOf( + aSessionData( + sessionId = A_USER_ID.value, + ) + ) + ), + ) + .navigationEventFlow() + .test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo(NavigationEvent.Idle) + elementClassicConnection.emitState( + anElementClassicReady() + ) + val finalState = awaitItem() + assertThat(finalState).isEqualTo(NavigationEvent.NavigateToOnBoarding) + advanceTimeBy(10_000) + expectNoEvents() + } + } + + @Test + fun `navigate to onboarding if Element Classic is not found`() = runTest { + val elementClassicConnection = FakeElementClassicConnection() + createHelper( + elementClassicConnection = elementClassicConnection, + ) + .navigationEventFlow() + .test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo(NavigationEvent.Idle) + elementClassicConnection.emitState( + ElementClassicConnectionState.ElementClassicNotFound + ) + val finalState = awaitItem() + assertThat(finalState).isEqualTo(NavigationEvent.NavigateToOnBoarding) + advanceTimeBy(10_000) + expectNoEvents() + } + } + + @Test + fun `navigate to onboarding if Element Classic has no session`() = runTest { + val elementClassicConnection = FakeElementClassicConnection() + createHelper( + elementClassicConnection = elementClassicConnection, + ) + .navigationEventFlow() + .test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo(NavigationEvent.Idle) + elementClassicConnection.emitState( + ElementClassicConnectionState.ElementClassicReadyNoSession + ) + val finalState = awaitItem() + assertThat(finalState).isEqualTo(NavigationEvent.NavigateToOnBoarding) + advanceTimeBy(10_000) + expectNoEvents() + } + } + + @Test + fun `navigate to onboarding if there has been an error`() = runTest { + val elementClassicConnection = FakeElementClassicConnection() + createHelper( + elementClassicConnection = elementClassicConnection, + ) + .navigationEventFlow() + .test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo(NavigationEvent.Idle) + elementClassicConnection.emitState( + ElementClassicConnectionState.Error(A_FAILURE_REASON) + ) + val finalState = awaitItem() + assertThat(finalState).isEqualTo(NavigationEvent.NavigateToOnBoarding) + advanceTimeBy(10_000) + expectNoEvents() + } + } + + @Test + fun `navigate to login with classic when the session can be retrieved`() = runTest { + val elementClassicConnection = FakeElementClassicConnection() + createHelper( + elementClassicConnection = elementClassicConnection, + ) + .navigationEventFlow() + .test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo(NavigationEvent.Idle) + elementClassicConnection.emitState( + anElementClassicReady() + ) + val finalState = awaitItem() + assertThat(finalState).isEqualTo(NavigationEvent.NavigateToLoginWithClassic(A_USER_ID)) + advanceTimeBy(10_000) + expectNoEvents() + } + } + + @Test + fun `navigate to login with classic when the session can be retrieved - ignore avatar update`() = runTest { + val elementClassicConnection = FakeElementClassicConnection() + createHelper( + elementClassicConnection = elementClassicConnection, + ) + .navigationEventFlow() + .test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo(NavigationEvent.Idle) + elementClassicConnection.emitState( + anElementClassicReady() + ) + val finalState = awaitItem() + assertThat(finalState).isEqualTo(NavigationEvent.NavigateToLoginWithClassic(A_USER_ID)) + // When the avatar is retrieved, no new event is emitted + elementClassicConnection.emitState( + anElementClassicReady( + avatar = createBitmap(1, 1) + ) + ) + advanceTimeBy(10_000) + expectNoEvents() + } + } + + @Test + fun `navigate to login with classic when the session can be retrieved and navigate again once the session is verified`() = runTest { + val elementClassicConnection = FakeElementClassicConnection() + createHelper( + elementClassicConnection = elementClassicConnection, + ) + .navigationEventFlow() + .test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo(NavigationEvent.Idle) + elementClassicConnection.emitState( + anElementClassicReady( + elementClassicSession = anElementClassicSession( + secrets = A_SECRET, + ) + ) + ) + val readyState = awaitItem() + assertThat(readyState).isEqualTo(NavigationEvent.NavigateToLoginWithClassic(A_USER_ID)) + // When the secret with the key backup is retrieved, NavigateToLoginWithClassic is emitted again + elementClassicConnection.emitState( + anElementClassicReady( + elementClassicSession = anElementClassicSession( + secrets = A_SECRET + A_SECRET, + ) + ) + ) + val finalState = awaitItem() + assertThat(finalState).isEqualTo(NavigationEvent.NavigateToLoginWithClassic(A_USER_ID)) + advanceTimeBy(10_000) + expectNoEvents() + } + } + + @Test + fun `navigate to login with classic if a session with another account already exists`() = runTest { + val elementClassicConnection = FakeElementClassicConnection() + createHelper( + elementClassicConnection = elementClassicConnection, + sessionStore = InMemorySessionStore( + initialList = listOf( + aSessionData( + sessionId = A_USER_ID_2.value, + ) + ) + ), + ) + .navigationEventFlow() + .test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo(NavigationEvent.Idle) + elementClassicConnection.emitState( + anElementClassicReady() + ) + val finalState = awaitItem() + assertThat(finalState).isEqualTo(NavigationEvent.NavigateToLoginWithClassic(A_USER_ID)) + advanceTimeBy(10_000) + expectNoEvents() + } + } + + @Test + fun `navigate to login with classic but do not navigate to OnBoarding once the user is logged in`() = runTest { + val elementClassicConnection = FakeElementClassicConnection() + val sessionStore = InMemorySessionStore( + initialList = listOf() + ) + createHelper( + elementClassicConnection = elementClassicConnection, + sessionStore = sessionStore, + ) + .navigationEventFlow() + .test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo(NavigationEvent.Idle) + elementClassicConnection.emitState( + anElementClassicReady() + ) + val navigateToLoginWithClassicState = awaitItem() + assertThat(navigateToLoginWithClassicState).isEqualTo(NavigationEvent.NavigateToLoginWithClassic(A_USER_ID)) + // User actually logs in + sessionStore.addSession( + aSessionData( + sessionId = A_USER_ID.value, + ) + ) + advanceTimeBy(10_000) + expectNoEvents() + } + } +} + +private fun createHelper( + elementClassicConnection: ElementClassicConnection = FakeElementClassicConnection(), + sessionStore: SessionStore = InMemorySessionStore(), +) = ClassicFlowNodeHelper( + elementClassicConnection = elementClassicConnection, + sessionStore = sessionStore, +) diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/FakeLoginWithClassicNavigator.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/FakeLoginWithClassicNavigator.kt new file mode 100644 index 0000000000..e5ff91aa91 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/FakeLoginWithClassicNavigator.kt @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2026 Element Creations 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.login.impl.screens.classic.loginwithclassic + +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeLoginWithClassicNavigator( + private val navigateToMissingKeyBackupResult: () -> Unit = { lambdaError() }, +) : LoginWithClassicNavigator { + override fun navigateToMissingKeyBackup() { + navigateToMissingKeyBackupResult() + } +} diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenterTest.kt new file mode 100644 index 0000000000..6b2a4fb0e1 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenterTest.kt @@ -0,0 +1,263 @@ +/* + * Copyright (c) 2026 Element Creations 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.login.impl.screens.classic.loginwithclassic + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.enterprise.test.FakeEnterpriseService +import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource +import io.element.android.features.login.impl.classic.ElementClassicConnection +import io.element.android.features.login.impl.classic.ElementClassicConnectionState +import io.element.android.features.login.impl.classic.FakeElementClassicConnection +import io.element.android.features.login.impl.classic.ROOM_KEYS_VERSION +import io.element.android.features.login.impl.classic.anElementClassicReady +import io.element.android.features.login.impl.classic.anElementClassicSession +import io.element.android.features.login.impl.login.LoginHelper +import io.element.android.features.login.impl.screens.onboarding.createLoginHelper +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.test.AN_EXCEPTION +import io.element.android.libraries.matrix.test.A_FAILURE_REASON +import io.element.android.libraries.matrix.test.A_SECRET +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.A_USER_NAME +import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService +import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.test +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class LoginWithClassicPresenterTest { + @Test + fun `present - initial state`() = runTest { + val presenter = createPresenter() + presenter.test { + val initialState = awaitItem() + assertThat(initialState.isElementPro).isFalse() + assertThat(initialState.userId).isEqualTo(A_USER_ID) + assertThat(initialState.displayName).isNull() + assertThat(initialState.avatar).isNull() + assertThat(initialState.loginWithClassicAction.isUninitialized()).isTrue() + assertThat(initialState.loginMode.isUninitialized()).isTrue() + } + } + + @Test + fun `present - initial state - element Pro`() = runTest { + val presenter = createPresenter( + isEnterpriseBuild = true, + ) + presenter.test { + val initialState = awaitItem() + assertThat(initialState.isElementPro).isTrue() + } + } + + @Test + fun `present - start login with correct state - user can login`() = runTest { + val authenticationService = FakeMatrixAuthenticationService( + setHomeserverResult = { + Result.failure(AN_EXCEPTION) + }, + ) + val elementClassicConnection = FakeElementClassicConnection( + startResult = {}, + ) + val presenter = createPresenter( + elementClassicConnection = elementClassicConnection, + loginHelper = createLoginHelper( + authenticationService = authenticationService, + ), + ) + presenter.test { + skipItems(1) + elementClassicConnection.emitState( + anElementClassicReady( + elementClassicSession = anElementClassicSession( + userId = A_USER_ID, + secrets = A_SECRET, + roomKeysVersion = ROOM_KEYS_VERSION, + doesContainBackupKey = true, + ), + displayName = A_USER_NAME, + ) + ) + val readyState = awaitItem() + assertThat(readyState.userId).isEqualTo(A_USER_ID) + assertThat(readyState.displayName).isEqualTo(A_USER_NAME) + readyState.eventSink(LoginWithClassicEvent.Submit) + val loadingState = awaitItem() + assertThat(loadingState.loginWithClassicAction.isLoading()).isTrue() + skipItems(1) + } + } + + @Test + fun `present - start login with no secrets - user can login and will have to verify manually`() = runTest { + val authenticationService = FakeMatrixAuthenticationService( + setHomeserverResult = { + Result.failure(AN_EXCEPTION) + }, + ) + val elementClassicConnection = FakeElementClassicConnection( + startResult = {}, + ) + val presenter = createPresenter( + elementClassicConnection = elementClassicConnection, + loginHelper = createLoginHelper( + authenticationService = authenticationService, + ), + ) + presenter.test { + skipItems(1) + elementClassicConnection.emitState( + anElementClassicReady( + elementClassicSession = anElementClassicSession( + userId = A_USER_ID, + secrets = null, + roomKeysVersion = null, + ), + displayName = A_USER_NAME, + ) + ) + val readyState = awaitItem() + assertThat(readyState.userId).isEqualTo(A_USER_ID) + assertThat(readyState.displayName).isEqualTo(A_USER_NAME) + readyState.eventSink(LoginWithClassicEvent.Submit) + val loadingState = awaitItem() + assertThat(loadingState.loginWithClassicAction.isLoading()).isTrue() + skipItems(1) + } + } + + @Test + fun `present - start login with secrets and without key backup - user will see the screen to enable key backup`() = runTest { + val authenticationService = FakeMatrixAuthenticationService( + setHomeserverResult = { + Result.failure(AN_EXCEPTION) + }, + ) + val elementClassicConnection = FakeElementClassicConnection( + startResult = {}, + ) + val navigateToMissingKeyBackupResult = lambdaRecorder { } + val presenter = createPresenter( + elementClassicConnection = elementClassicConnection, + loginHelper = createLoginHelper( + authenticationService = authenticationService, + ), + navigator = FakeLoginWithClassicNavigator( + navigateToMissingKeyBackupResult = navigateToMissingKeyBackupResult, + ), + ) + presenter.test { + skipItems(1) + elementClassicConnection.emitState( + anElementClassicReady( + elementClassicSession = anElementClassicSession( + userId = A_USER_ID, + secrets = A_SECRET, + roomKeysVersion = null, + doesContainBackupKey = false, + ), + displayName = A_USER_NAME, + ) + ) + val readyState = awaitItem() + assertThat(readyState.userId).isEqualTo(A_USER_ID) + assertThat(readyState.displayName).isEqualTo(A_USER_NAME) + readyState.eventSink(LoginWithClassicEvent.Submit) + navigateToMissingKeyBackupResult.assertions().isCalledOnce() + } + } + + @Test + fun `present - start login with secrets and with invalid key backup - user will see the screen to enable key backup`() = runTest { + val authenticationService = FakeMatrixAuthenticationService( + setHomeserverResult = { + Result.failure(AN_EXCEPTION) + }, + ) + val elementClassicConnection = FakeElementClassicConnection( + startResult = {}, + ) + val navigateToMissingKeyBackupResult = lambdaRecorder { } + val presenter = createPresenter( + elementClassicConnection = elementClassicConnection, + loginHelper = createLoginHelper( + authenticationService = authenticationService, + ), + navigator = FakeLoginWithClassicNavigator( + navigateToMissingKeyBackupResult = navigateToMissingKeyBackupResult, + ), + ) + presenter.test { + skipItems(1) + elementClassicConnection.emitState( + anElementClassicReady( + elementClassicSession = anElementClassicSession( + userId = A_USER_ID, + secrets = A_SECRET, + roomKeysVersion = ROOM_KEYS_VERSION, + // false here + doesContainBackupKey = false, + ), + displayName = A_USER_NAME, + ) + ) + val readyState = awaitItem() + assertThat(readyState.userId).isEqualTo(A_USER_ID) + assertThat(readyState.displayName).isEqualTo(A_USER_NAME) + readyState.eventSink(LoginWithClassicEvent.Submit) + navigateToMissingKeyBackupResult.assertions().isCalledOnce() + } + } + + @Test + fun `present - submit in wrong state and clear error`() = runTest { + val elementClassicConnection = FakeElementClassicConnection( + startResult = {}, + ) + val presenter = createPresenter( + elementClassicConnection = elementClassicConnection, + ) + presenter.test { + skipItems(1) + elementClassicConnection.emitState( + ElementClassicConnectionState.Error( + error = A_FAILURE_REASON, + ) + ) + val initialState = awaitItem() + assertThat(initialState.loginWithClassicAction.isUninitialized()).isTrue() + initialState.eventSink(LoginWithClassicEvent.Submit) + val errorState = awaitItem() + assertThat(errorState.loginWithClassicAction.isFailure()).isTrue() + errorState.eventSink(LoginWithClassicEvent.ClearError) + val clearedState = awaitItem() + assertThat(clearedState.loginWithClassicAction.isUninitialized()).isTrue() + } + } +} + +private fun createPresenter( + userId: UserId = A_USER_ID, + navigator: LoginWithClassicNavigator = FakeLoginWithClassicNavigator(), + loginHelper: LoginHelper = createLoginHelper(), + elementClassicConnection: ElementClassicConnection = FakeElementClassicConnection(), + accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()), + isEnterpriseBuild: Boolean = false, +) = LoginWithClassicPresenter( + userId = userId, + navigator = navigator, + loginHelper = loginHelper, + elementClassicConnection = elementClassicConnection, + accountProviderDataSource = accountProviderDataSource, + buildMeta = aBuildMeta( + isEnterpriseBuild = isEnterpriseBuild, + ), +) diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupPresenterTest.kt new file mode 100644 index 0000000000..447b0ba77b --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupPresenterTest.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026 Element Creations 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.login.impl.screens.classic.missingkeybackup + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.matrix.test.AN_APPLICATION_NAME +import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.tests.testutils.test +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class MissingKeyBackupPresenterTest { + @Test + fun `present - initial state`() = runTest { + val presenter = createPresenter() + presenter.test { + val initialState = awaitItem() + assertThat(initialState.appName).isEqualTo(AN_APPLICATION_NAME) + } + } +} + +private fun createPresenter( + buildMeta: BuildMeta = aBuildMeta(applicationName = AN_APPLICATION_NAME), +) = MissingKeyBackupPresenter( + buildMeta = buildMeta, +) diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenterTest.kt index 92099180ec..31a835cb8c 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenterTest.kt @@ -18,6 +18,7 @@ import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_PASSWORD import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_USER_NAME +import io.element.android.libraries.matrix.test.A_USER_NAME_2 import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService import io.element.android.libraries.matrix.test.auth.aMatrixHomeServerDetails import io.element.android.tests.testutils.WarmUpRule @@ -41,6 +42,20 @@ class LoginPasswordPresenterTest { } } + @Test + fun `present - initial login is in the first state and can be modified`() = runTest { + createLoginPasswordPresenter( + initialLogin = A_USER_NAME, + ).test { + val initialState = awaitItem() + assertThat(initialState.formState.login).isEqualTo(A_USER_NAME) + // Login can be changed + initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME_2)) + val loginChangedState = awaitItem() + assertThat(loginChangedState.formState.login).isEqualTo(A_USER_NAME_2) + } + } + @Test fun `present - enter login and password`() = runTest { val authenticationService = FakeMatrixAuthenticationService( @@ -140,9 +155,11 @@ class LoginPasswordPresenterTest { } private fun createLoginPasswordPresenter( + initialLogin: String = "", authenticationService: FakeMatrixAuthenticationService = FakeMatrixAuthenticationService(), accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()), ): LoginPasswordPresenter = LoginPasswordPresenter( + initialLogin = initialLogin, authenticationService = authenticationService, accountProviderDataSource = accountProviderDataSource, ) diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt index 1e971ef265..1fdfb7e070 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt @@ -16,7 +16,6 @@ import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.login.LoginHelper -import io.element.android.features.login.impl.screens.onboarding.classic.aLoginWithClassicState import io.element.android.features.login.impl.web.FakeWebClientUrlForAuthenticationRetriever import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever import io.element.android.features.wellknown.test.FakeWellknownRetriever @@ -83,16 +82,31 @@ class OnBoardingPresenterTest { ) presenter.test { val initialState = awaitItem() + assertThat(initialState.showBackButton).isFalse() assertThat(initialState.defaultAccountProvider).isNull() assertThat(initialState.canLoginWithQrCode).isFalse() assertThat(initialState.productionApplicationName).isEqualTo("B") assertThat(initialState.canCreateAccount).isEqualTo(OnBoardingConfig.CAN_CREATE_ACCOUNT) assertThat(initialState.canReportBug).isFalse() assertThat(initialState.isAddingAccount).isFalse() - assertThat(initialState.loginWithClassicState.canLoginWithClassic).isFalse() val finalState = awaitItem() assertThat(finalState.canLoginWithQrCode).isTrue() - assertThat(finalState.loginWithClassicState.canLoginWithClassic).isFalse() + } + } + + @Test + fun `present - initial state with back button`() = runTest { + val presenter = createPresenter( + params = OnBoardingNode.Params( + accountProvider = null, + loginHint = null, + showBackButton = true, + ), + ) + presenter.test { + val initialState = awaitItem() + assertThat(initialState.showBackButton).isTrue() + skipItems(1) } } @@ -162,6 +176,7 @@ class OnBoardingPresenterTest { params = OnBoardingNode.Params( accountProvider = ACCOUNT_PROVIDER_FROM_LINK, loginHint = null, + showBackButton = false, ), enterpriseService = FakeEnterpriseService( defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG, EnterpriseService.ANY_ACCOUNT_PROVIDER) }, @@ -184,6 +199,7 @@ class OnBoardingPresenterTest { params = OnBoardingNode.Params( accountProvider = ACCOUNT_PROVIDER_FROM_LINK, loginHint = null, + showBackButton = false, ), enterpriseService = FakeEnterpriseService( defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG, ACCOUNT_PROVIDER_FROM_CONFIG_2) }, @@ -206,6 +222,7 @@ class OnBoardingPresenterTest { params = OnBoardingNode.Params( accountProvider = ACCOUNT_PROVIDER_FROM_LINK, loginHint = null, + showBackButton = false, ), enterpriseService = FakeEnterpriseService( defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG) }, @@ -233,6 +250,7 @@ class OnBoardingPresenterTest { params = OnBoardingNode.Params( accountProvider = A_HOMESERVER_URL, loginHint = A_LOGIN_HINT, + showBackButton = false, ), enterpriseService = FakeEnterpriseService( isAllowedToConnectToHomeserverResult = { true }, @@ -265,7 +283,11 @@ class OnBoardingPresenterTest { } private fun createPresenter( - params: OnBoardingNode.Params = OnBoardingNode.Params(null, null), + params: OnBoardingNode.Params = OnBoardingNode.Params( + accountProvider = null, + loginHint = null, + showBackButton = false, + ), buildMeta: BuildMeta = aBuildMeta(), enterpriseService: EnterpriseService = FakeEnterpriseService(), wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(), @@ -287,7 +309,6 @@ private fun createPresenter( onBoardingLogoResIdProvider = onBoardingLogoResIdProvider, sessionStore = sessionStore, accountProviderDataSource = accountProviderDataSource, - loginWithClassicPresenter = { aLoginWithClassicState() }, ) fun createLoginHelper( diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt index c8dcd978c6..ad09445075 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt @@ -11,9 +11,11 @@ package io.element.android.features.login.impl.screens.onboarding import androidx.activity.ComponentActivity import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick -import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.testing.junit.testparameterinjector.KotlinTestParameters.namedTestValues +import com.google.testing.junit.testparameterinjector.TestParameter import io.element.android.features.login.impl.R import io.element.android.features.login.impl.login.LoginMode import io.element.android.libraries.architecture.AsyncData @@ -31,8 +33,9 @@ import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule import org.junit.runner.RunWith +import org.robolectric.RobolectricTestParameterInjector -@RunWith(AndroidJUnit4::class) +@RunWith(RobolectricTestParameterInjector::class) class OnboardingViewTest { @get:Rule val rule = createAndroidComposeRule() @@ -44,11 +47,15 @@ class OnboardingViewTest { rule.setOnboardingView( state = anOnBoardingState( canCreateAccount = true, + showDeveloperSettings = false, eventSink = eventSink, ), onCreateAccount = callback, ) rule.clickOn(R.string.screen_onboarding_sign_up) + // Developer settings should not be shown + val developerSettingsText = rule.activity.getString(CommonStrings.common_developer_options) + rule.onNodeWithContentDescription(developerSettingsText).assertDoesNotExist() } } @@ -83,21 +90,11 @@ class OnboardingViewTest { } @Test - fun `when can login with QR code - clicking on sign in manually calls the expected callback - can search account provider`() { - `when can login with QR code - clicking on sign in manually calls the expected callback`( - mustChooseAccountProvider = false, + fun `when can login with QR code - clicking on sign in manually calls the expected callback`( + @TestParameter mustChooseAccountProvider: Boolean = namedTestValues( + "can search account provider" to false, + "cannot search account provider" to true, ) - } - - @Test - fun `when can login with QR code - clicking on sign in manually calls the expected callback - cannot search account provider`() { - `when can login with QR code - clicking on sign in manually calls the expected callback`( - mustChooseAccountProvider = true, - ) - } - - private fun `when can login with QR code - clicking on sign in manually calls the expected callback`( - mustChooseAccountProvider: Boolean, ) { val eventSink = EventsRecorder(expectEvents = false) ensureCalledOnceWithParam(mustChooseAccountProvider) { callback -> @@ -114,21 +111,11 @@ class OnboardingViewTest { } @Test - fun `when cannot login with QR code or create account - clicking on continue calls the sign in callback - can search account provider`() { - `when cannot login with QR code or create account - clicking on continue calls the sign in callback`( - mustChooseAccountProvider = false, + fun `when cannot login with QR code or create account - clicking on continue calls the sign in callback`( + @TestParameter mustChooseAccountProvider: Boolean = namedTestValues( + "can search account provider" to false, + "cannot search account provider" to true, ) - } - - @Test - fun `when cannot login with QR code or create account - clicking on continue calls the sign in callback - cannot search account provider`() { - `when cannot login with QR code or create account - clicking on continue calls the sign in callback`( - mustChooseAccountProvider = true, - ) - } - - private fun `when cannot login with QR code or create account - clicking on continue calls the sign in callback`( - mustChooseAccountProvider: Boolean, ) { val eventSink = EventsRecorder(expectEvents = false) ensureCalledOnceWithParam(mustChooseAccountProvider) { callback -> @@ -190,6 +177,22 @@ class OnboardingViewTest { } } + @Test + fun `clicking on settings calls the developer settings callback`() { + val eventSink = EventsRecorder(expectEvents = false) + ensureCalledOnce { callback -> + rule.setOnboardingView( + state = anOnBoardingState( + showDeveloperSettings = true, + eventSink = eventSink, + ), + onDeveloperSettingsClick = callback, + ) + val text = rule.activity.getString(CommonStrings.common_developer_options) + rule.onNodeWithContentDescription(text).performClick() + } + } + @Test fun `cannot report a problem when the feature is disabled`() { val eventSink = EventsRecorder(expectEvents = false) @@ -253,6 +256,7 @@ class OnboardingViewTest { private fun AndroidComposeTestRule.setOnboardingView( state: OnBoardingState, onBackClick: () -> Unit = EnsureNeverCalled(), + onDeveloperSettingsClick: () -> Unit = EnsureNeverCalled(), onSignInWithQrCode: () -> Unit = EnsureNeverCalled(), onSignIn: (Boolean) -> Unit = EnsureNeverCalledWithParam(), onCreateAccount: () -> Unit = EnsureNeverCalled(), @@ -266,6 +270,7 @@ class OnboardingViewTest { OnBoardingView( state = state, onBackClick = onBackClick, + onDeveloperSettingsClick = onDeveloperSettingsClick, onSignInWithQrCode = onSignInWithQrCode, onSignIn = onSignIn, onCreateAccount = onCreateAccount, diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/LoginWithClassicPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/LoginWithClassicPresenterTest.kt deleted file mode 100644 index 437e65f21d..0000000000 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/LoginWithClassicPresenterTest.kt +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -@file:OptIn(ExperimentalCoroutinesApi::class) - -package io.element.android.features.login.impl.screens.onboarding.classic - -import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.featureflag.api.FeatureFlagService -import io.element.android.libraries.featureflag.api.FeatureFlags -import io.element.android.libraries.featureflag.test.FakeFeatureFlagService -import io.element.android.libraries.matrix.test.A_SECRET -import io.element.android.libraries.matrix.test.A_USER_ID -import io.element.android.libraries.sessionstorage.api.SessionStore -import io.element.android.libraries.sessionstorage.test.InMemorySessionStore -import io.element.android.libraries.sessionstorage.test.aSessionData -import io.element.android.tests.testutils.WarmUpRule -import io.element.android.tests.testutils.lambda.lambdaRecorder -import io.element.android.tests.testutils.test -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runTest -import org.junit.Rule -import org.junit.Test - -class LoginWithClassicPresenterTest { - @get:Rule - val warmUpRule = WarmUpRule() - - @Test - fun `present - initial state - feature disabled - start is not invoked`() = runTest { - val presenter = createPresenter( - elementClassicConnection = FakeElementClassicConnection( - startResult = { - error("start should not be invoked when feature is disabled") - }, - ) - ) - presenter.test { - val initialState = awaitItem() - assertThat(initialState.canLoginWithClassic).isFalse() - assertThat(initialState.loginWithClassicAction.isUninitialized()).isTrue() - } - } - - @Test - fun `present - feature enabled - start is invoked`() = runTest { - val startResult = lambdaRecorder {} - val presenter = createPresenter( - elementClassicConnection = FakeElementClassicConnection( - startResult = startResult, - ), - isFeatureEnabled = true, - ) - presenter.test { - val initialState = awaitItem() - assertThat(initialState.canLoginWithClassic).isFalse() - assertThat(initialState.loginWithClassicAction.isUninitialized()).isTrue() - val finalState = awaitItem() - assertThat(finalState.canLoginWithClassic).isFalse() - } - startResult.assertions().isCalledOnce() - } - - @Test - fun `present - emit request data invokes the expected method`() = runTest { - val requestDataResult = lambdaRecorder {} - val presenter = createPresenter( - elementClassicConnection = FakeElementClassicConnection( - startResult = {}, - requestDataResult = requestDataResult, - ), - isFeatureEnabled = true, - ) - presenter.test { - val initialState = awaitItem() - assertThat(initialState.canLoginWithClassic).isFalse() - assertThat(initialState.loginWithClassicAction.isUninitialized()).isTrue() - val nextState = awaitItem() - assertThat(nextState.canLoginWithClassic).isFalse() - nextState.eventSink(LoginWithClassicEvent.RefreshData) - } - requestDataResult.assertions().isCalledOnce() - } - - @Test - fun `present - start login with wrong state emits an error`() = runTest { - val presenter = createPresenter( - elementClassicConnection = FakeElementClassicConnection( - startResult = {}, - ), - isFeatureEnabled = true, - ) - presenter.test { - skipItems(1) - val state = awaitItem() - state.eventSink(LoginWithClassicEvent.StartLoginWithClassic) - val errorState = awaitItem() - assertThat(errorState.loginWithClassicAction.isFailure()).isTrue() - } - } - - @Test - fun `present - start login with correct state - user cancel`() = runTest { - val elementClassicConnection = FakeElementClassicConnection( - startResult = {}, - ) - val presenter = createPresenter( - elementClassicConnection = elementClassicConnection, - isFeatureEnabled = true, - ) - presenter.test { - skipItems(2) - elementClassicConnection.emitState( - ElementClassicConnectionState.ElementClassicReady(userId = A_USER_ID, secrets = A_SECRET) - ) - val readyState = awaitItem() - assertThat(readyState.canLoginWithClassic).isTrue() - readyState.eventSink(LoginWithClassicEvent.StartLoginWithClassic) - val confirmingState = awaitItem() - assertThat(confirmingState.loginWithClassicAction.isConfirming()).isTrue() - assertThat((confirmingState.loginWithClassicAction as ConfirmingLoginWithElementClassic).userId).isEqualTo(A_USER_ID) - confirmingState.eventSink(LoginWithClassicEvent.CloseDialog) - val finalState = awaitItem() - assertThat(finalState.loginWithClassicAction.isUninitialized()).isTrue() - } - } - - @Test - fun `present - start login with correct state - user confirms`() = runTest { - val elementClassicConnection = FakeElementClassicConnection( - startResult = {}, - ) - val presenter = createPresenter( - elementClassicConnection = elementClassicConnection, - isFeatureEnabled = true, - ) - presenter.test { - skipItems(2) - elementClassicConnection.emitState( - ElementClassicConnectionState.ElementClassicReady(userId = A_USER_ID, secrets = A_SECRET) - ) - val readyState = awaitItem() - assertThat(readyState.canLoginWithClassic).isTrue() - readyState.eventSink(LoginWithClassicEvent.StartLoginWithClassic) - val confirmingState = awaitItem() - assertThat(confirmingState.loginWithClassicAction.isConfirming()).isTrue() - assertThat((confirmingState.loginWithClassicAction as ConfirmingLoginWithElementClassic).userId).isEqualTo(A_USER_ID) - confirmingState.eventSink(LoginWithClassicEvent.DoLoginWithClassic) - val loadingState = awaitItem() - assertThat(loadingState.loginWithClassicAction.isLoading()).isTrue() - val finalState = awaitItem() - assertThat(finalState.loginWithClassicAction.isSuccess()).isTrue() - } - } - - @Test - fun `present - cannot sign in if a session with the same account already exists`() = runTest { - val elementClassicConnection = FakeElementClassicConnection( - startResult = {}, - ) - val presenter = createPresenter( - elementClassicConnection = elementClassicConnection, - isFeatureEnabled = true, - sessionStore = InMemorySessionStore( - initialList = listOf( - aSessionData( - sessionId = A_USER_ID.value, - ) - ) - ), - ) - presenter.test { - skipItems(2) - elementClassicConnection.emitState( - ElementClassicConnectionState.ElementClassicReady(userId = A_USER_ID, secrets = A_SECRET) - ) - // No new item, because canLoginWithClassic is still false - } - } - - @Test - fun `present - cannot sign in if the feature is disabled`() = runTest { - val elementClassicConnection = FakeElementClassicConnection() - val presenter = createPresenter( - elementClassicConnection = elementClassicConnection, - isFeatureEnabled = false, - ) - presenter.test { - skipItems(1) - // Note: it should not happen IRL - elementClassicConnection.emitState( - ElementClassicConnectionState.ElementClassicReady(userId = A_USER_ID, secrets = A_SECRET) - ) - // No new item, because canLoginWithClassic is still false - } - } -} - -private fun createPresenter( - elementClassicConnection: ElementClassicConnection = FakeElementClassicConnection(), - sessionStore: SessionStore = InMemorySessionStore(), - isFeatureEnabled: Boolean = false, - featureFlagService: FeatureFlagService = FakeFeatureFlagService( - initialState = mapOf(FeatureFlags.SignInWithClassic.key to isFeatureEnabled) - ), -) = LoginWithClassicPresenter( - elementClassicConnection = elementClassicConnection, - sessionStore = sessionStore, - featureFlagService = featureFlagService, -) diff --git a/features/logout/impl/src/main/res/values-cs/translations.xml b/features/logout/impl/src/main/res/values-cs/translations.xml index e2c2a68fd5..e19512c017 100644 --- a/features/logout/impl/src/main/res/values-cs/translations.xml +++ b/features/logout/impl/src/main/res/values-cs/translations.xml @@ -1,18 +1,18 @@ - "Opravdu se chcete odhlásit?" - "Odhlásit se" - "Odhlásit se" - "Odhlašování…" - "Chystáte se odhlásit z poslední relace. Pokud se nyní odhlásíte, ztratíte přístup ke svým šifrovaným zprávám." - "Vypnuli jste zálohování" - "Když jste přešli do režimu offline, vaše klíče se ještě stále zálohovaly. Znovu se připojte, aby bylo možné před odhlášením zálohovat vaše klíče." + "Opravdu chcete odstranit toto zařízení?" + "Odebrat toto zařízení" + "Odebrat toto zařízení" + "Odebírání zařízení…" + "Toto je vaše jediné zařízení. Pokud ho odstraníte, budete potřebovat klíč pro obnovení, abyste si při příštím přihlášení ověřili svou digitální identitu a obnovili šifrované chaty." + "Chystáte se ztratit přístup ke svým šifrovaným chatům" + "Vaše klíče se stále zálohovaly, když jste byli offline. Před odpojením tohoto zařízení se znovu připojte, aby se vaše klíče mohly zálohovat." "Vaše klíče jsou stále zálohovány" - "Před odhlášením prosím počkejte na dokončení." + "Před odstraněním tohoto zařízení počkejte, až se proces dokončí." "Vaše klíče jsou stále zálohovány" - "Odhlásit se" - "Chystáte se odhlásit z poslední relace. Pokud se nyní odhlásíte, ztratíte přístup ke svým šifrovaným zprávám." - "Obnovení není nastaveno" - "Chystáte se odhlásit z poslední relace. Pokud se nyní odhlásíte, můžete ztratit přístup k šifrovaným zprávám." - "Uložili jste si klíč pro obnovení?" + "Odebrat toto zařízení" + "Toto je vaše jediné zařízení. Pokud ho odstraníte, budete potřebovat klíč pro obnovení, abyste si při příštím přihlášení ověřili svou digitální identitu a obnovili šifrované chaty." + "Chystáte se ztratit přístup ke svým šifrovaným chatům" + "Toto je vaše jediné zařízení. Pokud ho odstraníte, budete potřebovat klíč pro obnovení, abyste si při příštím přihlášení ověřili svou digitální identitu a obnovili šifrované chaty." + "Před odebráním tohoto zařízení se ujistěte, že máte přístup ke klíči pro obnovení" diff --git a/features/logout/impl/src/main/res/values-hu/translations.xml b/features/logout/impl/src/main/res/values-hu/translations.xml index 2cf2b89e4a..e7eb8de99e 100644 --- a/features/logout/impl/src/main/res/values-hu/translations.xml +++ b/features/logout/impl/src/main/res/values-hu/translations.xml @@ -1,18 +1,18 @@ - "Biztos, hogy kijelentkezik?" - "Kijelentkezés" - "Kijelentkezés" - "Kijelentkezés…" - "Arra készül, hogy kijelentkezzen az utolsó munkamenetéből is. Ha most kijelentkezik, akkor elveszti a hozzáférését a titkosított üzeneteihez." - "Kikapcsolta a biztonsági mentést" - "A kulcsai mentése során bontotta a kapcsolatot. Kapcsolódjon újra, hogy a kulcsai továbbra is mentésre kerüljenek mielőtt kijelentkezik." + "Biztosan eltávolítja ezt az eszközt?" + "Eszköz eltávolítása" + "Eszköz eltávolítása" + "Eszköz eltávolítása…" + "Ez az egyetlen eszköze. Ha eltávolítja, a következő bejelentkezéskor szüksége lesz egy helyreállítási kulcsra a digitális személyazonossága megerősítéséhez és a titkosított csevegések helyreállításához." + "Hamarosan elveszíti a hozzáférését a titkosított csevegéseihez" + "A kulcsok biztonsági mentése még folyamatban volt, amikor megszűnt a hálózati kapcsolat. Csatlakozzon újra, hogy a kulcsok biztonsági mentése megtörténhessen, mielőtt eltávolítja ezt az eszközt." "A kulcsai mentése még folyamatban van" - "Kijelentkezés előtt várja meg a befejezését." + "Várja meg, amíg ez befejeződik, mielőtt eltávolítja ezt az eszközt." "A kulcsai mentése még folyamatban van" - "Kijelentkezés" - "Arra készül, hogy kijelentkezzen az utolsó munkamenetéből is. Ha most kijelentkezik, akkor elveszti a hozzáférését a titkosított üzeneteihez." - "A helyreállítás nincs beállítva" - "Arra készül, hogy kijelentkezzen az utolsó munkamenetéből is. Ha most kijelentkezik, akkor elveszítheti a hozzáférését a titkosított üzeneteihez." - "Mentette a helyreállítási kulcsát?" + "Eszköz eltávolítása" + "Ez az egyetlen eszköze. Ha eltávolítja, a következő bejelentkezéskor szüksége lesz egy helyreállítási kulcsra a digitális személyazonossága megerősítéséhez és a titkosított csevegések helyreállításához." + "Hamarosan elveszíti a hozzáférését a titkosított csevegéseihez" + "Ez az egyetlen eszköze. Ha eltávolítja, a következő bejelentkezéskor szüksége lesz egy helyreállítási kulcsra a digitális személyazonossága megerősítéséhez és a titkosított csevegések helyreállításához." + "Az eszköz eltávolítása előtt győződjön meg arról, hogy hozzáfér a helyreállítási kulcshoz" diff --git a/features/logout/impl/src/main/res/values-it/translations.xml b/features/logout/impl/src/main/res/values-it/translations.xml index 47d6bcf519..eea93297c0 100644 --- a/features/logout/impl/src/main/res/values-it/translations.xml +++ b/features/logout/impl/src/main/res/values-it/translations.xml @@ -1,18 +1,18 @@ - "Sei sicuro di voler uscire?" - "Disconnetti" - "Disconnetti" - "Disconnessione in corso…" - "Stai per disconnettere la tua ultima sessione. Se esci ora, perderai l\'accesso ai tuoi messaggi cifrati." - "Hai disattivato il backup" - "Il backup delle chiavi era ancora in corso quando sei andato offline. Riconnettiti per eseguire il backup delle chiavi prima di uscire." + "Sei sicuro di voler rimuovere questo dispositivo?" + "Rimuovi questo dispositivo" + "Rimuovi questo dispositivo" + "Rimozione del dispositivo…" + "Questo è il tuo unico dispositivo. Se lo rimuovi, avrai bisogno di una chiave di recupero per confermare la tua identità digitale e ripristinare le tue conversazioni cifrate al prossimo accesso." + "Stai per perdere l\'accesso alle tue conversazioni cifrate" + "Il backup delle tue chiavi era ancora in corso quando ti sei disconnesso. Riconnettiti in modo che il backup delle tue chiavi possa essere completato prima di rimuovere questo dispositivo." "Il backup delle chiavi è ancora in corso" - "Attendi il completamento dell\'operazione prima di uscire." + "Attendi il completamento dell\'operazione prima di rimuovere questo dispositivo." "Il backup delle chiavi è ancora in corso" - "Disconnetti" - "Stai per disconnettere la tua ultima sessione. Se esci ora, perderai l\'accesso ai tuoi messaggi cifrati." - "Recupero non impostato" - "Stai per disconnettere la tua ultima sessione. Se esci ora, potresti perdere l\'accesso ai tuoi messaggi cifrati." - "Hai salvato la chiave di recupero?" + "Rimuovi questo dispositivo" + "Questo è il tuo unico dispositivo. Se lo rimuovi, avrai bisogno di una chiave di recupero per confermare la tua identità digitale e ripristinare le tue conversazioni cifrate al prossimo accesso." + "Stai per perdere l\'accesso alle tue conversazioni cifrate" + "Questo è il tuo unico dispositivo. Se lo rimuovi, avrai bisogno di una chiave di recupero per confermare la tua identità digitale e ripristinare le tue conversazioni cifrate al prossimo accesso." + "Assicurati di avere accesso alla tua chiave di recupero prima di rimuovere questo dispositivo" diff --git a/features/logout/impl/src/main/res/values-ja/translations.xml b/features/logout/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..2082bacf6e --- /dev/null +++ b/features/logout/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,18 @@ + + + "本当にこの端末を削除しますか?" + "この端末を削除" + "この端末を削除" + "削除中…" + "この端末が唯一の端末です。削除を続行すると次回のログインの際に、デジタルIDと暗号化された会話を復元するために、回復鍵を入力する必要があります。" + "暗号化された会話は見られなくなります" + "鍵のバックアップ中にオフライン状態になりました。この端末を削除する前に、オンラインに復旧してバックアップを完了させてください。" + "鍵のバックアップは継続しています" + "端末の削除の前に、処理の完了をお待ち下さい。" + "鍵のバックアップは継続しています" + "この端末を削除" + "この端末が唯一の端末です。削除を続行すると次回のログインの際に、デジタルIDと暗号化された会話を復元するために、回復鍵を入力する必要があります。" + "暗号化された会話は見られなくなります" + "この端末が唯一の端末です。削除を続行すると次回のログインの際に、デジタルIDと暗号化された会話を復元するために、回復鍵を入力する必要があります。" + "この端末を削除する前に、回復鍵が手元にあることを確認してください。" + diff --git a/features/logout/impl/src/main/res/values-ru/translations.xml b/features/logout/impl/src/main/res/values-ru/translations.xml index f7ed9216c0..d96b8f24f2 100644 --- a/features/logout/impl/src/main/res/values-ru/translations.xml +++ b/features/logout/impl/src/main/res/values-ru/translations.xml @@ -1,18 +1,18 @@ - "Вы уверены, что вы хотите выйти?" + "Вы уверены, что хотите удалить это устройство?" "Удалить это устройство" "Удалить это устройство" - "Выполняется выход…" - "Вы собираетесь выйти из последнего сеанса. Если вы выйдете из системы сейчас, вы потеряете доступ к зашифрованным сообщениям." - "Вы отключили резервное копирование" - "Когда вы перешли в автономный режим, резервное копирование ваших ключей продолжалось. Повторно подключитесь, чтобы перед выходом из системы можно было создать резервную копию ключей." + "Удаление устройства…" + "Это ваше единственное устройство. Если вы его удалите, вам потребуется ключ восстановления, чтобы подтвердить свою цифровую личность и восстановить зашифрованные чаты при следующем входе в систему." + "Вы потеряете доступ к своим зашифрованным чатам" + "Когда вы отключились от сети, резервное копирование ваших ключей продолжалось. Подключитесь снова, чтобы резервная копия ваших ключей была создана, прежде чем вы отключите это устройство." "Резервное копирование ключей все еще продолжается" "Пожалуйста, дождитесь завершения процесса, прежде чем выходить из системы." "Резервное копирование ключей все еще продолжается" "Удалить это устройство" - "Вы собираетесь выйти из последнего сеанса. Если вы выйдете из системы сейчас, вы потеряете доступ к зашифрованным сообщениям." - "Восстановление не настроено" - "Вы собираетесь выйти из последнего сеанса. Если вы выйдете из системы сейчас, вы можете потерять доступ к зашифрованным сообщениям." - "Вы сохранили свой ключ восстановления?" + "Это ваше единственное устройство. Если вы его удалите, вам потребуется ключ восстановления, чтобы подтвердить свою цифровую личность и восстановить зашифрованные чаты при следующем входе в систему." + "Вы потеряете доступ к своим зашифрованным чатам" + "Это ваше единственное устройство. Если вы его удалите, вам потребуется ключ восстановления, чтобы подтвердить свою цифровую личность и восстановить зашифрованные чаты при следующем входе в систему." + "Перед тем как отключить это устройство, убедись, что у тебя есть доступ к ключу восстановления" diff --git a/features/logout/impl/src/main/res/values-sv/translations.xml b/features/logout/impl/src/main/res/values-sv/translations.xml index fdf0e5102e..c35a0455f9 100644 --- a/features/logout/impl/src/main/res/values-sv/translations.xml +++ b/features/logout/impl/src/main/res/values-sv/translations.xml @@ -1,16 +1,16 @@ - "Är du säker på att du vill logga ut?" - "Logga ut" - "Logga ut" - "Loggar ut …" + "Är du säker på att du vill ta bort den här enheten?" + "Ta bort den här enheten" + "Ta bort den här enheten" + "Tar bort enhet …" "Du är på väg att logga ut ur din senaste session. Om du loggar ut nu kommer du att förlora åtkomsten till dina krypterade meddelanden." "Du har stängt av säkerhetskopiering" "Dina nycklar säkerhetskopierades fortfarande när du gick offline. Anslut igen så att dina nycklar kan säkerhetskopieras innan du loggar ut." "Dina nycklar säkerhetskopieras fortfarande" "Vänta tills detta är klart innan du loggar ut." "Dina nycklar säkerhetskopieras fortfarande" - "Logga ut" + "Ta bort den här enheten" "Du är på väg att logga ut ur din sista session. Om du loggar ut nu förlorar du åtkomsten till dina krypterade meddelanden." "Återställning inte inställd" "Du är på väg att logga ut från din senaste session. Om du loggar ut nu kan du förlora åtkomsten till dina krypterade meddelanden." diff --git a/features/logout/impl/src/main/res/values-vi/translations.xml b/features/logout/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..6c1a028948 --- /dev/null +++ b/features/logout/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,18 @@ + + + "Bạn có chắc muốn gỡ thiết bị này không?" + "Gỡ bỏ thiết bị này" + "Gỡ bỏ thiết bị này" + "Đang gỡ thiết bị…" + "Đây là thiết bị duy nhất của bạn. Nếu xóa nó, bạn sẽ cần khóa khôi phục để xác nhận danh tính kỹ thuật số và khôi phục các cuộc trò chuyện được mã hóa lần tiếp theo khi đăng nhập." + "Bạn sắp mất quyền truy cập vào các cuộc trò chuyện được mã hóa" + "Bạn đã ngoại tuyến khi các khóa đang được sao lưu. Kết nối lại để hoàn tất sao lưu trước khi gỡ thiết bị." + "Khóa của bạn vẫn đang được sao lưu." + "Đợi quá trình hoàn tất rồi hãy gỡ thiết bị." + "Khóa của bạn vẫn đang được sao lưu." + "Gỡ bỏ thiết bị này" + "Bạn sắp đăng xuất khỏi phiên làm việc cuối cùng. Nếu bạn đăng xuất ngay bây giờ, bạn sẽ mất quyền truy cập vào các tin nhắn đã mã hóa của mình." + "Bạn sắp mất quyền truy cập vào các cuộc trò chuyện được mã hóa" + "Đây là thiết bị duy nhất của bạn. Nếu xóa nó, bạn sẽ cần khóa khôi phục để xác nhận danh tính kỹ thuật số và khôi phục các cuộc trò chuyện được mã hóa khi đăng nhập lần tới." + "Đảm bảo bạn có khóa khôi phục trước khi gỡ thiết bị này." + diff --git a/features/logout/impl/src/main/res/values-zh/translations.xml b/features/logout/impl/src/main/res/values-zh/translations.xml index 0a8ec07e87..d438c64ff3 100644 --- a/features/logout/impl/src/main/res/values-zh/translations.xml +++ b/features/logout/impl/src/main/res/values-zh/translations.xml @@ -1,16 +1,16 @@ - "确定要登出吗?" - "登出" - "登出" - "正在登出…" + "您确定要删除此设备吗?" + "删除此设备" + "删除此设备" + "正在删除设备……" "即将登出最后一个会话。如果现在登出,将无法访问加密的消息。" "您已关闭备份" "当你离线时,密钥仍在备份中。重新连接以便在登出之前备份密钥。" "您的密钥仍在备份中" "请等待此操作完成后再登出。" "您的密钥仍在备份中" - "登出" + "删除此设备" "即将登出最后一个会话。如果现在登出,将无法访问加密的消息。" "未设置恢复" "即将登出最后一个会话。如果现在登出,将无法访问加密的消息。" diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index d3dd21de67..646a19895a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -39,6 +39,7 @@ import io.element.android.features.messages.impl.pinned.DefaultPinnedEventsTimel import io.element.android.features.messages.impl.pinned.list.PinnedMessagesListNode import io.element.android.features.messages.impl.report.ReportMessageNode import io.element.android.features.messages.impl.threads.ThreadedMessagesNode +import io.element.android.features.messages.impl.threads.list.ThreadsListNode import io.element.android.features.messages.impl.timeline.TimelineController import io.element.android.features.messages.impl.timeline.debug.EventDebugInfoNode import io.element.android.features.messages.impl.timeline.model.TimelineItem @@ -179,6 +180,9 @@ class MessagesFlowNode( @Parcelize data class Thread(val threadRootId: ThreadId, val focusedEventId: EventId?) : NavTarget + + @Parcelize + data object ThreadsList : NavTarget } private val callback: MessagesEntryPoint.Callback = callback() @@ -294,6 +298,10 @@ class MessagesFlowNode( backstack.push(NavTarget.Thread(threadRootId, focusedEventId)) } + override fun navigateToThreadsList() { + backstack.push(NavTarget.ThreadsList) + } + override fun navigateToDeveloperSettings() { callback.navigateToDeveloperSettings() } @@ -432,6 +440,10 @@ class MessagesFlowNode( override fun handleForwardEventClick(eventId: EventId) { backstack.push(NavTarget.ForwardEvent(eventId = eventId, fromPinnedEvents = true)) } + + override fun navigateToThread(threadRootId: ThreadId) { + backstack.push(NavTarget.Thread(threadRootId, null)) + } } createNode(buildContext, plugins = listOf(callback)) } @@ -513,6 +525,14 @@ class MessagesFlowNode( } createNode(buildContext, listOf(inputs, callback)) } + NavTarget.ThreadsList -> { + val callback = object : ThreadsListNode.Callback { + override fun openThread(threadId: ThreadId) { + backstack.push(NavTarget.Thread(threadId, focusedEventId = null)) + } + } + createNode(buildContext, listOf(callback)) + } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt index 20cdc51035..a2cf4a3da0 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt @@ -131,6 +131,8 @@ class MessagesNode( fun navigateToPinnedMessagesList() fun navigateToKnockRequestsList() fun navigateToDeveloperSettings() + + fun navigateToThreadsList() } override fun onBuilt() { @@ -299,6 +301,7 @@ class MessagesNode( onViewRequestsClick = callback::navigateToKnockRequestsList, ) }, + onThreadsListClick = callback::navigateToThreadsList, ) roomMemberModerationRenderer.Render( state = state.roomMemberModerationState, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index d9c3d17afa..f115dd2799 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -16,6 +16,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable @@ -27,6 +28,7 @@ import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.PinUnpinAction import io.element.android.appconfig.MessageComposerConfig import io.element.android.features.messages.api.timeline.HtmlConverterProvider +import io.element.android.features.messages.impl.MessagesState.Threads import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState @@ -85,8 +87,11 @@ import io.element.android.libraries.recentemojis.api.AddRecentEmoji import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.analytics.api.AnalyticsService +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber @@ -160,6 +165,13 @@ class MessagesPresenter( val pinnedMessagesBannerState = pinnedMessagesBannerPresenter.present() val roomCallState = roomCallStatePresenter.present() val roomMemberModerationState = roomMemberModerationPresenter.present() + val threadsList by produceState(persistentListOf()) { + room.threadsListService.subscribeToItemUpdates() + .onStart { room.threadsListService.paginate() } + .collectLatest { value = it.toImmutableList() } + } + + val canOpenThreadList by featureFlagService.isFeatureEnabledFlow(FeatureFlags.RoomThreadList).collectAsState(initial = false) val userEventPermissions by room.permissionsAsState(UserEventPermissions.DEFAULT) { perms -> perms.userEventPermissions() @@ -250,12 +262,11 @@ class MessagesPresenter( is MessagesEvent.OnUserClicked -> { roomMemberModerationState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(event.user)) } - is MessagesEvent.MarkAsFullyReadAndExit -> coroutineScope.launch { - if (!markingAsReadAndExiting.getAndSet(true)) { + is MessagesEvent.MarkAsFullyReadAndExit -> if (!markingAsReadAndExiting.getAndSet(true)) { + coroutineScope.launch { val latestEventId = room.liveTimeline.getLatestEventId().getOrElse { Timber.w(it, "Failed to get latest event id to mark as fully read") - navigator.close() - return@launch + null } latestEventId?.let { eventId -> sessionCoroutineScope.launch { @@ -263,7 +274,6 @@ class MessagesPresenter( } } navigator.close() - markingAsReadAndExiting.set(false) } } } @@ -296,6 +306,11 @@ class MessagesPresenter( roomMemberModerationState = roomMemberModerationState, topBarSharedHistoryIcon = topBarSharedHistoryIcon, successorRoom = roomInfo.successorRoom, + threads = Threads( + hasThreads = canOpenThreadList && threadsList.isNotEmpty(), + // TODO calculate this properly based on the thread list and the read state of each thread + hasUnreadThreads = false, + ), eventSink = ::handleEvent, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt index c18fb461e0..862f30832b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt @@ -57,9 +57,15 @@ data class MessagesState( /** Type of "shared history" icon to show in the top bar. */ val topBarSharedHistoryIcon: SharedHistoryIcon, val successorRoom: SuccessorRoom?, + val threads: Threads, val eventSink: (MessagesEvent) -> Unit ) { val isTombstoned = successorRoom != null + + data class Threads( + val hasThreads: Boolean, + val hasUnreadThreads: Boolean, + ) } /** Type of "shared history" icon to show in the top bar. */ diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index d969ae1491..16021df3e9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -122,6 +122,10 @@ fun aMessagesState( roomMemberModerationState: RoomMemberModerationState = aRoomMemberModerationState(), topBarSharedHistoryIcon: SharedHistoryIcon = SharedHistoryIcon.NONE, successorRoom: SuccessorRoom? = null, + threads: MessagesState.Threads = MessagesState.Threads( + hasThreads = false, + hasUnreadThreads = false, + ), eventSink: (MessagesEvent) -> Unit = {}, ) = MessagesState( roomId = RoomId("!id:domain"), @@ -150,6 +154,7 @@ fun aMessagesState( roomMemberModerationState = roomMemberModerationState, topBarSharedHistoryIcon = topBarSharedHistoryIcon, successorRoom = successorRoom, + threads = threads, eventSink = eventSink, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 8e81ee74a7..bf20c8dc6b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -12,10 +12,12 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.expandVertically import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize @@ -26,6 +28,7 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.width import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -52,6 +55,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerEvent import io.element.android.features.messages.impl.actionlist.ActionListEvent import io.element.android.features.messages.impl.actionlist.ActionListView @@ -74,6 +78,7 @@ import io.element.android.features.messages.impl.timeline.aGroupedEvents import io.element.android.features.messages.impl.timeline.aTimelineItemDaySeparator import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.aTimelineState +import io.element.android.features.messages.impl.timeline.components.CallMenuItem import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionBottomSheet import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvent import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryEvent @@ -88,6 +93,7 @@ import io.element.android.features.messages.impl.topbars.MessagesViewTopBar import io.element.android.features.messages.impl.topbars.ThreadTopBar import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessagePermissionRationaleDialog import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageSendingFailedDialog +import io.element.android.features.roomcall.api.RoomCallState import io.element.android.libraries.androidutils.ui.hideKeyboard import io.element.android.libraries.designsystem.atomic.molecules.ComposerAlertMolecule import io.element.android.libraries.designsystem.components.ExpandableBottomSheetLayout @@ -99,6 +105,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.toAnnotatedString import io.element.android.libraries.designsystem.text.toDp import io.element.android.libraries.designsystem.theme.components.BottomSheetDragHandle +import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.utils.HideKeyboardWhenDisposed @@ -133,6 +140,7 @@ fun MessagesView( onCreatePollClick: () -> Unit, onJoinCallClick: (isAudioCall: Boolean) -> Unit, onViewAllPinnedMessagesClick: () -> Unit, + onThreadsListClick: () -> Unit, modifier: Modifier = Modifier, forceJumpToBottomVisibility: Boolean = false, knockRequestsBannerView: @Composable () -> Unit, @@ -224,12 +232,18 @@ fun MessagesView( roomAvatar = state.roomAvatar, isTombstoned = state.isTombstoned, heroes = state.heroes, - roomCallState = state.roomCallState, dmUserIdentityState = state.dmUserVerificationState, sharedHistoryIcon = state.topBarSharedHistoryIcon, onBackClick = { hidingKeyboard { onBackClick() } }, onRoomDetailsClick = { hidingKeyboard { onRoomDetailsClick() } }, - onJoinCallClick = onJoinCallClick, + menuActions = { + MessagesMenuActions( + displayThreads = state.timelineState.timelineMode !is Timeline.Mode.Thread && state.threads.hasThreads, + roomCallState = state.roomCallState, + onJoinCallClick = onJoinCallClick, + onThreadsListClick = onThreadsListClick + ) + } ) } }, @@ -397,6 +411,28 @@ fun MessagesView( ) } +@Composable +internal fun MessagesMenuActions( + displayThreads: Boolean, + roomCallState: RoomCallState, + onJoinCallClick: (isAudioCall: Boolean) -> Unit, + onThreadsListClick: () -> Unit, +) { + if (displayThreads) { + Icon( + modifier = Modifier.clickable(enabled = true, onClick = onThreadsListClick), + imageVector = CompoundIcons.ThreadsSolid(), + contentDescription = stringResource(CommonStrings.common_threads), + ) + Spacer(Modifier.width(8.dp)) + } + CallMenuItem( + roomCallState = roomCallState, + onJoinCallClick = onJoinCallClick, + ) + Spacer(Modifier.width(8.dp)) +} + @Composable private fun ReinviteDialog(state: MessagesState) { if (state.showReinvitePrompt) { @@ -601,6 +637,7 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class) onViewAllPinnedMessagesClick = { }, forceJumpToBottomVisibility = true, knockRequestsBannerView = {}, + onThreadsListClick = {}, ) } @@ -652,7 +689,8 @@ internal fun MessagesViewA11yPreview() = ElementPreview { onSendLocationClick = {}, onCreatePollClick = {}, onJoinCallClick = {}, - onViewAllPinnedMessagesClick = { }, + onViewAllPinnedMessagesClick = {}, + onThreadsListClick = {}, forceJumpToBottomVisibility = true, knockRequestsBannerView = {}, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt index b434656f7a..2c1a1bbe23 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt @@ -41,6 +41,7 @@ internal fun MessagesViewWithIdentityChangePreview( onCreatePollClick = {}, onJoinCallClick = {}, onViewAllPinnedMessagesClick = {}, - knockRequestsBannerView = {} + knockRequestsBannerView = {}, + onThreadsListClick = {}, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListEvent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListEvent.kt index 6ad4fbefe6..73ba08b29d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListEvent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListEvent.kt @@ -10,7 +10,9 @@ package io.element.android.features.messages.impl.pinned.list import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.libraries.matrix.api.core.ThreadId sealed interface PinnedMessagesListEvent { data class HandleAction(val action: TimelineItemAction, val event: TimelineItem.Event) : PinnedMessagesListEvent + data class OpenThread(val threadRootId: ThreadId) : PinnedMessagesListEvent } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNavigator.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNavigator.kt index a3728cb9f4..9633802f77 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNavigator.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNavigator.kt @@ -9,10 +9,12 @@ package io.element.android.features.messages.impl.pinned.list import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo interface PinnedMessagesListNavigator { fun viewInTimeline(eventId: EventId) fun navigateToEventDebugInfo(eventId: EventId?, debugInfo: TimelineItemDebugInfo) fun forwardEvent(eventId: EventId) + fun navigateToThread(threadRootId: ThreadId) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt index 292a77ba6a..cddc1831db 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt @@ -31,6 +31,7 @@ import io.element.android.libraries.androidutils.system.openUrlInExternalApp import io.element.android.libraries.architecture.callback import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.EventId +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.permalink.PermalinkParser @@ -55,6 +56,7 @@ class PinnedMessagesListNode( fun handlePermalinkClick(data: PermalinkData.RoomLink) fun navigateToEventDebugInfo(eventId: EventId?, debugInfo: TimelineItemDebugInfo) fun handleForwardEventClick(eventId: EventId) + fun navigateToThread(threadRootId: ThreadId) } private val callback: Callback = callback() @@ -95,6 +97,10 @@ class PinnedMessagesListNode( callback.handleForwardEventClick(eventId) } + override fun navigateToThread(threadRootId: ThreadId) { + callback.navigateToThread(threadRootId) + } + @Composable override fun View(modifier: Modifier) { CompositionLocalProvider( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt index f884cdac84..6cd037484d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt @@ -137,6 +137,7 @@ class PinnedMessagesListPresenter( fun handleEvent(event: PinnedMessagesListEvent) { when (event) { is PinnedMessagesListEvent.HandleAction -> sessionCoroutineScope.handleTimelineAction(event.action, event.event) + is PinnedMessagesListEvent.OpenThread -> navigator.navigateToThread(event.threadRootId) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt index b125bdcf6f..b212549a22 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt @@ -30,6 +30,7 @@ import io.element.android.features.messages.impl.actionlist.ActionListView import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.link.LinkEvent import io.element.android.features.messages.impl.link.LinkView +import io.element.android.features.messages.impl.timeline.TimelineEvent import io.element.android.features.messages.impl.timeline.components.TimelineItemRow import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData @@ -235,7 +236,12 @@ private fun PinnedMessagesListLoaded( onReadReceiptClick = {}, onSwipeToReply = {}, onJoinCallClick = {}, - eventSink = {}, + eventSink = { timelineItemEvent -> + when (timelineItemEvent) { + is TimelineEvent.OpenThread -> state.eventSink(PinnedMessagesListEvent.OpenThread(timelineItemEvent.threadRootEventId)) + else -> Unit + } + }, eventContentView = { event, contentModifier, onContentLayoutChange -> TimelineItemEventContentViewWrapper( event = event, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt index 4bb3471660..0949237862 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt @@ -300,6 +300,7 @@ class ThreadedMessagesNode( onViewAllPinnedMessagesClick = {}, modifier = modifier, knockRequestsBannerView = {}, + onThreadsListClick = {}, ) roomMemberModerationRenderer.Render( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadListRowItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadListRowItem.kt new file mode 100644 index 0000000000..3380a32f61 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadListRowItem.kt @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2026 Element Creations 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.messages.impl.threads.list + +import io.element.android.libraries.matrix.api.room.threads.ThreadListItem + +data class ThreadListRowItem( + val item: ThreadListItem, + val rootEventText: String?, + val latestEventText: String?, + val formattedTimestamp: String, +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListNode.kt new file mode 100644 index 0000000000..1954dc60a5 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListNode.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2026 Element Creations 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.messages.impl.threads.list + +import androidx.compose.runtime.Composable +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 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.RoomScope +import io.element.android.libraries.matrix.api.core.ThreadId + +@ContributesNode(RoomScope::class) +@AssistedInject +class ThreadsListNode( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: ThreadsListPresenter, +) : Node(buildContext, plugins = plugins) { + interface Callback : Plugin { + fun openThread(threadId: ThreadId) + } + + private val callback: Callback = callback() + + @Composable + override fun View(modifier: Modifier) { + ThreadsListView( + state = presenter.present(), + modifier = modifier, + onThreadClick = callback::openThread, + onBackClick = this::navigateUp, + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListPresenter.kt new file mode 100644 index 0000000000..9d15376e9f --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListPresenter.kt @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2026 Element Creations 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.messages.impl.threads.list + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject +import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentFactory +import io.element.android.features.messages.impl.utils.messagesummary.MessageSummaryFormatter +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.dateformatter.api.DateFormatter +import io.element.android.libraries.dateformatter.api.DateFormatterMode +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.JoinedRoom +import io.element.android.libraries.matrix.api.room.threads.ThreadListPaginationStatus +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.launch +import timber.log.Timber + +@Inject +class ThreadsListPresenter( + private val room: JoinedRoom, + private val timelineItemContentFactory: TimelineItemContentFactory, + private val messageSummaryFormatter: MessageSummaryFormatter, + private val dateFormatter: DateFormatter, +) : Presenter { + @Composable + override fun present(): ThreadsListState { + val coroutineScope = rememberCoroutineScope() + val threadsListService = room.threadsListService + + val threads by produceState(initialValue = persistentListOf(), key1 = threadsListService) { + threadsListService.subscribeToItemUpdates() + .onStart { threadsListService.paginate() } + .collect { items -> + Timber.d("Received thread list update with ${items.size} items") + value = items.map { item -> + val rootTimelineEvent = item.rootEvent.content?.let { + timelineItemContentFactory.create( + itemContent = it, + eventId = item.rootEvent.eventId, + isEditable = false, + sender = item.rootEvent.senderId, + senderProfile = item.rootEvent.senderProfile, + ) + } + val rootEventText = rootTimelineEvent?.let { messageSummaryFormatter.format(it) } + + val latestTimelineEvent = item.latestEvent?.content?.let { + timelineItemContentFactory.create( + itemContent = it, + eventId = item.latestEvent!!.eventId, + isEditable = false, + sender = item.latestEvent!!.senderId, + senderProfile = item.latestEvent!!.senderProfile, + ) + } + val latestEventText = latestTimelineEvent?.let { messageSummaryFormatter.format(it) } + + val formattedTimestamp = dateFormatter.format( + timestamp = item.latestEvent?.timestamp ?: item.rootEvent.timestamp, + mode = DateFormatterMode.TimeOrDate, + useRelative = true, + ) + + ThreadListRowItem( + item = item, + rootEventText = rootEventText, + latestEventText = latestEventText, + formattedTimestamp = formattedTimestamp, + ) + }.toImmutableList() + } + } + + val paginationStatus by produceState( + initialValue = ThreadListPaginationStatus.Idle(hasMoreToLoad = true), + key1 = threadsListService + ) { + threadsListService + .subscribeToPaginationUpdates() + .collect { value = it } + } + + val roomInfo by room.roomInfoFlow.collectAsState() + + DisposableEffect(Unit) { + onDispose { + threadsListService.destroy() + } + } + + fun handleEvent(event: ThreadsListEvents) { + when (event) { + ThreadsListEvents.Paginate -> if ((paginationStatus as? ThreadListPaginationStatus.Idle)?.hasMoreToLoad == true) { + coroutineScope.launch { + Timber.d("Paginating thread list: $paginationStatus") + threadsListService.paginate() + } + } else { + Timber.d("Not paginating since there is nothing else to load, current status: $paginationStatus") + } + } + } + + return ThreadsListState( + threads = threads, + roomId = room.roomId, + roomName = roomInfo.name ?: room.roomId.value, + roomAvatarUrl = roomInfo.avatarUrl, + isRoomTombstoned = roomInfo.successorRoom != null, + eventSink = ::handleEvent, + ) + } +} + +data class ThreadsListState( + val roomId: RoomId, + val roomName: String, + val roomAvatarUrl: String?, + val isRoomTombstoned: Boolean, + val threads: ImmutableList, + val eventSink: (ThreadsListEvents) -> Unit, +) + +sealed interface ThreadsListEvents { + data object Paginate : ThreadsListEvents +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListView.kt new file mode 100644 index 0000000000..c93af5c162 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListView.kt @@ -0,0 +1,380 @@ +/* + * Copyright (c) 2026 Element Creations 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.messages.impl.threads.list + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.itemsIndexed +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.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom +import io.element.android.libraries.designsystem.components.avatar.Avatar +import io.element.android.libraries.designsystem.components.avatar.AvatarData +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.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.HorizontalDivider +import io.element.android.libraries.designsystem.theme.components.Icon +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.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.core.asEventId +import io.element.android.libraries.matrix.api.room.threads.ThreadListItem +import io.element.android.libraries.matrix.api.room.threads.ThreadListItemEvent +import io.element.android.libraries.matrix.api.timeline.item.event.EventContent +import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent +import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails +import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.getAvatarUrl +import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName +import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.delay + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ThreadsListView( + state: ThreadsListState, + onThreadClick: (ThreadId) -> Unit, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + title = { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { + Avatar( + avatarData = AvatarData( + id = state.roomId.value, + name = state.roomName, + url = state.roomAvatarUrl, + size = AvatarSize.CurrentUserTopBar, + ), + avatarType = AvatarType.Room(isTombstoned = state.isRoomTombstoned), + contentDescription = null, + ) + Column { + Text( + text = stringResource(CommonStrings.common_threads), + style = ElementTheme.typography.fontBodyLgMedium, + color = ElementTheme.colors.textPrimary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = state.roomName, + style = ElementTheme.typography.fontBodyXsRegular, + color = ElementTheme.colors.textSecondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + }, + navigationIcon = { + BackButton(onBackClick) + } + ) + } + ) { padding -> + val lazyListState = rememberLazyListState() + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = padding, + state = lazyListState, + ) { + itemsIndexed(state.threads, key = { _, row -> row.item.threadId }) { index, row -> + ThreadListItemRow( + threadItem = row, + onClick = onThreadClick, + ) + + if (index < state.threads.size - 1) { + HorizontalDivider() + } + } + } + + ScrollHelper(lazyListState) { + state.eventSink(ThreadsListEvents.Paginate) + } + } +} + +@Composable +private fun ScrollHelper( + listState: LazyListState, + onPaginate: () -> Unit, +) { + val lastVisibleItemIndex by remember { + derivedStateOf { listState.firstVisibleItemIndex + listState.layoutInfo.visibleItemsInfo.size - 1 } + } + val needsPagination by remember { + derivedStateOf { + val canLoadNewItems = listState.isScrollInProgress || listState.firstVisibleItemScrollOffset == 0 + canLoadNewItems && lastVisibleItemIndex == listState.layoutInfo.totalItemsCount - 1 + } + } + LaunchedEffect(needsPagination, lastVisibleItemIndex) { + if (needsPagination) { + onPaginate() + delay(400L) + } + } +} + +@Composable +private fun ThreadListItemRow( + threadItem: ThreadListRowItem, + onClick: (ThreadId) -> Unit, +) { + Row( + modifier = Modifier + .clickable { onClick(threadItem.item.threadId) } + .fillMaxWidth() + .padding(top = 4.dp, bottom = 8.dp, start = 16.dp, end = 16.dp), + ) { + val rootEvent = threadItem.item.rootEvent + val senderProfile = rootEvent.senderProfile + Avatar( + modifier = Modifier.align(Alignment.CenterVertically), + avatarData = AvatarData( + id = rootEvent.senderId.value, + name = senderProfile.getDisambiguatedDisplayName(rootEvent.senderId), + url = senderProfile.getAvatarUrl(), + size = AvatarSize.ThreadsListItem, + ), + avatarType = AvatarType.User, + contentDescription = null, + ) + + Spacer(modifier = Modifier.width(16.dp)) + + Column(modifier = Modifier.fillMaxWidth()) { + // TODO actually compute these values based on the thread state (not available yet) + val hasMentions = false + val hasUnreadNotifications = false + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + modifier = Modifier.weight(1f), + text = senderProfile.getDisambiguatedDisplayName(rootEvent.senderId), + style = ElementTheme.typography.fontBodyLgMedium, + color = ElementTheme.colors.textPrimary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + Text( + text = threadItem.formattedTimestamp, + style = ElementTheme.typography.fontBodySmRegular, + color = if (hasUnreadNotifications || hasMentions) ElementTheme.colors.textActionAccent else ElementTheme.colors.textSecondary, + ) + } + + Spacer(modifier = Modifier.height(2.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.weight(1f), + text = threadItem.rootEventText.orEmpty(), + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textSecondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(7.dp) + ) { + if (hasMentions) { + Icon( + modifier = Modifier.size(14.dp), + imageVector = CompoundIcons.Mention(), + contentDescription = null, + tint = ElementTheme.colors.textActionAccent, + ) + } + + UnreadIndicatorAtom( + size = 14.dp, + isVisible = hasUnreadNotifications, + ) + } + } + + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "${threadItem.item.numberOfReplies}", + style = ElementTheme.typography.fontBodySmRegular, + color = ElementTheme.colors.textSecondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + Spacer(modifier = Modifier.width(4.dp)) + + Icon( + modifier = Modifier.size(20.dp), + imageVector = CompoundIcons.ThreadsSolid(), + contentDescription = null, + tint = ElementTheme.colors.iconSecondary + ) + + Spacer(modifier = Modifier.width(8.dp)) + + threadItem.item.latestEvent?.let { latestEvent -> + Avatar( + avatarData = AvatarData( + id = latestEvent.senderId.value, + name = latestEvent.senderProfile.getDisambiguatedDisplayName(latestEvent.senderId), + url = latestEvent.senderProfile.getAvatarUrl(), + size = AvatarSize.TimelineThreadLatestEventSender, + ), + avatarType = AvatarType.User, + contentDescription = null, + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = threadItem.latestEventText.orEmpty(), + style = ElementTheme.typography.fontBodySmRegular, + color = ElementTheme.colors.textSecondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } + } +} + +@PreviewsDayNight +@Composable +internal fun ThreadsListViewPreview() { + ElementPreview { + ThreadsListView( + state = ThreadsListState( + roomId = RoomId("!room-id:server"), + roomName = "Room name", + roomAvatarUrl = null, + threads = List(10) { aThreadListRowItem(threadId = ThreadId("\$thread-$it")) }.toImmutableList(), + isRoomTombstoned = false, + eventSink = {}, + ), + onThreadClick = {}, + onBackClick = {}, + ) + } +} + +@PreviewsDayNight +@Composable +internal fun ThreadListItemRowPreview() { + ElementPreview { + ThreadListItemRow( + threadItem = aThreadListRowItem(), + onClick = {}, + ) + } +} + +fun aThreadListRowItem( + threadId: ThreadId = ThreadId("\$a-thread-id"), + rootEvent: ThreadListItemEvent = aThreadListItemEvent(threadId = threadId), + latestEvent: ThreadListItemEvent? = aThreadListItemEvent(threadId = threadId), + numberOfReplies: Long = 42, + rootEventText: String? = "Hello world!", + latestEventText: String? = "Hello again!", + formattedTimestamp: String = "12:34", +) = ThreadListRowItem( + item = aThreadListItem( + threadId = threadId, + rootEvent = rootEvent, + latestEvent = latestEvent, + numberOfReplies = numberOfReplies, + ), + rootEventText = rootEventText, + latestEventText = latestEventText, + formattedTimestamp = formattedTimestamp, +) + +fun aThreadListItem( + threadId: ThreadId = ThreadId("\$a-thread-id"), + rootEvent: ThreadListItemEvent = aThreadListItemEvent(threadId = threadId), + latestEvent: ThreadListItemEvent? = aThreadListItemEvent(threadId = threadId), + numberOfReplies: Long = 42, +) = ThreadListItem( + rootEvent = rootEvent, + latestEvent = latestEvent, + numberOfReplies = numberOfReplies, +) + +fun aThreadListItemEvent( + threadId: ThreadId = ThreadId("\$a-thread-id"), + senderId: UserId = UserId("@a-user-id:server"), + senderProfile: ProfileDetails = ProfileDetails.Ready(displayName = "Alice", displayNameAmbiguous = false, avatarUrl = null), + isOwn: Boolean = false, + content: EventContent = MessageContent( + body = "Hello world!", + inReplyTo = null, + isEdited = false, + threadInfo = null, + type = TextMessageType("Hello world!", null), + ), + timestamp: Long = 0L, +) = ThreadListItemEvent( + eventId = threadId.asEventId(), + senderId = senderId, + senderProfile = senderProfile, + isOwn = isOwn, + content = content, + timestamp = timestamp, +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index 0137b1736d..0c5bb28890 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -84,6 +84,7 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.transform import kotlinx.coroutines.launch import timber.log.Timber @@ -262,11 +263,16 @@ private fun TimelinePrefetchingHelper( firstVisibleItemIndex + layoutInfo.visibleItemsInfo.size >= layoutInfo.totalItemsCount - 40 } + // If we have no timeline items, we need to back paginate to load some messages. This usually happens on all timelines except for live ones. + // This automatic pagination was previously done by the SDK, and we received a `Reset` update, but now we need to do it ourselves. + val isEmptyTimelineFlow = layoutInfoFlow.map { it.totalItemsCount == 0 } + combine( isCloseToStartOfLoadedTimelineFlow.distinctUntilChanged(), isScrollingFlow.distinctUntilChanged(), - ) { needsPrefetch, isScrolling -> - needsPrefetch && isScrolling + isEmptyTimelineFlow, + ) { needsPrefetch, isScrolling, isEmptyAndNeedsBackPagination -> + isEmptyAndNeedsBackPagination || needsPrefetch && isScrolling } .distinctUntilChanged() .collectLatest { needsPrefetch -> diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt index c9bc130905..0361eebc01 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt @@ -747,7 +747,7 @@ private fun MessageEventBubbleContent( } Box( modifier = talkbackCompatModifier - .border(1.dp, ElementTheme.colors.borderInteractiveSecondary, RoundedCornerShape(6.dp)) + .border(1.dp, ElementTheme.colors.separatorPrimary, RoundedCornerShape(6.dp)) .background(ElementTheme.colors.bgCanvasDefault, RoundedCornerShape(6.dp)) .padding(4.dp) ) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/topbars/MessagesViewTopBar.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/topbars/MessagesViewTopBar.kt index 24cd71ae84..4d7242ebf5 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/topbars/MessagesViewTopBar.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/topbars/MessagesViewTopBar.kt @@ -12,10 +12,9 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable @@ -30,8 +29,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.messages.impl.MessagesMenuActions import io.element.android.features.messages.impl.SharedHistoryIcon -import io.element.android.features.messages.impl.timeline.components.CallMenuItem import io.element.android.features.roomcall.api.RoomCallState import io.element.android.features.roomcall.api.aStandByCallState import io.element.android.features.roomcall.api.anOngoingCallState @@ -62,13 +61,12 @@ internal fun MessagesViewTopBar( roomAvatar: AvatarData, isTombstoned: Boolean, heroes: ImmutableList, - roomCallState: RoomCallState, dmUserIdentityState: IdentityState?, sharedHistoryIcon: SharedHistoryIcon, onRoomDetailsClick: () -> Unit, - onJoinCallClick: (isAudioCall: Boolean) -> Unit, onBackClick: () -> Unit, modifier: Modifier = Modifier, + menuActions: @Composable RowScope.() -> Unit, ) { TopAppBar( modifier = modifier, @@ -126,13 +124,7 @@ internal fun MessagesViewTopBar( } } }, - actions = { - CallMenuItem( - roomCallState = roomCallState, - onJoinCallClick = onJoinCallClick, - ) - Spacer(Modifier.width(8.dp)) - }, + actions = menuActions, windowInsets = WindowInsets(0.dp) ) } @@ -186,17 +178,24 @@ internal fun MessagesViewTopBarPreview() = ElementPreview { roomCallState: RoomCallState = RoomCallState.Unavailable, dmUserIdentityState: IdentityState? = null, sharedHistoryIcon: SharedHistoryIcon = SharedHistoryIcon.NONE, + displayThreads: Boolean = false, ) = MessagesViewTopBar( roomName = roomName, roomAvatar = roomAvatar, isTombstoned = isTombstoned, heroes = heroes, - roomCallState = roomCallState, dmUserIdentityState = dmUserIdentityState, sharedHistoryIcon = sharedHistoryIcon, onRoomDetailsClick = {}, - onJoinCallClick = {}, onBackClick = {}, + menuActions = { + MessagesMenuActions( + roomCallState = roomCallState, + displayThreads = displayThreads, + onJoinCallClick = {}, + onThreadsListClick = {}, + ) + } ) Column { AMessagesViewTopBar() @@ -237,5 +236,9 @@ internal fun MessagesViewTopBarPreview() = ElementPreview { roomName = "A room with world_readable history", sharedHistoryIcon = SharedHistoryIcon.WORLD_READABLE, ) + HorizontalDivider() + AMessagesViewTopBar( + displayThreads = true, + ) } } diff --git a/features/messages/impl/src/main/res/values-it/translations.xml b/features/messages/impl/src/main/res/values-it/translations.xml index 0d750e7ed5..4eb3fd61bb 100644 --- a/features/messages/impl/src/main/res/values-it/translations.xml +++ b/features/messages/impl/src/main/res/values-it/translations.xml @@ -35,7 +35,7 @@ "Registra video" "Allegato" "Libreria di foto e video" - "Posizione" + "Condividi posizione" "Sondaggio" "Formattazione del testo" "La cronologia dei messaggi non è attualmente disponibile." diff --git a/features/messages/impl/src/main/res/values-ja/translations.xml b/features/messages/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..4fd18e8db5 --- /dev/null +++ b/features/messages/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,81 @@ + + + "イベントの送信者と、使用された端末の所有者が一致しません。" + "この暗号化されたメッセージの真正性を、この端末では保証できません。" + "以前に検証されたユーザーにより暗号化されています。" + "暗号化されていません。" + "削除されたまたは不明な端末により暗号化されています。" + "所有者に検証されていない端末により暗号化されています。" + "未検証のユーザーにより暗号化されています。" + "アクティビティ" + "旗" + "食べ物" + "動物・自然" + "物" + "顔・人" + "旅・場所" + "最近使用" + "記号" + "古いアプリケーションを使用しているユーザーはキャプションを見られない可能性があります。" + "動画のアップロード画質を変更するにはタップしてください" + "ファイルをアップロードに失敗しました。" + "ファイルの処理に失敗しました。再試行してください。" + "ファイルのアップロードに失敗しました。再試行してください。" + "許容されている最大サイズは %1$s です。" + "ファイルが大きすぎるためアップロードできません" + "個数 %1$d / %2$d" + "画像の品質を最適化" + "処理中…" + "ユーザーをブロック" + "このユーザーからのメッセージをすべて非表示にする場合はチェックしてください。" + "このメッセージはホームサーバーの管理者に報告されます。暗号化されたメッセージを確認することはできません。" + "このコンテンツを通報する理由" + "カメラ" + "写真を撮影" + "動画を撮影" + "添付ファイル" + "アルバムの写真・動画" + "場所を共有" + "投票" + "書式設定" + "過去のメッセージを現在表示できません。" + "このルームの過去のメッセージを表示できません。確認するには、この端末を検証してください。" + "招待し直しますか?" + "このチャットにはあなた一人だけです" + "ルーム全体に通知" + "全員" + "再送信する" + "メッセージの送信に失敗しました" + "リアクションを追加" + "%1$s の始まりです。" + "ここが会話の開始点です。" + "非対応の着信です。新しい Element X を使用できないか確認してください。" + "一部を表示" + "メッセージをコピーしました" + "このルームに発言する権限がありません" + + "%1$d 人の反応 %2$s" + + + "あなたと %1$d 人の反応 %2$s" + + "%1$s と反応" + "一部を表示" + "さらに表示" + "リアクションのまとめを表示" + "新着" + + "%1$d 個のルーム更新点" + + "新しいルームに移動" + "このルームは移行して非アクティブ状態です" + "古いメッセージを表示" + "このルームは他のルームからの移行先です" + + "%1$s, %2$s 他 %3$d 人" + + + "%1$s が入力中" + + "%1$s と %2$s" + diff --git a/features/messages/impl/src/main/res/values-sv/translations.xml b/features/messages/impl/src/main/res/values-sv/translations.xml index 21d2cd5fe2..2e10fd3166 100644 --- a/features/messages/impl/src/main/res/values-sv/translations.xml +++ b/features/messages/impl/src/main/res/values-sv/translations.xml @@ -33,7 +33,7 @@ "Spela in video" "Bilaga" "Foto- och videobibliotek" - "Plats" + "Dela plats" "Omröstning" "Textformatering" "Meddelandehistoriken är för närvarande otillgänglig." diff --git a/features/messages/impl/src/main/res/values-vi/translations.xml b/features/messages/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..81ac0b7b8e --- /dev/null +++ b/features/messages/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,60 @@ + + + "Người gửi sự kiện không khớp với chủ sở hữu của thiết bị đã gửi nó." + "Tin nhắn mã hóa này không thể được xác thực trên thiết bị này." + "Được mã hóa bởi một người dùng đã từng được xác minh." + "Không được mã hóa" + "Được mã hóa bởi một thiết bị không xác định hoặc đã bị xóa." + "Được mã hóa bởi một thiết bị chưa được chủ sở hữu xác minh." + "Được mã hóa bởi một người dùng chưa được xác minh." + "Hoạt động" + "Cờ" + "Thực phẩm và đồ uống" + "Động vật và thiên nhiên" + "Đồ vật" + "Mặt cười & mọi người" + "Du lịch và địa danh" + "Biểu tượng" + "Xử lý phương tiện tải lên không thành công, vui lòng thử lại." + "Không thể tải lên tệp phương tiện. Vui lòng thử lại." + "Chặn người dùng" + "Chọn tùy chọn này nếu bạn muốn ẩn tất cả tin nhắn hiện tại và tương lai từ người dùng này." + "Tin nhắn này sẽ được báo cáo cho quản trị viên máy chủ của bạn. Họ sẽ không thể đọc bất kỳ tin nhắn được mã hóa nào." + "Lý do báo cáo nội dung này" + "Máy ảnh" + "Chụp ảnh" + "Quay video" + "Tệp đính kèm" + "Thư viện ảnh và video" + "Chia sẻ vị trí" + "Bỏ phiếu" + "Định dạng văn bản" + "Lịch sử tin nhắn hiện không khả dụng." + "Lịch sử tin nhắn không khả dụng trong phòng này. Vui lòng xác minh thiết bị này để xem lịch sử tin nhắn của bạn." + "Bạn có muốn mời họ quay lại không?" + "Bạn đang một mình trong cuộc trò chuyện này" + "Thông báo cho cả phòng" + "Mọi người" + "Gửi lại" + "Gửi tin nhắn không thành công" + "Thêm biểu cảm" + "Đây là sự kiện khởi đầu của phòng %1$s ." + "Đây là khởi đầu của cuộc trò chuyện này." + "Cuộc gọi không được hỗ trợ. Hãy hỏi xem người gọi có thể sử dụng ứng dụng Element X mới hay không." + "Thu gọn" + "Đã sao chép tin nhắn" + "Bạn không có quyền gửi tin nhắn trong phòng này" + "Thu gọn" + "Xem thêm" + "Mới" + + "%1$d số lượng phòng thay đổi" + + + "%1$s,%2$s và %3$d người khác" + + + "%1$s đang gõ" + + "%1$s và %2$s" + diff --git a/features/messages/impl/src/main/res/values-zh/translations.xml b/features/messages/impl/src/main/res/values-zh/translations.xml index 5247193b84..2a6b9bf78d 100644 --- a/features/messages/impl/src/main/res/values-zh/translations.xml +++ b/features/messages/impl/src/main/res/values-zh/translations.xml @@ -26,7 +26,7 @@ "第%1$d/%2$d项" "优化图像质量" "处理中…" - "封禁用户" + "屏蔽用户" "请确认是否要隐藏该用户当前和未来的所有信息" "此消息将举报给您的服务器管理员。他们无法读取任何加密消息。" "举报此内容的原因" @@ -35,13 +35,13 @@ "录制视频" "附件" "照片和视频库" - "位置" + "共享位置" "投票" "文本格式化" "消息历史记录当前不可用。" "此聊天室无法查看消息历史记录。请验证此设备以查看之。" "您想邀请他们回来吗?" - "聊天中只有你一个人" + "此聊天室中只有您一个人" "通知整个聊天室" "所有人" "再次发送" diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index f6967de0e5..6e12c607d8 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -24,6 +24,7 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer import io.element.android.features.messages.impl.messagecomposer.MessageComposerState import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState import io.element.android.features.messages.impl.pinned.banner.aLoadedPinnedMessagesBannerState +import io.element.android.features.messages.impl.threads.list.aThreadListItem import io.element.android.features.messages.impl.timeline.FakeMarkAsFullyRead import io.element.android.features.messages.impl.timeline.MarkAsFullyRead import io.element.android.features.messages.impl.timeline.TimelineController @@ -88,6 +89,7 @@ import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions +import io.element.android.libraries.matrix.test.room.threads.FakeThreadsListService import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.matrix.test.timeline.aTimelineItemDebugInfo import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails @@ -110,6 +112,7 @@ import io.element.android.tests.testutils.testWithLifecycleOwner import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runCurrent @@ -1258,6 +1261,35 @@ class MessagesPresenterTest { } } + @Test + fun `present - only has threads enabled if the feature flag is on`() = runTest { + val itemsFlow = MutableStateFlow(listOf(aThreadListItem())) + val room = FakeJoinedRoom( + threadsListService = FakeThreadsListService(items = itemsFlow) + ) + val featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.Threads.key to false) + ) + val presenter = createMessagesPresenter( + joinedRoom = room, + featureFlagService = featureFlagService + ) + presenter.testWithLifecycleOwner { + val initialState = awaitItem() + // The feature flag is disabled, so even if the thread list has items, it will return it doesn't have any + assertThat(initialState.threads.hasThreads).isFalse() + + // Enable the feature flag, now it should reflect the thread list state + featureFlagService.setFeatureEnabled(FeatureFlags.RoomThreadList, true) + skipItems(1) + assertThat(awaitItem().threads.hasThreads).isTrue() + + // And if we remove the items, it should update accordingly + itemsFlow.value = emptyList() + assertThat(awaitItem().threads.hasThreads).isFalse() + } + } + private fun roomPermissions( canStartCall: Boolean = true, canRedactOther: Boolean = true, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt index c78aa39265..62b9eac68d 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt @@ -73,6 +73,7 @@ import io.element.android.tests.testutils.assertNoNodeWithText import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnceWithParam +import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.setSafeContent import kotlinx.collections.immutable.persistentListOf @@ -521,6 +522,9 @@ class MessagesViewTest { rule.setMessagesView( state = stateWithActionListState, ) + // Clear initial 'LoadMore' event emitted when setting the state + eventsRecorder.clear() + val verifiedUserSendFailure = rule.activity.getString(CommonStrings.screen_timeline_item_menu_send_failure_changed_identity, "Alice") rule.onNodeWithText(verifiedUserSendFailure).performClick() // Give time for the close animation to complete @@ -584,6 +588,9 @@ class MessagesViewTest { ), ) rule.setMessagesView(state = state) + // Clear initial 'LoadMore' event emitted when setting the state + eventsRecorder.clear() + rule.onNodeWithText("This is a pinned message").performClick() eventsRecorder.assertSingle(TimelineEvent.FocusOnEvent(AN_EVENT_ID, debounce = FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS.milliseconds)) } @@ -600,12 +607,32 @@ class MessagesViewTest { timelineState = aTimelineState(eventSink = eventsRecorder) ) rule.setMessagesView(state = state) + // Clear initial 'LoadMore' event emitted when setting the state + eventsRecorder.clear() + val text = rule.activity.getString(R.string.screen_room_timeline_tombstoned_room_action) // The bottomsheet subcompose seems to make the node to appear twice rule.onAllNodesWithText(text).onFirst().performClick() eventsRecorder.assertSingle(TimelineEvent.NavigateToPredecessorOrSuccessorRoom(successorRoomId)) } + @Test + fun `clicking on threads list button calls the expected function`() { + val state = aMessagesState( + threads = MessagesState.Threads( + hasThreads = true, + hasUnreadThreads = false, + ) + ) + val onThreadsListClicked = lambdaRecorder {} + rule.setMessagesView( + state = state, + onThreadsListClicked = onThreadsListClicked, + ) + rule.onNodeWithContentDescription("Threads").performClick() + onThreadsListClicked.assertions().isCalledOnce() + } + @Test fun `no banner shown when there is no successor room`() { val eventsRecorder = EventsRecorder(expectEvents = false) @@ -630,6 +657,7 @@ private fun AndroidComposeTestRule.setMessa onCreatePollClick: () -> Unit = EnsureNeverCalled(), onJoinCallClick: (Boolean) -> Unit = EnsureNeverCalledWithParam(), onViewAllPinnedMessagesClick: () -> Unit = EnsureNeverCalled(), + onThreadsListClicked: () -> Unit = EnsureNeverCalled(), ) { setSafeContent { // Cannot use the RichTextEditor, so simulate a LocalInspectionMode @@ -646,6 +674,7 @@ private fun AndroidComposeTestRule.setMessa onJoinCallClick = onJoinCallClick, onViewAllPinnedMessagesClick = onViewAllPinnedMessagesClick, knockRequestsBannerView = {}, + onThreadsListClick = onThreadsListClicked, ) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt index c7eb7c0bce..2f87af3df0 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt @@ -48,38 +48,42 @@ internal fun TestScope.aTimelineItemsFactoryCreator(): TimelineItemsFactory.Crea } } +internal fun aTimelineItemContentFactory( + timelineEventFormatter: TimelineEventFormatter = aTimelineEventFormatter(), + matrixClient: FakeMatrixClient = FakeMatrixClient(), +): TimelineItemContentFactory = TimelineItemContentFactory( + messageFactory = TimelineItemContentMessageFactory( + fileSizeFormatter = FakeFileSizeFormatter(), + fileExtensionExtractor = FileExtensionExtractorWithoutValidation(), + htmlConverterProvider = FakeHtmlConverterProvider(), + permalinkParser = FakePermalinkParser(), + textPillificationHelper = FakeTextPillificationHelper(), + ), + redactedMessageFactory = TimelineItemContentRedactedFactory(), + stickerFactory = TimelineItemContentStickerFactory( + fileSizeFormatter = FakeFileSizeFormatter(), + fileExtensionExtractor = FileExtensionExtractorWithoutValidation() + ), + pollFactory = TimelineItemContentPollFactory(FakePollContentStateFactory()), + utdFactory = TimelineItemContentUTDFactory(), + roomMembershipFactory = TimelineItemContentRoomMembershipFactory(timelineEventFormatter), + profileChangeFactory = TimelineItemContentProfileChangeFactory(timelineEventFormatter), + stateFactory = TimelineItemContentStateFactory(timelineEventFormatter), + failedToParseMessageFactory = TimelineItemContentFailedToParseMessageFactory(), + failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory(), + sessionId = matrixClient.sessionId, +) + internal fun TestScope.aTimelineItemsFactory( config: TimelineItemsFactoryConfig, ): TimelineItemsFactory { - val timelineEventFormatter = aTimelineEventFormatter() val matrixClient = FakeMatrixClient() return TimelineItemsFactory( dispatchers = testCoroutineDispatchers(), eventItemFactoryCreator = object : TimelineItemEventFactory.Creator { override fun create(config: TimelineItemsFactoryConfig): TimelineItemEventFactory { return TimelineItemEventFactory( - contentFactory = TimelineItemContentFactory( - messageFactory = TimelineItemContentMessageFactory( - fileSizeFormatter = FakeFileSizeFormatter(), - fileExtensionExtractor = FileExtensionExtractorWithoutValidation(), - htmlConverterProvider = FakeHtmlConverterProvider(), - permalinkParser = FakePermalinkParser(), - textPillificationHelper = FakeTextPillificationHelper(), - ), - redactedMessageFactory = TimelineItemContentRedactedFactory(), - stickerFactory = TimelineItemContentStickerFactory( - fileSizeFormatter = FakeFileSizeFormatter(), - fileExtensionExtractor = FileExtensionExtractorWithoutValidation() - ), - pollFactory = TimelineItemContentPollFactory(FakePollContentStateFactory()), - utdFactory = TimelineItemContentUTDFactory(), - roomMembershipFactory = TimelineItemContentRoomMembershipFactory(timelineEventFormatter), - profileChangeFactory = TimelineItemContentProfileChangeFactory(timelineEventFormatter), - stateFactory = TimelineItemContentStateFactory(timelineEventFormatter), - failedToParseMessageFactory = TimelineItemContentFailedToParseMessageFactory(), - failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory(), - sessionId = matrixClient.sessionId, - ), + contentFactory = aTimelineItemContentFactory(matrixClient = matrixClient), matrixClient = matrixClient, dateFormatter = FakeDateFormatter(), permalinkParser = FakePermalinkParser(), diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/FakePinnedMessagesListNavigator.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/FakePinnedMessagesListNavigator.kt index 479139a45b..69eaae3699 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/FakePinnedMessagesListNavigator.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/FakePinnedMessagesListNavigator.kt @@ -9,6 +9,7 @@ package io.element.android.features.messages.impl.pinned.list import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo class FakePinnedMessagesListNavigator : PinnedMessagesListNavigator { @@ -26,4 +27,9 @@ class FakePinnedMessagesListNavigator : PinnedMessagesListNavigator { override fun forwardEvent(eventId: EventId) { onForwardEventClickLambda?.invoke(eventId) } + + var onOpenThreadLambda: ((ThreadId) -> Unit)? = null + override fun navigateToThread(threadRootId: ThreadId) { + onOpenThreadLambda?.invoke(threadRootId) + } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/threads/ThreadsListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/threads/ThreadsListPresenterTest.kt new file mode 100644 index 0000000000..9f8d210a16 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/threads/ThreadsListPresenterTest.kt @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2026 Element Creations 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.messages.impl.threads + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.messages.impl.fixtures.aTimelineItemContentFactory +import io.element.android.features.messages.impl.messagesummary.FakeMessageSummaryFormatter +import io.element.android.features.messages.impl.threads.list.ThreadsListEvents +import io.element.android.features.messages.impl.threads.list.ThreadsListPresenter +import io.element.android.features.messages.impl.threads.list.aThreadListItem +import io.element.android.libraries.dateformatter.test.FakeDateFormatter +import io.element.android.libraries.matrix.test.AN_AVATAR_URL +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_ROOM_NAME +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.matrix.test.room.threads.FakeThreadsListService +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.test +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class ThreadsListPresenterTest { + @Test + fun `present - initial state`() = runTest { + createThreadsListPresenter().test { + awaitItem().run { + assertThat(threads).isEmpty() + assertThat(roomId).isEqualTo(A_ROOM_ID) + assertThat(roomName).isEqualTo(A_ROOM_NAME) + assertThat(roomAvatarUrl).isEqualTo(AN_AVATAR_URL) + } + } + } + + @Test + fun `present - paginate`() = runTest { + val paginateRecorder = lambdaRecorder> { Result.success(Unit) } + val threadsListService = FakeThreadsListService(paginate = paginateRecorder) + val room = FakeJoinedRoom(threadsListService = threadsListService) + createThreadsListPresenter(room).test { + val initialItem = awaitItem() + + // Pagination is automatically triggered on start, so we should have one call to paginate already + paginateRecorder.assertions().isCalledOnce() + + initialItem.eventSink(ThreadsListEvents.Paginate) + + // Simulate a pagination result + threadsListService.emit(listOf(aThreadListItem())) + + // We should have a second call to paginate after the event is sent + paginateRecorder.assertions().isCalledExactly(2) + + // And we receive the new items + assertThat(awaitItem().threads).isNotEmpty() + } + } + + private fun createThreadsListPresenter( + room: FakeJoinedRoom = FakeJoinedRoom(), + ): ThreadsListPresenter { + return ThreadsListPresenter( + room = room, + timelineItemContentFactory = aTimelineItemContentFactory(), + messageSummaryFormatter = FakeMessageSummaryFormatter(), + dateFormatter = FakeDateFormatter(), + ) + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt index 3a97fbd9dc..c05625e2d3 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt @@ -67,24 +67,31 @@ class TimelineViewTest { @Test fun `reaching the end of the timeline does not send a LoadMore event`() { - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder() rule.setTimelineView( state = aTimelineState( + timelineItems = persistentListOf(aTimelineItemEvent(content = aTimelineItemImageContent())), eventSink = eventsRecorder, ), ) + eventsRecorder.assertSingle(TimelineEvent.OnScrollFinished(firstIndex = 0)) } @Test fun `scroll to bottom on live timeline does not emit the Event`() { - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder() rule.setTimelineView( state = aTimelineState( + timelineItems = persistentListOf(aTimelineItemEvent(content = aTimelineItemImageContent())), isLive = true, eventSink = eventsRecorder, ), forceJumpToBottomVisibility = true, ) + + eventsRecorder.assertSingle(TimelineEvent.OnScrollFinished(firstIndex = 0)) + eventsRecorder.clear() + val contentDescription = rule.activity.getString(CommonStrings.a11y_jump_to_bottom) rule.onNodeWithContentDescription(contentDescription).performClick() } @@ -94,15 +101,33 @@ class TimelineViewTest { val eventsRecorder = EventsRecorder() rule.setTimelineView( state = aTimelineState( + timelineItems = persistentListOf(aTimelineItemEvent(content = aTimelineItemImageContent())), isLive = false, eventSink = eventsRecorder, ), ) + + eventsRecorder.assertSingle(TimelineEvent.OnScrollFinished(firstIndex = 0)) + eventsRecorder.clear() + val contentDescription = rule.activity.getString(CommonStrings.a11y_jump_to_bottom) rule.onNodeWithContentDescription(contentDescription).performClick() eventsRecorder.assertSingle(TimelineEvent.JumpToLive) } + @Test + fun `an empty timeline triggers a prefetch`() { + val eventsRecorder = EventsRecorder() + rule.setTimelineView( + state = aTimelineState( + timelineItems = persistentListOf(), + eventSink = eventsRecorder, + ), + ) + + eventsRecorder.assertSingle(TimelineEvent.LoadMore(Timeline.PaginationDirection.BACKWARDS)) + } + @Test fun `show shield dialog`() { val eventsRecorder = EventsRecorder() @@ -133,11 +158,15 @@ class TimelineViewTest { val eventsRecorder = EventsRecorder() rule.setTimelineView( state = aTimelineState( + timelineItems = persistentListOf(aTimelineItemEvent(content = aTimelineItemImageContent())), isLive = false, eventSink = eventsRecorder, messageShield = aCriticalShield(), ), ) + eventsRecorder.assertSingle(TimelineEvent.OnScrollFinished(firstIndex = 0)) + eventsRecorder.clear() + rule.clickOn(CommonStrings.action_ok) eventsRecorder.assertSingle(TimelineEvent.HideShieldDialog) } diff --git a/features/messages/test/build.gradle.kts b/features/messages/test/build.gradle.kts index 29c00ece73..b839f8de06 100644 --- a/features/messages/test/build.gradle.kts +++ b/features/messages/test/build.gradle.kts @@ -26,5 +26,5 @@ dependencies { implementation(projects.libraries.voicerecorder.test) implementation(projects.services.analytics.test) implementation(projects.tests.testutils) - implementation(projects.libraries.mediaupload.impl) + implementation(projects.libraries.mediaupload.api) } diff --git a/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/timeline/voicemessages/composer/FakeDefaultVoiceMessageComposerPresenterFactory.kt b/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/timeline/voicemessages/composer/FakeDefaultVoiceMessageComposerPresenterFactory.kt index 17de179b55..466bfba4fd 100644 --- a/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/timeline/voicemessages/composer/FakeDefaultVoiceMessageComposerPresenterFactory.kt +++ b/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/timeline/voicemessages/composer/FakeDefaultVoiceMessageComposerPresenterFactory.kt @@ -12,13 +12,10 @@ import io.element.android.features.messages.impl.voicemessages.composer.DefaultV import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPlayer import io.element.android.features.messages.test.FakeMessageComposerContext import io.element.android.libraries.matrix.api.timeline.Timeline -import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.mediaplayer.test.FakeAudioFocus import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer import io.element.android.libraries.mediaupload.api.MediaSender -import io.element.android.libraries.mediaupload.impl.DefaultMediaSender -import io.element.android.libraries.mediaupload.test.FakeMediaOptimizationConfigProvider -import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor +import io.element.android.libraries.mediaupload.test.FakeMediaSender import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory import io.element.android.libraries.voicerecorder.test.FakeVoiceRecorder import io.element.android.services.analytics.test.FakeAnalyticsService @@ -26,12 +23,7 @@ import kotlinx.coroutines.CoroutineScope class FakeDefaultVoiceMessageComposerPresenterFactory( private val sessionCoroutineScope: CoroutineScope, - private val mediaSender: MediaSender = DefaultMediaSender( - preProcessor = FakeMediaPreProcessor(), - room = FakeJoinedRoom(), - timelineMode = Timeline.Mode.Live, - mediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(), - ), + private val mediaSender: MediaSender = FakeMediaSender(), ) : DefaultVoiceMessageComposerPresenter.Factory { override fun create(timelineMode: Timeline.Mode): DefaultVoiceMessageComposerPresenter { return DefaultVoiceMessageComposerPresenter( diff --git a/features/migration/impl/build.gradle.kts b/features/migration/impl/build.gradle.kts index a37c3be882..1ba0953349 100644 --- a/features/migration/impl/build.gradle.kts +++ b/features/migration/impl/build.gradle.kts @@ -24,7 +24,7 @@ dependencies { implementation(projects.features.migration.api) implementation(projects.libraries.architecture) implementation(projects.libraries.androidutils) - implementation(projects.libraries.preferences.impl) + implementation(projects.libraries.preferences.api) implementation(libs.androidx.datastore.preferences) implementation(projects.features.rageshake.api) implementation(projects.libraries.designsystem) diff --git a/features/networkmonitor/impl/src/main/kotlin/io/element/android/features/networkmonitor/impl/DefaultNetworkMonitor.kt b/features/networkmonitor/impl/src/main/kotlin/io/element/android/features/networkmonitor/impl/DefaultNetworkMonitor.kt index 7cffa057bc..6981d2c6af 100644 --- a/features/networkmonitor/impl/src/main/kotlin/io/element/android/features/networkmonitor/impl/DefaultNetworkMonitor.kt +++ b/features/networkmonitor/impl/src/main/kotlin/io/element/android/features/networkmonitor/impl/DefaultNetworkMonitor.kt @@ -15,13 +15,11 @@ import android.net.ConnectivityManager import android.net.Network import android.net.NetworkCapabilities import android.net.NetworkRequest -import android.os.Build import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.SingleIn import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkStatus -import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.di.annotations.ApplicationContext import kotlinx.coroutines.CoroutineScope @@ -44,7 +42,6 @@ import java.util.concurrent.atomic.AtomicInteger class DefaultNetworkMonitor( @ApplicationContext context: Context, @AppCoroutineScope appCoroutineScope: CoroutineScope, - private val buildMeta: BuildMeta, ) : NetworkMonitor { private val connectivityManager: ConnectivityManager = context.getSystemService(ConnectivityManager::class.java) @@ -76,17 +73,10 @@ class DefaultNetworkMonitor( } override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { - if (!buildMeta.isEnterpriseBuild) { - // The air-gapped environment detection is only relevant for the enterprise build. - return - } - if (network.networkHandle == connectivityManager.activeNetwork?.networkHandle) { // If the network doesn't have the NET_CAPABILITY_VALIDATED capability, it means that the network is not able to reach the internet // (according to Google), which is a common case in air-gapped environments. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - isInAirGappedEnvironment.value = !networkCapabilities.capabilities.contains(NetworkCapabilities.NET_CAPABILITY_VALIDATED) - } + isInAirGappedEnvironment.value = !networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) } } diff --git a/features/poll/api/src/main/res/values-ja/translations.xml b/features/poll/api/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..c66e27d85d --- /dev/null +++ b/features/poll/api/src/main/res/values-ja/translations.xml @@ -0,0 +1,8 @@ + + + + "総投票数うち%1$d%" + + "前の項目を削除します" + "投票の勝者はこちらです" + diff --git a/features/poll/api/src/main/res/values-vi/translations.xml b/features/poll/api/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..21651828df --- /dev/null +++ b/features/poll/api/src/main/res/values-vi/translations.xml @@ -0,0 +1,5 @@ + + + "Xóa lựa chọn trước đó" + "Đây là câu trả lời chiến thắng" + diff --git a/features/poll/impl/src/main/res/values-ja/translations.xml b/features/poll/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..a4cd3fbff3 --- /dev/null +++ b/features/poll/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,20 @@ + + + "選択肢を追加" + "結果を投票終了後に表示します" + "投票を非表示" + "選択肢 %1$d" + "変更は保存されていません。本当に戻りますか?" + "選択肢を削除 %1$s" + "質問またはトピック" + "何についての投票ですか?" + "投票を作成" + "本当にこの投票を削除しますか?" + "投票を削除" + "投票を編集" + "進行中の投票が見つかりません。" + "過去の投票が見つかりません。" + "進行中" + "過去" + "投票" + diff --git a/features/poll/impl/src/main/res/values-vi/translations.xml b/features/poll/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..dc29666c74 --- /dev/null +++ b/features/poll/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,19 @@ + + + "Thêm lựa chọn" + "Chỉ hiển thị kết quả sau khi cuộc thăm dò kết thúc" + "Ẩn phiếu" + "Lựa chọn %1$d" + "Các thay đổi của bạn chưa được lưu. Bạn có chắc muốn quay lại không?" + "Câu hỏi hoặc chủ đề" + "Cuộc thăm dò này về vấn đề gì?" + "Tạo Cuộc thăm dò ý kiến" + "Bạn có chắc chắn muốn xóa cuộc thăm dò này không?" + "Xóa cuộc thăm dò" + "Sửa cuộc thăm dò" + "Không tìm thấy bất kỳ cuộc thăm dò nào đang diễn ra." + "Không tìm thấy bất kỳ cuộc thăm dò nào trước đây." + "Đang diễn ra" + "Quá khứ" + "Cuộc thăm dò ý kiến" + diff --git a/features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/PreferencesEntryPoint.kt b/features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/PreferencesEntryPoint.kt index 04b471a498..e7fbe6069f 100644 --- a/features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/PreferencesEntryPoint.kt +++ b/features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/PreferencesEntryPoint.kt @@ -50,4 +50,14 @@ interface PreferencesEntryPoint : FeatureEntryPoint { fun navigateToRoomNotificationSettings(roomId: RoomId) fun navigateToEvent(roomId: RoomId, eventId: EventId) } + + fun createAppDeveloperSettingsNode( + parentNode: Node, + buildContext: BuildContext, + callback: DeveloperSettingsCallback, + ): Node + + interface DeveloperSettingsCallback : Plugin { + fun onDone() + } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/DefaultPreferencesEntryPoint.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/DefaultPreferencesEntryPoint.kt index 57c561400c..bacf1bfb48 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/DefaultPreferencesEntryPoint.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/DefaultPreferencesEntryPoint.kt @@ -13,6 +13,7 @@ import com.bumble.appyx.core.node.Node import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import io.element.android.features.preferences.api.PreferencesEntryPoint +import io.element.android.features.preferences.impl.developer.appsettings.AppDeveloperSettingsNode import io.element.android.libraries.architecture.createNode @ContributesBinding(AppScope::class) @@ -28,6 +29,17 @@ class DefaultPreferencesEntryPoint : PreferencesEntryPoint { plugins = listOf(params, callback) ) } + + override fun createAppDeveloperSettingsNode( + parentNode: Node, + buildContext: BuildContext, + callback: PreferencesEntryPoint.DeveloperSettingsCallback, + ): Node { + return parentNode.createNode( + buildContext = buildContext, + plugins = listOf(callback), + ) + } } internal fun PreferencesEntryPoint.InitialTarget.toNavTarget() = when (this) { diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt index 1804d7e070..12de2be746 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt @@ -9,15 +9,8 @@ package io.element.android.features.preferences.impl.developer import androidx.compose.ui.graphics.Color -import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem -import io.element.android.libraries.featureflag.ui.model.FeatureUiModel -import io.element.android.libraries.matrix.api.tracing.TraceLogPack sealed interface DeveloperSettingsEvents { - data class UpdateEnabledFeature(val feature: FeatureUiModel, val isEnabled: Boolean) : DeveloperSettingsEvents - data class SetCustomElementCallBaseUrl(val baseUrl: String?) : DeveloperSettingsEvents - data class SetTracingLogLevel(val logLevel: LogLevelItem) : DeveloperSettingsEvents - data class ToggleTracingLogPack(val logPack: TraceLogPack, val enabled: Boolean) : DeveloperSettingsEvents data class SetShowColorPicker(val show: Boolean) : DeveloperSettingsEvents data class ChangeBrandColor(val color: Color?) : DeveloperSettingsEvents data object ClearCache : DeveloperSettingsEvents diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt index a0d96be540..1598c2ef27 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt @@ -11,61 +11,37 @@ package io.element.android.features.preferences.impl.developer import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.key -import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.graphics.toArgb import dev.zacsweers.metro.Inject import io.element.android.features.enterprise.api.EnterpriseService -import io.element.android.features.preferences.impl.developer.tracing.toLogLevel -import io.element.android.features.preferences.impl.developer.tracing.toLogLevelItem -import io.element.android.features.preferences.impl.model.EnabledFeature +import io.element.android.features.preferences.impl.developer.appsettings.AppDeveloperSettingsState import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase import io.element.android.features.preferences.impl.tasks.ComputeCacheSizeUseCase import io.element.android.features.preferences.impl.tasks.VacuumStoresUseCase -import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState import io.element.android.libraries.androidutils.filesize.FileSizeFormatter import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.core.data.ByteUnit -import io.element.android.libraries.core.extensions.runCatchingExceptions -import io.element.android.libraries.core.meta.BuildMeta -import io.element.android.libraries.core.meta.BuildType -import io.element.android.libraries.featureflag.api.FeatureFlagService -import io.element.android.libraries.featureflag.api.FeatureFlags -import io.element.android.libraries.featureflag.ui.model.FeatureUiModel import io.element.android.libraries.matrix.api.analytics.GetDatabaseSizesUseCase import io.element.android.libraries.matrix.api.core.SessionId -import io.element.android.libraries.preferences.api.store.AppPreferencesStore -import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableMap -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableMap import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import java.net.URL @Inject class DeveloperSettingsPresenter( + private val appDeveloperSettingsPresenter: Presenter, private val sessionId: SessionId, - private val featureFlagService: FeatureFlagService, private val computeCacheSizeUseCase: ComputeCacheSizeUseCase, private val clearCacheUseCase: ClearCacheUseCase, - private val rageshakePresenter: Presenter, - private val appPreferencesStore: AppPreferencesStore, - private val buildMeta: BuildMeta, private val enterpriseService: EnterpriseService, private val vacuumStoresUseCase: VacuumStoresUseCase, private val databaseSizesUseCase: GetDatabaseSizesUseCase, @@ -73,10 +49,6 @@ class DeveloperSettingsPresenter( ) : Presenter { @Composable override fun present(): DeveloperSettingsState { - val rageshakeState = rageshakePresenter.present() - val enabledFeatures = remember { - mutableStateListOf() - } val cacheSize = remember { mutableStateOf>(AsyncData.Uninitialized) } @@ -89,38 +61,9 @@ class DeveloperSettingsPresenter( var showColorPicker by remember { mutableStateOf(false) } - val customElementCallBaseUrl by remember { - appPreferencesStore - .getCustomElementCallBaseUrlFlow() - }.collectAsState(initial = null) - - val tracingLogLevelFlow = remember { - appPreferencesStore.getTracingLogLevelFlow().map { AsyncData.Success(it.toLogLevelItem()) } - } - val tracingLogLevel by tracingLogLevelFlow.collectAsState(initial = AsyncData.Uninitialized) - val tracingLogPacks by produceState(persistentListOf()) { - appPreferencesStore.getTracingLogPacksFlow() - // Sort the entries alphabetically by its title - .map { it.sortedBy { pack -> pack.title } } - .collectLatest { value = it.toImmutableList() } - } - LaunchedEffect(Unit) { computeDatabaseSizes(databaseSizes) - featureFlagService.getAvailableFeatures() - .run { - // Never display room directory search in release builds for Play Store - if (buildMeta.flavorDescription == "GooglePlay" && buildMeta.buildType == BuildType.RELEASE) { - filterNot { it.key == FeatureFlags.RoomDirectorySearch.key } - } else { - this - } - } - .forEach { feature -> - enabledFeatures.add(EnabledFeature(feature, featureFlagService.isFeatureEnabled(feature))) - } } - val featureUiModels = createUiModels(enabledFeatures) val coroutineScope = rememberCoroutineScope() // Compute cache size each time the clear cache action value is changed LaunchedEffect(clearCacheAction.value.isSuccess()) { @@ -129,29 +72,7 @@ class DeveloperSettingsPresenter( fun handleEvent(event: DeveloperSettingsEvents) { when (event) { - is DeveloperSettingsEvents.UpdateEnabledFeature -> coroutineScope.updateEnabledFeature( - enabledFeatures = enabledFeatures, - featureKey = event.feature.key, - enabled = event.isEnabled, - triggerClearCache = { handleEvent(DeveloperSettingsEvents.ClearCache) } - ) - is DeveloperSettingsEvents.SetCustomElementCallBaseUrl -> coroutineScope.launch { - val urlToSave = event.baseUrl.takeIf { !it.isNullOrEmpty() } - appPreferencesStore.setCustomElementCallBaseUrl(urlToSave) - } DeveloperSettingsEvents.ClearCache -> coroutineScope.clearCache(clearCacheAction) - is DeveloperSettingsEvents.SetTracingLogLevel -> coroutineScope.launch { - appPreferencesStore.setTracingLogLevel(event.logLevel.toLogLevel()) - } - is DeveloperSettingsEvents.ToggleTracingLogPack -> coroutineScope.launch { - val currentPacks = tracingLogPacks.toMutableSet() - if (currentPacks.contains(event.logPack)) { - currentPacks.remove(event.logPack) - } else { - currentPacks.add(event.logPack) - } - appPreferencesStore.setTracingLogPacks(currentPacks) - } is DeveloperSettingsEvents.ChangeBrandColor -> coroutineScope.launch { showColorPicker = false val color = event.color @@ -170,56 +91,18 @@ class DeveloperSettingsPresenter( } } + val appDeveloperSettingsState = appDeveloperSettingsPresenter.present() return DeveloperSettingsState( - features = featureUiModels, + appDeveloperSettingsState = appDeveloperSettingsState, cacheSize = cacheSize.value, databaseSizes = databaseSizes.value, clearCacheAction = clearCacheAction.value, - rageshakeState = rageshakeState, - customElementCallBaseUrlState = CustomElementCallBaseUrlState( - baseUrl = customElementCallBaseUrl, - validator = ::customElementCallUrlValidator, - ), - tracingLogLevel = tracingLogLevel, - tracingLogPacks = tracingLogPacks, isEnterpriseBuild = enterpriseService.isEnterpriseBuild, showColorPicker = showColorPicker, eventSink = ::handleEvent, ) } - @Composable - private fun createUiModels( - enabledFeatures: SnapshotStateList, - ): ImmutableList { - return enabledFeatures.map { enabledFeature -> - key(enabledFeature.feature.key) { - remember(enabledFeature) { - FeatureUiModel( - key = enabledFeature.feature.key, - title = enabledFeature.feature.title, - description = enabledFeature.feature.description, - icon = null, - isEnabled = enabledFeature.isEnabled - ) - } - } - }.toImmutableList() - } - - private fun CoroutineScope.updateEnabledFeature( - enabledFeatures: SnapshotStateList, - featureKey: String, - enabled: Boolean, - @Suppress("UNUSED_PARAMETER") triggerClearCache: () -> Unit, - ) = launch { - val featureIndex = enabledFeatures.indexOfFirst { it.feature.key == featureKey }.takeIf { it != -1 } ?: return@launch - val feature = enabledFeatures[featureIndex].feature - if (featureFlagService.setFeatureEnabled(feature, enabled)) { - enabledFeatures[featureIndex] = enabledFeatures[featureIndex].copy(isEnabled = enabled) - } - } - private fun CoroutineScope.computeCacheSize(cacheSize: MutableState>) = launch { suspend { computeCacheSizeUseCase() @@ -253,12 +136,3 @@ class DeveloperSettingsPresenter( }.runCatchingUpdatingState(clearCacheAction) } } - -private fun customElementCallUrlValidator(url: String?): Boolean { - return runCatchingExceptions { - if (url.isNullOrEmpty()) return@runCatchingExceptions - val parsedUrl = URL(url) - if (parsedUrl.protocol !in listOf("http", "https")) error("Incorrect protocol") - if (parsedUrl.host.isNullOrBlank()) error("Missing host") - }.isSuccess -} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt index 920c8ec95c..fa5859a028 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt @@ -8,32 +8,19 @@ package io.element.android.features.preferences.impl.developer -import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem -import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState +import io.element.android.features.preferences.impl.developer.appsettings.AppDeveloperSettingsState import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.featureflag.ui.model.FeatureUiModel -import io.element.android.libraries.matrix.api.tracing.TraceLogPack -import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableMap data class DeveloperSettingsState( - val features: ImmutableList, + val appDeveloperSettingsState: AppDeveloperSettingsState, val cacheSize: AsyncData, val databaseSizes: AsyncData>, - val rageshakeState: RageshakePreferencesState, val clearCacheAction: AsyncAction, - val customElementCallBaseUrlState: CustomElementCallBaseUrlState, - val tracingLogLevel: AsyncData, - val tracingLogPacks: ImmutableList, val isEnterpriseBuild: Boolean, val showColorPicker: Boolean, val eventSink: (DeveloperSettingsEvents) -> Unit ) { val showLoader = clearCacheAction is AsyncAction.Loading } - -data class CustomElementCallBaseUrlState( - val baseUrl: String?, - val validator: (String?) -> Boolean, -) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt index b925eabe9e..28aefd3ad1 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt @@ -9,14 +9,11 @@ package io.element.android.features.preferences.impl.developer import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem -import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState +import io.element.android.features.preferences.impl.developer.appsettings.AppDeveloperSettingsState +import io.element.android.features.preferences.impl.developer.appsettings.anAppDeveloperSettingsState import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.featureflag.ui.model.aFeatureUiModelList -import io.element.android.libraries.matrix.api.tracing.TraceLogPack import kotlinx.collections.immutable.persistentMapOf -import kotlinx.collections.immutable.toImmutableList open class DeveloperSettingsStateProvider : PreviewParameterProvider { override val values: Sequence @@ -25,11 +22,6 @@ open class DeveloperSettingsStateProvider : PreviewParameterProvider = AsyncAction.Uninitialized, - customElementCallBaseUrlState: CustomElementCallBaseUrlState = aCustomElementCallBaseUrlState(), - traceLogPacks: List = emptyList(), isEnterpriseBuild: Boolean = false, showColorPicker: Boolean = false, eventSink: (DeveloperSettingsEvents) -> Unit = {}, ) = DeveloperSettingsState( - features = aFeatureUiModelList(), - rageshakeState = aRageshakePreferencesState(), + appDeveloperSettingsState = appDeveloperSettingsState, cacheSize = AsyncData.Success("1.2 MB"), databaseSizes = AsyncData.Success(persistentMapOf("state_store" to "1.2MB")), clearCacheAction = clearCacheAction, - customElementCallBaseUrlState = customElementCallBaseUrlState, - tracingLogLevel = AsyncData.Success(LogLevelItem.INFO), - tracingLogPacks = traceLogPacks.toImmutableList(), isEnterpriseBuild = isEnterpriseBuild, showColorPicker = showColorPicker, eventSink = eventSink, ) - -fun aCustomElementCallBaseUrlState( - baseUrl: String? = null, - validator: (String?) -> Boolean = { true }, -) = CustomElementCallBaseUrlState( - baseUrl = baseUrl, - validator = validator, -) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt index 444a391d43..3adf9a13de 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt @@ -10,40 +10,28 @@ package io.element.android.features.preferences.impl.developer import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.progressSemantics -import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Composable import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.KeyboardType 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.preferences.impl.R -import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem -import io.element.android.features.rageshake.api.preferences.RageshakePreferencesView +import io.element.android.features.preferences.impl.developer.appsettings.AppDeveloperSettingsView import io.element.android.libraries.designsystem.components.ProgressDialog import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory -import io.element.android.libraries.designsystem.components.preferences.PreferenceDropdown import io.element.android.libraries.designsystem.components.preferences.PreferencePage -import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch -import io.element.android.libraries.designsystem.components.preferences.PreferenceTextField import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.ListItem import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.featureflag.ui.FeatureListView -import io.element.android.libraries.featureflag.ui.model.FeatureUiModel -import io.element.android.libraries.matrix.api.tracing.TraceLogPack import io.element.android.libraries.ui.strings.CommonStrings import io.mhssn.colorpicker.ColorPickerDialog import io.mhssn.colorpicker.ColorPickerType -import kotlinx.collections.immutable.toImmutableList @OptIn(ExperimentalComposeUiApi::class) @Composable @@ -71,52 +59,12 @@ fun DeveloperSettingsView( title = stringResource(id = CommonStrings.common_developer_options) ) { // Note: this is OK to hardcode strings in this debug screen. - PreferenceCategory( - title = "Feature flags", - ) { - FeatureListContent(state) - } - NotificationCategory(onPushHistoryClick) - ElementCallCategory(state = state) - - PreferenceCategory(title = "Rust SDK") { - PreferenceDropdown( - title = "Tracing log level", - supportingText = "Requires app reboot", - selectedOption = state.tracingLogLevel.dataOrNull(), - options = LogLevelItem.entries.toImmutableList(), - onSelectOption = { logLevel -> - state.eventSink(DeveloperSettingsEvents.SetTracingLogLevel(logLevel)) - } - ) - } - PreferenceCategory(title = "Enable trace logs per SDK feature") { - Text( - text = "Requires app reboot", - style = ElementTheme.typography.fontBodyMdRegular, - color = ElementTheme.colors.textSecondary, - modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp) - ) - for (logPack in TraceLogPack.entries) { - PreferenceSwitch( - title = logPack.title, - isChecked = state.tracingLogPacks.contains(logPack), - onCheckedChange = { isChecked -> state.eventSink(DeveloperSettingsEvents.ToggleTracingLogPack(logPack, isChecked)) } - ) - } - } - - PreferenceCategory(title = "Showkase") { - ListItem( - headlineContent = { - Text("Open Showkase browser") - }, - onClick = onOpenShowkase - ) - } - RageshakePreferencesView( - state = state.rageshakeState, + AppDeveloperSettingsView( + state = state.appDeveloperSettingsState, + onOpenShowkase = onOpenShowkase, ) + NotificationCategory(onPushHistoryClick) + if (state.isEnterpriseBuild) { PreferenceCategory(title = "Theme") { ListItem( @@ -137,14 +85,6 @@ fun DeveloperSettingsView( ) } } - PreferenceCategory(title = "Crash") { - ListItem( - headlineContent = { - Text("Crash the app 💥") - }, - onClick = { error("This crash is a test.") } - ) - } val cache = state.cacheSize PreferenceCategory(title = "Cache") { ListItem( @@ -212,32 +152,6 @@ fun DeveloperSettingsView( ) } -@Composable -private fun ElementCallCategory( - state: DeveloperSettingsState, -) { - PreferenceCategory(title = "Element Call") { - val callUrlState = state.customElementCallBaseUrlState - - val supportingText = if (callUrlState.baseUrl.isNullOrEmpty()) { - stringResource(R.string.screen_advanced_settings_element_call_base_url_description) - } else { - callUrlState.baseUrl - } - PreferenceTextField( - headline = stringResource(R.string.screen_advanced_settings_element_call_base_url), - value = callUrlState.baseUrl, - placeholder = "https://.../room", - supportingText = supportingText, - validation = callUrlState.validator, - onValidationErrorMessage = stringResource(R.string.screen_advanced_settings_element_call_base_url_validation_error), - displayValue = { value -> !value.isNullOrEmpty() }, - keyboardOptions = KeyboardOptions.Default.copy(autoCorrectEnabled = false, keyboardType = KeyboardType.Uri), - onChange = { state.eventSink(DeveloperSettingsEvents.SetCustomElementCallBaseUrl(it)) } - ) - } -} - @Composable private fun NotificationCategory(onPushHistoryClick: () -> Unit) { PreferenceCategory(title = stringResource(id = R.string.screen_notification_settings_title)) { @@ -250,20 +164,6 @@ private fun NotificationCategory(onPushHistoryClick: () -> Unit) { } } -@Composable -private fun FeatureListContent( - state: DeveloperSettingsState, -) { - fun onFeatureEnabled(feature: FeatureUiModel, isEnabled: Boolean) { - state.eventSink(DeveloperSettingsEvents.UpdateEnabledFeature(feature, isEnabled)) - } - - FeatureListView( - features = state.features, - onCheckedChange = ::onFeatureEnabled, - ) -} - @PreviewsDayNight @Composable internal fun DeveloperSettingsViewPreview( @@ -273,6 +173,6 @@ internal fun DeveloperSettingsViewPreview( state = state, onOpenShowkase = {}, onPushHistoryClick = {}, - onBackClick = {} + onBackClick = {}, ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsEvent.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsEvent.kt new file mode 100644 index 0000000000..d9641a2810 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsEvent.kt @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2026 Element Creations 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.preferences.impl.developer.appsettings + +import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem +import io.element.android.libraries.featureflag.ui.model.FeatureUiModel +import io.element.android.libraries.matrix.api.tracing.TraceLogPack + +sealed interface AppDeveloperSettingsEvent { + data class UpdateEnabledFeature(val feature: FeatureUiModel, val isEnabled: Boolean) : AppDeveloperSettingsEvent + data class SetCustomElementCallBaseUrl(val baseUrl: String?) : AppDeveloperSettingsEvent + data class SetTracingLogLevel(val logLevel: LogLevelItem) : AppDeveloperSettingsEvent + data class ToggleTracingLogPack(val logPack: TraceLogPack, val enabled: Boolean) : AppDeveloperSettingsEvent +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsNode.kt new file mode 100644 index 0000000000..ae5e710d4b --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsNode.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2026 Element Creations 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.preferences.impl.developer.appsettings + +import androidx.activity.compose.LocalActivity +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.airbnb.android.showkase.models.Showkase +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.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode +import io.element.android.features.preferences.api.PreferencesEntryPoint +import io.element.android.libraries.architecture.callback +import io.element.android.libraries.designsystem.showkase.getBrowserIntent + +@ContributesNode(AppScope::class) +@AssistedInject +class AppDeveloperSettingsNode( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: AppDeveloperSettingsPresenter, +) : Node(buildContext, plugins = plugins) { + private val callback: PreferencesEntryPoint.DeveloperSettingsCallback = callback() + + @Composable + override fun View(modifier: Modifier) { + val activity = requireNotNull(LocalActivity.current) + fun openShowkase() { + val intent = Showkase.getBrowserIntent(activity) + activity.startActivity(intent) + } + + val state = presenter.present() + AppDeveloperSettingsPage( + state = state, + modifier = modifier, + onOpenShowkase = ::openShowkase, + onBackClick = callback::onDone, + ) + } +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPage.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPage.kt new file mode 100644 index 0000000000..81e1304e7b --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPage.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2026 Element Creations 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.preferences.impl.developer.appsettings + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.components.preferences.PreferencePage +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun AppDeveloperSettingsPage( + state: AppDeveloperSettingsState, + onOpenShowkase: () -> Unit, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { + BackHandler( + onBack = onBackClick, + ) + PreferencePage( + modifier = modifier, + onBackClick = { + onBackClick() + }, + title = "Application developer options", + ) { + AppDeveloperSettingsView( + state = state, + onOpenShowkase = onOpenShowkase, + modifier = Modifier.padding(top = 8.dp) + ) + } +} + +@PreviewsDayNight +@Composable +internal fun AppDeveloperSettingsPagePreview() = ElementPreview { + AppDeveloperSettingsPage( + state = anAppDeveloperSettingsState(), + onOpenShowkase = {}, + onBackClick = {}, + ) +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPresenter.kt new file mode 100644 index 0000000000..4c76c6ec7e --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPresenter.kt @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2026 Element Creations 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.preferences.impl.developer.appsettings + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.snapshots.SnapshotStateList +import dev.zacsweers.metro.Inject +import io.element.android.features.preferences.impl.developer.tracing.toLogLevel +import io.element.android.features.preferences.impl.developer.tracing.toLogLevelItem +import io.element.android.features.preferences.impl.model.EnabledFeature +import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.extensions.runCatchingExceptions +import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.core.meta.BuildType +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.ui.model.FeatureUiModel +import io.element.android.libraries.preferences.api.store.AppPreferencesStore +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import java.net.URL + +@Inject +class AppDeveloperSettingsPresenter( + private val featureFlagService: FeatureFlagService, + private val rageshakePresenter: Presenter, + private val appPreferencesStore: AppPreferencesStore, + private val buildMeta: BuildMeta, +) : Presenter { + @Composable + override fun present(): AppDeveloperSettingsState { + val rageshakeState = rageshakePresenter.present() + val enabledFeatures = remember { + mutableStateListOf() + } + val customElementCallBaseUrl by remember { + appPreferencesStore + .getCustomElementCallBaseUrlFlow() + }.collectAsState(initial = null) + + val tracingLogLevelFlow = remember { + appPreferencesStore.getTracingLogLevelFlow().map { AsyncData.Success(it.toLogLevelItem()) } + } + val tracingLogLevel by tracingLogLevelFlow.collectAsState(initial = AsyncData.Uninitialized) + val tracingLogPacks by produceState(persistentListOf()) { + appPreferencesStore.getTracingLogPacksFlow() + // Sort the entries alphabetically by its title + .map { it.sortedBy { pack -> pack.title } } + .collectLatest { value = it.toImmutableList() } + } + + LaunchedEffect(Unit) { + featureFlagService.getAvailableFeatures() + .run { + // Never display room directory search in release builds for Play Store + if (buildMeta.flavorDescription == "GooglePlay" && buildMeta.buildType == BuildType.RELEASE) { + filterNot { it.key == FeatureFlags.RoomDirectorySearch.key } + } else { + this + } + } + .forEach { feature -> + enabledFeatures.add(EnabledFeature(feature, featureFlagService.isFeatureEnabled(feature))) + } + } + val featureUiModels = createUiModels(enabledFeatures) + val coroutineScope = rememberCoroutineScope() + // Compute cache size each time the clear cache action value is changed + + fun handleEvent(event: AppDeveloperSettingsEvent) { + when (event) { + is AppDeveloperSettingsEvent.UpdateEnabledFeature -> coroutineScope.updateEnabledFeature( + enabledFeatures = enabledFeatures, + featureKey = event.feature.key, + enabled = event.isEnabled, + ) + is AppDeveloperSettingsEvent.SetCustomElementCallBaseUrl -> coroutineScope.launch { + val urlToSave = event.baseUrl.takeIf { !it.isNullOrEmpty() } + appPreferencesStore.setCustomElementCallBaseUrl(urlToSave) + } + is AppDeveloperSettingsEvent.SetTracingLogLevel -> coroutineScope.launch { + appPreferencesStore.setTracingLogLevel(event.logLevel.toLogLevel()) + } + is AppDeveloperSettingsEvent.ToggleTracingLogPack -> coroutineScope.launch { + val currentPacks = tracingLogPacks.toMutableSet() + if (currentPacks.contains(event.logPack)) { + currentPacks.remove(event.logPack) + } else { + currentPacks.add(event.logPack) + } + appPreferencesStore.setTracingLogPacks(currentPacks) + } + } + } + + return AppDeveloperSettingsState( + features = featureUiModels, + rageshakeState = rageshakeState, + customElementCallBaseUrlState = CustomElementCallBaseUrlState( + baseUrl = customElementCallBaseUrl, + validator = ::customElementCallUrlValidator, + ), + tracingLogLevel = tracingLogLevel, + tracingLogPacks = tracingLogPacks, + eventSink = ::handleEvent, + ) + } + + @Composable + private fun createUiModels( + enabledFeatures: SnapshotStateList, + ): ImmutableList { + return enabledFeatures.map { enabledFeature -> + key(enabledFeature.feature.key) { + remember(enabledFeature) { + FeatureUiModel( + key = enabledFeature.feature.key, + title = enabledFeature.feature.title, + description = enabledFeature.feature.description, + icon = null, + isEnabled = enabledFeature.isEnabled + ) + } + } + }.toImmutableList() + } + + private fun CoroutineScope.updateEnabledFeature( + enabledFeatures: SnapshotStateList, + featureKey: String, + enabled: Boolean, + ) = launch { + val featureIndex = enabledFeatures.indexOfFirst { it.feature.key == featureKey }.takeIf { it != -1 } ?: return@launch + val feature = enabledFeatures[featureIndex].feature + if (featureFlagService.setFeatureEnabled(feature, enabled)) { + enabledFeatures[featureIndex] = enabledFeatures[featureIndex].copy(isEnabled = enabled) + } + } +} + +private fun customElementCallUrlValidator(url: String?): Boolean { + return runCatchingExceptions { + if (url.isNullOrEmpty()) return@runCatchingExceptions + val parsedUrl = URL(url) + if (parsedUrl.protocol !in listOf("http", "https")) error("Incorrect protocol") + if (parsedUrl.host.isNullOrBlank()) error("Missing host") + }.isSuccess +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsState.kt new file mode 100644 index 0000000000..1eb5fd7fd3 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsState.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026 Element Creations 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.preferences.impl.developer.appsettings + +import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem +import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.featureflag.ui.model.FeatureUiModel +import io.element.android.libraries.matrix.api.tracing.TraceLogPack +import kotlinx.collections.immutable.ImmutableList + +data class AppDeveloperSettingsState( + val features: ImmutableList, + val rageshakeState: RageshakePreferencesState, + val customElementCallBaseUrlState: CustomElementCallBaseUrlState, + val tracingLogLevel: AsyncData, + val tracingLogPacks: ImmutableList, + val eventSink: (AppDeveloperSettingsEvent) -> Unit +) + +data class CustomElementCallBaseUrlState( + val baseUrl: String?, + val validator: (String?) -> Boolean, +) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsStateProvider.kt new file mode 100644 index 0000000000..494b3b6bbd --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsStateProvider.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2026 Element Creations 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.preferences.impl.developer.appsettings + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem +import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.featureflag.ui.model.aFeatureUiModelList +import io.element.android.libraries.matrix.api.tracing.TraceLogPack +import kotlinx.collections.immutable.toImmutableList + +open class AppDeveloperSettingsStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + anAppDeveloperSettingsState(), + anAppDeveloperSettingsState( + customElementCallBaseUrlState = aCustomElementCallBaseUrlState( + baseUrl = "https://call.element.ahoy", + ) + ), + ) +} + +fun anAppDeveloperSettingsState( + customElementCallBaseUrlState: CustomElementCallBaseUrlState = aCustomElementCallBaseUrlState(), + traceLogPacks: List = emptyList(), + eventSink: (AppDeveloperSettingsEvent) -> Unit = {}, +) = AppDeveloperSettingsState( + features = aFeatureUiModelList(), + rageshakeState = aRageshakePreferencesState(), + customElementCallBaseUrlState = customElementCallBaseUrlState, + tracingLogLevel = AsyncData.Success(LogLevelItem.INFO), + tracingLogPacks = traceLogPacks.toImmutableList(), + eventSink = eventSink, +) + +fun aCustomElementCallBaseUrlState( + baseUrl: String? = null, + validator: (String?) -> Boolean = { true }, +) = CustomElementCallBaseUrlState( + baseUrl = baseUrl, + validator = validator, +) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsView.kt new file mode 100644 index 0000000000..71051cf829 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsView.kt @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2026 Element Creations 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.preferences.impl.developer.appsettings + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +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.preferences.impl.R +import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem +import io.element.android.features.rageshake.api.preferences.RageshakePreferencesView +import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory +import io.element.android.libraries.designsystem.components.preferences.PreferenceDropdown +import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch +import io.element.android.libraries.designsystem.components.preferences.PreferenceTextField +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.ListItem +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.featureflag.ui.FeatureListView +import io.element.android.libraries.featureflag.ui.model.FeatureUiModel +import io.element.android.libraries.matrix.api.tracing.TraceLogPack +import kotlinx.collections.immutable.toImmutableList + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun AppDeveloperSettingsView( + state: AppDeveloperSettingsState, + onOpenShowkase: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.fillMaxWidth(), + ) { + // Note: this is OK to hardcode strings in this debug screen. + PreferenceCategory( + title = "Feature flags", + showTopDivider = false, + ) { + FeatureListContent(state) + } + ElementCallCategory(state = state) + PreferenceCategory(title = "Rust SDK") { + PreferenceDropdown( + title = "Tracing log level", + supportingText = "Requires app reboot", + selectedOption = state.tracingLogLevel.dataOrNull(), + options = LogLevelItem.entries.toImmutableList(), + onSelectOption = { logLevel -> + state.eventSink(AppDeveloperSettingsEvent.SetTracingLogLevel(logLevel)) + } + ) + } + PreferenceCategory(title = "Enable trace logs per SDK feature") { + Text( + text = "Requires app reboot", + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textSecondary, + modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp) + ) + for (logPack in TraceLogPack.entries) { + PreferenceSwitch( + title = logPack.title, + isChecked = state.tracingLogPacks.contains(logPack), + onCheckedChange = { isChecked -> state.eventSink(AppDeveloperSettingsEvent.ToggleTracingLogPack(logPack, isChecked)) } + ) + } + } + PreferenceCategory(title = "Showkase") { + ListItem( + headlineContent = { + Text("Open Showkase browser") + }, + onClick = onOpenShowkase + ) + } + PreferenceCategory(title = "Crash") { + ListItem( + headlineContent = { + Text("Crash the app 💥") + }, + onClick = { error("This crash is a test.") } + ) + } + RageshakePreferencesView( + state = state.rageshakeState, + ) + PreferenceCategory(title = "Crash") { + ListItem( + headlineContent = { + Text("Crash the app 💥") + }, + onClick = { error("This crash is a test.") } + ) + } + } +} + +@Composable +private fun ElementCallCategory( + state: AppDeveloperSettingsState, +) { + PreferenceCategory(title = "Element Call") { + val callUrlState = state.customElementCallBaseUrlState + + val supportingText = if (callUrlState.baseUrl.isNullOrEmpty()) { + stringResource(R.string.screen_advanced_settings_element_call_base_url_description) + } else { + callUrlState.baseUrl + } + PreferenceTextField( + headline = stringResource(R.string.screen_advanced_settings_element_call_base_url), + value = callUrlState.baseUrl, + placeholder = "https://.../room", + supportingText = supportingText, + validation = callUrlState.validator, + onValidationErrorMessage = stringResource(R.string.screen_advanced_settings_element_call_base_url_validation_error), + displayValue = { value -> !value.isNullOrEmpty() }, + keyboardOptions = KeyboardOptions.Default.copy(autoCorrectEnabled = false, keyboardType = KeyboardType.Uri), + onChange = { state.eventSink(AppDeveloperSettingsEvent.SetCustomElementCallBaseUrl(it)) } + ) + } +} + +@Composable +private fun FeatureListContent( + state: AppDeveloperSettingsState, +) { + fun onFeatureEnabled(feature: FeatureUiModel, isEnabled: Boolean) { + state.eventSink(AppDeveloperSettingsEvent.UpdateEnabledFeature(feature, isEnabled)) + } + + FeatureListView( + features = state.features, + onCheckedChange = ::onFeatureEnabled, + ) +} + +@PreviewsDayNight +@Composable +internal fun AppDeveloperSettingsViewPreview( + @PreviewParameter(AppDeveloperSettingsStateProvider::class) state: AppDeveloperSettingsState +) = ElementPreview { + AppDeveloperSettingsView( + state = state, + onOpenShowkase = {}, + ) +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/di/DeveloperSettingsModule.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/di/DeveloperSettingsModule.kt new file mode 100644 index 0000000000..bad0ccae0f --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/di/DeveloperSettingsModule.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2026 Element Creations 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.preferences.impl.developer.di + +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.Binds +import dev.zacsweers.metro.ContributesTo +import io.element.android.features.preferences.impl.developer.appsettings.AppDeveloperSettingsPresenter +import io.element.android.features.preferences.impl.developer.appsettings.AppDeveloperSettingsState +import io.element.android.libraries.architecture.Presenter + +@ContributesTo(AppScope::class) +@BindingContainer +interface DeveloperSettingsModule { + @Binds + fun bindAppDeveloperSettingsPresenter(presenter: AppDeveloperSettingsPresenter): Presenter +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt index bddae2fffb..1ab69f6007 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt @@ -15,6 +15,7 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable @@ -103,6 +104,14 @@ class EditUserProfilePresenter( } } + val homeserverCapabilities = matrixClient.homeserverCapabilities() + val canChangeDisplayName = produceState(true) { + value = homeserverCapabilities.canChangeDisplayName().getOrDefault(true) + } + val canChangeAvatar = produceState(true) { + value = homeserverCapabilities.canChangeAvatarUrl().getOrDefault(true) + } + val saveAction: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } val localCoroutineScope = rememberCoroutineScope() @@ -169,6 +178,8 @@ class EditUserProfilePresenter( saveButtonEnabled = canSave && saveAction.value !is AsyncAction.Loading, saveAction = saveAction.value, cameraPermissionState = cameraPermissionState, + canChangeDisplayName = canChangeDisplayName.value, + canChangeAvatarUrl = canChangeAvatar.value, eventSink = ::handleEvent, ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileState.kt index a638ed8378..a40f1710e2 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileState.kt @@ -22,5 +22,7 @@ data class EditUserProfileState( val saveButtonEnabled: Boolean, val saveAction: AsyncAction, val cameraPermissionState: PermissionsState, + val canChangeDisplayName: Boolean, + val canChangeAvatarUrl: Boolean, val eventSink: (EditUserProfileEvent) -> Unit ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileStateProvider.kt index ca9571aea5..13f69a7e1e 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileStateProvider.kt @@ -22,6 +22,7 @@ open class EditUserProfileStateProvider : PreviewParameterProvider = AsyncAction.Uninitialized, cameraPermissionState: PermissionsState = aPermissionsState(showDialog = false), + canChangeDisplayName: Boolean = true, + canChangeAvatarUrl: Boolean = true, eventSink: (EditUserProfileEvent) -> Unit = {}, ) = EditUserProfileState( userId = userId, @@ -42,5 +45,7 @@ fun aEditUserProfileState( saveButtonEnabled = saveButtonEnabled, saveAction = saveAction, cameraPermissionState = cameraPermissionState, + canChangeDisplayName = canChangeDisplayName, + canChangeAvatarUrl = canChangeAvatarUrl, eventSink = eventSink, ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt index 774dcedae0..d4571d7be5 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt @@ -120,6 +120,7 @@ fun EditUserProfileView( state = avatarPickerState, onClick = ::onAvatarClick, modifier = Modifier.align(Alignment.CenterHorizontally), + enabled = state.canChangeAvatarUrl, ) Spacer(modifier = Modifier.height(16.dp)) Text( @@ -134,6 +135,7 @@ fun EditUserProfileView( value = state.displayName, placeholder = stringResource(CommonStrings.common_room_name_placeholder), singleLine = true, + enabled = state.canChangeDisplayName, onValueChange = { state.eventSink(EditUserProfileEvent.UpdateDisplayName(it)) }, ) } diff --git a/features/preferences/impl/src/main/res/values-ja/translations.xml b/features/preferences/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..9cd0dfea96 --- /dev/null +++ b/features/preferences/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,82 @@ + + + "重要な電話を確実に受け取るため、端末がロックされている状態での全画面通知を、設定から許可してください。" + "通話品質を高める" + "通知の受信方法を選択してください" + "開発者モード" + "開発者向けの機能を表示します。" + "任意の Element Call のベースURL" + "任意の Element Call のベースURLを入力してください。" + "無効なURLです。プロトコル (http/https) が明記されていることと、アドレスが正しいことを確認してください。" + "ルームへの招待リクエストにアバターを表示しない" + "タイムラインにメディアのプレビューを表示しない" + "ラボ" + "写真や動画を高速で送信してデータ使用量を減らす" + "メディアの品質を最適化" + "制限と安全" + "自動的に画像を最適化してアップロード時間とファイルサイズを削減します。" + "画像のアップロード画質を最適化" + "%1$s 変更するにはタップしてください。" + "高画質 (1080p)" + "低画質 (480p)" + "標準 (720p)" + "動画のアップロード品質" + "プッシュ通知プロバイダー" + "リッチテキスト編集機能を無効化し、Markdown記法を手入力できるようにします。" + "既読を通知" + "機能がオフの場合、メッセージを確認したことを誰にも通知しません。他のユーザーの既読は確認することができます。" + "在席を共有" + "機能がオフの場合、既読の情報と入力中の通知を使用できなくなります。" + "常に非表示" + "常に表示" + "非公開ルームのみ" + "非表示のメディアはタップして表示することができます。" + "タイムラインにメディアを表示" + "タイムラインでメッセージのソースを表示する機能を追加します。" + "ブロックしたユーザーはいません" + "ブロックを解除" + "すべてのメッセージが再表示されます。" + "ユーザーのブロックを解除" + "ブロック解除中…" + "表示名" + "あなたの表示名" + "不明な問題が発生したため、情報の更新に失敗しました。" + "プロフィールを更新できません" + "プロフィールを編集" + "プロフィールを更新中…" + "スレッドへの返信を有効化" + "変更を適用するためにアプリケーションは再起動します。" + "開発段階の最新機能を試します。未完成のため変更や不安定な挙動を生じる可能性があります。" + "探究したいですか?" + "ラボ" + "追加設定" + "音声・ビデオ通話" + "設定の不一致" + "通知設定を簡素化し、見つけやすくしました。以前のカスタム設定の一部はここに表示されませんが、引き続き有効です。 + +続行すると、一部の設定が変更される可能性があります。" + "ダイレクトチャット" + "チャットごとのカスタム設定" + "通知設定の更新中に問題が発生しました。" + "すべてのメッセージ" + "メンションとキーワードのみ" + "ダイレクトチャットで以下の通知を受け取る" + "グループチャットでは以下の通知を受け取る" + "この端末で通知を受け取る" + "設定が修正されていません。再試行してください。" + "グループチャット" + "招待" + "暗号化されたルームでは、この機能にホームサーバーが対応しないため、一部のルームから通知が届かない可能性があります。" + "メンション" + "すべて" + "メンション" + "以下を通知する" + @ルームで通知を受け取る + "通知を受け取るには、%1$s を変更してください。" + "システム設定" + "システムで通知がオフです" + "通知" + "プッシュ履歴" + "トラブルシューティング" + "通知のトラブルシューティング" + diff --git a/features/preferences/impl/src/main/res/values-ru/translations.xml b/features/preferences/impl/src/main/res/values-ru/translations.xml index bf9d268b45..77be6fd8db 100644 --- a/features/preferences/impl/src/main/res/values-ru/translations.xml +++ b/features/preferences/impl/src/main/res/values-ru/translations.xml @@ -25,7 +25,7 @@ "Отключить редактор форматированного текста и включить Markdown." "Уведомления о прочтении" "Если этот параметр выключен, другие пользователи не будут видеть, прочитали ли вы сообщения. Вы по-прежнему будете видеть статус прочтения других пользователей." - "Поделиться присутствием" + "Делиться присутствием" "Если выключено, вы не будете видеть, кто печатает и читает сообщения, а также другие пользователи не будут знать, когда вы печатаете или читаете сообщения." "Всегда скрывать" "Всегда показывать" @@ -44,7 +44,7 @@ "Невозможно обновить профиль" "Редактировать профиль" "Обновление профиля…" - "Включить ответы в ветке" + "Включить ответы в обсуждениях" "Приложение перезапустится, чтобы применить это изменение." "Попробуйте функции в разработке. Эти функции ещё не завершены, они нестабильны и могут измениться." "Хотите попробовать?" diff --git a/features/preferences/impl/src/main/res/values-vi/translations.xml b/features/preferences/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..db242130ed --- /dev/null +++ b/features/preferences/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,61 @@ + + + "Chọn cách nhận thông báo" + "Chế độ nhà phát triển" + "Cho phép truy cập vào các tính năng và chức năng dành cho nhà phát triển." + "Địa chỉ tùy chỉnh máy chủ cuộc gọi Element" + "Đặt địa chỉ máy chủ cuộc gọi tùy chỉnh cho Element." + "Địa chỉ URL không hợp lệ. Hãy kiểm tra lại giao thức (http/https) và địa chỉ chính xác." + "Labs" + "Tắt trình soạn thảo văn bản nâng cao để nhập Markdown thủ công." + "Thông báo đã đọc" + "Nếu tắt, thông báo đã đọc của bạn sẽ không được gửi cho ai. Bạn vẫn sẽ nhận được thông báo đã đọc từ người khác." + "Chia sẻ trạng thái" + "Nếu tắt, bạn sẽ không thể gửi hoặc nhận thông báo đã đọc và thông báo \"đang nhập…\"." + "Bật tùy chọn xem nguồn tin nhắn trong dòng thời gian." + "Bạn chưa chặn ai cả" + "Bỏ chặn" + "Bạn sẽ có thể xem lại tất cả tin nhắn từ họ." + "Bỏ chặn người dùng" + "Đang mở khóa…" + "Tên hiển thị" + "Tên hiển thị của bạn" + "Có lỗi không xác định, thông tin không được cập nhật." + "Không thể cập nhật hồ sơ" + "Chỉnh sửa hồ sơ" + "Đang cập nhật hồ sơ…" + "Cho phép trả lời theo chủ đề" + "Ứng dụng sẽ khởi động lại để áp dụng thay đổi này." + "Hãy thử các ý tưởng mới nhất đang được phát triển. Các tính năng này chưa hoàn thiện; có thể không ổn định và có thể thay đổi." + "Bạn muốn thử tính năng thử nghiệm?" + "Labs" + "Cài đặt bổ sung" + "Cuộc gọi âm thanh và video" + "Cấu hình không khớp" + "Chúng tôi đã đơn giản hóa Cài đặt Thông báo để các tùy chọn dễ tìm hơn. Một số cài đặt tùy chỉnh bạn đã chọn trước đây không hiển thị ở đây, nhưng vẫn đang hoạt động. + +Nếu bạn tiếp tục, một số cài đặt của bạn có thể thay đổi." + "Trò chuyện trực tiếp" + "Cài đặt tùy chỉnh cho từng cuộc trò chuyện" + "Đã xảy ra lỗi khi cập nhật cài đặt thông báo." + "Tất cả tin nhắn." + "Chỉ đề cập và từ khóa" + "Trong chat riêng, nhắc tôi khi" + "Trong chat nhóm, nhắc tôi khi" + "Bật thông báo trên thiết bị này" + "Cấu hình chưa đúng, hãy thử lại." + "Trò chuyện nhóm" + "Lời mời" + "Máy chủ không hỗ trợ tùy chọn này trong phòng mã hóa, một số phòng có thể không thông báo." + "Nhắc đến" + "Tất cả" + "Nhắc đến" + "Thông báo cho tôi khi" + "Thông báo cho tôi khi @room" + "Để nhận thông báo, vui lòng thay đổi %1$s của bạn." + "cài đặt hệ thống" + "Thông báo hệ thống đã tắt" + "Thông báo" + "Khắc phục sự cố" + "Khắc phục sự cố thông báo" + diff --git a/features/preferences/impl/src/main/res/values-zh/translations.xml b/features/preferences/impl/src/main/res/values-zh/translations.xml index 0c576b58b6..0dac7ab6d8 100644 --- a/features/preferences/impl/src/main/res/values-zh/translations.xml +++ b/features/preferences/impl/src/main/res/values-zh/translations.xml @@ -6,7 +6,7 @@ "开发者模式" "允许开发人员访问特性和功能。" "自定义 Element Call URL" - "为 Element 通话设置根 URL。" + "为 Element 通话设置基本 URL。" "URL 无效,请确保包含协议(http/https)和正确的地址。" "在房间邀请请求中隐藏头像" "在时间轴中隐藏媒体预览" @@ -34,12 +34,12 @@ "在时间轴中显示媒体" "启用在时间轴中查看消息源码的选项。" "您没有屏蔽用户" - "解封" + "解除屏蔽" "可以重新接收他们的消息。" - "解封用户" + "解除屏蔽用户" "正在解除屏蔽……" "显示名称" - "你的显示名称" + "您的显示名称" "遇到未知错误,无法更改信息。" "无法更新个人资料" "编辑个人资料" diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt index 1fcf9bff70..ec70b19eab 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt @@ -14,27 +14,18 @@ import androidx.compose.ui.graphics.Color import com.google.common.truth.Truth.assertThat import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.test.FakeEnterpriseService -import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem +import io.element.android.features.preferences.impl.developer.appsettings.anAppDeveloperSettingsState import io.element.android.features.preferences.impl.tasks.FakeClearCacheUseCase import io.element.android.features.preferences.impl.tasks.FakeComputeCacheSizeUseCase import io.element.android.features.preferences.impl.tasks.VacuumStoresUseCase -import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.core.data.megaBytes -import io.element.android.libraries.core.meta.BuildMeta -import io.element.android.libraries.core.meta.BuildType -import io.element.android.libraries.featureflag.api.Feature -import io.element.android.libraries.featureflag.api.FeatureFlags -import io.element.android.libraries.featureflag.test.FakeFeature -import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.analytics.GetDatabaseSizesUseCase import io.element.android.libraries.matrix.api.analytics.SdkStoreSizes import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.test.A_SESSION_ID -import io.element.android.libraries.matrix.test.core.aBuildMeta -import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value @@ -51,17 +42,7 @@ class DeveloperSettingsPresenterTest { @Test fun `present - ensures initial states are correct`() = runTest { - val getAvailableFeaturesResult = lambdaRecorder> { _, _ -> - listOf( - FakeFeature( - key = "feature_1", - title = "Feature 1", - isInLabs = false, - ) - ) - } val presenter = createDeveloperSettingsPresenter( - featureFlagService = FakeFeatureFlagService(getAvailableFeaturesResult = getAvailableFeaturesResult), databaseSizesUseCase = GetDatabaseSizesUseCase { Result.success( SdkStoreSizes(stateStore = 10.megaBytes, eventCacheStore = 10.megaBytes, mediaStore = 10.megaBytes, cryptoStore = 10.megaBytes) @@ -70,22 +51,14 @@ class DeveloperSettingsPresenterTest { ) presenter.test { awaitItem().also { state -> - assertThat(state.features).isEmpty() + assertThat(state.appDeveloperSettingsState.features).isNotEmpty() assertThat(state.clearCacheAction).isEqualTo(AsyncAction.Uninitialized) assertThat(state.cacheSize).isEqualTo(AsyncData.Uninitialized) - assertThat(state.customElementCallBaseUrlState).isNotNull() - assertThat(state.customElementCallBaseUrlState.baseUrl).isNull() - assertThat(state.rageshakeState.isEnabled).isFalse() - assertThat(state.rageshakeState.isSupported).isTrue() - assertThat(state.rageshakeState.sensitivity).isEqualTo(0.3f) - assertThat(state.tracingLogLevel).isEqualTo(AsyncData.Uninitialized) assertThat(state.isEnterpriseBuild).isFalse() assertThat(state.showColorPicker).isFalse() } awaitItem().also { state -> - assertThat(state.features).isNotEmpty() - assertThat(state.features).hasSize(1) - assertThat(state.tracingLogLevel.dataOrNull()).isEqualTo(LogLevelItem.INFO) + assertThat(state.cacheSize.isLoading()).isTrue() } awaitItem().also { state -> assertThat(state.cacheSize).isInstanceOf(AsyncData.Success::class.java) @@ -98,37 +71,6 @@ class DeveloperSettingsPresenterTest { ) ) } - getAvailableFeaturesResult.assertions().isCalledOnce() - .with(value(false), value(false)) - } - } - - @Test - fun `present - ensures Room directory search is not present on release Google Play builds`() = runTest { - val buildMeta = aBuildMeta(buildType = BuildType.RELEASE, flavorDescription = "GooglePlay") - val presenter = createDeveloperSettingsPresenter(buildMeta = buildMeta) - presenter.test { - skipItems(2) - awaitItem().also { state -> - assertThat(state.features).doesNotContain(FeatureFlags.RoomDirectorySearch) - } - } - } - - @Test - fun `present - ensures state is updated when enabled feature event is triggered`() = runTest { - val presenter = createDeveloperSettingsPresenter() - presenter.test { - skipItems(2) - awaitItem().also { state -> - val feature = state.features.first { !it.isEnabled } - state.eventSink(DeveloperSettingsEvents.UpdateEnabledFeature(feature, !feature.isEnabled)) - } - awaitItem().also { state -> - val feature = state.features.first() - assertThat(feature.isEnabled).isTrue() - assertThat(feature.key).isEqualTo(feature.key) - } } } @@ -158,52 +100,6 @@ class DeveloperSettingsPresenterTest { } } - @Test - fun `present - custom element call base url`() = runTest { - val preferencesStore = InMemoryAppPreferencesStore() - val presenter = createDeveloperSettingsPresenter(preferencesStore = preferencesStore) - presenter.test { - skipItems(2) - awaitItem().also { state -> - assertThat(state.customElementCallBaseUrlState.baseUrl).isNull() - state.eventSink(DeveloperSettingsEvents.SetCustomElementCallBaseUrl("https://call.element.ahoy")) - } - awaitItem().also { state -> - assertThat(state.customElementCallBaseUrlState.baseUrl).isEqualTo("https://call.element.ahoy") - } - } - } - - @Test - fun `present - custom element call base url validator needs at least an HTTP scheme and host`() = runTest { - val presenter = createDeveloperSettingsPresenter() - presenter.test { - skipItems(2) - val urlValidator = awaitItem().customElementCallBaseUrlState.validator - assertThat(urlValidator("")).isTrue() // We allow empty string to clear the value and use the default one - assertThat(urlValidator("test")).isFalse() - assertThat(urlValidator("http://")).isFalse() - assertThat(urlValidator("geo://test")).isFalse() - assertThat(urlValidator("https://call.element.io")).isTrue() - } - } - - @Test - fun `present - changing tracing log level`() = runTest { - val preferences = InMemoryAppPreferencesStore() - val presenter = createDeveloperSettingsPresenter(preferencesStore = preferences) - presenter.test { - skipItems(2) - awaitItem().also { state -> - assertThat(state.tracingLogLevel.dataOrNull()).isEqualTo(LogLevelItem.INFO) - state.eventSink(DeveloperSettingsEvents.SetTracingLogLevel(LogLevelItem.TRACE)) - } - awaitItem().also { state -> - assertThat(state.tracingLogLevel.dataOrNull()).isEqualTo(LogLevelItem.TRACE) - } - } - } - @Test fun `present - enterprise build can change the brand color`() = runTest { val overrideBrandColorResult = lambdaRecorder { _, _ -> } @@ -250,33 +146,17 @@ class DeveloperSettingsPresenterTest { private fun createDeveloperSettingsPresenter( sessionId: SessionId = A_SESSION_ID, - featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService( - getAvailableFeaturesResult = { _, _ -> - listOf( - FakeFeature( - key = "feature_1", - title = "Feature 1", - isInLabs = false, - ) - ) - } - ), cacheSizeUseCase: FakeComputeCacheSizeUseCase = FakeComputeCacheSizeUseCase(), clearCacheUseCase: FakeClearCacheUseCase = FakeClearCacheUseCase(), - preferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(), - buildMeta: BuildMeta = aBuildMeta(), enterpriseService: EnterpriseService = FakeEnterpriseService(), vacuumStoresUseCase: VacuumStoresUseCase = VacuumStoresUseCase {}, databaseSizesUseCase: GetDatabaseSizesUseCase = GetDatabaseSizesUseCase { Result.success(SdkStoreSizes(null, null, null, null)) }, ): DeveloperSettingsPresenter { return DeveloperSettingsPresenter( + appDeveloperSettingsPresenter = { anAppDeveloperSettingsState() }, sessionId = sessionId, - featureFlagService = featureFlagService, computeCacheSizeUseCase = cacheSizeUseCase, clearCacheUseCase = clearCacheUseCase, - rageshakePresenter = { aRageshakePreferencesState() }, - appPreferencesStore = preferencesStore, - buildMeta = buildMeta, enterpriseService = enterpriseService, vacuumStoresUseCase = vacuumStoresUseCase, databaseSizesUseCase = databaseSizesUseCase, diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsViewTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsViewTest.kt index 3854e3f4a1..d4d02d7de9 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsViewTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsViewTest.kt @@ -9,20 +9,12 @@ package io.element.android.features.preferences.impl.developer import androidx.activity.ComponentActivity -import androidx.compose.ui.test.filterToOne -import androidx.compose.ui.test.hasAnyAncestor -import androidx.compose.ui.test.isDialog -import androidx.compose.ui.test.isEditable -import androidx.compose.ui.test.isFocusable import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performTextInput import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.preferences.impl.R -import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem -import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn @@ -53,7 +45,7 @@ class DeveloperSettingsViewTest { } } - @Config(qualifiers = "h1500dp") + @Config(qualifiers = "h2000dp") @Test fun `clicking on push history notification invokes the expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) @@ -68,22 +60,6 @@ class DeveloperSettingsViewTest { } } - @Config(qualifiers = "h1500dp") - @Test - fun `clicking on element call url open the dialogs and submit emits the expected event`() { - val eventsRecorder = EventsRecorder() - rule.setDeveloperSettingsView( - state = aDeveloperSettingsState( - eventSink = eventsRecorder - ), - ) - rule.clickOn(R.string.screen_advanced_settings_element_call_base_url) - val textInputNode = rule.onAllNodes(isEditable().and(isFocusable())).filterToOne(hasAnyAncestor(isDialog())) - textInputNode.performTextInput("https://call.element.dev") - rule.clickOn(CommonStrings.action_ok) - eventsRecorder.assertSingle(DeveloperSettingsEvents.SetCustomElementCallBaseUrl("https://call.element.dev")) - } - @Config(qualifiers = "h2000dp") @Test fun `clicking on open showkase invokes the expected callback`() { @@ -99,20 +75,6 @@ class DeveloperSettingsViewTest { } } - @Config(qualifiers = "h1024dp") - @Test - fun `clicking on log level emits the expected event`() { - val eventsRecorder = EventsRecorder() - rule.setDeveloperSettingsView( - state = aDeveloperSettingsState( - eventSink = eventsRecorder - ), - ) - rule.onNodeWithText("Tracing log level").performClick() - rule.onNodeWithText("Debug").performClick() - eventsRecorder.assertSingle(DeveloperSettingsEvents.SetTracingLogLevel(LogLevelItem.DEBUG)) - } - @Config(qualifiers = "h2200dp") @Test fun `clicking on clear cache emits the expected event`() { diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPageTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPageTest.kt new file mode 100644 index 0000000000..123f31ae8e --- /dev/null +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPageTest.kt @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2026 Element Creations 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.preferences.impl.developer.appsettings + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.filterToOne +import androidx.compose.ui.test.hasAnyAncestor +import androidx.compose.ui.test.isDialog +import androidx.compose.ui.test.isEditable +import androidx.compose.ui.test.isFocusable +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.preferences.impl.R +import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.pressBack +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith +import org.robolectric.annotation.Config + +@RunWith(AndroidJUnit4::class) +class AppDeveloperSettingsPageTest { + @get:Rule + val rule = createAndroidComposeRule() + + @Test + fun `clicking on back invokes the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { + rule.setAppDeveloperSettingsView( + state = anAppDeveloperSettingsState( + eventSink = eventsRecorder + ), + onBackClick = it + ) + rule.pressBack() + } + } + + @Config(qualifiers = "h1500dp") + @Test + fun `clicking on element call url open the dialogs and submit emits the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setAppDeveloperSettingsView( + state = anAppDeveloperSettingsState( + eventSink = eventsRecorder + ), + ) + rule.clickOn(R.string.screen_advanced_settings_element_call_base_url) + val textInputNode = rule.onAllNodes(isEditable().and(isFocusable())).filterToOne(hasAnyAncestor(isDialog())) + textInputNode.performTextInput("https://call.element.dev") + rule.clickOn(CommonStrings.action_ok) + eventsRecorder.assertSingle(AppDeveloperSettingsEvent.SetCustomElementCallBaseUrl("https://call.element.dev")) + } + + @Config(qualifiers = "h2000dp") + @Test + fun `clicking on open showkase invokes the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { + rule.setAppDeveloperSettingsView( + state = anAppDeveloperSettingsState( + eventSink = eventsRecorder + ), + onOpenShowkase = it + ) + rule.onNodeWithText("Open Showkase browser").performClick() + } + } + + @Config(qualifiers = "h1024dp") + @Test + fun `clicking on log level emits the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setAppDeveloperSettingsView( + state = anAppDeveloperSettingsState( + eventSink = eventsRecorder + ), + ) + rule.onNodeWithText("Tracing log level").performClick() + rule.onNodeWithText("Debug").performClick() + eventsRecorder.assertSingle(AppDeveloperSettingsEvent.SetTracingLogLevel(LogLevelItem.DEBUG)) + } +} + +private fun AndroidComposeTestRule.setAppDeveloperSettingsView( + state: AppDeveloperSettingsState, + onOpenShowkase: () -> Unit = EnsureNeverCalled(), + onBackClick: () -> Unit = EnsureNeverCalled(), +) { + setContent { + AppDeveloperSettingsPage( + state = state, + onOpenShowkase = onOpenShowkase, + onBackClick = onBackClick, + ) + } +} diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPresenterTest.kt new file mode 100644 index 0000000000..0e9d774e84 --- /dev/null +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPresenterTest.kt @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.preferences.impl.developer.appsettings + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem +import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.core.meta.BuildType +import io.element.android.libraries.featureflag.api.Feature +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeature +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore +import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import io.element.android.tests.testutils.test +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class AppDeveloperSettingsPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - ensures initial states are correct`() = runTest { + val getAvailableFeaturesResult = lambdaRecorder> { _, _ -> + listOf( + FakeFeature( + key = "feature_1", + title = "Feature 1", + isInLabs = false, + ) + ) + } + val presenter = createAppDeveloperSettingsPresenter( + featureFlagService = FakeFeatureFlagService(getAvailableFeaturesResult = getAvailableFeaturesResult), + ) + presenter.test { + awaitItem().also { state -> + assertThat(state.features).isEmpty() + assertThat(state.customElementCallBaseUrlState).isNotNull() + assertThat(state.customElementCallBaseUrlState.baseUrl).isNull() + assertThat(state.rageshakeState.isEnabled).isFalse() + assertThat(state.rageshakeState.isSupported).isTrue() + assertThat(state.rageshakeState.sensitivity).isEqualTo(0.3f) + assertThat(state.tracingLogLevel).isEqualTo(AsyncData.Uninitialized) + } + awaitItem().also { state -> + assertThat(state.features).isNotEmpty() + assertThat(state.features).hasSize(1) + assertThat(state.tracingLogLevel.dataOrNull()).isEqualTo(LogLevelItem.INFO) + } + getAvailableFeaturesResult.assertions().isCalledOnce() + .with(value(false), value(false)) + } + } + + @Test + fun `present - ensures Room directory search is not present on release Google Play builds`() = runTest { + val buildMeta = aBuildMeta(buildType = BuildType.RELEASE, flavorDescription = "GooglePlay") + val presenter = createAppDeveloperSettingsPresenter(buildMeta = buildMeta) + presenter.test { + skipItems(1) + awaitItem().also { state -> + assertThat(state.features).doesNotContain(FeatureFlags.RoomDirectorySearch) + } + } + } + + @Test + fun `present - ensures state is updated when enabled feature event is triggered`() = runTest { + val presenter = createAppDeveloperSettingsPresenter() + presenter.test { + skipItems(1) + awaitItem().also { state -> + val feature = state.features.first { !it.isEnabled } + state.eventSink(AppDeveloperSettingsEvent.UpdateEnabledFeature(feature, !feature.isEnabled)) + } + awaitItem().also { state -> + val feature = state.features.first() + assertThat(feature.isEnabled).isTrue() + assertThat(feature.key).isEqualTo(feature.key) + } + } + } + + @Test + fun `present - custom element call base url`() = runTest { + val preferencesStore = InMemoryAppPreferencesStore() + val presenter = createAppDeveloperSettingsPresenter(preferencesStore = preferencesStore) + presenter.test { + skipItems(1) + awaitItem().also { state -> + assertThat(state.customElementCallBaseUrlState.baseUrl).isNull() + state.eventSink(AppDeveloperSettingsEvent.SetCustomElementCallBaseUrl("https://call.element.ahoy")) + } + awaitItem().also { state -> + assertThat(state.customElementCallBaseUrlState.baseUrl).isEqualTo("https://call.element.ahoy") + } + } + } + + @Test + fun `present - custom element call base url validator needs at least an HTTP scheme and host`() = runTest { + val presenter = createAppDeveloperSettingsPresenter() + presenter.test { + skipItems(1) + val urlValidator = awaitItem().customElementCallBaseUrlState.validator + assertThat(urlValidator("")).isTrue() // We allow empty string to clear the value and use the default one + assertThat(urlValidator("test")).isFalse() + assertThat(urlValidator("http://")).isFalse() + assertThat(urlValidator("geo://test")).isFalse() + assertThat(urlValidator("https://call.element.io")).isTrue() + } + } + + @Test + fun `present - changing tracing log level`() = runTest { + val preferences = InMemoryAppPreferencesStore() + val presenter = createAppDeveloperSettingsPresenter(preferencesStore = preferences) + presenter.test { + skipItems(1) + awaitItem().also { state -> + assertThat(state.tracingLogLevel.dataOrNull()).isEqualTo(LogLevelItem.INFO) + state.eventSink(AppDeveloperSettingsEvent.SetTracingLogLevel(LogLevelItem.TRACE)) + } + awaitItem().also { state -> + assertThat(state.tracingLogLevel.dataOrNull()).isEqualTo(LogLevelItem.TRACE) + } + } + } + + private fun createAppDeveloperSettingsPresenter( + featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService( + getAvailableFeaturesResult = { _, _ -> + listOf( + FakeFeature( + key = "feature_1", + title = "Feature 1", + isInLabs = false, + ) + ) + } + ), + preferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(), + buildMeta: BuildMeta = aBuildMeta(), + ): AppDeveloperSettingsPresenter { + return AppDeveloperSettingsPresenter( + featureFlagService = featureFlagService, + rageshakePresenter = { aRageshakePreferencesState() }, + appPreferencesStore = preferencesStore, + buildMeta = buildMeta, + ) + } +} diff --git a/features/preferences/test/build.gradle.kts b/features/preferences/test/build.gradle.kts new file mode 100644 index 0000000000..7e3da4a6e8 --- /dev/null +++ b/features/preferences/test/build.gradle.kts @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2026 Element Creations 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.preferences.test" +} + +dependencies { + implementation(projects.features.preferences.api) + implementation(projects.tests.testutils) +} diff --git a/features/preferences/test/src/main/kotlin/io/element/android/features/preferences/test/FakePreferencesEntryPoint.kt b/features/preferences/test/src/main/kotlin/io/element/android/features/preferences/test/FakePreferencesEntryPoint.kt new file mode 100644 index 0000000000..c57ed434fa --- /dev/null +++ b/features/preferences/test/src/main/kotlin/io/element/android/features/preferences/test/FakePreferencesEntryPoint.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2026 Element Creations 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.preferences.test + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import io.element.android.features.preferences.api.PreferencesEntryPoint +import io.element.android.tests.testutils.lambda.lambdaError + +class FakePreferencesEntryPoint : PreferencesEntryPoint { + override fun createNode( + parentNode: Node, + buildContext: BuildContext, + params: PreferencesEntryPoint.Params, + callback: PreferencesEntryPoint.Callback, + ): Node { + lambdaError() + } + + override fun createAppDeveloperSettingsNode( + parentNode: Node, + buildContext: BuildContext, + callback: PreferencesEntryPoint.DeveloperSettingsCallback, + ): Node { + lambdaError() + } +} diff --git a/features/rageshake/api/src/main/res/values-ja/translations.xml b/features/rageshake/api/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..7afb523cd1 --- /dev/null +++ b/features/rageshake/api/src/main/res/values-ja/translations.xml @@ -0,0 +1,7 @@ + + + "%1$s は前回の利用時にクラッシュしました。クラッシュレポートを開発者に共有しますか?" + "怒り狂って端末を振っていますね。バグの報告をしますか?" + "怒り狂う" + "検出感度" + diff --git a/features/rageshake/api/src/main/res/values-lt/translations.xml b/features/rageshake/api/src/main/res/values-lt/translations.xml index 49762d57ca..88c49aca3c 100644 --- a/features/rageshake/api/src/main/res/values-lt/translations.xml +++ b/features/rageshake/api/src/main/res/values-lt/translations.xml @@ -2,6 +2,6 @@ "%1$s nulūžo paskutinį kartą, kai buvo naudojama. Ar norėtumėte su mumis pasidalyti strigčių ataskaita?" "Atrodo, kad nusivylęs purtote telefoną. Ar norėtumėte atidaryti pranešimo apie klaidas ekraną?" - "Rageshake" + "Gerai pakratyti" "Aptikimo riba" diff --git a/features/rageshake/api/src/main/res/values-vi/translations.xml b/features/rageshake/api/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..086619eed0 --- /dev/null +++ b/features/rageshake/api/src/main/res/values-vi/translations.xml @@ -0,0 +1,7 @@ + + + "%1$s đã bị lỗi ở lần sử dụng gần nhất. Bạn có muốn chia sẻ báo cáo lỗi với chúng tôi không?" + "Có vẻ như bạn đang lắc điện thoại vì bực bội. Bạn có muốn mở màn hình báo cáo lỗi không?" + "Lắc điện thoại" + "Ngưỡng phát hiện" + diff --git a/features/rageshake/impl/src/main/res/values-ja/translations.xml b/features/rageshake/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..7c46d2beb5 --- /dev/null +++ b/features/rageshake/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,20 @@ + + + "スクリーンショットを添付" + "追加の質問がある場合は、ご連絡ください。" + "返信を受け取る" + "スクリーンショットを編集" + "問題を教えてください。行った操作、想定した挙動、実際の挙動などについて、可能な限り詳細に記述してください。" + "問題の説明…" + "可能であれば英語で記入してください。" + "説明が過度に短いです。問題についてより詳細にご記入ください。" + "クラッシュログを送信" + "ログの記録を許可" + "ログのサイズが大きく、報告に添付することができません。ログは別の方法で送信してください。" + "スクリーンを送信" + "メッセージには、正常に動作していることを確認するため、ログが含まれています。ログを含めたくない場合は、オフにしてください。" + "%1$s は前回の利用時にクラッシュしました。クラッシュレポートを開発者に共有しますか?" + "通知について問題がある場合は、プッシュ通知の設定を添付することで、原因究明の手がかりになります。この設定には、ユーザーネームや通知のキーワードなどの個人情報が含まれる場合があります。ご注意ください。" + "通知設定を送信" + "ログを表示" + diff --git a/features/rageshake/impl/src/main/res/values-lt/translations.xml b/features/rageshake/impl/src/main/res/values-lt/translations.xml index c45c982ba5..cc79a5055e 100644 --- a/features/rageshake/impl/src/main/res/values-lt/translations.xml +++ b/features/rageshake/impl/src/main/res/values-lt/translations.xml @@ -10,6 +10,6 @@ "Siųsti gedimų žurnalus" "Leisti žurnalus" "Siųsti ekrano nuotrauką" - "Prie žinutės bus pridėti žurnalai, kad įsitikintumėme, jog viskas veikia tinkamai. Jei norite išsiųsti savo žinutę be žurnalų, išjunkite šį nustatymą." + "Žurnalai bus įtraukti į jūsų žinutę, kad būtų užtikrinta, jog viskas veikia tinkamai. Kad išsiųstumėte žinutę be žurnalų, išjunkite šį nustatymą." "%1$s nulūžo paskutinį kartą, kai buvo naudojama. Ar norėtumėte su mumis pasidalyti strigčių ataskaita?" diff --git a/features/rageshake/impl/src/main/res/values-vi/translations.xml b/features/rageshake/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..5a60edd206 --- /dev/null +++ b/features/rageshake/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,17 @@ + + + "Đính kèm ảnh chụp màn hình" + "Bạn có thể liên hệ với tôi nếu có bất kỳ câu hỏi nào khác." + "Liên hệ với tôi" + "Chỉnh sửa ảnh chụp màn hình" + "Hãy mô tả vấn đề. Bạn đã làm gì? Bạn mong đợi/dự đoán điều gì sẽ xảy ra? Điều gì thực sự đã xảy ra? Hãy trình bày càng chi tiết càng tốt." + "Hãy mô tả vấn đề…" + "Nếu có thể, vui lòng viết mô tả bằng tiếng Anh." + "Phần mô tả quá ngắn, vui lòng cung cấp thêm chi tiết về những gì đã xảy ra. Cảm ơn!" + "Gửi nhật ký sự cố" + "Cho phép ghi nhật ký" + "Gửi ảnh chụp màn hình" + "Nhật ký lỗi sẽ được đính kèm với tin nhắn của bạn để đảm bảo mọi thứ hoạt động bình thường. Để gửi tin nhắn mà không có nhật ký lỗi, hãy tắt cài đặt này." + "%1$s đã bị lỗi ở lần sử dụng gần nhất. Bạn có muốn chia sẻ báo cáo lỗi với chúng tôi không?" + "Xem nhật ký" + diff --git a/features/reportroom/impl/src/main/res/values-ja/translations.xml b/features/reportroom/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..9718ca432f --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,8 @@ + + + "報告は正常に送信されましたが、ルームの退出中に問題が発生しました。もう一度試してください。" + "ルームの退出に失敗" + "管理者にこのルームを報告します。メッセージが暗号化されている場合、管理者は内容を確認することができません。" + "報告の理由を説明してください…" + "ルームを通報" + diff --git a/features/reportroom/impl/src/main/res/values-vi/translations.xml b/features/reportroom/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..3037521f98 --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,5 @@ + + + "Hãy báo cáo phòng chat này cho quản trị viên của bạn. Nếu tin nhắn được mã hóa, quản trị viên sẽ không thể đọc được chúng." + "Báo cáo phòng" + diff --git a/features/rolesandpermissions/impl/src/main/res/values-ja/translations.xml b/features/rolesandpermissions/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..c9410e9e3c --- /dev/null +++ b/features/rolesandpermissions/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,82 @@ + + + "管理者" + "ユーザーの追放" + "設定の変更" + "メッセージの削除" + "メンバー" + "ユーザーの招待" + "スペースの管理" + "ルームを管理" + "メンバーの管理" + "メッセージと内容" + "モデレーター" + "ユーザーの削除" + "アバターの変更" + "詳細を編集" + "名前の変更" + "トピックの変更" + "メッセージの送信" + "権限" + "管理者を編集" + "この操作は取り消せません。このユーザーをあなたと同じ権限まで昇格します。" + "管理者を追加しますか?" + "この操作は取り消せません。選択したユーザーに所有権を譲与します。あなたがルームを退出すると恒久的に変更が適用されます。" + "所有権を譲与しますか?" + "降格" + "自身を降格しようとしているため、後から取り消すことはできません。このルームに他に特権を持つユーザーが存在しない場合、それを回復することはできなくなります。" + "自身を降格しますか?" + "%1$s (承認待ち)" + "承認待ち" + "管理者はモデレータの特権を有します。" + "所有者は管理者の特権を有します。" + "モデレーターを編集" + "所有者を選択" + "管理者" + "モデレーター" + "メンバー" + "未保存の変更内容があります。" + "変更を保存しますか?" + "追放されたユーザーはいません。" + + "%1$d 人の追放" + + "スペルを確認するか、新たに検索し直してください" + "\"%1$s\" の検索結果はありません" + + "%1$d 人" + + "ユーザーを追放" + "メンバーのみを削除" + "追放を解除" + "招待を受け取ると再度参加できます。" + "ユーザーの追放を解除" + "追放済み" + "メンバー" + + "%1$d 件の招待" + + "待機中" + "管理者" + "モデレーター" + "所有者" + "ルームのメンバー" + "%1$s の追放を解除中" + "管理者" + "管理者と所有者" + "自身の役割を変更" + "権限を譲与" + "モデレーターに譲与" + "メンバーの編集" + "メッセージと内容" + "モデレーター" + "所有者" + "権限" + "権限をリセット" + "権限をリセットすると現在の設定はすべて失われます。" + "権限をリセットしますか?" + "役割" + "ルームの詳細" + "スペースの詳細" + "役割と権限" + diff --git a/features/rolesandpermissions/impl/src/main/res/values-vi/translations.xml b/features/rolesandpermissions/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..5e6c736cbe --- /dev/null +++ b/features/rolesandpermissions/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,58 @@ + + + "Quản trị viên" + "Cấm người dùng" + "Xóa tin nhắn" + "Thành viên" + "Mời mọi người" + "Quản lý phòng trò chuyện" + "Quản lý thành viên" + "Tin nhắn và nội dung." + "Người điều hành" + "Gỡ người dùng" + "Đổi ảnh đại diện" + "Chỉnh sửa thông tin" + "Đổi tên" + "Đổi chủ đề" + "Gửi tin nhắn." + "Chỉnh sửa Quản trị viên" + "Bạn sẽ không thể hoàn tác hành động này. Bạn đang thăng quyền cho người dùng lên cùng cấp quyền với bạn." + "Thêm quản trị viên?" + "Giáng cấp" + "Bạn sẽ không thể hoàn tác thay đổi này vì bạn đang tự giáng cấp bản thân, nếu bạn là người dùng cuối cùng có đặc quyền trong phòng, nó sẽ không thể lấy lại đặc quyền." + "Giáng cấp bản thân?" + "%1$s (Đang chờ xử lý)" + "Chỉnh sửa Người điều hành" + "Quản trị viên" + "Người điều hành" + "Thành viên" + "Bạn có thay đổi chưa được lưu." + "Lưu thay đổi?" + "Hiện không có người dùng nào bị cấm." + + "%1$d người" + + "Cấm người dùng" + "Chỉ xóa thành viên" + "Bỏ cấm" + "Họ có thể tham gia lại phòng này nếu được mời." + "Bị cấm" + "Thành viên" + "Quản trị viên" + "Người điều hành" + "Thành viên phòng" + "Đang gỡ cấm %1$s" + "Quản trị viên" + "Thay đổi vai trò của tôi" + "Hạ cấp xuống thành thành viên" + "Hạ cấp xuống làm người điều hành" + "Quản lý thành viên" + "Tin nhắn và nội dung." + "Người điều hành" + "Đặt lại quyền truy cập" + "Sau khi bạn đặt lại quyền truy cập, bạn sẽ mất các cài đặt hiện tại." + "Đặt lại quyền truy cập?" + "Vai trò" + "Chi tiết phòng" + "Vai trò và quyền hạn" + diff --git a/features/rolesandpermissions/impl/src/main/res/values-zh/translations.xml b/features/rolesandpermissions/impl/src/main/res/values-zh/translations.xml index 768bed1a86..d3c4cada77 100644 --- a/features/rolesandpermissions/impl/src/main/res/values-zh/translations.xml +++ b/features/rolesandpermissions/impl/src/main/res/values-zh/translations.xml @@ -19,7 +19,7 @@ "发送消息" "权限" "编辑管理员" - "您将无法撤消此操作。您正在提升用户的权限,使其拥有与您平权。" + "您将无法撤消此操作。您正在提升用户的权限到与您相同的级别。" "添加管理员?" "此操作无法撤销。您正在将所有权转移给所选用户。一旦离开此界面,该操作将永久生效。" "转让所有权" @@ -50,7 +50,7 @@ "仅移除成员" "取消封禁" "如果受到邀请,他们可以重新加入聊天室。" - "从房间取消解封" + "解封用户" "已封禁用户" "成员" @@ -61,7 +61,7 @@ "协管员" "所有者" "聊天室成员" - "解除封禁 %1$s" + "正在解除封禁 %1$s" "管理员" "管理员和所有者" "更改我的角色" diff --git a/features/roomaliasresolver/impl/src/main/res/values-ja/translations.xml b/features/roomaliasresolver/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..4e3f765f88 --- /dev/null +++ b/features/roomaliasresolver/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,5 @@ + + + "ルームのプレビューを表示できません" + "ルームエイリアスを解決できません。" + diff --git a/features/roomaliasresolver/impl/src/main/res/values-vi/translations.xml b/features/roomaliasresolver/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..b08c97b72a --- /dev/null +++ b/features/roomaliasresolver/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,4 @@ + + + "Không thể hiển thị bản xem trước của phòng này" + diff --git a/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt b/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt index 54ffdf6c25..3d863321e3 100644 --- a/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt +++ b/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt @@ -20,6 +20,8 @@ import io.element.android.features.call.api.CurrentCallService import io.element.android.features.enterprise.api.SessionEnterpriseService import io.element.android.features.roomcall.api.RoomCallState import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.notification.CallIntent +import io.element.android.libraries.matrix.api.room.CallIntentConsensus import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.isDm import io.element.android.libraries.matrix.api.room.powerlevels.canCall @@ -57,8 +59,7 @@ class RoomCallStatePresenter( canJoinCall = canJoinCall, isUserInTheCall = isUserInTheCall, isUserLocallyInTheCall = isUserLocallyInTheCall, - // TODO resolve intent while the call is ongoing - isAudioCall = false + isAudioCall = roomInfo.activeCallIntentConsensus.isAudio(), ) else -> RoomCallState.StandBy( canStartCall = canJoinCall, @@ -70,3 +71,12 @@ class RoomCallStatePresenter( return callState } } + +fun CallIntentConsensus.isAudio(): Boolean { + val intent = when (this) { + is CallIntentConsensus.Full -> callIntent + is CallIntentConsensus.Partial -> callIntent + is CallIntentConsensus.None -> return false + } + return intent == CallIntent.AUDIO +} diff --git a/features/roomcall/impl/src/test/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenterTest.kt b/features/roomcall/impl/src/test/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenterTest.kt index 0a561ad59a..4c6fcf8e59 100644 --- a/features/roomcall/impl/src/test/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenterTest.kt +++ b/features/roomcall/impl/src/test/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenterTest.kt @@ -14,6 +14,8 @@ import io.element.android.features.call.api.CurrentCallService import io.element.android.features.call.test.FakeCurrentCallService import io.element.android.features.enterprise.test.FakeSessionEnterpriseService import io.element.android.features.roomcall.api.RoomCallState +import io.element.android.libraries.matrix.api.notification.CallIntent +import io.element.android.libraries.matrix.api.room.CallIntentConsensus import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.test.room.FakeBaseRoom @@ -188,6 +190,100 @@ class RoomCallStatePresenterTest { } } + @Test + fun `present - active call with audio Intent`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(true), + ).apply { + givenRoomInfo( + aRoomInfo( + hasRoomCall = true, + activeCallIntentConsensus = CallIntentConsensus.Full(CallIntent.AUDIO), + activeRoomCallParticipants = emptyList(), + ) + ) + } + ) + val presenter = createRoomCallStatePresenter( + joinedRoom = room, + currentCallService = FakeCurrentCallService(MutableStateFlow(CurrentCall.RoomCall(room.roomId))), + ) + presenter.test { + skipItems(1) + assertThat(awaitItem()).isEqualTo( + RoomCallState.OnGoing( + canJoinCall = true, + isAudioCall = true, + isUserInTheCall = false, + isUserLocallyInTheCall = true, + ) + ) + } + } + + @Test + fun `present - active call with partial audio Intent`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(true), + ).apply { + givenRoomInfo( + aRoomInfo( + hasRoomCall = true, + activeCallIntentConsensus = CallIntentConsensus.Partial(CallIntent.AUDIO, 1, 4), + ) + ) + } + ) + val presenter = createRoomCallStatePresenter( + joinedRoom = room, + currentCallService = FakeCurrentCallService(MutableStateFlow(CurrentCall.RoomCall(room.roomId))), + ) + presenter.test { + skipItems(1) + assertThat(awaitItem()).isEqualTo( + RoomCallState.OnGoing( + canJoinCall = true, + isAudioCall = true, + isUserInTheCall = false, + isUserLocallyInTheCall = true, + ) + ) + } + } + + @Test + fun `present - active call with no intent defaults to Audio`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(true), + ).apply { + givenRoomInfo( + aRoomInfo( + hasRoomCall = true, + activeCallIntentConsensus = CallIntentConsensus.None, + ) + ) + } + ) + val presenter = createRoomCallStatePresenter( + joinedRoom = room, + currentCallService = FakeCurrentCallService(MutableStateFlow(CurrentCall.RoomCall(room.roomId))), + ) + presenter.test { + skipItems(1) + assertThat(awaitItem()).isEqualTo( + RoomCallState.OnGoing( + canJoinCall = true, + isAudioCall = false, + isUserInTheCall = false, + isUserLocallyInTheCall = true, + ) + ) + } + } + @Test fun `present - user leaves the call`() = runTest { val room = FakeJoinedRoom( diff --git a/features/roomdetails/impl/src/main/res/values-it/translations.xml b/features/roomdetails/impl/src/main/res/values-it/translations.xml index 053e269bc4..22ec99f2b5 100644 --- a/features/roomdetails/impl/src/main/res/values-it/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-it/translations.xml @@ -1,5 +1,8 @@ + "I nuovi membri non vedono la cronologia" + "I nuovi membri vedono la cronologia" + "Chiunque può vedere la cronologia" "Per renderlo visibile nell\'elenco pubblico, avrai bisogno di un indirizzo." "Modifica indirizzo" "Si è verificato un errore durante l\'aggiornamento delle impostazioni di notifica." @@ -63,6 +66,7 @@ "Profilo" "Richieste di accesso" "Ruoli e autorizzazioni" + "Nome" "Sicurezza e privacy" "Sicurezza" "Condividi stanza" @@ -128,8 +132,10 @@ "Dettagli della stanza" "Ruoli e autorizzazioni" "Aggiungi indirizzo" + "Chiunque si trovi in spazi autorizzati può partecipare, ma tutti gli altri devono richiedere l\'accesso." "Chiunque deve richiedere l\'accesso." "Chiedi di entrare" + "Chiunque all\'interno di %1$s può partecipare, mentre tutti gli altri devono richiedere l\'accesso." "Sì, attiva la crittografia" "Una volta attivata, la crittografia di una stanza non può essere disattivata, la cronologia dei messaggi sarà visibile solo ai membri della stanza da quando sono stati invitati o da quando sono entrati nella stanza. Nessuno, oltre ai membri della stanza, sarà in grado di leggere i messaggi. Ciò potrebbe impedire ai bot e ai bridge di funzionare correttamente. @@ -140,19 +146,25 @@ Non consigliamo di attivare la crittografia per le stanze che chiunque può trov "Attiva la crittografia end-to-end" "Chiunque può partecipare." "Chiunque" + "Scegli quali membri dello spazio possono accedere a questa stanza senza invito.%1$s" + "Gestisci gli spazi" "Solo le persone invitate possono entrare." "Solo su invito" "Accesso" + "Chiunque si trovi in ​​spazi autorizzati può partecipare." + "Chiunque in %1$s può partecipare." + "Membri dello spazio" "Gli spazi non sono attualmente supportati" "Per renderlo visibile nell\'elenco pubblico, avrai bisogno di un indirizzo." "Indirizzo" "Consenti la ricerca di questa stanza effettuando una ricerca nell\'elenco delle stanze pubbliche di %1$s" "Consenti di essere trovato effettuando una ricerca nell\'elenco pubblico." "Visibile nell\'elenco pubblico" - "Chiunque" + "Chiunque (la cronologia è pubblica)" + "Le modifiche non interesseranno i messaggi passati, ma solo quelli nuovi. %1$s" "Chi può leggere la cronologia messaggi" - "Solo membri da quando sono stati invitati" - "Solo membri da dopo aver selezionato questa opzione" + "Members invited" + "Members (cronologia completa)" "Gli indirizzi delle stanze sono modi per trovare e accedervi. In questo modo puoi anche condividere facilmente la tua stanze con altri. Puoi scegliere di pubblicare la tua stanza nell\'elenco delle stanza pubbliche dell\'homeserver." "Pubblicazione della stanza" diff --git a/features/roomdetails/impl/src/main/res/values-ja/translations.xml b/features/roomdetails/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..70b3255437 --- /dev/null +++ b/features/roomdetails/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,171 @@ + + + "新しいメンバーは過去の内容を確認できない" + "新しいメンバーは過去の内容を確認できる" + "すべてのユーザーが過去の内容を確認できる" + "公開ディレクトリで自分を見つけられるようにするには、アドレスが必要です。" + "アドレスの編集" + "通知設定の更新中に問題が発生しました。" + "暗号化されたルームでは、この機能にホームサーバーが対応しないため、一部のルームから通知が届かない可能性があります。" + "投票" + "管理者" + "ユーザーの追放" + "メッセージの削除" + "メンバー" + "ユーザーの招待" + "メンバーの管理" + "メッセージと内容" + "モデレーター" + "ユーザーの削除" + "アバターの変更" + "詳細を編集" + "名前の変更" + "トピックの変更" + "メッセージの送信" + "管理者を編集" + "この操作は取り消せません。このユーザーをあなたと同じ権限まで昇格します。" + "管理者を追加しますか?" + "この操作は取り消せません。選択したユーザーに所有権を譲与します。あなたがルームを退出すると恒久的に変更が適用されます。" + "所有権を譲与しますか?" + "降格" + "自身を降格しようとしているため、後から取り消すことはできません。このルームに他に特権を持つユーザーが存在しない場合、それを回復することはできなくなります。" + "自身を降格しますか?" + "%1$s (承認待ち)" + "承認待ち" + "管理者はモデレータの特権を有します。" + "所有者は管理者の特権を有します。" + "モデレーターを編集" + "所有者を選択" + "管理者" + "モデレーター" + "メンバー" + "未保存の変更内容があります。" + "変更を保存しますか?" + "トピックを追加" + "暗号化済み" + "暗号化されていません" + "公開ルーム" + "詳細を編集" + "不明な問題が発生したため、情報の更新に失敗しました。" + "ルームを更新することができません" + "メッセージは、あなたと受信者のみが持つ鍵で暗号化されています。" + "メッセージの暗号化が有効です" + "通知設定の読み込み中に問題が発生しました。" + "ルームをミュートできませんでした。再試行してください。" + "ルームをミュート解除できませんでした。再試行してください。" + "終了するまでアプリを閉じないでください。" + "招待を準備中…" + "ユーザーを招待" + "会話を退出" + "ルームを退出" + "ファイルとメディア" + "カスタム" + "デフォルト" + "通知" + "ピン留めされたメッセージ" + "プロフィール" + "参加のリクエスト" + "役割と権限" + "名前" + "セキュリティとプライバシー" + "セキュリティ" + "ルームを共有" + "ルームの情報" + "トピック" + "詳細を更新中…" + "追放されたユーザーはいません。" + + "%1$d 人の追放" + + "スペルを確認するか、新たに検索し直してください" + "\"%1$s\" の検索結果はありません" + + "%1$d 人" + + "ユーザーを追放" + "メンバーのみを削除" + "追放を解除" + "招待を受け取ると再度参加できます。" + "ユーザーの追放を解除" + "追放済み" + "メンバー" + + "%1$d 件の招待" + + "待機中" + "管理者" + "モデレーター" + "所有者" + "ルームのメンバー" + "%1$s の追放を解除中" + "カスタム設定を許可" + "オンにするとデフォルト設定が上書きされます" + "このチャットで以下の通知を受け取る" + "%1$s から変更できます。" + "全体設定" + "デフォルト設定" + "カスタム設定を削除する" + "通知設定の読み込み中に問題が発生しました。" + "デフォルトの復元に失敗しました。再試行してください。" + "設定に失敗しました。再試行してください。" + "暗号化されたルームでは、この機能にホームサーバーが対応しないため、このルームからの通知を受信できません。" + "すべてのメッセージ" + "メンションとキーワードのみ" + "このルームでは以下の通知を受け取る" + "管理者" + "管理者と所有者" + "自身の役割を変更" + "権限を譲与" + "モデレーターに譲与" + "メンバーの編集" + "メッセージと内容" + "モデレーター" + "所有者" + "権限" + "権限をリセット" + "権限をリセットすると現在の設定はすべて失われます。" + "権限をリセットしますか?" + "役割" + "ルームの詳細" + "役割と権限" + "アドレスを追加" + "認証済みのスペースに所属するユーザーのみが参加できます。それ以外のユーザーは参加へのリクエストが必要です。" + "参加のリクエストが必須です。" + "参加をリクエスト" + "%1$s に所属するユーザーのみが参加できます。それ以外のユーザーは参加のリクエストが必要です。" + "暗号化を有効にする" + "暗号化が有効のルームを再び無効化することはできません。過去のメッセージの参照は、ユーザーが招待された、あるいは参加した以降に投稿された内容に限定されます。 +ルームのメンバー以外がメッセージを確認することはできないため、bot やブリッジのサービスが正常に動作しない可能性があります。 +公開スペースを暗号化することは一般に推奨されません。" + "暗号化を有効にしますか?" + "一度有効にすると元に戻すことはできません。" + "暗号化" + "エンドツーエンド暗号化を有効にする" + "誰でも参加できます" + "全員" + "招待無しで参加できるユーザーが所属するルームを選択してください。%1$s" + "スペースを管理" + "招待されたユーザーのみ参加できます。" + "招待制" + "アクセス" + "認証済みのスペースに所属するすべてのユーザーが参加できます。" + "%1$s に所属するすべてのユーザーが参加できます。" + "スペースのメンバー" + "スペースは現在対応していません。" + "公開ディレクトリで自分を見つけられるようにするには、アドレスが必要です。" + "アドレス" + "%1$s の公開ルームの検索結果に、このルームを表示します" + "公開ディレクトリの検索結果に表示" + "公開ディレクトリに表示" + "全員(履歴を公開)" + "過去のメッセージに変更は適用されません。新規のメッセージにのみ適用されます。%1$s" + "履歴を表示するユーザー" + "招待済みのユーザー" + "ユーザー (すべての履歴)" + "ルームアドレスはルームの検索やアクセスに役立ち、他のユーザーにルームを簡単に共有できます。 +ホームサーバーの公開ディレクトリにルームを表示するかを設定できます。" + "ルームの公開" + "ルームアドレスはルームの検索やアクセスに役立ち、他のユーザーにルームを簡単に共有できます。" + "視認性" + "セキュリティとプライバシー" + diff --git a/features/roomdetails/impl/src/main/res/values-vi/translations.xml b/features/roomdetails/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..a2ee35b563 --- /dev/null +++ b/features/roomdetails/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,107 @@ + + + "Thành viên mới không thể xem lịch sử" + "Thành viên mới có thể xem lịch sử" + "Ai cũng có thể xem lịch sử" + "Bạn cần một địa chỉ để hiển thị trong danh bạ công khai." + "Chỉnh sửa địa chỉ" + "Đã xảy ra lỗi khi cập nhật cài đặt thông báo." + "Máy chủ không hỗ trợ tùy chọn này trong phòng mã hóa, một số phòng có thể không thông báo." + "Cuộc thăm dò ý kiến" + "Quản trị viên" + "Cấm người dùng" + "Xóa tin nhắn" + "Thành viên" + "Mời mọi người" + "Quản lý thành viên" + "Tin nhắn và nội dung." + "Người điều hành" + "Gỡ người dùng" + "Đổi ảnh đại diện" + "Chỉnh sửa thông tin" + "Đổi tên" + "Đổi chủ đề" + "Gửi tin nhắn." + "Chỉnh sửa Quản trị viên" + "Bạn sẽ không thể hoàn tác hành động này. Bạn đang thăng quyền cho người dùng lên cùng cấp quyền với bạn." + "Thêm quản trị viên?" + "Giáng cấp" + "Bạn sẽ không thể hoàn tác thay đổi này vì bạn đang tự giáng cấp bản thân, nếu bạn là người dùng cuối cùng có đặc quyền trong phòng, nó sẽ không thể lấy lại đặc quyền." + "Giáng cấp bản thân?" + "%1$s (Đang chờ xử lý)" + "Chỉnh sửa Người điều hành" + "Quản trị viên" + "Người điều hành" + "Thành viên" + "Bạn có thay đổi chưa được lưu." + "Lưu thay đổi?" + "Thêm chủ đề" + "Chỉnh sửa thông tin" + "Có lỗi không xác định, thông tin không được cập nhật." + "Không thể cập nhật phòng" + "Tin nhắn được bảo mật bằng khóa. Chỉ bạn và người nhận mới có chìa khóa riêng để mở khóa." + "Mã hóa tin nhắn đã được bật" + "Đã xảy ra lỗi khi tải cài đặt thông báo." + "Không thể tắt tiếng phòng này, vui lòng thử lại." + "Không thể bật tiếng cho phòng này. Vui lòng thử lại." + "Mời ai đó" + "Rời khỏi cuộc trò chuyện" + "Rời phòng" + "Tùy chỉnh" + "Mặc định" + "Thông báo" + "Tin nhắn được ghim" + "Vai trò và quyền hạn" + "Tên" + "Bảo mật" + "Chia sẻ phòng" + "Chủ đề" + "Đang cập nhật thông tin…" + "Hiện không có người dùng nào bị cấm." + + "%1$d người" + + "Cấm người dùng" + "Chỉ xóa thành viên" + "Bỏ cấm" + "Họ có thể tham gia lại phòng này nếu được mời." + "Bị cấm" + "Thành viên" + "Quản trị viên" + "Người điều hành" + "Thành viên phòng" + "Đang gỡ cấm %1$s" + "Cho phép tùy chỉnh cài đặt" + "Kích hoạt sẽ thay thế cài đặt mặc định" + "Thông báo cho tôi trong cuộc trò chuyện này khi" + "Bạn có thể thay đổi trong %1$s của mình." + "cài đặt chung" + "Cài đặt mặc định" + "Xóa cài đặt tùy chỉnh" + "Đã xảy ra lỗi khi tải cài đặt thông báo." + "Không thể khôi phục chế độ mặc định, vui lòng thử lại." + "Không thể thiết lập chế độ, hãy thử lại nhé." + "Máy chủ không hỗ trợ tùy chọn này trong phòng mã hóa, bạn sẽ không nhận thông báo ở đây." + "Tất cả tin nhắn." + "Chỉ đề cập và từ khóa" + "Trong phòng này, thông báo cho tôi khi" + "Quản trị viên" + "Thay đổi vai trò của tôi" + "Hạ cấp xuống thành thành viên" + "Hạ cấp xuống làm người điều hành" + "Quản lý thành viên" + "Tin nhắn và nội dung." + "Người điều hành" + "Đặt lại quyền truy cập" + "Sau khi bạn đặt lại quyền truy cập, bạn sẽ mất các cài đặt hiện tại." + "Đặt lại quyền truy cập?" + "Vai trò" + "Chi tiết phòng" + "Vai trò và quyền hạn" + "Sau khi được kích hoạt, mã hóa cho một phòng chat không thể tắt được. Lịch sử tin nhắn chỉ hiển thị cho các thành viên phòng chat kể từ khi họ được mời hoặc kể từ khi họ tham gia phòng chat. +Không ai ngoài các thành viên phòng chat có thể đọc tin nhắn. Điều này có thể ngăn chặn bot và các thiết bị kết nối hoạt động đúng cách. +Chúng tôi không khuyến khích bật mã hóa cho các phòng chat mà bất kỳ ai cũng có thể tìm thấy và tham gia." + "Mã hóa" + "Thành viên không gian" + "Bạn cần một địa chỉ để hiển thị trong danh bạ công khai." + diff --git a/features/roomdetails/impl/src/main/res/values-zh/translations.xml b/features/roomdetails/impl/src/main/res/values-zh/translations.xml index 4101f67260..d803f01fb7 100644 --- a/features/roomdetails/impl/src/main/res/values-zh/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-zh/translations.xml @@ -23,7 +23,7 @@ "更改聊天室主题" "发送消息" "编辑管理员" - "您将无法撤消此操作。您正在提升用户的权限,使其拥有与您平权。" + "您将无法撤消此操作。您正在提升用户的权限到与您相同的级别。" "添加管理员?" "此操作无法撤销。您正在将所有权转移给所选用户。一旦离开此界面,该操作将永久生效。" "转让所有权" @@ -42,8 +42,8 @@ "您有未保存的更改。" "保存更改?" "添加主题" - "加密的" - "未加密的" + "已加密" + "未加密" "公共聊天室" "编辑详情" "出现未知错误,无法更改信息。" @@ -86,7 +86,7 @@ "仅移除成员" "取消封禁" "如果受到邀请,他们可以重新加入聊天室。" - "从房间取消解封" + "解封用户" "已封禁用户" "成员" @@ -97,7 +97,7 @@ "协管员" "所有者" "聊天室成员" - "解除封禁 %1$s" + "正在解除封禁 %1$s" "允许自定义设置" "开启此功能将覆盖您的默认设置" "在此聊天中通知我以下内容" diff --git a/features/roomdetailsedit/impl/src/main/res/values-ja/translations.xml b/features/roomdetailsedit/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..5dbdbe22e1 --- /dev/null +++ b/features/roomdetailsedit/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,7 @@ + + + "詳細を編集" + "不明な問題が発生したため、情報の更新に失敗しました。" + "ルームを更新することができません" + "詳細を更新中…" + diff --git a/features/roomdetailsedit/impl/src/main/res/values-vi/translations.xml b/features/roomdetailsedit/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..9a5d8948e4 --- /dev/null +++ b/features/roomdetailsedit/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,7 @@ + + + "Chỉnh sửa thông tin" + "Có lỗi không xác định, thông tin không được cập nhật." + "Không thể cập nhật phòng" + "Đang cập nhật thông tin…" + diff --git a/features/roomdirectory/impl/src/main/res/values-ja/translations.xml b/features/roomdirectory/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..f533dc8eab --- /dev/null +++ b/features/roomdirectory/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,5 @@ + + + "読み込みに失敗しました" + "ルーム階層" + diff --git a/features/roomdirectory/impl/src/main/res/values-vi/translations.xml b/features/roomdirectory/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..efbe223f79 --- /dev/null +++ b/features/roomdirectory/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,5 @@ + + + "Không tải được" + "Danh sách phòng" + diff --git a/features/roommembermoderation/impl/src/main/res/values-ja/translations.xml b/features/roommembermoderation/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..5ca793fd5a --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,22 @@ + + + "ユーザーを追放" + "追放" + "招待されても再度参加することはできません。" + "このメンバーを本当に追放しますか?" + "招待されても再度参加することはできませんが、すべてのルームとスペースにおける権限を維持します。" + "%1$s を追放中" + "削除" + "招待を受け取ると再度参加できます。" + "このメンバーを本当に削除しますか?" + "すべてのルームとスペースにおける権限を維持し、招待によって再度参加することができます。" + "プロフィールを表示" + "ユーザーを削除" + "メンバーを削除し、今後の参加を禁止しますか?" + "%1$s を削除中…" + "ユーザーの追放を解除" + "追放を解除" + "招待によって再度参加することができます。" + "このメンバーの追放を本当に解除しますか?" + "%1$s の追放を解除中" + diff --git a/features/roommembermoderation/impl/src/main/res/values-vi/translations.xml b/features/roommembermoderation/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..07d712cd4c --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,15 @@ + + + "Cấm người dùng" + "Cấm" + "Họ sẽ không thể tham gia lại ngay cả khi được mời." + "Xác nhận cấm thành viên này?" + "Đang cấm %1$s" + "Họ có thể tham gia lại phòng này nếu được mời." + "Xem hồ sơ" + "Xóa người dùng" + "Xóa thành viên và cấm tham gia trong tương lai?" + "Đang xóa %1$s…" + "Bỏ cấm" + "Đang gỡ cấm %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-zh/translations.xml b/features/roommembermoderation/impl/src/main/res/values-zh/translations.xml index aa8b2e3df8..2c18ee4216 100644 --- a/features/roommembermoderation/impl/src/main/res/values-zh/translations.xml +++ b/features/roommembermoderation/impl/src/main/res/values-zh/translations.xml @@ -5,7 +5,7 @@ "即使受到邀请,他们也无法再次加入聊天室。" "您确定要封禁该成员吗?" "即使再次受邀,他们也无法加入这个空间,但他们仍将保留其在任何房间或子空间的成员资格。" - "封禁 %1$s" + "正在封禁 %1$s" "移除" "如果受到邀请,他们可以重新加入聊天室。" "您确定要移除此成员吗?" @@ -14,9 +14,9 @@ "移除用户" "删除成员并禁止重新加入?" "正在移除 %1$s……" - "从房间取消解封" + "解封用户" "取消封禁" "如果再次收到邀请,他们可以重新加入该聊天室" "确定要解除该成员的封禁吗?" - "解除封禁 %1$s" + "正在解除封禁 %1$s" diff --git a/features/securebackup/impl/src/main/res/values-cs/translations.xml b/features/securebackup/impl/src/main/res/values-cs/translations.xml index 3669da34ee..171900cede 100644 --- a/features/securebackup/impl/src/main/res/values-cs/translations.xml +++ b/features/securebackup/impl/src/main/res/values-cs/translations.xml @@ -2,16 +2,17 @@ "Vypnout zálohování" "Zapnout zálohování" - "Bezpečně uložte svou kryptografickou identitu a klíče zpráv na serveru. To vám umožní zobrazit historii zpráv na všech nových zařízeních. %1$s." + "To vám umožní zobrazit historii chatu na všech nových zařízeních a je to nutné pro zálohování chatů a digitální identity. %1$s ." "Úložiště klíčů" - "Pro nastavení obnovení musí být zapnuto úložiště klíčů." + "Pro zálohování chatů musí být zapnuto ukládání klíčů." "Nahrát klíče z tohoto zařízení" "Povolit ukládání klíčů" "Změnit klíč pro obnovení" - "Obnovte svou kryptografickou identitu a historii zpráv pomocí klíče pro obnovení, pokud jste ztratili všechna stávající zařízení." + "Vaše chaty jsou automaticky zálohovány pomocí koncového šifrování. Chcete-li tuto zálohu obnovit a zachovat si svou digitální identitu v případě, že ztratíte přístup ke všem svým zařízením, budete potřebovat svůj klíč pro obnovení." "Zadejte klíč pro obnovení" "Vaše úložiště klíčů je momentálně nesynchronizované." - "Nastavení obnovy" + "Získat klíč pro obnovení" + "Vaše chaty jsou automaticky zálohovány pomocí koncového šifrování. Chcete-li tuto zálohu obnovit a zachovat si svou digitální identitu v případě, že ztratíte přístup ke všem svým zařízením, budete potřebovat svůj klíč pro obnovení." "Otevřít %1$s na stolním počítači" "Znovu se přihlaste ke svému účtu" "Když budete vyzváni k ověření vašeho zařízení, vyberte %1$s" @@ -23,12 +24,12 @@ "Podrobnosti o vašem účtu, kontaktech, preferencích a seznamu chatu budou zachovány" "Ztratíte svou stávající historii zpráv" "Budete muset znovu ověřit všechna stávající zařízení a kontakty" - "Obnovte svou identitu pouze v případě, že nemáte přístup k jinému přihlášenému zařízení a ztratili jste klíč pro obnovení." - "Obnovte svou identitu v případě, že nemůžete potvrdit jiným způsobem" - "Vypnout" - "Pokud se odhlásíte ze všech zařízení, přijdete o zašifrované zprávy." - "Opravdu chcete vypnout zálohování?" - "Vypnutím zálohování odstraníte zálohu aktuálního šifrovacího klíče a vypnete další bezpečnostní funkce. V tomto případě budete:" + "Digitální identitu resetujte pouze v případě, že nemáte přístup k jinému ověřenému zařízení a nemáte klíč pro obnovení." + "Nelze potvrdit? Budete muset resetovat svou digitální identitu." + "Smazat" + "Pokud odeberete všechna zařízení, ztratíte svou šifrovanou historii chatu a budete muset resetovat svou digitální identitu." + "Opravdu chcete smazat úložiště klíčů?" + "Smazáním úložiště klíčů odstraníte ze serveru klíče digitální identity a zpráv a vypnete následující bezpečnostní funkce:" "Nemít v nových zařízeních šifrovanou historii zpráv" "Ztratíte přístup k šifrovaným zprávám, pokud jste všude odhlášeni z %1$s" "Opravdu chcete vypnout zálohování?" @@ -58,12 +59,12 @@ "Vygenerovat klíč pro obnovení" "Toto s nikým nesdílejte!" "Nastavení obnovení bylo úspěšné" - "Nastavení obnovy" + "Získat klíč pro obnovení" "Ano, resetovat nyní" "Tento proces je nevratný." - "Opravdu chcete obnovit svou identitu?" + "Opravdu chcete resetovat svou digitální identitu?" "Došlo k neznámé chybě. Zkontrolujte, zda je heslo k účtu správné a zkuste to znovu." "Zadejte…" - "Potvrďte, že chcete obnovit svou identitu." + "Potvrďte, že chcete resetovat svou digitální identitu." "Pro pokračování zadejte heslo k účtu" diff --git a/features/securebackup/impl/src/main/res/values-fi/translations.xml b/features/securebackup/impl/src/main/res/values-fi/translations.xml index f7d09bb6ca..7cbe0f1af6 100644 --- a/features/securebackup/impl/src/main/res/values-fi/translations.xml +++ b/features/securebackup/impl/src/main/res/values-fi/translations.xml @@ -12,6 +12,7 @@ "Anna palautusavain" "Avainten säilytys ei ole tällä hetkellä synkronoitu." "Hanki palautusavain" + "Keskustelusi varmuuskopioidaan automaattisesti päästä päähän -salauksella. Jotta voit palauttaa tämän varmuuskopion ja säilyttää digitaalisen identiteettisi, kun menetät pääsyn kaikkiin laitteisiisi, tarvitset palautusavaimesi." "Avaa %1$s tietokoneella" "Kirjaudu tilillesi uudelleen" "Kun sinua pyydetään vahvistamaan laitteesi, valitse %1$s" diff --git a/features/securebackup/impl/src/main/res/values-hu/translations.xml b/features/securebackup/impl/src/main/res/values-hu/translations.xml index cf7ab1cb0c..a5d910e76b 100644 --- a/features/securebackup/impl/src/main/res/values-hu/translations.xml +++ b/features/securebackup/impl/src/main/res/values-hu/translations.xml @@ -12,6 +12,7 @@ "Adja meg a helyreállítási kulcsot" "A kulcstároló jelenleg nincs szinkronizálva." "Helyreállítási kulcs beszerzése" + "A csevegésekről automatikusan készül biztonsági mentés végpontok közötti titkosítással. A biztonsági mentés helyreállításához és digitális személyazonossága megőrzéséhez szüksége lesz a helyreállítási kulcsára, ha elveszíti a hozzáférést az összes eszközéhez." "Nyissa meg az %1$set egy asztali eszközön" "Jelentkezzen be újra a fiókjába" "Amikor az eszköz ellenőrzését kéri, válassza ezt a lehetőséget: %1$s" @@ -23,11 +24,11 @@ "A fiókadatok, a kapcsolatok, a beállítások és a csevegéslista megmarad" "Elveszíti meglévő üzenetelőzményeit" "Újból ellenőriznie kell az összes meglévő eszközét és csevegőpartnerét" - "Csak akkor állítsa alaphelyzetbe a személyazonosságát, ha nem fér hozzá másik bejelentkezett eszközhöz, és elvesztette a helyreállítási kulcsot." + "Csak akkor állítsa alaphelyzetbe a személyazonosságát, ha nem fér hozzá más bejelentkezett eszközhöz, és elveszítette a helyreállítási kulcsát." "Nem tudja megerősíteni? Alaphelyzetbe kell állítania a digitális személyazonosságát." - "Kikapcsolás" - "Ha kijelentkezik az összes eszközéről, akkor elveszti a titkosított üzeneteit." - "Biztos, hogy kikapcsolja a biztonsági mentéseket?" + "Törlés" + "Ha eltávolítja az összes eszközét, elveszíti titkosított csevegési előzményeit, és újra be kell állítania digitális személyazonosságát." + "Biztosan törölni szeretné a kulcstárolót?" "A kulcstároló törlése eltávolítja a digitális személyazonosságát és az üzenetkulcsait a kiszolgálóról, és kikapcsolja a következő biztonsági funkciókat:" "Nem lesznek meg a titkosított üzenetek előzményei az új eszközein" "Elveszti a hozzáférését a titkosított üzeneteihez, ha mindenhol kilép az %1$sből" diff --git a/features/securebackup/impl/src/main/res/values-it/translations.xml b/features/securebackup/impl/src/main/res/values-it/translations.xml index fe7ba13c60..188de2c3df 100644 --- a/features/securebackup/impl/src/main/res/values-it/translations.xml +++ b/features/securebackup/impl/src/main/res/values-it/translations.xml @@ -2,16 +2,17 @@ "Disattiva il backup" "Attiva il backup" - "Archivia la tua identità crittografica e le chiavi dei messaggi in modo sicuro sul server. Ciò ti consentirà di visualizzare la cronologia dei messaggi su tutti i nuovi dispositivi. %1$s." + "Questo ti permetterà di visualizzare la cronologia delle conversazioni su qualsiasi nuovo dispositivo ed è necessario per il backup delle chat e dell\'identità digitale.%1$s ." "Archiviazione chiavi" - "L\'archiviazione delle chiavi deve essere attivata per configurare il ripristino." + "Per eseguire il backup delle tue conversazioni, devi attivare l\'archiviazione delle chiavi." "Carica le chiavi da questo dispositivo" "Consenti l\'archiviazione delle chiavi" "Cambia la chiave di recupero" - "Recupera la tua identità crittografica e la cronologia dei messaggi con una chiave di recupero se hai perso tutti i dispositivi esistenti." + "Le tue conversazioni vengono automaticamente salvate con crittografia end-to-end. Per ripristinare questo backup e conservare la tua identità digitale quando perdi l\'accesso a tutti i tuoi dispositivi, avrai bisogno della tua chiave di recupero." "Inserisci la chiave di recupero" "L\'archiviazione delle chiavi non è sincronizzata." - "Configura il recupero" + "Ottieni la chiave di recupero" + "Le tue conversazioni vengono automaticamente salvate con crittografia end-to-end. Per ripristinare questo backup e conservare la tua identità digitale quando perdi l\'accesso a tutti i tuoi dispositivi, avrai bisogno della tua chiave di recupero." "Apri %1$s in un dispositivo desktop" "Accedi nuovamente al tuo account" "Quando ti viene chiesto di verificare il tuo dispositivo, seleziona %1$s" @@ -23,12 +24,12 @@ "I dettagli del tuo account, i contatti, le preferenze e l\'elenco delle conversazioni verranno conservati" "Perderai la cronologia dei messaggi esistente" "Dovrai verificare nuovamente tutti i dispositivi e i contatti esistenti" - "Reimposta la tua identità solo se non hai accesso a un altro dispositivo su cui hai effettuato l\'accesso e hai perso la chiave di recupero." - "Reimposta la tua identità nel caso in cui non riesci a confermare in un altro modo" - "Disattiva" - "Perderai i tuoi messaggi cifrati se sei disconnesso da tutti i dispositivi." - "Vuoi davvero disattivare il backup?" - "La disattivazione del backup rimuoverà il backup dell\'attuale chiave crittografica e disattiverà altre funzioni di sicurezza. In questo caso:" + "Reimposta la tua identità digitale solo se non hai accesso a un altro dispositivo verificato e non disponi della tua chiave di recupero." + "Non riesci a confermare? Dovrai reimpostare la tua identità digitale." + "Elimina" + "Se rimuovi tutti i tuoi dispositivi, perderai la cronologia delle conversazioni cifrate e dovrai reimpostare la tua identità digitale." + "Sei sicuro di voler eliminare l\'archivio delle chiavi?" + "L\'eliminazione della memoria delle chiavi rimuoverà l\'identità digitale e le chiavi dei messaggi dal server e disattiverà le seguenti funzionalità di sicurezza:" "Non avrai la cronologia dei messaggi cifrati su nuovi dispositivi" "Perderai l\'accesso ai tuoi messaggi cifrati se ti sei disconnesso da %1$s ovunque" "Vuoi davvero disattivare il backup?" @@ -58,12 +59,12 @@ "Genera la tua chiave di recupero" "Non condividerla con nessuno!" "Configurazione del recupero completata" - "Configura il recupero" + "Ottieni la chiave di recupero" "Sì, reimposta ora" "Questo processo è irreversibile." - "Sei sicuro di voler reimpostare la crittografia?" + "Sei sicuro di voler reimpostare la tua identità digitale?" "Si è verificato un errore sconosciuto. Controlla che la password del tuo account sia corretta e riprova." "Inserisci…" - "Conferma di voler reimpostare la crittografia." + "Conferma che desideri reimpostare la tua identità digitale." "Inserisci la password del tuo account per continuare" diff --git a/features/securebackup/impl/src/main/res/values-ja/translations.xml b/features/securebackup/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..7a7dd041fe --- /dev/null +++ b/features/securebackup/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,70 @@ + + + "保存されている鍵を削除" + "バックアップを有効化" + "暗号化されたデジタルIDとメッセージの鍵をサーバーに保存します。これにより、新しい端末から過去のメッセージを確認することができます。%1$s" + "鍵の保管庫" + "チャットをバックアップするには、鍵の保管庫を使用する必要があります。" + "この端末上の鍵をアップロードします" + "鍵の保管庫を使用します" + "回復鍵を変更" + "あなたのチャットはエンドツーエンド暗号化を使用して自動的にバックアップされています。すべての端末を使用できない状況で、このバックアップからデジタルIDを復元するには、回復鍵が必要となります。" + "回復鍵を入力" + "鍵の保管庫を現在同期できません。" + "回復鍵を作成" + "あなたのチャットはエンドツーエンド暗号化を使用して自動的にバックアップされています。すべての端末を使用できない状況で、このバックアップからデジタルIDを復元するには、回復鍵が必要となります。" + "%1$s をコンピュータで開く" + "再度サインインしてください" + "端末の認証を要求されたら %1$s を選択してください" + "\"すべてリセット\"" + "指示に従って回復鍵を作成してください" + "生成された回復鍵をパスワードマネージャや暗号化に対応するメモアプリに保存してください。" + "他の端末を使用して暗号化をリセット" + "リセットを続行" + "アカウントの情報と連絡先や設定などは残ります" + "サーバー上にのみ存在する過去のメッセージは確認できなくなります" + "すべての端末と連絡先を再度検証する必要があります" + "デジタルIDのリセットは、他のサインイン済みの端末と、回復鍵の両方へのアクセスを失った場合にのみ行ってください。" + "認証できませんか?デジタルIDをリセットする必要があります。" + "削除" + "すべての端末を削除してしまうと、暗号化された会話が失われ、デジタルIDをリセットする必要があります。" + "本当に鍵の保管庫を削除しますか?" + "鍵の保管庫を消去することにより、デジタルIDとメッセージの鍵はサーバーから削除され、次のセキュリティ機能が無効化されます:" + "新しい端末で暗号化された過去のメッセージを確認できなくなります" + "すべての端末で %1$s からサインアウトすると、暗号化されたメッセージを確認することはできなくなります。" + "本当に鍵を保管庫から削除しますか?" + "既存の回復鍵を紛失した場合は、新しい回復鍵を生成してください。回復鍵を更新すると、それ以前の回復鍵は使用できなくなります。" + "新しい回復鍵を生成する" + "誰にも共有しないでください!" + "回復鍵を更新しました" + "回復鍵を変更しますか?" + "新しい回復鍵を生成" + "誰にもこの画面を見せないでください!" + "鍵の保管庫にアクセスするには、もう一度お試しください。" + "回復鍵が間違っています" + "代わりにセキュリティキーまたはセキュリティフレーズを入力することも可能です。" + "回復鍵を入力してください…" + "回復鍵を紛失しましたか?" + "回復鍵が承認されました" + "回復鍵を入力してください" + "回復鍵をコピーしました" + "生成中…" + "回復鍵を保存" + "この回復鍵をパスワードマネージャーや、暗号化に対応するメモアプリなどに記録するか、物理的な金庫などに書き留めて保管してください。" + "タップして回復鍵をコピー" + "回復鍵を安全な場所に保管してください" + "後からこの回復鍵を確認することはできません。" + "回復鍵を保存しましたか?" + "鍵の保管庫は回復鍵によって保護されています。新しい回復鍵が必要な場合は「回復鍵を変更」を選択して再生成できます。" + "新しい回復鍵を生成する" + "誰にも共有しないでください!" + "回復鍵の設定に成功しました" + "回復鍵を作成" + "はい、リセットします" + "この操作は元に戻せません。" + "本当にデジタルIDをリセットしますか?" + "不明な問題が発生しました。アカウントのパスワードが正しいことを確認してもう一度試してください。" + "回復鍵を入力してください…" + "デジタルIDをリセットしようとしています。" + "アカウントのパスワードを入力" + diff --git a/features/securebackup/impl/src/main/res/values-ko/translations.xml b/features/securebackup/impl/src/main/res/values-ko/translations.xml index d2aec9c27d..182ae8d59a 100644 --- a/features/securebackup/impl/src/main/res/values-ko/translations.xml +++ b/features/securebackup/impl/src/main/res/values-ko/translations.xml @@ -4,14 +4,15 @@ "백업 활성화" "이 설정을 통해 새로운 기기에서도 대화 기록을 확인할 수 있으며, 대화 및 디지털 신원 백업을 위해 반드시 필요합니다. %1$s." "키 저장소" - "복구 설정을 하려면 키 저장을 켜야 합니다." + "대화 내용을 백업하려면 키 저장소를 켜야 합니다." "이 장치에서 키 업로드" "키 저장 허용" "복구 키 변경" - "기존의 모든 기기를 분실한 경우, 복구 키를 사용하여 암호화 ID와 메시지 기록을 복구할 수 있습니다." + "대화 내용은 종단간 암호화 기술로 자동 백업됩니다. 모든 기기를 사용할 수 없는 상황에서 백업을 복구하고 디지털 신원을 유지하려면 복구 키가 반드시 필요합니다." "복구 키를 입력하세요" "현재 키 저장소가 동기화되지 않았습니다." "복구 키 가져오기" + "대화 내용은 종단간 암호화로 자동 백업됩니다. 모든 기기를 사용할 수 없는 상황에서 백업을 복구하고 디지털 신원을 유지하려면 복구 키가 반드시 필요합니다." "데스크톱 장치에서 %1$s 을 엽니다." "계정에 다시 로그인하세요" "장치를 확인하라는 메시지가 표시되면, %1$s 을 선택하세요" diff --git a/features/securebackup/impl/src/main/res/values-ru/translations.xml b/features/securebackup/impl/src/main/res/values-ru/translations.xml index 35da59cdb9..ec5237235e 100644 --- a/features/securebackup/impl/src/main/res/values-ru/translations.xml +++ b/features/securebackup/impl/src/main/res/values-ru/translations.xml @@ -24,12 +24,12 @@ "Данные вашего аккаунта, контакты, настройки и список чатов будут сохранены" "Вы потеряете историю тех сообщений, которые хранятся только на сервере" "Вам нужно будет заново подтвердить все существующие устройства и контакты." - "Сбрасывайте личность только в том случае, если у вас нет доступа к другим устройству, на которых выполнен вход, и вы потеряли ключ восстановления." - "Не можете подтвердить? Вам потребуется сбросить личность вашей учетной записи." + "Сбрасывайте ключ шифрования только в том случае, если у вас нет доступа к другому устройству, на котором выполнен вход, и вы потеряли ключ восстановления." + "Не можете подтвердить? Вам потребуется сбросить свою цифровую идентификацию." "Удалить" "Вы потеряете зашифрованные сообщения, если выйдете из всех устройств." - "Вы действительно хотите отключить резервное копирование?" - "Удаление хранилища ключей приведёт к удалению вашей криптографической личности и ключей сообщений с сервера, а также отключению следующих функций безопасности:" + "Вы уверены, что хотите удалить хранилище ключей?" + "Удаление хранилища ключей приведет к удалению вашей цифровой идентификации и ключей сообщений с сервера, а также к отключению следующих функций безопасности:" "Нет зашифрованной истории сообщений на новых устройствах" "Вы потеряете доступ к зашифрованным сообщениям, если выйдете из %1$s везде" "Вы уверены, что хотите отключить хранение ключей и удалить их?" diff --git a/features/securebackup/impl/src/main/res/values-vi/translations.xml b/features/securebackup/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..70913a5e24 --- /dev/null +++ b/features/securebackup/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,44 @@ + + + "Xóa kho lưu trữ khóa" + "Bật tính năng sao lưu" + "Điều này cho phép bạn xem lịch sử trò chuyện trên bất kỳ thiết bị mới nào và cần thiết để sao lưu trò chuyện cũng như danh tính kỹ thuật số. %1$s." + "Lưu trữ khóa" + "Thay đổi khóa khôi phục." + "Nhập mã khôi phục." + "Kho lưu trữ khóa của bạn hiện đang không đồng bộ." + "Lấy khóa khôi phục." + "Xoá" + "Bạn sẽ mất các tin nhắn đã mã hóa nếu bạn đăng xuất khỏi tất cả các thiết bị." + "Bạn có chắc muốn xóa lưu trữ khóa không?" + "Xóa lưu trữ khóa sẽ loại bỏ danh tính kỹ thuật số và khóa tin nhắn của bạn khỏi máy chủ, đồng thời tắt các tính năng bảo mật sau:" + "Lịch sử tin nhắn mã hóa sẽ không có trên thiết bị mới." + "Bạn sẽ mất quyền truy cập vào các tin nhắn được mã hóa nếu đăng xuất khỏi %1$s trên tất cả thiết bị" + "Bạn có chắc muốn tắt lưu trữ khóa và xóa nó không?" + "Nếu mất khóa khôi phục hiện tại, hãy tạo khóa mới. Khóa cũ sẽ không còn dùng được sau khi thay đổi." + "Tạo khóa khôi phục mới." + "Đừng chia sẻ điều này với bất kỳ ai!" + "Khóa khôi phục đã thay đổi." + "Thay đổi khóa khôi phục?" + "Đảm bảo không ai có thể nhìn thấy màn hình này!" + "Thử lại để xác nhận quyền truy cập lưu trữ khóa." + "Khóa khôi phục không chính xác." + "Nếu bạn có khóa bảo mật hoặc mật khẩu, bạn cũng có thể dùng." + "Nhập…" + "Khóa khôi phục xác nhận thành công." + "Nhập khóa khôi phục của bạn." + "Đã sao chép khóa khôi phục." + "Đang tạo…" + "Lưu khóa khôi phục." + "Ghi khóa khôi phục vào nơi an toàn, như trình quản lý mật khẩu, ghi chú mã hóa hoặc két sắt." + "Chạm để sao chép khóa khôi phục." + "Hãy lưu khóa khôi phục ở nơi an toàn." + "Sau bước này, bạn sẽ không còn truy cập khóa khôi phục mới." + "Bạn đã lưu lại khóa khôi phục chưa?" + "Lưu trữ khóa của bạn được bảo vệ bằng một khóa khôi phục. Nếu cần một khóa khôi phục mới sau khi thiết lập, bạn có thể tạo lại bằng cách chọn \'Thay đổi khóa khôi phục\'." + "Tạo khóa khôi phục của bạn." + "Đừng chia sẻ điều này với bất kỳ ai!" + "Thiết lập khôi phục thành công" + "Lấy khóa khôi phục." + "Nhập…" + diff --git a/features/securebackup/impl/src/main/res/values-zh/translations.xml b/features/securebackup/impl/src/main/res/values-zh/translations.xml index 438a05f893..8d86e496cb 100644 --- a/features/securebackup/impl/src/main/res/values-zh/translations.xml +++ b/features/securebackup/impl/src/main/res/values-zh/translations.xml @@ -11,7 +11,7 @@ "如果您丢失了所有现有设备,使用恢复密钥恢复您的密码学身份和消息历史记录。" "输入恢复密钥" "您的密钥存储当前不同步。" - "设置恢复" + "获取恢复密钥" "在桌面设备中打开 %1$s" "再次登录您的账户" "当要求验证您的设备时,选择 %1$s" @@ -23,8 +23,8 @@ "您的账户信息、联系人、偏好设置和聊天列表将被保留" "您将丢失现有的消息历史记录" "您将需要再次验证所有您的现有设备和联系人" - "仅当您无法访问其他已登录设备并且丢失了恢复密钥时才重置您的身份。" - "如果您无法通过其他方式确认,请重置您的身份" + "仅当您无法访问其他已登录设备并且丢失了恢复密钥时才重置您的数字身份。" + "无法确认?那么你需要重置您的数字身份。" "关闭" "如果您登出所有设备,您的加密消息将丢失。" "您确定要关闭备份吗?" @@ -34,7 +34,7 @@ "您确定要关闭备份吗?" "如果您丢失了现有的恢复密钥,请获取新的恢复密钥。更改恢复密钥后,您的旧密钥将不再起作用。" "生成新的恢复密钥" - "不要告诉任何人!" + "请勿与任何人分享!" "恢复密钥已更改" "更改恢复密钥?" "创建新的恢复密钥" @@ -56,14 +56,14 @@ "您保存了恢复密钥吗?" "您的聊天备份受恢复密钥保护。如果您在安装后需要新的恢复密钥,则可以通过选择「更改恢复密钥」来重新创建。" "生成恢复密钥" - "不要告诉任何人!" + "请勿与任何人分享!" "恢复设置成功" - "设置恢复" + "获取恢复密钥" "是的,立即重置" "此过程不可逆。" - "您确定要重置加密吗?" + "您确定要重置您的数字身份吗?" "发生未知错误。请检查您的帐户密码是否正确,然后重试。" "输入……" - "确认您要重置加密。" + "确认您要重置您的数字身份。" "输入您的账户密码以继续" diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt index 9230c1183a..48ab41a820 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt @@ -78,9 +78,6 @@ class SecurityAndPrivacyPresenter( val isKnockEnabled by remember { featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock) }.collectAsState(false) - val isSpaceSettingsEnabled by remember { - featureFlagService.isFeatureEnabledFlow(FeatureFlags.SpaceSettings) - }.collectAsState(false) val saveAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } val homeserverName = remember { matrixClient.userIdServerName() } @@ -248,7 +245,6 @@ class SecurityAndPrivacyPresenter( saveAction = saveAction.value, permissions = permissions, isSpace = roomInfo.isSpace, - isSpaceSettingsEnabled = isSpaceSettingsEnabled, selectableJoinedSpaces = selectableJoinedSpaces, spaceSelectionMode = spaceSelectionMode, eventSink = ::handleEvent, diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt index 6ec47ba183..26e77b3c70 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt @@ -29,7 +29,6 @@ data class SecurityAndPrivacyState( val homeserverName: String, val showEnableEncryptionConfirmation: Boolean, private val isKnockEnabled: Boolean, - private val isSpaceSettingsEnabled: Boolean, val saveAction: AsyncAction, val isSpace: Boolean, private val permissions: SecurityAndPrivacyPermissions, @@ -37,7 +36,7 @@ data class SecurityAndPrivacyState( private val spaceSelectionMode: SpaceSelectionMode, val eventSink: (SecurityAndPrivacyEvent) -> Unit ) { - val isSpaceMemberSelectable = isSpaceSettingsEnabled && spaceSelectionMode != SpaceSelectionMode.None + val isSpaceMemberSelectable = spaceSelectionMode != SpaceSelectionMode.None // Show SpaceMember option in two cases: // - SpaceMember is the current saved value diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt index 95cb45d641..19124302e3 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt @@ -138,7 +138,6 @@ fun aSecurityAndPrivacyState( isSpace: Boolean = false, selectableJoinedSpaces: Set = emptySet(), spaceSelectionMode: SpaceSelectionMode = SpaceSelectionMode.None, - isSpaceSettingsEnabled: Boolean = true, eventSink: (SecurityAndPrivacyEvent) -> Unit = {} ) = SecurityAndPrivacyState( editedSettings = editedSettings, @@ -151,6 +150,5 @@ fun aSecurityAndPrivacyState( isSpace = isSpace, selectableJoinedSpaces = selectableJoinedSpaces.toImmutableSet(), spaceSelectionMode = spaceSelectionMode, - isSpaceSettingsEnabled = isSpaceSettingsEnabled, eventSink = eventSink, ) diff --git a/features/securityandprivacy/impl/src/main/res/values-it/translations.xml b/features/securityandprivacy/impl/src/main/res/values-it/translations.xml index 0cf3326022..41b0c1a047 100644 --- a/features/securityandprivacy/impl/src/main/res/values-it/translations.xml +++ b/features/securityandprivacy/impl/src/main/res/values-it/translations.xml @@ -2,9 +2,16 @@ "Per renderlo visibile nell\'elenco pubblico, avrai bisogno di un indirizzo." "Modifica indirizzo" + "Spazi in cui i membri possono entrare nella stanza senza invito." + "Gestisci gli spazi" + "(Spazio sconosciuto)" + "Altri spazi di cui non sei membro" + "I tuoi spazi" "Aggiungi indirizzo" + "Chiunque si trovi in spazi autorizzati può partecipare, ma tutti gli altri devono richiedere l\'accesso." "Chiunque deve richiedere l\'accesso." "Chiedi di entrare" + "Chiunque all\'interno di %1$s può partecipare, mentre tutti gli altri devono richiedere l\'accesso." "Sì, attiva la crittografia" "Una volta attivata, la crittografia di una stanza non può essere disattivata, la cronologia dei messaggi sarà visibile solo ai membri della stanza da quando sono stati invitati o da quando sono entrati nella stanza. Nessuno, oltre ai membri della stanza, sarà in grado di leggere i messaggi. Ciò potrebbe impedire ai bot e ai bridge di funzionare correttamente. @@ -15,19 +22,25 @@ Non consigliamo di attivare la crittografia per le stanze che chiunque può trov "Attiva la crittografia end-to-end" "Chiunque può partecipare." "Chiunque" + "Scegli quali membri dello spazio possono accedere a questa stanza senza invito.%1$s" + "Gestisci gli spazi" "Solo le persone invitate possono entrare." "Solo su invito" "Accesso" + "Chiunque si trovi in ​​spazi autorizzati può partecipare." + "Chiunque in %1$s può partecipare." + "Membri dello spazio" "Gli spazi non sono attualmente supportati" "Per renderlo visibile nell\'elenco pubblico, avrai bisogno di un indirizzo." "Indirizzo" "Consenti la ricerca di questa stanza effettuando una ricerca nell\'elenco delle stanze pubbliche di %1$s" "Consenti di essere trovato effettuando una ricerca nell\'elenco pubblico." "Visibile nell\'elenco pubblico" - "Chiunque" + "Chiunque (la cronologia è pubblica)" + "Le modifiche non interesseranno i messaggi passati, ma solo quelli nuovi. %1$s" "Chi può leggere la cronologia messaggi" - "Solo membri da quando sono stati invitati" - "Solo membri da dopo aver selezionato questa opzione" + "Members invited" + "Members (cronologia completa)" "Gli indirizzi delle stanze sono modi per trovare e accedervi. In questo modo puoi anche condividere facilmente la tua stanze con altri. Puoi scegliere di pubblicare la tua stanza nell\'elenco delle stanza pubbliche dell\'homeserver." "Pubblicazione della stanza" diff --git a/features/securityandprivacy/impl/src/main/res/values-ja/translations.xml b/features/securityandprivacy/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..110d19a325 --- /dev/null +++ b/features/securityandprivacy/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,50 @@ + + + "公開ディレクトリで自分を見つけられるようにするには、アドレスが必要です。" + "アドレスの編集" + "招待なしでルームへの参加が可能なスペース" + "スペースを管理" + "(不明なスペース)" + "参加していない他のスペース" + "あなたのスペース" + "アドレスを追加" + "認証済みのスペースに所属するユーザーのみが参加できます。それ以外のユーザーは参加へのリクエストが必要です。" + "参加のリクエストが必須です。" + "参加をリクエスト" + "%1$s に所属するユーザーのみが参加できます。それ以外のユーザーは参加のリクエストが必要です。" + "暗号化を有効にする" + "暗号化が有効のルームを再び無効化することはできません。過去のメッセージの参照は、ユーザーが招待された、あるいは参加した以降に投稿された内容に限定されます。 +ルームのメンバー以外がメッセージを確認することはできないため、bot やブリッジのサービスが正常に動作しない可能性があります。 +公開スペースを暗号化することは一般に推奨されません。" + "暗号化を有効にしますか?" + "一度有効にすると元に戻すことはできません。" + "暗号化" + "エンドツーエンド暗号化を有効にする" + "誰でも参加できます" + "全員" + "招待無しで参加できるユーザーが所属するルームを選択してください。%1$s" + "スペースを管理" + "招待されたユーザーのみ参加できます。" + "招待制" + "アクセス" + "認証済みのスペースに所属するすべてのユーザーが参加できます。" + "%1$s に所属するすべてのユーザーが参加できます。" + "スペースのメンバー" + "スペースは現在対応していません。" + "公開ディレクトリで自分を見つけられるようにするには、アドレスが必要です。" + "アドレス" + "%1$s の公開ルームの検索結果に、このルームを表示します" + "公開ディレクトリの検索結果に表示" + "公開ディレクトリに表示" + "全員(履歴を公開)" + "過去のメッセージに変更は適用されません。新規のメッセージにのみ適用されます。%1$s" + "履歴を表示するユーザー" + "招待済みのユーザー" + "ユーザー (すべての履歴)" + "ルームアドレスはルームの検索やアクセスに役立ち、他のユーザーにルームを簡単に共有できます。 +ホームサーバーの公開ディレクトリにルームを表示するかを設定できます。" + "ルームの公開" + "ルームアドレスはルームの検索やアクセスに役立ち、他のユーザーにルームを簡単に共有できます。" + "視認性" + "セキュリティとプライバシー" + diff --git a/features/securityandprivacy/impl/src/main/res/values-vi/translations.xml b/features/securityandprivacy/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..c400036307 --- /dev/null +++ b/features/securityandprivacy/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,11 @@ + + + "Bạn cần một địa chỉ để hiển thị trong danh bạ công khai." + "Chỉnh sửa địa chỉ" + "Sau khi được kích hoạt, mã hóa cho một phòng chat không thể tắt được. Lịch sử tin nhắn chỉ hiển thị cho các thành viên phòng chat kể từ khi họ được mời hoặc kể từ khi họ tham gia phòng chat. +Không ai ngoài các thành viên phòng chat có thể đọc tin nhắn. Điều này có thể ngăn chặn bot và các thiết bị kết nối hoạt động đúng cách. +Chúng tôi không khuyến khích bật mã hóa cho các phòng chat mà bất kỳ ai cũng có thể tìm thấy và tham gia." + "Mã hóa" + "Thành viên không gian" + "Bạn cần một địa chỉ để hiển thị trong danh bạ công khai." + diff --git a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenterTest.kt b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenterTest.kt index d2844c79f0..34b4222053 100644 --- a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenterTest.kt +++ b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenterTest.kt @@ -416,11 +416,6 @@ class SecurityAndPrivacyPresenterTest { val presenter = createSecurityAndPrivacyPresenter( room = room, matrixClient = client, - featureFlagService = FakeFeatureFlagService( - initialState = mapOf( - FeatureFlags.SpaceSettings.key to true, - ) - ) ) presenter.test { skipItems(1) @@ -461,11 +456,6 @@ class SecurityAndPrivacyPresenterTest { room = room, navigator = navigator, matrixClient = client, - featureFlagService = FakeFeatureFlagService( - initialState = mapOf( - FeatureFlags.SpaceSettings.key to true, - ) - ) ) presenter.test { skipItems(1) @@ -587,7 +577,6 @@ class SecurityAndPrivacyPresenterTest { featureFlagService = FakeFeatureFlagService( initialState = mapOf( FeatureFlags.Knock.key to true, - FeatureFlags.SpaceSettings.key to true, ) ) ) @@ -633,7 +622,6 @@ class SecurityAndPrivacyPresenterTest { featureFlagService = FakeFeatureFlagService( initialState = mapOf( FeatureFlags.Knock.key to true, - FeatureFlags.SpaceSettings.key to true, ) ) ) @@ -859,9 +847,6 @@ class SecurityAndPrivacyPresenterTest { val presenter = createSecurityAndPrivacyPresenter( room = room, matrixClient = client, - featureFlagService = FakeFeatureFlagService( - initialState = mapOf(FeatureFlags.SpaceSettings.key to true) - ) ) presenter.test { skipItems(1) @@ -901,9 +886,6 @@ class SecurityAndPrivacyPresenterTest { val presenter = createSecurityAndPrivacyPresenter( room = room, matrixClient = client, - featureFlagService = FakeFeatureFlagService( - initialState = mapOf(FeatureFlags.SpaceSettings.key to true) - ) ) presenter.test { skipItems(1) @@ -975,7 +957,6 @@ class SecurityAndPrivacyPresenterTest { featureFlagService = FakeFeatureFlagService( initialState = mapOf( FeatureFlags.Knock.key to true, - FeatureFlags.SpaceSettings.key to true, ) ) ) diff --git a/features/signedout/impl/src/main/res/values-ja/translations.xml b/features/signedout/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..27fc2a1d4b --- /dev/null +++ b/features/signedout/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,8 @@ + + + "他のセッションでパスワードを変更しました" + "このセッションは他のセッションより削除されました" + "サーバー管理者があなたのアクセスを無効にしました" + "以下のいずれかの理由によってサインアウトされました。%s を引き続き使用するには再度サインインしてください。" + "サインアウトしました" + diff --git a/features/signedout/impl/src/main/res/values-vi/translations.xml b/features/signedout/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..320f4a8731 --- /dev/null +++ b/features/signedout/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,8 @@ + + + "Bạn đã thay đổi mật khẩu trên phiên đăng nhập khác." + "Bạn đã xóa phiên đăng nhập này từ một phiên khác." + "Quản trị viên của máy chủ đã thu hồi quyền truy cập của bạn." + "Bạn có thể đã bị đăng xuất vì một trong những lý do được liệt kê bên dưới. Vui lòng đăng nhập lại để tiếp tục sử dụng %s ." + "Bạn đã đăng xuất." + diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt index 807d139e6a..5ce6575493 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt @@ -29,8 +29,6 @@ import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.mapState import io.element.android.libraries.di.annotations.SessionCoroutineScope -import io.element.android.libraries.featureflag.api.FeatureFlagService -import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias @@ -66,7 +64,6 @@ class SpacePresenter( private val joinRoom: JoinRoom, private val acceptDeclineInvitePresenter: Presenter, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, - private val featureFlagService: FeatureFlagService, private val spaceService: SpaceService, ) : Presenter { private var children by mutableStateOf>(persistentListOf()) @@ -99,16 +96,13 @@ class SpacePresenter( val permissions by room.permissionsAsState(SpacePermissions.DEFAULT) { perms -> perms.spacePermissions() } - val isSpaceSettingsEnabled by remember { - featureFlagService.isFeatureEnabledFlow(FeatureFlags.SpaceSettings) - }.collectAsState(false) val roomInfo by room.roomInfoFlow.collectAsState() val canAccessSpaceSettings by remember { - derivedStateOf { isSpaceSettingsEnabled && permissions.settingsPermissions.hasAny(roomInfo.joinRule) } + derivedStateOf { permissions.settingsPermissions.hasAny(roomInfo.joinRule) } } val canEditSpaceGraph by remember { - derivedStateOf { isSpaceSettingsEnabled && permissions.canEditSpaceGraph } + derivedStateOf { permissions.canEditSpaceGraph } } val (joinActions, setJoinActions) = remember { mutableStateOf(emptyMap>()) } diff --git a/features/space/impl/src/main/res/values-be/translations.xml b/features/space/impl/src/main/res/values-be/translations.xml index dbd39abf09..1a3adafd72 100644 --- a/features/space/impl/src/main/res/values-be/translations.xml +++ b/features/space/impl/src/main/res/values-be/translations.xml @@ -1,4 +1,5 @@ + "Пакінуць прастору" "Ролі і дазволы" diff --git a/features/space/impl/src/main/res/values-it/translations.xml b/features/space/impl/src/main/res/values-it/translations.xml index 62f4787002..20859ee70f 100644 --- a/features/space/impl/src/main/res/values-it/translations.xml +++ b/features/space/impl/src/main/res/values-it/translations.xml @@ -8,10 +8,20 @@ "Seleziona le stanze che desideri abbandonare e di cui non sei l\'unico amministratore:" "Prima di poter uscire, devi assegnare un altro amministratore a questo spazio." + "Sei l\'unico proprietario di %1$s. Devi trasferire la proprietà a qualcun altro prima di andartene." "Non verrai rimosso dalle seguenti stanze perché sei l\'unico amministratore:" "Uscire da %1$s?" "Sei l\'unico amministratore di %1$s" + "Trasferisci proprietà" + "Stanza" + "L\'aggiunta di una stanza non influirà sull\'accesso alla stessa. Per modificare l\'accesso, vai su Impostazioni stanza > Sicurezza & privacy." + "Aggiungi la tua prima stanza" "Visualizza membri" + "La rimozione di una stanza non influirà sull\'accesso alla stessa. Per modificare l\'accesso, vai su Informazioni sulla stanza > Sicurezza & privacy" + + "Rimuovi %1$d stanza da %2$s" + "Rimuovi %1$d stanze da %2$s" + "Esci dallo spazio" "Ruoli e autorizzazioni" "Sicurezza e privacy" diff --git a/features/space/impl/src/main/res/values-ja/translations.xml b/features/space/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..e00d045038 --- /dev/null +++ b/features/space/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,26 @@ + + + "所有者を選択" + "%1$s (管理者)" + + "%1$d 個のルームとスペースを退出" + + "あなたが唯一の管理者であるルーム以外を選択してください。" + "このスペースを退出する前に、新しく管理者を設定してください。" + "あなたは %1$s の唯一の所有者です。退出する前に所有権を譲与する必要があります。" + "あなたが唯一の管理者であるため、以下のルームからは退出しません。" + "%1$s を退出しますか?" + "あなたが唯一の %1$s の管理者です。" + "所有権の譲渡" + "ルーム" + "ルームの追加はルームへのアクセスに影響しません。アクセスの設定は、ルームの設定 > セキュリティーとプライバシー から変更できます。" + "最初のルームを追加しましょう" + "メンバーを表示" + "ルームの削除はルームへのアクセスに影響しません。アクセスの設定は、ルームの設定 > セキュリティーとプライバシー から変更できます。" + + "%2$s から%1$d 個のルームを削除" + + "スペースを退出" + "役割と権限" + "セキュリティとプライバシー" + diff --git a/features/space/impl/src/main/res/values-sv/translations.xml b/features/space/impl/src/main/res/values-sv/translations.xml index 794d7db626..f4427c384d 100644 --- a/features/space/impl/src/main/res/values-sv/translations.xml +++ b/features/space/impl/src/main/res/values-sv/translations.xml @@ -1,6 +1,7 @@ "Välj ägare" + "Välj de rum du vill lämna och som du inte är ensam administratör för:" "Lämna %1$s?" "Lämna utrymmet" "Roller och behörigheter" diff --git a/features/space/impl/src/main/res/values-vi/translations.xml b/features/space/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..a19747d029 --- /dev/null +++ b/features/space/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,5 @@ + + + "Rời space" + "Vai trò và quyền hạn" + diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt index 1d38e2e0f7..ba4e10a447 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt @@ -11,6 +11,9 @@ package io.element.android.features.space.impl.root import com.google.common.truth.Truth.assertThat +import com.google.testing.junit.testparameterinjector.KotlinTestParameters.namedTestValues +import com.google.testing.junit.testparameterinjector.TestParameter +import com.google.testing.junit.testparameterinjector.TestParameterInjector import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState @@ -19,8 +22,6 @@ import io.element.android.features.invite.api.toInviteData import io.element.android.features.invite.test.InMemorySeenInvitesStore import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.featureflag.api.FeatureFlags -import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomIdOrAlias @@ -51,8 +52,10 @@ import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Test +import org.junit.runner.RunWith import im.vector.app.features.analytics.plan.JoinedRoom as AnalyticsJoinedRoom +@RunWith(TestParameterInjector::class) class SpacePresenterTest { @Test fun `present - initial state`() = runTest { @@ -75,16 +78,7 @@ class SpacePresenterTest { } @Test - fun `present - canAccessSpaceSettings false when space settings ff is enabled but no permissions`() = runTest { - val presenter = createSpacePresenter(spaceSettingsEnabled = true) - presenter.test { - val state = awaitItem() - assertThat(state.canAccessSpaceSettings).isFalse() - } - } - - @Test - fun `present - canAccessSpaceSettings true when space settings ff is enabled and has permissions`() = runTest { + fun `present - canAccessSpaceSettings true when has permissions`() = runTest { val room = FakeBaseRoom( roomPermissions = FakeRoomPermissions( canSendState = { true } @@ -92,7 +86,6 @@ class SpacePresenterTest { ) val presenter = createSpacePresenter( room = room, - spaceSettingsEnabled = true, ) presenter.test { skipItems(1) @@ -271,21 +264,11 @@ class SpacePresenterTest { } @Test - fun `present - accept invite is transmitted to acceptDeclineInviteState`() { - `invite action is transmitted to acceptDeclineInviteState`( - acceptInvite = true, - ) - } - - @Test - fun `present - decline invite is transmitted to acceptDeclineInviteState`() { - `invite action is transmitted to acceptDeclineInviteState`( - acceptInvite = false, - ) - } - - private fun `invite action is transmitted to acceptDeclineInviteState`( - acceptInvite: Boolean, + fun `present - invite action is transmitted to acceptDeclineInviteState`( + @TestParameter acceptInvite: Boolean = namedTestValues( + "accept" to true, + "decline" to false, + ), ) = runTest { val eventRecorder = EventsRecorder() val anInvitedRoom = aSpaceRoom( @@ -627,7 +610,6 @@ class SpacePresenterTest { lambda = { _, _, _ -> Result.success(Unit) }, ), acceptDeclineInvitePresenter: Presenter = Presenter { anAcceptDeclineInviteState() }, - spaceSettingsEnabled: Boolean = false, spaceService: FakeSpaceService = FakeSpaceService(), ): SpacePresenter { return SpacePresenter( @@ -638,11 +620,6 @@ class SpacePresenterTest { joinRoom = joinRoom, acceptDeclineInvitePresenter = acceptDeclineInvitePresenter, sessionCoroutineScope = this, - featureFlagService = FakeFeatureFlagService( - initialState = mapOf( - FeatureFlags.SpaceSettings.key to spaceSettingsEnabled, - ) - ), spaceService = spaceService, ) } diff --git a/features/startchat/api/src/main/kotlin/io/element/android/features/startchat/api/ConfirmingStartDmWithMatrixUser.kt b/features/startchat/api/src/main/kotlin/io/element/android/features/startchat/api/ConfirmingStartDmWithMatrixUser.kt index 5bf015c0f0..059002d983 100644 --- a/features/startchat/api/src/main/kotlin/io/element/android/features/startchat/api/ConfirmingStartDmWithMatrixUser.kt +++ b/features/startchat/api/src/main/kotlin/io/element/android/features/startchat/api/ConfirmingStartDmWithMatrixUser.kt @@ -13,4 +13,5 @@ import io.element.android.libraries.matrix.api.user.MatrixUser data class ConfirmingStartDmWithMatrixUser( val matrixUser: MatrixUser, + val isUserIdentityUnknown: Boolean, ) : AsyncAction.Confirming diff --git a/features/startchat/impl/build.gradle.kts b/features/startchat/impl/build.gradle.kts index 6ab1a361e9..805dcc742b 100644 --- a/features/startchat/impl/build.gradle.kts +++ b/features/startchat/impl/build.gradle.kts @@ -38,7 +38,7 @@ dependencies { implementation(projects.libraries.mediapickers.api) implementation(projects.libraries.mediaupload.api) implementation(projects.libraries.permissions.api) - implementation(projects.libraries.usersearch.impl) + implementation(projects.libraries.usersearch.api) implementation(projects.services.analytics.api) implementation(libs.coil.compose) implementation(projects.libraries.featureflag.api) diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/DefaultStartDMAction.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/DefaultStartDMAction.kt index a484fe2e72..3bfbd1ca18 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/DefaultStartDMAction.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/DefaultStartDMAction.kt @@ -15,6 +15,8 @@ import io.element.android.features.startchat.api.ConfirmingStartDmWithMatrixUser import io.element.android.features.startchat.api.StartDMAction import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.StartDMResult @@ -26,6 +28,7 @@ import io.element.android.services.analytics.api.AnalyticsService class DefaultStartDMAction( private val matrixClient: MatrixClient, private val analyticsService: AnalyticsService, + private val featureFlagService: FeatureFlagService, ) : StartDMAction { override suspend fun execute( matrixUser: MatrixUser, @@ -44,7 +47,11 @@ class DefaultStartDMAction( actionState.value = AsyncAction.Failure(result.throwable) } StartDMResult.DmDoesNotExist -> { - actionState.value = ConfirmingStartDmWithMatrixUser(matrixUser = matrixUser) + val identityState = matrixClient.encryptionService.getUserIdentity(matrixUser.userId, fallbackToServer = false).getOrNull() + actionState.value = ConfirmingStartDmWithMatrixUser( + matrixUser = matrixUser, + isUserIdentityUnknown = featureFlagService.isFeatureEnabled(FeatureFlags.EnableKeyShareOnInvite) && identityState == null + ) } } } diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenter.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenter.kt index e176f202ad..7afbe19c3d 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenter.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenter.kt @@ -58,6 +58,8 @@ class StartChatPresenter( featureFlagService.isFeatureEnabledFlow(FeatureFlags.RoomDirectorySearch) }.collectAsState(initial = false) + val enableKeyShareOnInvite = featureFlagService.isFeatureEnabledFlow(FeatureFlags.EnableKeyShareOnInvite).collectAsState(false) + fun handleEvent(event: StartChatEvents) { when (event) { is StartChatEvents.StartDM -> localCoroutineScope.launch { @@ -76,6 +78,7 @@ class StartChatPresenter( userListState = userListState, startDmAction = startDmActionState.value, isRoomDirectorySearchEnabled = isRoomDirectorySearchEnabled, + enableKeyShareOnInvite = enableKeyShareOnInvite.value, eventSink = ::handleEvent, ) } diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatState.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatState.kt index 65f977d3e3..989a5b8d20 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatState.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatState.kt @@ -17,5 +17,6 @@ data class StartChatState( val userListState: UserListState, val startDmAction: AsyncAction, val isRoomDirectorySearchEnabled: Boolean, + val enableKeyShareOnInvite: Boolean, val eventSink: (StartChatEvents) -> Unit, ) diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatStateProvider.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatStateProvider.kt index 448ad1a80a..17d83a9e11 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatStateProvider.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatStateProvider.kt @@ -16,6 +16,7 @@ import io.element.android.features.startchat.impl.userlist.aUserListState import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.usersearch.api.UserSearchResult import kotlinx.collections.immutable.persistentListOf @@ -52,7 +53,7 @@ open class StartChatStateProvider : PreviewParameterProvider { ) ), aCreateRoomRootState( - startDmAction = ConfirmingStartDmWithMatrixUser(aMatrixUser()), + startDmAction = aConfirmingStartDmWithMatrixUser() ), aCreateRoomRootState( isRoomDirectorySearchEnabled = true, @@ -60,6 +61,16 @@ open class StartChatStateProvider : PreviewParameterProvider { ) } +fun aConfirmingStartDmWithMatrixUser( + matrixUser: MatrixUser = aMatrixUser(), + isUserIdentityUnknown: Boolean = false +): ConfirmingStartDmWithMatrixUser { + return ConfirmingStartDmWithMatrixUser( + matrixUser, + isUserIdentityUnknown + ) +} + fun aCreateRoomRootState( applicationName: String = "Element X Preview", userListState: UserListState = aUserListState(), @@ -71,5 +82,6 @@ fun aCreateRoomRootState( userListState = userListState, startDmAction = startDmAction, isRoomDirectorySearchEnabled = isRoomDirectorySearchEnabled, + enableKeyShareOnInvite = false, eventSink = eventSink, ) diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatView.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatView.kt index 0b8da1bd94..28bf52549e 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatView.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatView.kt @@ -130,6 +130,8 @@ fun StartChatView( if (data is ConfirmingStartDmWithMatrixUser) { CreateDmConfirmationBottomSheet( matrixUser = data.matrixUser, + enableKeyShareOnInvite = state.enableKeyShareOnInvite, + isUserIdentityUnknown = data.isUserIdentityUnknown, onSendInvite = { state.eventSink(StartChatEvents.StartDM(data.matrixUser)) }, diff --git a/features/startchat/impl/src/main/res/values-ja/translations.xml b/features/startchat/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..ed490becfc --- /dev/null +++ b/features/startchat/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,12 @@ + + + "新しいルーム" + "ルーム階層" + "新しい会話を開始する際に問題が発生しました。" + "アドレスからルームに参加" + "有効なアドレスではありません" + "入力してください…" + "ルームが見つかりました" + "ルームが見つかりません" + "例) #room-name:matrix.org" + diff --git a/features/startchat/impl/src/main/res/values-vi/translations.xml b/features/startchat/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..765b5bc302 --- /dev/null +++ b/features/startchat/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,6 @@ + + + "Phòng mới" + "Danh sách phòng" + "Đã xảy ra lỗi khi cố gắng bắt đầu cuộc trò chuyện" + diff --git a/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/DefaultStartDMActionTest.kt b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/DefaultStartDMActionTest.kt index 122775f2cc..88b935e47d 100644 --- a/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/DefaultStartDMActionTest.kt +++ b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/DefaultStartDMActionTest.kt @@ -13,14 +13,21 @@ import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.CreatedRoom import io.element.android.features.startchat.api.ConfirmingStartDmWithMatrixUser import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.tests.testutils.lambda.lambdaRecorder import kotlinx.coroutines.test.runTest import org.junit.Test @@ -67,7 +74,12 @@ class DefaultStartDMActionTest { @Test fun `when dm is not found, and createIfDmDoesNotExist is false, assert dm is not created and state is updated to confirmation state`() = runTest { - val matrixClient = FakeMatrixClient().apply { + val encryptionService = FakeEncryptionService( + getUserIdentityResult = { Result.success(null) } + ) + val matrixClient = FakeMatrixClient( + encryptionService = encryptionService + ).apply { givenFindDmResult(Result.success(null)) givenCreateDmResult(Result.success(A_ROOM_ID)) } @@ -76,7 +88,7 @@ class DefaultStartDMActionTest { val state = mutableStateOf>(AsyncAction.Uninitialized) val matrixUser = aMatrixUser() action.execute(matrixUser, false, state) - assertThat(state.value).isEqualTo(ConfirmingStartDmWithMatrixUser(matrixUser)) + assertThat(state.value).isEqualTo(ConfirmingStartDmWithMatrixUser(matrixUser, isUserIdentityUnknown = false)) assertThat(analyticsService.capturedEvents).isEmpty() } @@ -94,13 +106,38 @@ class DefaultStartDMActionTest { assertThat(analyticsService.capturedEvents).isEmpty() } + @Test + fun `when history sharing enabled, user identity fetched and identity unknown`() = runTest { + val getUserIdentityResult = lambdaRecorder> { _ -> Result.success(null) } + val encryptionService = FakeEncryptionService(getUserIdentityResult = getUserIdentityResult) + val matrixClient = FakeMatrixClient(encryptionService = encryptionService).apply { + givenFindDmResult(Result.success(null)) + } + val featureFlagService = FakeFeatureFlagService().apply { + setFeatureEnabled(FeatureFlags.EnableKeyShareOnInvite, true) + } + + val action = createStartDMAction( + matrixClient = matrixClient, + featureFlagService = featureFlagService + ) + val state = mutableStateOf>(AsyncAction.Uninitialized) + + action.execute(aMatrixUser(), false, state) + + assertThat(getUserIdentityResult.assertions().isCalledOnce()) + assertThat(state.value).isEqualTo(ConfirmingStartDmWithMatrixUser(aMatrixUser(), isUserIdentityUnknown = true)) + } + private fun createStartDMAction( matrixClient: MatrixClient = FakeMatrixClient(), analyticsService: AnalyticsService = FakeAnalyticsService(), + featureFlagService: FeatureFlagService = FakeFeatureFlagService() ): DefaultStartDMAction { return DefaultStartDMAction( matrixClient = matrixClient, analyticsService = analyticsService, + featureFlagService = featureFlagService, ) } } diff --git a/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenterTest.kt b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenterTest.kt index 7c209d9052..2bc15e989d 100644 --- a/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenterTest.kt +++ b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenterTest.kt @@ -102,7 +102,7 @@ class StartChatPresenterTest { @Test fun `present - start DM action confirmation scenario - cancel`() = runTest { val matrixUser = MatrixUser(UserId("@name:domain")) - val startDMConfirmationResult = ConfirmingStartDmWithMatrixUser(matrixUser) + val startDMConfirmationResult = ConfirmingStartDmWithMatrixUser(matrixUser, isUserIdentityUnknown = false) val executeResult = lambdaRecorder>, Unit> { _, _, actionState -> actionState.value = startDMConfirmationResult } @@ -130,7 +130,7 @@ class StartChatPresenterTest { @Test fun `present - start DM action confirmation scenario - confirm`() = runTest { val matrixUser = MatrixUser(UserId("@name:domain")) - val startDMConfirmationResult = ConfirmingStartDmWithMatrixUser(matrixUser) + val startDMConfirmationResult = ConfirmingStartDmWithMatrixUser(matrixUser, isUserIdentityUnknown = false) val executeResult = lambdaRecorder>, Unit> { _, _, actionState -> actionState.value = startDMConfirmationResult } diff --git a/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileState.kt b/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileState.kt index 0e0016ee14..e2a309c17f 100644 --- a/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileState.kt +++ b/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileState.kt @@ -26,6 +26,7 @@ data class UserProfileState( val dmRoomId: RoomId?, val canCall: Boolean, val snackbarMessage: SnackbarMessage?, + val enableKeyShareOnInvite: Boolean, val eventSink: (UserProfileEvents) -> Unit ) { enum class ConfirmationDialog { diff --git a/features/userprofile/impl/build.gradle.kts b/features/userprofile/impl/build.gradle.kts index 0b65441cc3..3e68fb2b9c 100644 --- a/features/userprofile/impl/build.gradle.kts +++ b/features/userprofile/impl/build.gradle.kts @@ -34,6 +34,7 @@ dependencies { implementation(projects.libraries.uiStrings) implementation(projects.libraries.androidutils) implementation(projects.libraries.mediaviewer.api) + implementation(projects.libraries.featureflag.api) implementation(projects.features.call.api) implementation(projects.features.enterprise.api) implementation(projects.features.verifysession.api) @@ -46,6 +47,7 @@ dependencies { testCommonDependencies(libs, true) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.mediaviewer.test) + testImplementation(projects.libraries.featureflag.test) testImplementation(projects.features.call.test) testImplementation(projects.features.verifysession.test) testImplementation(projects.features.startchat.test) diff --git a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt index 7e09a03ec3..a451d86b70 100644 --- a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt +++ b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState @@ -31,6 +32,8 @@ import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.bool.orFalse +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId @@ -50,6 +53,7 @@ class UserProfilePresenter( private val client: MatrixClient, private val startDMAction: StartDMAction, private val sessionEnterpriseService: SessionEnterpriseService, + private val featureFlagService: FeatureFlagService, ) : Presenter { @AssistedFactory interface Factory { @@ -101,6 +105,8 @@ class UserProfilePresenter( } val userProfile by produceState(null) { value = client.getProfile(userId).getOrNull() } + val enableKeyShareOnInvite = featureFlagService.isFeatureEnabledFlow(FeatureFlags.EnableKeyShareOnInvite).collectAsState(false) + fun handleEvent(event: UserProfileEvents) { when (event) { is UserProfileEvents.BlockUser -> { @@ -153,6 +159,7 @@ class UserProfilePresenter( dmRoomId = dmRoomId, canCall = canCall, snackbarMessage = null, + enableKeyShareOnInvite = enableKeyShareOnInvite.value, eventSink = ::handleEvent, ) } diff --git a/features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/UserProfilePresenterTest.kt b/features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/UserProfilePresenterTest.kt index 511effe750..1325b46bc0 100644 --- a/features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/UserProfilePresenterTest.kt +++ b/features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/UserProfilePresenterTest.kt @@ -24,6 +24,7 @@ import io.element.android.features.userprofile.api.UserProfileVerificationState import io.element.android.features.userprofile.impl.root.UserProfilePresenter import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId @@ -324,7 +325,7 @@ class UserProfilePresenterTest { @Test fun `present - start DM action confirmation scenario - cancel`() = runTest { val matrixUser = MatrixUser(UserId("@alice:server.org")) - val startDMConfirmationResult = ConfirmingStartDmWithMatrixUser(matrixUser) + val startDMConfirmationResult = ConfirmingStartDmWithMatrixUser(matrixUser, false) val executeResult = lambdaRecorder>, Unit> { _, _, actionState -> actionState.value = startDMConfirmationResult } @@ -354,7 +355,7 @@ class UserProfilePresenterTest { @Test fun `present - start DM action confirmation scenario - confirm`() = runTest { val matrixUser = MatrixUser(UserId("@alice:server.org")) - val startDMConfirmationResult = ConfirmingStartDmWithMatrixUser(matrixUser) + val startDMConfirmationResult = ConfirmingStartDmWithMatrixUser(matrixUser, false) val executeResult = lambdaRecorder>, Unit> { _, _, actionState -> actionState.value = startDMConfirmationResult } @@ -414,6 +415,7 @@ class UserProfilePresenterTest { sessionEnterpriseService = FakeSessionEnterpriseService( isElementCallAvailableResult = { isElementCallAvailable }, ), + featureFlagService = FakeFeatureFlagService() ) } } diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileStateProvider.kt b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileStateProvider.kt index 49a2fee4b5..a4bbcd6aa4 100644 --- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileStateProvider.kt +++ b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileStateProvider.kt @@ -31,7 +31,7 @@ open class UserProfileStateProvider : PreviewParameterProvider aUserProfileState(isBlocked = AsyncData.Loading(true), verificationState = UserProfileVerificationState.UNKNOWN), aUserProfileState(startDmActionState = AsyncAction.Loading), aUserProfileState(canCall = true), - aUserProfileState(startDmActionState = ConfirmingStartDmWithMatrixUser(aMatrixUser())), + aUserProfileState(startDmActionState = ConfirmingStartDmWithMatrixUser(aMatrixUser(), isUserIdentityUnknown = false)), aUserProfileState(verificationState = UserProfileVerificationState.VERIFICATION_VIOLATION), ) } @@ -61,5 +61,6 @@ fun aUserProfileState( dmRoomId = dmRoomId, canCall = canCall, snackbarMessage = snackbarMessage, + enableKeyShareOnInvite = false, eventSink = eventSink, ) diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt index 380bb006ab..34f992f77d 100644 --- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt +++ b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt @@ -114,6 +114,8 @@ fun UserProfileView( if (data is ConfirmingStartDmWithMatrixUser) { CreateDmConfirmationBottomSheet( matrixUser = data.matrixUser, + enableKeyShareOnInvite = state.enableKeyShareOnInvite, + isUserIdentityUnknown = data.isUserIdentityUnknown, onSendInvite = { state.eventSink(UserProfileEvents.StartDM) }, diff --git a/features/userprofile/shared/src/main/res/values-ja/translations.xml b/features/userprofile/shared/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..33ba6b6a85 --- /dev/null +++ b/features/userprofile/shared/src/main/res/values-ja/translations.xml @@ -0,0 +1,19 @@ + + + "ブロック" + "ブロックしたユーザーのメッセージは非表示になり、新しく送信することもできません。ブロックはいつでも解除することができます。" + "ユーザーをブロック" + "ブロックを解除" + "すべてのメッセージが再表示されます。" + "ユーザーのブロックを解除" + "ブロック" + "ブロックしたユーザーのメッセージは非表示になり、新しく送信することもできません。ブロックはいつでも解除することができます。" + "ユーザーをブロック" + "プロフィール" + "ブロックを解除" + "すべてのメッセージが再表示されます。" + "ユーザーのブロックを解除" + "このユーザーを検証するにはWeb版アプリを使用してください。" + "%1$s を検証" + "新しい会話を開始する際に問題が発生しました。" + diff --git a/features/userprofile/shared/src/main/res/values-vi/translations.xml b/features/userprofile/shared/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..122d8a2153 --- /dev/null +++ b/features/userprofile/shared/src/main/res/values-vi/translations.xml @@ -0,0 +1,16 @@ + + + "Chặn" + "Người dùng bị chặn sẽ không thể gửi tin nhắn cho bạn và tất cả tin nhắn của họ sẽ bị ẩn. Bạn có thể bỏ chặn họ bất cứ lúc nào." + "Chặn người dùng" + "Bỏ chặn" + "Bạn sẽ có thể xem lại tất cả tin nhắn từ họ." + "Bỏ chặn người dùng" + "Chặn" + "Người dùng bị chặn sẽ không thể gửi tin nhắn cho bạn và tất cả tin nhắn của họ sẽ bị ẩn. Bạn có thể bỏ chặn họ bất cứ lúc nào." + "Chặn người dùng" + "Bỏ chặn" + "Bạn sẽ có thể xem lại tất cả tin nhắn từ họ." + "Bỏ chặn người dùng" + "Đã xảy ra lỗi khi cố gắng bắt đầu cuộc trò chuyện" + diff --git a/features/userprofile/shared/src/main/res/values-zh/translations.xml b/features/userprofile/shared/src/main/res/values-zh/translations.xml index b1b37bdbdc..38c4e3e8eb 100644 --- a/features/userprofile/shared/src/main/res/values-zh/translations.xml +++ b/features/userprofile/shared/src/main/res/values-zh/translations.xml @@ -1,18 +1,18 @@ - "封禁" - "被封禁的用户无法给你发消息,并且他们的消息会被隐藏。你可以随时解封。" - "封禁用户" - "解封" + "屏蔽" + "被屏蔽的用户无法给你发消息,并且他们的消息会被隐藏。你可以随时解除屏蔽。" + "屏蔽用户" + "解除屏蔽" "可以重新接收他们的消息。" - "解封用户" - "封禁" - "被封禁的用户无法给你发消息,并且他们的消息会被隐藏。你可以随时解封。" - "封禁用户" + "解除屏蔽用户" + "屏蔽" + "被屏蔽的用户无法给你发消息,并且他们的消息会被隐藏。你可以随时解除屏蔽。" + "屏蔽用户" "个人资料" - "解封" + "解除屏蔽" "可以重新接收他们的消息。" - "解封用户" + "解除屏蔽用户" "使用 Web 应用程序验证此用户。" "验证 %1$s" "在开始聊天时发生了错误" diff --git a/features/verifysession/impl/src/main/res/values-cs/translations.xml b/features/verifysession/impl/src/main/res/values-cs/translations.xml index 5d0b28bc0a..3c3e68e1f0 100644 --- a/features/verifysession/impl/src/main/res/values-cs/translations.xml +++ b/features/verifysession/impl/src/main/res/values-cs/translations.xml @@ -2,8 +2,8 @@ "Nemůžete potvrdit?" "Vytvoření nového klíče pro obnovení" - "Ověřte toto zařízení a nastavte zabezpečené zasílání zpráv." - "Potvrďte, že jste to vy" + "Vyberte způsob ověření pro nastavení zabezpečeného zasílání zpráv." + "Potvrďte svou digitální identitu" "Použít jiné zařízení" "Použít klíč pro obnovení" "Nyní můžete bezpečně číst nebo odesílat zprávy, a kdokoli, s kým chatujete, může tomuto zařízení důvěřovat." @@ -17,7 +17,7 @@ "Potvrďte, že níže uvedená čísla odpovídají číslům zobrazeným na vaší druhé relaci." "Porovnejte čísla" "Nyní můžete bezpečně číst nebo odesílat zprávy na svém druhém zařízení." - "Nyní můžete důvěřovat identitě tohoto uživatele při odesílání nebo přijímání zpráv." + "Nyní můžete při odesílání nebo přijímání zpráv důvěřovat digitální identitě tohoto uživatele." "Zařízení ověřeno" "Zadejte klíč pro obnovení" "Buď vypršel časový limit požadavku, požadavek byl zamítnut, nebo došlo k nesouladu ověření." @@ -42,7 +42,7 @@ "Otevřete aplikaci na jiném ověřeném zařízení" "Pro větší bezpečnost ověřte tohoto uživatele porovnáním sady emotikonů na svých zařízeních. Proveďte to pomocí důvěryhodného způsobu komunikace." "Ověřte tohoto uživatele?" - "Pro větší bezpečnost chce jiný uživatel ověřit vaši identitu. Zobrazí se vám sada emotikonů k porovnání." + "Z důvodu zvýšené bezpečnosti chce jiný uživatel ověřit vaši digitální identitu. Zobrazí se vám sada emodži, které je třeba porovnat." "Na druhém zařízení byste měli vidět vyskakovací okno. Začněte s ověrením tam." "Spusťte ověření na druhém zařízení" "Spusťte ověření na druhém zařízení" @@ -50,5 +50,5 @@ "Po přijetí budete moci pokračovat v ověřování." "Pro pokračování přijměte požadavek na zahájení ověření v jiné relaci." "Čekání na přijetí žádosti" - "Odhlašování…" + "Odebírání zařízení…" diff --git a/features/verifysession/impl/src/main/res/values-hu/translations.xml b/features/verifysession/impl/src/main/res/values-hu/translations.xml index b7617168ee..666ff26af1 100644 --- a/features/verifysession/impl/src/main/res/values-hu/translations.xml +++ b/features/verifysession/impl/src/main/res/values-hu/translations.xml @@ -50,5 +50,5 @@ "Az elfogadása után folytathatja az ellenőrzést." "A folytatáshoz fogadja el az ellenőrzési folyamat indítási kérését a másik munkamenetében." "Várakozás a kérés elfogadására" - "Kijelentkezés…" + "Eszköz eltávolítása…" diff --git a/features/verifysession/impl/src/main/res/values-it/translations.xml b/features/verifysession/impl/src/main/res/values-it/translations.xml index 5b11c27cbe..2bb22e98f9 100644 --- a/features/verifysession/impl/src/main/res/values-it/translations.xml +++ b/features/verifysession/impl/src/main/res/values-it/translations.xml @@ -2,8 +2,8 @@ "Non puoi confermare?" "Crea una nuova chiave di recupero" - "Verifica questo dispositivo per segnare i tuoi messaggi come sicuri." - "Conferma la tua identità" + "Scegli come effettuare la verifica per configurare la messaggistica sicura." + "Conferma la tua identità digitale" "Usa un altro dispositivo" "Usa la chiave di recupero" "Ora puoi leggere o inviare messaggi in tutta sicurezza e anche chi chatta con te può fidarsi di questo dispositivo." @@ -17,7 +17,7 @@ "Conferma che i numeri seguenti corrispondano a quelli mostrati nell\'altra sessione." "Confronta i numeri" "Ora puoi leggere o inviare messaggi in modo sicuro sul tuo altro dispositivo." - "Ora puoi fidarti dell\'identità di questo utente quando invii o ricevi messaggi." + "Ora puoi fidarti dell\'identità digitale di questo utente quando invii o ricevi messaggi." "Dispositivo verificato" "Inserisci la chiave di recupero" "La richiesta è scaduta, è stata rifiutata o c\'è stata una mancata corrispondenza nella verifica." @@ -42,7 +42,7 @@ "Apri l\'app su un altro dispositivo verificato" "Per una maggiore sicurezza, verifica questo utente confrontando un set di emoji sui tuoi dispositivi. A tale scopo, utilizza un metodo di comunicazione affidabile." "Verificare questo utente?" - "Per una maggiore sicurezza, un altro utente desidera verificare la tua identità. Ti verrà mostrato un set di emoji da confrontare." + "Per maggiore sicurezza, un altro utente vuole verificare la tua identità digitale. Ti verrà mostrata una serie di emoji da confrontare." "Dovresti vedere un popup sull\'altro dispositivo. Inizia subito la verifica da lì." "Avvia la verifica sull\'altro dispositivo" "Avvia la verifica sull\'altro dispositivo" @@ -50,5 +50,5 @@ "Una volta accettata, potrai proseguire con la verifica." "Accetta la richiesta di avviare il processo di verifica nell\'altra sessione per continuare." "In attesa di accettare la richiesta" - "Disconnessione in corso…" + "Rimozione del dispositivo…" diff --git a/features/verifysession/impl/src/main/res/values-ja/translations.xml b/features/verifysession/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..72360fda21 --- /dev/null +++ b/features/verifysession/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,54 @@ + + + "認証できませんか?" + "回復鍵を新規作成します" + "安全なメッセージを設定するための検証方法を選択してください。" + "デジタルIDの認証" + "他の端末を使用" + "回復鍵を使用" + "メッセージのやり取りを安全に行えるようになりました。他のユーザーはこの端末を信頼できます。" + "検証済みの端末" + "他の端末を使用" + "一方の端末を待機中…" + "問題が発生しました。リクエストがタイムアウトまたは拒否されました。" + "以下の絵文字が、もう一方の端末の表示と一致することを確認してください。" + "絵文字の比較" + "一方のユーザーの端末上に表示される絵文字と一致することを確認してください。" + "もう一方のセッションと数字が一致することを確認してください。" + "数字を比較してください" + "もう一方の端末でも、安全なメッセージのやり取りが可能になりました。" + "メッセージのやり取りにおいて、このユーザーのデジタルIDを信頼できるようになりました。" + "検証済みの端末" + "回復鍵を入力" + "リクエストがタイムアウトしたか、リクエストの拒否あるいは検証に不一致がありました。" + "暗号化された過去のメッセージを確認するには本人検証が必要です。" + "既存のセッションを使用" + "検証を再試行" + "検証を実行" + "一致を待機中…" + "絵文字の組み合わせを比較してください。" + "絵文字が双方で一致して表示されていることを確認してください。" + "サインイン済み" + "リクエストがタイムアウトしたか、リクエストの拒否あるいは検証に不一致がありました。" + "検証に失敗しました" + "あなたが検証を開始した場合にのみ続行してください。" + "他の端末を検証して過去のメッセージの安全を保ってください。" + "もう一方の端末でも、安全なメッセージのやり取りが可能になりました。" + "検証済みの端末" + "検証をリクエスト済み" + "一致しません" + "一致します" + "検証を開始する前に、他の端末でアプリケーションを開いてください。" + "検証済みの他の端末でアプリケーションを開いてください" + "安全性を高めるために、絵文字の組み合わせを使用してこのユーザーを検証してください。これにより安全にやり取りを行うことができるようになります。" + "このユーザーを検証しますか?" + "安全性を高めるために、相手ユーザーがあなたのデジタルIDを検証することを要求しています。比較用の絵文字の組み合わせが表示されます。" + "一方の端末でポップアップが表示されます。そこから検証を開始してください。" + "一方の端末で検証を開始してください" + "一方の端末で検証を開始してください" + "一方の端末を待機中" + "検証を承認することで続行できます。" + "他の端末で検証リクエストを承認してください。" + "リクエストの承認を待機中" + "削除中…" + diff --git a/features/verifysession/impl/src/main/res/values-ko/translations.xml b/features/verifysession/impl/src/main/res/values-ko/translations.xml index 262a4d3052..493ac0d685 100644 --- a/features/verifysession/impl/src/main/res/values-ko/translations.xml +++ b/features/verifysession/impl/src/main/res/values-ko/translations.xml @@ -17,7 +17,7 @@ "아래 숫자가 다른 세션에 표시된 숫자와 일치하는지 확인하세요." "숫자 비교" "이제 다른 기기에서도 안전하게 메시지를 읽거나 보낼 수 있습니다." - "이제 메시지를 보내거나 받을 때 이 사용자의 신원을 신뢰할 수 있습니다." + "이제 메시지를 주고받을 때 이 사용자의 디지털 신원을 신뢰할 수 있습니다." "기기 검증됨" "복구 키를 입력하세요" "요청이 시간 초과되었거나, 요청이 거부되었거나, 검증 불일치가 발생했습니다." @@ -42,7 +42,7 @@ "다른 검증된 장치에서 앱을 실행하세요" "보안을 강화하려면, 기기에 표시된 이모티콘을 비교하여 이 사용자를 확인하세요. 신뢰할 수 있는 통신 수단을 사용하여 확인하시기 바랍니다." "이 사용자를 검증하시겠습니까?" - "추가 보안 위해 다른 사용자가 귀하의 신원을 확인하고자 합니다. 비교할 이모티콘 세트가 표시됩니다." + "보안 강화를 위해 상대방이 귀하의 디지털 신원을 확인하려고 합니다. 화면에 표시되는 이모지 세트가 서로 일치하는지 비교해 주세요." "다른 기기에 팝업이 표시될 것입니다. 지금 그곳에서 확인을 시작하세요." "다른 장치에서 검증 시작" "다른 장치에서 검증 시작" diff --git a/features/verifysession/impl/src/main/res/values-ru/translations.xml b/features/verifysession/impl/src/main/res/values-ru/translations.xml index 8672229640..91974caa1f 100644 --- a/features/verifysession/impl/src/main/res/values-ru/translations.xml +++ b/features/verifysession/impl/src/main/res/values-ru/translations.xml @@ -2,7 +2,7 @@ "Не можете подтвердить?" "Создайте новый ключ восстановления" - "Подтвердите это устройство, чтобы настроить безопасный обмен сообщениями." + "Выберите способ подтверждения для настройки защищенного обмена сообщениями." "Подтвердите личность" "Использовать другое устройство" "Использовать ключ восстановления" @@ -17,7 +17,7 @@ "Убедитесь, что приведенные ниже числа совпадают с цифрами, показанными в другом сеансе." "Сравните числа" "Теперь вы можете безопасно читать или отправлять сообщения на новом устройстве." - "Теперь вы можете доверять сообщениям этого пользователя." + "Теперь ты можешь доверять цифровой идентичности этого пользователя при отправке или получении сообщений." "Устройство проверено" "Введите ключ восстановления" "Время ожидания подтверждения истекло, запрос был отклонён, или произошла ошибка." @@ -50,5 +50,5 @@ "После принятия запроса вы сможете продолжить проверку." "Чтобы продолжить, примите запрос на запуск процесса подтверждения в другом сеансе." "Ожидание принятия запроса" - "Выполняется выход…" + "Удаление устройства…" diff --git a/features/verifysession/impl/src/main/res/values-sv/translations.xml b/features/verifysession/impl/src/main/res/values-sv/translations.xml index 100f4964da..9f4008d0a4 100644 --- a/features/verifysession/impl/src/main/res/values-sv/translations.xml +++ b/features/verifysession/impl/src/main/res/values-sv/translations.xml @@ -50,5 +50,5 @@ "När det har accepterats kommer du kunna fortsätta verifieringen." "Godkänn begäran om att starta verifieringsprocessen på din andra session för att fortsätta." "Väntar på att acceptera begäran" - "Loggar ut …" + "Tar bort enhet …" diff --git a/features/verifysession/impl/src/main/res/values-vi/translations.xml b/features/verifysession/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..7cb5b916cf --- /dev/null +++ b/features/verifysession/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,31 @@ + + + "Chọn phương thức xác minh để bật nhắn tin bảo mật." + "Xác nhận danh tính kỹ thuật số của bạn" + "Giờ đây bạn có thể đọc và gửi tin nhắn một cách an toàn, và những người bạn trò chuyện cũng có thể tin tưởng thiết bị này." + "Thiết bị được xác thực" + "Đang chờ trên thiết bị khác…" + "Có vẻ như có điều gì đó không đúng. Hoặc yêu cầu đã hết thời gian chờ hoặc yêu cầu đã bị từ chối." + "Hãy xác nhận rằng các biểu tượng cảm xúc bên dưới khớp với các biểu tượng hiển thị trên thiết bị khác của bạn." + "So sánh các biểu tượng cảm xúc" + "Xác nhận rằng các số bên dưới khớp với số hiển thị trên thiết bị đăng nhập khác của bạn." + "So sánh số liệu" + "Giờ đây, bạn có thể đọc hoặc gửi tin nhắn một cách an toàn trên thiết bị khác của mình." + "Thiết bị được xác thực" + "Nhập mã khôi phục." + "Hãy chứng minh đó là bạn để truy cập vào lịch sử tin nhắn đã mã hóa của bạn." + "Mở một phiên hiện có" + "Thử xác minh lại" + "Tôi đã sẵn sàng" + "Đang chờ ghép đôi…" + "So sánh một bộ biểu tượng cảm xúc duy nhất." + "So sánh các biểu tượng cảm xúc riêng biệt, đảm bảo chúng xuất hiện theo cùng một thứ tự." + "Xác minh thất bại" + "Giờ đây, bạn có thể đọc hoặc gửi tin nhắn một cách an toàn trên thiết bị khác của mình." + "Thiết bị được xác thực" + "Chúng không khớp nhau" + "Chúng khớp với nhau" + "Hãy chấp nhận yêu cầu bắt đầu quá trình xác minh trong phiên làm việc khác của bạn để tiếp tục." + "Đang chờ chấp nhận yêu cầu" + "Đang gỡ thiết bị…" + diff --git a/features/verifysession/impl/src/main/res/values-zh/translations.xml b/features/verifysession/impl/src/main/res/values-zh/translations.xml index 54b6de5d68..b48bfe3f87 100644 --- a/features/verifysession/impl/src/main/res/values-zh/translations.xml +++ b/features/verifysession/impl/src/main/res/values-zh/translations.xml @@ -2,8 +2,8 @@ "无法确认?" "创建新的恢复密钥" - "验证此设备以开始安全地收发消息。" - "确认这是你" + "选择验证方式以设置安全的消息传输。" + "确认您的数字身份" "使用其他设备" "使用恢复密钥" "现在,您可以安全地阅读或发送消息,与您聊天的人也会信任此设备。" @@ -17,7 +17,7 @@ "确认以下数字与其他会话中显示的一致。" "比较数字" "现在您可以在其他设备上安全地阅读或发送消息。" - "现在您可以在发送或接收消息时信任该用户的身份。" + "现在您可以在发送或接收消息时信任该用户的数字身份。" "设备已验证" "输入恢复密钥" "要么请求超时,要么请求被拒绝,要么验证不匹配。" @@ -42,7 +42,7 @@ "在另一台验证的设备上打开应用" "为了提高安全性,请通过比较设备上的一组表情符号来验证此用户。通过使用安全方式来做到这一点,如面对面。" "验证此用户?" - "为了提高安全性,另一位用户想要验证您的身份。您将看到一组表情符号供您比较。" + "为了额外的安全性,另一位用户想要验证您的数字身份。您将看到一组表情符号供您比较。" "您应该会在另一台设备上看到一个弹出窗口。现在从那里开始验证。" "在另一台设备上开始验证" "在另一台设备上开始验证" @@ -50,5 +50,5 @@ "一旦被接受,您将能够继续进行验证。" "请在其他会话中接受验证请求。" "等待接受请求" - "正在登出…" + "正在删除设备……" diff --git a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderView.kt b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderView.kt index dce5ffeae3..2a6b4031ef 100644 --- a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderView.kt +++ b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderView.kt @@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Folder import androidx.compose.material.icons.outlined.SubdirectoryArrowLeft import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme @@ -111,7 +112,7 @@ private fun ItemRow( } is Item.Folder -> { ListItem( - leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Folder())), + leadingContent = ListItemContent.Icon(IconSource.Vector(Icons.Outlined.Folder)), headlineContent = { Text( text = item.name, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ca5f42c118..068516bcd1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,7 @@ android_gradle_plugin = "8.13.2" # When updating this, please also update the version in the file ./idea/kotlinc.xml kotlin = "2.3.20" -kotlinpoet = "2.2.0" +kotlinpoet = "2.3.0" ksp = "2.3.6" firebaseAppDistribution = "5.2.1" @@ -17,7 +17,7 @@ constraintlayout = "2.2.1" constraintlayout_compose = "1.1.1" lifecycle = "2.10.0" activity = "1.13.0" -media3 = "1.9.3" +media3 = "1.10.0" camera = "1.5.3" work = "2.11.2" @@ -46,15 +46,15 @@ showkase = "1.0.5" # When upgrading this version, check state restoration still works fine. appyx = "1.7.1" sqldelight = "2.3.2" -wysiwyg = "2.41.1" -telephoto = "0.18.0" +wysiwyg = "2.41.3" +telephoto = "0.19.0" haze = "1.7.2" # Dependency analysis dependencyAnalysis = "3.6.1" # DI -metro = "0.12.0" +metro = "0.13.2" # Auto service autoservice = "1.1.1" @@ -165,10 +165,10 @@ test_mockk = "io.mockk:mockk:1.14.9" test_konsist = "com.lemonappdev:konsist:0.17.3" test_turbine = "app.cash.turbine:turbine:1.2.1" test_truth = "com.google.truth:truth:1.4.5" -test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.21" +test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.22" test_robolectric = "org.robolectric:robolectric:4.16.1" test_appyx_junit = { module = "com.bumble.appyx:testing-junit4", version.ref = "appyx" } -test_composable_preview_scanner = "io.github.sergio-sastre.ComposablePreviewScanner:android:0.8.1" +test_composable_preview_scanner = "io.github.sergio-sastre.ComposablePreviewScanner:android:0.8.2" test_detekt_api = { module = "io.gitlab.arturbosch.detekt:detekt-api", version.ref = "detekt" } test_detekt_test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version.ref = "detekt" } @@ -178,7 +178,7 @@ test_detekt_test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version # https://github.com/matrix-org/matrix-rust-components-kotlin/commits/main/sdk/sdk-android/src/main/kotlin/org/matrix/rustcomponents/sdk/matrix_sdk_ffi.kt # All new features should not be implemented in the pull request that upgrades the version, developers should # only fix API breaks and may add some TODOs. -matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.03.31" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.04.15" # Others coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" } @@ -200,14 +200,14 @@ matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" } sqldelight-driver-jvm = { module = "app.cash.sqldelight:sqlite-driver", version.ref = "sqldelight" } sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" } -sqlcipher = "net.zetetic:sqlcipher-android:4.14.0" +sqlcipher = "net.zetetic:sqlcipher-android:4.14.1" sqlite = "androidx.sqlite:sqlite-ktx:2.6.2" unifiedpush = "org.unifiedpush.android:connector:3.3.2" vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0" -telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" } +telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil3", version.ref = "telephoto" } telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "telephoto" } statemachine = "com.freeletics.flowredux:compose:1.2.2" -maplibre = "org.maplibre.gl:android-sdk:13.0.1" +maplibre = "org.maplibre.gl:android-sdk:13.0.2" maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.2" maplibre_compose = "org.maplibre.compose:maplibre-compose:0.12.1" maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.2" @@ -221,7 +221,7 @@ color_picker = "io.mhssn:colorpicker:1.0.0" # Analytics posthog = "com.posthog:posthog-android:3.39.0" -sentry = "io.sentry:sentry-android:8.36.0" +sentry = "io.sentry:sentry-android:8.37.1" # main branch can be tested replacing the version with main-SNAPSHOT matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.33.2" diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/service/ServiceBinder.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/service/ServiceBinder.kt new file mode 100644 index 0000000000..ba71ca131f --- /dev/null +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/service/ServiceBinder.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026 Element Creations 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.libraries.androidutils.service + +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import io.element.android.libraries.di.annotations.ApplicationContext + +interface ServiceBinder { + fun bindService(service: Intent, conn: ServiceConnection, flags: Int): Boolean + fun unbindService(conn: ServiceConnection) +} + +@ContributesBinding(AppScope::class) +class DefaultServiceBinder( + @ApplicationContext private val context: Context, +) : ServiceBinder { + override fun bindService(service: Intent, conn: ServiceConnection, flags: Int): Boolean { + return context.bindService(service, conn, flags) + } + + override fun unbindService(conn: ServiceConnection) { + context.unbindService(conn) + } +} diff --git a/libraries/androidutils/src/main/res/values-ja/translations.xml b/libraries/androidutils/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..c27f0f3ed2 --- /dev/null +++ b/libraries/androidutils/src/main/res/values-ja/translations.xml @@ -0,0 +1,4 @@ + + + "このアクションを処理できるアプリが見つかりません。" + diff --git a/libraries/androidutils/src/main/res/values-vi/translations.xml b/libraries/androidutils/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..bf30372f4a --- /dev/null +++ b/libraries/androidutils/src/main/res/values-vi/translations.xml @@ -0,0 +1,4 @@ + + + "Không tìm thấy ứng dụng tương thích nào để xử lý hành động này." + diff --git a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/appyx/FaderOrSliderTransitionHandler.kt b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/appyx/FaderOrSliderTransitionHandler.kt new file mode 100644 index 0000000000..c8da7439a5 --- /dev/null +++ b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/appyx/FaderOrSliderTransitionHandler.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2026 Element Creations 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.libraries.architecture.appyx + +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.Transition +import androidx.compose.animation.core.spring +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.navigation.transition.ModifierTransitionHandler +import com.bumble.appyx.core.navigation.transition.TransitionDescriptor +import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.NewRoot +import com.bumble.appyx.navmodel.backstack.operation.Replace +import com.bumble.appyx.navmodel.backstack.transitionhandler.rememberBackstackFader +import com.bumble.appyx.navmodel.backstack.transitionhandler.rememberBackstackSlider + +/** + * A TransitionHandler that uses fade transition when the operation is Replace or NewRoot, + * and slide transition for all other cases. + */ +private class FaderOrSliderTransitionHandler( + private val slider: ModifierTransitionHandler, + private val fader: ModifierTransitionHandler, +) : ModifierTransitionHandler() { + override fun createModifier( + modifier: Modifier, + transition: Transition, + descriptor: TransitionDescriptor + ): Modifier { + val operation = descriptor.operation + val useFader = operation is Replace || operation is NewRoot + val handler = if (useFader) fader else slider + return handler.createModifier(modifier, transition, descriptor) + } +} + +@Composable +fun rememberFaderOrSliderTransitionHandler(): ModifierTransitionHandler { + val slider = rememberBackstackSlider( + transitionSpec = { spring(stiffness = Spring.StiffnessMediumLow) }, + ) + val fader = rememberBackstackFader( + transitionSpec = { spring(stiffness = Spring.StiffnessMediumLow) }, + ) + return rememberDelegateTransitionHandler { + FaderOrSliderTransitionHandler(slider, fader) + } +} diff --git a/libraries/compound/screenshots/Compound Icons - Dark.png b/libraries/compound/screenshots/Compound Icons - Dark.png index f140517dff..2e3fd0d04d 100644 --- a/libraries/compound/screenshots/Compound Icons - Dark.png +++ b/libraries/compound/screenshots/Compound Icons - Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:37f6acca46890e98087ece62e2716fa60791479fab02999406050517e3b79307 -size 240187 +oid sha256:7901fea2f578c8ed796160c9c08f417c61f4fb21580f958844fdf0cb794adf8a +size 239731 diff --git a/libraries/compound/screenshots/Compound Icons - Light.png b/libraries/compound/screenshots/Compound Icons - Light.png index c84421b6fa..6e1bfc80cd 100644 --- a/libraries/compound/screenshots/Compound Icons - Light.png +++ b/libraries/compound/screenshots/Compound Icons - Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a2de5e6d24dcbe0baa75a69485f5a308466fa599625bcbdb0cb96e9bc5a1b708 -size 253233 +oid sha256:245f012d419817f6557d92a71729b3b70092f24f0eba37f2f1fc431ad27592be +size 252969 diff --git a/libraries/compound/screenshots/Compound Icons - Rtl.png b/libraries/compound/screenshots/Compound Icons - Rtl.png index 89be63840a..aa1b5d93a7 100644 --- a/libraries/compound/screenshots/Compound Icons - Rtl.png +++ b/libraries/compound/screenshots/Compound Icons - Rtl.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae1cb46d82acbb23cc172f41e20a41bbe88c350ab53c20e5b2a91f2c16590fbf -size 254525 +oid sha256:2d15c52b21cc279d306fa187cd0c318820109b5ec66270e6447e1b02e800eeba +size 254206 diff --git a/libraries/compound/screenshots/Compound Vector Icons - Dark.png b/libraries/compound/screenshots/Compound Vector Icons - Dark.png index 702fd9b425..a01fbf18b4 100644 --- a/libraries/compound/screenshots/Compound Vector Icons - Dark.png +++ b/libraries/compound/screenshots/Compound Vector Icons - Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a8a9b6e61758a40d01028a4edb4a4d21b845b83b3e0793ed0934e48f3d9eea0 -size 94637 +oid sha256:85ef188fa3a27e42f4beafc899c1f3e7e8bcfad980ed76af6a03f76d70d6a511 +size 93807 diff --git a/libraries/compound/screenshots/Compound Vector Icons - Light.png b/libraries/compound/screenshots/Compound Vector Icons - Light.png index 76b1cc49bd..e227f1579c 100644 --- a/libraries/compound/screenshots/Compound Vector Icons - Light.png +++ b/libraries/compound/screenshots/Compound Vector Icons - Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f29d225df71587fefe07ec8739b84f1a0469786c6b1d6778da0bad33d19574e -size 101183 +oid sha256:f6e38386e95dc0c50384f06fca122ce14851ceff8ffc7865e394c1b4fccc5db6 +size 100555 diff --git a/libraries/compound/src/main/assets/theme.iife.js b/libraries/compound/src/main/assets/theme.iife.js index 1dd693cbd4..e7205e4f81 100644 --- a/libraries/compound/src/main/assets/theme.iife.js +++ b/libraries/compound/src/main/assets/theme.iife.js @@ -1 +1 @@ -var CompoundTheme=(function($t){"use strict";const{min:oi,max:si}=Math,qt=(e,t=0,r=1)=>oi(si(t,e),r),xe=e=>{e._clipped=!1,e._unclipped=e.slice(0);for(let t=0;t<=3;t++)t<3?((e[t]<0||e[t]>255)&&(e._clipped=!0),e[t]=qt(e[t],0,255)):t===3&&(e[t]=qt(e[t],0,1));return e},fn={};for(let e of["Boolean","Number","String","Function","Array","Date","RegExp","Undefined","Null"])fn[`[object ${e}]`]=e.toLowerCase();function N(e){return fn[Object.prototype.toString.call(e)]||"object"}const E=(e,t=null)=>e.length>=3?Array.prototype.slice.call(e):N(e[0])=="object"&&t?t.split("").filter(r=>e[0][r]!==void 0).map(r=>e[0][r]):e[0].slice(0),Et=e=>{if(e.length<2)return null;const t=e.length-1;return N(e[t])=="string"?e[t].toLowerCase():null},{PI:Wt,min:hn,max:dn}=Math,et=e=>Math.round(e*100)/100,Ce=e=>Math.round(e*100)/100,ft=Wt*2,ke=Wt/3,ii=Wt/180,ai=180/Wt;function bn(e){return[...e.slice(0,3).reverse(),...e.slice(3)]}const $={format:{},autodetect:[]};let _=class{constructor(...t){const r=this;if(N(t[0])==="object"&&t[0].constructor&&t[0].constructor===this.constructor)return t[0];let n=Et(t),o=!1;if(!n){o=!0,$.sorted||($.autodetect=$.autodetect.sort((i,s)=>s.p-i.p),$.sorted=!0);for(let i of $.autodetect)if(n=i.test(...t),n)break}if($.format[n]){const i=$.format[n].apply(null,o?t:t.slice(0,-1));r._rgb=xe(i)}else throw new Error("unknown format: "+t);r._rgb.length===3&&r._rgb.push(1)}toString(){return N(this.hex)=="function"?this.hex():`[${this._rgb.join(",")}]`}};const ci="3.2.0",k=(...e)=>new _(...e);k.version=ci;const Lt={aliceblue:"#f0f8ff",antiquewhite:"#faebd7",aqua:"#00ffff",aquamarine:"#7fffd4",azure:"#f0ffff",beige:"#f5f5dc",bisque:"#ffe4c4",black:"#000000",blanchedalmond:"#ffebcd",blue:"#0000ff",blueviolet:"#8a2be2",brown:"#a52a2a",burlywood:"#deb887",cadetblue:"#5f9ea0",chartreuse:"#7fff00",chocolate:"#d2691e",coral:"#ff7f50",cornflowerblue:"#6495ed",cornsilk:"#fff8dc",crimson:"#dc143c",cyan:"#00ffff",darkblue:"#00008b",darkcyan:"#008b8b",darkgoldenrod:"#b8860b",darkgray:"#a9a9a9",darkgreen:"#006400",darkgrey:"#a9a9a9",darkkhaki:"#bdb76b",darkmagenta:"#8b008b",darkolivegreen:"#556b2f",darkorange:"#ff8c00",darkorchid:"#9932cc",darkred:"#8b0000",darksalmon:"#e9967a",darkseagreen:"#8fbc8f",darkslateblue:"#483d8b",darkslategray:"#2f4f4f",darkslategrey:"#2f4f4f",darkturquoise:"#00ced1",darkviolet:"#9400d3",deeppink:"#ff1493",deepskyblue:"#00bfff",dimgray:"#696969",dimgrey:"#696969",dodgerblue:"#1e90ff",firebrick:"#b22222",floralwhite:"#fffaf0",forestgreen:"#228b22",fuchsia:"#ff00ff",gainsboro:"#dcdcdc",ghostwhite:"#f8f8ff",gold:"#ffd700",goldenrod:"#daa520",gray:"#808080",green:"#008000",greenyellow:"#adff2f",grey:"#808080",honeydew:"#f0fff0",hotpink:"#ff69b4",indianred:"#cd5c5c",indigo:"#4b0082",ivory:"#fffff0",khaki:"#f0e68c",laserlemon:"#ffff54",lavender:"#e6e6fa",lavenderblush:"#fff0f5",lawngreen:"#7cfc00",lemonchiffon:"#fffacd",lightblue:"#add8e6",lightcoral:"#f08080",lightcyan:"#e0ffff",lightgoldenrod:"#fafad2",lightgoldenrodyellow:"#fafad2",lightgray:"#d3d3d3",lightgreen:"#90ee90",lightgrey:"#d3d3d3",lightpink:"#ffb6c1",lightsalmon:"#ffa07a",lightseagreen:"#20b2aa",lightskyblue:"#87cefa",lightslategray:"#778899",lightslategrey:"#778899",lightsteelblue:"#b0c4de",lightyellow:"#ffffe0",lime:"#00ff00",limegreen:"#32cd32",linen:"#faf0e6",magenta:"#ff00ff",maroon:"#800000",maroon2:"#7f0000",maroon3:"#b03060",mediumaquamarine:"#66cdaa",mediumblue:"#0000cd",mediumorchid:"#ba55d3",mediumpurple:"#9370db",mediumseagreen:"#3cb371",mediumslateblue:"#7b68ee",mediumspringgreen:"#00fa9a",mediumturquoise:"#48d1cc",mediumvioletred:"#c71585",midnightblue:"#191970",mintcream:"#f5fffa",mistyrose:"#ffe4e1",moccasin:"#ffe4b5",navajowhite:"#ffdead",navy:"#000080",oldlace:"#fdf5e6",olive:"#808000",olivedrab:"#6b8e23",orange:"#ffa500",orangered:"#ff4500",orchid:"#da70d6",palegoldenrod:"#eee8aa",palegreen:"#98fb98",paleturquoise:"#afeeee",palevioletred:"#db7093",papayawhip:"#ffefd5",peachpuff:"#ffdab9",peru:"#cd853f",pink:"#ffc0cb",plum:"#dda0dd",powderblue:"#b0e0e6",purple:"#800080",purple2:"#7f007f",purple3:"#a020f0",rebeccapurple:"#663399",red:"#ff0000",rosybrown:"#bc8f8f",royalblue:"#4169e1",saddlebrown:"#8b4513",salmon:"#fa8072",sandybrown:"#f4a460",seagreen:"#2e8b57",seashell:"#fff5ee",sienna:"#a0522d",silver:"#c0c0c0",skyblue:"#87ceeb",slateblue:"#6a5acd",slategray:"#708090",slategrey:"#708090",snow:"#fffafa",springgreen:"#00ff7f",steelblue:"#4682b4",tan:"#d2b48c",teal:"#008080",thistle:"#d8bfd8",tomato:"#ff6347",turquoise:"#40e0d0",violet:"#ee82ee",wheat:"#f5deb3",white:"#ffffff",whitesmoke:"#f5f5f5",yellow:"#ffff00",yellowgreen:"#9acd32"},ui=/^#?([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/,li=/^#?([A-Fa-f0-9]{8}|[A-Fa-f0-9]{4})$/,pn=e=>{if(e.match(ui)){(e.length===4||e.length===7)&&(e=e.substr(1)),e.length===3&&(e=e.split(""),e=e[0]+e[0]+e[1]+e[1]+e[2]+e[2]);const t=parseInt(e,16),r=t>>16,n=t>>8&255,o=t&255;return[r,n,o,1]}if(e.match(li)){(e.length===5||e.length===9)&&(e=e.substr(1)),e.length===4&&(e=e.split(""),e=e[0]+e[0]+e[1]+e[1]+e[2]+e[2]+e[3]+e[3]);const t=parseInt(e,16),r=t>>24&255,n=t>>16&255,o=t>>8&255,i=Math.round((t&255)/255*100)/100;return[r,n,o,i]}throw new Error(`unknown hex color: ${e}`)},{round:Ut}=Math,mn=(...e)=>{let[t,r,n,o]=E(e,"rgba"),i=Et(e)||"auto";o===void 0&&(o=1),i==="auto"&&(i=o<1?"rgba":"rgb"),t=Ut(t),r=Ut(r),n=Ut(n);let a="000000"+(t<<16|r<<8|n).toString(16);a=a.substr(a.length-6);let c="0"+Ut(o*255).toString(16);switch(c=c.substr(c.length-2),i.toLowerCase()){case"rgba":return`#${a}${c}`;case"argb":return`#${c}${a}`;default:return`#${a}`}};_.prototype.name=function(){const e=mn(this._rgb,"rgb");for(let t of Object.keys(Lt))if(Lt[t]===e)return t.toLowerCase();return e},$.format.named=e=>{if(e=e.toLowerCase(),Lt[e])return pn(Lt[e]);throw new Error("unknown color name: "+e)},$.autodetect.push({p:5,test:(e,...t)=>{if(!t.length&&N(e)==="string"&&Lt[e.toLowerCase()])return"named"}}),_.prototype.alpha=function(e,t=!1){return e!==void 0&&N(e)==="number"?t?(this._rgb[3]=e,this):new _([this._rgb[0],this._rgb[1],this._rgb[2],e],"rgb"):this._rgb[3]},_.prototype.clipped=function(){return this._rgb._clipped||!1};const ct={Kn:18,labWhitePoint:"d65",Xn:.95047,Yn:1,Zn:1.08883,kE:216/24389,kKE:8,kK:24389/27,RefWhiteRGB:{X:.95047,Y:1,Z:1.08883},MtxRGB2XYZ:{m00:.4124564390896922,m01:.21267285140562253,m02:.0193338955823293,m10:.357576077643909,m11:.715152155287818,m12:.11919202588130297,m20:.18043748326639894,m21:.07217499330655958,m22:.9503040785363679},MtxXYZ2RGB:{m00:3.2404541621141045,m01:-.9692660305051868,m02:.055643430959114726,m10:-1.5371385127977166,m11:1.8760108454466942,m12:-.2040259135167538,m20:-.498531409556016,m21:.041556017530349834,m22:1.0572251882231791},As:.9414285350000001,Bs:1.040417467,Cs:1.089532651,MtxAdaptMa:{m00:.8951,m01:-.7502,m02:.0389,m10:.2664,m11:1.7135,m12:-.0685,m20:-.1614,m21:.0367,m22:1.0296},MtxAdaptMaI:{m00:.9869929054667123,m01:.43230526972339456,m02:-.008528664575177328,m10:-.14705425642099013,m11:.5183602715367776,m12:.04004282165408487,m20:.15996265166373125,m21:.0492912282128556,m22:.9684866957875502}},fi=new Map([["a",[1.0985,.35585]],["b",[1.0985,.35585]],["c",[.98074,1.18232]],["d50",[.96422,.82521]],["d55",[.95682,.92149]],["d65",[.95047,1.08883]],["e",[1,1,1]],["f2",[.99186,.67393]],["f7",[.95041,1.08747]],["f11",[1.00962,.6435]],["icc",[.96422,.82521]]]);function ht(e){const t=fi.get(String(e).toLowerCase());if(!t)throw new Error("unknown Lab illuminant "+e);ct.labWhitePoint=e,ct.Xn=t[0],ct.Zn=t[1]}function Kt(){return ct.labWhitePoint}const Re=(...e)=>{e=E(e,"lab");const[t,r,n]=e,[o,i,s]=hi(t,r,n),[a,c,u]=gn(o,i,s);return[a,c,u,e.length>3?e[3]:1]},hi=(e,t,r)=>{const{kE:n,kK:o,kKE:i,Xn:s,Yn:a,Zn:c}=ct,u=(e+16)/116,f=.002*t+u,l=u-.005*r,h=f*f*f,d=l*l*l,b=h>n?h:(116*f-16)/o,g=e>i?Math.pow((e+16)/116,3):e/o,m=d>n?d:(116*l-16)/o,y=b*s,L=g*a,w=m*c;return[y,L,w]},qe=e=>{const t=Math.sign(e);return e=Math.abs(e),(e<=.0031308?e*12.92:1.055*Math.pow(e,1/2.4)-.055)*t},gn=(e,t,r)=>{const{MtxAdaptMa:n,MtxAdaptMaI:o,MtxXYZ2RGB:i,RefWhiteRGB:s,Xn:a,Yn:c,Zn:u}=ct,f=a*n.m00+c*n.m10+u*n.m20,l=a*n.m01+c*n.m11+u*n.m21,h=a*n.m02+c*n.m12+u*n.m22,d=s.X*n.m00+s.Y*n.m10+s.Z*n.m20,b=s.X*n.m01+s.Y*n.m11+s.Z*n.m21,g=s.X*n.m02+s.Y*n.m12+s.Z*n.m22,m=(e*n.m00+t*n.m10+r*n.m20)*(d/f),y=(e*n.m01+t*n.m11+r*n.m21)*(b/l),L=(e*n.m02+t*n.m12+r*n.m22)*(g/h),w=m*o.m00+y*o.m10+L*o.m20,x=m*o.m01+y*o.m11+L*o.m21,S=m*o.m02+y*o.m12+L*o.m22,R=qe(w*i.m00+x*i.m10+S*i.m20),M=qe(w*i.m01+x*i.m11+S*i.m21),p=qe(w*i.m02+x*i.m12+S*i.m22);return[R*255,M*255,p*255]},Me=(...e)=>{const[t,r,n,...o]=E(e,"rgb"),[i,s,a]=_n(t,r,n),[c,u,f]=di(i,s,a);return[c,u,f,...o.length>0&&o[0]<1?[o[0]]:[]]};function di(e,t,r){const{Xn:n,Yn:o,Zn:i,kE:s,kK:a}=ct,c=e/n,u=t/o,f=r/i,l=c>s?Math.pow(c,1/3):(a*c+16)/116,h=u>s?Math.pow(u,1/3):(a*u+16)/116,d=f>s?Math.pow(f,1/3):(a*f+16)/116;return[116*h-16,500*(l-h),200*(h-d)]}function Oe(e){const t=Math.sign(e);return e=Math.abs(e),(e<=.04045?e/12.92:Math.pow((e+.055)/1.055,2.4))*t}const _n=(e,t,r)=>{e=Oe(e/255),t=Oe(t/255),r=Oe(r/255);const{MtxRGB2XYZ:n,MtxAdaptMa:o,MtxAdaptMaI:i,Xn:s,Yn:a,Zn:c,As:u,Bs:f,Cs:l}=ct;let h=e*n.m00+t*n.m10+r*n.m20,d=e*n.m01+t*n.m11+r*n.m21,b=e*n.m02+t*n.m12+r*n.m22;const g=s*o.m00+a*o.m10+c*o.m20,m=s*o.m01+a*o.m11+c*o.m21,y=s*o.m02+a*o.m12+c*o.m22;let L=h*o.m00+d*o.m10+b*o.m20,w=h*o.m01+d*o.m11+b*o.m21,x=h*o.m02+d*o.m12+b*o.m22;return L*=g/u,w*=m/f,x*=y/l,h=L*i.m00+w*i.m10+x*i.m20,d=L*i.m01+w*i.m11+x*i.m21,b=L*i.m02+w*i.m12+x*i.m22,[h,d,b]};_.prototype.lab=function(){return Me(this._rgb)},Object.assign(k,{lab:(...e)=>new _(...e,"lab"),getLabWhitePoint:Kt,setLabWhitePoint:ht}),$.format.lab=Re,$.autodetect.push({p:2,test:(...e)=>{if(e=E(e,"lab"),N(e)==="array"&&e.length===3)return"lab"}}),_.prototype.darken=function(e=1){const t=this,r=t.lab();return r[0]-=ct.Kn*e,new _(r,"lab").alpha(t.alpha(),!0)},_.prototype.brighten=function(e=1){return this.darken(-e)},_.prototype.darker=_.prototype.darken,_.prototype.brighter=_.prototype.brighten,_.prototype.get=function(e){const[t,r]=e.split("."),n=this[t]();if(r){const o=t.indexOf(r)-(t.substr(0,2)==="ok"?2:0);if(o>-1)return n[o];throw new Error(`unknown channel ${r} in mode ${t}`)}else return n};const{pow:bi}=Math,pi=1e-7,mi=20;_.prototype.luminance=function(e,t="rgb"){if(e!==void 0&&N(e)==="number"){if(e===0)return new _([0,0,0,this._rgb[3]],"rgb");if(e===1)return new _([255,255,255,this._rgb[3]],"rgb");let r=this.luminance(),n=mi;const o=(s,a)=>{const c=s.interpolate(a,.5,t),u=c.luminance();return Math.abs(e-u)e?o(s,c):o(c,a)},i=(r>e?o(new _([0,0,0]),this):o(this,new _([255,255,255]))).rgb();return new _([...i,this._rgb[3]])}return gi(...this._rgb.slice(0,3))};const gi=(e,t,r)=>(e=Ae(e),t=Ae(t),r=Ae(r),.2126*e+.7152*t+.0722*r),Ae=e=>(e/=255,e<=.03928?e/12.92:bi((e+.055)/1.055,2.4)),H={},Nt=(e,t,r=.5,...n)=>{let o=n[0]||"lrgb";if(!H[o]&&!n.length&&(o=Object.keys(H)[0]),!H[o])throw new Error(`interpolation mode ${o} is not defined`);return N(e)!=="object"&&(e=new _(e)),N(t)!=="object"&&(t=new _(t)),H[o](e,t,r).alpha(e.alpha()+r*(t.alpha()-e.alpha()))};_.prototype.mix=_.prototype.interpolate=function(e,t=.5,...r){return Nt(this,e,t,...r)},_.prototype.premultiply=function(e=!1){const t=this._rgb,r=t[3];return e?(this._rgb=[t[0]*r,t[1]*r,t[2]*r,r],this):new _([t[0]*r,t[1]*r,t[2]*r,r],"rgb")};const{sin:_i,cos:vi}=Math,vn=(...e)=>{let[t,r,n]=E(e,"lch");return isNaN(n)&&(n=0),n=n*ii,[t,vi(n)*r,_i(n)*r]},Se=(...e)=>{e=E(e,"lch");const[t,r,n]=e,[o,i,s]=vn(t,r,n),[a,c,u]=Re(o,i,s);return[a,c,u,e.length>3?e[3]:1]},yi=(...e)=>{const t=bn(E(e,"hcl"));return Se(...t)},{sqrt:wi,atan2:xi,round:Ci}=Math,yn=(...e)=>{const[t,r,n]=E(e,"lab"),o=wi(r*r+n*n);let i=(xi(n,r)*ai+360)%360;return Ci(o*1e4)===0&&(i=Number.NaN),[t,o,i]},$e=(...e)=>{const[t,r,n,...o]=E(e,"rgb"),[i,s,a]=Me(t,r,n),[c,u,f]=yn(i,s,a);return[c,u,f,...o.length>0&&o[0]<1?[o[0]]:[]]};_.prototype.lch=function(){return $e(this._rgb)},_.prototype.hcl=function(){return bn($e(this._rgb))},Object.assign(k,{lch:(...e)=>new _(...e,"lch"),hcl:(...e)=>new _(...e,"hcl")}),$.format.lch=Se,$.format.hcl=yi,["lch","hcl"].forEach(e=>$.autodetect.push({p:2,test:(...t)=>{if(t=E(t,e),N(t)==="array"&&t.length===3)return e}})),_.prototype.saturate=function(e=1){const t=this,r=t.lch();return r[1]+=ct.Kn*e,r[1]<0&&(r[1]=0),new _(r,"lch").alpha(t.alpha(),!0)},_.prototype.desaturate=function(e=1){return this.saturate(-e)},_.prototype.set=function(e,t,r=!1){const[n,o]=e.split("."),i=this[n]();if(o){const s=n.indexOf(o)-(n.substr(0,2)==="ok"?2:0);if(s>-1){if(N(t)=="string")switch(t.charAt(0)){case"+":i[s]+=+t;break;case"-":i[s]+=+t;break;case"*":i[s]*=+t.substr(1);break;case"/":i[s]/=+t.substr(1);break;default:i[s]=+t}else if(N(t)==="number")i[s]=t;else throw new Error("unsupported value for Color.set");const a=new _(i,n);return r?(this._rgb=a._rgb,this):a}throw new Error(`unknown channel ${o} in mode ${n}`)}else return i},_.prototype.tint=function(e=.5,...t){return Nt(this,"white",e,...t)},_.prototype.shade=function(e=.5,...t){return Nt(this,"black",e,...t)};const ki=(e,t,r)=>{const n=e._rgb,o=t._rgb;return new _(n[0]+r*(o[0]-n[0]),n[1]+r*(o[1]-n[1]),n[2]+r*(o[2]-n[2]),"rgb")};H.rgb=ki;const{sqrt:Ee,pow:Tt}=Math,Ri=(e,t,r)=>{const[n,o,i]=e._rgb,[s,a,c]=t._rgb;return new _(Ee(Tt(n,2)*(1-r)+Tt(s,2)*r),Ee(Tt(o,2)*(1-r)+Tt(a,2)*r),Ee(Tt(i,2)*(1-r)+Tt(c,2)*r),"rgb")};H.lrgb=Ri;const qi=(e,t,r)=>{const n=e.lab(),o=t.lab();return new _(n[0]+r*(o[0]-n[0]),n[1]+r*(o[1]-n[1]),n[2]+r*(o[2]-n[2]),"lab")};H.lab=qi;const jt=(e,t,r,n)=>{let o,i;n==="hsl"?(o=e.hsl(),i=t.hsl()):n==="hsv"?(o=e.hsv(),i=t.hsv()):n==="hcg"?(o=e.hcg(),i=t.hcg()):n==="hsi"?(o=e.hsi(),i=t.hsi()):n==="lch"||n==="hcl"?(n="hcl",o=e.hcl(),i=t.hcl()):n==="oklch"&&(o=e.oklch().reverse(),i=t.oklch().reverse());let s,a,c,u,f,l;(n.substr(0,1)==="h"||n==="oklch")&&([s,c,f]=o,[a,u,l]=i);let h,d,b,g;return!isNaN(s)&&!isNaN(a)?(a>s&&a-s>180?g=a-(s+360):a180?g=a+360-s:g=a-s,d=s+r*g):isNaN(s)?isNaN(a)?d=Number.NaN:(d=a,(f==1||f==0)&&n!="hsv"&&(h=u)):(d=s,(l==1||l==0)&&n!="hsv"&&(h=c)),h===void 0&&(h=c+r*(u-c)),b=f+r*(l-f),n==="oklch"?new _([b,h,d],n):new _([d,h,b],n)},wn=(e,t,r)=>jt(e,t,r,"lch");H.lch=wn,H.hcl=wn;const Mi=e=>{if(N(e)=="number"&&e>=0&&e<=16777215){const t=e>>16,r=e>>8&255,n=e&255;return[t,r,n,1]}throw new Error("unknown num color: "+e)},Oi=(...e)=>{const[t,r,n]=E(e,"rgb");return(t<<16)+(r<<8)+n};_.prototype.num=function(){return Oi(this._rgb)},Object.assign(k,{num:(...e)=>new _(...e,"num")}),$.format.num=Mi,$.autodetect.push({p:5,test:(...e)=>{if(e.length===1&&N(e[0])==="number"&&e[0]>=0&&e[0]<=16777215)return"num"}});const Ai=(e,t,r)=>{const n=e.num(),o=t.num();return new _(n+r*(o-n),"num")};H.num=Ai;const{floor:Si}=Math,$i=(...e)=>{e=E(e,"hcg");let[t,r,n]=e,o,i,s;n=n*255;const a=r*255;if(r===0)o=i=s=n;else{t===360&&(t=0),t>360&&(t-=360),t<0&&(t+=360),t/=60;const c=Si(t),u=t-c,f=n*(1-r),l=f+a*(1-u),h=f+a*u,d=f+a;switch(c){case 0:[o,i,s]=[d,h,f];break;case 1:[o,i,s]=[l,d,f];break;case 2:[o,i,s]=[f,d,h];break;case 3:[o,i,s]=[f,l,d];break;case 4:[o,i,s]=[h,f,d];break;case 5:[o,i,s]=[d,f,l];break}}return[o,i,s,e.length>3?e[3]:1]},Ei=(...e)=>{const[t,r,n]=E(e,"rgb"),o=hn(t,r,n),i=dn(t,r,n),s=i-o,a=s*100/255,c=o/(255-s)*100;let u;return s===0?u=Number.NaN:(t===i&&(u=(r-n)/s),r===i&&(u=2+(n-t)/s),n===i&&(u=4+(t-r)/s),u*=60,u<0&&(u+=360)),[u,a,c]};_.prototype.hcg=function(){return Ei(this._rgb)};const Li=(...e)=>new _(...e,"hcg");k.hcg=Li,$.format.hcg=$i,$.autodetect.push({p:1,test:(...e)=>{if(e=E(e,"hcg"),N(e)==="array"&&e.length===3)return"hcg"}});const Ni=(e,t,r)=>jt(e,t,r,"hcg");H.hcg=Ni;const{cos:Pt}=Math,Ti=(...e)=>{e=E(e,"hsi");let[t,r,n]=e,o,i,s;return isNaN(t)&&(t=0),isNaN(r)&&(r=0),t>360&&(t-=360),t<0&&(t+=360),t/=360,t<1/3?(s=(1-r)/3,o=(1+r*Pt(ft*t)/Pt(ke-ft*t))/3,i=1-(s+o)):t<2/3?(t-=1/3,o=(1-r)/3,i=(1+r*Pt(ft*t)/Pt(ke-ft*t))/3,s=1-(o+i)):(t-=2/3,i=(1-r)/3,s=(1+r*Pt(ft*t)/Pt(ke-ft*t))/3,o=1-(i+s)),o=qt(n*o*3),i=qt(n*i*3),s=qt(n*s*3),[o*255,i*255,s*255,e.length>3?e[3]:1]},{min:ji,sqrt:Pi,acos:zi}=Math,Bi=(...e)=>{let[t,r,n]=E(e,"rgb");t/=255,r/=255,n/=255;let o;const i=ji(t,r,n),s=(t+r+n)/3,a=s>0?1-i/s:0;return a===0?o=NaN:(o=(t-r+(t-n))/2,o/=Pi((t-r)*(t-r)+(t-n)*(r-n)),o=zi(o),n>r&&(o=ft-o),o/=ft),[o*360,a,s]};_.prototype.hsi=function(){return Bi(this._rgb)};const Ii=(...e)=>new _(...e,"hsi");k.hsi=Ii,$.format.hsi=Ti,$.autodetect.push({p:2,test:(...e)=>{if(e=E(e,"hsi"),N(e)==="array"&&e.length===3)return"hsi"}});const Gi=(e,t,r)=>jt(e,t,r,"hsi");H.hsi=Gi;const Le=(...e)=>{e=E(e,"hsl");const[t,r,n]=e;let o,i,s;if(r===0)o=i=s=n*255;else{const a=[0,0,0],c=[0,0,0],u=n<.5?n*(1+r):n+r-n*r,f=2*n-u,l=t/360;a[0]=l+1/3,a[1]=l,a[2]=l-1/3;for(let h=0;h<3;h++)a[h]<0&&(a[h]+=1),a[h]>1&&(a[h]-=1),6*a[h]<1?c[h]=f+(u-f)*6*a[h]:2*a[h]<1?c[h]=u:3*a[h]<2?c[h]=f+(u-f)*(2/3-a[h])*6:c[h]=f;[o,i,s]=[c[0]*255,c[1]*255,c[2]*255]}return e.length>3?[o,i,s,e[3]]:[o,i,s,1]},xn=(...e)=>{e=E(e,"rgba");let[t,r,n]=e;t/=255,r/=255,n/=255;const o=hn(t,r,n),i=dn(t,r,n),s=(i+o)/2;let a,c;return i===o?(a=0,c=Number.NaN):a=s<.5?(i-o)/(i+o):(i-o)/(2-i-o),t==i?c=(r-n)/(i-o):r==i?c=2+(n-t)/(i-o):n==i&&(c=4+(t-r)/(i-o)),c*=60,c<0&&(c+=360),e.length>3&&e[3]!==void 0?[c,a,s,e[3]]:[c,a,s]};_.prototype.hsl=function(){return xn(this._rgb)};const Fi=(...e)=>new _(...e,"hsl");k.hsl=Fi,$.format.hsl=Le,$.autodetect.push({p:2,test:(...e)=>{if(e=E(e,"hsl"),N(e)==="array"&&e.length===3)return"hsl"}});const Ki=(e,t,r)=>jt(e,t,r,"hsl");H.hsl=Ki;const{floor:Xi}=Math,Di=(...e)=>{e=E(e,"hsv");let[t,r,n]=e,o,i,s;if(n*=255,r===0)o=i=s=n;else{t===360&&(t=0),t>360&&(t-=360),t<0&&(t+=360),t/=60;const a=Xi(t),c=t-a,u=n*(1-r),f=n*(1-r*c),l=n*(1-r*(1-c));switch(a){case 0:[o,i,s]=[n,l,u];break;case 1:[o,i,s]=[f,n,u];break;case 2:[o,i,s]=[u,n,l];break;case 3:[o,i,s]=[u,f,n];break;case 4:[o,i,s]=[l,u,n];break;case 5:[o,i,s]=[n,u,f];break}}return[o,i,s,e.length>3?e[3]:1]},{min:Vi,max:Yi}=Math,Zi=(...e)=>{e=E(e,"rgb");let[t,r,n]=e;const o=Vi(t,r,n),i=Yi(t,r,n),s=i-o;let a,c,u;return u=i/255,i===0?(a=Number.NaN,c=0):(c=s/i,t===i&&(a=(r-n)/s),r===i&&(a=2+(n-t)/s),n===i&&(a=4+(t-r)/s),a*=60,a<0&&(a+=360)),[a,c,u]};_.prototype.hsv=function(){return Zi(this._rgb)};const Ji=(...e)=>new _(...e,"hsv");k.hsv=Ji,$.format.hsv=Di,$.autodetect.push({p:2,test:(...e)=>{if(e=E(e,"hsv"),N(e)==="array"&&e.length===3)return"hsv"}});const Hi=(e,t,r)=>jt(e,t,r,"hsv");H.hsv=Hi;function Qt(e,t){let r=e.length;Array.isArray(e[0])||(e=[e]),Array.isArray(t[0])||(t=t.map(s=>[s]));let n=t[0].length,o=t[0].map((s,a)=>t.map(c=>c[a])),i=e.map(s=>o.map(a=>Array.isArray(s)?s.reduce((c,u,f)=>c+u*(a[f]||0),0):a.reduce((c,u)=>c+u*s,0)));return r===1&&(i=i[0]),n===1?i.map(s=>s[0]):i}const Ne=(...e)=>{e=E(e,"lab");const[t,r,n,...o]=e,[i,s,a]=Wi([t,r,n]),[c,u,f]=gn(i,s,a);return[c,u,f,...o.length>0&&o[0]<1?[o[0]]:[]]};function Wi(e){var t=[[1.2268798758459243,-.5578149944602171,.2813910456659647],[-.0405757452148008,1.112286803280317,-.0717110580655164],[-.0763729366746601,-.4214933324022432,1.5869240198367816]],r=[[1,.3963377773761749,.2158037573099136],[1,-.1055613458156586,-.0638541728258133],[1,-.0894841775298119,-1.2914855480194092]],n=Qt(r,e);return Qt(t,n.map(o=>o**3))}const Te=(...e)=>{const[t,r,n,...o]=E(e,"rgb"),i=_n(t,r,n);return[...Ui(i),...o.length>0&&o[0]<1?[o[0]]:[]]};function Ui(e){const t=[[.819022437996703,.3619062600528904,-.1288737815209879],[.0329836539323885,.9292868615863434,.0361446663506424],[.0481771893596242,.2642395317527308,.6335478284694309]],r=[[.210454268309314,.7936177747023054,-.0040720430116193],[1.9779985324311684,-2.42859224204858,.450593709617411],[.0259040424655478,.7827717124575296,-.8086757549230774]],n=Qt(t,e);return Qt(r,n.map(o=>Math.cbrt(o)))}_.prototype.oklab=function(){return Te(this._rgb)},Object.assign(k,{oklab:(...e)=>new _(...e,"oklab")}),$.format.oklab=Ne,$.autodetect.push({p:2,test:(...e)=>{if(e=E(e,"oklab"),N(e)==="array"&&e.length===3)return"oklab"}});const Qi=(e,t,r)=>{const n=e.oklab(),o=t.oklab();return new _(n[0]+r*(o[0]-n[0]),n[1]+r*(o[1]-n[1]),n[2]+r*(o[2]-n[2]),"oklab")};H.oklab=Qi;const ta=(e,t,r)=>jt(e,t,r,"oklch");H.oklch=ta;const{pow:je,sqrt:Pe,PI:ze,cos:Cn,sin:kn,atan2:ea}=Math,ra=(e,t="lrgb",r=null)=>{const n=e.length;r||(r=Array.from(new Array(n)).map(()=>1));const o=n/r.reduce(function(l,h){return l+h});if(r.forEach((l,h)=>{r[h]*=o}),e=e.map(l=>new _(l)),t==="lrgb")return na(e,r);const i=e.shift(),s=i.get(t),a=[];let c=0,u=0;for(let l=0;l{const d=l.get(t);f+=l.alpha()*r[h+1];for(let b=0;b=360;)h-=360;s[l]=h}else s[l]=s[l]/a[l];return f/=n,new _(s,t).alpha(f>.99999?1:f,!0)},na=(e,t)=>{const r=e.length,n=[0,0,0,0];for(let o=0;o.9999999&&(n[3]=1),new _(xe(n))},{pow:oa}=Math;function te(e){let t="rgb",r=k("#ccc"),n=0,o=[0,1],i=[0,1],s=[],a=[0,0],c=!1,u=[],f=!1,l=0,h=1,d=!1,b={},g=!0,m=1;const y=function(p){if(p=p||["#fff","#000"],p&&N(p)==="string"&&k.brewer&&k.brewer[p.toLowerCase()]&&(p=k.brewer[p.toLowerCase()]),N(p)==="array"){p.length===1&&(p=[p[0],p[0]]),p=p.slice(0);for(let C=0;C=c[O];)O++;return O-1}return 0};let w=p=>p,x=p=>p;const S=function(p,C){let O,q;if(C==null&&(C=!1),isNaN(p)||p===null)return r;C?q=p:c&&c.length>2?q=L(p)/(c.length-2):h!==l?q=(p-l)/(h-l):q=1,q=x(q),C||(q=w(q)),m!==1&&(q=oa(q,m)),q=a[0]+q*(1-a[0]-a[1]),q=qt(q,0,1);const T=Math.floor(q*1e4);if(g&&b[T])O=b[T];else{if(N(u)==="array")for(let A=0;A=P&&A===s.length-1){O=u[A];break}if(q>P&&qb={};y(e);const M=function(p){const C=k(S(p));return f&&C[f]?C[f]():C};return M.classes=function(p){if(p!=null){if(N(p)==="array")c=p,o=[p[0],p[p.length-1]];else{const C=k.analyze(o);p===0?c=[C.min,C.max]:c=k.limits(C,"e",p)}return M}return c},M.domain=function(p){if(!arguments.length)return i;i=p.slice(0),l=p[0],h=p[p.length-1],s=[];const C=u.length;if(p.length===C&&l!==h)for(let O of Array.from(p))s.push((O-l)/(h-l));else{for(let O=0;O2){const O=p.map((T,A)=>A/(p.length-1)),q=p.map(T=>(T-l)/(h-l));q.every((T,A)=>O[A]===T)||(x=T=>{if(T<=0||T>=1)return T;let A=0;for(;T>=q[A+1];)A++;const P=(T-q[A])/(q[A+1]-q[A]);return O[A]+P*(O[A+1]-O[A])})}}return o=[l,h],M},M.mode=function(p){return arguments.length?(t=p,R(),M):t},M.range=function(p,C){return y(p),M},M.out=function(p){return f=p,M},M.spread=function(p){return arguments.length?(n=p,M):n},M.correctLightness=function(p){return p==null&&(p=!0),d=p,R(),d?w=function(C){const O=S(0,!0).lab()[0],q=S(1,!0).lab()[0],T=O>q;let A=S(C,!0).lab()[0];const P=O+(q-O)*C;let G=A-P,Y=0,U=1,ut=20;for(;Math.abs(G)>.01&&ut-- >0;)(function(){return T&&(G*=-1),G<0?(Y=C,C+=(U-C)*.5):(U=C,C+=(Y-C)*.5),A=S(C,!0).lab()[0],G=A-P})();return C}:w=C=>C,M},M.padding=function(p){return p!=null?(N(p)==="number"&&(p=[p,p]),a=p,M):a},M.colors=function(p,C){arguments.length<2&&(C="hex");let O=[];if(arguments.length===0)O=u.slice(0);else if(p===1)O=[M(.5)];else if(p>1){const q=o[0],T=o[1]-q;O=sa(0,p).map(A=>M(q+A/(p-1)*T))}else{e=[];let q=[];if(c&&c.length>2)for(let T=1,A=c.length,P=1<=A;P?TA;P?T++:T--)q.push((c[T-1]+c[T])*.5);else q=o;O=q.map(T=>M(T))}return k[C]&&(O=O.map(q=>q[C]())),O},M.cache=function(p){return p!=null?(g=p,M):g},M.gamma=function(p){return p!=null?(m=p,M):m},M.nodata=function(p){return p!=null?(r=k(p),M):r},M}function sa(e,t,r){let n=[],o=ei;o?s++:s--)n.push(s);return n}const ia=function(e){let t=[1,1];for(let r=1;rnew _(i)),e.length===2)[r,n]=e.map(i=>i.lab()),t=function(i){const s=[0,1,2].map(a=>r[a]+i*(n[a]-r[a]));return new _(s,"lab")};else if(e.length===3)[r,n,o]=e.map(i=>i.lab()),t=function(i){const s=[0,1,2].map(a=>(1-i)*(1-i)*r[a]+2*(1-i)*i*n[a]+i*i*o[a]);return new _(s,"lab")};else if(e.length===4){let i;[r,n,o,i]=e.map(s=>s.lab()),t=function(s){const a=[0,1,2].map(c=>(1-s)*(1-s)*(1-s)*r[c]+3*(1-s)*(1-s)*s*n[c]+3*(1-s)*s*s*o[c]+s*s*s*i[c]);return new _(a,"lab")}}else if(e.length>=5){let i,s,a;i=e.map(c=>c.lab()),a=e.length-1,s=ia(a),t=function(c){const u=1-c,f=[0,1,2].map(l=>i.reduce((h,d,b)=>h+s[b]*u**(a-b)*c**b*d[l],0));return new _(f,"lab")}}else throw new RangeError("No point in running bezier with only one color.");return t},ca=e=>{const t=aa(e);return t.scale=()=>te(t),t},{round:Rn}=Math;_.prototype.rgb=function(e=!0){return e===!1?this._rgb.slice(0,3):this._rgb.slice(0,3).map(Rn)},_.prototype.rgba=function(e=!0){return this._rgb.slice(0,4).map((t,r)=>r<3?e===!1?t:Rn(t):t)},Object.assign(k,{rgb:(...e)=>new _(...e,"rgb")}),$.format.rgb=(...e)=>{const t=E(e,"rgba");return t[3]===void 0&&(t[3]=1),t},$.autodetect.push({p:3,test:(...e)=>{if(e=E(e,"rgba"),N(e)==="array"&&(e.length===3||e.length===4&&N(e[3])=="number"&&e[3]>=0&&e[3]<=1))return"rgb"}});const nt=(e,t,r)=>{if(!nt[r])throw new Error("unknown blend mode "+r);return nt[r](e,t)},_t=e=>(t,r)=>{const n=k(r).rgb(),o=k(t).rgb();return k.rgb(e(n,o))},vt=e=>(t,r)=>{const n=[];return n[0]=e(t[0],r[0]),n[1]=e(t[1],r[1]),n[2]=e(t[2],r[2]),n},ua=e=>e,la=(e,t)=>e*t/255,fa=(e,t)=>e>t?t:e,ha=(e,t)=>e>t?e:t,da=(e,t)=>255*(1-(1-e/255)*(1-t/255)),ba=(e,t)=>t<128?2*e*t/255:255*(1-2*(1-e/255)*(1-t/255)),pa=(e,t)=>255*(1-(1-t/255)/(e/255)),ma=(e,t)=>e===255?255:(e=255*(t/255)/(1-e/255),e>255?255:e);nt.normal=_t(vt(ua)),nt.multiply=_t(vt(la)),nt.screen=_t(vt(da)),nt.overlay=_t(vt(ba)),nt.darken=_t(vt(fa)),nt.lighten=_t(vt(ha)),nt.dodge=_t(vt(ma)),nt.burn=_t(vt(pa));const{pow:ga,sin:_a,cos:va}=Math;function ya(e=300,t=-1.5,r=1,n=1,o=[0,1]){let i=0,s;N(o)==="array"?s=o[1]-o[0]:(s=0,o=[o,o]);const a=function(c){const u=ft*((e+120)/360+t*c),f=ga(o[0]+s*c,n),h=(i!==0?r[0]+c*i:r)*f*(1-f)/2,d=va(u),b=_a(u),g=f+h*(-.14861*d+1.78277*b),m=f+h*(-.29227*d-.90649*b),y=f+h*(1.97294*d);return k(xe([g*255,m*255,y*255,1]))};return a.start=function(c){return c==null?e:(e=c,a)},a.rotations=function(c){return c==null?t:(t=c,a)},a.gamma=function(c){return c==null?n:(n=c,a)},a.hue=function(c){return c==null?r:(r=c,N(r)==="array"?(i=r[1]-r[0],i===0&&(r=r[1])):i=0,a)},a.lightness=function(c){return c==null?o:(N(c)==="array"?(o=c,s=c[1]-c[0]):(o=[c,c],s=0),a)},a.scale=()=>k.scale(a),a.hue(r),a}const wa="0123456789abcdef",{floor:xa,random:Ca}=Math,ka=(e=Ca)=>{let t="#";for(let r=0;r<6;r++)t+=wa.charAt(xa(e()*16));return new _(t,"hex")},{log:qn,pow:Ra,floor:qa,abs:Ma}=Math;function Mn(e,t=null){const r={min:Number.MAX_VALUE,max:Number.MAX_VALUE*-1,sum:0,values:[],count:0};return N(e)==="object"&&(e=Object.values(e)),e.forEach(n=>{t&&N(n)==="object"&&(n=n[t]),n!=null&&!isNaN(n)&&(r.values.push(n),r.sum+=n,nr.max&&(r.max=n),r.count+=1)}),r.domain=[r.min,r.max],r.limits=(n,o)=>On(r,n,o),r}function On(e,t="equal",r=7){N(e)=="array"&&(e=Mn(e));const{min:n,max:o}=e,i=e.values.sort((a,c)=>a-c);if(r===1)return[n,o];const s=[];if(t.substr(0,1)==="c"&&(s.push(n),s.push(o)),t.substr(0,1)==="e"){s.push(n);for(let a=1;a 0");const a=Math.LOG10E*qn(n),c=Math.LOG10E*qn(o);s.push(n);for(let u=1;u200&&(l=!1)}const b={};for(let m=0;mm-y),s.push(g[0]);for(let m=1;m{e=new _(e),t=new _(t);const r=e.luminance(),n=t.luminance();return r>n?(r+.05)/(n+.05):(n+.05)/(r+.05)};const An=.027,Aa=5e-4,Sa=.1,Sn=1.14,ee=.022,$n=1.414,$a=(e,t)=>{e=new _(e),t=new _(t),e.alpha()<1&&(e=Nt(t,e,e.alpha(),"rgb"));const r=En(...e.rgb()),n=En(...t.rgb()),o=r>=ee?r:r+Math.pow(ee-r,$n),i=n>=ee?n:n+Math.pow(ee-n,$n),s=Math.pow(i,.56)-Math.pow(o,.57),a=Math.pow(i,.65)-Math.pow(o,.62),c=Math.abs(i-o)0?c-An:c+An)*100};function En(e,t,r){return .2126729*Math.pow(e/255,2.4)+.7151522*Math.pow(t/255,2.4)+.072175*Math.pow(r/255,2.4)}const{sqrt:dt,pow:F,min:Ea,max:La,atan2:Ln,abs:Nn,cos:re,sin:Tn,exp:Na,PI:jn}=Math;function Ta(e,t,r=1,n=1,o=1){var i=function(Ct){return 360*Ct/(2*jn)},s=function(Ct){return 2*jn*Ct/360};e=new _(e),t=new _(t);const[a,c,u]=Array.from(e.lab()),[f,l,h]=Array.from(t.lab()),d=(a+f)/2,b=dt(F(c,2)+F(u,2)),g=dt(F(l,2)+F(h,2)),m=(b+g)/2,y=.5*(1-dt(F(m,7)/(F(m,7)+F(25,7)))),L=c*(1+y),w=l*(1+y),x=dt(F(L,2)+F(u,2)),S=dt(F(w,2)+F(h,2)),R=(x+S)/2,M=i(Ln(u,L)),p=i(Ln(h,w)),C=M>=0?M:M+360,O=p>=0?p:p+360,q=Nn(C-O)>180?(C+O+360)/2:(C+O)/2,T=1-.17*re(s(q-30))+.24*re(s(2*q))+.32*re(s(3*q+6))-.2*re(s(4*q-63));let A=O-C;A=Nn(A)<=180?A:O<=C?A+360:A-360,A=2*dt(x*S)*Tn(s(A)/2);const P=f-a,G=S-x,Y=1+.015*F(d-50,2)/dt(20+F(d-50,2)),U=1+.045*R,ut=1+.015*R*T,xt=30*Na(-F((q-275)/25,2)),lt=-(2*dt(F(R,7)/(F(R,7)+F(25,7))))*Tn(2*s(xt)),Mt=dt(F(P/(r*Y),2)+F(G/(n*U),2)+F(A/(o*ut),2)+lt*(G/(n*U))*(A/(o*ut)));return La(0,Ea(100,Mt))}function ja(e,t,r="lab"){e=new _(e),t=new _(t);const n=e.get(r),o=t.get(r);let i=0;for(let s in n){const a=(n[s]||0)-(o[s]||0);i+=a*a}return Math.sqrt(i)}const Pa=(...e)=>{try{return new _(...e),!0}catch{return!1}},za={cool(){return te([k.hsl(180,1,.9),k.hsl(250,.7,.4)])},hot(){return te(["#000","#f00","#ff0","#fff"]).mode("rgb")}},Be={OrRd:["#fff7ec","#fee8c8","#fdd49e","#fdbb84","#fc8d59","#ef6548","#d7301f","#b30000","#7f0000"],PuBu:["#fff7fb","#ece7f2","#d0d1e6","#a6bddb","#74a9cf","#3690c0","#0570b0","#045a8d","#023858"],BuPu:["#f7fcfd","#e0ecf4","#bfd3e6","#9ebcda","#8c96c6","#8c6bb1","#88419d","#810f7c","#4d004b"],Oranges:["#fff5eb","#fee6ce","#fdd0a2","#fdae6b","#fd8d3c","#f16913","#d94801","#a63603","#7f2704"],BuGn:["#f7fcfd","#e5f5f9","#ccece6","#99d8c9","#66c2a4","#41ae76","#238b45","#006d2c","#00441b"],YlOrBr:["#ffffe5","#fff7bc","#fee391","#fec44f","#fe9929","#ec7014","#cc4c02","#993404","#662506"],YlGn:["#ffffe5","#f7fcb9","#d9f0a3","#addd8e","#78c679","#41ab5d","#238443","#006837","#004529"],Reds:["#fff5f0","#fee0d2","#fcbba1","#fc9272","#fb6a4a","#ef3b2c","#cb181d","#a50f15","#67000d"],RdPu:["#fff7f3","#fde0dd","#fcc5c0","#fa9fb5","#f768a1","#dd3497","#ae017e","#7a0177","#49006a"],Greens:["#f7fcf5","#e5f5e0","#c7e9c0","#a1d99b","#74c476","#41ab5d","#238b45","#006d2c","#00441b"],YlGnBu:["#ffffd9","#edf8b1","#c7e9b4","#7fcdbb","#41b6c4","#1d91c0","#225ea8","#253494","#081d58"],Purples:["#fcfbfd","#efedf5","#dadaeb","#bcbddc","#9e9ac8","#807dba","#6a51a3","#54278f","#3f007d"],GnBu:["#f7fcf0","#e0f3db","#ccebc5","#a8ddb5","#7bccc4","#4eb3d3","#2b8cbe","#0868ac","#084081"],Greys:["#ffffff","#f0f0f0","#d9d9d9","#bdbdbd","#969696","#737373","#525252","#252525","#000000"],YlOrRd:["#ffffcc","#ffeda0","#fed976","#feb24c","#fd8d3c","#fc4e2a","#e31a1c","#bd0026","#800026"],PuRd:["#f7f4f9","#e7e1ef","#d4b9da","#c994c7","#df65b0","#e7298a","#ce1256","#980043","#67001f"],Blues:["#f7fbff","#deebf7","#c6dbef","#9ecae1","#6baed6","#4292c6","#2171b5","#08519c","#08306b"],PuBuGn:["#fff7fb","#ece2f0","#d0d1e6","#a6bddb","#67a9cf","#3690c0","#02818a","#016c59","#014636"],Viridis:["#440154","#482777","#3f4a8a","#31678e","#26838f","#1f9d8a","#6cce5a","#b6de2b","#fee825"],Spectral:["#9e0142","#d53e4f","#f46d43","#fdae61","#fee08b","#ffffbf","#e6f598","#abdda4","#66c2a5","#3288bd","#5e4fa2"],RdYlGn:["#a50026","#d73027","#f46d43","#fdae61","#fee08b","#ffffbf","#d9ef8b","#a6d96a","#66bd63","#1a9850","#006837"],RdBu:["#67001f","#b2182b","#d6604d","#f4a582","#fddbc7","#f7f7f7","#d1e5f0","#92c5de","#4393c3","#2166ac","#053061"],PiYG:["#8e0152","#c51b7d","#de77ae","#f1b6da","#fde0ef","#f7f7f7","#e6f5d0","#b8e186","#7fbc41","#4d9221","#276419"],PRGn:["#40004b","#762a83","#9970ab","#c2a5cf","#e7d4e8","#f7f7f7","#d9f0d3","#a6dba0","#5aae61","#1b7837","#00441b"],RdYlBu:["#a50026","#d73027","#f46d43","#fdae61","#fee090","#ffffbf","#e0f3f8","#abd9e9","#74add1","#4575b4","#313695"],BrBG:["#543005","#8c510a","#bf812d","#dfc27d","#f6e8c3","#f5f5f5","#c7eae5","#80cdc1","#35978f","#01665e","#003c30"],RdGy:["#67001f","#b2182b","#d6604d","#f4a582","#fddbc7","#ffffff","#e0e0e0","#bababa","#878787","#4d4d4d","#1a1a1a"],PuOr:["#7f3b08","#b35806","#e08214","#fdb863","#fee0b6","#f7f7f7","#d8daeb","#b2abd2","#8073ac","#542788","#2d004b"],Set2:["#66c2a5","#fc8d62","#8da0cb","#e78ac3","#a6d854","#ffd92f","#e5c494","#b3b3b3"],Accent:["#7fc97f","#beaed4","#fdc086","#ffff99","#386cb0","#f0027f","#bf5b17","#666666"],Set1:["#e41a1c","#377eb8","#4daf4a","#984ea3","#ff7f00","#ffff33","#a65628","#f781bf","#999999"],Set3:["#8dd3c7","#ffffb3","#bebada","#fb8072","#80b1d3","#fdb462","#b3de69","#fccde5","#d9d9d9","#bc80bd","#ccebc5","#ffed6f"],Dark2:["#1b9e77","#d95f02","#7570b3","#e7298a","#66a61e","#e6ab02","#a6761d","#666666"],Paired:["#a6cee3","#1f78b4","#b2df8a","#33a02c","#fb9a99","#e31a1c","#fdbf6f","#ff7f00","#cab2d6","#6a3d9a","#ffff99","#b15928"],Pastel2:["#b3e2cd","#fdcdac","#cbd5e8","#f4cae4","#e6f5c9","#fff2ae","#f1e2cc","#cccccc"],Pastel1:["#fbb4ae","#b3cde3","#ccebc5","#decbe4","#fed9a6","#ffffcc","#e5d8bd","#fddaec","#f2f2f2"]},Pn=Object.keys(Be),zn=new Map(Pn.map(e=>[e.toLowerCase(),e])),Ba=typeof Proxy=="function"?new Proxy(Be,{get(e,t){const r=t.toLowerCase();if(zn.has(r))return e[zn.get(r)]},getOwnPropertyNames(){return Object.getOwnPropertyNames(Pn)}}):Be,Ia=(...e)=>{e=E(e,"cmyk");const[t,r,n,o]=e,i=e.length>4?e[4]:1;return o===1?[0,0,0,i]:[t>=1?0:255*(1-t)*(1-o),r>=1?0:255*(1-r)*(1-o),n>=1?0:255*(1-n)*(1-o),i]},{max:Bn}=Math,Ga=(...e)=>{let[t,r,n]=E(e,"rgb");t=t/255,r=r/255,n=n/255;const o=1-Bn(t,Bn(r,n)),i=o<1?1/(1-o):0,s=(1-t-o)*i,a=(1-r-o)*i,c=(1-n-o)*i;return[s,a,c,o]};_.prototype.cmyk=function(){return Ga(this._rgb)},Object.assign(k,{cmyk:(...e)=>new _(...e,"cmyk")}),$.format.cmyk=Ia,$.autodetect.push({p:2,test:(...e)=>{if(e=E(e,"cmyk"),N(e)==="array"&&e.length===4)return"cmyk"}});const Fa=(...e)=>{const t=E(e,"hsla");let r=Et(e)||"lsa";return t[0]=et(t[0]||0)+"deg",t[1]=et(t[1]*100)+"%",t[2]=et(t[2]*100)+"%",r==="hsla"||t.length>3&&t[3]<1?(t[3]="/ "+(t.length>3?t[3]:1),r="hsla"):t.length=3,`${r.substr(0,3)}(${t.join(" ")})`},Ka=(...e)=>{const t=E(e,"lab");let r=Et(e)||"lab";return t[0]=et(t[0])+"%",t[1]=et(t[1]),t[2]=et(t[2]),r==="laba"||t.length>3&&t[3]<1?t[3]="/ "+(t.length>3?t[3]:1):t.length=3,`lab(${t.join(" ")})`},Xa=(...e)=>{const t=E(e,"lch");let r=Et(e)||"lab";return t[0]=et(t[0])+"%",t[1]=et(t[1]),t[2]=isNaN(t[2])?"none":et(t[2])+"deg",r==="lcha"||t.length>3&&t[3]<1?t[3]="/ "+(t.length>3?t[3]:1):t.length=3,`lch(${t.join(" ")})`},Da=(...e)=>{const t=E(e,"lab");return t[0]=et(t[0]*100)+"%",t[1]=Ce(t[1]),t[2]=Ce(t[2]),t.length>3&&t[3]<1?t[3]="/ "+(t.length>3?t[3]:1):t.length=3,`oklab(${t.join(" ")})`},In=(...e)=>{const[t,r,n,...o]=E(e,"rgb"),[i,s,a]=Te(t,r,n),[c,u,f]=yn(i,s,a);return[c,u,f,...o.length>0&&o[0]<1?[o[0]]:[]]},Va=(...e)=>{const t=E(e,"lch");return t[0]=et(t[0]*100)+"%",t[1]=Ce(t[1]),t[2]=isNaN(t[2])?"none":et(t[2])+"deg",t.length>3&&t[3]<1?t[3]="/ "+(t.length>3?t[3]:1):t.length=3,`oklch(${t.join(" ")})`},{round:Ie}=Math,Ya=(...e)=>{const t=E(e,"rgba");let r=Et(e)||"rgb";if(r.substr(0,3)==="hsl")return Fa(xn(t),r);if(r.substr(0,3)==="lab"){const n=Kt();ht("d50");const o=Ka(Me(t),r);return ht(n),o}if(r.substr(0,3)==="lch"){const n=Kt();ht("d50");const o=Xa($e(t),r);return ht(n),o}return r.substr(0,5)==="oklab"?Da(Te(t)):r.substr(0,5)==="oklch"?Va(In(t)):(t[0]=Ie(t[0]),t[1]=Ie(t[1]),t[2]=Ie(t[2]),(r==="rgba"||t.length>3&&t[3]<1)&&(t[3]="/ "+(t.length>3?t[3]:1),r="rgba"),`${r.substr(0,3)}(${t.slice(0,r==="rgb"?3:4).join(" ")})`)},Gn=(...e)=>{e=E(e,"lch");const[t,r,n,...o]=e,[i,s,a]=vn(t,r,n),[c,u,f]=Ne(i,s,a);return[c,u,f,...o.length>0&&o[0]<1?[o[0]]:[]]},bt=/((?:-?\d+)|(?:-?\d+(?:\.\d+)?)%|none)/.source,ot=/((?:-?(?:\d+(?:\.\d*)?|\.\d+)%?)|none)/.source,ne=/((?:-?(?:\d+(?:\.\d*)?|\.\d+)%)|none)/.source,rt=/\s*/.source,zt=/\s+/.source,Ge=/\s*,\s*/.source,oe=/((?:-?(?:\d+(?:\.\d*)?|\.\d+)(?:deg)?)|none)/.source,Bt=/\s*(?:\/\s*((?:[01]|[01]?\.\d+)|\d+(?:\.\d+)?%))?/.source,Fn=new RegExp("^rgba?\\("+rt+[bt,bt,bt].join(zt)+Bt+"\\)$"),Kn=new RegExp("^rgb\\("+rt+[bt,bt,bt].join(Ge)+rt+"\\)$"),Xn=new RegExp("^rgba\\("+rt+[bt,bt,bt,ot].join(Ge)+rt+"\\)$"),Dn=new RegExp("^hsla?\\("+rt+[oe,ne,ne].join(zt)+Bt+"\\)$"),Vn=new RegExp("^hsl?\\("+rt+[oe,ne,ne].join(Ge)+rt+"\\)$"),Yn=/^hsla\(\s*(-?\d+(?:\.\d+)?),\s*(-?\d+(?:\.\d+)?)%\s*,\s*(-?\d+(?:\.\d+)?)%\s*,\s*([01]|[01]?\.\d+)\)$/,Zn=new RegExp("^lab\\("+rt+[ot,ot,ot].join(zt)+Bt+"\\)$"),Jn=new RegExp("^lch\\("+rt+[ot,ot,oe].join(zt)+Bt+"\\)$"),Hn=new RegExp("^oklab\\("+rt+[ot,ot,ot].join(zt)+Bt+"\\)$"),Wn=new RegExp("^oklch\\("+rt+[ot,ot,oe].join(zt)+Bt+"\\)$"),{round:Un}=Math,It=e=>e.map((t,r)=>r<=2?qt(Un(t),0,255):t),K=(e,t=0,r=100,n=!1)=>(typeof e=="string"&&e.endsWith("%")&&(e=parseFloat(e.substring(0,e.length-1))/100,n?e=t+(e+1)*.5*(r-t):e=t+e*(r-t)),+e),W=(e,t)=>e==="none"?t:e,Fe=e=>{if(e=e.toLowerCase().trim(),e==="transparent")return[0,0,0,0];let t;if($.format.named)try{return $.format.named(e)}catch{}if((t=e.match(Fn))||(t=e.match(Kn))){let r=t.slice(1,4);for(let o=0;o<3;o++)r[o]=+K(W(r[o],0),0,255);r=It(r);const n=t[4]!==void 0?+K(t[4],0,1):1;return r[3]=n,r}if(t=e.match(Xn)){const r=t.slice(1,5);for(let n=0;n<4;n++)r[n]=+K(r[n],0,255);return r}if((t=e.match(Dn))||(t=e.match(Vn))){const r=t.slice(1,4);r[0]=+W(r[0].replace("deg",""),0),r[1]=+K(W(r[1],0),0,100)*.01,r[2]=+K(W(r[2],0),0,100)*.01;const n=It(Le(r)),o=t[4]!==void 0?+K(t[4],0,1):1;return n[3]=o,n}if(t=e.match(Yn)){const r=t.slice(1,4);r[1]*=.01,r[2]*=.01;const n=Le(r);for(let o=0;o<3;o++)n[o]=Un(n[o]);return n[3]=+t[4],n}if(t=e.match(Zn)){const r=t.slice(1,4);r[0]=K(W(r[0],0),0,100),r[1]=K(W(r[1],0),-125,125,!0),r[2]=K(W(r[2],0),-125,125,!0);const n=Kt();ht("d50");const o=It(Re(r));ht(n);const i=t[4]!==void 0?+K(t[4],0,1):1;return o[3]=i,o}if(t=e.match(Jn)){const r=t.slice(1,4);r[0]=K(r[0],0,100),r[1]=K(W(r[1],0),0,150,!1),r[2]=+W(r[2].replace("deg",""),0);const n=Kt();ht("d50");const o=It(Se(r));ht(n);const i=t[4]!==void 0?+K(t[4],0,1):1;return o[3]=i,o}if(t=e.match(Hn)){const r=t.slice(1,4);r[0]=K(W(r[0],0),0,1),r[1]=K(W(r[1],0),-.4,.4,!0),r[2]=K(W(r[2],0),-.4,.4,!0);const n=It(Ne(r)),o=t[4]!==void 0?+K(t[4],0,1):1;return n[3]=o,n}if(t=e.match(Wn)){const r=t.slice(1,4);r[0]=K(W(r[0],0),0,1),r[1]=K(W(r[1],0),0,.4,!1),r[2]=+W(r[2].replace("deg",""),0);const n=It(Gn(r)),o=t[4]!==void 0?+K(t[4],0,1):1;return n[3]=o,n}};Fe.test=e=>Fn.test(e)||Dn.test(e)||Zn.test(e)||Jn.test(e)||Hn.test(e)||Wn.test(e)||Kn.test(e)||Xn.test(e)||Vn.test(e)||Yn.test(e)||e==="transparent",_.prototype.css=function(e){return Ya(this._rgb,e)};const Za=(...e)=>new _(...e,"css");k.css=Za,$.format.css=Fe,$.autodetect.push({p:5,test:(e,...t)=>{if(!t.length&&N(e)==="string"&&Fe.test(e))return"css"}}),$.format.gl=(...e)=>{const t=E(e,"rgba");return t[0]*=255,t[1]*=255,t[2]*=255,t};const Ja=(...e)=>new _(...e,"gl");k.gl=Ja,_.prototype.gl=function(){const e=this._rgb;return[e[0]/255,e[1]/255,e[2]/255,e[3]]},_.prototype.hex=function(e){return mn(this._rgb,e)};const Ha=(...e)=>new _(...e,"hex");k.hex=Ha,$.format.hex=pn,$.autodetect.push({p:4,test:(e,...t)=>{if(!t.length&&N(e)==="string"&&[3,4,5,6,7,8,9].indexOf(e.length)>=0)return"hex"}});const{log:se}=Math,Qn=e=>{const t=e/100;let r,n,o;return t<66?(r=255,n=t<6?0:-155.25485562709179-.44596950469579133*(n=t-2)+104.49216199393888*se(n),o=t<20?0:-254.76935184120902+.8274096064007395*(o=t-10)+115.67994401066147*se(o)):(r=351.97690566805693+.114206453784165*(r=t-55)-40.25366309332127*se(r),n=325.4494125711974+.07943456536662342*(n=t-50)-28.0852963507957*se(n),o=255),[r,n,o,1]},{round:Wa}=Math,Ua=(...e)=>{const t=E(e,"rgb"),r=t[0],n=t[2];let o=1e3,i=4e4;const s=.4;let a;for(;i-o>s;){a=(i+o)*.5;const c=Qn(a);c[2]/c[0]>=n/r?i=a:o=a}return Wa(a)};_.prototype.temp=_.prototype.kelvin=_.prototype.temperature=function(){return Ua(this._rgb)};const Ke=(...e)=>new _(...e,"temp");Object.assign(k,{temp:Ke,kelvin:Ke,temperature:Ke}),$.format.temp=$.format.kelvin=$.format.temperature=Qn,_.prototype.oklch=function(){return In(this._rgb)},Object.assign(k,{oklch:(...e)=>new _(...e,"oklch")}),$.format.oklch=Gn,$.autodetect.push({p:2,test:(...e)=>{if(e=E(e,"oklch"),N(e)==="array"&&e.length===3)return"oklch"}}),Object.assign(k,{analyze:Mn,average:ra,bezier:ca,blend:nt,brewer:Ba,Color:_,colors:Lt,contrast:Oa,contrastAPCA:$a,cubehelix:ya,deltaE:Ta,distance:ja,input:$,interpolate:Nt,limits:On,mix:Nt,random:ka,scale:te,scales:za,valid:Pa});class v{constructor(){this.hex="#000000",this.rgb_r=0,this.rgb_g=0,this.rgb_b=0,this.xyz_x=0,this.xyz_y=0,this.xyz_z=0,this.luv_l=0,this.luv_u=0,this.luv_v=0,this.lch_l=0,this.lch_c=0,this.lch_h=0,this.hsluv_h=0,this.hsluv_s=0,this.hsluv_l=0,this.hpluv_h=0,this.hpluv_p=0,this.hpluv_l=0,this.r0s=0,this.r0i=0,this.r1s=0,this.r1i=0,this.g0s=0,this.g0i=0,this.g1s=0,this.g1i=0,this.b0s=0,this.b0i=0,this.b1s=0,this.b1i=0}static fromLinear(t){return t<=.0031308?12.92*t:1.055*Math.pow(t,.4166666666666667)-.055}static toLinear(t){return t>.04045?Math.pow((t+.055)/1.055,2.4):t/12.92}static yToL(t){return t<=v.epsilon?t/v.refY*v.kappa:116*Math.pow(t/v.refY,.3333333333333333)-16}static lToY(t){return t<=8?v.refY*t/v.kappa:v.refY*Math.pow((t+16)/116,3)}static rgbChannelToHex(t){const r=Math.round(t*255),n=r%16,o=(r-n)/16|0;return v.hexChars.charAt(o)+v.hexChars.charAt(n)}static hexToRgbChannel(t,r){const n=v.hexChars.indexOf(t.charAt(r)),o=v.hexChars.indexOf(t.charAt(r+1));return(n*16+o)/255}static distanceFromOriginAngle(t,r,n){const o=r/(Math.sin(n)-t*Math.cos(n));return o<0?1/0:o}static distanceFromOrigin(t,r){return Math.abs(r)/Math.sqrt(Math.pow(t,2)+1)}static min6(t,r,n,o,i,s){return Math.min(t,Math.min(r,Math.min(n,Math.min(o,Math.min(i,s)))))}rgbToHex(){this.hex="#",this.hex+=v.rgbChannelToHex(this.rgb_r),this.hex+=v.rgbChannelToHex(this.rgb_g),this.hex+=v.rgbChannelToHex(this.rgb_b)}hexToRgb(){this.hex=this.hex.toLowerCase(),this.rgb_r=v.hexToRgbChannel(this.hex,1),this.rgb_g=v.hexToRgbChannel(this.hex,3),this.rgb_b=v.hexToRgbChannel(this.hex,5)}xyzToRgb(){this.rgb_r=v.fromLinear(v.m_r0*this.xyz_x+v.m_r1*this.xyz_y+v.m_r2*this.xyz_z),this.rgb_g=v.fromLinear(v.m_g0*this.xyz_x+v.m_g1*this.xyz_y+v.m_g2*this.xyz_z),this.rgb_b=v.fromLinear(v.m_b0*this.xyz_x+v.m_b1*this.xyz_y+v.m_b2*this.xyz_z)}rgbToXyz(){const t=v.toLinear(this.rgb_r),r=v.toLinear(this.rgb_g),n=v.toLinear(this.rgb_b);this.xyz_x=.41239079926595*t+.35758433938387*r+.18048078840183*n,this.xyz_y=.21263900587151*t+.71516867876775*r+.072192315360733*n,this.xyz_z=.019330818715591*t+.11919477979462*r+.95053215224966*n}xyzToLuv(){const t=this.xyz_x+15*this.xyz_y+3*this.xyz_z;let r=4*this.xyz_x,n=9*this.xyz_y;t!==0?(r/=t,n/=t):(r=NaN,n=NaN),this.luv_l=v.yToL(this.xyz_y),this.luv_l===0?(this.luv_u=0,this.luv_v=0):(this.luv_u=13*this.luv_l*(r-v.refU),this.luv_v=13*this.luv_l*(n-v.refV))}luvToXyz(){if(this.luv_l===0){this.xyz_x=0,this.xyz_y=0,this.xyz_z=0;return}const t=this.luv_u/(13*this.luv_l)+v.refU,r=this.luv_v/(13*this.luv_l)+v.refV;this.xyz_y=v.lToY(this.luv_l),this.xyz_x=0-9*this.xyz_y*t/((t-4)*r-t*r),this.xyz_z=(9*this.xyz_y-15*r*this.xyz_y-r*this.xyz_x)/(3*r)}luvToLch(){if(this.lch_l=this.luv_l,this.lch_c=Math.sqrt(this.luv_u*this.luv_u+this.luv_v*this.luv_v),this.lch_c<1e-8)this.lch_h=0;else{const t=Math.atan2(this.luv_v,this.luv_u);this.lch_h=t*180/Math.PI,this.lch_h<0&&(this.lch_h=360+this.lch_h)}}lchToLuv(){const t=this.lch_h/180*Math.PI;this.luv_l=this.lch_l,this.luv_u=Math.cos(t)*this.lch_c,this.luv_v=Math.sin(t)*this.lch_c}calculateBoundingLines(t){const r=Math.pow(t+16,3)/1560896,n=r>v.epsilon?r:t/v.kappa,o=n*(284517*v.m_r0-94839*v.m_r2),i=n*(838422*v.m_r2+769860*v.m_r1+731718*v.m_r0),s=n*(632260*v.m_r2-126452*v.m_r1),a=n*(284517*v.m_g0-94839*v.m_g2),c=n*(838422*v.m_g2+769860*v.m_g1+731718*v.m_g0),u=n*(632260*v.m_g2-126452*v.m_g1),f=n*(284517*v.m_b0-94839*v.m_b2),l=n*(838422*v.m_b2+769860*v.m_b1+731718*v.m_b0),h=n*(632260*v.m_b2-126452*v.m_b1);this.r0s=o/s,this.r0i=i*t/s,this.r1s=o/(s+126452),this.r1i=(i-769860)*t/(s+126452),this.g0s=a/u,this.g0i=c*t/u,this.g1s=a/(u+126452),this.g1i=(c-769860)*t/(u+126452),this.b0s=f/h,this.b0i=l*t/h,this.b1s=f/(h+126452),this.b1i=(l-769860)*t/(h+126452)}calcMaxChromaHpluv(){const t=v.distanceFromOrigin(this.r0s,this.r0i),r=v.distanceFromOrigin(this.r1s,this.r1i),n=v.distanceFromOrigin(this.g0s,this.g0i),o=v.distanceFromOrigin(this.g1s,this.g1i),i=v.distanceFromOrigin(this.b0s,this.b0i),s=v.distanceFromOrigin(this.b1s,this.b1i);return v.min6(t,r,n,o,i,s)}calcMaxChromaHsluv(t){const r=t/360*Math.PI*2,n=v.distanceFromOriginAngle(this.r0s,this.r0i,r),o=v.distanceFromOriginAngle(this.r1s,this.r1i,r),i=v.distanceFromOriginAngle(this.g0s,this.g0i,r),s=v.distanceFromOriginAngle(this.g1s,this.g1i,r),a=v.distanceFromOriginAngle(this.b0s,this.b0i,r),c=v.distanceFromOriginAngle(this.b1s,this.b1i,r);return v.min6(n,o,i,s,a,c)}hsluvToLch(){if(this.hsluv_l>99.9999999)this.lch_l=100,this.lch_c=0;else if(this.hsluv_l<1e-8)this.lch_l=0,this.lch_c=0;else{this.lch_l=this.hsluv_l,this.calculateBoundingLines(this.hsluv_l);const t=this.calcMaxChromaHsluv(this.hsluv_h);this.lch_c=t/100*this.hsluv_s}this.lch_h=this.hsluv_h}lchToHsluv(){if(this.lch_l>99.9999999)this.hsluv_s=0,this.hsluv_l=100;else if(this.lch_l<1e-8)this.hsluv_s=0,this.hsluv_l=0;else{this.calculateBoundingLines(this.lch_l);const t=this.calcMaxChromaHsluv(this.lch_h);this.hsluv_s=this.lch_c/t*100,this.hsluv_l=this.lch_l}this.hsluv_h=this.lch_h}hpluvToLch(){if(this.hpluv_l>99.9999999)this.lch_l=100,this.lch_c=0;else if(this.hpluv_l<1e-8)this.lch_l=0,this.lch_c=0;else{this.lch_l=this.hpluv_l,this.calculateBoundingLines(this.hpluv_l);const t=this.calcMaxChromaHpluv();this.lch_c=t/100*this.hpluv_p}this.lch_h=this.hpluv_h}lchToHpluv(){if(this.lch_l>99.9999999)this.hpluv_p=0,this.hpluv_l=100;else if(this.lch_l<1e-8)this.hpluv_p=0,this.hpluv_l=0;else{this.calculateBoundingLines(this.lch_l);const t=this.calcMaxChromaHpluv();this.hpluv_p=this.lch_c/t*100,this.hpluv_l=this.lch_l}this.hpluv_h=this.lch_h}hsluvToRgb(){this.hsluvToLch(),this.lchToLuv(),this.luvToXyz(),this.xyzToRgb()}hpluvToRgb(){this.hpluvToLch(),this.lchToLuv(),this.luvToXyz(),this.xyzToRgb()}hsluvToHex(){this.hsluvToRgb(),this.rgbToHex()}hpluvToHex(){this.hpluvToRgb(),this.rgbToHex()}rgbToHsluv(){this.rgbToXyz(),this.xyzToLuv(),this.luvToLch(),this.lchToHpluv(),this.lchToHsluv()}rgbToHpluv(){this.rgbToXyz(),this.xyzToLuv(),this.luvToLch(),this.lchToHpluv(),this.lchToHpluv()}hexToHsluv(){this.hexToRgb(),this.rgbToHsluv()}hexToHpluv(){this.hexToRgb(),this.rgbToHpluv()}}v.hexChars="0123456789abcdef",v.refY=1,v.refU=.19783000664283,v.refV=.46831999493879,v.kappa=903.2962962,v.epsilon=.0088564516,v.m_r0=3.240969941904521,v.m_r1=-1.537383177570093,v.m_r2=-.498610760293,v.m_g0=-.96924363628087,v.m_g1=1.87596750150772,v.m_g2=.041555057407175,v.m_b0=.055630079696993,v.m_b1=-.20397695888897,v.m_b2=1.056971514242878;function to(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var ie={exports:{}},Xe,eo;function Xt(){if(eo)return Xe;eo=1;function e(t,r){return Object.prototype.hasOwnProperty.call(t,r)}return Xe=e,Xe}var De,ro;function Ve(){if(ro)return De;ro=1;var e=Xt(),t,r;function n(){r=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],t=!0;for(var s in{toString:null})t=!1}function o(s,a,c){var u,f=0;t==null&&n();for(u in s)if(i(a,s,u,c)===!1)break;if(t)for(var l=s.constructor,h=!!l&&s===l.prototype;(u=r[f++])&&!((u!=="constructor"||!h&&e(s,u))&&s[u]!==Object.prototype[u]&&i(a,s,u,c)===!1););}function i(s,a,c,u){return s.call(u,a[c],c,a)}return De=o,De}var Ye,no;function oo(){if(no)return Ye;no=1;var e=Ve();function t(r){var n=[];return e(r,function(o,i){typeof o=="function"&&n.push(i)}),n.sort()}return Ye=t,Ye}var Ze,so;function Dt(){if(so)return Ze;so=1;function e(t,r,n){var o=t.length;r==null?r=0:r<0?r=Math.max(o+r,0):r=Math.min(r,o),n==null?n=o:n<0?n=Math.max(o+n,0):n=Math.min(n,o);for(var i=[];r1?n(arguments,1):e(i);r(a,function(c){i[c]=t(i[c],i)})}return Ue=o,Ue}var Qe,uo;function V(){if(uo)return Qe;uo=1;var e=Xt(),t=Ve();function r(n,o,i){t(n,function(s,a){if(e(n,a))return o.call(i,n[a],a,n)})}return Qe=r,Qe}var tr,lo;function ec(){if(lo)return tr;lo=1;function e(t){return t}return tr=e,tr}var er,fo;function ho(){if(fo)return er;fo=1;function e(t){return function(r){return r[t]}}return er=e,er}var rr,bo;function nr(){if(bo)return rr;bo=1;var e=/^\[object (.*)\]$/,t=Object.prototype.toString,r;function n(o){return o===null?"Null":o===r?"Undefined":e.exec(t.call(o))[1]}return rr=n,rr}var or,po;function sr(){if(po)return or;po=1;var e=nr();function t(r,n){return e(r)===n}return or=t,or}var ir,mo;function rc(){if(mo)return ir;mo=1;var e=sr(),t=Array.isArray||function(r){return e(r,"Array")};return ir=t,ir}var ar,go;function _o(){if(go)return ar;go=1;var e=V(),t=rc();function r(s,a){for(var c=-1,u=s.length;++cs&&(s=c,i=a);return i}return Or=t,Or}var Ar,Do;function Sr(){if(Do)return Ar;Do=1;var e=V();function t(r){var n=[];return e(r,function(o,i){n.push(o)}),n}return Ar=t,Ar}var $r,Vo;function bc(){if(Vo)return $r;Vo=1;var e=dc(),t=Sr();function r(n,o){return e(t(n),o)}return $r=r,$r}var Er,Yo;function Zo(){if(Yo)return Er;Yo=1;var e=V();function t(n,o){for(var i=0,s=arguments.length,a;++i2;if(!t(n)&&!a)throw new Error("reduce of empty object with no initial value");return e(n,function(c,u,f){a?i=o.call(s,i,c,u,f):(i=c,a=!0)}),i}return Dr=r,Dr}var Vr,ls;function qc(){if(ls)return Vr;ls=1;var e=Lo(),t=yt();function r(n,o,i){return o=t(o,i),e(n,function(s,a,c){return!o(s,a,c)},i)}return Vr=r,Vr}var Yr,fs;function Mc(){if(fs)return Yr;fs=1;var e=sr();function t(r){return e(r,"Function")}return Yr=t,Yr}var Zr,hs;function Oc(){if(hs)return Zr;hs=1;var e=Mc();function t(r,n){var o=r[n];if(o!==void 0)return e(o)?o.call(r):o}return Zr=t,Zr}var Jr,ds;function Ac(){if(ds)return Jr;ds=1;var e=es();function t(r,n,o){var i=/^(.+)\.(.+)$/.exec(n);i?e(r,i[1])[i[2]]=o:r[n]=o}return Jr=t,Jr}var Hr,bs;function Sc(){if(bs)return Hr;bs=1;var e=Bo();function t(r,n){if(e(r,n)){for(var o=n.split("."),i=o.pop();n=o.shift();)r=r[n];return delete r[i]}else return!0}return Hr=t,Hr}var Wr,ps;function Ur(){return ps||(ps=1,Wr={bindAll:tc(),contains:nc(),deepFillIn:oc(),deepMatches:_o(),deepMixIn:sc(),equals:ac(),every:qo(),fillIn:cc(),filter:Lo(),find:uc(),flatten:lc(),forIn:Ve(),forOwn:V(),functions:oo(),get:Po(),has:Bo(),hasOwn:Xt(),keys:fc(),map:Fo(),matches:hc(),max:bc(),merge:gc(),min:vc(),mixIn:Zo(),namespace:es(),omit:xc(),pick:Cc(),pluck:kc(),reduce:Rc(),reject:qc(),result:Oc(),set:Ac(),size:cs(),some:lr(),unset:Sc(),values:Sr()}),Wr}var ms;function gs(){return ms||(ms=1,(function(e,t){Object.defineProperty(t,"__esModule",{value:!0});var r=Ur(),n={A:{x:.44758,y:.40745},C:{x:.31006,y:.31616},D50:{x:.34567,y:.35851},D65:{x:.31272,y:.32903},D55:{x:.33243,y:.34744},D75:{x:.29903,y:.31488}},o=(0,r.map)(n,function(i){var s=100*(i.x/i.y),a=100,c=100*(1-i.x-i.y)/i.y;return[s,a,c]});t.default=o,e.exports=t.default})(ie,ie.exports)),ie.exports}var ae={exports:{}},_s;function vs(){return _s||(_s=1,(function(e,t){Object.defineProperty(t,"__esModule",{value:!0});var r=Math,n=r.pow,o=r.sign,i=r.abs,s={decode:function(l){return l<=.04045?l/12.92:n((l+.055)/1.055,2.4)},encode:function(l){return l<=.0031308?12.92*l:1.055*n(l,1/2.4)-.055}},a={encode:function(l){return l<.001953125?16*l:n(l,1/1.8)},decode:function(l){return l<16*.001953125?l/16:n(l,1.8)}};function c(f){return{decode:function(h){return o(h)*n(i(h),f)},encode:function(h){return o(h)*n(i(h),1/f)}}}var u={sRGB:{r:{x:.64,y:.33},g:{x:.3,y:.6},b:{x:.15,y:.06},gamma:s},"Adobe RGB":{r:{x:.64,y:.33},g:{x:.21,y:.71},b:{x:.15,y:.06},gamma:c(2.2)},"Wide Gamut RGB":{r:{x:.7347,y:.2653},g:{x:.1152,y:.8264},b:{x:.1566,y:.0177},gamma:c(563/256)},"ProPhoto RGB":{r:{x:.7347,y:.2653},g:{x:.1596,y:.8404},b:{x:.0366,y:1e-4},gamma:a}};t.default=u,e.exports=t.default})(ae,ae.exports)),ae.exports}var pt={},ys;function ws(){if(ys)return pt;ys=1,Object.defineProperty(pt,"__esModule",{value:!0});function e(s){return[[s[0][0],s[1][0],s[2][0]],[s[0][1],s[1][1],s[2][1]],[s[0][2],s[1][2],s[2][2]]]}function t(s){return s[0][0]*(s[2][2]*s[1][1]-s[2][1]*s[1][2])+s[1][0]*(s[2][1]*s[0][2]-s[2][2]*s[0][1])+s[2][0]*(s[1][2]*s[0][1]-s[1][1]*s[0][2])}function r(s){var a=1/t(s);return[[(s[2][2]*s[1][1]-s[2][1]*s[1][2])*a,(s[2][1]*s[0][2]-s[2][2]*s[0][1])*a,(s[1][2]*s[0][1]-s[1][1]*s[0][2])*a],[(s[2][0]*s[1][2]-s[2][2]*s[1][0])*a,(s[2][2]*s[0][0]-s[2][0]*s[0][2])*a,(s[1][0]*s[0][2]-s[1][2]*s[0][0])*a],[(s[2][1]*s[1][0]-s[2][0]*s[1][1])*a,(s[2][0]*s[0][1]-s[2][1]*s[0][0])*a,(s[1][1]*s[0][0]-s[1][0]*s[0][1])*a]]}function n(s,a){return[s[0][0]*a[0]+s[0][1]*a[1]+s[0][2]*a[2],s[1][0]*a[0]+s[1][1]*a[1]+s[1][2]*a[2],s[2][0]*a[0]+s[2][1]*a[1]+s[2][2]*a[2]]}function o(s,a){return[[s[0][0]*a[0],s[0][1]*a[1],s[0][2]*a[2]],[s[1][0]*a[0],s[1][1]*a[1],s[1][2]*a[2]],[s[2][0]*a[0],s[2][1]*a[1],s[2][2]*a[2]]]}function i(s,a){return[[s[0][0]*a[0][0]+s[0][1]*a[1][0]+s[0][2]*a[2][0],s[0][0]*a[0][1]+s[0][1]*a[1][1]+s[0][2]*a[2][1],s[0][0]*a[0][2]+s[0][1]*a[1][2]+s[0][2]*a[2][2]],[s[1][0]*a[0][0]+s[1][1]*a[1][0]+s[1][2]*a[2][0],s[1][0]*a[0][1]+s[1][1]*a[1][1]+s[1][2]*a[2][1],s[1][0]*a[0][2]+s[1][1]*a[1][2]+s[1][2]*a[2][2]],[s[2][0]*a[0][0]+s[2][1]*a[1][0]+s[2][2]*a[2][0],s[2][0]*a[0][1]+s[2][1]*a[1][1]+s[2][2]*a[2][1],s[2][0]*a[0][2]+s[2][1]*a[1][2]+s[2][2]*a[2][2]]]}return pt.transpose=e,pt.determinant=t,pt.inverse=r,pt.multiply=n,pt.scalar=o,pt.product=i,pt}var Yt={},xs;function $c(){if(xs)return Yt;xs=1,Object.defineProperty(Yt,"__esModule",{value:!0});var e=Math,t=e.PI;function r(o){for(var i=o*180/t;i<0;)i+=360;for(;i>360;)i-=360;return i}function n(o){for(var i=t*o/180;i<0;)i+=2*t;for(;i>2*t;)i-=2*t;return i}return Yt.fromRadian=r,Yt.toRadian=n,Yt}var Zt={},Cs;function Ec(){if(Cs)return Zt;Cs=1,Object.defineProperty(Zt,"__esModule",{value:!0});var e=Math,t=e.round;function r(o){return o[0]=="#"&&(o=o.slice(1)),o.length<6&&(o=o.split("").map(function(i){return i+i}).join("")),o.match(/../g).map(function(i){return parseInt(i,16)/255})}function n(o){var i=o.map(function(s){return s=t(255*s).toString(16),s.length<2&&(s="0"+s),s}).join("");return"#"+i}return Zt.fromHex=r,Zt.toHex=n,Zt}var ce={exports:{}},ks;function Lc(){return ks||(ks=1,(function(e,t){Object.defineProperty(t,"__esModule",{value:!0});var r=ws(),n=u(r),o=gs(),i=c(o),s=vs(),a=c(s);function c(l){return l&&l.__esModule?l:{default:l}}function u(l){if(l&&l.__esModule)return l;var h={};if(l!=null)for(var d in l)Object.prototype.hasOwnProperty.call(l,d)&&(h[d]=l[d]);return h.default=l,h}function f(){var l=arguments.length<=0||arguments[0]===void 0?a.default.sRGB:arguments[0],h=arguments.length<=1||arguments[1]===void 0?i.default.D65:arguments[1],d=[l.r,l.g,l.b],b=n.transpose(d.map(function(w){var x=w.x/w.y,S=1,R=(1-w.x-w.y)/w.y;return[x,S,R]})),g=l.gamma,m=n.multiply(n.inverse(b),h),y=n.scalar(b,m),L=n.inverse(y);return{fromRgb:function(x){return n.multiply(y,x.map(g.decode))},toRgb:function(x){return n.multiply(L,x).map(g.encode)}}}t.default=f,e.exports=t.default})(ce,ce.exports)),ce.exports}var Qr,Rs;function ue(){if(Rs)return Qr;Rs=1;var e=gs(),t=vs(),r=ws(),n=$c(),o=Ec(),i=Lc();return Qr={illuminant:e,workspace:t,matrix:r,degree:n,rgb:o,xyz:i},Qr}var Nc=ue();const le=to(Nc);var st={},qs;function fe(){if(qs)return st;qs=1,Object.defineProperty(st,"__esModule",{value:!0}),st.cfs=st.distance=st.lerp=st.corLerp=void 0;var e=Ur();function t(h,d,b){return d in h?Object.defineProperty(h,d,{value:b,enumerable:!0,configurable:!0,writable:!0}):h[d]=b,h}function r(h){if(Array.isArray(h)){for(var d=0,b=Array(h.length);dm/2&&(h>d?d+=m:h+=m)}return((1-b)*h+b*d)%(m||1/0)}function u(h,d,b){var g={};for(var m in h)g[m]=c(h[m],d[m],b,m);return g}function f(h,d){var b=0;for(var g in h)b+=i(h[g]-d[g],2);return s(b)}function l(h){return e.merge.apply(void 0,r(h.split("").map(function(d){return t({},d,!0)})))}return st.corLerp=c,st.lerp=u,st.distance=f,st.cfs=l,st}var he={exports:{}},Ms;function Tc(){return Ms||(Ms=1,(function(e,t){var r=(function(){function s(a,c){var u=[],f=!0,l=!1,h=void 0;try{for(var d=a[Symbol.iterator](),b;!(f=(b=d.next()).done)&&(u.push(b.value),!(c&&u.length===c));f=!0);}catch(g){l=!0,h=g}finally{try{!f&&d.return&&d.return()}finally{if(l)throw h}}return u}return function(a,c){if(Array.isArray(a))return a;if(Symbol.iterator in Object(a))return s(a,c);throw new TypeError("Invalid attempt to destructure non-iterable instance")}})();Object.defineProperty(t,"__esModule",{value:!0});var n=ue(),o=fe();function i(s,a){var c=arguments.length<=2||arguments[2]===void 0?1e-6:arguments[2],u=-c,f=1+c,l=Math,h=l.min,d=l.max,b=["000","fff"].map(function(R){return a.fromXyz(s.fromRgb(n.rgb.fromHex(R)))}),g=r(b,2),m=g[0],y=g[1];function L(R){var M=s.toRgb(a.toXyz(R)),p=M.map(function(C){return C>=u&&C<=f}).reduce(function(C,O){return C&&O},!0);return[p,M]}function w(R,M){for(var p=arguments.length<=2||arguments[2]===void 0?.001:arguments[2];(0,o.distance)(R,M)>p;){var C=(0,o.lerp)(R,M,.5),O=L(C),q=r(O,1),T=q[0];T?R=C:M=C}return R}function x(R){return(0,o.lerp)(m,y,R)}function S(R){return R.map(function(M){return d(u,h(f,M))})}return{contains:L,limit:w,spine:x,crop:S}}t.default=i,e.exports=t.default})(he,he.exports)),he.exports}var de={exports:{}},it={},Os;function As(){if(Os)return it;Os=1;var e=(function(){function l(h,d){var b=[],g=!0,m=!1,y=void 0;try{for(var L=h[Symbol.iterator](),w;!(g=(w=L.next()).done)&&(b.push(w.value),!(d&&b.length===d));g=!0);}catch(x){m=!0,y=x}finally{try{!g&&L.return&&L.return()}finally{if(m)throw y}}return b}return function(h,d){if(Array.isArray(h))return h;if(Symbol.iterator in Object(h))return l(h,d);throw new TypeError("Invalid attempt to destructure non-iterable instance")}})();Object.defineProperty(it,"__esModule",{value:!0}),it.toNotation=it.fromNotation=it.toHue=it.fromHue=void 0;var t=fe(),r=Math,n=r.floor,o=[{s:"R",h:20.14,e:.8,H:0},{s:"Y",h:90,e:.7,H:100},{s:"G",h:164.25,e:1,H:200},{s:"B",h:237.53,e:1.2,H:300},{s:"R",h:380.14,e:.8,H:400}],i=o.map(function(l){return l.s}).slice(0,-1).join("");function s(l){l50){var g=[d,h];h=g[0],d=g[1],b=100-b}return b<1?i[h]:i[h]+b.toFixed()+i[d]}return it.fromHue=s,it.toHue=a,it.fromNotation=u,it.toNotation=f,it}var Ss;function jc(){return Ss||(Ss=1,(function(e,t){var r=(function(){function P(G,Y){var U=[],ut=!0,xt=!1,Jt=void 0;try{for(var lt=G[Symbol.iterator](),Mt;!(ut=(Mt=lt.next()).done)&&(U.push(Mt.value),!(Y&&U.length===Y));ut=!0);}catch(Ct){xt=!0,Jt=Ct}finally{try{!ut&<.return&<.return()}finally{if(xt)throw Jt}}return U}return function(G,Y){if(Array.isArray(G))return G;if(Symbol.iterator in Object(G))return P(G,Y);throw new TypeError("Invalid attempt to destructure non-iterable instance")}})();Object.defineProperty(t,"__esModule",{value:!0});var n=ue(),o=As(),i=c(o),s=fe(),a=Ur();function c(P){if(P&&P.__esModule)return P;var G={};if(P!=null)for(var Y in P)Object.prototype.hasOwnProperty.call(P,Y)&&(G[Y]=P[Y]);return G.default=P,G}var u=Math,f=u.pow,l=u.sqrt,h=u.exp,d=u.abs,b=u.sign,g=Math,m=g.sin,y=g.cos,L=g.atan2,w={average:{F:1,c:.69,N_c:1},dim:{F:.9,c:.59,N_c:.9},dark:{F:.8,c:.535,N_c:.8}},x=[[.7328,.4296,-.1624],[-.7036,1.6975,.0061],[.003,.0136,.9834]],S=[[.38971,.68898,-.07868],[-.22981,1.1834,.04641],[0,0,1]],R=x,M=n.matrix.inverse(x),p=n.matrix.product(S,n.matrix.inverse(x)),C=n.matrix.product(x,n.matrix.inverse(S)),O={whitePoint:n.illuminant.D65,adaptingLuminance:40,backgroundLuminance:20,surroundType:"average",discounting:!1},q=(0,s.cfs)("QJMCshH"),T=(0,s.cfs)("JCh");function A(){var P=arguments.length<=0||arguments[0]===void 0?{}:arguments[0],G=arguments.length<=1||arguments[1]===void 0?q:arguments[1];P=(0,a.merge)(O,P);var Y=P.whitePoint,U=P.adaptingLuminance,ut=P.backgroundLuminance,xt=w[P.surroundType],Jt=xt.F,lt=xt.c,Mt=xt.N_c,Ct=Y[1],Ys=1/(5*U+1),Ot=.2*f(Ys,4)*5*U+.1*f(1-f(Ys,4),2)*f(5*U,1/3),_e=ut/Ct,an=.725*f(1/_e,.2),Zs=an,Js=1.48+l(_e),Hs=P.discounting?1:Jt*(1-1/3.6*h(-(U+42)/92)),d0=n.matrix.multiply(x,Y),b0=d0.map(function(j){return Hs*Ct/j+1-Hs}),cn=r(b0,3),Ws=cn[0],Us=cn[1],Qs=cn[2],p0=ti(Y),m0=ei(p0),ve=ri(m0);function ti(j){var z=n.matrix.multiply(R,j),B=r(z,3),Z=B[0],D=B[1],tt=B[2];return[Ws*Z,Us*D,Qs*tt]}function g0(j){var z=r(j,3),B=z[0],Z=z[1],D=z[2];return n.matrix.multiply(M,[B/Ws,Z/Us,D/Qs])}function ei(j){return n.matrix.multiply(p,j).map(function(z){var B=f(Ot*d(z)/100,.42);return b(z)*400*B/(27.13+B)+.1})}function _0(j){return n.matrix.multiply(C,j.map(function(z){var B=z-.1;return b(B)*100/Ot*f(27.13*d(B)/(400-d(B)),2.380952380952381)}))}function ri(j){var z=r(j,3),B=z[0],Z=z[1],D=z[2];return(B*2+Z+D/20-.305)*an}function un(j){return 4/lt*l(j/100)*(ve+4)*f(Ot,.25)}function v0(j){return 6.25*f(lt*j/((ve+4)*f(Ot,.25)),2)}function ni(j){return j*f(Ot,.25)}function y0(j,z){return f(j/100,2)*z/f(Ot,.25)}function w0(j){return j/f(Ot,.25)}function x0(j,z){return 100*l(j/z)}function ln(j,z){var B=z.Q,Z=z.J,D=z.M,tt=z.C,at=z.s,mt=z.h,gt=z.H,J={};return j.J&&(J.J=isNaN(Z)?v0(B):Z),j.C&&(isNaN(tt)?isNaN(D)?(B=isNaN(B)?un(Z):B,J.C=y0(at,B)):J.C=w0(D):J.C=z.C),j.h&&(J.h=isNaN(mt)?i.toHue(gt):mt),j.Q&&(J.Q=isNaN(B)?un(Z):B),j.M&&(J.M=isNaN(D)?ni(tt):D),j.s&&(isNaN(at)?(B=isNaN(B)?un(Z):B,D=isNaN(D)?ni(tt):D,J.s=x0(D,B)):J.s=at),j.H&&(J.H=isNaN(gt)?i.fromHue(mt):gt),J}function C0(j){var z=ti(j),B=ei(z),Z=r(B,3),D=Z[0],tt=Z[1],at=Z[2],mt=D-tt*12/11+at/11,gt=(D+tt-2*at)/9,J=L(gt,mt),Ft=n.degree.fromRadian(J),ye=1/4*(y(J+2)+3.8),we=ri(B),Ht=100*f(we/ve,lt*Js),kt=5e4/13*Mt*Zs*ye*l(mt*mt+gt*gt)/(D+tt+21/20*at),Rt=f(kt,.9)*l(Ht/100)*f(1.64-f(.29,_e),.73);return ln(G,{J:Ht,C:Rt,h:Ft})}function k0(j){var z=ln(T,j),B=z.J,Z=z.C,D=z.h,tt=n.degree.toRadian(D),at=f(Z/(l(B/100)*f(1.64-f(.29,_e),.73)),10/9),mt=1/4*(y(tt+2)+3.8),gt=ve*f(B/100,1/lt/Js),J=5e4/13*Mt*Zs*mt/at,Ft=gt/an+.305,ye=Ft*61/20*460/1403,we=61/20*220/1403,Ht=21/20*6300/1403-27/1403,kt=m(tt),Rt=y(tt),At,St;at===0||isNaN(at)?At=St=0:d(kt)>=d(Rt)?(St=ye/(J/kt+we*Rt/kt+Ht),At=St*Rt/kt):(At=ye/(J/Rt+we+Ht*kt/Rt),St=At*kt/Rt);var R0=[20/61*Ft+451/1403*At+288/1403*St,20/61*Ft-891/1403*At-261/1403*St,20/61*Ft-220/1403*At-6300/1403*St],q0=_0(R0),M0=g0(q0);return M0}return{fromXyz:C0,toXyz:k0,fillOut:ln}}t.default=A,e.exports=t.default})(de,de.exports)),de.exports}var be={exports:{}},$s;function Pc(){return $s||($s=1,(function(e,t){Object.defineProperty(t,"__esModule",{value:!0});var r=ue(),n=Math,o=n.sqrt,i=n.pow,s=n.exp,a=n.log,c=n.cos,u=n.sin,f=n.atan2,l={LCD:{K_L:.77,c_1:.007,c_2:.0053},SCD:{K_L:1.24,c_1:.007,c_2:.0363},UCS:{K_L:1,c_1:.007,c_2:.0228}};function h(){var d=arguments.length<=0||arguments[0]===void 0?"UCS":arguments[0],b=l[d],g=b.K_L,m=b.c_1,y=b.c_2;function L(S){var R=S.J,M=S.M,p=S.h,C=r.degree.toRadian(p),O=(1+100*m)*R/(1+m*R),q=1/y*a(1+y*M),T=q*c(C),A=q*u(C);return{J_p:O,a_p:T,b_p:A}}function w(S){var R=S.J_p,M=S.a_p,p=S.b_p,C=-R/(m*R-100*m-1),O=o(i(M,2)+i(p,2)),q=(s(y*O)-1)/y,T=f(p,M),A=r.degree.fromRadian(T);return{J:C,M:q,h:A}}function x(S,R){return o(i((S.J_p-R.J_p)/g,2)+i(S.a_p-R.a_p,2)+i(S.b_p-R.b_p,2))}return{fromCam:L,toCam:w,distance:x}}t.default=h,e.exports=t.default})(be,be.exports)),be.exports}var tn,Es;function zc(){if(Es)return tn;Es=1;var e=fe(),t=Tc(),r=jc(),n=Pc(),o=As();return tn={gamut:t,cfs:e.cfs,lerp:e.lerp,cam:r,ucs:n,hq:o},tn}var Bc=zc();const Ls=to(Bc);function Ns(e){const t=new v;return t.rgb_r=e[0],t.rgb_g=e[1],t.rgb_b=e[2],t.rgbToHsluv(),[t.hsluv_h,t.hsluv_s,t.hsluv_l]}function Ic(e){const t=new v;return t.hsluv_h=e[0],t.hsluv_s=e[1],t.hsluv_l=e[2],t.hsluvToRgb(),[t.rgb_r,t.rgb_g,t.rgb_b]}const Ts=Ls.cam({whitePoint:le.illuminant.D65,adaptingLuminance:40,backgroundLuminance:20,surroundType:"average",discounting:!1},Ls.cfs("JCh")),js=le.xyz(le.workspace.sRGB,le.illuminant.D65),Ps=e=>js.toRgb(Ts.toXyz({J:e[0],C:e[1],h:e[2]})),en=e=>{const t=Ts.fromXyz(js.fromRgb(e));return[t.J,t.C,t.h]},[Gc,Fc]=(()=>{const e={k_l:1,c1:.007,c2:.0228},t=Math.PI,r=64/t/5,n=1/(5*r+1),o=.2*n**4*(5*r)+.1*(1-n**4)**2*(5*r)**(1/3);return[i=>{const[s,a,c]=i,u=a*o**.25;let f=(1+100*e.c1)*s/(1+e.c1*s);f/=e.k_l;const l=1/e.c2*Math.log(1+e.c2*u),h=l*Math.cos(c*(t/180)),d=l*Math.sin(c*(t/180));return[f,h,d]},i=>{const[s,a,c]=i,u=Math.sqrt(a*a+c*c),f=(Math.exp(u*e.c2)-1)/e.c2,l=(180/t*Math.atan2(c,a)+360)%360,h=f/o**.25;return[s/(1+e.c1*(100-s)),h,l]}]})(),Kc=e=>Ps(Fc(e)),zs=e=>Gc(en(e)),pe=console;pe.color=(e,t="")=>{const n=k(e).luminance();pe.log(`%c${e} ${t}`,`background-color: ${e};padding: 5px; border-radius: 5px; color: ${n>.5?"#000":"#fff"}`)},pe.ramp=(e,t=1)=>{pe.log("%c ",`font-size: 1px;line-height: 16px;background: ${k.getCSSGradient(e,t)};padding: 0 0 0 200px; border-radius: 2px;`)};const Bs=(e,t,r,n,o,i,s=.1)=>{if(e===r||t===n)return!0;const a=(n-t)/(r-e),c=(i+o/a-t+a*e)/(a+1/a),u=i+o/a-c/a;return(o-c)**2+(i-u)**2{const o=(t[0]+r[0])/2,i=e(o);return Bs(...t,...r,o,i,n)?null:[o,i]},rn=(e,t,r,n=.1)=>{const o=(r-t)/10,i=[];for(let s=t;sMath.round(e*10**t)/10**t,Dc=(e,t=1,r=90,n=.005)=>{const o=rn(c=>e(c).gl()[0],0,t,n),i=rn(c=>e(c).gl()[1],0,t,n),s=rn(c=>e(c).gl()[2],0,t,n),a=Array.from(new Set([...o.map(c=>me(c[0])),...i.map(c=>me(c[0])),...s.map(c=>me(c[0]))].sort((c,u)=>c-u)));return`linear-gradient(${r}deg, ${a.map(c=>`${e(c).hex()} ${me(c*100)}%`).join()});`},Vc=e=>{e.Color.prototype.jch=function(){return en(this._rgb.slice(0,3).map(o=>o/255))},e.jch=(...o)=>new e.Color(...Ps(o).map(i=>Math.floor(i*255)),"rgb"),e.Color.prototype.jab=function(){return zs(this._rgb.slice(0,3).map(o=>o/255))},e.jab=(...o)=>new e.Color(...Kc(o).map(i=>Math.floor(i*255)),"rgb"),e.Color.prototype.hsluv=function(){return Ns(this._rgb.slice(0,3).map(o=>o/255))},e.hsluv=(...o)=>new e.Color(...Ic(o).map(i=>Math.floor(i*255)),"rgb");const t=e.interpolate,r={jch:en,jab:zs,hsluv:Ns},n=(o,i,s)=>(Math.abs(o-i)>360/2&&(o>i?i+=360:o+=360),((1-s)*o+s*i)%360);e.interpolate=(o,i,s=.5,a="lrgb")=>{if(r[a]){typeof o!="object"&&(o=new e.Color(o)),typeof i!="object"&&(i=new e.Color(i));const c=r[a](o.gl()),u=r[a](i.gl()),f=Number.isNaN(o.hsl()[0]),l=Number.isNaN(i.hsl()[0]);let h,d,b;switch(a){case"hsluv":c[1]<1e-10&&(c[0]=u[0]),c[1]===0&&(c[1]=u[1]),u[1]<1e-10&&(u[0]=c[0]),u[1]===0&&(u[1]=c[1]),h=n(c[0],u[0],s),d=c[1]+(u[1]-c[1])*s,b=c[2]+(u[2]-c[2])*s;break;case"jch":f&&(c[2]=u[2]),l&&(u[2]=c[2]),h=c[0]+(u[0]-c[0])*s,d=c[1]+(u[1]-c[1])*s,b=n(c[2],u[2],s);break;default:h=c[0]+(u[0]-c[0])*s,d=c[1]+(u[1]-c[1])*s,b=c[2]+(u[2]-c[2])*s}return e[a](h,d,b).alpha(o.alpha()+s*(i.alpha()-o.alpha()))}return t(o,i,s,a)},e.getCSSGradient=Dc};const X={mainTRC:2.4,sRco:.2126729,sGco:.7151522,sBco:.072175,normBG:.56,normTXT:.57,revTXT:.62,revBG:.65,blkThrs:.022,blkClmp:1.414,scaleBoW:1.14,scaleWoB:1.14,loBoWoffset:.027,loWoBoffset:.027,deltaYmin:5e-4,loClip:.1};function Is(e,t,r=-1){const n=[0,1.1];if(isNaN(e)||isNaN(t)||Math.min(e,t)n[1])return 0;let o=0,i=0,s="BoW";return e=e>X.blkThrs?e:e+Math.pow(X.blkThrs-e,X.blkClmp),t=t>X.blkThrs?t:t+Math.pow(X.blkThrs-t,X.blkClmp),Math.abs(t-e)e?(o=(Math.pow(t,X.normBG)-Math.pow(e,X.normTXT))*X.scaleBoW,i=o-.1?0:o+X.loWoBoffset),r<0?i*100:r==0?Math.round(Math.abs(i)*100)+""+s+"":Number.isInteger(r)?(i*100).toFixed(r):0)}function ge(e=[0,0,0]){function t(r){return Math.pow(r/255,X.mainTRC)}return X.sRco*t(e[0])+X.sGco*t(e[1])+X.sBco*t(e[2])}const Gs=(e,t,r,n,o,i,s,a,c)=>{const u=1-c,f=u*u,l=f*u,d=c*c*c,b=l*e+f*3*c*r+u*3*c*c*o+d*s,g=l*t+f*3*c*n+u*3*c*c*i+d*a;return{x:b,y:g}},Yc=(e,t)=>{const r=[];let n={x:+e[0],y:+e[1]};for(let o=0,i=e.length;i-2*!0>o;o+=2){const s=[{x:+e[o-2],y:+e[o-1]},{x:+e[o],y:+e[o+1]},{x:+e[o+2],y:+e[o+3]},{x:+e[o+4],y:+e[o+5]}];i-4===o?s[3]=s[2]:o||(s[0]={x:+e[o],y:+e[o+1]}),r.push([n.x,n.y,(-s[0].x+6*s[1].x+s[2].x)/6,(-s[0].y+6*s[1].y+s[2].y)/6,(s[1].x+6*s[2].x-s[3].x)/6,(s[1].y+6*s[2].y-s[3].y)/6,s[2].x,s[2].y]),n=s[2]}return r},Zc=(e,t,r,n,o,i,s,a)=>{let u=e,f=t,l=0;for(let h=1;h<5;h++){const{x:d,y:b}=Gs(e,t,r,n,o,i,s,a,h/5);l+=Math.hypot(d-u,b-f),u=d,f=b}return l+=Math.hypot(s-u,a-f),l},Jc=(e,t,r,n,o,i,s,a)=>{const c=Math.floor(Zc(e,t,r,n,o,i,s,a)*.75),u=[];let f=0;for(let l=0;l<=c;l++){const h=l/c,d=Gs(e,t,r,n,o,i,s,a,h),b=Math.round(d.x);if(u[b]=d.y,b-f>1){const g=u[f],m=u[b];for(let y=f+1;yu[Math.round(l)]||null},Gt={CAM02:"jab",CAM02p:"jch",HEX:"hex",HSL:"hsl",HSLuv:"hsluv",HSV:"hsv",LAB:"lab",LCH:"lch",RGB:"rgb",OKLAB:"oklab",OKLCH:"oklch"};function wt(e,t=0){const r=10**t;return Math.round(e*r)/r}function Hc(e,t){let r;return e>1?r=(e-1)*t+1:e<-1?r=(e+1)*t-1:r=1,wt(r,2)}function Wc(e){return k(String(e)).jch()}function Uc(e){return k(String(e)).hsluv()}function Qc(e,t,r){const n=[[],[],[]];if(e.forEach((i,s)=>n.forEach((a,c)=>a.push(t[s],i[c]))),r==="hcl"){const i=n[1];for(let s=1;s{const s=[];for(let a=1;a{i[c]=i[a]}),s.length=0;break}if(s.length){const a=k("#ccc").jch()[2];s.forEach(c=>{i[c]=a})}s.length=0;for(let a=i.length-1;a>0;a-=2)if(Number.isNaN(i[a]))s.push(a);else{s.forEach(c=>{i[c]=i[a]});break}for(let a=1;aYc(i).map(s=>Jc(...s)));return i=>{const s=o.map(a=>{for(let c=0;cn*i**e+o}function nn({swatches:e,colorKeys:t,colorspace:r,colorSpace:n=r??"LAB",shift:o=1,fullScale:i=!0,smooth:s=!1,distributeLightness:a="linear",sortColor:c=!0,asFun:u=!1}={}){r!==void 0&&console.warn("Leonardo: `colorspace` is deprecated. Use `colorSpace` instead.");const f=Gt[n];if(!f)throw new Error(`Colorspace “${n}” not supported`);if(!t)throw new Error(`Colorkeys missing: returned “${t}”`);let l;if(i)l=t.map(w=>e-e*(k(w).jch()[0]/100)).sort((w,x)=>w-x).concat(e),l.unshift(0);else{let w=t.map(R=>k(R).jch()[0]/100),x=Math.min(...w),S=Math.max(...w);l=w.map(R=>R===0||isNaN((R-x)/(S-x))?0:e-(R-x)/(S-x)*e).sort((R,M)=>R-M)}let h=t0(o,[1,e],[1,e]);if(h=l.map(w=>Math.max(0,h(w))),l=h,a==="polynomial"){const w=R=>Math.sqrt(Math.sqrt((Math.pow(R,2.25)+Math.pow(R,4))/2));l=h.map(R=>R/e).map(R=>w(R)*e)}const d=t.map((w,x)=>({colorKeys:Wc(w),index:x})).sort((w,x)=>x.colorKeys[0]-w.colorKeys[0]).map(w=>t[w.index]);let b=[],g;if(i){const w=f==="lch"?k.lch(...k("#fff").lch()):"#ffffff",x=f==="lch"?k.lch(...k("#000").lch()):"#000000";b=[w,...d,x]}else c?b=d:b=t;let m;if(s){const w=b;if(b=b.map(x=>k(String(x))[f]()),f==="hcl"&&b.forEach(x=>{x[1]=Number.isNaN(x[1])?0:x[1]}),f==="jch")for(let x=0;xg(S))}else g=k.scale(b.map(w=>typeof w=="object"&&w.constructor===k.Color?w:String(w))).domain(l).mode(f);return u?g:(!s||s===!1?g.colors(e):m).filter(w=>w!=null)}function e0(e,t){const r=[],n={};return Object.keys(e).forEach(s=>{n[e[s][t]]=e[s]}),Object.keys(n).forEach(s=>r.push(n[s])),r}function r0(e){return Number.isNaN(e)?0:e}function on(e,t,r=!1){if(!e)throw new Error(`Cannot convert color value of “${e}”`);if(!Gt[t])throw new Error(`Cannot convert to colorspace “${t}”`);const n=Gt[t],o=k(String(e))[n]();if(t==="HSL"&&o.pop(),t==="HEX"){if(r){const u=k(String(e)).rgb();return{r:u[0],g:u[1],b:u[2]}}return o}const i={};let s=o.map(r0);s=s.map((u,f)=>{let l=wt(u),h=f;n==="hsluv"&&(h+=2);let d=n.charAt(h);return n==="jch"&&d==="c"&&(d="C"),i[d==="j"?"J":d]=l,n in{lab:1,lch:1,jab:1,jch:1}?r||(d==="l"||d==="j")&&(l+="%"):n!=="hsluv"&&(d==="s"||d==="l"||d==="v")&&(i[d]=wt(u,2),r||(l=wt(u*100),l+="%")),l});const c=`${n}(${s.join(", ")})`;return r?i:c}function Fs(e,t,r){const n=[e,t,r].map(o=>(o/=255,o<=.03928?o/12.92:((o+.055)/1.055)**2.4));return n[0]*.2126+n[1]*.7152+n[2]*.0722}function n0(e,t,r,n="wcag2"){if(r===void 0){const o=k.rgb(...t).hsluv()[2];r=wt(o/100,2)}if(n==="wcag2"){const o=Fs(e[0],e[1],e[2]),i=Fs(t[0],t[1],t[2]),s=(o+.05)/(i+.05),a=(i+.05)/(o+.05);return r<.5?s>=1?s:-a:s<1?a:s===1?s:-s}else{if(n==="wcag3")return r<.5?Is(ge(e),ge(t))*-1:Is(ge(e),ge(t));throw new Error(`Contrast calculation method ${n} unsupported; use 'wcag2' or 'wcag3'`)}}function o0(e,t){if(!e)throw new Error("Array undefined");if(!Array.isArray(e))throw new Error("Passed object is not an array");const r=t==="wcag2"?0:1;return Math.min(...e.filter(n=>n>=r))}function s0(e,t){if(!e)throw new Error("Ratios undefined");e=e.sort((a,c)=>a-c);const r=o0(e,t),n=e.indexOf(r),o=[],i=e.slice(0,n),s=e.slice(n,e.length);for(let a=0;aa-c),o}const i0=(e,t,r,n,o)=>{const s=nn({swatches:3e3,colorKeys:e._modifiedKeys,colorspace:e._colorspace,shift:1,smooth:e._smooth,asFun:!0}),a={},c=l=>{if(a[l])return a[l];const h=k(s(l)).rgb(),d=n0(h,t,r,o);return a[l]=d,d},u=l=>{const h=c(0),d=c(3e3),b=hg&&w;)w--,m/=2,Lf.push(s(u(+l)))),f};class Q{constructor({name:t,colorKeys:r,colorspace:n,colorSpace:o=n??"RGB",ratios:i,smooth:s=!1,output:a="HEX",saturation:c=100}){if(n!==void 0&&console.warn("Leonardo: `colorspace` is deprecated. Use `colorSpace` instead."),this._name=t,this._colorKeys=r,this._modifiedKeys=r,this._colorspace=o,this._ratios=i,this._smooth=s,this._output=a,this._saturation=c,!this._name)throw new Error("Color missing name");if(!this._colorKeys)throw new Error("Color Keys are undefined");if(!Gt[this._colorspace])throw new Error(`Colorspace “${o}” not supported`);if(!Gt[this._output])throw new Error(`Output “${this._output}” not supported`);for(let u=0;u{let n=k(`${r}`).oklch(),i=n[1]*(this._saturation/100),s=k.oklch(n[0],i,n[2]),a=k.rgb(s).hex();t.push(a)}),this._modifiedKeys=t,this._generateColorScale()}_generateColorScale(){this._colorScale=nn({swatches:3e3,colorKeys:this._modifiedKeys,colorSpace:this._colorspace,shift:1,smooth:this._smooth,asFun:!0})}}class Ks extends Q{get backgroundColorScale(){return this._backgroundColorScale||this._generateColorScale(),this._backgroundColorScale}_generateColorScale(){Q.prototype._generateColorScale.call(this);const t=nn({swatches:1e3,colorKeys:this._colorKeys,colorspace:this._colorspace,shift:1,smooth:this._smooth});t.push(...this.colorKeys);const r=t.map((i,s)=>({value:Math.round(Uc(i)[2]),index:s})),o=e0(r,"value").map(i=>t[i.index]);return o.length>=101&&(o.length=100,o.push("#ffffff")),this._backgroundColorScale=o.map(i=>on(i,this._output)),this._backgroundColorScale}}class a0{constructor({colors:t,backgroundColor:r,lightness:n,contrast:o=1,saturation:i=100,output:s="HEX",formula:a="wcag2"}){if(this._output=s,this._colors=t,this._lightness=n,this._saturation=i,this._formula=a,this._setBackgroundColor(r),this._setBackgroundColorValue(),this._contrast=o,!this._colors)throw new Error("No colors are defined");if(!this._backgroundColor)throw new Error("Background color is undefined");if(t.forEach(c=>{if(!c.ratios)throw new Error(`Color ${c.name}'s ratios are undefined`)}),!Gt[this._output])throw new Error(`Output “${s}” not supported`);this._saturation<100&&this._updateColorSaturation(this._saturation),this._findContrastColors(),this._findContrastColorPairs(),this._findContrastColorValues()}set formula(t){this._formula=t,this._findContrastColors()}get formula(){return this._formula}set contrast(t){this._contrast=t,this._findContrastColors()}get contrast(){return this._contrast}set lightness(t){this._lightness=t,this._setBackgroundColor(this._backgroundColor),this._findContrastColors()}get lightness(){return this._lightness}set saturation(t){this._saturation=t,this._updateColorSaturation(t),this._findContrastColors()}get saturation(){return this._saturation}set backgroundColor(t){this._setBackgroundColor(t),this._findContrastColors()}get backgroundColorValue(){return this._backgroundColorValue}get backgroundColor(){return this._backgroundColor}set colors(t){this._colors=t,this._findContrastColors()}get colors(){return this._colors}set addColor(t){this._colors.push(t),this._findContrastColors()}set removeColor(t){const r=this._colors.filter(n=>n.name!==t.name);this._colors=r,this._findContrastColors()}set updateColor(t){if(Array.isArray(t))for(let r=0;rs.name===t[r].color);n=n[0];let o=this._colors.indexOf(n);const i=this._colors.filter(s=>s.name!==t[r].color);t[r].name&&(n.name=t[r].name),t[r].colorKeys&&(n.colorKeys=t[r].colorKeys),t[r].ratios&&(n.ratios=t[r].ratios),(t[r].colorSpace!==void 0||t[r].colorspace!==void 0)&&(t[r].colorspace!==void 0&&t[r].colorSpace===void 0&&console.warn("Leonardo: `colorspace` is deprecated. Use `colorSpace` instead."),n.colorSpace=t[r].colorSpace??t[r].colorspace),t[r].smooth&&(n.smooth=t[r].smooth),n._generateColorScale(),i.splice(o,0,n),this._colors=i}else{let r=this._colors.filter(i=>i.name===t.color);r=r[0];let n=this._colors.indexOf(r);const o=this._colors.filter(i=>i.name!==t.color);t.name&&(r.name=t.name),t.colorKeys&&(r.colorKeys=t.colorKeys),t.ratios&&(r.ratios=t.ratios),(t.colorSpace!==void 0||t.colorspace!==void 0)&&(t.colorspace!==void 0&&t.colorSpace===void 0&&console.warn("Leonardo: `colorspace` is deprecated. Use `colorSpace` instead."),r.colorSpace=t.colorSpace??t.colorspace),t.smooth&&(r.smooth=t.smooth),r._generateColorScale(),o.splice(n,0,r),this._colors=o}this._findContrastColors()}set output(t){this._output=t,this._colors.forEach(r=>{r.output=this._output}),this._backgroundColor.output=this._output,this._findContrastColors()}get output(){return this._output}get contrastColors(){return this._contrastColors}get contrastColorPairs(){return this._contrastColorPairs}get contrastColorValues(){return this._contrastColorValues}_setBackgroundColor(t){if(typeof t=="string"){const r=new Ks({name:"background",colorKeys:[t],output:"RGB"}),n=wt(k(String(t)).hsluv()[2]);this._backgroundColor=r,this._lightness=n,this._backgroundColorValue=r[this._lightness]}else{t.output="RGB";const r=t.backgroundColorScale[this._lightness];this._backgroundColor=t,this._backgroundColorValue=r}}_setBackgroundColorValue(){this._backgroundColorValue=this._backgroundColor.backgroundColorScale[this._lightness]}_updateColorSaturation(t){this._colors.map(r=>{r.saturation=t})}_findContrastColors(){const t=k(String(this._backgroundColorValue)).rgb(),r=this._lightness/100,o={background:on(this._backgroundColorValue,this._output)},i=[],s=[],a={...o};return i.push(o),this._colors.map(c=>{if(c.ratios!==void 0){let u;const f=[],l={name:c.name,values:f};let h;Array.isArray(c.ratios)?h=c.ratios:Array.isArray(c.ratios)||(u=Object.keys(c.ratios),h=Object.values(c.ratios)),h=h.map(b=>Hc(+b,this._contrast));const d=i0(c,t,r,h,this._formula).map(b=>on(b,this._output));for(let b=0;b{const t=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(e);return[Number.parseInt(t[1],16),Number.parseInt(t[2],16),Number.parseInt(t[3],16)]},Ds=(e,t,r)=>{const n=e/255,o=t/255,i=r/255,s=Math.min(n,o,i),a=Math.max(n,o,i),c=a-s;let u=0,f=0,l=0;return c===0?u=0:a===n?u=(o-i)/c%6:a===o?u=(i-n)/c+2:u=(n-o)/c+4,u=Math.round(u*60),u<0&&(u+=360),l=(a+s)/2,f=c===0?0:c/(1-Math.abs(2*l-1)),f=+(f*100).toFixed(1),l=+(l*100).toFixed(1),[u,f,Math.round(l)]},u0=(e,t,r,n)=>{const o=r/100,i=t*Math.min(o,1-o)/100,s=d=>{const b=(d+e/30)%12,g=o-i*Math.max(Math.min(b-3,9-b,1),-1);return Math.round(255*g).toString(16).padStart(2,"0").toUpperCase()},a=s(0),c=s(8),u=s(4),l=((d,b,g)=>Math.min(Math.max(d,b),g))(n,0,1),h=Math.round(l*255).toString(16).padStart(2,"0").toUpperCase();return`#${a}${c}${u}${h}`},l0=(e,t,r=1)=>{const n=Xs(e),o=Xs(t==="white"?"#FFFFFF":t==="black"?"#000000":t),i=n.map((u,f)=>[(u-o[f])/(255-o[f]),(u-o[f])/(0-o[f])]),s=c0(Math.max(...i.flat().filter(u=>/^-?\d+\.?\d*$/.test(u)))),a=n.map((u,f)=>Math.round((u-o[f]+o[f]*s)/s));if(a.includes(Number.NaN)){const u=Ds(n[0],n[1],n[2]);return{h:u[0],s:Math.round(u[1]*r),l:u[2],a:1}}const c=Ds(a[0],a[1],a[2]);return{h:c[0],s:Math.round(c[1]*r),l:c[2],a:s}},sn={backgroundColor:"gray",colorSpace:"OKLCH",colorSmoothing:!1,formula:"wcag2",output:"HEX",colors:{gray:[I(215,20,90),I(215,8,50),I(215,6,25)],red:[I(358,100,58),I(350,100,30)],orange:[I(32,100,48),I(12,100,30)],yellow:[I(50,100,50),I(25,100,20)],lime:[I(100,68,50),I(115,86,25)],green:[I(163,87,42),I(168,100,25)],cyan:[I(185,80,45),I(200,98,35)],blue:[I(212,98,46),I(222,95,25)],purple:[I(258,94,64),I(265,100,35)],fuchsia:[I(295,56,50),I(285,80,25)],pink:[I(334,90,50),I(330,91,25)]},themes:{light:{ratios:[1.03,1.06,1.12,1.25,1.5,1.75,2.25,3.5,5.25,6.5,8,10.5,13.75,16.75],contrast:1,lightness:100,saturation:100},dark:{ratios:[1.03,1.06,1.12,1.25,1.5,1.75,2.25,3.5,5.25,6.5,8,10.5,13.75,16],contrast:1,lightness:6,saturation:97},lightHc:{ratios:[1.06,1.12,1.25,1.37,1.75,2.25,3.25,4.75,8.87,10,11.75,13.25,16,17],contrast:1,lightness:100,saturation:100},darkHc:{ratios:[1.06,1.12,1.25,1.37,1.75,2.25,3.25,4.75,8.87,10,11.75,13.25,16,17],contrast:1,lightness:6,saturation:97}}};function I(e,t,r){return k.hsl(e,t/100,r/100).hex()}function f0(e,t){const r=e.colorSpace,n=e.colorSmoothing,o=e.themes[t].ratios,i=new Ks({name:"gray",colorKeys:e.colors.gray,colorspace:r,ratios:o,smooth:n}),s=new Q({name:"blue",colorKeys:e.colors.blue,colorspace:r,ratios:o,smooth:n}),a=new Q({name:"cyan",colorKeys:e.colors.cyan,colorspace:r,ratios:o,smooth:n}),c=new Q({name:"fuchsia",colorKeys:e.colors.fuchsia,colorspace:r,ratios:o,smooth:n}),u=new Q({name:"green",colorKeys:e.colors.green,colorspace:r,ratios:o,smooth:n}),f=new Q({name:"lime",colorKeys:e.colors.lime,colorspace:r,ratios:o,smooth:n}),l=new Q({name:"orange",colorKeys:e.colors.orange,colorspace:r,ratios:o,smooth:n}),h=new Q({name:"pink",colorKeys:e.colors.pink,colorspace:r,ratios:o,smooth:n}),d=new Q({name:"purple",colorKeys:e.colors.purple,colorspace:r,ratios:o,smooth:n}),b=new Q({name:"red",colorKeys:e.colors.red,colorspace:r,ratios:o,smooth:n}),g=new Q({name:"yellow",colorKeys:e.colors.yellow,colorspace:r,ratios:o,smooth:n}),m={gray:i,red:b,orange:l,yellow:g,lime:f,green:u,cyan:a,blue:s,purple:d,fuchsia:c,pink:h};return e.colors.custom&&(m.custom=new Q({name:"custom",colorKeys:e.colors.custom,colorspace:r,ratios:o,smooth:n})),new a0({colors:Object.values(m),backgroundColor:m[e.backgroundColor],contrast:e.themes[t].contrast,lightness:e.themes[t].lightness,saturation:e.themes[t].saturation,output:e.output,formula:e.formula}).contrastColors}function Vs(e){const t={};for(const r of Object.keys(e.themes))t[r]=f0(e,r);return t}function h0(e){sn.colors.custom=[e];const t=Vs(sn);return Object.fromEntries(Object.entries(t).map(([r,n])=>{const o=n.find(s=>s&&s.name==="custom"),i=Object.fromEntries(o.values.map(({name:s,value:a})=>[s,a]));for(const[s,a]of Object.entries(i)){const c=l0(a,n[0].background);i[`alpha${s.charAt(0).toUpperCase()+s.slice(1)}`]=u0(c.h,c.s,c.l,c.a)}return[r,i]}))}return $t.generateCustomColors=h0,$t.generateThemesJson=Vs,$t.hslToHex=I,$t.leonardoConfig=sn,Object.defineProperty($t,Symbol.toStringTag,{value:"Module"}),$t})({}); +var CompoundTheme=(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var t=Object.create,n=Object.defineProperty,r=Object.getOwnPropertyDescriptor,i=Object.getOwnPropertyNames,a=Object.getPrototypeOf,o=Object.prototype.hasOwnProperty,s=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),c=(e,t,a,s)=>{if(t&&typeof t==`object`||typeof t==`function`)for(var c=i(t),l=0,u=c.length,d;lt[e]).bind(null,d),enumerable:!(s=r(t,d))||s.enumerable});return e},l=(e,r,i)=>(i=e==null?{}:t(a(e)),c(r||!e||!e.__esModule?n(i,`default`,{value:e,enumerable:!0}):i,e)),{min:u,max:d}=Math,f=(e,t=0,n=1)=>u(d(t,e),n),p=e=>{e._clipped=!1,e._unclipped=e.slice(0);for(let t=0;t<=3;t++)t<3?((e[t]<0||e[t]>255)&&(e._clipped=!0),e[t]=f(e[t],0,255)):t===3&&(e[t]=f(e[t],0,1));return e},m={};for(let e of[`Boolean`,`Number`,`String`,`Function`,`Array`,`Date`,`RegExp`,`Undefined`,`Null`])m[`[object ${e}]`]=e.toLowerCase();function h(e){return m[Object.prototype.toString.call(e)]||`object`}var g=(e,t=null)=>e.length>=3?Array.prototype.slice.call(e):h(e[0])==`object`&&t?t.split(``).filter(t=>e[0][t]!==void 0).map(t=>e[0][t]):e[0].slice(0),_=e=>{if(e.length<2)return null;let t=e.length-1;return h(e[t])==`string`?e[t].toLowerCase():null},{PI:v,min:y,max:b}=Math,x=e=>Math.round(e*100)/100,S=e=>Math.round(e*100)/100,C=v*2,w=v/3,T=v/180,ee=180/v;function E(e){return[...e.slice(0,3).reverse(),...e.slice(3)]}var D={format:{},autodetect:[]},O=class{constructor(...e){let t=this;if(h(e[0])===`object`&&e[0].constructor&&e[0].constructor===this.constructor)return e[0];let n=_(e),r=!1;if(!n){r=!0,D.sorted||=(D.autodetect=D.autodetect.sort((e,t)=>t.p-e.p),!0);for(let t of D.autodetect)if(n=t.test(...e),n)break}if(D.format[n])t._rgb=p(D.format[n].apply(null,r?e:e.slice(0,-1)));else throw Error(`unknown format: `+e);t._rgb.length===3&&t._rgb.push(1)}toString(){return h(this.hex)==`function`?this.hex():`[${this._rgb.join(`,`)}]`}},te=`3.2.0`,k=(...e)=>new O(...e);k.version=te;var A={aliceblue:`#f0f8ff`,antiquewhite:`#faebd7`,aqua:`#00ffff`,aquamarine:`#7fffd4`,azure:`#f0ffff`,beige:`#f5f5dc`,bisque:`#ffe4c4`,black:`#000000`,blanchedalmond:`#ffebcd`,blue:`#0000ff`,blueviolet:`#8a2be2`,brown:`#a52a2a`,burlywood:`#deb887`,cadetblue:`#5f9ea0`,chartreuse:`#7fff00`,chocolate:`#d2691e`,coral:`#ff7f50`,cornflowerblue:`#6495ed`,cornsilk:`#fff8dc`,crimson:`#dc143c`,cyan:`#00ffff`,darkblue:`#00008b`,darkcyan:`#008b8b`,darkgoldenrod:`#b8860b`,darkgray:`#a9a9a9`,darkgreen:`#006400`,darkgrey:`#a9a9a9`,darkkhaki:`#bdb76b`,darkmagenta:`#8b008b`,darkolivegreen:`#556b2f`,darkorange:`#ff8c00`,darkorchid:`#9932cc`,darkred:`#8b0000`,darksalmon:`#e9967a`,darkseagreen:`#8fbc8f`,darkslateblue:`#483d8b`,darkslategray:`#2f4f4f`,darkslategrey:`#2f4f4f`,darkturquoise:`#00ced1`,darkviolet:`#9400d3`,deeppink:`#ff1493`,deepskyblue:`#00bfff`,dimgray:`#696969`,dimgrey:`#696969`,dodgerblue:`#1e90ff`,firebrick:`#b22222`,floralwhite:`#fffaf0`,forestgreen:`#228b22`,fuchsia:`#ff00ff`,gainsboro:`#dcdcdc`,ghostwhite:`#f8f8ff`,gold:`#ffd700`,goldenrod:`#daa520`,gray:`#808080`,green:`#008000`,greenyellow:`#adff2f`,grey:`#808080`,honeydew:`#f0fff0`,hotpink:`#ff69b4`,indianred:`#cd5c5c`,indigo:`#4b0082`,ivory:`#fffff0`,khaki:`#f0e68c`,laserlemon:`#ffff54`,lavender:`#e6e6fa`,lavenderblush:`#fff0f5`,lawngreen:`#7cfc00`,lemonchiffon:`#fffacd`,lightblue:`#add8e6`,lightcoral:`#f08080`,lightcyan:`#e0ffff`,lightgoldenrod:`#fafad2`,lightgoldenrodyellow:`#fafad2`,lightgray:`#d3d3d3`,lightgreen:`#90ee90`,lightgrey:`#d3d3d3`,lightpink:`#ffb6c1`,lightsalmon:`#ffa07a`,lightseagreen:`#20b2aa`,lightskyblue:`#87cefa`,lightslategray:`#778899`,lightslategrey:`#778899`,lightsteelblue:`#b0c4de`,lightyellow:`#ffffe0`,lime:`#00ff00`,limegreen:`#32cd32`,linen:`#faf0e6`,magenta:`#ff00ff`,maroon:`#800000`,maroon2:`#7f0000`,maroon3:`#b03060`,mediumaquamarine:`#66cdaa`,mediumblue:`#0000cd`,mediumorchid:`#ba55d3`,mediumpurple:`#9370db`,mediumseagreen:`#3cb371`,mediumslateblue:`#7b68ee`,mediumspringgreen:`#00fa9a`,mediumturquoise:`#48d1cc`,mediumvioletred:`#c71585`,midnightblue:`#191970`,mintcream:`#f5fffa`,mistyrose:`#ffe4e1`,moccasin:`#ffe4b5`,navajowhite:`#ffdead`,navy:`#000080`,oldlace:`#fdf5e6`,olive:`#808000`,olivedrab:`#6b8e23`,orange:`#ffa500`,orangered:`#ff4500`,orchid:`#da70d6`,palegoldenrod:`#eee8aa`,palegreen:`#98fb98`,paleturquoise:`#afeeee`,palevioletred:`#db7093`,papayawhip:`#ffefd5`,peachpuff:`#ffdab9`,peru:`#cd853f`,pink:`#ffc0cb`,plum:`#dda0dd`,powderblue:`#b0e0e6`,purple:`#800080`,purple2:`#7f007f`,purple3:`#a020f0`,rebeccapurple:`#663399`,red:`#ff0000`,rosybrown:`#bc8f8f`,royalblue:`#4169e1`,saddlebrown:`#8b4513`,salmon:`#fa8072`,sandybrown:`#f4a460`,seagreen:`#2e8b57`,seashell:`#fff5ee`,sienna:`#a0522d`,silver:`#c0c0c0`,skyblue:`#87ceeb`,slateblue:`#6a5acd`,slategray:`#708090`,slategrey:`#708090`,snow:`#fffafa`,springgreen:`#00ff7f`,steelblue:`#4682b4`,tan:`#d2b48c`,teal:`#008080`,thistle:`#d8bfd8`,tomato:`#ff6347`,turquoise:`#40e0d0`,violet:`#ee82ee`,wheat:`#f5deb3`,white:`#ffffff`,whitesmoke:`#f5f5f5`,yellow:`#ffff00`,yellowgreen:`#9acd32`},ne=/^#?([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/,re=/^#?([A-Fa-f0-9]{8}|[A-Fa-f0-9]{4})$/,ie=e=>{if(e.match(ne)){(e.length===4||e.length===7)&&(e=e.substr(1)),e.length===3&&(e=e.split(``),e=e[0]+e[0]+e[1]+e[1]+e[2]+e[2]);let t=parseInt(e,16);return[t>>16,t>>8&255,t&255,1]}if(e.match(re)){(e.length===5||e.length===9)&&(e=e.substr(1)),e.length===4&&(e=e.split(``),e=e[0]+e[0]+e[1]+e[1]+e[2]+e[2]+e[3]+e[3]);let t=parseInt(e,16);return[t>>24&255,t>>16&255,t>>8&255,Math.round((t&255)/255*100)/100]}throw Error(`unknown hex color: ${e}`)},{round:ae}=Math,oe=(...e)=>{let[t,n,r,i]=g(e,`rgba`),a=_(e)||`auto`;i===void 0&&(i=1),a===`auto`&&(a=i<1?`rgba`:`rgb`),t=ae(t),n=ae(n),r=ae(r);let o=`000000`+(t<<16|n<<8|r).toString(16);o=o.substr(o.length-6);let s=`0`+ae(i*255).toString(16);switch(s=s.substr(s.length-2),a.toLowerCase()){case`rgba`:return`#${o}${s}`;case`argb`:return`#${s}${o}`;default:return`#${o}`}};O.prototype.name=function(){let e=oe(this._rgb,`rgb`);for(let t of Object.keys(A))if(A[t]===e)return t.toLowerCase();return e},D.format.named=e=>{if(e=e.toLowerCase(),A[e])return ie(A[e]);throw Error(`unknown color name: `+e)},D.autodetect.push({p:5,test:(e,...t)=>{if(!t.length&&h(e)===`string`&&A[e.toLowerCase()])return`named`}}),O.prototype.alpha=function(e,t=!1){return e!==void 0&&h(e)===`number`?t?(this._rgb[3]=e,this):new O([this._rgb[0],this._rgb[1],this._rgb[2],e],`rgb`):this._rgb[3]},O.prototype.clipped=function(){return this._rgb._clipped||!1};var j={Kn:18,labWhitePoint:`d65`,Xn:.95047,Yn:1,Zn:1.08883,t0:.137931034,t1:.206896552,t2:.12841855,t3:.008856452,kE:216/24389,kKE:8,kK:24389/27,RefWhiteRGB:{X:.95047,Y:1,Z:1.08883},MtxRGB2XYZ:{m00:.4124564390896922,m01:.21267285140562253,m02:.0193338955823293,m10:.357576077643909,m11:.715152155287818,m12:.11919202588130297,m20:.18043748326639894,m21:.07217499330655958,m22:.9503040785363679},MtxXYZ2RGB:{m00:3.2404541621141045,m01:-.9692660305051868,m02:.055643430959114726,m10:-1.5371385127977166,m11:1.8760108454466942,m12:-.2040259135167538,m20:-.498531409556016,m21:.041556017530349834,m22:1.0572251882231791},As:.9414285350000001,Bs:1.040417467,Cs:1.089532651,MtxAdaptMa:{m00:.8951,m01:-.7502,m02:.0389,m10:.2664,m11:1.7135,m12:-.0685,m20:-.1614,m21:.0367,m22:1.0296},MtxAdaptMaI:{m00:.9869929054667123,m01:.43230526972339456,m02:-.008528664575177328,m10:-.14705425642099013,m11:.5183602715367776,m12:.04004282165408487,m20:.15996265166373125,m21:.0492912282128556,m22:.9684866957875502}},se=new Map([[`a`,[1.0985,.35585]],[`b`,[1.0985,.35585]],[`c`,[.98074,1.18232]],[`d50`,[.96422,.82521]],[`d55`,[.95682,.92149]],[`d65`,[.95047,1.08883]],[`e`,[1,1,1]],[`f2`,[.99186,.67393]],[`f7`,[.95041,1.08747]],[`f11`,[1.00962,.6435]],[`icc`,[.96422,.82521]]]);function M(e){let t=se.get(String(e).toLowerCase());if(!t)throw Error(`unknown Lab illuminant `+e);j.labWhitePoint=e,j.Xn=t[0],j.Zn=t[1]}function ce(){return j.labWhitePoint}var N=(...e)=>{e=g(e,`lab`);let[t,n,r]=e,[i,a,o]=le(t,n,r),[s,c,l]=de(i,a,o);return[s,c,l,e.length>3?e[3]:1]},le=(e,t,n)=>{let{kE:r,kK:i,kKE:a,Xn:o,Yn:s,Zn:c}=j,l=(e+16)/116,u=.002*t+l,d=l-.005*n,f=u*u*u,p=d*d*d,m=f>r?f:(116*u-16)/i,h=e>a?((e+16)/116)**3:e/i,g=p>r?p:(116*d-16)/i;return[m*o,h*s,g*c]},ue=e=>{let t=Math.sign(e);return e=Math.abs(e),(e<=.0031308?e*12.92:1.055*e**(1/2.4)-.055)*t},de=(e,t,n)=>{let{MtxAdaptMa:r,MtxAdaptMaI:i,MtxXYZ2RGB:a,RefWhiteRGB:o,Xn:s,Yn:c,Zn:l}=j,u=s*r.m00+c*r.m10+l*r.m20,d=s*r.m01+c*r.m11+l*r.m21,f=s*r.m02+c*r.m12+l*r.m22,p=o.X*r.m00+o.Y*r.m10+o.Z*r.m20,m=o.X*r.m01+o.Y*r.m11+o.Z*r.m21,h=o.X*r.m02+o.Y*r.m12+o.Z*r.m22,g=(e*r.m00+t*r.m10+n*r.m20)*(p/u),_=(e*r.m01+t*r.m11+n*r.m21)*(m/d),v=(e*r.m02+t*r.m12+n*r.m22)*(h/f),y=g*i.m00+_*i.m10+v*i.m20,b=g*i.m01+_*i.m11+v*i.m21,x=g*i.m02+_*i.m12+v*i.m22,S=ue(y*a.m00+b*a.m10+x*a.m20),C=ue(y*a.m01+b*a.m11+x*a.m21),w=ue(y*a.m02+b*a.m12+x*a.m22);return[S*255,C*255,w*255]},fe=(...e)=>{let[t,n,r,...i]=g(e,`rgb`),[a,o,s]=he(t,n,r),[c,l,u]=pe(a,o,s);return[c,l,u,...i.length>0&&i[0]<1?[i[0]]:[]]};function pe(e,t,n){let{Xn:r,Yn:i,Zn:a,kE:o,kK:s}=j,c=e/r,l=t/i,u=n/a,d=c>o?c**(1/3):(s*c+16)/116,f=l>o?l**(1/3):(s*l+16)/116,p=u>o?u**(1/3):(s*u+16)/116;return[116*f-16,500*(d-f),200*(f-p)]}function me(e){let t=Math.sign(e);return e=Math.abs(e),(e<=.04045?e/12.92:((e+.055)/1.055)**2.4)*t}var he=(e,t,n)=>{e=me(e/255),t=me(t/255),n=me(n/255);let{MtxRGB2XYZ:r,MtxAdaptMa:i,MtxAdaptMaI:a,Xn:o,Yn:s,Zn:c,As:l,Bs:u,Cs:d}=j,f=e*r.m00+t*r.m10+n*r.m20,p=e*r.m01+t*r.m11+n*r.m21,m=e*r.m02+t*r.m12+n*r.m22,h=o*i.m00+s*i.m10+c*i.m20,g=o*i.m01+s*i.m11+c*i.m21,_=o*i.m02+s*i.m12+c*i.m22,v=f*i.m00+p*i.m10+m*i.m20,y=f*i.m01+p*i.m11+m*i.m21,b=f*i.m02+p*i.m12+m*i.m22;return v*=h/l,y*=g/u,b*=_/d,f=v*a.m00+y*a.m10+b*a.m20,p=v*a.m01+y*a.m11+b*a.m21,m=v*a.m02+y*a.m12+b*a.m22,[f,p,m]};O.prototype.lab=function(){return fe(this._rgb)},Object.assign(k,{lab:(...e)=>new O(...e,`lab`),getLabWhitePoint:ce,setLabWhitePoint:M}),D.format.lab=N,D.autodetect.push({p:2,test:(...e)=>{if(e=g(e,`lab`),h(e)===`array`&&e.length===3)return`lab`}}),O.prototype.darken=function(e=1){let t=this,n=t.lab();return n[0]-=j.Kn*e,new O(n,`lab`).alpha(t.alpha(),!0)},O.prototype.brighten=function(e=1){return this.darken(-e)},O.prototype.darker=O.prototype.darken,O.prototype.brighter=O.prototype.brighten,O.prototype.get=function(e){let[t,n]=e.split(`.`),r=this[t]();if(n){let e=t.indexOf(n)-(t.substr(0,2)===`ok`?2:0);if(e>-1)return r[e];throw Error(`unknown channel ${n} in mode ${t}`)}else return r};var{pow:ge}=Math,_e=1e-7,ve=20;O.prototype.luminance=function(e,t=`rgb`){if(e!==void 0&&h(e)===`number`){if(e===0)return new O([0,0,0,this._rgb[3]],`rgb`);if(e===1)return new O([255,255,255,this._rgb[3]],`rgb`);let n=this.luminance(),r=ve,i=(n,a)=>{let o=n.interpolate(a,.5,t),s=o.luminance();return Math.abs(e-s)<_e||!r--?o:s>e?i(n,o):i(o,a)};return new O([...(n>e?i(new O([0,0,0]),this):i(this,new O([255,255,255]))).rgb(),this._rgb[3]])}return ye(...this._rgb.slice(0,3))};var ye=(e,t,n)=>(e=be(e),t=be(t),n=be(n),.2126*e+.7152*t+.0722*n),be=e=>(e/=255,e<=.03928?e/12.92:ge((e+.055)/1.055,2.4)),P={},F=(e,t,n=.5,...r)=>{let i=r[0]||`lrgb`;if(!P[i]&&!r.length&&(i=Object.keys(P)[0]),!P[i])throw Error(`interpolation mode ${i} is not defined`);return h(e)!==`object`&&(e=new O(e)),h(t)!==`object`&&(t=new O(t)),P[i](e,t,n).alpha(e.alpha()+n*(t.alpha()-e.alpha()))};O.prototype.mix=O.prototype.interpolate=function(e,t=.5,...n){return F(this,e,t,...n)},O.prototype.premultiply=function(e=!1){let t=this._rgb,n=t[3];return e?(this._rgb=[t[0]*n,t[1]*n,t[2]*n,n],this):new O([t[0]*n,t[1]*n,t[2]*n,n],`rgb`)};var{sin:xe,cos:Se}=Math,Ce=(...e)=>{let[t,n,r]=g(e,`lch`);return isNaN(r)&&(r=0),r*=T,[t,Se(r)*n,xe(r)*n]},we=(...e)=>{e=g(e,`lch`);let[t,n,r]=e,[i,a,o]=Ce(t,n,r),[s,c,l]=N(i,a,o);return[s,c,l,e.length>3?e[3]:1]},Te=(...e)=>we(...E(g(e,`hcl`))),{sqrt:Ee,atan2:De,round:Oe}=Math,ke=(...e)=>{let[t,n,r]=g(e,`lab`),i=Ee(n*n+r*r),a=(De(r,n)*ee+360)%360;return Oe(i*1e4)===0&&(a=NaN),[t,i,a]},Ae=(...e)=>{let[t,n,r,...i]=g(e,`rgb`),[a,o,s]=fe(t,n,r),[c,l,u]=ke(a,o,s);return[c,l,u,...i.length>0&&i[0]<1?[i[0]]:[]]};O.prototype.lch=function(){return Ae(this._rgb)},O.prototype.hcl=function(){return E(Ae(this._rgb))},Object.assign(k,{lch:(...e)=>new O(...e,`lch`),hcl:(...e)=>new O(...e,`hcl`)}),D.format.lch=we,D.format.hcl=Te,[`lch`,`hcl`].forEach(e=>D.autodetect.push({p:2,test:(...t)=>{if(t=g(t,e),h(t)===`array`&&t.length===3)return e}})),O.prototype.saturate=function(e=1){let t=this,n=t.lch();return n[1]+=j.Kn*e,n[1]<0&&(n[1]=0),new O(n,`lch`).alpha(t.alpha(),!0)},O.prototype.desaturate=function(e=1){return this.saturate(-e)},O.prototype.set=function(e,t,n=!1){let[r,i]=e.split(`.`),a=this[r]();if(i){let e=r.indexOf(i)-(r.substr(0,2)===`ok`?2:0);if(e>-1){if(h(t)==`string`)switch(t.charAt(0)){case`+`:a[e]+=+t;break;case`-`:a[e]+=+t;break;case`*`:a[e]*=+t.substr(1);break;case`/`:a[e]/=+t.substr(1);break;default:a[e]=+t}else if(h(t)===`number`)a[e]=t;else throw Error(`unsupported value for Color.set`);let i=new O(a,r);return n?(this._rgb=i._rgb,this):i}throw Error(`unknown channel ${i} in mode ${r}`)}else return a},O.prototype.tint=function(e=.5,...t){return F(this,`white`,e,...t)},O.prototype.shade=function(e=.5,...t){return F(this,`black`,e,...t)},P.rgb=(e,t,n)=>{let r=e._rgb,i=t._rgb;return new O(r[0]+n*(i[0]-r[0]),r[1]+n*(i[1]-r[1]),r[2]+n*(i[2]-r[2]),`rgb`)};var{sqrt:je,pow:Me}=Math;P.lrgb=(e,t,n)=>{let[r,i,a]=e._rgb,[o,s,c]=t._rgb;return new O(je(Me(r,2)*(1-n)+Me(o,2)*n),je(Me(i,2)*(1-n)+Me(s,2)*n),je(Me(a,2)*(1-n)+Me(c,2)*n),`rgb`)},P.lab=(e,t,n)=>{let r=e.lab(),i=t.lab();return new O(r[0]+n*(i[0]-r[0]),r[1]+n*(i[1]-r[1]),r[2]+n*(i[2]-r[2]),`lab`)};var Ne=(e,t,n,r)=>{let i,a;r===`hsl`?(i=e.hsl(),a=t.hsl()):r===`hsv`?(i=e.hsv(),a=t.hsv()):r===`hcg`?(i=e.hcg(),a=t.hcg()):r===`hsi`?(i=e.hsi(),a=t.hsi()):r===`lch`||r===`hcl`?(r=`hcl`,i=e.hcl(),a=t.hcl()):r===`oklch`&&(i=e.oklch().reverse(),a=t.oklch().reverse());let o,s,c,l,u,d;(r.substr(0,1)===`h`||r===`oklch`)&&([o,c,u]=i,[s,l,d]=a);let f,p,m,h;return!isNaN(o)&&!isNaN(s)?(h=s>o&&s-o>180?s-(o+360):s180?s+360-o:s-o,p=o+n*h):isNaN(o)?isNaN(s)?p=NaN:(p=s,(u==1||u==0)&&r!=`hsv`&&(f=l)):(p=o,(d==1||d==0)&&r!=`hsv`&&(f=c)),f===void 0&&(f=c+n*(l-c)),m=u+n*(d-u),r===`oklch`?new O([m,f,p],r):new O([p,f,m],r)},Pe=(e,t,n)=>Ne(e,t,n,`lch`);P.lch=Pe,P.hcl=Pe;var Fe=e=>{if(h(e)==`number`&&e>=0&&e<=16777215)return[e>>16,e>>8&255,e&255,1];throw Error(`unknown num color: `+e)},Ie=(...e)=>{let[t,n,r]=g(e,`rgb`);return(t<<16)+(n<<8)+r};O.prototype.num=function(){return Ie(this._rgb)},Object.assign(k,{num:(...e)=>new O(...e,`num`)}),D.format.num=Fe,D.autodetect.push({p:5,test:(...e)=>{if(e.length===1&&h(e[0])===`number`&&e[0]>=0&&e[0]<=16777215)return`num`}}),P.num=(e,t,n)=>{let r=e.num();return new O(r+n*(t.num()-r),`num`)};var{floor:Le}=Math,Re=(...e)=>{e=g(e,`hcg`);let[t,n,r]=e,i,a,o;r*=255;let s=n*255;if(n===0)i=a=o=r;else{t===360&&(t=0),t>360&&(t-=360),t<0&&(t+=360),t/=60;let e=Le(t),c=t-e,l=r*(1-n),u=l+s*(1-c),d=l+s*c,f=l+s;switch(e){case 0:[i,a,o]=[f,d,l];break;case 1:[i,a,o]=[u,f,l];break;case 2:[i,a,o]=[l,f,d];break;case 3:[i,a,o]=[l,u,f];break;case 4:[i,a,o]=[d,l,f];break;case 5:[i,a,o]=[f,l,u];break}}return[i,a,o,e.length>3?e[3]:1]},ze=(...e)=>{let[t,n,r]=g(e,`rgb`),i=y(t,n,r),a=b(t,n,r),o=a-i,s=o*100/255,c=i/(255-o)*100,l;return o===0?l=NaN:(t===a&&(l=(n-r)/o),n===a&&(l=2+(r-t)/o),r===a&&(l=4+(t-n)/o),l*=60,l<0&&(l+=360)),[l,s,c]};O.prototype.hcg=function(){return ze(this._rgb)},k.hcg=(...e)=>new O(...e,`hcg`),D.format.hcg=Re,D.autodetect.push({p:1,test:(...e)=>{if(e=g(e,`hcg`),h(e)===`array`&&e.length===3)return`hcg`}}),P.hcg=(e,t,n)=>Ne(e,t,n,`hcg`);var{cos:Be}=Math,Ve=(...e)=>{e=g(e,`hsi`);let[t,n,r]=e,i,a,o;return isNaN(t)&&(t=0),isNaN(n)&&(n=0),t>360&&(t-=360),t<0&&(t+=360),t/=360,t<1/3?(o=(1-n)/3,i=(1+n*Be(C*t)/Be(w-C*t))/3,a=1-(o+i)):t<2/3?(t-=1/3,i=(1-n)/3,a=(1+n*Be(C*t)/Be(w-C*t))/3,o=1-(i+a)):(t-=2/3,a=(1-n)/3,o=(1+n*Be(C*t)/Be(w-C*t))/3,i=1-(a+o)),i=f(r*i*3),a=f(r*a*3),o=f(r*o*3),[i*255,a*255,o*255,e.length>3?e[3]:1]},{min:He,sqrt:Ue,acos:We}=Math,Ge=(...e)=>{let[t,n,r]=g(e,`rgb`);t/=255,n/=255,r/=255;let i,a=He(t,n,r),o=(t+n+r)/3,s=o>0?1-a/o:0;return s===0?i=NaN:(i=(t-n+(t-r))/2,i/=Ue((t-n)*(t-n)+(t-r)*(n-r)),i=We(i),r>n&&(i=C-i),i/=C),[i*360,s,o]};O.prototype.hsi=function(){return Ge(this._rgb)},k.hsi=(...e)=>new O(...e,`hsi`),D.format.hsi=Ve,D.autodetect.push({p:2,test:(...e)=>{if(e=g(e,`hsi`),h(e)===`array`&&e.length===3)return`hsi`}}),P.hsi=(e,t,n)=>Ne(e,t,n,`hsi`);var Ke=(...e)=>{e=g(e,`hsl`);let[t,n,r]=e,i,a,o;if(n===0)i=a=o=r*255;else{let e=[0,0,0],s=[0,0,0],c=r<.5?r*(1+n):r+n-r*n,l=2*r-c,u=t/360;e[0]=u+1/3,e[1]=u,e[2]=u-1/3;for(let t=0;t<3;t++)e[t]<0&&(e[t]+=1),e[t]>1&&--e[t],6*e[t]<1?s[t]=l+(c-l)*6*e[t]:2*e[t]<1?s[t]=c:3*e[t]<2?s[t]=l+(c-l)*(2/3-e[t])*6:s[t]=l;[i,a,o]=[s[0]*255,s[1]*255,s[2]*255]}return e.length>3?[i,a,o,e[3]]:[i,a,o,1]},qe=(...e)=>{e=g(e,`rgba`);let[t,n,r]=e;t/=255,n/=255,r/=255;let i=y(t,n,r),a=b(t,n,r),o=(a+i)/2,s,c;return a===i?(s=0,c=NaN):s=o<.5?(a-i)/(a+i):(a-i)/(2-a-i),t==a?c=(n-r)/(a-i):n==a?c=2+(r-t)/(a-i):r==a&&(c=4+(t-n)/(a-i)),c*=60,c<0&&(c+=360),e.length>3&&e[3]!==void 0?[c,s,o,e[3]]:[c,s,o]};O.prototype.hsl=function(){return qe(this._rgb)},k.hsl=(...e)=>new O(...e,`hsl`),D.format.hsl=Ke,D.autodetect.push({p:2,test:(...e)=>{if(e=g(e,`hsl`),h(e)===`array`&&e.length===3)return`hsl`}}),P.hsl=(e,t,n)=>Ne(e,t,n,`hsl`);var{floor:Je}=Math,Ye=(...e)=>{e=g(e,`hsv`);let[t,n,r]=e,i,a,o;if(r*=255,n===0)i=a=o=r;else{t===360&&(t=0),t>360&&(t-=360),t<0&&(t+=360),t/=60;let e=Je(t),s=t-e,c=r*(1-n),l=r*(1-n*s),u=r*(1-n*(1-s));switch(e){case 0:[i,a,o]=[r,u,c];break;case 1:[i,a,o]=[l,r,c];break;case 2:[i,a,o]=[c,r,u];break;case 3:[i,a,o]=[c,l,r];break;case 4:[i,a,o]=[u,c,r];break;case 5:[i,a,o]=[r,c,l];break}}return[i,a,o,e.length>3?e[3]:1]},{min:Xe,max:Ze}=Math,Qe=(...e)=>{e=g(e,`rgb`);let[t,n,r]=e,i=Xe(t,n,r),a=Ze(t,n,r),o=a-i,s,c,l;return l=a/255,a===0?(s=NaN,c=0):(c=o/a,t===a&&(s=(n-r)/o),n===a&&(s=2+(r-t)/o),r===a&&(s=4+(t-n)/o),s*=60,s<0&&(s+=360)),[s,c,l]};O.prototype.hsv=function(){return Qe(this._rgb)},k.hsv=(...e)=>new O(...e,`hsv`),D.format.hsv=Ye,D.autodetect.push({p:2,test:(...e)=>{if(e=g(e,`hsv`),h(e)===`array`&&e.length===3)return`hsv`}}),P.hsv=(e,t,n)=>Ne(e,t,n,`hsv`);function $e(e,t){let n=e.length;Array.isArray(e[0])||(e=[e]),Array.isArray(t[0])||(t=t.map(e=>[e]));let r=t[0].length,i=t[0].map((e,n)=>t.map(e=>e[n])),a=e.map(e=>i.map(t=>Array.isArray(e)?e.reduce((e,n,r)=>e+n*(t[r]||0),0):t.reduce((t,n)=>t+n*e,0)));return n===1&&(a=a[0]),r===1?a.map(e=>e[0]):a}var et=(...e)=>{e=g(e,`lab`);let[t,n,r,...i]=e,[a,o,s]=tt([t,n,r]),[c,l,u]=de(a,o,s);return[c,l,u,...i.length>0&&i[0]<1?[i[0]]:[]]};function tt(e){return $e([[1.2268798758459243,-.5578149944602171,.2813910456659647],[-.0405757452148008,1.112286803280317,-.0717110580655164],[-.0763729366746601,-.4214933324022432,1.5869240198367816]],$e([[1,.3963377773761749,.2158037573099136],[1,-.1055613458156586,-.0638541728258133],[1,-.0894841775298119,-1.2914855480194092]],e).map(e=>e**3))}var nt=(...e)=>{let[t,n,r,...i]=g(e,`rgb`);return[...rt(he(t,n,r)),...i.length>0&&i[0]<1?[i[0]]:[]]};function rt(e){return $e([[.210454268309314,.7936177747023054,-.0040720430116193],[1.9779985324311684,-2.42859224204858,.450593709617411],[.0259040424655478,.7827717124575296,-.8086757549230774]],$e([[.819022437996703,.3619062600528904,-.1288737815209879],[.0329836539323885,.9292868615863434,.0361446663506424],[.0481771893596242,.2642395317527308,.6335478284694309]],e).map(e=>Math.cbrt(e)))}O.prototype.oklab=function(){return nt(this._rgb)},Object.assign(k,{oklab:(...e)=>new O(...e,`oklab`)}),D.format.oklab=et,D.autodetect.push({p:2,test:(...e)=>{if(e=g(e,`oklab`),h(e)===`array`&&e.length===3)return`oklab`}}),P.oklab=(e,t,n)=>{let r=e.oklab(),i=t.oklab();return new O(r[0]+n*(i[0]-r[0]),r[1]+n*(i[1]-r[1]),r[2]+n*(i[2]-r[2]),`oklab`)},P.oklch=(e,t,n)=>Ne(e,t,n,`oklch`);var{pow:it,sqrt:at,PI:ot,cos:st,sin:ct,atan2:lt}=Math,ut=(e,t=`lrgb`,n=null)=>{let r=e.length;n||=Array.from(Array(r)).map(()=>1);let i=r/n.reduce(function(e,t){return e+t});if(n.forEach((e,t)=>{n[t]*=i}),e=e.map(e=>new O(e)),t===`lrgb`)return dt(e,n);let a=e.shift(),o=a.get(t),s=[],c=0,l=0;for(let e=0;e{let i=e.get(t);u+=e.alpha()*n[r+1];for(let e=0;e=360;)t-=360;o[e]=t}else o[e]=o[e]/s[e];return u/=r,new O(o,t).alpha(u>.99999?1:u,!0)},dt=(e,t)=>{let n=e.length,r=[0,0,0,0];for(let i=0;i.9999999&&(r[3]=1),new O(p(r))},{pow:ft}=Math;function pt(e){let t=`rgb`,n=k(`#ccc`),r=0,i=[0,1],a=[0,1],o=[],s=[0,0],c=!1,l=[],u=!1,d=0,p=1,m=!1,g={},_=!0,v=1,y=function(e){if(e||=[`#fff`,`#000`],e&&h(e)===`string`&&k.brewer&&k.brewer[e.toLowerCase()]&&(e=k.brewer[e.toLowerCase()]),h(e)===`array`){e.length===1&&(e=[e[0],e[0]]),e=e.slice(0);for(let t=0;t=c[n];)n++;return n-1}return 0},x=e=>e,S=e=>e,C=function(e,r){let i,a;if(r??=!1,isNaN(e)||e===null)return n;a=r?e:c&&c.length>2?b(e)/(c.length-2):p===d?1:(e-d)/(p-d),a=S(a),r||(a=x(a)),v!==1&&(a=ft(a,v)),a=s[0]+a*(1-s[0]-s[1]),a=f(a,0,1);let u=Math.floor(a*1e4);if(_&&g[u])i=g[u];else{if(h(l)===`array`)for(let e=0;e=n&&e===o.length-1){i=l[e];break}if(a>n&&ag={};y(e);let T=function(e){let t=k(C(e));return u&&t[u]?t[u]():t};return T.classes=function(e){if(e!=null){if(h(e)===`array`)c=e,i=[e[0],e[e.length-1]];else{let t=k.analyze(i);c=e===0?[t.min,t.max]:k.limits(t,`e`,e)}return T}return c},T.domain=function(e){if(!arguments.length)return a;a=e.slice(0),d=e[0],p=e[e.length-1],o=[];let t=l.length;if(e.length===t&&d!==p)for(let t of Array.from(e))o.push((t-d)/(p-d));else{for(let e=0;e2){let t=e.map((t,n)=>n/(e.length-1)),n=e.map(e=>(e-d)/(p-d));n.every((e,n)=>t[n]===e)||(S=e=>{if(e<=0||e>=1)return e;let r=0;for(;e>=n[r+1];)r++;let i=(e-n[r])/(n[r+1]-n[r]);return t[r]+i*(t[r+1]-t[r])})}}return i=[d,p],T},T.mode=function(e){return arguments.length?(t=e,w(),T):t},T.range=function(e,t){return y(e,t),T},T.out=function(e){return u=e,T},T.spread=function(e){return arguments.length?(r=e,T):r},T.correctLightness=function(e){return e??=!0,m=e,w(),x=m?function(e){let t=C(0,!0).lab()[0],n=C(1,!0).lab()[0],r=t>n,i=C(e,!0).lab()[0],a=t+(n-t)*e,o=i-a,s=0,c=1,l=20;for(;Math.abs(o)>.01&&l-- >0;)(function(){return r&&(o*=-1),o<0?(s=e,e+=(c-e)*.5):(c=e,e+=(s-e)*.5),i=C(e,!0).lab()[0],o=i-a})();return e}:e=>e,T},T.padding=function(e){return e==null?s:(h(e)===`number`&&(e=[e,e]),s=e,T)},T.colors=function(t,n){arguments.length<2&&(n=`hex`);let r=[];if(arguments.length===0)r=l.slice(0);else if(t===1)r=[T(.5)];else if(t>1){let e=i[0],n=i[1]-e;r=mt(0,t,!1).map(r=>T(e+r/(t-1)*n))}else{e=[];let t=[];if(c&&c.length>2)for(let e=1,n=c.length,r=1<=n;r?en;r?e++:e--)t.push((c[e-1]+c[e])*.5);else t=i;r=t.map(e=>T(e))}return k[n]&&(r=r.map(e=>e[n]())),r},T.cache=function(e){return e==null?_:(_=e,T)},T.gamma=function(e){return e==null?v:(v=e,T)},T.nodata=function(e){return e==null?n:(n=k(e),T)},T}function mt(e,t,n){let r=[],i=ea;i?t++:t--)r.push(t);return r}var ht=function(e){let t=[1,1];for(let n=1;nnew O(e)),e.length===2)[n,r]=e.map(e=>e.lab()),t=function(e){return new O([0,1,2].map(t=>n[t]+e*(r[t]-n[t])),`lab`)};else if(e.length===3)[n,r,i]=e.map(e=>e.lab()),t=function(e){return new O([0,1,2].map(t=>(1-e)*(1-e)*n[t]+2*(1-e)*e*r[t]+e*e*i[t]),`lab`)};else if(e.length===4){let a;[n,r,i,a]=e.map(e=>e.lab()),t=function(e){return new O([0,1,2].map(t=>(1-e)*(1-e)*(1-e)*n[t]+3*(1-e)*(1-e)*e*r[t]+3*(1-e)*e*e*i[t]+e*e*e*a[t]),`lab`)}}else if(e.length>=5){let n,r,i;n=e.map(e=>e.lab()),i=e.length-1,r=ht(i),t=function(e){let t=1-e;return new O([0,1,2].map(a=>n.reduce((n,o,s)=>n+r[s]*t**(i-s)*e**s*o[a],0)),`lab`)}}else throw RangeError(`No point in running bezier with only one color.`);return t},_t=e=>{let t=gt(e);return t.scale=()=>pt(t),t},{round:vt}=Math;O.prototype.rgb=function(e=!0){return e===!1?this._rgb.slice(0,3):this._rgb.slice(0,3).map(vt)},O.prototype.rgba=function(e=!0){return this._rgb.slice(0,4).map((t,n)=>n<3?e===!1?t:vt(t):t)},Object.assign(k,{rgb:(...e)=>new O(...e,`rgb`)}),D.format.rgb=(...e)=>{let t=g(e,`rgba`);return t[3]===void 0&&(t[3]=1),t},D.autodetect.push({p:3,test:(...e)=>{if(e=g(e,`rgba`),h(e)===`array`&&(e.length===3||e.length===4&&h(e[3])==`number`&&e[3]>=0&&e[3]<=1))return`rgb`}});var I=(e,t,n)=>{if(!I[n])throw Error(`unknown blend mode `+n);return I[n](e,t)},L=e=>(t,n)=>{let r=k(n).rgb(),i=k(t).rgb();return k.rgb(e(r,i))},R=e=>(t,n)=>{let r=[];return r[0]=e(t[0],n[0]),r[1]=e(t[1],n[1]),r[2]=e(t[2],n[2]),r};I.normal=L(R(e=>e)),I.multiply=L(R((e,t)=>e*t/255)),I.screen=L(R((e,t)=>255*(1-(1-e/255)*(1-t/255)))),I.overlay=L(R((e,t)=>t<128?2*e*t/255:255*(1-2*(1-e/255)*(1-t/255)))),I.darken=L(R((e,t)=>e>t?t:e)),I.lighten=L(R((e,t)=>e>t?e:t)),I.dodge=L(R((e,t)=>e===255?255:(e=t/255*255/(1-e/255),e>255?255:e))),I.burn=L(R((e,t)=>255*(1-(1-t/255)/(e/255))));var{pow:yt,sin:bt,cos:xt}=Math;function St(e=300,t=-1.5,n=1,r=1,i=[0,1]){let a=0,o;h(i)===`array`?o=i[1]-i[0]:(o=0,i=[i,i]);let s=function(s){let c=C*((e+120)/360+t*s),l=yt(i[0]+o*s,r),u=(a===0?n:n[0]+s*a)*l*(1-l)/2,d=xt(c),f=bt(c),m=l+u*(-.14861*d+1.78277*f),h=l+u*(-.29227*d-.90649*f),g=l+1.97294*d*u;return k(p([m*255,h*255,g*255,1]))};return s.start=function(t){return t==null?e:(e=t,s)},s.rotations=function(e){return e==null?t:(t=e,s)},s.gamma=function(e){return e==null?r:(r=e,s)},s.hue=function(e){return e==null?n:(n=e,h(n)===`array`?(a=n[1]-n[0],a===0&&(n=n[1])):a=0,s)},s.lightness=function(e){return e==null?i:(h(e)===`array`?(i=e,o=e[1]-e[0]):(i=[e,e],o=0),s)},s.scale=()=>k.scale(s),s.hue(n),s}var Ct=`0123456789abcdef`,{floor:wt,random:Tt}=Math,Et=(e=Tt)=>{let t=`#`;for(let n=0;n<6;n++)t+=Ct.charAt(wt(e()*16));return new O(t,`hex`)},{log:Dt,pow:Ot,floor:kt,abs:At}=Math;function jt(e,t=null){let n={min:Number.MAX_VALUE,max:Number.MAX_VALUE*-1,sum:0,values:[],count:0};return h(e)===`object`&&(e=Object.values(e)),e.forEach(e=>{t&&h(e)===`object`&&(e=e[t]),e!=null&&!isNaN(e)&&(n.values.push(e),n.sum+=e,en.max&&(n.max=e),n.count+=1)}),n.domain=[n.min,n.max],n.limits=(e,t)=>Mt(n,e,t),n}function Mt(e,t=`equal`,n=7){h(e)==`array`&&(e=jt(e));let{min:r,max:i}=e,a=e.values.sort((e,t)=>e-t);if(n===1)return[r,i];let o=[];if(t.substr(0,1)===`c`&&(o.push(r),o.push(i)),t.substr(0,1)===`e`){o.push(r);for(let e=1;e 0`);let e=Math.LOG10E*Dt(r),t=Math.LOG10E*Dt(i);o.push(r);for(let r=1;r200&&(l=!1)}let f={};for(let e=0;ee-t),o.push(p[0]);for(let e=1;e{e=new O(e),t=new O(t);let n=e.luminance(),r=t.luminance();return n>r?(n+.05)/(r+.05):(r+.05)/(n+.05)},Pt=.027,Ft=5e-4,It=.1,Lt=1.14,Rt=.022,zt=1.414,Bt=(e,t)=>{e=new O(e),t=new O(t),e.alpha()<1&&(e=F(t,e,e.alpha(),`rgb`));let n=Vt(...e.rgb()),r=Vt(...t.rgb()),i=n>=Rt?n:n+(Rt-n)**+zt,a=r>=Rt?r:r+(Rt-r)**+zt,o=a**.56-i**.57,s=a**.65-i**.62,c=Math.abs(a-i)0?c-Pt:c+Pt)*100};function Vt(e,t,n){return .2126729*(e/255)**2.4+.7151522*(t/255)**2.4+.072175*(n/255)**2.4}var{sqrt:z,pow:B,min:Ht,max:Ut,atan2:Wt,abs:Gt,cos:Kt,sin:qt,exp:Jt,PI:Yt}=Math;function Xt(e,t,n=1,r=1,i=1){var a=function(e){return 360*e/(2*Yt)},o=function(e){return 2*Yt*e/360};e=new O(e),t=new O(t);let[s,c,l]=Array.from(e.lab()),[u,d,f]=Array.from(t.lab()),p=(s+u)/2,m=(z(B(c,2)+B(l,2))+z(B(d,2)+B(f,2)))/2,h=.5*(1-z(B(m,7)/(B(m,7)+B(25,7)))),g=c*(1+h),_=d*(1+h),v=z(B(g,2)+B(l,2)),y=z(B(_,2)+B(f,2)),b=(v+y)/2,x=a(Wt(l,g)),S=a(Wt(f,_)),C=x>=0?x:x+360,w=S>=0?S:S+360,T=Gt(C-w)>180?(C+w+360)/2:(C+w)/2,ee=1-.17*Kt(o(T-30))+.24*Kt(o(2*T))+.32*Kt(o(3*T+6))-.2*Kt(o(4*T-63)),E=w-C;E=Gt(E)<=180?E:w<=C?E+360:E-360,E=2*z(v*y)*qt(o(E)/2);let D=u-s,te=y-v,k=1+.015*B(p-50,2)/z(20+B(p-50,2)),A=1+.045*b,ne=1+.015*b*ee,re=30*Jt(-B((T-275)/25,2)),ie=-(2*z(B(b,7)/(B(b,7)+B(25,7))))*qt(2*o(re));return Ut(0,Ht(100,z(B(D/(n*k),2)+B(te/(r*A),2)+B(E/(i*ne),2)+ie*(te/(r*A))*(E/(i*ne)))))}function Zt(e,t,n=`lab`){e=new O(e),t=new O(t);let r=e.get(n),i=t.get(n),a=0;for(let e in r){let t=(r[e]||0)-(i[e]||0);a+=t*t}return Math.sqrt(a)}var Qt=(...e)=>{try{return new O(...e),!0}catch{return!1}},$t={cool(){return pt([k.hsl(180,1,.9),k.hsl(250,.7,.4)])},hot(){return pt([`#000`,`#f00`,`#ff0`,`#fff`],[0,.25,.75,1]).mode(`rgb`)}},en={OrRd:[`#fff7ec`,`#fee8c8`,`#fdd49e`,`#fdbb84`,`#fc8d59`,`#ef6548`,`#d7301f`,`#b30000`,`#7f0000`],PuBu:[`#fff7fb`,`#ece7f2`,`#d0d1e6`,`#a6bddb`,`#74a9cf`,`#3690c0`,`#0570b0`,`#045a8d`,`#023858`],BuPu:[`#f7fcfd`,`#e0ecf4`,`#bfd3e6`,`#9ebcda`,`#8c96c6`,`#8c6bb1`,`#88419d`,`#810f7c`,`#4d004b`],Oranges:[`#fff5eb`,`#fee6ce`,`#fdd0a2`,`#fdae6b`,`#fd8d3c`,`#f16913`,`#d94801`,`#a63603`,`#7f2704`],BuGn:[`#f7fcfd`,`#e5f5f9`,`#ccece6`,`#99d8c9`,`#66c2a4`,`#41ae76`,`#238b45`,`#006d2c`,`#00441b`],YlOrBr:[`#ffffe5`,`#fff7bc`,`#fee391`,`#fec44f`,`#fe9929`,`#ec7014`,`#cc4c02`,`#993404`,`#662506`],YlGn:[`#ffffe5`,`#f7fcb9`,`#d9f0a3`,`#addd8e`,`#78c679`,`#41ab5d`,`#238443`,`#006837`,`#004529`],Reds:[`#fff5f0`,`#fee0d2`,`#fcbba1`,`#fc9272`,`#fb6a4a`,`#ef3b2c`,`#cb181d`,`#a50f15`,`#67000d`],RdPu:[`#fff7f3`,`#fde0dd`,`#fcc5c0`,`#fa9fb5`,`#f768a1`,`#dd3497`,`#ae017e`,`#7a0177`,`#49006a`],Greens:[`#f7fcf5`,`#e5f5e0`,`#c7e9c0`,`#a1d99b`,`#74c476`,`#41ab5d`,`#238b45`,`#006d2c`,`#00441b`],YlGnBu:[`#ffffd9`,`#edf8b1`,`#c7e9b4`,`#7fcdbb`,`#41b6c4`,`#1d91c0`,`#225ea8`,`#253494`,`#081d58`],Purples:[`#fcfbfd`,`#efedf5`,`#dadaeb`,`#bcbddc`,`#9e9ac8`,`#807dba`,`#6a51a3`,`#54278f`,`#3f007d`],GnBu:[`#f7fcf0`,`#e0f3db`,`#ccebc5`,`#a8ddb5`,`#7bccc4`,`#4eb3d3`,`#2b8cbe`,`#0868ac`,`#084081`],Greys:[`#ffffff`,`#f0f0f0`,`#d9d9d9`,`#bdbdbd`,`#969696`,`#737373`,`#525252`,`#252525`,`#000000`],YlOrRd:[`#ffffcc`,`#ffeda0`,`#fed976`,`#feb24c`,`#fd8d3c`,`#fc4e2a`,`#e31a1c`,`#bd0026`,`#800026`],PuRd:[`#f7f4f9`,`#e7e1ef`,`#d4b9da`,`#c994c7`,`#df65b0`,`#e7298a`,`#ce1256`,`#980043`,`#67001f`],Blues:[`#f7fbff`,`#deebf7`,`#c6dbef`,`#9ecae1`,`#6baed6`,`#4292c6`,`#2171b5`,`#08519c`,`#08306b`],PuBuGn:[`#fff7fb`,`#ece2f0`,`#d0d1e6`,`#a6bddb`,`#67a9cf`,`#3690c0`,`#02818a`,`#016c59`,`#014636`],Viridis:[`#440154`,`#482777`,`#3f4a8a`,`#31678e`,`#26838f`,`#1f9d8a`,`#6cce5a`,`#b6de2b`,`#fee825`],Spectral:[`#9e0142`,`#d53e4f`,`#f46d43`,`#fdae61`,`#fee08b`,`#ffffbf`,`#e6f598`,`#abdda4`,`#66c2a5`,`#3288bd`,`#5e4fa2`],RdYlGn:[`#a50026`,`#d73027`,`#f46d43`,`#fdae61`,`#fee08b`,`#ffffbf`,`#d9ef8b`,`#a6d96a`,`#66bd63`,`#1a9850`,`#006837`],RdBu:[`#67001f`,`#b2182b`,`#d6604d`,`#f4a582`,`#fddbc7`,`#f7f7f7`,`#d1e5f0`,`#92c5de`,`#4393c3`,`#2166ac`,`#053061`],PiYG:[`#8e0152`,`#c51b7d`,`#de77ae`,`#f1b6da`,`#fde0ef`,`#f7f7f7`,`#e6f5d0`,`#b8e186`,`#7fbc41`,`#4d9221`,`#276419`],PRGn:[`#40004b`,`#762a83`,`#9970ab`,`#c2a5cf`,`#e7d4e8`,`#f7f7f7`,`#d9f0d3`,`#a6dba0`,`#5aae61`,`#1b7837`,`#00441b`],RdYlBu:[`#a50026`,`#d73027`,`#f46d43`,`#fdae61`,`#fee090`,`#ffffbf`,`#e0f3f8`,`#abd9e9`,`#74add1`,`#4575b4`,`#313695`],BrBG:[`#543005`,`#8c510a`,`#bf812d`,`#dfc27d`,`#f6e8c3`,`#f5f5f5`,`#c7eae5`,`#80cdc1`,`#35978f`,`#01665e`,`#003c30`],RdGy:[`#67001f`,`#b2182b`,`#d6604d`,`#f4a582`,`#fddbc7`,`#ffffff`,`#e0e0e0`,`#bababa`,`#878787`,`#4d4d4d`,`#1a1a1a`],PuOr:[`#7f3b08`,`#b35806`,`#e08214`,`#fdb863`,`#fee0b6`,`#f7f7f7`,`#d8daeb`,`#b2abd2`,`#8073ac`,`#542788`,`#2d004b`],Set2:[`#66c2a5`,`#fc8d62`,`#8da0cb`,`#e78ac3`,`#a6d854`,`#ffd92f`,`#e5c494`,`#b3b3b3`],Accent:[`#7fc97f`,`#beaed4`,`#fdc086`,`#ffff99`,`#386cb0`,`#f0027f`,`#bf5b17`,`#666666`],Set1:[`#e41a1c`,`#377eb8`,`#4daf4a`,`#984ea3`,`#ff7f00`,`#ffff33`,`#a65628`,`#f781bf`,`#999999`],Set3:[`#8dd3c7`,`#ffffb3`,`#bebada`,`#fb8072`,`#80b1d3`,`#fdb462`,`#b3de69`,`#fccde5`,`#d9d9d9`,`#bc80bd`,`#ccebc5`,`#ffed6f`],Dark2:[`#1b9e77`,`#d95f02`,`#7570b3`,`#e7298a`,`#66a61e`,`#e6ab02`,`#a6761d`,`#666666`],Paired:[`#a6cee3`,`#1f78b4`,`#b2df8a`,`#33a02c`,`#fb9a99`,`#e31a1c`,`#fdbf6f`,`#ff7f00`,`#cab2d6`,`#6a3d9a`,`#ffff99`,`#b15928`],Pastel2:[`#b3e2cd`,`#fdcdac`,`#cbd5e8`,`#f4cae4`,`#e6f5c9`,`#fff2ae`,`#f1e2cc`,`#cccccc`],Pastel1:[`#fbb4ae`,`#b3cde3`,`#ccebc5`,`#decbe4`,`#fed9a6`,`#ffffcc`,`#e5d8bd`,`#fddaec`,`#f2f2f2`]},tn=Object.keys(en),nn=new Map(tn.map(e=>[e.toLowerCase(),e])),rn=typeof Proxy==`function`?new Proxy(en,{get(e,t){let n=t.toLowerCase();if(nn.has(n))return e[nn.get(n)]},getOwnPropertyNames(){return Object.getOwnPropertyNames(tn)}}):en,an=(...e)=>{e=g(e,`cmyk`);let[t,n,r,i]=e,a=e.length>4?e[4]:1;return i===1?[0,0,0,a]:[t>=1?0:255*(1-t)*(1-i),n>=1?0:255*(1-n)*(1-i),r>=1?0:255*(1-r)*(1-i),a]},{max:on}=Math,sn=(...e)=>{let[t,n,r]=g(e,`rgb`);t/=255,n/=255,r/=255;let i=1-on(t,on(n,r)),a=i<1?1/(1-i):0;return[(1-t-i)*a,(1-n-i)*a,(1-r-i)*a,i]};O.prototype.cmyk=function(){return sn(this._rgb)},Object.assign(k,{cmyk:(...e)=>new O(...e,`cmyk`)}),D.format.cmyk=an,D.autodetect.push({p:2,test:(...e)=>{if(e=g(e,`cmyk`),h(e)===`array`&&e.length===4)return`cmyk`}});var cn=(...e)=>{let t=g(e,`hsla`),n=_(e)||`lsa`;return t[0]=x(t[0]||0)+`deg`,t[1]=x(t[1]*100)+`%`,t[2]=x(t[2]*100)+`%`,n===`hsla`||t.length>3&&t[3]<1?(t[3]=`/ `+(t.length>3?t[3]:1),n=`hsla`):t.length=3,`${n.substr(0,3)}(${t.join(` `)})`},ln=(...e)=>{let t=g(e,`lab`),n=_(e)||`lab`;return t[0]=x(t[0])+`%`,t[1]=x(t[1]),t[2]=x(t[2]),n===`laba`||t.length>3&&t[3]<1?t[3]=`/ `+(t.length>3?t[3]:1):t.length=3,`lab(${t.join(` `)})`},un=(...e)=>{let t=g(e,`lch`),n=_(e)||`lab`;return t[0]=x(t[0])+`%`,t[1]=x(t[1]),t[2]=isNaN(t[2])?`none`:x(t[2])+`deg`,n===`lcha`||t.length>3&&t[3]<1?t[3]=`/ `+(t.length>3?t[3]:1):t.length=3,`lch(${t.join(` `)})`},dn=(...e)=>{let t=g(e,`lab`);return t[0]=x(t[0]*100)+`%`,t[1]=S(t[1]),t[2]=S(t[2]),t.length>3&&t[3]<1?t[3]=`/ `+(t.length>3?t[3]:1):t.length=3,`oklab(${t.join(` `)})`},fn=(...e)=>{let[t,n,r,...i]=g(e,`rgb`),[a,o,s]=nt(t,n,r),[c,l,u]=ke(a,o,s);return[c,l,u,...i.length>0&&i[0]<1?[i[0]]:[]]},pn=(...e)=>{let t=g(e,`lch`);return t[0]=x(t[0]*100)+`%`,t[1]=S(t[1]),t[2]=isNaN(t[2])?`none`:x(t[2])+`deg`,t.length>3&&t[3]<1?t[3]=`/ `+(t.length>3?t[3]:1):t.length=3,`oklch(${t.join(` `)})`},{round:mn}=Math,hn=(...e)=>{let t=g(e,`rgba`),n=_(e)||`rgb`;if(n.substr(0,3)===`hsl`)return cn(qe(t),n);if(n.substr(0,3)===`lab`){let e=ce();M(`d50`);let r=ln(fe(t),n);return M(e),r}if(n.substr(0,3)===`lch`){let e=ce();M(`d50`);let r=un(Ae(t),n);return M(e),r}return n.substr(0,5)===`oklab`?dn(nt(t)):n.substr(0,5)===`oklch`?pn(fn(t)):(t[0]=mn(t[0]),t[1]=mn(t[1]),t[2]=mn(t[2]),(n===`rgba`||t.length>3&&t[3]<1)&&(t[3]=`/ `+(t.length>3?t[3]:1),n=`rgba`),`${n.substr(0,3)}(${t.slice(0,n===`rgb`?3:4).join(` `)})`)},gn=(...e)=>{e=g(e,`lch`);let[t,n,r,...i]=e,[a,o,s]=Ce(t,n,r),[c,l,u]=et(a,o,s);return[c,l,u,...i.length>0&&i[0]<1?[i[0]]:[]]},V=`((?:-?\\d+)|(?:-?\\d+(?:\\.\\d+)?)%|none)`,H=`((?:-?(?:\\d+(?:\\.\\d*)?|\\.\\d+)%?)|none)`,_n=`((?:-?(?:\\d+(?:\\.\\d*)?|\\.\\d+)%)|none)`,U=`\\s*`,vn=`\\s+`,yn=`\\s*,\\s*`,bn=`((?:-?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:deg)?)|none)`,xn=`\\s*(?:\\/\\s*((?:[01]|[01]?\\.\\d+)|\\d+(?:\\.\\d+)?%))?`,Sn=RegExp(`^rgba?\\(`+U+[V,V,V].join(vn)+xn+`\\)$`),Cn=RegExp(`^rgb\\(`+U+[V,V,V].join(yn)+U+`\\)$`),wn=RegExp(`^rgba\\(`+U+[V,V,V,H].join(yn)+U+`\\)$`),Tn=RegExp(`^hsla?\\(`+U+[bn,_n,_n].join(vn)+xn+`\\)$`),En=RegExp(`^hsl?\\(`+U+[bn,_n,_n].join(yn)+U+`\\)$`),Dn=/^hsla\(\s*(-?\d+(?:\.\d+)?),\s*(-?\d+(?:\.\d+)?)%\s*,\s*(-?\d+(?:\.\d+)?)%\s*,\s*([01]|[01]?\.\d+)\)$/,On=RegExp(`^lab\\(`+U+[H,H,H].join(vn)+xn+`\\)$`),kn=RegExp(`^lch\\(`+U+[H,H,bn].join(vn)+xn+`\\)$`),An=RegExp(`^oklab\\(`+U+[H,H,H].join(vn)+xn+`\\)$`),jn=RegExp(`^oklch\\(`+U+[H,H,bn].join(vn)+xn+`\\)$`),{round:Mn}=Math,Nn=e=>e.map((e,t)=>t<=2?f(Mn(e),0,255):e),W=(e,t=0,n=100,r=!1)=>(typeof e==`string`&&e.endsWith(`%`)&&(e=parseFloat(e.substring(0,e.length-1))/100,e=r?t+(e+1)*.5*(n-t):t+e*(n-t)),+e),G=(e,t)=>e===`none`?t:e,Pn=e=>{if(e=e.toLowerCase().trim(),e===`transparent`)return[0,0,0,0];let t;if(D.format.named)try{return D.format.named(e)}catch{}if((t=e.match(Sn))||(t=e.match(Cn))){let e=t.slice(1,4);for(let t=0;t<3;t++)e[t]=+W(G(e[t],0),0,255);e=Nn(e);let n=t[4]===void 0?1:+W(t[4],0,1);return e[3]=n,e}if(t=e.match(wn)){let e=t.slice(1,5);for(let t=0;t<4;t++)e[t]=+W(e[t],0,255);return e}if((t=e.match(Tn))||(t=e.match(En))){let e=t.slice(1,4);e[0]=+G(e[0].replace(`deg`,``),0),e[1]=W(G(e[1],0),0,100)*.01,e[2]=W(G(e[2],0),0,100)*.01;let n=Nn(Ke(e));return n[3]=t[4]===void 0?1:+W(t[4],0,1),n}if(t=e.match(Dn)){let e=t.slice(1,4);e[1]*=.01,e[2]*=.01;let n=Ke(e);for(let e=0;e<3;e++)n[e]=Mn(n[e]);return n[3]=+t[4],n}if(t=e.match(On)){let e=t.slice(1,4);e[0]=W(G(e[0],0),0,100),e[1]=W(G(e[1],0),-125,125,!0),e[2]=W(G(e[2],0),-125,125,!0);let n=ce();M(`d50`);let r=Nn(N(e));return M(n),r[3]=t[4]===void 0?1:+W(t[4],0,1),r}if(t=e.match(kn)){let e=t.slice(1,4);e[0]=W(e[0],0,100),e[1]=W(G(e[1],0),0,150,!1),e[2]=+G(e[2].replace(`deg`,``),0);let n=ce();M(`d50`);let r=Nn(we(e));return M(n),r[3]=t[4]===void 0?1:+W(t[4],0,1),r}if(t=e.match(An)){let e=t.slice(1,4);e[0]=W(G(e[0],0),0,1),e[1]=W(G(e[1],0),-.4,.4,!0),e[2]=W(G(e[2],0),-.4,.4,!0);let n=Nn(et(e));return n[3]=t[4]===void 0?1:+W(t[4],0,1),n}if(t=e.match(jn)){let e=t.slice(1,4);e[0]=W(G(e[0],0),0,1),e[1]=W(G(e[1],0),0,.4,!1),e[2]=+G(e[2].replace(`deg`,``),0);let n=Nn(gn(e));return n[3]=t[4]===void 0?1:+W(t[4],0,1),n}};Pn.test=e=>Sn.test(e)||Tn.test(e)||On.test(e)||kn.test(e)||An.test(e)||jn.test(e)||Cn.test(e)||wn.test(e)||En.test(e)||Dn.test(e)||e===`transparent`,O.prototype.css=function(e){return hn(this._rgb,e)},k.css=(...e)=>new O(...e,`css`),D.format.css=Pn,D.autodetect.push({p:5,test:(e,...t)=>{if(!t.length&&h(e)===`string`&&Pn.test(e))return`css`}}),D.format.gl=(...e)=>{let t=g(e,`rgba`);return t[0]*=255,t[1]*=255,t[2]*=255,t},k.gl=(...e)=>new O(...e,`gl`),O.prototype.gl=function(){let e=this._rgb;return[e[0]/255,e[1]/255,e[2]/255,e[3]]},O.prototype.hex=function(e){return oe(this._rgb,e)},k.hex=(...e)=>new O(...e,`hex`),D.format.hex=ie,D.autodetect.push({p:4,test:(e,...t)=>{if(!t.length&&h(e)===`string`&&[3,4,5,6,7,8,9].indexOf(e.length)>=0)return`hex`}});var{log:Fn}=Math,In=e=>{let t=e/100,n,r,i;return t<66?(n=255,r=t<6?0:-155.25485562709179-.44596950469579133*(r=t-2)+104.49216199393888*Fn(r),i=t<20?0:-254.76935184120902+.8274096064007395*(i=t-10)+115.67994401066147*Fn(i)):(n=351.97690566805693+.114206453784165*(n=t-55)-40.25366309332127*Fn(n),r=325.4494125711974+.07943456536662342*(r=t-50)-28.0852963507957*Fn(r),i=255),[n,r,i,1]},{round:Ln}=Math,Rn=(...e)=>{let t=g(e,`rgb`),n=t[0],r=t[2],i=1e3,a=4e4,o;for(;a-i>.4;){o=(a+i)*.5;let e=In(o);e[2]/e[0]>=r/n?a=o:i=o}return Ln(o)};O.prototype.temp=O.prototype.kelvin=O.prototype.temperature=function(){return Rn(this._rgb)};var zn=(...e)=>new O(...e,`temp`);Object.assign(k,{temp:zn,kelvin:zn,temperature:zn}),D.format.temp=D.format.kelvin=D.format.temperature=In,O.prototype.oklch=function(){return fn(this._rgb)},Object.assign(k,{oklch:(...e)=>new O(...e,`oklch`)}),D.format.oklch=gn,D.autodetect.push({p:2,test:(...e)=>{if(e=g(e,`oklch`),h(e)===`array`&&e.length===3)return`oklch`}}),Object.assign(k,{analyze:jt,average:ut,bezier:_t,blend:I,brewer:rn,Color:O,colors:A,contrast:Nt,contrastAPCA:Bt,cubehelix:St,deltaE:Xt,distance:Zt,input:D,interpolate:F,limits:Mt,mix:F,random:Et,scale:pt,scales:$t,valid:Qt});var K=k,q=class e{constructor(){this.hex=`#000000`,this.rgb_r=0,this.rgb_g=0,this.rgb_b=0,this.xyz_x=0,this.xyz_y=0,this.xyz_z=0,this.luv_l=0,this.luv_u=0,this.luv_v=0,this.lch_l=0,this.lch_c=0,this.lch_h=0,this.hsluv_h=0,this.hsluv_s=0,this.hsluv_l=0,this.hpluv_h=0,this.hpluv_p=0,this.hpluv_l=0,this.r0s=0,this.r0i=0,this.r1s=0,this.r1i=0,this.g0s=0,this.g0i=0,this.g1s=0,this.g1i=0,this.b0s=0,this.b0i=0,this.b1s=0,this.b1i=0}static fromLinear(e){return e<=.0031308?12.92*e:1.055*e**(1/2.4)-.055}static toLinear(e){return e>.04045?((e+.055)/1.055)**2.4:e/12.92}static yToL(t){return t<=e.epsilon?t/e.refY*e.kappa:116*(t/e.refY)**(1/3)-16}static lToY(t){return t<=8?e.refY*t/e.kappa:e.refY*((t+16)/116)**3}static rgbChannelToHex(t){let n=Math.round(t*255),r=n%16,i=(n-r)/16|0;return e.hexChars.charAt(i)+e.hexChars.charAt(r)}static hexToRgbChannel(t,n){let r=e.hexChars.indexOf(t.charAt(n)),i=e.hexChars.indexOf(t.charAt(n+1));return(r*16+i)/255}static distanceFromOriginAngle(e,t,n){let r=t/(Math.sin(n)-e*Math.cos(n));return r<0?1/0:r}static distanceFromOrigin(e,t){return Math.abs(t)/Math.sqrt(e**2+1)}static min6(e,t,n,r,i,a){return Math.min(e,Math.min(t,Math.min(n,Math.min(r,Math.min(i,a)))))}rgbToHex(){this.hex=`#`,this.hex+=e.rgbChannelToHex(this.rgb_r),this.hex+=e.rgbChannelToHex(this.rgb_g),this.hex+=e.rgbChannelToHex(this.rgb_b)}hexToRgb(){this.hex=this.hex.toLowerCase(),this.rgb_r=e.hexToRgbChannel(this.hex,1),this.rgb_g=e.hexToRgbChannel(this.hex,3),this.rgb_b=e.hexToRgbChannel(this.hex,5)}xyzToRgb(){this.rgb_r=e.fromLinear(e.m_r0*this.xyz_x+e.m_r1*this.xyz_y+e.m_r2*this.xyz_z),this.rgb_g=e.fromLinear(e.m_g0*this.xyz_x+e.m_g1*this.xyz_y+e.m_g2*this.xyz_z),this.rgb_b=e.fromLinear(e.m_b0*this.xyz_x+e.m_b1*this.xyz_y+e.m_b2*this.xyz_z)}rgbToXyz(){let t=e.toLinear(this.rgb_r),n=e.toLinear(this.rgb_g),r=e.toLinear(this.rgb_b);this.xyz_x=.41239079926595*t+.35758433938387*n+.18048078840183*r,this.xyz_y=.21263900587151*t+.71516867876775*n+.072192315360733*r,this.xyz_z=.019330818715591*t+.11919477979462*n+.95053215224966*r}xyzToLuv(){let t=this.xyz_x+15*this.xyz_y+3*this.xyz_z,n=4*this.xyz_x,r=9*this.xyz_y;t===0?(n=NaN,r=NaN):(n/=t,r/=t),this.luv_l=e.yToL(this.xyz_y),this.luv_l===0?(this.luv_u=0,this.luv_v=0):(this.luv_u=13*this.luv_l*(n-e.refU),this.luv_v=13*this.luv_l*(r-e.refV))}luvToXyz(){if(this.luv_l===0){this.xyz_x=0,this.xyz_y=0,this.xyz_z=0;return}let t=this.luv_u/(13*this.luv_l)+e.refU,n=this.luv_v/(13*this.luv_l)+e.refV;this.xyz_y=e.lToY(this.luv_l),this.xyz_x=0-9*this.xyz_y*t/((t-4)*n-t*n),this.xyz_z=(9*this.xyz_y-15*n*this.xyz_y-n*this.xyz_x)/(3*n)}luvToLch(){this.lch_l=this.luv_l,this.lch_c=Math.sqrt(this.luv_u*this.luv_u+this.luv_v*this.luv_v),this.lch_c<1e-8?this.lch_h=0:(this.lch_h=Math.atan2(this.luv_v,this.luv_u)*180/Math.PI,this.lch_h<0&&(this.lch_h=360+this.lch_h))}lchToLuv(){let e=this.lch_h/180*Math.PI;this.luv_l=this.lch_l,this.luv_u=Math.cos(e)*this.lch_c,this.luv_v=Math.sin(e)*this.lch_c}calculateBoundingLines(t){let n=(t+16)**3/1560896,r=n>e.epsilon?n:t/e.kappa,i=r*(284517*e.m_r0-94839*e.m_r2),a=r*(838422*e.m_r2+769860*e.m_r1+731718*e.m_r0),o=r*(632260*e.m_r2-126452*e.m_r1),s=r*(284517*e.m_g0-94839*e.m_g2),c=r*(838422*e.m_g2+769860*e.m_g1+731718*e.m_g0),l=r*(632260*e.m_g2-126452*e.m_g1),u=r*(284517*e.m_b0-94839*e.m_b2),d=r*(838422*e.m_b2+769860*e.m_b1+731718*e.m_b0),f=r*(632260*e.m_b2-126452*e.m_b1);this.r0s=i/o,this.r0i=a*t/o,this.r1s=i/(o+126452),this.r1i=(a-769860)*t/(o+126452),this.g0s=s/l,this.g0i=c*t/l,this.g1s=s/(l+126452),this.g1i=(c-769860)*t/(l+126452),this.b0s=u/f,this.b0i=d*t/f,this.b1s=u/(f+126452),this.b1i=(d-769860)*t/(f+126452)}calcMaxChromaHpluv(){let t=e.distanceFromOrigin(this.r0s,this.r0i),n=e.distanceFromOrigin(this.r1s,this.r1i),r=e.distanceFromOrigin(this.g0s,this.g0i),i=e.distanceFromOrigin(this.g1s,this.g1i),a=e.distanceFromOrigin(this.b0s,this.b0i),o=e.distanceFromOrigin(this.b1s,this.b1i);return e.min6(t,n,r,i,a,o)}calcMaxChromaHsluv(t){let n=t/360*Math.PI*2,r=e.distanceFromOriginAngle(this.r0s,this.r0i,n),i=e.distanceFromOriginAngle(this.r1s,this.r1i,n),a=e.distanceFromOriginAngle(this.g0s,this.g0i,n),o=e.distanceFromOriginAngle(this.g1s,this.g1i,n),s=e.distanceFromOriginAngle(this.b0s,this.b0i,n),c=e.distanceFromOriginAngle(this.b1s,this.b1i,n);return e.min6(r,i,a,o,s,c)}hsluvToLch(){this.hsluv_l>99.9999999?(this.lch_l=100,this.lch_c=0):this.hsluv_l<1e-8?(this.lch_l=0,this.lch_c=0):(this.lch_l=this.hsluv_l,this.calculateBoundingLines(this.hsluv_l),this.lch_c=this.calcMaxChromaHsluv(this.hsluv_h)/100*this.hsluv_s),this.lch_h=this.hsluv_h}lchToHsluv(){if(this.lch_l>99.9999999)this.hsluv_s=0,this.hsluv_l=100;else if(this.lch_l<1e-8)this.hsluv_s=0,this.hsluv_l=0;else{this.calculateBoundingLines(this.lch_l);let e=this.calcMaxChromaHsluv(this.lch_h);this.hsluv_s=this.lch_c/e*100,this.hsluv_l=this.lch_l}this.hsluv_h=this.lch_h}hpluvToLch(){this.hpluv_l>99.9999999?(this.lch_l=100,this.lch_c=0):this.hpluv_l<1e-8?(this.lch_l=0,this.lch_c=0):(this.lch_l=this.hpluv_l,this.calculateBoundingLines(this.hpluv_l),this.lch_c=this.calcMaxChromaHpluv()/100*this.hpluv_p),this.lch_h=this.hpluv_h}lchToHpluv(){if(this.lch_l>99.9999999)this.hpluv_p=0,this.hpluv_l=100;else if(this.lch_l<1e-8)this.hpluv_p=0,this.hpluv_l=0;else{this.calculateBoundingLines(this.lch_l);let e=this.calcMaxChromaHpluv();this.hpluv_p=this.lch_c/e*100,this.hpluv_l=this.lch_l}this.hpluv_h=this.lch_h}hsluvToRgb(){this.hsluvToLch(),this.lchToLuv(),this.luvToXyz(),this.xyzToRgb()}hpluvToRgb(){this.hpluvToLch(),this.lchToLuv(),this.luvToXyz(),this.xyzToRgb()}hsluvToHex(){this.hsluvToRgb(),this.rgbToHex()}hpluvToHex(){this.hpluvToRgb(),this.rgbToHex()}rgbToHsluv(){this.rgbToXyz(),this.xyzToLuv(),this.luvToLch(),this.lchToHpluv(),this.lchToHsluv()}rgbToHpluv(){this.rgbToXyz(),this.xyzToLuv(),this.luvToLch(),this.lchToHpluv(),this.lchToHpluv()}hexToHsluv(){this.hexToRgb(),this.rgbToHsluv()}hexToHpluv(){this.hexToRgb(),this.rgbToHpluv()}};q.hexChars=`0123456789abcdef`,q.refY=1,q.refU=.19783000664283,q.refV=.46831999493879,q.kappa=903.2962962,q.epsilon=.0088564516,q.m_r0=3.240969941904521,q.m_r1=-1.537383177570093,q.m_r2=-.498610760293,q.m_g0=-.96924363628087,q.m_g1=1.87596750150772,q.m_g2=.041555057407175,q.m_b0=.055630079696993,q.m_b1=-.20397695888897,q.m_b2=1.056971514242878;var Bn=s(((e,t)=>{function n(e,t){return Object.prototype.hasOwnProperty.call(e,t)}t.exports=n})),Vn=s(((e,t)=>{var n=Bn(),r,i;function a(){for(var e in i=[`toString`,`toLocaleString`,`valueOf`,`hasOwnProperty`,`isPrototypeOf`,`propertyIsEnumerable`,`constructor`],r=!0,{toString:null})r=!1}function o(e,t,o){var c,l=0;for(c in r??a(),e)if(s(t,e,c,o)===!1)break;if(r)for(var u=e.constructor,d=!!u&&e===u.prototype;(c=i[l++])&&!((c!==`constructor`||!d&&n(e,c))&&e[c]!==Object.prototype[c]&&s(t,e,c,o)===!1););}function s(e,t,n,r){return e.call(r,t[n],n,t)}t.exports=o})),Hn=s(((e,t)=>{var n=Vn();function r(e){var t=[];return n(e,function(e,n){typeof e==`function`&&t.push(n)}),t.sort()}t.exports=r})),Un=s(((e,t)=>{function n(e,t,n){var r=e.length;t=t==null?0:t<0?Math.max(r+t,0):Math.min(t,r),n=n==null?r:n<0?Math.max(r+n,0):Math.min(n,r);for(var i=[];t{var n=Un();function r(e,t,r){var i=n(arguments,2);return function(){return e.apply(t,i.concat(n(arguments)))}}t.exports=r})),Gn=s(((e,t)=>{function n(e,t,n){if(e!=null)for(var r=-1,i=e.length;++r{var n=Hn(),r=Wn(),i=Gn(),a=Un();function o(e,t){i(arguments.length>1?a(arguments,1):n(e),function(t){e[t]=r(e[t],e)})}t.exports=o})),J=s(((e,t)=>{var n=Bn(),r=Vn();function i(e,t,i){r(e,function(r,a){if(n(e,a))return t.call(i,e[a],a,e)})}t.exports=i})),qn=s(((e,t)=>{function n(e){return e}t.exports=n})),Jn=s(((e,t)=>{function n(e){return function(t){return t[e]}}t.exports=n})),Yn=s(((e,t)=>{var n=/^\[object (.*)\]$/,r=Object.prototype.toString,i;function a(e){return e===null?`Null`:e===i?`Undefined`:n.exec(r.call(e))[1]}t.exports=a})),Xn=s(((e,t)=>{var n=Yn();function r(e,t){return n(e)===t}t.exports=r})),Zn=s(((e,t)=>{var n=Xn();t.exports=Array.isArray||function(e){return n(e,`Array`)}})),Qn=s(((e,t)=>{var n=J(),r=Zn();function i(e,t){for(var n=-1,r=e.length;++n{var n=qn(),r=Jn(),i=Qn();function a(e,t){if(e==null)return n;switch(typeof e){case`function`:return t===void 0?e:function(n,r,i){return e.call(t,n,r,i)};case`object`:return function(t){return i(t,e)};case`string`:case`number`:return r(e)}}t.exports=a})),$n=s(((e,t)=>{var n=J(),r=Y();function i(e,t,i){t=r(t,i);var a=!1;return n(e,function(n,r){if(t(n,r,e))return a=!0,!1}),a}t.exports=i})),er=s(((e,t)=>{var n=$n();function r(e,t){return n(e,function(e){return e===t})}t.exports=r})),tr=s(((e,t)=>{function n(e){return!!e&&typeof e==`object`&&e.constructor===Object}t.exports=n})),nr=s(((e,t)=>{var n=J(),r=tr();function i(e,t){for(var a=0,o=arguments.length,s;++a{var n=J(),r=tr();function i(e,t){for(var r=0,i=arguments.length,o;++r{var n=J(),r=Y();function i(e,t,i){t=r(t,i);var a=!0;return n(e,function(n,r){if(!t(n,r,e))return a=!1,!1}),a}t.exports=i})),ar=s(((e,t)=>{var n=Xn();function r(e){return n(e,`Object`)}t.exports=r})),or=s(((e,t)=>{function n(e,t){return e===t?e!==0||1/e==1/t:e!==e&&t!==t}t.exports=n})),sr=s(((e,t)=>{var n=Bn(),r=ir(),i=ar(),a=or();function o(e){return function(t,r){return n(this,r)&&e(t,this[r])}}function s(e,t){return n(this,t)}function c(e,t,n){return n||=a,!i(e)||!i(t)?n(e,t):r(e,o(n),t)&&r(t,s,e)}t.exports=c})),cr=s(((e,t)=>{var n=Gn(),r=Un(),i=J();function a(e,t){return n(r(arguments,1),function(t){i(t,function(t,n){e[n]??(e[n]=t)})}),e}t.exports=a})),lr=s(((e,t)=>{var n=J(),r=Y();function i(e,t,i){t=r(t,i);var a={};return n(e,function(e,n,r){t(e,n,r)&&(a[n]=e)}),a}t.exports=i})),ur=s(((e,t)=>{var n=$n(),r=Y();function i(e,t,i){t=r(t,i);var a;return n(e,function(e,n,r){if(t(e,n,r))return a=e,!0}),a}t.exports=i})),dr=s(((e,t)=>{var n=J(),r=tr();function i(e,t,a,o){return n(e,function(e,n){var s=a?a+`.`+n:n;o!==0&&r(e)?i(e,t,s,o-1):t[s]=e}),t}function a(e,t){return e==null?{}:(t??=-1,i(e,{},``,t))}t.exports=a})),fr=s(((e,t)=>{function n(e){switch(typeof e){case`string`:case`number`:case`boolean`:return!0}return e==null}t.exports=n})),pr=s(((e,t)=>{fr();function n(e,t){for(var n=t.split(`.`),r=n.pop();t=n.shift();)if(e=e[t],e==null)return;return e[r]}t.exports=n})),mr=s(((e,t)=>{var n=pr(),r;function i(e,t){return n(e,t)!==r}t.exports=i})),hr=s(((e,t)=>{var n=J();t.exports=Object.keys||function(e){var t=[];return n(e,function(e,n){t.push(n)}),t}})),gr=s(((e,t)=>{var n=J(),r=Y();function i(e,t,i){t=r(t,i);var a={};return n(e,function(e,n,r){a[n]=t(e,n,r)}),a}t.exports=i})),_r=s(((e,t)=>{var n=J();function r(e,t){var r=!0;return n(t,function(t,n){if(e[n]!==t)return r=!1}),r}t.exports=r})),vr=s(((e,t)=>{var n=Y();function r(e,t,r){if(e==null||!e.length)return 1/0;if(e.length&&!t)return Math.max.apply(Math,e);t=n(t,r);for(var i,a=-1/0,o,s,c=-1,l=e.length;++ca&&(a=s,i=o);return i}t.exports=r})),yr=s(((e,t)=>{var n=J();function r(e){var t=[];return n(e,function(e,n){t.push(e)}),t}t.exports=r})),br=s(((e,t)=>{var n=vr(),r=yr();function i(e,t){return n(r(e),t)}t.exports=i})),xr=s(((e,t)=>{var n=J();function r(e,t){for(var r=0,a=arguments.length,o;++r{var n=Yn(),r=tr(),i=xr();function a(e){switch(n(e)){case`Object`:return o(e);case`Array`:return l(e);case`RegExp`:return s(e);case`Date`:return c(e);default:return e}}function o(e){return r(e)?i({},e):e}function s(e){var t=``;return t+=e.multiline?`m`:``,t+=e.global?`g`:``,t+=e.ignoreCase?`i`:``,new RegExp(e.source,t)}function c(e){return new Date(+e)}function l(e){return e.slice()}t.exports=a})),Cr=s(((e,t)=>{var n=Sr(),r=J(),i=Yn(),a=tr();function o(e,t){switch(i(e)){case`Object`:return s(e,t);case`Array`:return c(e,t);default:return n(e)}}function s(e,t){if(a(e)){var n={};return r(e,function(e,n){this[n]=o(e,t)},n),n}else if(t)return t(e);else return e}function c(e,t){for(var n=[],r=-1,i=e.length;++r{var n=Bn(),r=Cr(),i=ar();function a(){for(var e=1,t,o,s,c=r(arguments[0]);s=arguments[e++];)for(t in s)n(s,t)&&(o=s[t],i(o)&&i(c[t])?c[t]=a(c[t],o):c[t]=r(o));return c}t.exports=a})),Tr=s(((e,t)=>{var n=Y();function r(e,t,r){if(e==null||!e.length)return-1/0;if(e.length&&!t)return Math.min.apply(Math,e);t=n(t,r);for(var i,a=1/0,o,s,c=-1,l=e.length;++c{var n=Tr(),r=yr();function i(e,t){return n(r(e),t)}t.exports=i})),Dr=s(((e,t)=>{var n=Gn();function r(e,t){return t&&n(t.split(`.`),function(t){e[t]||(e[t]={}),e=e[t]}),e}t.exports=r})),Or=s(((e,t)=>{function n(e,t,n){if(n||=0,e==null)return-1;for(var r=e.length,i=n<0?r+n:n;i{var n=Or();function r(e,t){return n(e,t)!==-1}t.exports=r})),Ar=s(((e,t)=>{var n=Un(),r=kr();function i(e,t){var i=typeof arguments[1]==`string`?n(arguments,1):arguments[1],a={};for(var o in e)e.hasOwnProperty(o)&&!r(i,o)&&(a[o]=e[o]);return a}t.exports=i})),jr=s(((e,t)=>{var n=Un();function r(e,t){for(var r=typeof arguments[1]==`string`?n(arguments,1):arguments[1],i={},a=0,o;o=r[a++];)i[o]=e[o];return i}t.exports=r})),Mr=s(((e,t)=>{var n=gr(),r=Jn();function i(e,t){return n(e,r(t))}t.exports=i})),Nr=s(((e,t)=>{var n=J();function r(e){var t=0;return n(e,function(){t++}),t}t.exports=r})),Pr=s(((e,t)=>{var n=J(),r=Nr();function i(e,t,i,a){var o=arguments.length>2;if(!r(e)&&!o)throw Error(`reduce of empty object with no initial value`);return n(e,function(e,n,r){o?i=t.call(a,i,e,n,r):(i=e,o=!0)}),i}t.exports=i})),Fr=s(((e,t)=>{var n=lr(),r=Y();function i(e,t,i){return t=r(t,i),n(e,function(e,n,r){return!t(e,n,r)},i)}t.exports=i})),Ir=s(((e,t)=>{var n=Xn();function r(e){return n(e,`Function`)}t.exports=r})),Lr=s(((e,t)=>{var n=Ir();function r(e,t){var r=e[t];if(r!==void 0)return n(r)?r.call(e):r}t.exports=r})),Rr=s(((e,t)=>{var n=Dr();function r(e,t,r){var i=/^(.+)\.(.+)$/.exec(t);i?n(e,i[1])[i[2]]=r:e[t]=r}t.exports=r})),zr=s(((e,t)=>{var n=mr();function r(e,t){if(n(e,t)){for(var r=t.split(`.`),i=r.pop();t=r.shift();)e=e[t];return delete e[i]}else return!0}t.exports=r})),Br=s(((e,t)=>{t.exports={bindAll:Kn(),contains:er(),deepFillIn:nr(),deepMatches:Qn(),deepMixIn:rr(),equals:sr(),every:ir(),fillIn:cr(),filter:lr(),find:ur(),flatten:dr(),forIn:Vn(),forOwn:J(),functions:Hn(),get:pr(),has:mr(),hasOwn:Bn(),keys:hr(),map:gr(),matches:_r(),max:br(),merge:wr(),min:Er(),mixIn:xr(),namespace:Dr(),omit:Ar(),pick:jr(),pluck:Mr(),reduce:Pr(),reject:Fr(),result:Lr(),set:Rr(),size:Nr(),some:$n(),unset:zr(),values:yr()}})),Vr=s(((e,t)=>{Object.defineProperty(e,`__esModule`,{value:!0}),e.default=(0,Br().map)({A:{x:.44758,y:.40745},C:{x:.31006,y:.31616},D50:{x:.34567,y:.35851},D65:{x:.31272,y:.32903},D55:{x:.33243,y:.34744},D75:{x:.29903,y:.31488}},function(e){return[100*(e.x/e.y),100,100*(1-e.x-e.y)/e.y]}),t.exports=e.default})),Hr=s(((e,t)=>{Object.defineProperty(e,`__esModule`,{value:!0});var n=Math,r=n.pow,i=n.sign,a=n.abs,o={decode:function(e){return e<=.04045?e/12.92:r((e+.055)/1.055,2.4)},encode:function(e){return e<=.0031308?12.92*e:1.055*r(e,1/2.4)-.055}},s={encode:function(e){return e<.001953125?16*e:r(e,1/1.8)},decode:function(e){return e<16*.001953125?e/16:r(e,1.8)}};function c(e){return{decode:function(t){return i(t)*r(a(t),e)},encode:function(t){return i(t)*r(a(t),1/e)}}}e.default={sRGB:{r:{x:.64,y:.33},g:{x:.3,y:.6},b:{x:.15,y:.06},gamma:o},"Adobe RGB":{r:{x:.64,y:.33},g:{x:.21,y:.71},b:{x:.15,y:.06},gamma:c(2.2)},"Wide Gamut RGB":{r:{x:.7347,y:.2653},g:{x:.1152,y:.8264},b:{x:.1566,y:.0177},gamma:c(563/256)},"ProPhoto RGB":{r:{x:.7347,y:.2653},g:{x:.1596,y:.8404},b:{x:.0366,y:1e-4},gamma:s}},t.exports=e.default})),Ur=s((e=>{Object.defineProperty(e,`__esModule`,{value:!0});function t(e){return[[e[0][0],e[1][0],e[2][0]],[e[0][1],e[1][1],e[2][1]],[e[0][2],e[1][2],e[2][2]]]}function n(e){return e[0][0]*(e[2][2]*e[1][1]-e[2][1]*e[1][2])+e[1][0]*(e[2][1]*e[0][2]-e[2][2]*e[0][1])+e[2][0]*(e[1][2]*e[0][1]-e[1][1]*e[0][2])}function r(e){var t=1/n(e);return[[(e[2][2]*e[1][1]-e[2][1]*e[1][2])*t,(e[2][1]*e[0][2]-e[2][2]*e[0][1])*t,(e[1][2]*e[0][1]-e[1][1]*e[0][2])*t],[(e[2][0]*e[1][2]-e[2][2]*e[1][0])*t,(e[2][2]*e[0][0]-e[2][0]*e[0][2])*t,(e[1][0]*e[0][2]-e[1][2]*e[0][0])*t],[(e[2][1]*e[1][0]-e[2][0]*e[1][1])*t,(e[2][0]*e[0][1]-e[2][1]*e[0][0])*t,(e[1][1]*e[0][0]-e[1][0]*e[0][1])*t]]}function i(e,t){return[e[0][0]*t[0]+e[0][1]*t[1]+e[0][2]*t[2],e[1][0]*t[0]+e[1][1]*t[1]+e[1][2]*t[2],e[2][0]*t[0]+e[2][1]*t[1]+e[2][2]*t[2]]}function a(e,t){return[[e[0][0]*t[0],e[0][1]*t[1],e[0][2]*t[2]],[e[1][0]*t[0],e[1][1]*t[1],e[1][2]*t[2]],[e[2][0]*t[0],e[2][1]*t[1],e[2][2]*t[2]]]}function o(e,t){return[[e[0][0]*t[0][0]+e[0][1]*t[1][0]+e[0][2]*t[2][0],e[0][0]*t[0][1]+e[0][1]*t[1][1]+e[0][2]*t[2][1],e[0][0]*t[0][2]+e[0][1]*t[1][2]+e[0][2]*t[2][2]],[e[1][0]*t[0][0]+e[1][1]*t[1][0]+e[1][2]*t[2][0],e[1][0]*t[0][1]+e[1][1]*t[1][1]+e[1][2]*t[2][1],e[1][0]*t[0][2]+e[1][1]*t[1][2]+e[1][2]*t[2][2]],[e[2][0]*t[0][0]+e[2][1]*t[1][0]+e[2][2]*t[2][0],e[2][0]*t[0][1]+e[2][1]*t[1][1]+e[2][2]*t[2][1],e[2][0]*t[0][2]+e[2][1]*t[1][2]+e[2][2]*t[2][2]]]}e.transpose=t,e.determinant=n,e.inverse=r,e.multiply=i,e.scalar=a,e.product=o})),Wr=s((e=>{Object.defineProperty(e,`__esModule`,{value:!0});var t=Math.PI;function n(e){for(var n=e*180/t;n<0;)n+=360;for(;n>360;)n-=360;return n}function r(e){for(var n=t*e/180;n<0;)n+=2*t;for(;n>2*t;)n-=2*t;return n}e.fromRadian=n,e.toRadian=r})),Gr=s((e=>{Object.defineProperty(e,`__esModule`,{value:!0});var t=Math.round;function n(e){return e[0]==`#`&&(e=e.slice(1)),e.length<6&&(e=e.split(``).map(function(e){return e+e}).join(``)),e.match(/../g).map(function(e){return parseInt(e,16)/255})}function r(e){return`#`+e.map(function(e){return e=t(255*e).toString(16),e.length<2&&(e=`0`+e),e}).join(``)}e.fromHex=n,e.toHex=r})),Kr=s(((e,t)=>{Object.defineProperty(e,`__esModule`,{value:!0});var n=o(Ur()),r=a(Vr()),i=a(Hr());function a(e){return e&&e.__esModule?e:{default:e}}function o(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[n]=e[n]);return t.default=e,t}function s(){var e=arguments.length<=0||arguments[0]===void 0?i.default.sRGB:arguments[0],t=arguments.length<=1||arguments[1]===void 0?r.default.D65:arguments[1],a=[e.r,e.g,e.b],o=n.transpose(a.map(function(e){return[e.x/e.y,1,(1-e.x-e.y)/e.y]})),s=e.gamma,c=n.multiply(n.inverse(o),t),l=n.scalar(o,c),u=n.inverse(l);return{fromRgb:function(e){return n.multiply(l,e.map(s.decode))},toRgb:function(e){return n.multiply(u,e).map(s.encode)}}}e.default=s,t.exports=e.default})),qr=s(((e,t)=>{t.exports={illuminant:Vr(),workspace:Hr(),matrix:Ur(),degree:Wr(),rgb:Gr(),xyz:Kr()}})),Jr=s((e=>{Object.defineProperty(e,`__esModule`,{value:!0}),e.cfs=e.distance=e.lerp=e.corLerp=void 0;var t=Br();function n(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function r(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);ti/2&&(e>t?t+=i:e+=i),((1-n)*e+n*t)%(i||1/0)}function u(e,t,n){var r={};for(var i in e)r[i]=l(e[i],t[i],n,i);return r}function d(e,t){var n=0;for(var r in e)n+=o(e[r]-t[r],2);return s(n)}function f(e){return t.merge.apply(void 0,r(e.split(``).map(function(e){return n({},e,!0)})))}e.corLerp=l,e.lerp=u,e.distance=d,e.cfs=f})),Yr=s(((e,t)=>{var n=function(){function e(e,t){var n=[],r=!0,i=!1,a=void 0;try{for(var o=e[Symbol.iterator](),s;!(r=(s=o.next()).done)&&(n.push(s.value),!(t&&n.length===t));r=!0);}catch(e){i=!0,a=e}finally{try{!r&&o.return&&o.return()}finally{if(i)throw a}}return n}return function(t,n){if(Array.isArray(t))return t;if(Symbol.iterator in Object(t))return e(t,n);throw TypeError(`Invalid attempt to destructure non-iterable instance`)}}();Object.defineProperty(e,`__esModule`,{value:!0});var r=qr(),i=Jr();function a(e,t){var a=arguments.length<=2||arguments[2]===void 0?1e-6:arguments[2],o=-a,s=1+a,c=Math,l=c.min,u=c.max,d=n([`000`,`fff`].map(function(n){return t.fromXyz(e.fromRgb(r.rgb.fromHex(n)))}),2),f=d[0],p=d[1];function m(n){var r=e.toRgb(t.toXyz(n));return[r.map(function(e){return e>=o&&e<=s}).reduce(function(e,t){return e&&t},!0),r]}function h(e,t){for(var r=arguments.length<=2||arguments[2]===void 0?.001:arguments[2];(0,i.distance)(e,t)>r;){var a=(0,i.lerp)(e,t,.5);n(m(a),1)[0]?e=a:t=a}return e}function g(e){return(0,i.lerp)(f,p,e)}function _(e){return e.map(function(e){return u(o,l(s,e))})}return{contains:m,limit:h,spine:g,crop:_}}e.default=a,t.exports=e.default})),Xr=s((e=>{var t=function(){function e(e,t){var n=[],r=!0,i=!1,a=void 0;try{for(var o=e[Symbol.iterator](),s;!(r=(s=o.next()).done)&&(n.push(s.value),!(t&&n.length===t));r=!0);}catch(e){i=!0,a=e}finally{try{!r&&o.return&&o.return()}finally{if(i)throw a}}return n}return function(t,n){if(Array.isArray(t))return t;if(Symbol.iterator in Object(t))return e(t,n);throw TypeError(`Invalid attempt to destructure non-iterable instance`)}}();Object.defineProperty(e,`__esModule`,{value:!0}),e.toNotation=e.fromNotation=e.toHue=e.fromHue=void 0;var n=Jr(),r=Math.floor,i=[{s:`R`,h:20.14,e:.8,H:0},{s:`Y`,h:90,e:.7,H:100},{s:`G`,h:164.25,e:1,H:200},{s:`B`,h:237.53,e:1.2,H:300},{s:`R`,h:380.14,e:.8,H:400}],a=i.map(function(e){return e.s}).slice(0,-1).join(``);function o(e){e50){var o=[n,t];t=o[0],n=o[1],i=100-i}return i<1?a[t]:a[t]+i.toFixed()+a[n]}e.fromHue=o,e.toHue=s,e.fromNotation=l,e.toNotation=u})),Zr=s(((e,t)=>{var n=function(){function e(e,t){var n=[],r=!0,i=!1,a=void 0;try{for(var o=e[Symbol.iterator](),s;!(r=(s=o.next()).done)&&(n.push(s.value),!(t&&n.length===t));r=!0);}catch(e){i=!0,a=e}finally{try{!r&&o.return&&o.return()}finally{if(i)throw a}}return n}return function(t,n){if(Array.isArray(t))return t;if(Symbol.iterator in Object(t))return e(t,n);throw TypeError(`Invalid attempt to destructure non-iterable instance`)}}();Object.defineProperty(e,`__esModule`,{value:!0});var r=qr(),i=s(Xr()),a=Jr(),o=Br();function s(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[n]=e[n]);return t.default=e,t}var c=Math,l=c.pow,u=c.sqrt,d=c.exp,f=c.abs,p=c.sign,m=Math,h=m.sin,g=m.cos,_=m.atan2,v={average:{F:1,c:.69,N_c:1},dim:{F:.9,c:.59,N_c:.9},dark:{F:.8,c:.535,N_c:.8}},y=[[.7328,.4296,-.1624],[-.7036,1.6975,.0061],[.003,.0136,.9834]],b=[[.38971,.68898,-.07868],[-.22981,1.1834,.04641],[0,0,1]],x=y,S=r.matrix.inverse(y),C=r.matrix.product(b,r.matrix.inverse(y)),w=r.matrix.product(y,r.matrix.inverse(b)),T={whitePoint:r.illuminant.D65,adaptingLuminance:40,backgroundLuminance:20,surroundType:`average`,discounting:!1},ee=(0,a.cfs)(`QJMCshH`),E=(0,a.cfs)(`JCh`);function D(){var e=arguments.length<=0||arguments[0]===void 0?{}:arguments[0],t=arguments.length<=1||arguments[1]===void 0?ee:arguments[1];e=(0,o.merge)(T,e);var a=e.whitePoint,s=e.adaptingLuminance,c=e.backgroundLuminance,m=v[e.surroundType],b=m.F,D=m.c,O=m.N_c,te=a[1],k=1/(5*s+1),A=.2*l(k,4)*5*s+.1*l(1-l(k,4),2)*l(5*s,1/3),ne=c/te,re=.725*l(1/ne,.2),ie=re,ae=1.48+u(ne),oe=e.discounting?1:b*(1-1/3.6*d(-(s+42)/92)),j=n(r.matrix.multiply(y,a).map(function(e){return oe*te/e+1-oe}),3),se=j[0],M=j[1],ce=j[2],N=pe(de(le(a)));function le(e){var t=n(r.matrix.multiply(x,e),3),i=t[0],a=t[1],o=t[2];return[se*i,M*a,ce*o]}function ue(e){var t=n(e,3),i=t[0],a=t[1],o=t[2];return r.matrix.multiply(S,[i/se,a/M,o/ce])}function de(e){return r.matrix.multiply(C,e).map(function(e){var t=l(A*f(e)/100,.42);return p(e)*400*t/(27.13+t)+.1})}function fe(e){return r.matrix.multiply(w,e.map(function(e){var t=e-.1;return p(t)*100/A*l(27.13*f(t)/(400-f(t)),1/.42)}))}function pe(e){var t=n(e,3),r=t[0],i=t[1],a=t[2];return(r*2+i+a/20-.305)*re}function me(e){return 4/D*u(e/100)*(N+4)*l(A,.25)}function he(e){return 6.25*l(D*e/((N+4)*l(A,.25)),2)}function ge(e){return e*l(A,.25)}function _e(e,t){return l(e/100,2)*t/l(A,.25)}function ve(e){return e/l(A,.25)}function ye(e,t){return 100*u(e/t)}function be(e,t){var n=t.Q,r=t.J,a=t.M,o=t.C,s=t.s,c=t.h,l=t.H,u={};return e.J&&(u.J=isNaN(r)?he(n):r),e.C&&(isNaN(o)?isNaN(a)?(n=isNaN(n)?me(r):n,u.C=_e(s,n)):u.C=ve(a):u.C=t.C),e.h&&(u.h=isNaN(c)?i.toHue(l):c),e.Q&&(u.Q=isNaN(n)?me(r):n),e.M&&(u.M=isNaN(a)?ge(o):a),e.s&&(isNaN(s)?(n=isNaN(n)?me(r):n,a=isNaN(a)?ge(o):a,u.s=ye(a,n)):u.s=s),e.H&&(u.H=isNaN(l)?i.fromHue(c):l),u}function P(e){var i=de(le(e)),a=n(i,3),o=a[0],s=a[1],c=a[2],d=o-s*12/11+c/11,f=(o+s-2*c)/9,p=_(f,d),m=r.degree.fromRadian(p),h=1/4*(g(p+2)+3.8),v=100*l(pe(i)/N,D*ae);return be(t,{J:v,C:l(5e4/13*O*ie*h*u(d*d+f*f)/(o+s+21/20*c),.9)*u(v/100)*l(1.64-l(.29,ne),.73),h:m})}function F(e){var t=be(E,e),n=t.J,i=t.C,a=t.h,o=r.degree.toRadian(a),s=l(i/(u(n/100)*l(1.64-l(.29,ne),.73)),10/9),c=1/4*(g(o+2)+3.8),d=N*l(n/100,1/D/ae),p=5e4/13*O*ie*c/s,m=d/re+.305,_=m*61/20*460/1403,v=61/20*220/1403,y=21/20*6300/1403-27/1403,b=h(o),x=g(o),S,C;return s===0||isNaN(s)?S=C=0:f(b)>=f(x)?(C=_/(p/b+v*x/b+y),S=C*x/b):(S=_/(p/x+v+y*b/x),C=S*b/x),ue(fe([20/61*m+451/1403*S+288/1403*C,20/61*m-891/1403*S-261/1403*C,20/61*m-220/1403*S-6300/1403*C]))}return{fromXyz:P,toXyz:F,fillOut:be}}e.default=D,t.exports=e.default})),Qr=s(((e,t)=>{Object.defineProperty(e,`__esModule`,{value:!0});var n=qr(),r=Math,i=r.sqrt,a=r.pow,o=r.exp,s=r.log,c=r.cos,l=r.sin,u=r.atan2,d={LCD:{K_L:.77,c_1:.007,c_2:.0053},SCD:{K_L:1.24,c_1:.007,c_2:.0363},UCS:{K_L:1,c_1:.007,c_2:.0228}};function f(){var e=d[arguments.length<=0||arguments[0]===void 0?`UCS`:arguments[0]],t=e.K_L,r=e.c_1,f=e.c_2;function p(e){var t=e.J,i=e.M,a=e.h,o=n.degree.toRadian(a),u=(1+100*r)*t/(1+r*t),d=1/f*s(1+f*i);return{J_p:u,a_p:d*c(o),b_p:d*l(o)}}function m(e){var t=e.J_p,s=e.a_p,c=e.b_p,l=-t/(r*t-100*r-1),d=(o(f*i(a(s,2)+a(c,2)))-1)/f,p=u(c,s);return{J:l,M:d,h:n.degree.fromRadian(p)}}function h(e,n){return i(a((e.J_p-n.J_p)/t,2)+a(e.a_p-n.a_p,2)+a(e.b_p-n.b_p,2))}return{fromCam:p,toCam:m,distance:h}}e.default=f,t.exports=e.default})),$r=s(((e,t)=>{var n=Jr(),r=Yr(),i=Zr(),a=Qr(),o=Xr();t.exports={gamut:r,cfs:n.cfs,lerp:n.lerp,cam:i,ucs:a,hq:o}})),ei=l(qr(),1),ti=l($r(),1);function ni(e){let t=new q;return t.rgb_r=e[0],t.rgb_g=e[1],t.rgb_b=e[2],t.rgbToHsluv(),[t.hsluv_h,t.hsluv_s,t.hsluv_l]}function ri(e){let t=new q;return t.hsluv_h=e[0],t.hsluv_s=e[1],t.hsluv_l=e[2],t.hsluvToRgb(),[t.rgb_r,t.rgb_g,t.rgb_b]}var ii=ti.default.cam({whitePoint:ei.default.illuminant.D65,adaptingLuminance:40,backgroundLuminance:20,surroundType:`average`,discounting:!1},ti.default.cfs(`JCh`)),ai=ei.default.xyz(ei.default.workspace.sRGB,ei.default.illuminant.D65),oi=e=>ai.toRgb(ii.toXyz({J:e[0],C:e[1],h:e[2]})),si=e=>{let t=ii.fromXyz(ai.fromRgb(e));return[t.J,t.C,t.h]},[ci,li]=(()=>{let e={k_l:1,c1:.007,c2:.0228},t=Math.PI,n=64/t/5,r=1/(5*n+1),i=.2*r**4*(5*n)+.1*(1-r**4)**2*(5*n)**(1/3);return[n=>{let[r,a,o]=n,s=a*i**.25,c=(1+100*e.c1)*r/(1+e.c1*r);c/=e.k_l;let l=1/e.c2*Math.log(1+e.c2*s),u=l*Math.cos(t/180*o),d=l*Math.sin(t/180*o);return[c,u,d]},n=>{let[r,a,o]=n,s=Math.sqrt(a*a+o*o),c=(Math.exp(s*e.c2)-1)/e.c2,l=(180/t*Math.atan2(o,a)+360)%360,u=c/i**.25;return[r/(1+e.c1*(100-r)),u,l]}]})(),ui=e=>oi(li(e)),di=e=>ci(si(e)),fi=console;fi.color=(e,t=``)=>{let n=K(e).luminance();fi.log(`%c${e} ${t}`,`background-color: ${e};padding: 5px; border-radius: 5px; color: ${n>.5?`#000`:`#fff`}`)},fi.ramp=(e,t=1)=>{fi.log(`%c `,`font-size: 1px;line-height: 16px;background: ${K.getCSSGradient(e,t)};padding: 0 0 0 200px; border-radius: 2px;`)};var pi=(e,t,n,r,i,a,o=.1)=>{if(e===n||t===r)return!0;let s=(r-t)/(n-e),c=(a+i/s-t+s*e)/(s+1/s),l=a+i/s-c/s;return(i-c)**2+(a-l)**2{let i=(t[0]+n[0])/2,a=e(i);return pi(...t,...n,i,a,r)?null:[i,a]},hi=(e,t,n,r=.1)=>{let i=(n-t)/10,a=[];for(let r=t;rMath.round(e*10**t)/10**t,_i=(e,t=1,n=90,r=.005)=>{let i=hi(t=>e(t).gl()[0],0,t,r),a=hi(t=>e(t).gl()[1],0,t,r),o=hi(t=>e(t).gl()[2],0,t,r);return`linear-gradient(${n}deg, ${Array.from(new Set([...i.map(e=>gi(e[0])),...a.map(e=>gi(e[0])),...o.map(e=>gi(e[0]))].sort((e,t)=>e-t))).map(t=>`${e(t).hex()} ${gi(t*100)}%`).join()});`},vi=e=>{e.Color.prototype.jch=function(){return si(this._rgb.slice(0,3).map(e=>e/255))},e.jch=(...t)=>new e.Color(...oi(t).map(e=>Math.floor(e*255)),`rgb`),e.Color.prototype.jab=function(){return di(this._rgb.slice(0,3).map(e=>e/255))},e.jab=(...t)=>new e.Color(...ui(t).map(e=>Math.floor(e*255)),`rgb`),e.Color.prototype.hsluv=function(){return ni(this._rgb.slice(0,3).map(e=>e/255))},e.hsluv=(...t)=>new e.Color(...ri(t).map(e=>Math.floor(e*255)),`rgb`);let t=e.interpolate,n={jch:si,jab:di,hsluv:ni},r=(e,t,n)=>(Math.abs(e-t)>360/2&&(e>t?t+=360:e+=360),((1-n)*e+n*t)%360);e.interpolate=(i,a,o=.5,s=`lrgb`)=>{if(n[s]){typeof i!=`object`&&(i=new e.Color(i)),typeof a!=`object`&&(a=new e.Color(a));let t=n[s](i.gl()),c=n[s](a.gl()),l=Number.isNaN(i.hsl()[0]),u=Number.isNaN(a.hsl()[0]),d,f,p;switch(s){case`hsluv`:t[1]<1e-10&&(t[0]=c[0]),t[1]===0&&(t[1]=c[1]),c[1]<1e-10&&(c[0]=t[0]),c[1]===0&&(c[1]=t[1]),d=r(t[0],c[0],o),f=t[1]+(c[1]-t[1])*o,p=t[2]+(c[2]-t[2])*o;break;case`jch`:l&&(t[2]=c[2]),u&&(c[2]=t[2]),d=t[0]+(c[0]-t[0])*o,f=t[1]+(c[1]-t[1])*o,p=r(t[2],c[2],o);break;default:d=t[0]+(c[0]-t[0])*o,f=t[1]+(c[1]-t[1])*o,p=t[2]+(c[2]-t[2])*o}return e[s](d,f,p).alpha(i.alpha()+o*(a.alpha()-i.alpha()))}return t(i,a,o,s)},e.getCSSGradient=_i},X={mainTRC:2.4,get mainTRCencode(){return 1/this.mainTRC},sRco:.2126729,sGco:.7151522,sBco:.072175,normBG:.56,normTXT:.57,revTXT:.62,revBG:.65,blkThrs:.022,blkClmp:1.414,scaleBoW:1.14,scaleWoB:1.14,loBoWoffset:.027,loWoBoffset:.027,deltaYmin:5e-4,loClip:.1,mFactor:1.9468554433171,get mFactInv(){return 1/this.mFactor},mOffsetIn:.0387393816571401,mExpAdj:.283343396420869,get mExp(){return this.mExpAdj/this.blkClmp},mOffsetOut:.312865795870758};function yi(e,t,n=-1){let r=[0,1.1];if(isNaN(e)||isNaN(t)||Math.min(e,t)r[1])return 0;let i=0,a=0,o=`BoW`;return e=e>X.blkThrs?e:e+(X.blkThrs-e)**+X.blkClmp,t=t>X.blkThrs?t:t+(X.blkThrs-t)**+X.blkClmp,Math.abs(t-e)e?(i=(t**+X.normBG-e**+X.normTXT)*X.scaleBoW,a=i-X.loClip?0:i+X.loWoBoffset),n<0?a*100:n==0?Math.round(Math.abs(a)*100)+``+o+``:Number.isInteger(n)?(a*100).toFixed(n):0)}function bi(e=[0,0,0]){function t(e){return(e/255)**X.mainTRC}return X.sRco*t(e[0])+X.sGco*t(e[1])+X.sBco*t(e[2])}var xi=(e,t,n,r,i,a,o,s,c)=>{let l=1-c,u=l*l,d=u*l,f=c*c*c;return{x:d*e+u*3*c*n+l*3*c*c*i+f*o,y:d*t+u*3*c*r+l*3*c*c*a+f*s}},Si=(e,t)=>{let n=[],r={x:+e[0],y:+e[1]};for(let i=0,a=e.length;a-2*!t>i;i+=2){let o=[{x:+e[i-2],y:+e[i-1]},{x:+e[i],y:+e[i+1]},{x:+e[i+2],y:+e[i+3]},{x:+e[i+4],y:+e[i+5]}];t?i?a-4===i?o[3]={x:+e[0],y:+e[1]}:a-2===i&&(o[2]={x:+e[0],y:+e[1]},o[3]={x:+e[2],y:+e[3]}):o[0]={x:+e[a-2],y:+e[a-1]}:a-4===i?o[3]=o[2]:i||(o[0]={x:+e[i],y:+e[i+1]}),n.push([r.x,r.y,(-o[0].x+6*o[1].x+o[2].x)/6,(-o[0].y+6*o[1].y+o[2].y)/6,(o[1].x+6*o[2].x-o[3].x)/6,(o[1].y+6*o[2].y-o[3].y)/6,o[2].x,o[2].y]),r=o[2]}return n},Ci=(e,t,n,r,i,a,o,s)=>{let c=e,l=t,u=0;for(let d=1;d<5;d++){let{x:f,y:p}=xi(e,t,n,r,i,a,o,s,d/5);u+=Math.hypot(f-c,p-l),c=f,l=p}return u+=Math.hypot(o-c,s-l),u},wi=(e,t,n,r,i,a,o,s)=>{let c=Math.floor(Ci(e,t,n,r,i,a,o,s)*.75),l=[],u=0;for(let d=0;d<=c;d++){let f=xi(e,t,n,r,i,a,o,s,d/c),p=Math.round(f.x);if(l[p]=f.y,p-u>1){let e=l[u],t=l[p];for(let n=u+1;nl[Math.round(e)]||null},Ti={CAM02:`jab`,CAM02p:`jch`,HEX:`hex`,HSL:`hsl`,HSLuv:`hsluv`,HSV:`hsv`,LAB:`lab`,LCH:`lch`,RGB:`rgb`,OKLAB:`oklab`,OKLCH:`oklch`};function Z(e,t=0){let n=10**t;return Math.round(e*n)/n}function Ei(e,t){let n;return n=e>1?(e-1)*t+1:e<-1?(e+1)*t-1:1,Z(n,2)}function Di(e){return K(String(e)).jch()}function Oi(e){return K(String(e)).hsluv()}function ki(e,t,n){let r=[[],[],[]];if(e.forEach((e,n)=>r.forEach((r,i)=>r.push(t[n],e[i]))),n===`hcl`){let e=r[1];for(let t=1;t{let t=[];for(let n=1;n{e[t]=e[n]}),t.length=0;break}if(t.length){let n=K(`#ccc`).jch()[2];t.forEach(t=>{e[t]=n})}t.length=0;for(let n=e.length-1;n>0;n-=2)if(Number.isNaN(e[n]))t.push(n);else{t.forEach(t=>{e[t]=e[n]});break}for(let t=1;tSi(e).map(e=>wi(...e)));return e=>{let t=i.map(t=>{for(let n=0;nr*t**e+i}function ji({swatches:e,colorKeys:t,colorspace:n,colorSpace:r=n??`LAB`,shift:i=1,fullScale:a=!0,smooth:o=!1,distributeLightness:s=`linear`,sortColor:c=!0,asFun:l=!1}={}){n!==void 0&&console.warn("Leonardo: `colorspace` is deprecated. Use `colorSpace` instead.");let u=Ti[r];if(!u)throw Error(`Colorspace “${r}” not supported`);if(!t)throw Error(`Colorkeys missing: returned “${t}”`);let d;if(a)d=t.map(t=>e-e*(K(t).jch()[0]/100)).sort((e,t)=>e-t).concat(e),d.unshift(0);else{let n=t.map(e=>K(e).jch()[0]/100),r=Math.min(...n),i=Math.max(...n);d=n.map(t=>t===0||isNaN((t-r)/(i-r))?0:e-(t-r)/(i-r)*e).sort((e,t)=>e-t)}let f=Ai(i,[1,e],[1,e]);if(f=d.map(e=>Math.max(0,f(e))),d=f,s===`polynomial`){let t=e=>Math.sqrt(Math.sqrt((e**2.25+e**4)/2));d=f.map(t=>t/e).map(n=>t(n)*e)}let p=t.map((e,t)=>({colorKeys:Di(e),index:t})).sort((e,t)=>t.colorKeys[0]-e.colorKeys[0]).map(e=>t[e.index]),m=[],h;if(a){let e=u===`lch`?K.lch(...K(`#fff`).lch()):`#ffffff`,t=u===`lch`?K.lch(...K(`#000`).lch()):`#000000`;m=[e,...p,t]}else m=c?p:t;let g;if(o){let t=m;if(m=m.map(e=>K(String(e))[u]()),u===`hcl`&&m.forEach(e=>{e[1]=Number.isNaN(e[1])?0:e[1]}),u===`jch`)for(let e=0;eh(t))}else h=K.scale(m.map(e=>typeof e==`object`&&e.constructor===K.Color?e:String(e))).domain(d).mode(u);return l?h:(!o||o===!1?h.colors(e):g).filter(e=>e!=null)}function Mi(e,t){let n=[],r={};return Object.keys(e).forEach(n=>{r[e[n][t]]=e[n]}),Object.keys(r).forEach(e=>n.push(r[e])),n}function Ni(e){return Number.isNaN(e)?0:e}function Pi(e,t,n=!1){if(!e)throw Error(`Cannot convert color value of “${e}”`);if(!Ti[t])throw Error(`Cannot convert to colorspace “${t}”`);let r=Ti[t],i=K(String(e))[r]();if(t===`HSL`&&i.pop(),t===`HEX`){if(n){let t=K(String(e)).rgb();return{r:t[0],g:t[1],b:t[2]}}return i}let a={},o=i.map(Ni);o=o.map((e,t)=>{let i=Z(e),o=t;r===`hsluv`&&(o+=2);let s=r.charAt(o);return r===`jch`&&s===`c`&&(s=`C`),a[s===`j`?`J`:s]=i,r in{lab:1,lch:1,jab:1,jch:1}?n||(s===`l`||s===`j`)&&(i+=`%`):r!==`hsluv`&&(s===`s`||s===`l`||s===`v`)&&(a[s]=Z(e,2),n||(i=Z(e*100),i+=`%`)),i});let s=`${r}(${o.join(`, `)})`;return n?a:s}function Fi(e,t,n){let r=[e,t,n].map(e=>(e/=255,e<=.03928?e/12.92:((e+.055)/1.055)**2.4));return r[0]*.2126+r[1]*.7152+r[2]*.0722}function Ii(e,t,n,r=`wcag2`){if(n===void 0){let e=K.rgb(...t).hsluv()[2];n=Z(e/100,2)}if(r===`wcag2`){let r=Fi(e[0],e[1],e[2]),i=Fi(t[0],t[1],t[2]),a=(r+.05)/(i+.05),o=(i+.05)/(r+.05);return n<.5?a>=1?a:-o:a<1?o:a===1?a:-a}else if(r===`wcag3`)return n<.5?yi(bi(e),bi(t))*-1:yi(bi(e),bi(t));else throw Error(`Contrast calculation method ${r} unsupported; use 'wcag2' or 'wcag3'`)}function Li(e,t){if(!e)throw Error(`Array undefined`);if(!Array.isArray(e))throw Error(`Passed object is not an array`);let n=t===`wcag2`?0:1;return Math.min(...e.filter(e=>e>=n))}function Ri(e,t){if(!e)throw Error(`Ratios undefined`);e=e.sort((e,t)=>e-t);let n=Li(e,t),r=e.indexOf(n),i=[],a=e.slice(0,r),o=e.slice(r,e.length);for(let e=0;ee-t),i}var zi=(e,t,n,r,i)=>{let a=3e3,o=ji({swatches:a,colorKeys:e._modifiedKeys,colorspace:e._colorspace,shift:1,smooth:e._smooth,asFun:!0}),s={},c=e=>{if(s[e])return s[e];let r=Ii(K(o(e)).rgb(),t,n,i);return s[e]=r,r},l=e=>{let t=c(0).01&&o;)o--,n/=2,iu.push(o(l(+e)))),u},Q=class{constructor({name:e,colorKeys:t,colorspace:n,colorSpace:r=n??`RGB`,ratios:i,smooth:a=!1,output:o=`HEX`,saturation:s=100}){if(n!==void 0&&console.warn("Leonardo: `colorspace` is deprecated. Use `colorSpace` instead."),this._name=e,this._colorKeys=t,this._modifiedKeys=t,this._colorspace=r,this._ratios=i,this._smooth=a,this._output=o,this._saturation=s,!this._name)throw Error(`Color missing name`);if(!this._colorKeys)throw Error(`Color Keys are undefined`);if(!Ti[this._colorspace])throw Error(`Colorspace “${r}” not supported`);if(!Ti[this._output])throw Error(`Output “${this._output}” not supported`);for(let e=0;e{let n=K(`${t}`).oklch(),r=n[1]*(this._saturation/100),i=K.oklch(n[0],r,n[2]),a=K.rgb(i).hex();e.push(a)}),this._modifiedKeys=e,this._generateColorScale()}_generateColorScale(){this._colorScale=ji({swatches:3e3,colorKeys:this._modifiedKeys,colorSpace:this._colorspace,shift:1,smooth:this._smooth,asFun:!0})}},Bi=class extends Q{get backgroundColorScale(){return this._backgroundColorScale||this._generateColorScale(),this._backgroundColorScale}_generateColorScale(){Q.prototype._generateColorScale.call(this);let e=ji({swatches:1e3,colorKeys:this._colorKeys,colorspace:this._colorspace,shift:1,smooth:this._smooth});e.push(...this.colorKeys);let t=Mi(e.map((e,t)=>({value:Math.round(Oi(e)[2]),index:t})),`value`).map(t=>e[t.index]);return t.length>=101&&(t.length=100,t.push(`#ffffff`)),this._backgroundColorScale=t.map(e=>Pi(e,this._output)),this._backgroundColorScale}},Vi=class{constructor({colors:e,backgroundColor:t,lightness:n,contrast:r=1,saturation:i=100,output:a=`HEX`,formula:o=`wcag2`}){if(this._output=a,this._colors=e,this._lightness=n,this._saturation=i,this._formula=o,this._setBackgroundColor(t),this._setBackgroundColorValue(),this._contrast=r,!this._colors)throw Error(`No colors are defined`);if(!this._backgroundColor)throw Error(`Background color is undefined`);if(e.forEach(e=>{if(!e.ratios)throw Error(`Color ${e.name}'s ratios are undefined`)}),!Ti[this._output])throw Error(`Output “${a}” not supported`);this._saturation<100&&this._updateColorSaturation(this._saturation),this._findContrastColors(),this._findContrastColorPairs(),this._findContrastColorValues()}set formula(e){this._formula=e,this._findContrastColors()}get formula(){return this._formula}set contrast(e){this._contrast=e,this._findContrastColors()}get contrast(){return this._contrast}set lightness(e){this._lightness=e,this._setBackgroundColor(this._backgroundColor),this._findContrastColors()}get lightness(){return this._lightness}set saturation(e){this._saturation=e,this._updateColorSaturation(e),this._findContrastColors()}get saturation(){return this._saturation}set backgroundColor(e){this._setBackgroundColor(e),this._findContrastColors()}get backgroundColorValue(){return this._backgroundColorValue}get backgroundColor(){return this._backgroundColor}set colors(e){this._colors=e,this._findContrastColors()}get colors(){return this._colors}set addColor(e){this._colors.push(e),this._findContrastColors()}set removeColor(e){this._colors=this._colors.filter(t=>t.name!==e.name),this._findContrastColors()}set updateColor(e){if(Array.isArray(e))for(let t=0;tn.name===e[t].color);n=n[0];let r=this._colors.indexOf(n),i=this._colors.filter(n=>n.name!==e[t].color);e[t].name&&(n.name=e[t].name),e[t].colorKeys&&(n.colorKeys=e[t].colorKeys),e[t].ratios&&(n.ratios=e[t].ratios),(e[t].colorSpace!==void 0||e[t].colorspace!==void 0)&&(e[t].colorspace!==void 0&&e[t].colorSpace===void 0&&console.warn("Leonardo: `colorspace` is deprecated. Use `colorSpace` instead."),n.colorSpace=e[t].colorSpace??e[t].colorspace),e[t].smooth&&(n.smooth=e[t].smooth),n._generateColorScale(),i.splice(r,0,n),this._colors=i}else{let t=this._colors.filter(t=>t.name===e.color);t=t[0];let n=this._colors.indexOf(t),r=this._colors.filter(t=>t.name!==e.color);e.name&&(t.name=e.name),e.colorKeys&&(t.colorKeys=e.colorKeys),e.ratios&&(t.ratios=e.ratios),(e.colorSpace!==void 0||e.colorspace!==void 0)&&(e.colorspace!==void 0&&e.colorSpace===void 0&&console.warn("Leonardo: `colorspace` is deprecated. Use `colorSpace` instead."),t.colorSpace=e.colorSpace??e.colorspace),e.smooth&&(t.smooth=e.smooth),t._generateColorScale(),r.splice(n,0,t),this._colors=r}this._findContrastColors()}set output(e){this._output=e,this._colors.forEach(e=>{e.output=this._output}),this._backgroundColor.output=this._output,this._findContrastColors()}get output(){return this._output}get contrastColors(){return this._contrastColors}get contrastColorPairs(){return this._contrastColorPairs}get contrastColorValues(){return this._contrastColorValues}_setBackgroundColor(e){if(typeof e==`string`){let t=new Bi({name:`background`,colorKeys:[e],output:`RGB`}),n=Z(K(String(e)).hsluv()[2]);this._backgroundColor=t,this._lightness=n,this._backgroundColorValue=t[this._lightness]}else{e.output=`RGB`;let t=e.backgroundColorScale[this._lightness];this._backgroundColor=e,this._backgroundColorValue=t}}_setBackgroundColorValue(){this._backgroundColorValue=this._backgroundColor.backgroundColorScale[this._lightness]}_updateColorSaturation(e){this._colors.map(t=>{t.saturation=e})}_findContrastColors(){let e=K(String(this._backgroundColorValue)).rgb(),t=this._lightness/100,n={background:Pi(this._backgroundColorValue,this._output)},r=[],i=[],a={...n};return r.push(n),this._colors.map(n=>{if(n.ratios!==void 0){let o,s=[],c={name:n.name,values:s},l;Array.isArray(n.ratios)?l=n.ratios:Array.isArray(n.ratios)||(o=Object.keys(n.ratios),l=Object.values(n.ratios)),l=l.map(e=>Ei(+e,this._contrast));let u=zi(n,e,t,l,this._formula).map(e=>Pi(e,this._output));for(let e=0;e{let t=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(e);return[Number.parseInt(t[1],16),Number.parseInt(t[2],16),Number.parseInt(t[3],16)]},Wi=(e,t,n)=>{let r=e/255,i=t/255,a=n/255,o=Math.min(r,i,a),s=Math.max(r,i,a),c=s-o,l=0,u=0,d=0;return l=c===0?0:s===r?(i-a)/c%6:s===i?(a-r)/c+2:(r-i)/c+4,l=Math.round(l*60),l<0&&(l+=360),d=(s+o)/2,u=c===0?0:c/(1-Math.abs(2*d-1)),u=+(u*100).toFixed(1),d=+(d*100).toFixed(1),[l,u,Math.round(d)]},Gi=(e,t,n,r)=>{let i=n/100,a=t*Math.min(i,1-i)/100,o=t=>{let n=(t+e/30)%12,r=i-a*Math.max(Math.min(n-3,9-n,1),-1);return Math.round(255*r).toString(16).padStart(2,`0`).toUpperCase()},s=o(0),c=o(8),l=o(4),u=((e,t,n)=>Math.min(Math.max(e,t),n))(r,0,1);return`#${s}${c}${l}${Math.round(u*255).toString(16).padStart(2,`0`).toUpperCase()}`},Ki=(e,t,n=1)=>{let r=Ui(e),i=Ui(t===`white`?`#FFFFFF`:t===`black`?`#000000`:t),a=r.map((e,t)=>[(e-i[t])/(255-i[t]),(e-i[t])/(0-i[t])]),o=Hi(Math.max(...a.flat().filter(e=>/^-?\d+\.?\d*$/.test(e)))),s=r.map((e,t)=>Math.round((e-i[t]+i[t]*o)/o));if(s.includes(NaN)){let e=Wi(r[0],r[1],r[2]);return{h:e[0],s:Math.round(e[1]*n),l:e[2],a:1}}let c=Wi(s[0],s[1],s[2]);return{h:c[0],s:Math.round(c[1]*n),l:c[2],a:o}},qi={backgroundColor:`gray`,colorSpace:`OKLCH`,colorSmoothing:!1,formula:`wcag2`,output:`HEX`,colors:{gray:[$(215,20,90),$(215,8,50),$(215,6,25)],red:[$(358,100,58),$(350,100,30)],orange:[$(32,100,48),$(12,100,30)],yellow:[$(50,100,50),$(25,100,20)],lime:[$(100,68,50),$(115,86,25)],green:[$(163,87,42),$(168,100,25)],cyan:[$(185,80,45),$(200,98,35)],blue:[$(212,98,46),$(222,95,25)],purple:[$(258,94,64),$(265,100,35)],fuchsia:[$(295,56,50),$(285,80,25)],pink:[$(334,90,50),$(330,91,25)]},themes:{light:{ratios:[1.03,1.06,1.12,1.25,1.5,1.75,2.25,3.5,5.25,6.5,8,10.5,13.75,16.75],contrast:1,lightness:100,saturation:100},dark:{ratios:[1.03,1.06,1.12,1.25,1.5,1.75,2.25,3.5,5.25,6.5,8,10.5,13.75,16],contrast:1,lightness:6,saturation:97},lightHc:{ratios:[1.06,1.12,1.25,1.37,1.75,2.25,3.25,4.75,8.87,10,11.75,13.25,16,17],contrast:1,lightness:100,saturation:100},darkHc:{ratios:[1.06,1.12,1.25,1.37,1.75,2.25,3.25,4.75,8.87,10,11.75,13.25,16,17],contrast:1,lightness:6,saturation:97}}};function $(e,t,n){return K.hsl(e,t/100,n/100).hex()}function Ji(e,t){let n=e.colorSpace,r=e.colorSmoothing,i=e.themes[t].ratios,a=new Bi({name:`gray`,colorKeys:e.colors.gray,colorspace:n,ratios:i,smooth:r}),o=new Q({name:`blue`,colorKeys:e.colors.blue,colorspace:n,ratios:i,smooth:r}),s=new Q({name:`cyan`,colorKeys:e.colors.cyan,colorspace:n,ratios:i,smooth:r}),c=new Q({name:`fuchsia`,colorKeys:e.colors.fuchsia,colorspace:n,ratios:i,smooth:r}),l=new Q({name:`green`,colorKeys:e.colors.green,colorspace:n,ratios:i,smooth:r}),u=new Q({name:`lime`,colorKeys:e.colors.lime,colorspace:n,ratios:i,smooth:r}),d=new Q({name:`orange`,colorKeys:e.colors.orange,colorspace:n,ratios:i,smooth:r}),f=new Q({name:`pink`,colorKeys:e.colors.pink,colorspace:n,ratios:i,smooth:r}),p=new Q({name:`purple`,colorKeys:e.colors.purple,colorspace:n,ratios:i,smooth:r}),m={gray:a,red:new Q({name:`red`,colorKeys:e.colors.red,colorspace:n,ratios:i,smooth:r}),orange:d,yellow:new Q({name:`yellow`,colorKeys:e.colors.yellow,colorspace:n,ratios:i,smooth:r}),lime:u,green:l,cyan:s,blue:o,purple:p,fuchsia:c,pink:f};return e.colors.custom&&(m.custom=new Q({name:`custom`,colorKeys:e.colors.custom,colorspace:n,ratios:i,smooth:r})),new Vi({colors:Object.values(m),backgroundColor:m[e.backgroundColor],contrast:e.themes[t].contrast,lightness:e.themes[t].lightness,saturation:e.themes[t].saturation,output:e.output,formula:e.formula}).contrastColors}function Yi(e){let t={};for(let n of Object.keys(e.themes))t[n]=Ji(e,n);return t}function Xi(e){qi.colors.custom=[e];let t=Yi(qi);return Object.fromEntries(Object.entries(t).map(([e,t])=>{let n=t.find(e=>e&&e.name===`custom`),r=Object.fromEntries(n.values.map(({name:e,value:t})=>[e,t]));for(let[e,n]of Object.entries(r)){let i=Ki(n,t[0].background);r[`alpha${e.charAt(0).toUpperCase()+e.slice(1)}`]=Gi(i.h,i.s,i.l,i.a)}return[e,r]}))}return e.generateCustomColors=Xi,e.generateThemesJson=Yi,e.hslToHex=$,e.leonardoConfig=qi,e})({}); \ No newline at end of file diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/CompoundIcons.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/CompoundIcons.kt index c5ac159b5a..5851a758f0 100644 --- a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/CompoundIcons.kt +++ b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/CompoundIcons.kt @@ -229,9 +229,6 @@ object CompoundIcons { @Composable fun Filter(): ImageVector { return ImageVector.vectorResource(R.drawable.ic_compound_filter) } - @Composable fun Folder(): ImageVector { - return ImageVector.vectorResource(R.drawable.ic_compound_folder) - } @Composable fun Forward(): ImageVector { return ImageVector.vectorResource(R.drawable.ic_compound_forward) } @@ -771,7 +768,6 @@ object CompoundIcons { FileError(), Files(), Filter(), - Folder(), Forward(), FullScreen(), Grid(), @@ -1000,7 +996,6 @@ object CompoundIcons { R.drawable.ic_compound_file_error, R.drawable.ic_compound_files, R.drawable.ic_compound_filter, - R.drawable.ic_compound_folder, R.drawable.ic_compound_forward, R.drawable.ic_compound_full_screen, R.drawable.ic_compound_grid, diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColors.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColors.kt index 1e3c97d179..f5a54bb7be 100644 --- a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColors.kt +++ b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColors.kt @@ -91,6 +91,8 @@ data class SemanticColors( val bgSubtleSecondaryLevel0: Color, /** Subtle background colour for success state elements. State: Rest. */ val bgSuccessSubtle: Color, + /** Accent borders for containers */ + val borderAccentPrimary: Color, /** accent border intended for keylines on message highlights */ val borderAccentSubtle: Color, /** High-contrast border for critical state. State: Hover. */ @@ -171,6 +173,8 @@ data class SemanticColors( val iconTertiary: Color, /** Translucent version of tertiary icon. Refer to it for intended use. */ val iconTertiaryAlpha: Color, + /** Used to separate core sections of the UI as well as containers */ + val separatorPrimary: Color, /** Accent text colour for plain actions. */ val textActionAccent: Color, /** Default text colour for plain actions. */ diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsDark.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsDark.kt index adeed8d5a3..35a1234854 100644 --- a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsDark.kt +++ b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsDark.kt @@ -61,6 +61,7 @@ val compoundColorsDark = SemanticColors( bgSubtleSecondary = DarkColorTokens.colorGray300, bgSubtleSecondaryLevel0 = DarkColorTokens.colorThemeBg, bgSuccessSubtle = DarkColorTokens.colorGreen200, + borderAccentPrimary = DarkColorTokens.colorGreen900, borderAccentSubtle = DarkColorTokens.colorGreen700, borderCriticalHovered = DarkColorTokens.colorRed1000, borderCriticalPrimary = DarkColorTokens.colorRed900, @@ -101,6 +102,7 @@ val compoundColorsDark = SemanticColors( iconSuccessPrimary = DarkColorTokens.colorGreen900, iconTertiary = DarkColorTokens.colorGray800, iconTertiaryAlpha = DarkColorTokens.colorAlphaGray800, + separatorPrimary = DarkColorTokens.colorGray400, textActionAccent = DarkColorTokens.colorGreen900, textActionPrimary = DarkColorTokens.colorGray1400, textBadgeAccent = DarkColorTokens.colorGreen1100, diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsDarkHc.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsDarkHc.kt index 9bab04f833..802e3d56b0 100644 --- a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsDarkHc.kt +++ b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsDarkHc.kt @@ -61,6 +61,7 @@ val compoundColorsHcDark = SemanticColors( bgSubtleSecondary = DarkHcColorTokens.colorGray300, bgSubtleSecondaryLevel0 = DarkHcColorTokens.colorThemeBg, bgSuccessSubtle = DarkHcColorTokens.colorGreen200, + borderAccentPrimary = DarkHcColorTokens.colorGreen900, borderAccentSubtle = DarkHcColorTokens.colorGreen700, borderCriticalHovered = DarkHcColorTokens.colorRed1000, borderCriticalPrimary = DarkHcColorTokens.colorRed900, @@ -101,6 +102,7 @@ val compoundColorsHcDark = SemanticColors( iconSuccessPrimary = DarkHcColorTokens.colorGreen900, iconTertiary = DarkHcColorTokens.colorGray800, iconTertiaryAlpha = DarkHcColorTokens.colorAlphaGray800, + separatorPrimary = DarkHcColorTokens.colorGray400, textActionAccent = DarkHcColorTokens.colorGreen900, textActionPrimary = DarkHcColorTokens.colorGray1400, textBadgeAccent = DarkHcColorTokens.colorGreen1100, diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsLight.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsLight.kt index 033efc63ef..166f9ddafc 100644 --- a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsLight.kt +++ b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsLight.kt @@ -61,6 +61,7 @@ val compoundColorsLight = SemanticColors( bgSubtleSecondary = LightColorTokens.colorGray300, bgSubtleSecondaryLevel0 = LightColorTokens.colorGray300, bgSuccessSubtle = LightColorTokens.colorGreen200, + borderAccentPrimary = LightColorTokens.colorGreen900, borderAccentSubtle = LightColorTokens.colorGreen700, borderCriticalHovered = LightColorTokens.colorRed1000, borderCriticalPrimary = LightColorTokens.colorRed900, @@ -101,6 +102,7 @@ val compoundColorsLight = SemanticColors( iconSuccessPrimary = LightColorTokens.colorGreen900, iconTertiary = LightColorTokens.colorGray800, iconTertiaryAlpha = LightColorTokens.colorAlphaGray800, + separatorPrimary = LightColorTokens.colorGray400, textActionAccent = LightColorTokens.colorGreen900, textActionPrimary = LightColorTokens.colorGray1400, textBadgeAccent = LightColorTokens.colorGreen1100, diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsLightHc.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsLightHc.kt index c1f677d156..8914b41bfc 100644 --- a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsLightHc.kt +++ b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsLightHc.kt @@ -61,6 +61,7 @@ val compoundColorsHcLight = SemanticColors( bgSubtleSecondary = LightHcColorTokens.colorGray300, bgSubtleSecondaryLevel0 = LightHcColorTokens.colorGray300, bgSuccessSubtle = LightHcColorTokens.colorGreen200, + borderAccentPrimary = LightHcColorTokens.colorGreen900, borderAccentSubtle = LightHcColorTokens.colorGreen700, borderCriticalHovered = LightHcColorTokens.colorRed1000, borderCriticalPrimary = LightHcColorTokens.colorRed900, @@ -101,6 +102,7 @@ val compoundColorsHcLight = SemanticColors( iconSuccessPrimary = LightHcColorTokens.colorGreen900, iconTertiary = LightHcColorTokens.colorGray800, iconTertiaryAlpha = LightHcColorTokens.colorAlphaGray800, + separatorPrimary = LightHcColorTokens.colorGray400, textActionAccent = LightHcColorTokens.colorGreen900, textActionPrimary = LightHcColorTokens.colorGray1400, textBadgeAccent = LightHcColorTokens.colorGreen1100, diff --git a/libraries/dateformatter/impl/src/main/res/values-ja/translations.xml b/libraries/dateformatter/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..d044992377 --- /dev/null +++ b/libraries/dateformatter/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,5 @@ + + + "%1$s %2$s" + "今月" + diff --git a/libraries/dateformatter/impl/src/main/res/values-vi/translations.xml b/libraries/dateformatter/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..b846434e9e --- /dev/null +++ b/libraries/dateformatter/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,5 @@ + + + "%1$s lúc %2$s" + "Tháng này" + diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt index 53d5a7c281..b1e3356fc3 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt @@ -13,10 +13,12 @@ import androidx.compose.ui.unit.dp enum class AvatarSize(val dp: Dp) { CurrentUserTopBar(32.dp), + CurrentRoomTopBar(32.dp), IncomingCall(140.dp), RoomDetailsHeader(96.dp), RoomListItem(52.dp), + ThreadsListItem(52.dp), SpaceListItem(52.dp), diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/BitmapAvatar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/BitmapAvatar.kt new file mode 100644 index 0000000000..f25174b7b0 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/BitmapAvatar.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2026 Element Creations 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.libraries.designsystem.components.avatar + +import android.graphics.Bitmap +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import coil3.compose.AsyncImagePainter +import coil3.compose.SubcomposeAsyncImage +import coil3.compose.SubcomposeAsyncImageContent +import io.element.android.libraries.designsystem.components.avatar.internal.InitialLetterAvatar +import timber.log.Timber + +// For user avatar only. +@Composable +fun BitmapAvatar( + avatarData: AvatarData, + bitmap: Bitmap?, + modifier: Modifier = Modifier, + contentDescription: String? = null, +) { + val avatarShape = AvatarType.User.avatarShape() + when { + bitmap == null -> InitialLetterAvatar( + avatarData = avatarData, + avatarShape = avatarShape, + forcedAvatarSize = null, + modifier = modifier, + contentDescription = contentDescription, + ) + else -> { + val size = avatarData.size.dp + SubcomposeAsyncImage( + model = bitmap, + contentDescription = contentDescription, + contentScale = ContentScale.Crop, + modifier = modifier + .size(size) + .clip(avatarShape) + ) { + val collectedState by painter.state.collectAsState() + when (val state = collectedState) { + is AsyncImagePainter.State.Success -> SubcomposeAsyncImageContent() + is AsyncImagePainter.State.Error -> { + SideEffect { + Timber.e( + state.result.throwable, + "Error loading avatar $state\n${state.result}" + ) + } + InitialLetterAvatar( + avatarData = avatarData, + avatarShape = avatarShape, + forcedAvatarSize = null, + contentDescription = contentDescription, + ) + } + else -> InitialLetterAvatar( + avatarData = avatarData, + avatarShape = avatarShape, + forcedAvatarSize = null, + contentDescription = contentDescription, + ) + } + } + } + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/WindowSizeClassUtils.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/WindowSizeClassUtils.kt new file mode 100644 index 0000000000..ba0752e8a2 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/WindowSizeClassUtils.kt @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2026 Element Creations 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.libraries.designsystem.utils + +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo +import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass +import androidx.compose.runtime.Composable + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +fun hasCompactHeightWindowSize(): Boolean { + return currentWindowAdaptiveInfo().windowSizeClass.heightSizeClass == WindowHeightSizeClass.Compact +} diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/StateContentFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/StateContentFormatter.kt index f9d38fd8a7..5abfa89ef0 100644 --- a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/StateContentFormatter.kt +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/StateContentFormatter.kt @@ -118,15 +118,6 @@ class StateContentFormatter( "PolicyRuleUser" } } - OtherState.RoomAliases -> when (renderingMode) { - RenderingMode.RoomList -> { - Timber.v("Filtering timeline item for room state change: $content") - null - } - RenderingMode.Timeline -> { - "RoomAliases" - } - } OtherState.RoomCanonicalAlias -> when (renderingMode) { RenderingMode.RoomList -> { Timber.v("Filtering timeline item for room state change: $content") diff --git a/libraries/eventformatter/impl/src/main/res/values-ja/translations.xml b/libraries/eventformatter/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..8613aae2df --- /dev/null +++ b/libraries/eventformatter/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,73 @@ + + + "(アバターも変更)" + "%1$s がアバターを変更" + "あなたがアバターを変更" + "%1$s がメンバーに降格" + "%1$s がモデレーターに降格" + "%1$sが表示名を変更: %2$s > %3$s" + "あなたが表示名を変更: %1$s > %2$s" + "%1$sが表示名を削除 (%2$s)" + "表示名を削除 (%1$s)" + "%1$sが表示名を設定: %2$s" + "あなたが表示名を設定: %1$s" + "%1$s が管理者に昇格" + "%1$s がモデレーターに昇格" + "%1$sがルームアバターを変更" + "あなたがルームアバターを変更" + "%1$sがルームアバターを削除" + "あなたがルームアバターを削除" + "%1$s が %2$s を追放" + "あなたが %1$s を追放" + "あなたが %1$s を追放: %2$s" + "%1$s が %2$s を追放: %3$s" + "%1$s がルームを作成" + "あなたがルームを作成" + "%1$s が %2$s を招待" + "%1$s が招待を受諾" + "あなたが招待を受諾" + "あなたが %1$s を招待" + "%1$s があなたを招待" + "%1$s がルームに参加" + "あなたがルームに参加" + "%1$s が参加をリクエスト" + "%1$s が %2$s の参加を許可" + "あなたが %1$s の参加を許可" + "あなたが参加をリクエスト" + "%1$s が %2$s の参加リクエストを拒否" + "あなたが %1$s の参加リクエストを拒否" + "%1$s があなたの参加リクエストを拒否" + "%1$s が参加リクエストを取り消し" + "あなたが参加リクエストを取り消し" + "%1$s がルームを退出" + "あなたがルームを退出" + "%1$s がルーム名を変更: %2$s" + "あなたがルーム名を変更: %1$s" + "%1$s がルーム名を削除" + "あなたがルーム名を削除" + "%1$s による変更はありません" + "あなたによる変更はありません" + "%1$s はピン留めメッセージを変更しました" + "あなたがピン留めメッセージを変更しました" + "%1$s がメッセージをピン留め" + "あなたがメッセージをピン留め" + "%1$s がメッセージのピン留めを解除" + "あなたがメッセージのピン留めを解除" + "%1$s が招待を拒否" + "あなたが招待を拒否" + "%1$s が %2$s を削除" + "あなたが %1$s を削除" + "あなたが%1$s を削除: %2$s" + "%1$s が %2$s を削除: %3$s" + "%1$s が %2$s をルームに招待" + "あなたが %1$s をルームに招待" + "%1$s が %2$s へのルームの招待を取り消し" + "あなたが %1$s へのルームの招待を取り消し" + "%1$s がトピックを変更: %2$s" + "あなたがトピックを変更: %1$s" + "%1$s がルームのトピックを削除" + "あなたがルームのトピックを削除" + "%1$s が %2$s の追放を解除" + "あなたが %1$s の追放を解除" + "%1$s がメンバーシップに未知の変更を追加" + diff --git a/libraries/eventformatter/impl/src/main/res/values-lt/translations.xml b/libraries/eventformatter/impl/src/main/res/values-lt/translations.xml index 9fb1486d59..9a632b8f5a 100644 --- a/libraries/eventformatter/impl/src/main/res/values-lt/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-lt/translations.xml @@ -1,13 +1,13 @@ "(taip pat buvo pakeistas ir avataras)" - "%1$s pakeitė savo avatarą" - "Jūs pakeitėte savo avatarą" + "%1$s pakeitė savo pseudoportretą" + "Jūs pakeitėte savo pseudoportretą" "%1$s pakeitė savo slapyvardį iš %2$s į %3$s" "Jūs pakeitėte savo slapyvardį iš %1$s į %2$s" "%1$s pašalino savo slapyvardį (jis buvo %2$s)" "Jūs pašalinote savo slapyvardį (jis buvo %1$s)" - "%1$s pakeitė savo slapyvardį į %2$s" + "%1$s nustatė savo rodomą vardą į %2$s" "Jūs nustatėte savo slapyvardį į %1$s" "%1$s pakeitė kambario avatarą" "Jūs pakeitėte kambario avatarą" @@ -21,7 +21,7 @@ "%1$s priėmė kvietimą" "Priėmėte kvietimą" "Jūs pakvietėte %1$s" - "%1$s pakvietė Jus" + "%1$s pakvietė jus" "%1$s prisijungė prie kambario" "Jūs prisijungėte prie kambario" "%1$s prašo prisijungti" diff --git a/libraries/eventformatter/impl/src/main/res/values-vi/translations.xml b/libraries/eventformatter/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..7598ba0cb9 --- /dev/null +++ b/libraries/eventformatter/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,64 @@ + + + "(ảnh hồ sơ cũng được thay)" + "%1$s đổi ảnh hồ sơ" + "Bạn đổi ảnh hồ sơ" + "%1$s bị giáng cấp xuống thành thành viên" + "%1$s bị giáng chức xuống làm người điều hành" + "%1$s đổi tên hiển thị từ %2$s sang %3$s" + "Bạn đổi tên hiển thị từ %1$s sang %2$s" + "%1$s xoá tên hiển thị (trước kia là %2$s)" + "Bạn xoá tên hiển thị (trước kia là %1$s)" + "%1$s đặt tên hiển thị thành %2$s" + "Bạn đặt tên hiển thị thành %1$s" + "%1$s đã được thăng chức lên quản trị viên" + "%1$s đã được thăng chức lên làm người điều hành" + "%1$s đổi ảnh phòng" + "Bạn đổi ảnh phòng" + "%1$s đã xóa ảnh đại diện của phòng." + "Bạn đã xóa hình đại diện của phòng trò chuyện" + "%1$s cấm %2$s vào phòng" + "Bạn cấm %1$s vào phòng" + "%1$s tạo phòng này" + "Bạn tạo phòng này" + "%1$s mời %2$s" + "%1$s đã chấp nhận lời mời" + "Bạn đã chấp nhận lời mời" + "Bạn mời %1$s" + "%1$s mời bạn" + "%1$s vào phòng" + "Bạn vào phòng" + "%1$s đang yêu cầu tham gia" + "%1$s được cấp quyền truy cập vào %2$s" + "Bạn đã cho phép %1$s tham gia" + "Bạn đã yêu cầu tham gia" + "%1$s đã từ chối yêu cầu tham gia của %2$s" + "Bạn đã từ chối yêu cầu tham gia của %1$s" + "%1$s đã từ chối yêu cầu tham gia của bạn" + "%1$s không còn mong muốn tham gia" + "Bạn đã hủy yêu cầu tham gia" + "%1$s rời phòng" + "Bạn rời phòng" + "%1$s đổi tên phòng thành %2$s" + "Bạn đổi tên phòng thành %1$s" + "%1$s xóa tên phòng" + "Bạn xóa tên phòng" + "%1$s không có thay đổi nào" + "Bạn chưa thực hiện thay đổi nào" + "Bạn đã thay đổi tin nhắn được ghim" + "%1$s từ chối lời mời" + "Bạn từ chối lời mời" + "%1$s cho %2$s cút khỏi phòng" + "Bạn cho %1$s cút khỏi phòng" + "%1$s đã gửi lời mời đến %2$s để tham gia phòng trò chuyện" + "Bạn đã gửi lời mời đến %1$s để tham gia phòng trò chuyện" + "%1$s đã thu hồi lời mời tham gia phòng trò chuyện của %2$s " + "Bạn đã thu hồi lời mời tham gia phòng trò chuyện của %1$s " + "%1$s đổi chủ đề sang: %2$s" + "Bạn đổi chủ đề sang: %1$s" + "%1$s đã xóa chủ đề phòng" + "Bạn đã xóa chủ đề của phòng." + "%1$s hủy lệnh cấm với %2$s" + "Bạn hủy lệnh cấm với %1$s" + "%1$s đã thực hiện một thay đổi không xác định đối với tư cách thành viên của họ" + diff --git a/libraries/eventformatter/impl/src/main/res/values-zh/translations.xml b/libraries/eventformatter/impl/src/main/res/values-zh/translations.xml index 9776510d6f..cd22ebed38 100644 --- a/libraries/eventformatter/impl/src/main/res/values-zh/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-zh/translations.xml @@ -54,11 +54,11 @@ "%1$s 取消置顶了一条消息" "您取消置顶了一条消息" "%1$s 拒绝了邀请" - "你拒绝了邀请" - "%1$s 移除了 %2$s" - "你移除了 %1$s" - "您已删除%1$s :%2$s" - "%1$s已移除%2$s:%3$s" + "您拒绝了邀请" + "%1$s 已移除 %2$s" + "您移除了 %1$s" + "您移除了 %1$s:%2$s" + "%1$s 移除了 %2$s:%3$s" "%1$s 向 %2$s 发送了加入聊天室的邀请" "你邀请 %1$s 加入聊天室" "%1$s 撤销了 %2$s 加入聊天室的邀请" diff --git a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatterTest.kt b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatterTest.kt index e91bed409e..38334afad5 100644 --- a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatterTest.kt +++ b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatterTest.kt @@ -601,7 +601,6 @@ class DefaultPinnedMessagesBannerFormatterTest { OtherState.PolicyRuleRoom, OtherState.PolicyRuleServer, OtherState.PolicyRuleUser, - OtherState.RoomAliases, OtherState.RoomCanonicalAlias, OtherState.RoomGuestAccess, OtherState.RoomHistoryVisibility, diff --git a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLatestEventFormatterTest.kt b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLatestEventFormatterTest.kt index 2345af8a33..e1e8717c4c 100644 --- a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLatestEventFormatterTest.kt +++ b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLatestEventFormatterTest.kt @@ -746,7 +746,6 @@ class DefaultRoomLatestEventFormatterTest { OtherState.PolicyRuleRoom, OtherState.PolicyRuleServer, OtherState.PolicyRuleUser, - OtherState.RoomAliases, OtherState.RoomCanonicalAlias, OtherState.RoomGuestAccess, OtherState.RoomHistoryVisibility, diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt index a8e59e5c6d..eaa32e8adc 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt @@ -70,27 +70,6 @@ enum class FeatureFlags( defaultValue = { false }, isFinished = false, ), - CreateSpaces( - key = "feature.createSpaces", - title = "Create spaces", - description = "Allow creating spaces.", - defaultValue = { true }, - isFinished = false, - ), - SpaceSettings( - key = "feature.spaceSettings", - title = "Space settings", - description = "Allow managing space settings such as details, permissions and privacy.", - defaultValue = { true }, - isFinished = false, - ), - RoomListSpaceFilters( - key = "feature.roomListSpaceFilters", - title = "Room list space filters", - description = "Allow filtering the room list by space.", - defaultValue = { true }, - isFinished = false, - ), PrintLogsToLogcat( key = "feature.print_logs_to_logcat", title = "Print logs to logcat", @@ -176,4 +155,11 @@ enum class FeatureFlags( defaultValue = { false }, isFinished = false, ), + RoomThreadList( + key = "feature.room_thread_list", + title = "Add a list of threads in a room", + description = "Add a new screen with a list of threads in a room.", + defaultValue = { false }, + isFinished = false, + ), } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/HomeserverCapabilitiesProvider.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/HomeserverCapabilitiesProvider.kt new file mode 100644 index 0000000000..e914656f5f --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/HomeserverCapabilitiesProvider.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026 Element Creations 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.libraries.matrix.api + +/** + * Provides information about the capabilities of the homeserver. + * + * Spec: https://spec.matrix.org/latest/client-server-api/#capabilities-negotiation + */ +interface HomeserverCapabilitiesProvider { + /** + * Manually refresh the capabilities of the homeserver performing a network request. + */ + suspend fun refresh(): Result + + /** + * Indicates whether the homeserver allows the user to change their display name. + */ + suspend fun canChangeDisplayName(): Result + + /** + * Indicates whether the homeserver allows the user to change their avatar URL. + */ + suspend fun canChangeAvatarUrl(): Result +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index 773dbaaa07..35fd7e8551 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -223,6 +223,8 @@ interface MatrixClient { * Resets the cached client `well-known` config by the SDK. */ suspend fun resetWellKnownConfig(): Result + + fun homeserverCapabilities(): HomeserverCapabilitiesProvider } /** diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/ConfirmingLoginWithElementClassic.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/ElementClassicSession.kt similarity index 56% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/ConfirmingLoginWithElementClassic.kt rename to libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/ElementClassicSession.kt index 5fae0afdd5..d094019db2 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/ConfirmingLoginWithElementClassic.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/ElementClassicSession.kt @@ -5,11 +5,14 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.login.impl.screens.onboarding.classic +package io.element.android.libraries.matrix.api.auth -import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.core.UserId -class ConfirmingLoginWithElementClassic( +data class ElementClassicSession( val userId: UserId, -) : AsyncAction.Confirming + val homeserverUrl: String?, + val secrets: String?, + val roomKeysVersion: String?, + val doesContainBackupKey: Boolean, +) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt index 1c574ad467..7c82668242 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt @@ -14,6 +14,7 @@ import io.element.android.libraries.matrix.api.auth.external.ExternalSession import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.UserId interface MatrixAuthenticationService { /** @@ -52,6 +53,20 @@ interface MatrixAuthenticationService { */ suspend fun cancelOidcLogin(): Result + /** + * Set the existing data about Element Classic session, if any. + */ + fun setElementClassicSession(session: ElementClassicSession?) + + /** + * Check if the provided secrets from Element Classic session contain a key backup. + */ + fun doSecretsContainBackupKey( + userId: UserId, + secrets: String, + backupInfo: String, + ): Boolean + /** * Attempt to login using the [callbackUrl] provided by the Oidc page. */ diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt index 5dce175237..c58a458865 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt @@ -95,7 +95,6 @@ sealed interface NotificationContent { data object PolicyRuleRoom : StateEvent data object PolicyRuleServer : StateEvent data object PolicyRuleUser : StateEvent - data object RoomAliases : StateEvent data object RoomAvatar : StateEvent data object RoomCanonicalAlias : StateEvent data object RoomCreate : StateEvent diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/JoinedRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/JoinedRoom.kt index 808f37c7c9..32a6f2e409 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/JoinedRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/JoinedRoom.kt @@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.room.knock.KnockRequest import io.element.android.libraries.matrix.api.room.location.LiveLocationShare import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange +import io.element.android.libraries.matrix.api.room.threads.ThreadsListService import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver @@ -44,6 +45,8 @@ interface JoinedRoom : BaseRoom { */ val liveTimeline: Timeline + val threadsListService: ThreadsListService + /** * Create a new timeline. * @param createTimelineParams contains parameters about how to filter the timeline. Will also configure the date separators. diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/StateEventType.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/StateEventType.kt index 41d64afff1..705dd25122 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/StateEventType.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/StateEventType.kt @@ -13,7 +13,6 @@ sealed interface StateEventType { data object PolicyRuleServer : StateEventType data object PolicyRuleUser : StateEventType data object CallMember : StateEventType - data object RoomAliases : StateEventType data object RoomAvatar : StateEventType data object RoomCanonicalAlias : StateEventType data object RoomCreate : StateEventType diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/threads/ThreadListItem.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/threads/ThreadListItem.kt new file mode 100644 index 0000000000..8282caafd1 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/threads/ThreadListItem.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2026 Element Creations 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.libraries.matrix.api.room.threads + +import androidx.compose.runtime.Immutable +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.core.toThreadId +import io.element.android.libraries.matrix.api.timeline.item.event.EventContent +import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails + +@Immutable +data class ThreadListItem( + val rootEvent: ThreadListItemEvent, + val latestEvent: ThreadListItemEvent?, + val numberOfReplies: Long, +) { + val threadId = rootEvent.eventId.toThreadId() +} + +@Immutable +data class ThreadListItemEvent( + val eventId: EventId, + val senderId: UserId, + val senderProfile: ProfileDetails, + val isOwn: Boolean, + val content: EventContent?, + val timestamp: Long, +) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/threads/ThreadListPaginationStatus.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/threads/ThreadListPaginationStatus.kt new file mode 100644 index 0000000000..0716ca7c11 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/threads/ThreadListPaginationStatus.kt @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2026 Element Creations 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.libraries.matrix.api.room.threads + +sealed interface ThreadListPaginationStatus { + data class Idle( + val hasMoreToLoad: Boolean, + ) : ThreadListPaginationStatus + + data object Loading : ThreadListPaginationStatus +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/threads/ThreadsListService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/threads/ThreadsListService.kt new file mode 100644 index 0000000000..7f819c540c --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/threads/ThreadsListService.kt @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2026 Element Creations 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.libraries.matrix.api.room.threads + +import kotlinx.coroutines.flow.Flow + +interface ThreadsListService { + fun subscribeToItemUpdates(): Flow> + fun subscribeToPaginationUpdates(): Flow + suspend fun paginate(): Result + suspend fun reset(): Result + fun destroy() +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/OtherState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/OtherState.kt index ed3f53169f..8b4a7eaa13 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/OtherState.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/OtherState.kt @@ -16,7 +16,6 @@ sealed interface OtherState { data object PolicyRuleRoom : OtherState data object PolicyRuleServer : OtherState data object PolicyRuleUser : OtherState - data object RoomAliases : OtherState data class RoomAvatar(val url: String?) : OtherState data object RoomCanonicalAlias : OtherState data object RoomCreate : OtherState diff --git a/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/core/UserIdTest.kt b/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/core/UserIdTest.kt new file mode 100644 index 0000000000..eb97ce0d8c --- /dev/null +++ b/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/core/UserIdTest.kt @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2026 Element Creations 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.libraries.matrix.api.core + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class UserIdTest { + @Test + fun `valid user id`() { + val userId = UserId("@alice:example.org") + assertThat(userId.extractedDisplayName).isEqualTo("alice") + assertThat(userId.domainName).isEqualTo("example.org") + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustHomeserverCapabilitiesProvider.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustHomeserverCapabilitiesProvider.kt new file mode 100644 index 0000000000..d82e389aa7 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustHomeserverCapabilitiesProvider.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2026 Element Creations 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.libraries.matrix.impl + +import io.element.android.libraries.core.extensions.runCatchingExceptions +import io.element.android.libraries.matrix.api.HomeserverCapabilitiesProvider +import org.matrix.rustcomponents.sdk.HomeserverCapabilities + +class RustHomeserverCapabilitiesProvider( + private val homeserverCapabilities: HomeserverCapabilities, +) : HomeserverCapabilitiesProvider { + override suspend fun refresh(): Result = runCatchingExceptions { + homeserverCapabilities.refresh() + } + + override suspend fun canChangeDisplayName(): Result = runCatchingExceptions { + homeserverCapabilities.canChangeDisplayname() + } + + override suspend fun canChangeAvatarUrl(): Result = runCatchingExceptions { + homeserverCapabilities.canChangeAvatar() + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 1c87e73ba2..bb6806b5d4 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -17,6 +17,7 @@ import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.core.extensions.mapFailure import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.matrix.api.HomeserverCapabilitiesProvider import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.analytics.SdkStoreSizes import io.element.android.libraries.matrix.api.core.DeviceId @@ -835,6 +836,10 @@ class RustMatrixClient( val request = PerformDatabaseVacuumRequestBuilder(sessionId) sessionCoroutineScope.launch { workManagerScheduler.submit(request) } } + + override fun homeserverCapabilities(): HomeserverCapabilitiesProvider { + return RustHomeserverCapabilitiesProvider(innerClient.homeserverCapabilities()) + } } private fun defaultRoomCreationPowerLevels(isPublic: Boolean, isSpace: Boolean) = PowerLevels( diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt index 7cd9fbedf5..9fe7c7cd1f 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt @@ -16,6 +16,7 @@ import io.element.android.libraries.core.extensions.mapFailure import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.auth.AuthenticationException +import io.element.android.libraries.matrix.api.auth.ElementClassicSession import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails import io.element.android.libraries.matrix.api.auth.OidcDetails @@ -25,6 +26,7 @@ import io.element.android.libraries.matrix.api.auth.external.ExternalSession import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.matrix.impl.ClientBuilderSlidingSync import io.element.android.libraries.matrix.impl.RustMatrixClientFactory @@ -50,6 +52,7 @@ import org.matrix.rustcomponents.sdk.QrCodeData import org.matrix.rustcomponents.sdk.QrCodeDecodeException import org.matrix.rustcomponents.sdk.QrLoginProgress import org.matrix.rustcomponents.sdk.QrLoginProgressListener +import org.matrix.rustcomponents.sdk.SecretsBundleWithUserId import timber.log.Timber import uniffi.matrix_sdk.OAuthAuthorizationData import kotlin.time.Duration.Companion.seconds @@ -64,6 +67,9 @@ class RustMatrixAuthenticationService( private val passphraseGenerator: PassphraseGenerator, private val oidcConfigurationProvider: OidcConfigurationProvider, ) : MatrixAuthenticationService { + // Any existing Element Classic session that we want to try to import secrets from during login. + private var elementClassicSession: ElementClassicSession? = null + // Passphrase which will be used for new sessions. Existing sessions will use the passphrase // stored in the SessionData. private val pendingPassphrase = getDatabasePassphrase() @@ -138,9 +144,15 @@ class RustMatrixAuthenticationService( runCatchingExceptions { val client = currentClient ?: error("You need to call `setHomeserver()` first") val currentSessionPaths = sessionPaths ?: error("You need to call `setHomeserver()` first") - client.login(username, password, "Element X Android", null) + client.login( + username = username, + password = password, + initialDeviceName = "Element X Android", + deviceId = null, + ) // Ensure that the user is not already logged in with the same account ensureNotAlreadyLoggedIn(client) + tryToImportSecretForElementClassicSession(client) val sessionData = client.session() .toSessionData( isTokenValid = true, @@ -162,6 +174,53 @@ class RustMatrixAuthenticationService( } } + private suspend fun tryToImportSecretForElementClassicSession(client: Client) { + elementClassicSession + ?.takeIf { + // Note: the SDK will also do this check + it.userId.value == client.userId() + } + ?.let { + val secrets = it.secrets + val roomKeysVersion = it.roomKeysVersion + if (secrets == null || roomKeysVersion == null) { + Timber.d("No secrets or roomKeysVersion found for Element Classic session ${it.userId}, skipping import") + } else { + Timber.d("Trying to import secrets for Element Classic session ${it.userId}") + runCatchingExceptions { + SecretsBundleWithUserId.fromStr( + userId = it.userId.value, + bundle = secrets, + backupInfo = roomKeysVersion, + ).use { secretsBundle -> + client.encryption().importSecretsBundle(secretsBundle) + } + }.onFailure { failure -> + Timber.e(failure, "Failed to import secrets for Element Classic session ${it.userId}") + } + } + } + } + + override fun doSecretsContainBackupKey( + userId: UserId, + secrets: String, + backupInfo: String, + ): Boolean { + return try { + SecretsBundleWithUserId.fromStr( + userId = userId.value, + bundle = secrets, + backupInfo = backupInfo, + ).use { secretsBundle -> + secretsBundle.containsBackupKey() + } + } catch (failure: Exception) { + Timber.e(failure, "Failed to parse secrets for Element Classic session $userId") + false + } + } + override suspend fun importCreatedSession(externalSession: ExternalSession): Result = withContext(coroutineDispatchers.io) { runCatchingExceptions { @@ -233,6 +292,10 @@ class RustMatrixAuthenticationService( } } + override fun setElementClassicSession(session: ElementClassicSession?) { + elementClassicSession = session + } + /** * callbackUrl should be the uriRedirect from OidcClientMetadata (with all the parameters). */ @@ -241,14 +304,15 @@ class RustMatrixAuthenticationService( runCatchingExceptions { val client = currentClient ?: error("You need to call `setHomeserver()` first") val currentSessionPaths = sessionPaths ?: error("You need to call `setHomeserver()` first") - client.loginWithOidcCallback(callbackUrl) - + client.loginWithOidcCallback( + callbackUrl = callbackUrl, + ) // Free the pending data since we won't use it to abort the flow anymore pendingOAuthAuthorizationData?.close() pendingOAuthAuthorizationData = null - // Ensure that the user is not already logged in with the same account ensureNotAlreadyLoggedIn(client) + tryToImportSecretForElementClassicSession(client) val sessionData = client.session().toSessionData( isTokenValid = true, loginType = LoginType.OIDC, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt index 6ca7d27a8f..bef24003b9 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt @@ -13,6 +13,7 @@ import dev.zacsweers.metro.ContributesTo import dev.zacsweers.metro.Provides import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.annotations.SessionCoroutineScope +import io.element.android.libraries.matrix.api.HomeserverCapabilitiesProvider import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.encryption.EncryptionService @@ -90,4 +91,9 @@ object SessionMatrixModule { fun providesSpaceService(matrixClient: MatrixClient): SpaceService { return matrixClient.spaceService } + + @Provides + fun providesHomeserverCapabilitiesProvider(matrixClient: MatrixClient): HomeserverCapabilitiesProvider { + return matrixClient.homeserverCapabilities() + } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt index 96c4bdf3c4..7e65a1cc5c 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt @@ -49,7 +49,6 @@ private fun StateEventContent.toContent(): NotificationContent.StateEvent { StateEventContent.PolicyRuleRoom -> NotificationContent.StateEvent.PolicyRuleRoom StateEventContent.PolicyRuleServer -> NotificationContent.StateEvent.PolicyRuleServer StateEventContent.PolicyRuleUser -> NotificationContent.StateEvent.PolicyRuleUser - StateEventContent.RoomAliases -> NotificationContent.StateEvent.RoomAliases StateEventContent.RoomAvatar -> NotificationContent.StateEvent.RoomAvatar StateEventContent.RoomCanonicalAlias -> NotificationContent.StateEvent.RoomCanonicalAlias StateEventContent.RoomCreate -> NotificationContent.StateEvent.RoomCreate diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt index 644c5aefc2..e6287d0d16 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt @@ -34,6 +34,7 @@ import io.element.android.libraries.matrix.api.room.location.LiveLocationShare import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange import io.element.android.libraries.matrix.api.room.roomNotificationSettings +import io.element.android.libraries.matrix.api.room.threads.ThreadsListService import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver @@ -43,10 +44,11 @@ import io.element.android.libraries.matrix.impl.mapper.map import io.element.android.libraries.matrix.impl.room.history.map import io.element.android.libraries.matrix.impl.room.join.map import io.element.android.libraries.matrix.impl.room.knock.RustKnockRequest -import io.element.android.libraries.matrix.impl.room.location.map import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher +import io.element.android.libraries.matrix.impl.room.threads.RustThreadsListService import io.element.android.libraries.matrix.impl.roomdirectory.map import io.element.android.libraries.matrix.impl.timeline.RustTimeline +import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper import io.element.android.libraries.matrix.impl.util.MessageEventContent import io.element.android.libraries.matrix.impl.util.mxCallbackFlow import io.element.android.libraries.matrix.impl.widget.RustWidgetDriver @@ -68,7 +70,6 @@ import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.DateDividerMode import org.matrix.rustcomponents.sdk.IdentityStatusChangeListener import org.matrix.rustcomponents.sdk.KnockRequestsListener -import org.matrix.rustcomponents.sdk.LiveLocationShareListener import org.matrix.rustcomponents.sdk.RoomMessageEventMessageType import org.matrix.rustcomponents.sdk.RoomSendQueueUpdate import org.matrix.rustcomponents.sdk.SendQueueListener @@ -147,6 +148,12 @@ class JoinedRustRoom( override val liveTimeline = liveInnerTimeline.map(mode = Timeline.Mode.Live) + override val threadsListService: ThreadsListService = RustThreadsListService( + inner = innerRoom.threadListService(), + contentMapper = TimelineEventContentMapper(), + roomCoroutineScope = roomCoroutineScope, + ) + override val syncUpdateFlow = flow { var counter = 0L liveTimeline.onSyncedEventReceived.collect { @@ -504,13 +511,7 @@ class JoinedRustRoom( } override fun subscribeToLiveLocationShares(): Flow> { - return mxCallbackFlow { - innerRoom.subscribeToLiveLocationShares(object : LiveLocationShareListener { - override fun call(liveLocationShares: List) { - trySend(liveLocationShares.map { it.map() }) - } - }) - } + TODO("Not implemented yet") } override suspend fun startLiveLocationShare(durationMillis: Long): Result = withContext(roomDispatcher) { @@ -536,6 +537,7 @@ class JoinedRustRoom( override fun destroy() { baseRoom.destroy() liveInnerTimeline.destroy() + threadsListService.destroy() Timber.d("Room $roomId destroyed") } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/StateEventType.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/StateEventType.kt index 76fea0beef..897d9a34cb 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/StateEventType.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/StateEventType.kt @@ -16,7 +16,6 @@ fun StateEventType.map(): RustStateEventType = when (this) { StateEventType.PolicyRuleServer -> RustStateEventType.PolicyRuleServer StateEventType.PolicyRuleUser -> RustStateEventType.PolicyRuleUser StateEventType.CallMember -> RustStateEventType.CallMember - StateEventType.RoomAliases -> RustStateEventType.RoomAliases StateEventType.RoomAvatar -> RustStateEventType.RoomAvatar StateEventType.RoomCanonicalAlias -> RustStateEventType.RoomCanonicalAlias StateEventType.RoomCreate -> RustStateEventType.RoomCreate @@ -46,7 +45,6 @@ fun RustStateEventType.map(): StateEventType = when (this) { RustStateEventType.PolicyRuleServer -> StateEventType.PolicyRuleServer RustStateEventType.PolicyRuleUser -> StateEventType.PolicyRuleUser RustStateEventType.CallMember -> StateEventType.CallMember - RustStateEventType.RoomAliases -> StateEventType.RoomAliases RustStateEventType.RoomAvatar -> StateEventType.RoomAvatar RustStateEventType.RoomCanonicalAlias -> StateEventType.RoomCanonicalAlias RustStateEventType.RoomCreate -> StateEventType.RoomCreate diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/location/LiveLocationShareMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/location/LiveLocationShareMapper.kt deleted file mode 100644 index 3b80c1c61f..0000000000 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/location/LiveLocationShareMapper.kt +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations 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.libraries.matrix.impl.room.location - -import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.room.location.LiveLocationShare -import org.matrix.rustcomponents.sdk.LiveLocationShare as RustLiveLocationShare - -fun RustLiveLocationShare.map(): LiveLocationShare { - return LiveLocationShare( - userId = UserId(userId), - lastGeoUri = lastLocation.location.geoUri, - lastTimestamp = lastLocation.ts.toLong(), - isLive = isLive, - ) -} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/threads/RustThreadsListService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/threads/RustThreadsListService.kt new file mode 100644 index 0000000000..a74c5bc378 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/threads/RustThreadsListService.kt @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2026 Element Creations 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.libraries.matrix.impl.room.threads + +import io.element.android.libraries.core.extensions.runCatchingExceptions +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.threads.ThreadListItem +import io.element.android.libraries.matrix.api.room.threads.ThreadListItemEvent +import io.element.android.libraries.matrix.api.room.threads.ThreadListPaginationStatus +import io.element.android.libraries.matrix.api.room.threads.ThreadsListService +import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper +import io.element.android.libraries.matrix.impl.timeline.item.event.map +import io.element.android.libraries.matrix.impl.util.mxCallbackFlow +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart +import org.matrix.rustcomponents.sdk.ThreadListEntriesListener +import org.matrix.rustcomponents.sdk.ThreadListPaginationStateListener +import org.matrix.rustcomponents.sdk.ThreadListUpdate +import uniffi.matrix_sdk_ui.ThreadListPaginationState +import org.matrix.rustcomponents.sdk.ThreadListService as InnerThreadListService + +class RustThreadsListService( + private val inner: InnerThreadListService, + private val roomCoroutineScope: CoroutineScope, + private val contentMapper: TimelineEventContentMapper = TimelineEventContentMapper(), +) : ThreadsListService { + private var itemSubscriptionJob: Job? = null + + private val items = MutableStateFlow>(emptyList()) + + override fun subscribeToItemUpdates(): Flow> { + if (itemSubscriptionJob?.isActive != true) { + itemSubscriptionJob = doSubscribeToItemUpdates() + } + + return items + } + + private fun doSubscribeToItemUpdates(): Job { + val updatesFlow = mxCallbackFlow { + inner.subscribeToItemsUpdates(object : ThreadListEntriesListener { + override fun onUpdate(diff: List) { + trySend(diff) + } + }) + } + + return updatesFlow + .onStart { items.value = inner.items().map { it.map(contentMapper) } } + .onEach { diff -> + val updated = items.value.toMutableList() + updated.apply(diff, contentMapper) + items.value = updated + } + .launchIn(roomCoroutineScope) + } + + override fun subscribeToPaginationUpdates(): Flow { + return mxCallbackFlow { + inner.subscribeToPaginationStateUpdates(object : ThreadListPaginationStateListener { + override fun onUpdate(state: ThreadListPaginationState) { + trySend(state.map()) + } + }).also { + // Send the initial state + trySend(inner.paginationState().map()) + } + } + } + + override suspend fun paginate(): Result = runCatchingExceptions { + inner.paginate() + } + + override suspend fun reset(): Result = runCatchingExceptions { + inner.reset() + } + + override fun destroy() { + itemSubscriptionJob?.cancel() + inner.destroy() + } +} + +private fun MutableList.apply( + diff: List, + contentMapper: TimelineEventContentMapper +) { + for (diffItem in diff) { + when (diffItem) { + is ThreadListUpdate.Append -> { + val newItems = diffItem.values.map { it.map(contentMapper) } + addAll(newItems) + } + ThreadListUpdate.Clear -> clear() + is ThreadListUpdate.Insert -> { + add(diffItem.index.toInt(), diffItem.value.map(contentMapper)) + } + ThreadListUpdate.PopBack -> { + removeAt(lastIndex) + } + ThreadListUpdate.PopFront -> { + removeAt(0) + } + is ThreadListUpdate.PushBack -> { + add(diffItem.value.map(contentMapper)) + } + is ThreadListUpdate.PushFront -> { + add(0, diffItem.value.map(contentMapper)) + } + is ThreadListUpdate.Remove -> { + removeAt(diffItem.index.toInt()) + } + is ThreadListUpdate.Reset -> { + clear() + addAll(diffItem.values.map { it.map(contentMapper) }) + } + is ThreadListUpdate.Set -> { + set(diffItem.index.toInt(), diffItem.value.map(contentMapper)) + } + is ThreadListUpdate.Truncate -> { + subList(diffItem.length.toInt(), size).clear() + } + } + } +} + +fun org.matrix.rustcomponents.sdk.ThreadListItem.map(contentMapper: TimelineEventContentMapper): ThreadListItem = ThreadListItem( + rootEvent = rootEvent.map(contentMapper), + latestEvent = latestEvent?.map(contentMapper), + numberOfReplies = numReplies.toLong(), +) + +fun org.matrix.rustcomponents.sdk.ThreadListItemEvent.map(contentMapper: TimelineEventContentMapper): ThreadListItemEvent = ThreadListItemEvent( + eventId = EventId(eventId), + senderId = UserId(sender), + isOwn = isOwn, + senderProfile = senderProfile.map(), + content = content?.let(contentMapper::map), + timestamp = timestamp.toLong(), +) + +fun ThreadListPaginationState.map(): ThreadListPaginationStatus = when (this) { + is ThreadListPaginationState.Idle -> ThreadListPaginationStatus.Idle(hasMoreToLoad = !endReached) + ThreadListPaginationState.Loading -> ThreadListPaginationStatus.Loading +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt index 8a184311de..7b398529a0 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt @@ -130,6 +130,8 @@ class RustTimeline( Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode is Timeline.Mode.FocusedOnEvent) ) + private val loggerTag = "Timeline($mode)" + init { when (mode) { is Timeline.Mode.Live, is Timeline.Mode.FocusedOnEvent -> coroutineScope.fetchMembers() @@ -177,10 +179,11 @@ class RustTimeline( } private fun updatePaginationStatus(direction: Timeline.PaginationDirection, update: (Timeline.PaginationStatus) -> Timeline.PaginationStatus) { - when (direction) { + val result = when (direction) { Timeline.PaginationDirection.BACKWARDS -> backwardPaginationStatus.getAndUpdate(update) Timeline.PaginationDirection.FORWARDS -> forwardPaginationStatus.getAndUpdate(update) } + Timber.tag(loggerTag).d("updatePaginationStatus $direction: $result") } // Use NonCancellable to avoid breaking the timeline when the coroutine is cancelled. @@ -195,12 +198,13 @@ class RustTimeline( } }.onFailure { error -> if (error is TimelineException.CannotPaginate) { - Timber.d("Can't paginate $direction on room ${joinedRoom.roomId} with paginationStatus: ${backwardPaginationStatus.value}") + Timber.tag(loggerTag).d("Can't paginate $direction on room ${joinedRoom.roomId} with paginationStatus: ${backwardPaginationStatus.value}") } else { updatePaginationStatus(direction) { it.copy(isPaginating = false) } - Timber.e(error, "Error paginating $direction on room ${joinedRoom.roomId}") + Timber.tag(loggerTag).e(error, "Error paginating $direction on room ${joinedRoom.roomId}") } }.onSuccess { hasReachedEnd -> + Timber.tag(loggerTag).d("Finished paginating $direction on room ${joinedRoom.roomId}, hasReachedEnd: $hasReachedEnd") updatePaginationStatus(direction) { it.copy(isPaginating = false, hasMoreToLoad = !hasReachedEnd) } } } @@ -264,7 +268,7 @@ class RustTimeline( try { inner.fetchMembers() } catch (exception: Exception) { - Timber.e(exception, "Error fetching members for room ${joinedRoom.roomId}") + Timber.tag(loggerTag).e(exception, "Error fetching members for room ${joinedRoom.roomId}") } } @@ -370,7 +374,7 @@ class RustTimeline( formattedCaption: String?, inReplyToEventId: EventId?, ): Result { - Timber.d("Sending image ${file.path.hash()}") + Timber.tag(loggerTag).d("Sending image ${file.path.hash()}") return sendAttachment(listOfNotNull(file, thumbnailFile)) { inner.sendImage( params = UploadParameters( @@ -396,7 +400,7 @@ class RustTimeline( formattedCaption: String?, inReplyToEventId: EventId?, ): Result { - Timber.d("Sending video ${file.path.hash()}") + Timber.tag(loggerTag).d("Sending video ${file.path.hash()}") return sendAttachment(listOfNotNull(file, thumbnailFile)) { inner.sendVideo( params = UploadParameters( @@ -421,7 +425,7 @@ class RustTimeline( formattedCaption: String?, inReplyToEventId: EventId?, ): Result { - Timber.d("Sending audio ${file.path.hash()}") + Timber.tag(loggerTag).d("Sending audio ${file.path.hash()}") return sendAttachment(listOf(file)) { inner.sendAudio( params = UploadParameters( @@ -445,7 +449,7 @@ class RustTimeline( formattedCaption: String?, inReplyToEventId: EventId?, ): Result { - Timber.d("Sending file ${file.path.hash()}") + Timber.tag(loggerTag).d("Sending file ${file.path.hash()}") return sendAttachment(listOf(file)) { inner.sendFile( params = UploadParameters( @@ -475,7 +479,7 @@ class RustTimeline( runCatchingExceptions { roomContentForwarder.forward(fromTimeline = inner, eventId = eventId, toRoomIds = roomIds) }.onFailure { - Timber.e(it) + Timber.tag(loggerTag).e(it) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt index 2145bd2a7d..d617df60db 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt @@ -223,7 +223,6 @@ private fun RustOtherState.map(): OtherState { RustOtherState.PolicyRuleRoom -> OtherState.PolicyRuleRoom RustOtherState.PolicyRuleServer -> OtherState.PolicyRuleServer RustOtherState.PolicyRuleUser -> OtherState.PolicyRuleUser - RustOtherState.RoomAliases -> OtherState.RoomAliases is RustOtherState.RoomAvatar -> OtherState.RoomAvatar(url) RustOtherState.RoomCanonicalAlias -> OtherState.RoomCanonicalAlias RustOtherState.RoomCreate -> OtherState.RoomCreate diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustHomeserverCapabilitiesProviderTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustHomeserverCapabilitiesProviderTest.kt new file mode 100644 index 0000000000..8d3377d698 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustHomeserverCapabilitiesProviderTest.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2026 Element Creations 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.libraries.matrix.impl + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiHomeserverCapabilities +import io.element.android.tests.testutils.lambda.lambdaRecorder +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class RustHomeserverCapabilitiesProviderTest { + @Test + fun `refresh calls client refresh`() = runTest { + val refreshLambda = lambdaRecorder {} + val provider = createCapabilitiesProvider( + capabilities = FakeFfiHomeserverCapabilities(refresh = refreshLambda), + ) + assertThat(provider.refresh().isSuccess).isTrue() + refreshLambda.assertions().isCalledOnce() + } + + @Test + fun `refresh fails when client refresh does`() = runTest { + val refreshLambda = lambdaRecorder { throw IllegalStateException("Failed to refresh capabilities") } + val provider = createCapabilitiesProvider( + capabilities = FakeFfiHomeserverCapabilities(refresh = refreshLambda), + ) + assertThat(provider.refresh().isFailure).isTrue() + refreshLambda.assertions().isCalledOnce() + } + + @Test + fun `canChangeDisplayName returns expected value`() = runTest { + val provider = createCapabilitiesProvider( + capabilities = FakeFfiHomeserverCapabilities(canChangeDisplayName = { true }), + ) + assertThat(provider.canChangeDisplayName().getOrNull()).isTrue() + } + + @Test + fun `canChangeAvatarUrl returns expected value`() = runTest { + val provider = createCapabilitiesProvider( + capabilities = FakeFfiHomeserverCapabilities(canChangeAvatar = { true }), + ) + assertThat(provider.canChangeAvatarUrl().getOrNull()).isTrue() + } + + @Test + fun `canChangeDisplayName returns failure when client throws`() = runTest { + val provider = createCapabilitiesProvider( + capabilities = FakeFfiHomeserverCapabilities(canChangeDisplayName = { throw IllegalStateException("Failed to get display name capability") }), + ) + assert(provider.canChangeDisplayName().isFailure) + } + + private fun createCapabilitiesProvider( + capabilities: FakeFfiHomeserverCapabilities = FakeFfiHomeserverCapabilities(), + ) = RustHomeserverCapabilitiesProvider(capabilities) +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/EventTimelineItem.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/EventTimelineItem.kt index 41823a0fbb..8689de3c9b 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/EventTimelineItem.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/EventTimelineItem.kt @@ -14,10 +14,9 @@ import io.element.android.libraries.matrix.test.A_USER_ID import org.matrix.rustcomponents.sdk.EventOrTransactionId import org.matrix.rustcomponents.sdk.EventSendState import org.matrix.rustcomponents.sdk.EventTimelineItem -import org.matrix.rustcomponents.sdk.EventTimelineItemDebugInfo +import org.matrix.rustcomponents.sdk.LazyTimelineItemProvider import org.matrix.rustcomponents.sdk.ProfileDetails import org.matrix.rustcomponents.sdk.Receipt -import org.matrix.rustcomponents.sdk.ShieldState import org.matrix.rustcomponents.sdk.TimelineItemContent import uniffi.matrix_sdk_ui.EventItemOrigin @@ -26,37 +25,35 @@ internal fun aRustEventTimelineItem( eventOrTransactionId: EventOrTransactionId = EventOrTransactionId.EventId(AN_EVENT_ID.value), sender: String = A_USER_ID.value, senderProfile: ProfileDetails = ProfileDetails.Unavailable, + forwarder: String? = null, + forwarderProfile: ProfileDetails? = null, isOwn: Boolean = true, isEditable: Boolean = true, content: TimelineItemContent = aRustTimelineItemContentMsgLike(), + eventTypeRaw: String? = null, timestamp: ULong = 0uL, - debugInfo: EventTimelineItemDebugInfo = anEventTimelineItemDebugInfo(), localSendState: EventSendState? = null, + localCreatedAt: ULong? = null, readReceipts: Map = emptyMap(), origin: EventItemOrigin? = EventItemOrigin.SYNC, canBeRepliedTo: Boolean = true, - shieldsState: ShieldState = ShieldState.None, - localCreatedAt: ULong? = null, - forwarder: String? = null, - forwarderProfile: ProfileDetails? = null, + lazyProvider: LazyTimelineItemProvider = FakeFfiLazyTimelineItemProvider(), ) = EventTimelineItem( isRemote = isRemote, eventOrTransactionId = eventOrTransactionId, sender = sender, senderProfile = senderProfile, - timestamp = timestamp, - isOwn = isOwn, - isEditable = isEditable, - canBeRepliedTo = canBeRepliedTo, - content = content, - localSendState = localSendState, - readReceipts = readReceipts, - origin = origin, - localCreatedAt = localCreatedAt, - lazyProvider = FakeFfiLazyTimelineItemProvider( - debugInfo = debugInfo, - shieldsState = shieldsState, - ), forwarder = forwarder, forwarderProfile = forwarderProfile, + isOwn = isOwn, + isEditable = isEditable, + content = content, + eventTypeRaw = eventTypeRaw, + timestamp = timestamp, + localSendState = localSendState, + localCreatedAt = localCreatedAt, + readReceipts = readReceipts, + origin = origin, + canBeRepliedTo = canBeRepliedTo, + lazyProvider = lazyProvider, ) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt index 2aec38fcde..57ffcddb37 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt @@ -17,6 +17,7 @@ import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.ClientDelegate import org.matrix.rustcomponents.sdk.CreateRoomParameters import org.matrix.rustcomponents.sdk.Encryption +import org.matrix.rustcomponents.sdk.HomeserverCapabilities import org.matrix.rustcomponents.sdk.HomeserverLoginDetails import org.matrix.rustcomponents.sdk.IgnoredUsersListener import org.matrix.rustcomponents.sdk.NoHandle @@ -50,6 +51,7 @@ class FakeFfiClient( private val homeserverLoginDetailsResult: () -> HomeserverLoginDetails = { lambdaError() }, private val getStoreSizesResult: () -> StoreSizes = { lambdaError() }, private val createRoomResult: (CreateRoomParameters) -> String = { lambdaError() }, + private val homeserverCapabilities: HomeserverCapabilities = FakeFfiHomeserverCapabilities(), private val closeResult: () -> Unit = {}, ) : Client(NoHandle) { override fun userId(): String = userId @@ -103,5 +105,9 @@ class FakeFfiClient( return createRoomResult(request) } + override fun homeserverCapabilities(): HomeserverCapabilities { + return homeserverCapabilities + } + override fun close() = closeResult() } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiHomeserverCapabilities.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiHomeserverCapabilities.kt new file mode 100644 index 0000000000..4c60cbbb49 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiHomeserverCapabilities.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026 Element Creations 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.libraries.matrix.impl.fixtures.fakes + +import io.element.android.tests.testutils.lambda.lambdaError +import org.matrix.rustcomponents.sdk.ExtendedProfileFields +import org.matrix.rustcomponents.sdk.HomeserverCapabilities +import org.matrix.rustcomponents.sdk.NoHandle + +class FakeFfiHomeserverCapabilities( + private val refresh: () -> Unit = { lambdaError() }, + private val canChangeDisplayName: () -> Boolean = { lambdaError() }, + private val canChangeAvatar: () -> Boolean = { lambdaError() }, + private val canChangePassword: () -> Boolean = { lambdaError() }, + private val canChangeThirdPartyIds: () -> Boolean = { lambdaError() }, + private val canGetLoginToken: () -> Boolean = { lambdaError() }, + private val forgetsRoomWhenLeaving: () -> Boolean = { lambdaError() }, + private val extendedProfileFields: () -> ExtendedProfileFields = { lambdaError() }, +) : HomeserverCapabilities(NoHandle) { + override suspend fun refresh() = refresh.invoke() + override suspend fun canChangeDisplayname(): Boolean = canChangeDisplayName.invoke() + override suspend fun canChangeAvatar(): Boolean = canChangeAvatar.invoke() + override suspend fun canChangePassword(): Boolean = canChangePassword.invoke() + override suspend fun canChangeThirdpartyIds(): Boolean = canChangeThirdPartyIds.invoke() + override suspend fun canGetLoginToken(): Boolean = canGetLoginToken.invoke() + override suspend fun forgetsRoomWhenLeaving(): Boolean = forgetsRoomWhenLeaving.invoke() + override suspend fun extendedProfileFields(): ExtendedProfileFields = extendedProfileFields.invoke() +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiThreadListService.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiThreadListService.kt new file mode 100644 index 0000000000..009e6a3348 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiThreadListService.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2026 Element Creations 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.libraries.matrix.impl.fixtures.fakes + +import org.matrix.rustcomponents.sdk.NoHandle +import org.matrix.rustcomponents.sdk.TaskHandle +import org.matrix.rustcomponents.sdk.ThreadListEntriesListener +import org.matrix.rustcomponents.sdk.ThreadListItem +import org.matrix.rustcomponents.sdk.ThreadListPaginationStateListener +import org.matrix.rustcomponents.sdk.ThreadListService +import org.matrix.rustcomponents.sdk.ThreadListUpdate +import uniffi.matrix_sdk_ui.ThreadListPaginationState + +class FakeFfiThreadListService( + private val subscribeToItemsUpdates: (ThreadListEntriesListener) -> TaskHandle = { FakeFfiTaskHandle() }, + private val subscribeToPaginationStateUpdates: (ThreadListPaginationStateListener) -> TaskHandle = { FakeFfiTaskHandle() }, + private val items: () -> List = { emptyList() }, + private val paginationState: () -> ThreadListPaginationState = { ThreadListPaginationState.Idle(endReached = false) }, + private val paginate: suspend () -> Unit = {}, + private val reset: suspend () -> Unit = {}, + private val destroy: () -> Unit = {}, +) : ThreadListService(NoHandle) { + private var itemsListener: ThreadListEntriesListener? = null + private var paginationStateListener: ThreadListPaginationStateListener? = null + + override fun subscribeToItemsUpdates(listener: ThreadListEntriesListener): TaskHandle { + itemsListener = listener + return subscribeToItemsUpdates.invoke(listener) + } + + override fun subscribeToPaginationStateUpdates(listener: ThreadListPaginationStateListener): TaskHandle { + paginationStateListener = listener + return subscribeToPaginationStateUpdates.invoke(listener) + } + + override fun items(): List = items.invoke() + + override fun paginationState(): ThreadListPaginationState = paginationState.invoke() + + override suspend fun paginate() = paginate.invoke() + + override suspend fun reset() = reset.invoke() + + override fun destroy() = destroy.invoke() + + fun emitUpdates(updates: List) { + itemsListener?.onUpdate(updates) + } + + fun emitPaginationState(state: ThreadListPaginationState) { + paginationStateListener?.onUpdate(state) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/StateEventTypeTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/StateEventTypeTest.kt index 428bb7db7a..93a8f0908f 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/StateEventTypeTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/StateEventTypeTest.kt @@ -20,7 +20,6 @@ class StateEventTypeTest { assertThat(RustStateEventType.PolicyRuleRoom.map()).isEqualTo(StateEventType.PolicyRuleRoom) assertThat(RustStateEventType.PolicyRuleServer.map()).isEqualTo(StateEventType.PolicyRuleServer) assertThat(RustStateEventType.PolicyRuleUser.map()).isEqualTo(StateEventType.PolicyRuleUser) - assertThat(RustStateEventType.RoomAliases.map()).isEqualTo(StateEventType.RoomAliases) assertThat(RustStateEventType.RoomAvatar.map()).isEqualTo(StateEventType.RoomAvatar) assertThat(RustStateEventType.RoomCanonicalAlias.map()).isEqualTo(StateEventType.RoomCanonicalAlias) assertThat(RustStateEventType.RoomCreate.map()).isEqualTo(StateEventType.RoomCreate) @@ -47,7 +46,6 @@ class StateEventTypeTest { assertThat(StateEventType.PolicyRuleRoom.map()).isEqualTo(RustStateEventType.PolicyRuleRoom) assertThat(StateEventType.PolicyRuleServer.map()).isEqualTo(RustStateEventType.PolicyRuleServer) assertThat(StateEventType.PolicyRuleUser.map()).isEqualTo(RustStateEventType.PolicyRuleUser) - assertThat(StateEventType.RoomAliases.map()).isEqualTo(RustStateEventType.RoomAliases) assertThat(StateEventType.RoomAvatar.map()).isEqualTo(RustStateEventType.RoomAvatar) assertThat(StateEventType.RoomCanonicalAlias.map()).isEqualTo(RustStateEventType.RoomCanonicalAlias) assertThat(StateEventType.RoomCreate.map()).isEqualTo(RustStateEventType.RoomCreate) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/threads/RustThreadsListServiceTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/threads/RustThreadsListServiceTest.kt new file mode 100644 index 0000000000..0fc9ad0603 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/threads/RustThreadsListServiceTest.kt @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2026 Element Creations 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.libraries.matrix.impl.room.threads + +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.room.threads.ThreadListPaginationStatus +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustTimelineItemContentMsgLike +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTaskHandle +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiThreadListService +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.A_TIMESTAMP +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.tests.testutils.lambda.lambdaRecorder +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.matrix.rustcomponents.sdk.ProfileDetails +import org.matrix.rustcomponents.sdk.TaskHandle +import org.matrix.rustcomponents.sdk.ThreadListEntriesListener +import org.matrix.rustcomponents.sdk.ThreadListItem +import org.matrix.rustcomponents.sdk.ThreadListItemEvent +import org.matrix.rustcomponents.sdk.ThreadListPaginationStateListener +import org.matrix.rustcomponents.sdk.ThreadListUpdate +import uniffi.matrix_sdk_ui.ThreadListPaginationState + +@OptIn(ExperimentalCoroutinesApi::class) +class RustThreadsListServiceTest { + @Test + fun `subscribing to item updates calls the FFI method and allows retrieving new items`() = runTest { + val subscribeToItemsUpdatesRecorder = lambdaRecorder { FakeFfiTaskHandle() } + val inner = FakeFfiThreadListService(subscribeToItemsUpdates = subscribeToItemsUpdatesRecorder) + val service = createThreadsListService(inner = inner) + + service.subscribeToItemUpdates().test { + assertThat(awaitItem()).isEmpty() + + runCurrent() + subscribeToItemsUpdatesRecorder.assertions().isCalledOnce() + + inner.emitUpdates(listOf(aRustThreadListUpdate())) + + assertThat(awaitItem()).isNotEmpty() + } + } + + @Suppress("UnusedFlow") + @Test + fun `subscribing to item updates twice only calls the FFI method once`() = runTest { + val subscribeToItemsUpdatesRecorder = lambdaRecorder { FakeFfiTaskHandle() } + val inner = FakeFfiThreadListService(subscribeToItemsUpdates = subscribeToItemsUpdatesRecorder) + val service = createThreadsListService(inner = inner) + + service.subscribeToItemUpdates() + service.subscribeToItemUpdates() + + runCurrent() + + subscribeToItemsUpdatesRecorder.assertions().isCalledOnce() + } + + @Test + fun `subscribing to pagination updates calls the FFI method and allows retrieving new items`() = runTest { + val subscribeToPaginationUpdatesRecorder = lambdaRecorder { FakeFfiTaskHandle() } + val inner = FakeFfiThreadListService(subscribeToPaginationStateUpdates = subscribeToPaginationUpdatesRecorder) + val service = createThreadsListService(inner = inner) + + service.subscribeToPaginationUpdates().test { + assertThat(awaitItem()).isEqualTo(ThreadListPaginationStatus.Idle(hasMoreToLoad = true)) + + runCurrent() + subscribeToPaginationUpdatesRecorder.assertions().isCalledOnce() + + inner.emitPaginationState(ThreadListPaginationState.Loading) + + assertThat(awaitItem()).isEqualTo(ThreadListPaginationStatus.Loading) + } + } + + @Test + fun `paginate calls the FFI method`() = runTest { + val paginateRecorder = lambdaRecorder {} + val inner = FakeFfiThreadListService(paginate = paginateRecorder) + val service = createThreadsListService(inner = inner) + + service.paginate() + + paginateRecorder.assertions().isCalledOnce() + } + + @Test + fun `reset calls the FFI method`() = runTest { + val resetRecorder = lambdaRecorder {} + val inner = FakeFfiThreadListService(reset = resetRecorder) + val service = createThreadsListService(inner = inner) + + service.reset() + + resetRecorder.assertions().isCalledOnce() + } + + @Test + fun `destroy calls the FFI method`() = runTest { + val destroyRecorder = lambdaRecorder {} + val inner = FakeFfiThreadListService(destroy = destroyRecorder) + val service = createThreadsListService(inner = inner) + + service.destroy() + + destroyRecorder.assertions().isCalledOnce() + } + + private fun TestScope.createThreadsListService( + inner: FakeFfiThreadListService = FakeFfiThreadListService(), + ) = RustThreadsListService( + inner = inner, + roomCoroutineScope = backgroundScope, + ) + + private fun aRustThreadListUpdate() = ThreadListUpdate.Append( + values = listOf( + ThreadListItem( + rootEvent = ThreadListItemEvent( + eventId = AN_EVENT_ID.value, + timestamp = A_TIMESTAMP.toULong(), + sender = A_USER_ID.value, + senderProfile = ProfileDetails.Pending, + isOwn = true, + content = aRustTimelineItemContentMsgLike(), + ), + numReplies = 0u, + latestEvent = null, + ) + ), + ) +} diff --git a/libraries/matrix/test/build.gradle.kts b/libraries/matrix/test/build.gradle.kts index ccb1a37a25..63836d857a 100644 --- a/libraries/matrix/test/build.gradle.kts +++ b/libraries/matrix/test/build.gradle.kts @@ -19,7 +19,6 @@ dependencies { api(projects.libraries.matrix.api) api(libs.coroutines.core) implementation(libs.coroutines.test) - implementation(projects.libraries.matrix.impl) implementation(projects.services.analytics.api) implementation(projects.tests.testutils) implementation(libs.kotlinx.collections.immutable) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeHomeserverCapabilitiesProvider.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeHomeserverCapabilitiesProvider.kt new file mode 100644 index 0000000000..c098388c89 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeHomeserverCapabilitiesProvider.kt @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2026 Element Creations 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.libraries.matrix.test + +import io.element.android.libraries.matrix.api.HomeserverCapabilitiesProvider + +class FakeHomeserverCapabilitiesProvider( + private val refresh: () -> Result = { Result.success(Unit) }, + private val canChangeDisplayName: () -> Result = { Result.success(true) }, + private val canChangeAvatarUrl: () -> Result = { Result.success(true) }, +) : HomeserverCapabilitiesProvider { + override suspend fun refresh(): Result = refresh.invoke() + override suspend fun canChangeDisplayName(): Result = canChangeDisplayName.invoke() + override suspend fun canChangeAvatarUrl(): Result = canChangeAvatarUrl.invoke() +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index 56527574d7..742af160ae 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.matrix.test +import io.element.android.libraries.matrix.api.HomeserverCapabilitiesProvider import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.analytics.SdkStoreSizes import io.element.android.libraries.matrix.api.core.DeviceId @@ -84,6 +85,7 @@ class FakeMatrixClient( override val roomDirectoryService: RoomDirectoryService = FakeRoomDirectoryService(), override val mediaPreviewService: MediaPreviewService = FakeMediaPreviewService(), override val roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(), + private val homeserverCapabilitiesProvider: FakeHomeserverCapabilitiesProvider = FakeHomeserverCapabilitiesProvider(), private val accountManagementUrlResult: (AccountManagementAction?) -> Result = { lambdaError() }, private val resolveRoomAliasResult: (RoomAlias) -> Result> = { Result.success( @@ -384,4 +386,8 @@ class FakeMatrixClient( override suspend fun resetWellKnownConfig(): Result { return resetWellKnownConfigLambda() } + + override fun homeserverCapabilities(): HomeserverCapabilitiesProvider { + return homeserverCapabilitiesProvider + } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeMatrixAuthenticationService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeMatrixAuthenticationService.kt index c4acccb55c..238ad2663d 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeMatrixAuthenticationService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeMatrixAuthenticationService.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.matrix.test.auth import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.auth.ElementClassicSession import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails import io.element.android.libraries.matrix.api.auth.OidcDetails @@ -17,6 +18,7 @@ import io.element.android.libraries.matrix.api.auth.external.ExternalSession import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.FakeMatrixClient @@ -32,6 +34,8 @@ class FakeMatrixAuthenticationService( lambdaRecorder Unit, Result> { _, _ -> Result.success(A_SESSION_ID) }, private val importCreatedSessionLambda: (ExternalSession) -> Result = { lambdaError() }, private val setHomeserverResult: (String) -> Result = { lambdaError() }, + private val setElementClassicSessionResult: (ElementClassicSession?) -> Unit = { lambdaError() }, + private val doSecretsContainBackupKeyResult: (UserId, String, String) -> Boolean = { _, _, _ -> lambdaError() }, ) : MatrixAuthenticationService { private var oidcError: Throwable? = null private var oidcCancelError: Throwable? = null @@ -108,4 +112,12 @@ class FakeMatrixAuthenticationService( fun givenMatrixClient(matrixClient: MatrixClient) { this.matrixClient = matrixClient } + + override fun setElementClassicSession(session: ElementClassicSession?) { + setElementClassicSessionResult(session) + } + + override fun doSecretsContainBackupKey(userId: UserId, secrets: String, backupInfo: String): Boolean { + return doSecretsContainBackupKeyResult(userId, secrets, backupInfo) + } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/mxc/FakeMxcTools.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/mxc/FakeMxcTools.kt index c348cd351c..d49a1cc22d 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/mxc/FakeMxcTools.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/mxc/FakeMxcTools.kt @@ -8,8 +8,12 @@ package io.element.android.libraries.matrix.test.mxc import io.element.android.libraries.matrix.api.mxc.MxcTools -import io.element.android.libraries.matrix.impl.mxc.DefaultMxcTools +import io.element.android.tests.testutils.lambda.lambdaError class FakeMxcTools( - private val delegate: MxcTools = DefaultMxcTools() -) : MxcTools by delegate + private val mxcUri2FilePathResult: (String) -> String? = { lambdaError() } +) : MxcTools { + override fun mxcUri2FilePath(mxcUri: String): String? { + return mxcUri2FilePathResult(mxcUri) + } +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeJoinedRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeJoinedRoom.kt index a4580334e4..84497b38de 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeJoinedRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeJoinedRoom.kt @@ -35,6 +35,7 @@ import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService +import io.element.android.libraries.matrix.test.room.threads.FakeThreadsListService import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.simulateLongTask @@ -56,6 +57,7 @@ class FakeJoinedRoom( override val roomNotificationSettingsStateFlow: StateFlow = MutableStateFlow(RoomNotificationSettingsState.Unknown), override val knockRequestsFlow: Flow> = MutableStateFlow(emptyList()), + override val threadsListService: FakeThreadsListService = FakeThreadsListService(), private val roomNotificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), private var createTimelineResult: (CreateTimelineParams) -> Result = { lambdaError() }, private val editMessageLambda: (EventId, String, String?, List) -> Result = { _, _, _, _ -> lambdaError() }, diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/threads/FakeThreadsListService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/threads/FakeThreadsListService.kt new file mode 100644 index 0000000000..a1e719ffb2 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/threads/FakeThreadsListService.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2026 Element Creations 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.libraries.matrix.test.room.threads + +import io.element.android.libraries.matrix.api.room.threads.ThreadListItem +import io.element.android.libraries.matrix.api.room.threads.ThreadListPaginationStatus +import io.element.android.libraries.matrix.api.room.threads.ThreadsListService +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +class FakeThreadsListService( + private val items: MutableStateFlow> = MutableStateFlow(emptyList()), + private val paginationStatus: MutableStateFlow = MutableStateFlow(ThreadListPaginationStatus.Idle(hasMoreToLoad = true)), + private val subscribeToItemUpdates: () -> Flow> = { items }, + private val subscribeToPaginationUpdates: () -> Flow = { paginationStatus }, + private val paginate: suspend () -> Result = { Result.success(Unit) }, + private val reset: suspend () -> Result = { Result.success(Unit) }, + private val destroy: () -> Unit = {}, +) : ThreadsListService { + override fun subscribeToItemUpdates(): Flow> { + return subscribeToItemUpdates.invoke() + } + + override fun subscribeToPaginationUpdates(): Flow { + return subscribeToPaginationUpdates.invoke() + } + + override suspend fun paginate(): Result { + return paginate.invoke() + } + + override suspend fun reset(): Result { + return reset.invoke() + } + + override fun destroy() { + return destroy.invoke() + } + + suspend fun emit(items: List) { + this.items.emit(items) + } +} diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/CreateDmConfirmationBottomSheet.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/CreateDmConfirmationBottomSheet.kt index dca173d780..7e0e51050a 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/CreateDmConfirmationBottomSheet.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/CreateDmConfirmationBottomSheet.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.matrix.ui.components +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -22,9 +23,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule +import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule +import io.element.android.libraries.designsystem.components.BigIcon import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarType @@ -33,6 +38,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.IconSource import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet +import io.element.android.libraries.designsystem.theme.components.OutlinedButton import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.R @@ -48,10 +54,23 @@ import io.element.android.libraries.ui.strings.CommonStrings @Composable fun CreateDmConfirmationBottomSheet( matrixUser: MatrixUser, + enableKeyShareOnInvite: Boolean, + isUserIdentityUnknown: Boolean, onSendInvite: () -> Unit, onDismiss: () -> Unit, modifier: Modifier = Modifier, ) { + val titleContent = if (enableKeyShareOnInvite && isUserIdentityUnknown) { + stringResource(R.string.screen_bottom_sheet_create_dm_unknown_user_title) + } else { + stringResource(R.string.screen_bottom_sheet_create_dm_title) + } + val descriptionContent = if (enableKeyShareOnInvite && isUserIdentityUnknown) { + stringResource(R.string.screen_bottom_sheet_create_dm_unknown_user_content) + } else { + stringResource(R.string.screen_bottom_sheet_create_dm_message, matrixUser.getFullName()) + } + ModalBottomSheet( modifier = modifier, onDismissRequest = onDismiss, @@ -63,47 +82,95 @@ fun CreateDmConfirmationBottomSheet( .padding(top = 24.dp, bottom = 16.dp, start = 16.dp, end = 16.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { - Avatar( - avatarData = matrixUser.getAvatarData(AvatarSize.DmCreationConfirmation), - avatarType = AvatarType.User, - ) - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = stringResource(R.string.screen_bottom_sheet_create_dm_title), - style = ElementTheme.typography.fontHeadingMdBold, - color = ElementTheme.colors.textPrimary, - textAlign = TextAlign.Center, - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = stringResource(R.string.screen_bottom_sheet_create_dm_message, matrixUser.getFullName()), - style = ElementTheme.typography.fontBodyMdRegular, - color = ElementTheme.colors.textSecondary, - textAlign = TextAlign.Center, - ) - Spacer(modifier = Modifier.height(40.dp)) - Button( - modifier = Modifier.fillMaxWidth(), - onClick = onSendInvite, - leadingIcon = IconSource.Vector(CompoundIcons.UserAdd()), - text = stringResource(R.string.screen_bottom_sheet_create_dm_confirmation_button_title), - ) - Spacer(modifier = Modifier.height(16.dp)) - TextButton( - modifier = Modifier.fillMaxWidth(), - onClick = onDismiss, - text = stringResource(CommonStrings.action_cancel), - ) + if (isUserIdentityUnknown) { + IconTitleSubtitleMolecule( + modifier = Modifier.padding( + bottom = 16.dp, + start = 16.dp, + end = 16.dp, + ), + title = titleContent, + subTitle = descriptionContent, + iconStyle = BigIcon.Style.Default(CompoundIcons.UserAddSolid()), + ) + MatrixUserRow(matrixUser) + Spacer(modifier = Modifier.height(32.dp)) + ButtonRowMolecule( + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + OutlinedButton( + modifier = Modifier.weight(1f), + text = stringResource(CommonStrings.action_cancel), + onClick = onDismiss + ) + Button( + modifier = Modifier.weight(1f), + text = stringResource(CommonStrings.action_continue), + onClick = onSendInvite + ) + } + Spacer(modifier = Modifier.height(32.dp)) + } else { + Avatar( + avatarData = matrixUser.getAvatarData(AvatarSize.DmCreationConfirmation), + avatarType = AvatarType.User, + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = titleContent, + style = ElementTheme.typography.fontHeadingMdBold, + color = ElementTheme.colors.textPrimary, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = descriptionContent, + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textSecondary, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(40.dp)) + Button( + modifier = Modifier.fillMaxWidth(), + onClick = onSendInvite, + leadingIcon = IconSource.Vector(CompoundIcons.UserAdd()), + text = stringResource(R.string.screen_bottom_sheet_create_dm_confirmation_button_title), + ) + Spacer(modifier = Modifier.height(16.dp)) + TextButton( + modifier = Modifier.fillMaxWidth(), + onClick = onDismiss, + text = stringResource(CommonStrings.action_cancel), + ) + } } } } @PreviewsDayNight @Composable -internal fun CreateDmConfirmationBottomSheetPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) = ElementPreview { +internal fun CreateDmConfirmationBottomSheetPreview(@PreviewParameter( + CreateDmConfirmationBottomSheetStateProvider::class +) state: CreateDmConfirmationBottomSheetState) = ElementPreview { CreateDmConfirmationBottomSheet( - matrixUser = matrixUser, + matrixUser = state.matrixUser, + enableKeyShareOnInvite = state.enableKeyShareOnInvite, + isUserIdentityUnknown = state.isUserIdentityUnknown, onSendInvite = {}, onDismiss = {}, ) } + +data class CreateDmConfirmationBottomSheetState( + val matrixUser: MatrixUser, + val enableKeyShareOnInvite: Boolean, + val isUserIdentityUnknown: Boolean, +) + +class CreateDmConfirmationBottomSheetStateProvider : PreviewParameterProvider { + override val values = sequenceOf( + CreateDmConfirmationBottomSheetState(matrixUser = aMatrixUser(), enableKeyShareOnInvite = false, isUserIdentityUnknown = false), + CreateDmConfirmationBottomSheetState(matrixUser = aMatrixUser(), enableKeyShareOnInvite = true, isUserIdentityUnknown = false), + CreateDmConfirmationBottomSheetState(matrixUser = aMatrixUser(), enableKeyShareOnInvite = true, isUserIdentityUnknown = true), + ) +} diff --git a/libraries/matrixui/src/main/res/values-ja/translations.xml b/libraries/matrixui/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..ce2b24041c --- /dev/null +++ b/libraries/matrixui/src/main/res/values-ja/translations.xml @@ -0,0 +1,9 @@ + + + "招待を送信" + "%1$s とチャットを始めますか?" + "招待を送信しますか?" + "この人物とのチャットがありません。続行する前に、まず招待してください。" + "この新しい連絡先と新規にチャットを開始しますか?" + "%1$s (%2$s) があなたを招待しました" + diff --git a/libraries/matrixui/src/main/res/values-vi/translations.xml b/libraries/matrixui/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..638996cb06 --- /dev/null +++ b/libraries/matrixui/src/main/res/values-vi/translations.xml @@ -0,0 +1,4 @@ + + + "%1$s(%2$s ) đã mời bạn" + diff --git a/libraries/matrixui/src/main/res/values/localazy.xml b/libraries/matrixui/src/main/res/values/localazy.xml index b27021ebd6..c4000519a5 100644 --- a/libraries/matrixui/src/main/res/values/localazy.xml +++ b/libraries/matrixui/src/main/res/values/localazy.xml @@ -3,5 +3,7 @@ "Send invite" "Would you like to start a chat with %1$s?" "Send invite?" + "You currently don’t have any chats with this person. Confirm inviting them before continuing." + "Start a chat with this new contact?" "%1$s (%2$s) invited you" diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt index 2b1883619e..2ab1cd0619 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt @@ -195,18 +195,16 @@ class AndroidMediaPreProcessor( file = file, mimeType = mimeType, ) - val imageInfo = contentResolver.openInputStream(uri).use { input -> - val bitmap = BitmapFactory.decodeStream(input, null, null)!! - ImageInfo( - width = bitmap.width.toLong(), - height = bitmap.height.toLong(), - mimetype = mimeType, - size = file.length(), - thumbnailInfo = thumbnailResult?.info, - thumbnailSource = null, - blurhash = thumbnailResult?.blurhash, - ) - } + val (width, height) = extractOrientedImageDimensions(file) + val imageInfo = ImageInfo( + width = width, + height = height, + mimetype = mimeType, + size = file.length(), + thumbnailInfo = thumbnailResult?.info, + thumbnailSource = null, + blurhash = thumbnailResult?.blurhash, + ) removeSensitiveImageMetadata(file) return MediaUploadInfo.Image( file = file, @@ -354,6 +352,23 @@ class AndroidMediaPreProcessor( return contentResolver.openInputStream(uri)?.use { createTmpFileWithInput(it) } ?: error("Could not copy the contents of $uri to a temporary file") } + + private fun extractOrientedImageDimensions(file: File): Pair { + val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } + BitmapFactory.decodeFile(file.path, options) + + val rawWidth = options.outWidth.toLong() + val rawHeight = options.outHeight.toLong() + val orientation = tryOrNull { + ExifInterface(file).getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED) + } ?: ExifInterface.ORIENTATION_UNDEFINED + + return orientedImageDimensions( + rawWidth = rawWidth, + rawHeight = rawHeight, + orientation = orientation, + ) + } } private fun ImageCompressionResult.toImageInfo(mimeType: String, thumbnailResult: ThumbnailResult?) = ImageInfo( @@ -371,3 +386,18 @@ private fun MediaMetadataRetriever.extractDuration(): Duration { val durationInMs = extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0L return durationInMs.milliseconds } + +internal fun orientedImageDimensions(rawWidth: Long, rawHeight: Long, orientation: Int): Pair { + return if (orientation.rotatesRightAngle()) { + rawHeight to rawWidth + } else { + rawWidth to rawHeight + } +} + +private fun Int.rotatesRightAngle(): Boolean { + return this == ExifInterface.ORIENTATION_ROTATE_90 || + this == ExifInterface.ORIENTATION_ROTATE_270 || + this == ExifInterface.ORIENTATION_TRANSPOSE || + this == ExifInterface.ORIENTATION_TRANSVERSE +} diff --git a/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessorTest.kt b/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessorTest.kt index f4b4e7d4a5..57726ac5a2 100644 --- a/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessorTest.kt +++ b/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessorTest.kt @@ -12,6 +12,7 @@ import android.content.Context import android.net.Uri import android.os.Build import androidx.core.net.toUri +import androidx.exifinterface.media.ExifInterface import androidx.test.platform.app.InstrumentationRegistry import com.google.common.truth.Truth.assertThat import io.element.android.libraries.androidutils.file.TemporaryUriDeleter @@ -42,6 +43,30 @@ import kotlin.time.Duration @RunWith(RobolectricTestRunner::class) class AndroidMediaPreProcessorTest { + @Test + fun `orientedImageDimensions swaps width and height for 90 degree exif orientation`() { + val (width, height) = orientedImageDimensions( + rawWidth = 4032, + rawHeight = 2268, + orientation = ExifInterface.ORIENTATION_ROTATE_90, + ) + + assertThat(width).isEqualTo(2268) + assertThat(height).isEqualTo(4032) + } + + @Test + fun `orientedImageDimensions keeps width and height for upright exif orientation`() { + val (width, height) = orientedImageDimensions( + rawWidth = 4032, + rawHeight = 2268, + orientation = ExifInterface.ORIENTATION_NORMAL, + ) + + assertThat(width).isEqualTo(4032) + assertThat(height).isEqualTo(2268) + } + private suspend fun TestScope.process( asset: Asset, mediaOptimizationConfig: MediaOptimizationConfig, diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/image/MediaImageView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/image/MediaImageView.kt index 306a18b4d6..4c73dd215d 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/image/MediaImageView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/image/MediaImageView.kt @@ -23,7 +23,7 @@ import io.element.android.libraries.mediaviewer.api.local.LocalMedia import io.element.android.libraries.mediaviewer.impl.local.LocalMediaViewState import io.element.android.libraries.mediaviewer.impl.local.rememberLocalMediaViewState import io.element.android.libraries.ui.strings.CommonStrings -import me.saket.telephoto.zoomable.coil.ZoomableAsyncImage +import me.saket.telephoto.zoomable.coil3.ZoomableAsyncImage import me.saket.telephoto.zoomable.rememberZoomableImageState @Composable diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerStateProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerStateProvider.kt index 85aecc41a8..b1a2bde350 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerStateProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerStateProvider.kt @@ -29,6 +29,14 @@ import io.element.android.libraries.mediaviewer.impl.details.aMediaDeleteConfirm import io.element.android.libraries.mediaviewer.impl.details.aMediaDetailsBottomSheetState import kotlinx.collections.immutable.toImmutableList +private const val LONG_CAPTION = "This is a very long caption that should be scrollable in the media viewer. " + + "It contains multiple lines of text to demonstrate the scrolling behavior. " + + "Line 1: Lorem ipsum dolor sit amet, consectetur adipiscing elit. " + + "Line 2: Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. " + + "Line 3: Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris. " + + "Line 4: Duis aute irure dolor in reprehenderit in voluptate velit esse cillum. " + + "Line 5: Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia." + open class MediaViewerStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( @@ -170,6 +178,22 @@ open class MediaViewerStateProvider : PreviewParameterProvider ) ) ), + anImageMediaInfo( + senderName = "Alice", + dateSent = "21 NOV, 2024", + caption = LONG_CAPTION, + ).let { + aMediaViewerState( + listOf( + aMediaViewerPageData( + downloadedMedia = AsyncData.Success( + LocalMedia(Uri.EMPTY, it) + ), + mediaInfo = it, + ) + ) + ) + }, ) } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt index 7439909330..95ce7c631f 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt @@ -17,18 +17,24 @@ import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -39,14 +45,17 @@ import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -69,6 +78,7 @@ import io.element.android.libraries.designsystem.theme.components.IconButton 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.hasCompactHeightWindowSize import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState import io.element.android.libraries.matrix.api.media.MediaSource @@ -102,8 +112,9 @@ fun MediaViewerView( val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) var showOverlay by remember { mutableStateOf(true) } - val defaultBottomPaddingInPixels = if (LocalInspectionMode.current) 303 else 0 val currentData = state.listData.getOrNull(state.currentIndex) + val defaultBottomPaddingInPixels = if (LocalInspectionMode.current && !hasCompactHeightWindowSize()) 303 else 0 + BackHandler { onBackClick() } Scaffold( modifier, @@ -153,10 +164,11 @@ fun MediaViewerView( // So we need to update this value only when the `settledPage` value changes. It seems like a bug that needs to be fixed in Compose. page == pagerState.settledPage } + val navigationBarPadding = WindowInsets.navigationBars.getBottom(LocalDensity.current) MediaViewerPage( isDisplayed = isDisplayed, showOverlay = showOverlay, - bottomPaddingInPixels = bottomPaddingInPixels, + bottomPaddingInPixels = (bottomPaddingInPixels - navigationBarPadding).coerceAtLeast(0), data = dataForPage, textFileViewer = textFileViewer, onDismiss = onBackClick, @@ -175,9 +187,7 @@ fun MediaViewerView( // Bottom bar AnimatedVisibility(visible = showOverlay, enter = fadeIn(), exit = fadeOut()) { Box( - modifier = Modifier - .fillMaxSize() - .navigationBarsPadding() + modifier = Modifier.fillMaxSize() ) { MediaViewerBottomBar( modifier = Modifier.align(Alignment.BottomCenter), @@ -538,19 +548,46 @@ private fun MediaViewerBottomBar( if (showDivider) { HorizontalDivider() } - Text( + val scrollState = rememberScrollState() + val showBottomShadow by remember { derivedStateOf { scrollState.value < scrollState.maxValue } } + Box( modifier = Modifier .fillMaxWidth() - .padding(16.dp), - text = caption, - maxLines = 5, - overflow = TextOverflow.Ellipsis, - style = ElementTheme.typography.fontBodyLgRegular, - ) + .heightIn(max = if (hasCompactHeightWindowSize()) maxCaptionHeightLandscape else maxCaptionHeightPortrait), + ) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + .verticalScroll(scrollState) + .navigationBarsPadding(), + text = caption, + style = ElementTheme.typography.fontBodyLgRegular, + ) + if (showBottomShadow) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + .align(Alignment.BottomCenter) + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color.Transparent, + bgCanvasWithTransparency, + ), + ), + ), + ) + } + } } } } +private val maxCaptionHeightPortrait = 200.dp +private val maxCaptionHeightLandscape = 128.dp + @Composable private fun ThumbnailView( thumbnailSource: MediaSource?, @@ -604,3 +641,14 @@ internal fun MediaViewerViewPreview(@PreviewParameter(MediaViewerStateProvider:: onBackClick = {}, ) } + +@Preview(device = "${Devices.PHONE}, orientation=landscape") +@Composable +internal fun MediaViewerViewLandscapePreview(@PreviewParameter(MediaViewerStateProvider::class) state: MediaViewerState) = ElementPreviewDark { + MediaViewerView( + state = state, + audioFocus = null, + textFileViewer = { _, _ -> }, + onBackClick = {}, + ) +} diff --git a/libraries/mediaviewer/impl/src/main/res/values-ja/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..cb4da33df8 --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,21 @@ + + + "このファイルはルームから削除され、他のユーザーは確認することができなくなります。" + "ファイルを削除しますか?" + "インターネット接続を確認した上、再度お試しください。" + "このルームに投稿された文書ファイルや音声ファイル・メッセージはここに表示されます。" + "アップロードされたファイルはありません" + "ファイルを読み込み中…" + "メディアを読み込み中…" + "ファイル" + "メディア" + "このルームに投稿された画像と動画はここに表示されます。" + "アップロードされたメディアはありません" + "ファイルとメディア" + "ファイル形式" + "ファイル名" + "これ以上ファイルはありません" + "これ以上メディアはありません" + "アップロード元" + "アップロード先" + diff --git a/libraries/permissions/api/src/main/res/values-ja/translations.xml b/libraries/permissions/api/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..1749b855a3 --- /dev/null +++ b/libraries/permissions/api/src/main/res/values-ja/translations.xml @@ -0,0 +1,7 @@ + + + "カメラを使用するには、本体の設定から権限を付与する必要があります。" + "本体の設定から権限を付与してください。" + "マイクを使用するには、本体の設定から権限を付与してください。" + "通知を表示するには、本体の設定から権限を付与してください。" + diff --git a/libraries/permissions/api/src/main/res/values-vi/translations.xml b/libraries/permissions/api/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..c575d3b3bb --- /dev/null +++ b/libraries/permissions/api/src/main/res/values-vi/translations.xml @@ -0,0 +1,7 @@ + + + "Để ứng dụng sử dụng camera, vui lòng cấp quyền trong cài đặt hệ thống." + "Vui lòng cấp quyền trong cài đặt hệ thống." + "Để ứng dụng có thể sử dụng micro, vui lòng cấp quyền trong cài đặt hệ thống." + "Để ứng dụng hiển thị thông báo, vui lòng cấp quyền trong cài đặt hệ thống." + diff --git a/libraries/permissions/impl/src/main/res/values-ja/translations.xml b/libraries/permissions/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..c29d26fd69 --- /dev/null +++ b/libraries/permissions/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,5 @@ + + + "アプリケーションが通知を表示できることを確認してください。" + "権限の確認" + diff --git a/libraries/permissions/impl/src/main/res/values-vi/translations.xml b/libraries/permissions/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..0f960c909e --- /dev/null +++ b/libraries/permissions/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,5 @@ + + + "Kiểm tra xem ứng dụng có hiển thị thông báo hay không." + "Kiểm tra quyền truy cập" + diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt index cf76b26e64..64583bd1d4 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt @@ -302,7 +302,6 @@ class DefaultNotifiableEventResolver( NotificationContent.StateEvent.PolicyRuleRoom, NotificationContent.StateEvent.PolicyRuleServer, NotificationContent.StateEvent.PolicyRuleUser, - NotificationContent.StateEvent.RoomAliases, NotificationContent.StateEvent.RoomAvatar, NotificationContent.StateEvent.RoomCanonicalAlias, NotificationContent.StateEvent.RoomCreate, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/FetchPushForegroundService.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/FetchPushForegroundService.kt index da5dc80707..d54b7f5497 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/FetchPushForegroundService.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/FetchPushForegroundService.kt @@ -106,7 +106,7 @@ class FetchPushForegroundService : Service() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) { coroutineScope.launch { delay(wakelockTimeout) - onTimeout(startId) + onTimeoutAction(calledByTheSystem = false) } } @@ -124,7 +124,11 @@ class FetchPushForegroundService : Service() { override fun onTimeout(startId: Int) { super.onTimeout(startId) + onTimeoutAction(calledByTheSystem = true) + } + private fun onTimeoutAction(calledByTheSystem: Boolean) { + Timber.d("onTimeoutAction, calledByTheSystem: $calledByTheSystem, isOnForeground: $isOnForeground") if (isOnForeground) { Timber.d("Wakelock timeout reached, stopping FetchPushForegroundService") coroutineScope.launch { pushHandlingWakeLock.unlock() } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/workmanager/SyncPendingNotificationsRequestBuilder.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/workmanager/SyncPendingNotificationsRequestBuilder.kt index bdb8389feb..4c1da42660 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/workmanager/SyncPendingNotificationsRequestBuilder.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/workmanager/SyncPendingNotificationsRequestBuilder.kt @@ -76,7 +76,6 @@ class DefaultSyncPendingNotificationsRequestBuilder( .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) // If we're in an air-gapped environment, we shouldn't validate internet connectivity, as the checker will fail and the worker won't run at all. - // Note this will always be false for FOSS, since the feature is only enabled in Element Pro. if (networkMonitor.isInAirGappedEnvironment.first()) { Timber.d("In an air-gapped environment, not adding NET_CAPABILITY_VALIDATED to the network request") networkRequestBuilder.removeCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) diff --git a/libraries/push/impl/src/main/res/values-it/translations.xml b/libraries/push/impl/src/main/res/values-it/translations.xml index 9e44c2d7f9..6ed91c3b6b 100644 --- a/libraries/push/impl/src/main/res/values-it/translations.xml +++ b/libraries/push/impl/src/main/res/values-it/translations.xml @@ -15,6 +15,11 @@ "Non è stato possibile registrare il distributore di notifiche UnifiedPush, quindi non riceverai più notifiche. Controlla le impostazioni delle notifiche dell\'app e lo stato del distributore push." "Hai nuovi messaggi." + + "Hai %d nuovo messaggio." + "Hai %d nuovi messaggi." + + "📞 Chiamata in arrivo" "📹 Chiamata in arrivo" "** Invio fallito - si prega di aprire la stanza" "Entra" @@ -38,6 +43,8 @@ "%1$s ti ha invitato a unirti alla stanza" "Io" "%1$s ti ha menzionato o risposto" + "Ti abbiamo invitato a unirti allo spazio" + "%1$s ti ha invitato a unirti allo spazio" "Stai visualizzando la notifica! Cliccami!" "Discussione in %1$s" "%1$s: %2$s" diff --git a/libraries/push/impl/src/main/res/values-ja/translations.xml b/libraries/push/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..f115a54042 --- /dev/null +++ b/libraries/push/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,93 @@ + + + "通話" + "イベントを監視中" + "通常の通知" + "着信" + "サイレント通知" + + "%1$s: %2$d件のメッセージ" + + + "%d 件の通知" + + "Unified Push の通知配信サービス (notification distributor) を登録できないため、通知を受け取ることができません。通知の設定と通知ディストリビューター (push distributor) の状況を確認してください。" + "新着メッセージがあります。" + + "新着のメッセージが%d 件あります。" + + "📞 着信" + "📹 通話着信" + "** 送信失敗 - ルームを開いてください" + "参加" + "拒否" + + "%d 件の招待" + + "チャットにあなたを招待しました" + "%1$sがあなたをチャットに招待しました" + "%1$s があなたをメンションしました" + "新着メッセージ" + + "%d 件の新着メッセージ" + + "%1$sへのリアクション" + "既読にする" + "クイック返信" + "ルームに招待されました" + "%1$s があなたをルームに招待しました" + "自分" + "%1$s がメンションまたは返信しました" + "スペースに招待されました" + "%1$s があなたをスペースに招待しました" + "通知を表示しています。タップしてください。" + "%1$sのスレッド" + "%1$s: %2$s" + "%1$s: %2$s %3$s" + + "%d件の未読メッセージ" + + "%1$sと%2$s" + "%2$sに%1$s" + "%2$sに%1$sと%3$s" + + "%d 個のルーム" + + "バックグラウンド同期" + "Google サービス" + "有効なGoogle Play 開発者サービスがありません。通知が正しく機能しない可能性があります。" + "ブロックしたユーザーを確認中" + "ブロックしたユーザーを表示" + "ブロックしたユーザーはいません。" + + "%1$d 人のユーザーをブロックしました。以降の通知を受信しません。" + + "ブロックしたユーザー" + "現在のプロバイダーの名前を取得してください。" + "プッシュ通知プロバイダーが選択されていません。" + "現在のプッシュ通知プロバイダーは %1$s で、現在のプッシュ通知ディストリビューター は %2$s です。しかし、ディストリビューター %3$s は見つかりませんでした。アンインストールされている可能性があります。" + "現在のプッシュ通知プロバイダーは %1$s ですが、ディストリビューターが設定されていません。" + "現在のプッシュ通知プロバイダー: %1$s" + "現在のプッシュ通知プロバイダー: %1$s (%2$s)" + "現在のプッシュ通知プロバイダー" + "少なくとも一つ以上のプッシュ通知プロバイダーに、アプリケーションが対応していることを確認してください。" + "対応しているプッシュ通知プロバイダーが見つかりませんでした。" + + "%1$d 個の通知プロバイダーを発見: %2$s" + + "このアプリケーションは %1$s に対応しています。" + "プッシュ通知プロバイダーへの対応状況" + "アプリケーションが通知を表示できることを確認してください。" + "通知がタップされていません。" + "通知を表示できません。" + "通知がタップされました。" + "通知の表示" + "テストを続行するには、通知にタップしてください。" + "プッシュ通知をアプリケーションが受信していることを確認してください。" + "エラー: プッシュ通知プロバイダーがリクエストを拒否しました。" + "エラー: %1$s" + "エラー: プッシュ通知をテストできません。" + "エラー: 通知の待機がタイムアウトしました。" + "プッシュ通知のループバックに %1$d ms 要しました。" + "プッシュ通知のループバックをテスト" + diff --git a/libraries/push/impl/src/main/res/values-ko/translations.xml b/libraries/push/impl/src/main/res/values-ko/translations.xml index 3aa015392f..f4399cda2b 100644 --- a/libraries/push/impl/src/main/res/values-ko/translations.xml +++ b/libraries/push/impl/src/main/res/values-ko/translations.xml @@ -16,6 +16,7 @@ "%d개의 새 메시지가 있습니다." + "📞 수신 전화" "📹 수신 전화" "** 전송 실패 - 방을 열여주세요" "참가하기" diff --git a/libraries/push/impl/src/main/res/values-ru/translations.xml b/libraries/push/impl/src/main/res/values-ru/translations.xml index 24b1412500..c0b597588b 100644 --- a/libraries/push/impl/src/main/res/values-ru/translations.xml +++ b/libraries/push/impl/src/main/res/values-ru/translations.xml @@ -51,7 +51,7 @@ "Пригласил(а) вас в пространство" "%1$s пригласил(а) вас в пространство" "Вы просматриваете уведомление! Нажмите на меня!" - "Ветка в %1$s" + "Обсуждение в %1$s" "%1$s: %2$s" "%1$s: %2$s %3$s" diff --git a/libraries/push/impl/src/main/res/values-vi/translations.xml b/libraries/push/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..5592ff0e3a --- /dev/null +++ b/libraries/push/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,61 @@ + + + "Gọi" + "Đang lắng nghe sự kiện" + "Thông báo ồn ào" + "Cuộc gọi đang đổ chuông" + "Thông báo im lặng" + + "%1$s:%2$d tin nhắn" + + + "%d thông báo" + + "Bạn có tin nhắn mới." + "📹 Cuộc gọi đến" + "** Không gửi được - vui lòng mở phòng" + "Tham gia" + "Từ chối" + + "%d lời mời" + + "Đã mời bạn trò chuyện" + "Đã nhắc đến bạn: %1$s" + "Tin nhắn mới" + + "%dtin nhắn mới" + + "Đã thả %1$s vào tin nhắn" + "Đánh dấu đã đọc" + "Trả lời nhanh" + "Đã mời bạn tham gia phòng" + "Tôi" + "Bạn đang xem thông báo! Bấm vào đây!" + "%1$s:%2$s" + "%1$s: %2$s %3$s" + + "%dtin nhắn chưa đọc đã thông báo" + + "%1$s và %2$s" + "%1$s in %2$s" + "%1$s trong %2$s và %3$s" + + "%d phòng" + + "Đồng bộ hóa trong nền" + "Dịch vụ của Google" + "Không tìm thấy Dịch vụ Google Play hợp lệ. Thông báo có thể không hoạt động đúng cách." + "Người dùng bị chặn" + "Hãy đảm bảo rằng ứng dụng hỗ trợ ít nhất một nhà cung cấp thông báo đẩy." + "Không tìm thấy hỗ trợ từ nhà cung cấp thông báo đẩy." + + "Đã tìm thấy %1$d nhà cung cấp thông báo đẩy: %2$s" + + "Hỗ trợ nhà cung cấp thông báo đẩy" + "Kiểm tra xem ứng dụng có thể hiển thị thông báo hay không." + "Thông báo chưa được nhấp vào." + "Không thể hiển thị thông báo." + "Thông báo đã được nhấp!" + "Hiển thị thông báo" + "Hãy nhấp vào thông báo để tiếp tục thử nghiệm." + diff --git a/libraries/push/impl/src/main/res/values-zh/translations.xml b/libraries/push/impl/src/main/res/values-zh/translations.xml index 9a9057b07b..7e59c317c3 100644 --- a/libraries/push/impl/src/main/res/values-zh/translations.xml +++ b/libraries/push/impl/src/main/res/values-zh/translations.xml @@ -16,6 +16,7 @@ "您有 %d 条新消息。" + "📞 来电" "📹 来电" "** 无法发送——请打开聊天室" "加入" @@ -57,7 +58,7 @@ "找不到有效的 Google Play 服务。通知可能无法正常工作。" "检查被阻止的用户" "查看被屏蔽的用户" - "没有用户被阻止。" + "未屏蔽任何用户。" "您已屏蔽 %1$d 位用户。您将不再收到这些用户的推送通知。" @@ -87,6 +88,6 @@ "错误:%1$s。" "错误,无法测试推送。" "错误,等待推送超时。" - "推送回路耗时%1$d 毫秒。" + "推送回路耗时 %1$d 毫秒。" "测试推送回路" diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt index 63b903a3f7..33634e1767 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt @@ -56,7 +56,6 @@ import io.element.android.libraries.push.impl.notifications.model.FallbackNotifi import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent -import io.element.android.libraries.push.test.notifications.FakeCallNotificationEventResolver import io.element.android.services.toolbox.impl.strings.AndroidStringProvider import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP import io.element.android.services.toolbox.test.systemclock.FakeSystemClock @@ -835,7 +834,6 @@ class DefaultNotifiableEventResolverTest { testNoResults(NotificationContent.StateEvent.PolicyRuleRoom) testNoResults(NotificationContent.StateEvent.PolicyRuleServer) testNoResults(NotificationContent.StateEvent.PolicyRuleUser) - testNoResults(NotificationContent.StateEvent.RoomAliases) testNoResults(NotificationContent.StateEvent.RoomAvatar) testNoResults(NotificationContent.StateEvent.RoomCanonicalAlias) testNoResults(NotificationContent.StateEvent.RoomCreate) diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandlerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandlerTest.kt index 9bba1c32d3..e9f6b76b7a 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandlerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandlerTest.kt @@ -18,7 +18,6 @@ import io.element.android.libraries.matrix.test.notification.FakeNotificationSer import io.element.android.libraries.matrix.test.notification.aNotificationData import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDataFactory import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent -import io.element.android.libraries.push.test.notifications.FakeCallNotificationEventResolver import io.element.android.tests.testutils.lambda.lambdaRecorder import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent diff --git a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeCallNotificationEventResolver.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/FakeCallNotificationEventResolver.kt similarity index 87% rename from libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeCallNotificationEventResolver.kt rename to libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/FakeCallNotificationEventResolver.kt index f923d0c9fe..a1049683f5 100644 --- a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeCallNotificationEventResolver.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/FakeCallNotificationEventResolver.kt @@ -6,11 +6,10 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.push.test.notifications +package io.element.android.libraries.push.impl.notifications import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.notification.NotificationData -import io.element.android.libraries.push.impl.notifications.CallNotificationEventResolver import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.tests.testutils.lambda.lambdaError diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt index cc6e4674f9..a16568d400 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt @@ -26,8 +26,8 @@ import io.element.android.libraries.push.impl.history.PushHistoryService import io.element.android.libraries.push.impl.notifications.FakeNotificationResultProcessor import io.element.android.libraries.push.impl.test.DefaultTestPush import io.element.android.libraries.push.impl.troubleshoot.DiagnosticPushHandler +import io.element.android.libraries.push.impl.workmanager.FakeSyncPendingNotificationsRequestBuilder import io.element.android.libraries.push.impl.workmanager.SyncPendingNotificationsRequestBuilder -import io.element.android.libraries.push.test.workmanager.FakeSyncPendingNotificationsRequestBuilder import io.element.android.libraries.pushproviders.api.PushData import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStore diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/DefaultSyncPendingNotificationsRequestBuilderTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/DefaultSyncPendingNotificationsRequestBuilderTest.kt index 796d5d192f..e65bea20a9 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/DefaultSyncPendingNotificationsRequestBuilderTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/DefaultSyncPendingNotificationsRequestBuilderTest.kt @@ -80,6 +80,9 @@ class DefaultSyncPendingNotificationsRequestBuilderTest { sessionId = A_SESSION_ID, sdkVersion = 33, isInAirGapEnvironment = false, + featureFlagService = FakeFeatureFlagService(initialState = mapOf( + FeatureFlags.ValidateNetworkWhenSchedulingNotificationFetching.key to true + )), ) val results = request.build() @@ -100,6 +103,9 @@ class DefaultSyncPendingNotificationsRequestBuilderTest { sessionId = A_SESSION_ID, sdkVersion = 33, isInAirGapEnvironment = true, + featureFlagService = FakeFeatureFlagService(initialState = mapOf( + FeatureFlags.ValidateNetworkWhenSchedulingNotificationFetching.key to true + )), ) val results = request.build() diff --git a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/workmanager/FakeSyncPendingNotificationsRequestBuilder.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/FakeSyncPendingNotificationsRequestBuilder.kt similarity index 78% rename from libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/workmanager/FakeSyncPendingNotificationsRequestBuilder.kt rename to libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/FakeSyncPendingNotificationsRequestBuilder.kt index ef0e38991e..f2da936b87 100644 --- a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/workmanager/FakeSyncPendingNotificationsRequestBuilder.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/FakeSyncPendingNotificationsRequestBuilder.kt @@ -5,9 +5,8 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.push.test.workmanager +package io.element.android.libraries.push.impl.workmanager -import io.element.android.libraries.push.impl.workmanager.SyncPendingNotificationsRequestBuilder import io.element.android.libraries.workmanager.api.WorkManagerRequestWrapper class FakeSyncPendingNotificationsRequestBuilder( diff --git a/libraries/push/test/build.gradle.kts b/libraries/push/test/build.gradle.kts index 475d4a4ae5..9dedeb3996 100644 --- a/libraries/push/test/build.gradle.kts +++ b/libraries/push/test/build.gradle.kts @@ -18,7 +18,6 @@ dependencies { api(projects.libraries.push.api) api(projects.libraries.pushproviders.api) implementation(projects.libraries.designsystem) - implementation(projects.libraries.push.impl) implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixui) implementation(projects.libraries.workmanager.api) diff --git a/libraries/pushproviders/firebase/src/main/res/values-ja/translations.xml b/libraries/pushproviders/firebase/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..5c07e2ad79 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/res/values-ja/translations.xml @@ -0,0 +1,11 @@ + + + "Firebase が利用可能であることを確認してください。" + "Firebase を利用できません。" + "Firebase は利用可能です。" + "Firebase の確認" + "Firebase トークンが利用可能であることを確認してください。" + "Firebase トークンが不明です。" + "Firebase トークン: %1$s" + "Firebase トークンの確認" + diff --git a/libraries/pushproviders/unifiedpush/src/main/res/values-ja/translations.xml b/libraries/pushproviders/unifiedpush/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..a081ba04ff --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/res/values-ja/translations.xml @@ -0,0 +1,9 @@ + + + "UnifiedPush のディストリビューターが利用可能であることを確認してください。" + "プッシュ通知ディストリビューターが見つかりませんでした。" + + "%1$d 個のディストリビューターを発見: %2$s" + + "UnifiedPush を確認" + diff --git a/libraries/slashcommands/api/src/main/kotlin/io/element/android/libraries/slashcommands/api/SlashCommand.kt b/libraries/slashcommands/api/src/main/kotlin/io/element/android/libraries/slashcommands/api/SlashCommand.kt index 770543e548..50d5a5ce32 100644 --- a/libraries/slashcommands/api/src/main/kotlin/io/element/android/libraries/slashcommands/api/SlashCommand.kt +++ b/libraries/slashcommands/api/src/main/kotlin/io/element/android/libraries/slashcommands/api/SlashCommand.kt @@ -51,6 +51,7 @@ sealed interface SlashCommand { data class ChangeDisplayName(val displayName: String) : SlashCommandAdmin data class ChangeDisplayNameForRoom(val displayName: String) : SlashCommandAdmin data class ChangeRoomAvatar(val url: String) : SlashCommandAdmin + data class ChangeAvatar(val url: String) : SlashCommandAdmin data class ChangeAvatarForRoom(val url: String) : SlashCommandAdmin data class SendSpoiler(val message: String) : SlashCommandSendMessage data class SendWithPrefix(val prefix: MessagePrefix, val message: CharSequence) : SlashCommandSendMessage diff --git a/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/Command.kt b/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/Command.kt index 0b7b58a15f..0d9e1e8c72 100644 --- a/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/Command.kt +++ b/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/Command.kt @@ -120,6 +120,15 @@ enum class Command( isDevCommand = true, isSupported = false, ), + CHANGE_AVATAR( + command = "/myavatar", + parameters = "", + description = R.string.slash_command_description_avatar, + isAllowedInThread = false, + // Dev command since user has to know the mxc url + isDevCommand = true, + isSupported = false, + ), CHANGE_AVATAR_FOR_ROOM( command = "/myroomavatar", parameters = "", diff --git a/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/CommandExecutor.kt b/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/CommandExecutor.kt index 0acd3af6f8..ad252cb224 100644 --- a/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/CommandExecutor.kt +++ b/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/CommandExecutor.kt @@ -44,6 +44,7 @@ class CommandExecutor( ): Result { return when (slashCommand) { is SlashCommand.BanUser -> banUser(slashCommand) + is SlashCommand.ChangeAvatar -> changeAvatar() is SlashCommand.ChangeAvatarForRoom -> changeAvatarForRoom() is SlashCommand.ChangeDisplayName -> changeDisplayName(slashCommand) is SlashCommand.ChangeDisplayNameForRoom -> changeDisplayNameForRoom() @@ -178,6 +179,10 @@ class CommandExecutor( return matrixClient.setDisplayName(slashCommand.displayName) } + private fun changeAvatar(): Result { + return Result.failure(Exception("Not yet implemented")) + } + private fun changeAvatarForRoom(): Result { return Result.failure(Exception("Not yet implemented")) } diff --git a/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/CommandParser.kt b/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/CommandParser.kt index 85a045f50c..55125af20b 100644 --- a/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/CommandParser.kt +++ b/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/CommandParser.kt @@ -107,6 +107,18 @@ class CommandParser( syntaxError(Command.ROOM_AVATAR) } } + Command.CHANGE_AVATAR.matches(slashCommand) -> { + if (messageParts.size == 2) { + val url = messageParts[1] + if (url.isMxcUrl()) { + SlashCommand.ChangeAvatar(url) + } else { + syntaxError(Command.CHANGE_AVATAR) + } + } else { + syntaxError(Command.CHANGE_AVATAR) + } + } Command.CHANGE_AVATAR_FOR_ROOM.matches(slashCommand) -> { if (messageParts.size == 2) { val url = messageParts[1] diff --git a/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/DefaultSlashCommandService.kt b/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/DefaultSlashCommandService.kt index ba2786c944..6cd8688cad 100644 --- a/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/DefaultSlashCommandService.kt +++ b/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/DefaultSlashCommandService.kt @@ -11,6 +11,7 @@ import dev.zacsweers.metro.ContributesBinding import io.element.android.libraries.di.RoomScope import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.matrix.api.HomeserverCapabilitiesProvider import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.slashcommands.api.SlashCommand @@ -18,6 +19,8 @@ import io.element.android.libraries.slashcommands.api.SlashCommandService import io.element.android.libraries.slashcommands.api.SlashCommandSuggestion import io.element.android.services.toolbox.api.strings.StringProvider import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withTimeoutOrNull +import kotlin.time.Duration.Companion.seconds @ContributesBinding(RoomScope::class) class DefaultSlashCommandService( @@ -26,6 +29,7 @@ class DefaultSlashCommandService( private val stringProvider: StringProvider, private val appPreferencesStore: AppPreferencesStore, private val featureFlagService: FeatureFlagService, + private val capabilitiesProvider: HomeserverCapabilitiesProvider, ) : SlashCommandService { override suspend fun getSuggestions( text: String, @@ -33,19 +37,41 @@ class DefaultSlashCommandService( ): List { if (!featureFlagService.isFeatureEnabled(FeatureFlags.SlashCommand)) return emptyList() val isDeveloperModeEnabled = appPreferencesStore.isDeveloperModeEnabledFlow().first() - return Command.entries.filter { - it.startsWith(text) - }.filter { - !isInThread || it.isAllowedInThread - }.filter { - !it.isDevCommand || isDeveloperModeEnabled - }.map { - SlashCommandSuggestion( - command = it.command, - parameters = it.parameters, - description = stringProvider.getString(it.description), - ) - } + return Command.entries + .asSequence() + .filter { it.startsWith(text) } + .filter { !isInThread || it.isAllowedInThread } + .filter { !it.isDevCommand || isDeveloperModeEnabled } + // Don't include the change display name commands if the user can't change their display name + .run { + val canUserChangeDisplayName = withTimeoutOrNull(5.seconds) { + capabilitiesProvider.canChangeDisplayName().getOrNull() + } ?: false + if (!canUserChangeDisplayName) { + filterNot { it == Command.CHANGE_DISPLAY_NAME || it == Command.CHANGE_DISPLAY_NAME_FOR_ROOM } + } else { + this + } + } + // Don't include the change avatar commands if the user can't change their avatar url + .run { + val canUserChangeAvatar = withTimeoutOrNull(5.seconds) { + capabilitiesProvider.canChangeAvatarUrl().getOrNull() + } ?: false + if (!canUserChangeAvatar) { + filterNot { it == Command.CHANGE_AVATAR || it == Command.CHANGE_AVATAR_FOR_ROOM } + } else { + this + } + } + .map { + SlashCommandSuggestion( + command = it.command, + parameters = it.parameters, + description = stringProvider.getString(it.description), + ) + } + .toList() } override suspend fun parse( diff --git a/libraries/slashcommands/impl/src/main/res/values/temporary.xml b/libraries/slashcommands/impl/src/main/res/values/temporary.xml index 0a8f2a0034..26232ea9b3 100644 --- a/libraries/slashcommands/impl/src/main/res/values/temporary.xml +++ b/libraries/slashcommands/impl/src/main/res/values/temporary.xml @@ -26,6 +26,7 @@ Set the room topic Removes user with given id from this room Changes your display nickname + Changes your profile picture in all rooms Sends the given message with confetti Sends the given message with snowfall Sends a message as plain text, without interpreting it as markdown diff --git a/libraries/slashcommands/impl/src/test/kotlin/io/element/android/libraries/slashcommands/impl/CommandParserTest.kt b/libraries/slashcommands/impl/src/test/kotlin/io/element/android/libraries/slashcommands/impl/CommandParserTest.kt index 0887847a40..f5a6f54dfd 100644 --- a/libraries/slashcommands/impl/src/test/kotlin/io/element/android/libraries/slashcommands/impl/CommandParserTest.kt +++ b/libraries/slashcommands/impl/src/test/kotlin/io/element/android/libraries/slashcommands/impl/CommandParserTest.kt @@ -78,12 +78,19 @@ class CommandParserTest { } @Test - fun parseSlashCommandPlainAndNick() = runTest { + fun parseSlashCommandPlain() = runTest { test("/plain hello", SlashCommand.SendPlainText("hello")) test("/plain", SlashCommand.ErrorSyntax("A string/plain, /plain ")) + } + @Test + fun parseSlashCommandNickAndMyAvatar() = runTest { test("/nick John", SlashCommand.ChangeDisplayName("John")) test("/nick", SlashCommand.ErrorSyntax("A string/nick, /nick ")) + + test("/myavatar mxc://matrix.org/abc", SlashCommand.ChangeAvatar("mxc://matrix.org/abc")) + test("/myavatar http://notmxc", SlashCommand.ErrorSyntax("A string/myavatar, /myavatar ")) + test("/myavatar", SlashCommand.ErrorSyntax("A string/myavatar, /myavatar ")) } @Test diff --git a/libraries/slashcommands/impl/src/test/kotlin/io/element/android/libraries/slashcommands/impl/DefaultSlashCommandServiceTest.kt b/libraries/slashcommands/impl/src/test/kotlin/io/element/android/libraries/slashcommands/impl/DefaultSlashCommandServiceTest.kt index 243f25666c..cee4d17b21 100644 --- a/libraries/slashcommands/impl/src/test/kotlin/io/element/android/libraries/slashcommands/impl/DefaultSlashCommandServiceTest.kt +++ b/libraries/slashcommands/impl/src/test/kotlin/io/element/android/libraries/slashcommands/impl/DefaultSlashCommandServiceTest.kt @@ -13,6 +13,7 @@ import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.room.IntentionalMention import io.element.android.libraries.matrix.api.timeline.MsgType +import io.element.android.libraries.matrix.test.FakeHomeserverCapabilitiesProvider 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 @@ -116,6 +117,26 @@ class DefaultSlashCommandServiceTest { sendMessage.assertions().isCalledOnce() } + @Test + fun `canChangeDisplayName is respected in suggestions`() = runTest { + var result = false + val capabilitiesProvider = FakeHomeserverCapabilitiesProvider( + canChangeDisplayName = { Result.success(result) }, + ) + val sut = createDefaultSlashCommandService(capabilitiesProvider = capabilitiesProvider) + + // Initially, with a disabled capability, the change display name command should not be in the suggestions + var changeNameCommand = sut.getSuggestions("", isInThread = false) + .find { it.command == Command.CHANGE_DISPLAY_NAME.command } + assertThat(changeNameCommand).isNull() + + // When the capability is true, the command should be included in the suggestions + result = true + changeNameCommand = sut.getSuggestions("", isInThread = false) + .find { it.command == Command.CHANGE_DISPLAY_NAME.command } + assertThat(changeNameCommand).isNotNull() + } + @Test fun `proceedAdmin delegates to commandExecutor`() = runTest { val leaveRoomLambda = lambdaRecorder> { @@ -155,11 +176,13 @@ class DefaultSlashCommandServiceTest { commandExecutor: CommandExecutor = createCommandExecutor( stringProvider = stringProvider, ), + capabilitiesProvider: FakeHomeserverCapabilitiesProvider = FakeHomeserverCapabilitiesProvider(), ) = DefaultSlashCommandService( commandParser = commandParser, commandExecutor = commandExecutor, stringProvider = stringProvider, appPreferencesStore = appPreferencesStore, featureFlagService = featureFlagService, + capabilitiesProvider = capabilitiesProvider, ) } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ComposerModeView.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ComposerModeView.kt index 15aabdcbb0..08d89e25fb 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ComposerModeView.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ComposerModeView.kt @@ -134,7 +134,7 @@ private fun ReplyToModeView( modifier .clip(RoundedCornerShape(6.dp)) .background(ElementTheme.colors.bgCanvasDefault) - .border(1.dp, ElementTheme.colors.borderInteractiveSecondary, RoundedCornerShape(6.dp)) + .border(1.dp, ElementTheme.colors.separatorPrimary, RoundedCornerShape(6.dp)) .padding(4.dp) ) { InReplyToView( diff --git a/libraries/textcomposer/impl/src/main/res/values-ja/translations.xml b/libraries/textcomposer/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..6dea90b306 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,33 @@ + + + "添付ファイルを追加" + "箇条リスト" + "書式設定を中止して閉じる" + "コードブロックを切替" + "キャプションを追加" + "暗号化されたメッセージ…" + "メッセージ…" + "暗号化されていないメッセージ…" + "リンクを作成" + "リンクを編集" + "%1$s の状態: %2$s" + "太字" + "斜体" + "無効" + "オフ" + "オン" + "取り消し線を追加" + "下線を追加" + "全画面モードの切替" + "インデント" + "コード部の書式" + "リンクを設定" + "番号リスト" + "記述設定を開く" + "引用の表示切替" + "リンクを削除" + "インデントを削除" + "リンク" + "古いアプリケーションを使用しているユーザーはキャプションを見られない可能性があります。" + "長押しで録音" + diff --git a/libraries/textcomposer/impl/src/main/res/values-vi/translations.xml b/libraries/textcomposer/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..42b78d23f3 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,25 @@ + + + "Thêm tệp đính kèm" + "Chuyển đổi danh sách dấu đầu dòng" + "Hủy và đóng định dạng văn bản" + "Bật/tắt khối mã" + "Tin nhắn…" + "Tạo liên kết" + "Sửa liên kết" + "Áp dụng định dạng in đậm" + "Áp dụng định dạng in nghiêng" + "Áp dụng định dạng gạch ngang" + "Áp dụng định dạng gạch chân" + "Bật/tắt chế độ toàn màn hình" + "Thụt lề" + "Áp dụng định dạng mã trong dòng" + "Đặt liên kết" + "Chuyển đổi danh sách được đánh số" + "Mở tùy chọn soạn tin" + "Chuyển sang Trích dẫn" + "Xóa liên kết" + "Bỏ thụt lề" + "Liên kết" + "Nhấn giữ để ghi âm" + diff --git a/libraries/troubleshoot/impl/src/main/res/values-ja/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..a7357d7aae --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,12 @@ + + + "プッシュ履歴" + "テストを実行" + "再度テスト" + "一部のテストで失敗しました。詳細を確認してください。" + "テストを実行することで、不安定な通知を生じさせる設定の問題を特定できます。" + "修正を試行" + "テストは問題なく完了しました。" + "通知のトラブルシューティング" + "一部のテストはあなたの操作が必要です。詳細を確認してください。" + diff --git a/libraries/troubleshoot/impl/src/main/res/values-lt/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-lt/translations.xml new file mode 100644 index 0000000000..2befbbf398 --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/res/values-lt/translations.xml @@ -0,0 +1,5 @@ + + + "Vykdyti testus" + "Vykdyti testus dar kartą" + diff --git a/libraries/troubleshoot/impl/src/main/res/values-vi/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..977938afab --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,6 @@ + + + "Chạy thử nghiệm" + "Chạy các bài kiểm tra để phát hiện vấn đề trong cấu hình có thể khiến thông báo không hoạt động như mong đợi." + "Khắc phục sự cố thông báo" + diff --git a/libraries/ui-strings/src/main/res/values-be/translations.xml b/libraries/ui-strings/src/main/res/values-be/translations.xml index 6e2374a5aa..4011474193 100644 --- a/libraries/ui-strings/src/main/res/values-be/translations.xml +++ b/libraries/ui-strings/src/main/res/values-be/translations.xml @@ -1,5 +1,6 @@ + "Дадаць рэакцыю: %1$s" "Адрас" "Аватар" "Выдаліць" @@ -8,15 +9,21 @@ "Уведзена %1$d лічбы" "Уведзена %1$d лічб" + "Змяніць аватар" + "Поўны адрас будзе %1$s" + "Дэталі шыфравання" "Схаваць пароль" "Далучыцца да выкліку" "Перайсці ўніз" "Толькі згадкі" "Гук адключаны" + "Новыя згадкі" + "Новыя паведамленні" "Старонка %1$d" "Паўза" "Поле PIN-кода" "Прайграць" + "Хуткасць прайгравання" "Апытанне" "Апытанне скончана" "QR-код" @@ -34,11 +41,14 @@ "Адправіць файлы" "Паказаць пароль" "Пазваніць" + "Аватар карыстальніка" "Меню карыстальніка" "Паглядзець падрабязнасці" "Запісаць галасавое паведамленне." "Спыніць запіс" + "Ваш аватар" "Прыняць" + "Дадаць подпіс" "Дадаць у хроніку" "Назад" "Званок" @@ -52,26 +62,36 @@ "Пацвердзіць пароль" "Працягнуць" "Капіраваць" + "Скапіяваць подпіс" "Скапіраваць спасылку" "Скапіраваць спасылку на паведамленне" + "Скапіяваць тэкст" "Стварыць" "Стварыце пакой" "Дэактываваць" "Дэактываваць уліковы запіс" "Адхіліць" + "Адхіліць і заблакіраваць" "Выдаліць апытанне" + "Зняць выбар з усіх" "Адключыць" "Адмяніць" "Aдхіліць" "Гатова" "Рэдагаваць" + "Рэдагаваць подпіс" "Рэдагаваць апытанне" "Уключыць" "Скончыць апытанне" "Увядзіце PIN-код" + "Даследаваць публічныя прасторы" + "Завяршыць" "Забылі пароль?" "Пераслаць" "Вярнуцца" + "Перайсці да роляў і дазволаў" + "Перайсці ў Налады" + "Ігнараваць" "Запрасіць" "Запрасіць карыстальнікаў" "Запрасіць карыстальнікаў у %1$s" @@ -82,6 +102,7 @@ "Пакінуць" "Пакінуць размову" "Пакінуць пакой" + "Пакінуць прастору" "Загрузіць больш" "Кіраванне ўліковым запісам" "Кіраванне прыладамі" diff --git a/libraries/ui-strings/src/main/res/values-cs/translations.xml b/libraries/ui-strings/src/main/res/values-cs/translations.xml index f343571f1e..38d9ac3d8e 100644 --- a/libraries/ui-strings/src/main/res/values-cs/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml @@ -122,6 +122,7 @@ "Opustit prostor" "Načíst více" "Spravovat účet" + "Správa účtu a zařízení" "Spravovat zařízení" "Spravovat místnosti" "Zpráva" @@ -164,14 +165,15 @@ "Sdílet aktuální polohu" "Zobrazit" "Přihlásit se znovu" - "Odhlásit se" - "Přesto se odhlásit" + "Odebrat toto zařízení" + "Přesto toto zařízení odebrat" "Přeskočit" "Začít" "Zahájit chat" "Začít znovu" "Zahájit ověření" "Klepnutím načtete mapu" + "Zastavit" "Vyfotit" "Klepnutím zobrazíte možnosti" "Přeložit" @@ -192,6 +194,7 @@ "Pokročilá nastavení" "obrázek" "Analytika" + "Synchronizace oznámení…" "Opustili jste místnost" "Byli jste odhlášeni z relace" "Vzhled" @@ -225,6 +228,7 @@ "Prázdný soubor" "Šifrování" "Šifrování povoleno" + "Končí v %1$s" "Zadejte svůj PIN" "Chyba" "Došlo k chybě, nemusíte dostávat oznámení o nových zprávách. Vyřešte prosím problémy s oznámeními z nastavení. @@ -251,6 +255,8 @@ Důvod: %1$s." "Řádek zkopírován do schránky" "Odkaz zkopírován do schránky" "Připojit nové zařízení" + "Aktuální poloha" + "Sdílení aktuální polohy skončilo" "Načítání…" "Načítání dalších…" @@ -351,9 +357,10 @@ Důvod: %1$s." "Nastavení" "Sdílet prostor" "Noví členové vidí historii" + "Sdílená aktuální poloha" "Sdílená poloha" "Sdílený prostor" - "Odhlašování" + "Odebrání zařízení" "Něco se nepovedlo" "Narazili jsme na problém. Zkuste to prosím znovu." "Prostor" @@ -374,12 +381,13 @@ Důvod: %1$s." "Text" "Oznámení třetích stran" "Vlákno" + "Vlákna" "Téma" "O čem je tato místnost?" "Nelze dešifrovat" "Šifrováno nezabezpečeným zařízením" "Nemáte přístup k této zprávě" - "Ověřená identita odesílatele se změnila" + "Ověřená digitální identita odesílatele byla resetována" "Pozvánky nebylo možné odeslat jednomu nebo více uživatelům." "Nelze odeslat pozvánky" "Odemknout" @@ -404,16 +412,17 @@ Důvod: %1$s." "Hlasová zpráva" "Čekání…" "Čekání na dešifrovací klíč" + "Čekání na aktuální polohu…" "Kdokoli může vidět historii" "Vy" "%1$s (%2$s) sdílel(a) tuto zprávu v době, kdy jste nebyli v místnosti." "%1$s sdílel(a) tuto zprávu v době, kdy jste nebyli v místnosti." "Tato místnost byla nastavena tak, aby noví členové mohli číst historii. %1$s" - "Identita uživatele %1$s se změnila. %2$s" - "Identita uživatele %1$s %2$s se změnila. %3$s" + "Identita uživatele %1$s byla resetována. %2$s" + "Identita uživatele %1$s %2$s byla resetována. %3$s" "(%1$s)" - "Identita uživatele %1$s se změnila." - "Identita uživatele %1$s %2$s se změnila. %3$s" + "Identita uživatele %1$s byla resetována." + "Identita uživatele %1$s %2$s byla resetována. %3$s" "Zrušit ověření" "Povolit přístup" "Odkaz %1$s vás přesměruje na jinou stránku %2$s @@ -445,6 +454,7 @@ Opravdu chcete pokračovat?" "%1$s nemá přístup k vaší poloze. Zkuste to prosím později." "Nepodařilo se nahrát hlasovou zprávu." "Místnost již neexistuje nebo pozvánka již není platná." + "Pro přístup k funkcím založeným na poloze prosím povolte GPS." "Zpráva nebyla nalezena" "%1$s nemá oprávnění k přístupu k vaší poloze. Přístup můžete povolit v Nastavení." "%1$s nemá oprávnění k přístupu k vaší poloze. Povolit přístup níže." @@ -473,11 +483,11 @@ Opravdu chcete pokračovat?" "%1$d Připnutých zpráv" "Připnuté zprávy" - "Chystáte se přejít na svůj %1$s účet a obnovit svou identitu. Poté budete přesměrováni zpět do aplikace." - "Nemůžete to potvrdit? Přejděte na svůj účet a resetujte svou identitu." + "Chystáte se přejít na svůj účet %1$s, abyste resetovali svou digitální identitu. Poté budete přesměrováni zpět do aplikace." + "Nemůžete to potvrdit? Přejděte do svého účtu a resetujte svou digitální identitu." "Zrušit ověření a odeslat" "Ověření můžete zrušit a přesto odeslat tuto zprávu, nebo můžete prozatím zrušit a zkusit to znovu později po opětovném ověření %1$s." - "Vaše zpráva nebyla odeslána, protože ověřená identita uživatele %1$s se změnila" + "Vaše zpráva nebyla odeslána, protože byla resetována ověřená digitální identita %1$s" "Přesto odeslat zprávu" "%1$s používá jedno nebo více neověřených zařízení. Zprávu můžete přesto odeslat, nebo můžete prozatím zrušit a zkusit to znovu později poté, co %2$s ověří všechna svá zařízení." "Vaše zpráva nebyla odeslána, protože%1$s neověřil(a) všechna zařízení" @@ -509,7 +519,7 @@ Opravdu chcete pokračovat?" "Prostory" "Sdíleno %1$s" "Na mapě" - "Zpráva nebyla odeslána, protože ověřená identita uživatele %1$s se změnila." + "Zpráva nebyla odeslána, protože byla resetována ověřená digitální identita %1$s." "Zpráva nebyla odeslána, protože%1$s neověřil(a) všechna zařízení." "Zpráva nebyla odeslána, protože jste neověřili jedno nebo více zařízení." "Poloha" @@ -519,5 +529,5 @@ Opravdu chcete pokračovat?" "Pro přístup k historickým zprávám musíte toto zařízení ověřit" "Nemáte přístup k této zprávě" "Nelze dešifrovat zprávu" - "Tato zpráva byla zablokována buď proto, že jste neověřili své zařízení, nebo proto, že odesílatel potřebuje ověřit vaši identitu." + "Tato zpráva byla zablokována buď proto, že jste neověřili své zařízení, nebo proto, že odesílatel musí ověřit vaši digitální identitu." diff --git a/libraries/ui-strings/src/main/res/values-fi/translations.xml b/libraries/ui-strings/src/main/res/values-fi/translations.xml index 182f403140..7be84a8716 100644 --- a/libraries/ui-strings/src/main/res/values-fi/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fi/translations.xml @@ -374,6 +374,7 @@ Syy: %1$s." "Teksti" "Kolmannen osapuolen ilmoitukset" "Viestiketju" + "Viestiketjut" "Aihe" "Mistä tässä huoneessa on kyse?" "Salauksen purkaminen ei onnistunut" @@ -466,6 +467,12 @@ Haluatko varmasti jatkaa?" "Poista %1$s" "Asetukset" "Median valinta epäonnistui, yritä uudelleen." + "Avaa Element Classic" + "Avaa Element Classic laitteellasi" + "Mene kohtaan \"Asetukset\" > \"Tietoturva ja yksityisyys\"" + "Osiossa \"Salausavainten hallinta\", paina \"Salattujen viestien palautus\"." + "Noudata ohjeita" + "Palaa takaisin %1$s -sovellukseen" "Tervetuloa takaisin" "Paina viestiä ja valitse “%1$s” lisätäksesi sen tänne." "Kiinnitä tärkeät viestit, jotta ne löytyvät helposti." diff --git a/libraries/ui-strings/src/main/res/values-fr/translations.xml b/libraries/ui-strings/src/main/res/values-fr/translations.xml index 1dad21ff65..bec6d7f4ca 100644 --- a/libraries/ui-strings/src/main/res/values-fr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fr/translations.xml @@ -374,6 +374,7 @@ Raison : %1$s." "Texte" "Avis de tiers" "Fil de discussion" + "Fils de discussion" "Sujet" "De quoi s’agit-il dans ce salon ?" "Échec de déchiffrement" @@ -466,6 +467,13 @@ Raison : %1$s." "Supprimer %1$s" "Paramètres" "Échec de la sélection du média, veuillez réessayer." + "Ouvrir Element Classic" + "Ouvrez Element Classic sur votre appareil" + "Aller à Paramètres > Sécurité et vie privée" + "Dans Gestion des clés cryptographiques, sélectionnez Récupération des messages chiffrés" + "Suivez les instructions pour activer votre stockage de clés" + "Revenez à %1$s" + "Activez le stockage de vos clés avant de continuer avec %1$s" "Bon retour parmi nous" "Cliquez (clic long) sur un message et choisissez « %1$s » pour qu‘il apparaisse ici." "Épinglez les messages importants pour leur donner plus de visibilité" diff --git a/libraries/ui-strings/src/main/res/values-hu/translations.xml b/libraries/ui-strings/src/main/res/values-hu/translations.xml index bc4535dda1..9586b392e9 100644 --- a/libraries/ui-strings/src/main/res/values-hu/translations.xml +++ b/libraries/ui-strings/src/main/res/values-hu/translations.xml @@ -120,6 +120,7 @@ "Tér elhagyása" "Továbbiak betöltése" "Fiók kezelése" + "Fiók és eszközök kezelése" "Eszközök kezelése" "Szobák kezelése" "Üzenet" @@ -162,14 +163,15 @@ "Valós idejű hely megosztása" "Megjelenítés" "Jelentkezzen be újra" - "Kijelentkezés" - "Kijelentkezés mindenképp" + "Eszköz eltávolítása" + "Eszköz eltávolítása mindenképpen" "Kihagyás" "Indítás" "Csevegés indítása" "Újrakezdés" "Ellenőrzés elindítása" "Koppintson a térkép betöltéséhez" + "Leállítás" "Fénykép készítése" "Koppintson a beállításokért" "Fordítás" @@ -190,6 +192,7 @@ "Speciális beállítások" "egy kép" "Elemzések" + "Értesítések szinkronizálása…" "Elhagyta a szobát" "Ki lett jelentkeztetve a munkamenetből" "Megjelenítés" @@ -223,6 +226,7 @@ "Üres fájl" "Titkosítás" "Titkosítás engedélyezve" + "Vége: %1$s" "Adja meg a PIN-kódját" "Hiba" "Hiba történt, előfordulhat, hogy nem kap értesítést az új üzenetekről. Az értesítések hibaelhárítása a beállításokban található. @@ -249,6 +253,8 @@ Ok: %1$s." "A sor a vágólapra másolva" "Hivatkozás a vágólapra másolva" "Új eszköz összekapcsolása" + "Élő helymeghatározás" + "Élő pozíciómegosztás befejezve" "Betöltés…" "Továbbiak betöltése…" @@ -344,9 +350,10 @@ Ok: %1$s." "Beállítások" "Tér megosztása" "Az új tagok látják az előzményeket" + "Megosztott élő helymeghatározás" "Megosztott tartózkodási hely" "Megosztott tér" - "Kijelentkezés" + "Eszköz eltávolítása" "Valamilyen hiba történt" "Problémába ütköztünk. Próbálja újra." "Tér" @@ -366,6 +373,7 @@ Ok: %1$s." "Szöveg" "Harmadik felek nyilatkozatai" "Üzenetszál" + "Üzenetszálak" "Téma" "Miről szól ez a szoba?" "Nem lehet visszafejteni" @@ -396,6 +404,7 @@ Ok: %1$s." "Hangüzenet" "Várakozás…" "Várakozás erre az üzenetre" + "Várakozás az élő helymeghatározásra…" "Bárki láthatja az előzményeket" "Ön" "%1$s (%2$s) megosztotta ezt az üzenetet, mivel Ön nem volt a szobában, amikor elküldték." @@ -457,6 +466,13 @@ Biztos, hogy folytatja?" "Eltávolítás: %1$s" "Beállítások" "Nem sikerült kiválasztani a médiát, próbálja újra." + "Nyissa meg az Element Classic alkalmazást" + "Nyissa meg az Element Classic alkalmazást az eszközén" + "Lépjen a Beállítások > Biztonság és adatvédelem menüponthoz" + "A Kriptográfiai kulcsok kezelése részben válassza a Titkosított üzenetek helyreállítása lehetőséget" + "Kövesse az utasításokat a kulcstároló engedélyezéséhez" + "Térjen vissza ide: %1$s" + "Engedélyezze a kulcstárolást a folytatás előtt ide: %1$s" "Üdvözöljük újra!" "Nyomjon hosszan az üzenetre, és válassza a „%1$s” lehetőséget, hogy itt szerepeljen." "Tűzze ki a fontos üzeneteket, hogy könnyen felfedezhetők legyenek" diff --git a/libraries/ui-strings/src/main/res/values-it/translations.xml b/libraries/ui-strings/src/main/res/values-it/translations.xml index 99c5db6077..0d73a8c121 100644 --- a/libraries/ui-strings/src/main/res/values-it/translations.xml +++ b/libraries/ui-strings/src/main/res/values-it/translations.xml @@ -1,6 +1,7 @@ "Aggiungi reazione: %1$s" + "Indirizzo" "Avatar" "Riduci al minimo il campo di testo del messaggio" "Elimina" @@ -26,9 +27,12 @@ "Pausa" "Messaggio vocale, durata: %1$s, posizione attuale: %2$s" "Campo del PIN" + "Posizione fissata" "Riproduci" + "Velocità di riproduzione" "Sondaggio" "Sondaggio terminato" + "Codice QR" "Reagisci con %1$s" "Reagisci con altri emoji" "Visualizzato da %1$s e %2$s" @@ -42,9 +46,12 @@ "Rimuovere la reazione con %1$s" "Avatar della stanza" "Invia file" + "Posizione del mittente" "Azione richiesta a tempo limitato, hai un minuto per la verifica" "Mostra password" "Avvia una chiamata" + "Avvia una videochiamata" + "Avvia una chiamata vocale" "Stanza obsoleta" "Avatar utente" "Menu utente" @@ -56,6 +63,7 @@ "Il tuo avatar" "Accetta" "Aggiungi didascalia" + "Aggiungi stanze esistenti" "Aggiungi alla conversazione" "Indietro" "Chiama" @@ -75,6 +83,7 @@ "Copia testo" "Crea" "Crea stanza" + "Crea spazio" "Disattiva" "Disattiva account" "Rifiuta" @@ -91,6 +100,7 @@ "Attiva" "Termina sondaggio" "Inserisci PIN" + "Esplora gli spazi pubblici" "Fine" "Password dimenticata?" "Inoltra" @@ -111,6 +121,7 @@ "Esci dallo spazio" "Carica altro" "Gestisci account" + "Gestisci account & dispositivi" "Gestisci dispositivi" "Gestisci le stanze" "Invia messaggio" @@ -150,18 +161,21 @@ "Invia messaggio vocale" "Condividi" "Condividi collegamento" + "Condividi la posizione in tempo reale" "Mostra" "Accedi di nuovo" - "Disconnetti" - "Disconnetti comunque" + "Rimuovi questo dispositivo" + "Rimuovi comunque questo dispositivo" "Salta" "Inizia" "Avvia conversazione" "Ricomincia" "Avvia la verifica" "Tocca per caricare la mappa" + "Ferma" "Scatta foto" "Tocca per le opzioni" + "Traduci" "Riprova" "Rimuovi dai fissati" "Visualizza" @@ -179,6 +193,7 @@ "Impostazioni avanzate" "un\'immagine" "Statistiche di utilizzo" + "Sincronizzazione delle notifiche…" "Hai lasciato la stanza" "Sei stato disconnesso dalla sessione" "Aspetto" @@ -191,6 +206,7 @@ "Copiato negli appunti" "Copyright" "Creazione stanza…" + "Creazione spazio…" "Richiesta annullata" "Hai lasciato la stanza" "Hai lasciato lo spazio" @@ -211,6 +227,7 @@ "File vuoto" "Crittografia" "Crittografia abilitata" + "Termina alle %1$s" "Inserisci il PIN" "Errore" "Si è verificato un errore, potresti non ricevere notifiche per nuovi messaggi. Risolvi i problemi relativi alle notifiche dalle impostazioni. @@ -236,6 +253,9 @@ Motivo:. %1$s" "Chiaro" "Riga copiata negli appunti" "Collegamento copiato negli appunti" + "Collega un nuovo dispositivo" + "Posizione in tempo reale" + "Posizione in tempo reale terminata" "Caricamento…" "Caricamento in corso…" @@ -248,10 +268,12 @@ Motivo:. %1$s" "Messaggio" "Azioni messaggio" + "Impossibile inviare il messaggio" "Impaginazione del messaggio" "Messaggio rimosso" "Moderno" "Silenzia" + "Nome" "%1$s (%2$s)" "Nessun risultato" "Nessun nome della stanza" @@ -260,6 +282,7 @@ Motivo:. %1$s" "Non in linea" "Licenze open source" "o" + "Altre opzioni" "Password" "Persone" "Collegamento permanente" @@ -277,8 +300,10 @@ Motivo:. %1$s" "Preparazione…" "Informativa sulla privacy" + "Privato" "Stanza privata" "Spazio privato" + "Pubblico" "Stanza pubblica" "Spazio pubblico" "Reazione" @@ -286,6 +311,7 @@ Motivo:. %1$s" "Motivo" "Chiave di recupero" "Aggiornamento…" + "Rimozione…" "%1$d risposta" "%1$d risposte" @@ -295,6 +321,7 @@ Motivo:. %1$s" "Segnala un problema" "Segnalazione inviata" "Editor di testo avanzato" + "Ruolo" "Stanza" "Nome stanza" "ad es. il nome del tuo progetto" @@ -310,6 +337,10 @@ Motivo:. %1$s" "Sicurezza" "Visto da" "Seleziona un account" + + "%1$d selezionato" + "%1$d selezionati" + "Invia a" "Invio in corso…" "Invio fallito" @@ -320,12 +351,16 @@ Motivo:. %1$s" "URL del server" "Impostazioni" "Condividi lo spazio" + "I nuovi membri vedono la cronologia" + "Posizione in tempo reale condivisa" "Posizione condivisa" "Spazio condiviso" - "Disconnessione" + "Rimozione del dispositivo" "Qualcosa è andato storto" "Abbiamo riscontrato un problema. Per favore riprova." "Spazio" + "Membri dello spazio" + "Di cosa tratta questo spazio?" "%1$d Spazio" "%1$d Spazi" @@ -333,18 +368,20 @@ Motivo:. %1$s" "Avvio della conversazione…" "Adesivo" "Operazione riuscita" + "Suggeriti" "Suggerimenti" "Sincronizzazione" "Sistema" "Testo" "Comunicazioni di terze parti" "Discussione" + "Discussioni" "Argomento" - "Di cosa parla questa stanza?" + "Di cosa riguarda questa stanza?" "Impossibile decrittografare" "Inviato da un dispositivo non sicuro" "Non hai accesso a questo messaggio" - "L\'identità verificata del mittente è stata reimpostata" + "L\'identità digitale verificata del mittente è stata reimpostata" "Non è stato possibile spedire inviti a uno o più utenti." "Impossibile inviare inviti" "Sblocca" @@ -369,13 +406,19 @@ Motivo:. %1$s" "Messaggio vocale" "In attesa…" "In attesa del messaggio" + "In attesa della posizione in tempo reale…" + "Chiunque può vedere la cronologia" "Tu" - "L\'identità di %1$s è stata reimpostata. %2$s" - "L\'identità %2$s di %1$s sembra essere cambiata. %3$s" + "%1$s (%2$s) ha condiviso questo messaggio poiché non eri nella stanza quando è stato inviato." + "%1$s ha condiviso questo messaggio poiché non eri nella stanza quando è stato inviato." + "Questa stanza è stata configurata in modo che i nuovi membri possano leggere la cronologia. %1$s" + "%1$sL\'identità digitale di %2$s" + "%1$sL\'identità digitale di %2$s è stata reimpostata. %3$s" "(%1$s)" - "L\'identità di %1$s è stata reimpostata." - "L\'identità %2$s di %1$s è stata reimpostata. %3$s" + "L\'identità digitale di %1$s è stata reimpostata." + "L\'identità digitale %2$s di %1$s è stata reimpostata. %3$s" "Ritira verifica" + "Consenti l\'accesso" "Il link %1$s ti porta ad un altro sito %2$s Sei sicuro di voler continuare?" @@ -405,6 +448,7 @@ Sei sicuro di voler continuare?" "%1$s non è riuscito ad accedere alla tua posizione. Riprova più tardi." "Invio del messaggio vocale fallito." "La stanza non esiste più o l\'invito non è più valido." + "Attiva il GPS per accedere alle funzioni basate sulla posizione." "Messaggio non trovato" "%1$s non ha l\'autorizzazione di accedere alla tua posizione. Puoi attivare l\'accesso nelle impostazioni." "%1$s non ha l\'autorizzazione per accedere alla tua posizione. Attiva l\'accesso di seguito." @@ -424,6 +468,14 @@ Sei sicuro di voler continuare?" "Rimuovi %1$s" "Impostazioni" "Selezione del file multimediale fallita, riprova." + "Apri Element Classic" + "Apri Element Classic sul tuo dispositivo" + "Vai su Impostazioni > Sicurezza & privacy" + "Nella gestione delle chiavi crittografiche, seleziona Recupero dei messaggi cifrati" + "Segui le istruzioni per abilitare l\'archiviazione delle chiavi" + "Torna a %1$s" + "Abilita l\'archivio delle chiavi prima di procedere con %1$s" + "Bentornato" "Premi su un messaggio e scegli “%1$s” per includerlo qui." "Fissa i messaggi importanti così che possano essere trovati facilmente" @@ -431,11 +483,11 @@ Sei sicuro di voler continuare?" "%1$d Messaggi fissati" "Messaggi fissati" - "Stai per accedere al tuo account di %1$s per ripristinare la tua identità. Dopodiché verrai riportato all\'app." - "Non riesci a confermare? Vai al tuo account per ripristinare la tua identità." + "Stai per accedere al tuo account %1$s per reimpostare la tua identità digitale. Al termine, verrai reindirizzato all\'app." + "Non riesci a confermare? Vai al tuo account per reimpostare la tua identità digitale." "Ritira la verifica e invia" "Puoi ritirare la tua verifica e inviare comunque questo messaggio, oppure annullarlo per ora e riprovare più tardi dopo aver riverificato %1$s." - "Il tuo messaggio non è stato inviato perché l\'identità verificata di %1$s è stata reimpostata." + "Il tuo messaggio non è stato inviato perché l\'identità digitale verificata di %1$sè stata reimpostata" "Invia comunque il messaggio" "%1$s sta usando uno o più dispositivi non verificati. Puoi inviare il messaggio in ogni caso, oppure annullarlo e riprovare più tardi quando %2$s avrà verificato tutti i suoi dispositivi." "Il tuo messaggio non è stato inviato perché %1$s non ha verificato tutti i dispositivi." @@ -458,12 +510,16 @@ Sei sicuro di voler continuare?" "Apri in Apple Maps" "Apri in Google Maps" "Apri in OpenStreetMap" - "Condividi questa posizione" + "Condividi la posizione selezionata" + "Opzioni di condivisione" "Spazi che hai creato o a cui hai aderito." "%1$s • %2$s" + "Crea spazi per organizzare le stanze" "%1$s spazio" "Spazi" - "Messaggio non inviato perché l\'identità verificata di %1$s è stata reimpostata." + "Condivisa %1$s" + "Sulla mappa" + "Il messaggio non è stato inviato perché l\'identità digitale verificata di %1$sè stata reimpostata." "Messaggio non inviato perché %1$s non ha verificato tutti i dispositivi." "Messaggio non inviato perché non hai verificato uno o più dispositivi." "Posizione" @@ -473,5 +529,5 @@ Sei sicuro di voler continuare?" "È necessario verificare questo dispositivo per accedere alla cronologia messaggi" "Non hai accesso a questo messaggio" "Impossibile decifrare il messaggio" - "Questo messaggio è stato bloccato perché il dispositivo non è verificato o perché il mittente deve verificare la tua identità." + "Questo messaggio è stato bloccato perché non hai verificato il tuo dispositivo o perché il mittente deve verificare la tua identità digitale." diff --git a/libraries/ui-strings/src/main/res/values-ja/translations.xml b/libraries/ui-strings/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..f77a92966b --- /dev/null +++ b/libraries/ui-strings/src/main/res/values-ja/translations.xml @@ -0,0 +1,525 @@ + + + "リアクションを追加: %1$s" + "アドレス" + "アバター" + "入力欄を縮小" + "削除" + + "%1$d 桁入力済" + + "アバターを編集" + "完全なアドレスは %1$s になります。" + "暗号化の詳細" + "入力欄を拡大" + "パスワードを非表示" + "通話に参加" + "一番下へ" + "現在地に移動" + "メンションのみ" + "ミュート有効" + "新規メンション" + "新着メッセージ" + "通話中" + "他のユーザーのアバター" + "%1$d ページ" + "一時停止" + "音声メッセージ 長さ: %1$s 再生位置: %2$s" + "PIN入力欄" + "ピン留めした位置情報" + "再生" + "再生速度" + "投票" + "投票終了" + "QRコード" + "リアクション: %1$s" + "他の絵文字でリアクション" + "既読: %1$s, %2$s" + + "既読: %1$s ほか %2$d 人" + + "%1$s 既読" + "タップしてすべて表示" + "%1$s リアクションを削除" + "%1$s リアクションを削除" + "ルームのアバター" + "ファイルを送信" + "送信者の位置情報" + "1分以内に検証を完了してください" + "パスワードを表示" + "通話を開始" + "ビデオ通話を開始" + "音声通話を開始" + "埋没したルーム" + "ユーザーのアバター" + "ユーザーメニュー" + "アバターを表示" + "詳細を表示" + "音声メッセージ 長さ: %1$s" + "音声メッセージを録音してください。" + "録音を停止" + "あなたのアバター" + "承諾" + "キャプションを追加" + "既存のルームを追加" + "タイムラインに追加" + "戻る" + "通話" + "キャンセル" + "今回のみキャンセル" + "写真を選択" + "クリア" + "閉じる" + "検証してください" + "確認" + "パスワードを確認" + "続行" + "コピー" + "キャプションをコピー" + "リンクをコピー" + "リンクをメッセージにコピー" + "テキストをコピー" + "作成" + "ルームを作成" + "スペースを作成" + "停止" + "アカウントを無効化" + "拒否" + "拒否してブロック" + "投票を削除" + "全ての選択を解除" + "無効化" + "破棄" + "無視" + "完了" + "編集" + "キャプションを編集" + "投票を編集" + "有効化" + "投票を終了" + "PINを入力" + "公開スペースを探す" + "完了" + "パスワードをお忘れですか?" + "転送" + "戻る" + "役割と権限に移動" + "設定に移動" + "無視" + "招待" + "ユーザーを招待" + "%1$s にユーザーを招待" + "%1$s にユーザーを招待" + "招待" + "参加" + "詳細" + "退出" + "会話を退出" + "ルームを退出" + "スペースを退出" + "さらに表示" + "アカウントを管理" + "アカウントと端末を管理" + "端末を管理" + "ルームを管理" + "メッセージ" + "最小化" + "次へ" + "いいえ" + "後で" + "OK" + "コンテキストメニューを開く" + "設定" + "他のアプリで開く" + "ピン留め" + "クイック返信" + "引用" + "リアクション" + "拒否" + "削除" + "キャプションを削除" + "メッセージを削除" + "返信" + "スレッドで返信" + "報告" + "バグを報告" + "コンテンツを報告" + "会話を通報" + "ルームを通報" + "リセット" + "IDをリセット" + "再試行" + "復号化を再試行" + "保存" + "検索" + "すべて選択" + "送信" + "編集したメッセージを送信" + "メッセージを送信" + "音声メッセージを送信" + "共有" + "リンクを共有" + "ライブ位置情報を共有" + "表示" + "再度サインイン" + "この端末を削除" + "強制的に削除" + "スキップ" + "開始" + "チャットを開始" + "やり直す" + "検証を開始" + "タップして地図を読み込む" + "停止" + "写真を撮影" + "タップしてオプションを表示" + "翻訳" + "もう一度やり直してください" + "ピン留めを解除" + "表示" + "タイムラインで表示" + "ソースコードを表示" + "はい" + "再試行する" + "使用しているサーバーがより高速な新しいプロトコルに対応しました。将来、古いプロトコルへの対応が打ち切られ、強制的にログアウトされる可能性があるため、今すぐにログアウトし、再度ログインし直すことを推奨します。" + "アップグレードがあります" + "アプリケーションについて" + "利用規定" + "アカウントを追加" + "別のアカウントを追加" + "キャプションを追加" + "高度な設定" + "画像" + "分析" + "通知を同期中…" + "あなたがルームを退出" + "セッションからログアウトされました" + "外観" + "音声" + "ベータ版" + "ブロックしたユーザー" + "ふきだし" + "通話を開始しました" + "チャットをバックアップ" + "クリップボードにコピーしました" + "著作権" + "ルームを作成中…" + "スペースを作成中…" + "リクエストはキャンセルされました" + "ルームを退出しました" + "スペースから退出しました" + "招待は却下されました" + "ダーク" + "復号化エラー" + "詳細" + "開発者向けオプション" + "端末ID" + "ダイレクトチャット" + "次回からは表示しない" + "ダウンロードに失敗しました" + "ダウンロード中" + "(編集済み)" + "編集中" + "キャプションを編集" + "* %1$s %2$s" + "空のファイル" + "暗号化" + "暗号化が有効です" + "%1$s に終了" + "PINを入力してください" + "エラー" + "問題が発生しました。新着のメッセージの通知を受け取れない可能性があります。設定から通知のトラブルシューティングを行ってください。 + +理由: %1$s" + "全員" + "失敗" + "お気に入り" + "お気に入り" + "ファイル" + "ファイルを削除しました" + "ファイルを保存しました" + "ファイルはダウンロードに保存されました" + "メッセージを転送" + "頻繁に使用" + "GIF" + "画像" + "%1$s に返信" + "APK をインストール" + "このMatrix IDは見つからないため、招待が届かない可能性があります。" + "ルームを退出しています" + "スペースを退出しています" + "ライト" + "行をクリップボードにコピーしました" + "リンクをクリップボードにコピーしました" + "新しい端末から接続" + "ライブ位置情報" + "ライブ位置情報が終了しました" + "読み込み中…" + "読み込み中…" + + "他 %d 人" + + + "%1$d 人のメンバー" + + "メッセージ" + "メッセージアクション" + "メッセージの送信に失敗" + "メッセージのレイアウト" + "メッセージは削除されました" + "モダン" + "ミュート" + "名前" + "%1$s (%2$s)" + "結果なし" + "ルーム名なし" + "スペース名なし" + "暗号化されていません" + "オフライン" + "オープンソースライセンス" + "または" + "他のオプション" + "パスワード" + "人" + "固定リンク" + "権限" + "ピン留め" + "インターネット接続を確認してください" + "お待ちください…" + "本当に投票を終了しますか?" + "投票: %1$s" + "総投票数:%1$s" + "結果は投票終了後に表示されます" + + "%d 票" + + "準備中…" + "プライバシーポリシー" + "非公開" + "非公開ルーム" + "非公開スペース" + "公開" + "公開ルーム" + "公開スペース" + "リアクション" + "リアクション" + "理由" + "回復鍵" + "更新中…" + "削除しています…" + + "%1$d 件の返信" + + "%1$s に返信" + "バグを報告" + "問題を報告" + "報告は送信されました" + "リッチテキストエディター" + "役割" + "ルーム" + "ルーム名" + "例: プロジェクトの名称" + + "%1$d 個のルーム" + + "保存された変更" + "保存中…" + "画面ロック" + "ユーザーを検索" + "検索結果" + "セキュリティ" + "既読" + "アカウントを選択" + + "%1$d 個を選択" + + "送る:" + "送信中…" + "送信失敗" + "送信済" + "。" + "このサーバーには対応していません" + "サーバーに接続できません" + "サーバー URL" + "設定" + "スペースを共有" + "新しいメンバーは履歴を閲覧できます" + "共有したライブ位置情報" + "共有された位置情報" + "共有されたスペース" + "端末を削除中" + "問題が発生しました" + "問題が発生しました。再度お試しください。" + "スペース" + "スペースのメンバー" + "このスペースは何についてのものですか?" + + "%1$d 個のスペース" + + "チャットを開始しています…" + "ステッカー" + "成功" + "推奨される" + "提案" + "同期中" + "システム" + "テキスト" + "第三者に関する通知" + "スレッド" + "スレッド" + "トピック" + "何についてのルームですか?" + "復号化できません" + "安全でないデバイスから送信されました" + "このメッセージにアクセスできません" + "送信者の検証済みのデジタルIDがリセットされました" + "1人以上のユーザーに招待を送信できませんでした。" + "招待を送信できません" + "ロック解除" + "ミュート解除" + "非対応の通話" + "非対応のイベント" + "ユーザー名" + "検証がキャンセルされました" + "検証完了" + "検証に失敗しました" + "検証済み" + "端末を検証" + "IDを検証" + "ユーザーの検証" + "動画" + "高画質" + "高画質・ファイルサイズ大" + "低画質" + "最速アップロード・ファイルサイズ小" + "標準画質" + "中程度の画質とアップロード速度" + "音声メッセージ" + "待機中…" + "メッセージを待機中" + "ライブ位置情報を待機中…" + "誰でも履歴を閲覧できます" + "あなた" + "あなたが参加する以前に送信されたメッセージを、%1$s (%2$s) が共有しました。" + "あなたが参加する以前に送信されたメッセージを、%1$s が共有しました。" + "このルームは新しいメンバーが過去の内容を確認できるように設定されています。%1$s" + "%1$s のデジタルIDはリセットされました。%2$s" + "%1$s の %2$s のデジタルIDはリセットされました。%3$s" + "(%1$s)" + "%1$s のデジタルIDはリセットされました。" + "%1$s %2$s のデジタルIDはリセットされました。%3$s" + "検証のリクエストを却下" + "アクセスを許可" + "リンク %1$s が別サイト %2$s に遷移しようとしています。 + +続行しますか?" + "リンクを再度確認してください" + "アップロードする動画のデフォルト画質を選択してください。" + "動画のアップロード品質" + "アップロードできる最大サイズは %1$s です。" + "アップロードの許容サイズを超えています。" + "ルームを通報しました" + "ルームを通報し退出" + "確認" + "エラー" + "成功" + "警告" + "未保存の変更内容があります。" + "変更は保存されていません。本当に戻りますか?" + "変更を保存しますか?" + "アップロードできる最大サイズは %1$s です。" + "アップロードする画質を選択してください。" + "動画のアップロード品質を選択" + "絵文字を検索" + "この端末では既に %1$s としてログインしています。" + "Matrix Authentication Service とアカウント作成に対応するため、このホームサーバーはアップグレードが必要です。" + "固定リンクの作成に失敗しました" + "%1$s でマップを読み込めません。時間を置いて再度お試しください。" + "メッセージの読み込みに失敗しました" + "%1$s は現在地にアクセスできませんでした。時間を置いて再度お試しください。" + "音声メッセージの送信に失敗しました。" + "このルームは存在しないか、招待が無効です。" + "位置情報に基づく機能を使用するには、GPSを有効化してください。" + "メッセージが見つかりません" + "現在地を取得する権限が %1$s にありません。設定で権限を追加できます。" + "現在地を取得する権限が %1$s にありません。以下から許可してください。" + "%1$s には、マイクの使用が許可されていません。音声メッセージのために、マイクの使用を許可してください。" + "ネットワークまたはサーバーの問題である可能性があります。" + "このルームアドレスは既に存在しています。ルームの名称を変更するか、アドレスを編集してください。" + "一部の文字は使用できません。使用できるのは、英字、数字と記号 ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _ です。" + "送信されていないメッセージがあります。" + "申し訳ありません。エラーが発生しました。" + "🔐️ %1$s に参加してください" + "%1$s で話しましょう: %2$s" + "%1$s Android" + "開発者にバグを報告するには端末を振ってください。" + "スクリーンショット" + "%1$s: %2$s" + "選択肢" + "%1$s を削除" + "設定" + "ファイルの選択に失敗しました。再試行してください。" + "Element Classic を開く" + "Element Classic をこの端末で開く" + "「設定- セキュリティとプライバシー」に移動します" + "暗号鍵の管理から、暗号化されたメッセージの回復を選択します" + "指示に従って、鍵の保管庫を有効化してください" + "%1$s に戻ってください" + "%1$s に続行する前に、鍵の保管庫を有効化してください" + "アカウントを確認中" + "おかえりなさい" + "メッセージを長押しし \"%1$s\" を選択してください" + "重要なメッセージをピン留めして容易に見つけられるようにします" + + "%1$d 個のピン留めされたメッセージ" + + "ピン留めされたメッセージ" + "デジタルIDをリセットするため %1$s のアカウントの設定に移動します。完了後、アプリに遷移します。" + "認証できませんか?アカウントに移動してデジタルIDをリセットできます。" + "検証の要求を取り下げて送信" + "検証の要求を取り下げてメッセージの送信を続行するか、送信をキャンセルして %1$s の検証が完了した後に再試行することができます。" + "%1$s の検証済みのデジタルIDがリセットされたため、メッセージは送信されませんでした。" + "メッセージを強制送信" + "%1$s は未検証の端末を使用しています。メッセージを強制的に送信するか、%2$s がすべての端末を検証した後に送信を再試行することができます。" + "%1$s に未検証の端末が存在するため、メッセージは送信されませんでした。" + "検証の完了していない端末があります。メッセージを強制的に送信するか、すべての端末を検証した後に送信を再試行することができます。" + "未検証の端末が存在するため、メッセージは送信されませんでした。" + "管理者または所有者を編集" + "ファイルの処理に失敗しました。再試行してください。" + "ユーザーの詳細を取得できませんでした" + "%1$s のメッセージ" + "展開" + "縮小" + "ライブ位置情報を共有中" + "すでにこのルームを表示しています" + "%2$s 個のうち %1$s" + "%1$s 個のピン留めされたメッセージ" + "メッセージを読み込み中…" + "すべて表示" + "チャット" + "場所を共有" + "現在地を共有する" + "Apple Maps で開く" + "Google Maps で開く" + "OpenStreetMapで開く" + "この位置情報を共有する" + "共有設定" + "作成または参加したスペースです。" + "%1$s・%2$s" + "スペースを作成してルームを整頓しましょう" + "%1$s スペース" + "スペース" + "%1$s 共有" + "マップ上" + "%1$s の検証済みのデジタルIDがリセットされたため、メッセージは送信されませんでした。" + "%1$s に未検証の端末が存在するため、メッセージは送信されませんでした。" + "未検証の端末が存在するため、メッセージは送信されませんでした。" + "位置情報" + "バージョン: %1$s (%2$s)" + "ja" + "この端末で過去のメッセージを表示できません" + "過去のメッセージを表示するには、この端末を検証する必要があります" + "このメッセージにアクセスできません" + "メッセージの復号化に失敗" + "この端末を検証していないか、送信者があなたのデジタルIDを検証していないため、このメッセージはブロックされました。" + diff --git a/libraries/ui-strings/src/main/res/values-ko/translations.xml b/libraries/ui-strings/src/main/res/values-ko/translations.xml index a9d560b852..4174e4d15d 100644 --- a/libraries/ui-strings/src/main/res/values-ko/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ko/translations.xml @@ -26,6 +26,7 @@ "일시중지" "음성 메시지, 지속 시간: %1$s, 현재 위치: %2$s" "PIN 필드" + "고정된 위치" "재생" "재생 속도" "투표" @@ -43,9 +44,11 @@ "%1$s 반응을 제거하세요" "방 아바타" "파일 보내기" + "발신자 위치" "제한 시간 내 인증이 필요합니다.1분 안에 확인해 주세요." "비밀번호 표시" "통화 시작" + "음성 통화 시작" "묘비 방" "사용자 아바타" "사용자 메뉴" @@ -115,6 +118,7 @@ "스페이스 떠나기" "더 불러오기" "계정 관리" + "계정 및 기기 관리" "기기 관리" "방 관리" "메시지" @@ -165,6 +169,7 @@ "다시 시작하다" "인증 시작" "탭해서 지도 불러오기" + "중지" "사진 찍기" "옵션을 보려면 탭하세요" "번역" @@ -185,6 +190,7 @@ "고급 설정" "이미지" "통계" + "알림 동기화 중…" "방을 떠남" "세션에서 로그아웃되었습니다." "외관" @@ -218,6 +224,7 @@ "빈 파일" "암호화" "암호화 활성화됨" + "%1$s에 종료" "PIN을 입력하세요" "오류" "오류가 발생했습니다, 새 메시지 알림을 받지 못할 수 있습니다. 설정에서 알림 문제를 해결하세요. @@ -244,6 +251,8 @@ "줄이 클립보드에 복사되었습니다." "링크가 클립보드에 복사됨" "새 기기 연결" + "실시간 위치" + "실시간 위치 공유 종료" "로딩 중…" "더 많은 내용이 로딩 중…" @@ -268,6 +277,7 @@ "오프라인" "오픈 소스 라이선스" "또는" + "기타 옵션" "비밀번호" "사람" "퍼머링크" @@ -333,6 +343,7 @@ "설정" "스페이스 공유" "새 멤버에게 대화 기록 공개" + "공유 중인 실시간 위치" "공유된 위치" "공유된 스페이스" "기기 제거" @@ -354,12 +365,13 @@ "글자" "제3자 고지" "스레드" + "스레드" "주제" "이 방은 어떤 곳인가요?" "해독 불가" "보안되지 않은 장치에서 전송됨" "이 메시지에 액세스할 수 없습니다" - "발신자의 검증된 신원이 재설정되었습니다." + "발신자의 인증된 디지털 신원이 재설정되었습니다" "한 명 이상의 사용자에게 초대를 보낼 수 없습니다." "초대를 보낼 수 없음" "잠금 해제" @@ -384,16 +396,17 @@ "음성 메시지" "대기 중…" "이 메시지를 기다리고 있습니다" + "실시간 위치 정보를 기다리는 중…" "누구나 대화 기록 보기 가능" "당신" "%1$s(%2$s)님이 이 메시지를 공유했습니다. 메시지 전송 당시 귀하가 방에 참여 중이 아니었기 때문입니다." "%1$s님이 이 메시지를 공유했습니다. 메시지 전송 당시 귀하가 방에 참여 중이 아니었기 때문입니다." "이 방은 새 멤버가 이전 대화 기록을 읽을 수 있도록 설정되었습니다. %1$s" - "%1$s 의 신원이 재설정되었습니다. %2$s" + "%1$s님의 디지털 신원이 재설정되었습니다. %2$s" "%1$s의 %2$s 신원이 재설정되었습니다. %3$s" "(%1$s)" - "%1$s의 신원이 재설정되었습니다." - "%1$s의 %2$s 신원이 재설정되었습니다. %3$s" + "%1$s의 디지털 신원이 재설정되었습니다." + "%1$s님의 %2$s 디지털 신원이 재설정되었습니다. %3$s" "확인 취소" "액세스 허용" "%1$s 링크는 다른 사이트로 이동합니다 %2$s @@ -427,6 +440,7 @@ "%1$s가 위치에 접근할 수 없습니다. 나중에 다시 시도해 주세요." "음성 메시지 업로드에 실패했습니다." "해당 방이 더 이상 존재하지 않거나 초대장이 더 이상 유효하지 않습니다." + "위치 기반 기능을 사용하려면 GPS를 켜 주세요." "메시지를 찾을 수 없습니다" "%1$s에서 위치에 접근할 수 있는 권한이 없습니다. 설정에서 활성화가 가능합니다." "%1$s에서 위치에 접근할 수 있는 권한이 없습니다. 아래에서 허용해주세요." @@ -446,6 +460,14 @@ "%1$s 제거" "설정" "미디어 선택에 실패했습니다. 다시 시도해 주세요." + "Element Classic 열기" + "기기에서 Element Classic 앱을 열어 주세요" + "설정 > 보안 및 개인정보 보호로 이동하세요" + "암호화 키 관리에서 \'암호화된 메시지 복구\'를 선택하세요" + "안내에 따라 키 저장소를 활성화해 주세요" + "%1$s(으)로 돌아가기" + "%1$s(으)로 진행하기 전에 키 저장소를 활성화해 주세요." + "다시 오신 것을 환영합니다" "메시지를 누르고 \"%1$s\" 를 선택하여 여기에 포함합니다." "중요한 메시지를 고정하여 쉽게 찾을 수 있도록 합니다" @@ -456,7 +478,7 @@ "확인할 수 없나요? 계정 설정으로 이동하여 디지털 신원을 재설정하세요." "인증 철회 및 전송" "확인 절차를 철회하고 이 메시지를 보내거나, 지금 취소하고 나중에 %1$s 을 확인한 후 다시 시도할 수 있습니다." - "%1$s의 인증된 신원이 재설정되어 귀하의 메시지가 전송되지 않았습니다." + "%1$s님의 인증된 디지털 신원이 재설정되어 메시지를 전송하지 못했습니다." "아무튼 메시지 보내기" "%1$s 는 하나 이상의 확인되지 않은 장치를 사용하고 있습니다. 메시지를 보내거나, %2$s 이 모든 장치를 확인한 후에 다시 시도할 수 있습니다." "%1$s 이(가) 모든 기기를 확인하지 않았기 때문에 귀하의 메시지가 전송되지 않았습니다." @@ -480,12 +502,15 @@ "Google Maps에서 열기" "OpenStreetMap에서 열기" "선택한 위치 공유" + "공유 옵션" "당신이 스페이스를 만들거나 가입했습니다." "%1$s•%2$s" "스페이스를 생성하여 방을 체계적으로 관리해 보세요." "%1$s 스페이스" "스페이스" - "%1$s의 인증된 신원이 재설정되어 메시지가 전송되지 않았습니다." + "공유된 %1$s" + "지도에서 보기" + "%1$s님의 인증된 디지털 신원이 재설정되어 메시지를 보내지 못했습니다." "%1$s 이 모든 장치를 확인하지 않았기 때문에 메시지가 전송되지 않았습니다." "하나 이상의 기기를 확인하지 않았기 때문에 메시지가 전송되지 않았습니다." "위치" @@ -495,5 +520,5 @@ "이전 메시지에 액세스하려면 이 장치를 확인해야 합니다." "이 메시지에 액세스할 수 없습니다" "메시지를 해독할 수 없습니다." - "이 메시지는 귀하가 기기를 확인하지 않았거나 발신자가 귀하의 신원을 확인해야 하기 때문에 차단되었습니다." + "기기가 인증되지 않았거나, 발신자가 귀하의 디지털 신원을 확인해야 하므로 이 메시지가 차단되었습니다." diff --git a/libraries/ui-strings/src/main/res/values-lt/translations.xml b/libraries/ui-strings/src/main/res/values-lt/translations.xml index 18eafdb13c..1ebbceedd5 100644 --- a/libraries/ui-strings/src/main/res/values-lt/translations.xml +++ b/libraries/ui-strings/src/main/res/values-lt/translations.xml @@ -88,7 +88,7 @@ "Kūrėjo nustatymai" "Asmeninis pokalbis" "(redaguota)" - "Taisymas" + "Redagavimas" "* %1$s %2$s" "Šifravimas įjungtas" "Klaida" @@ -96,7 +96,7 @@ "Failas išsaugotas aplanke Atsisiuntimai" "Persiųsti žinutę" "GIF" - "Paveikslėlis" + "Vaizdas" "Šio Matrix ID nepavyksta rasti, todėl kvietimas gali būti negautas." "Paliekamas kambarys" "Nuoroda nukopijuota į iškarpinę" @@ -170,6 +170,7 @@ "%1$s Android" "Papurtykite, kad praneštumėte apie klaidą" "Nepavyko pasirinkti laikmenos, pabandykite dar kartą." + "Sveiki sugrįžę" "Nepavyko apdoroti įkeliamos laikmenos, bandykite dar kartą." "Nepavyko gauti naudotojo išsamios informacijos." "Bendrinti vietą" diff --git a/libraries/ui-strings/src/main/res/values-pl/translations.xml b/libraries/ui-strings/src/main/res/values-pl/translations.xml index bd6eaab990..6812885c9c 100644 --- a/libraries/ui-strings/src/main/res/values-pl/translations.xml +++ b/libraries/ui-strings/src/main/res/values-pl/translations.xml @@ -1,6 +1,7 @@ "Dodaj reakcję: %1$s" + "Adres" "Awatar" "Zmniejsz pole tekstowe wiadomości" "Usuń" @@ -27,6 +28,7 @@ "Wstrzymaj" "Wiadomość głosowa, czas trwania: %1$s, aktualna pozycja: %2$s" "Pole PIN" + "Przypięta lokalizacja" "Odtwórz" "Ankieta" "Zakończona ankieta" @@ -148,8 +150,8 @@ "Wyślij edytowaną wiadomość" "Wyślij wiadomość" "Wyślij wiadomość głosową" - "Udostępnij" - "Udostępnij link" + "Wyślij" + "Wyślij link" "Pokaż" "Zaloguj się ponownie" "Wyloguj" diff --git a/libraries/ui-strings/src/main/res/values-ru/translations.xml b/libraries/ui-strings/src/main/res/values-ru/translations.xml index e03a51eeb3..5e4d7f9b67 100644 --- a/libraries/ui-strings/src/main/res/values-ru/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ru/translations.xml @@ -52,6 +52,7 @@ "Требуется действие, на которое есть ограничение по времени, у вас есть одна минута для проверки" "Показать пароль" "Начать звонок" + "Начать видеозвонок" "Начать голосовой вызов" "Брошенная комната" "Аватар пользователя" @@ -143,7 +144,7 @@ "Удалить подпись" "Удалить сообщение" "Ответить" - "Ответить в ветке" + "Обсудить" "Пожаловаться" "Сообщить об ошибке" "Пожаловаться на содержание" @@ -382,7 +383,8 @@ "Системное" "Текст" "Уведомления о третьих лицах" - "Ветка" + "Обсуждение" + "Обсуждения" "Тема" "О чем эта комната?" "Невозможно расшифровать" @@ -475,6 +477,13 @@ "Удалить %1$s" "Настройки" "Не удалось выбрать медиа, попробуйте еще раз." + "Открыть Element Classic" + "Откройте Element Classic на своем устройстве." + "Перейдите в Настройки > Безопасность и конфиденциальность" + "В разделе «Управление криптографическими ключами» выбери «Восстановление зашифрованных сообщений»" + "Следуйте инструкциям, чтобы активировать хранилище ключей" + "Вернитесь к %1$s" + "Перед продолжением активируйте хранилище ключей %1$s" "С возвращением" "Нажмите на сообщение и выберите «%1$s», чтобы добавить его сюда." "Закрепите важные сообщения, чтобы их можно было легко найти" @@ -488,7 +497,7 @@ "Не можете подтвердить? Перейдите в свой аккаунт, чтобы сбросить свою личность." "Сбросить верификацию и отправить" "Вы можете либо сбросить подтверждение и всё равно отправить это сообщение, либо отменить его сейчас и повторить попытку после повторного подтверждения %1$s." - "Ваше сообщение не было отправлено, потому что подтвержденная личность %1$s была сброшена" + "Ваше сообщение не было отправлено, потому что подтвержденная идентификации %1$s была сброшена" "Все равно отправить сообщение" "У %1$s есть одно или несколько неподтвержденных устройств. Вы все равно можете отправить сообщение или отменить его пока и повторить попытку позже, когда %2$s подтвердить все свои устройства." "Ваше сообщение не было отправлено, потому что %1$s имеет неподтвержденные устройства" @@ -500,6 +509,7 @@ "Сообщение в %1$s" "Развернуть" "Уменьшить" + "Поделиться текущим местоположением…" "Эта комната уже просматривается!" "%1$s из %2$s" "%1$s закрепленные сообщения" @@ -520,7 +530,7 @@ "Пространства" "Поделился %1$s" "На карте" - "Сообщение не отправлено, потому что подтвержденная личность %1$s была сброшена." + "Сообщение не отправлено, так как подтвержденная цифровая идентичность %1$s была сброшена." "Сообщение не отправлено, потому что %1$s не подтвердил(а) все свои устройства." "Сообщение не отправлено, поскольку вы не подтвердили одно или несколько своих устройств." "Местоположение" @@ -530,5 +540,5 @@ "Вам необходимо подтвердить это устройство чтобы получить доступ к истории сообщений" "Вы не имеете доступа к этому сообщению" "Не удалось расшифровать сообщение" - "Это сообщение было заблокировано, так как вы не подтвердили свое устройство, либо отправителю необходимо подтвердить вашу личность." + "Это сообщение было заблокировано либо потому, что вы не подтвердили свое устройство, либо потому, что отправителю необходимо подтвердить вашу цифровую личность." diff --git a/libraries/ui-strings/src/main/res/values-sv/translations.xml b/libraries/ui-strings/src/main/res/values-sv/translations.xml index 0969270a3c..dfcd514209 100644 --- a/libraries/ui-strings/src/main/res/values-sv/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sv/translations.xml @@ -146,8 +146,8 @@ "Dela länk" "Visa" "Logga in igen" - "Logga ut" - "Logga ut ändå" + "Ta bort den här enheten" + "Ta bort den här enheten ändå" "Hoppa över" "Starta" "Starta chat" diff --git a/libraries/ui-strings/src/main/res/values-vi/translations.xml b/libraries/ui-strings/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..1cc07bb8cf --- /dev/null +++ b/libraries/ui-strings/src/main/res/values-vi/translations.xml @@ -0,0 +1,425 @@ + + + "Thêm biểu cảm: %1$s" + "Địa chỉ" + "Ảnh đại diện" + "Thu nhỏ ô nhập tin nhắn" + "Xóa" + + "Đã nhập %1$d chữ số" + + "Đổi ảnh đại diện" + "Đường dẫn đầy đủ của phòng là %1$s" + "Chi tiết mã hóa" + "Mở rộng ô nhập tin nhắn" + "Ẩn mật khẩu" + "Tham gia cuộc gọi" + "Xuống cuối" + "Di chuyển bản đồ đến vị trí của tôi" + "Chỉ khi được đề cập tới" + "Tắt thông báo" + "Đề cập mới" + "Tin nhắn mới" + "Cuộc gọi hiện thời" + "Ảnh đại diện của người dùng khác" + "Trang %1$d" + "Tạm dừng" + "Tin nhắn thoại, thời lượng: %1$s, vị trí hiện tại: %2$s" + "Trường mã PIN" + "Vị trí được ghim" + "Phát" + "Tốc độ phát" + "Bỏ phiếu" + "Đã kết thúc cuộc thăm dò" + "Mã QR" + "Phản ứng với %1$s" + "Phản ứng với các biểu tượng cảm xúc khác" + "Đọc bởi %1$s và %2$s" + + "%1$s và %2$d người khác đã đọc." + + "Đọc bởi %1$s" + "Nhấn để hiển thị tất cả" + "Xóa phản ứng: %1$s" + "Loại bỏ phản ứng với %1$s" + "Ảnh đại diện phòng" + "Gửi tệp" + "Vị trí người gửi" + "Yêu cầu hành động có giới hạn thời gian, bạn có một phút để xác minh" + "Hiện mật khẩu" + "Gọi" + "Bắt đầu cuộc gọi thoại" + "Phòng Tombstone" + "Ảnh đại diện của người dùng" + "Menu người dùng" + "Xem ảnh đại diện" + "Xem chi tiết" + "Tin nhắn thoại, thời lượng: %1$s" + "Ghi âm tin nhắn thoại" + "Dừng ghi" + "Ảnh đại diện của bạn" + "Đồng ý" + "Thêm chú thích" + "Thêm các phòng trò chuyện hiện có" + "Thêm vào dòng thời gian" + "Quay lại" + "Gọi" + "Hủy" + "Hủy ngay" + "Chọn ảnh" + "Xoá" + "Đóng" + "Xác minh hoàn tất" + "Xác nhận" + "Xác nhận mật khẩu" + "Tiếp tục" + "Chép" + "Sao chép chú thích" + "Chép liên kết" + "Sao chép liên kết đến tin nhắn" + "Chép văn bản" + "Tạo" + "Tạo phòng" + "Tạo không gian" + "Hủy kích hoạt" + "Vô hiệu hóa tài khoản" + "Từ chối" + "Từ chối và chặn" + "Xóa cuộc thăm dò" + "Bỏ chọn tất cả" + "Tắt" + "Hủy" + "Bỏ qua" + "Xong" + "Chỉnh sửa" + "Chỉnh sửa chú thích" + "Sửa cuộc thăm dò" + "Bật" + "Kết thúc cuộc thăm dò" + "Nhập mã PIN" + "Khám phá các không gian công cộng" + "Hoàn tất" + "Quên mật khẩu?" + "Chuyển tiếp" + "Quay lại" + "Đi tới vai trò và quyền" + "Vào cài đặt" + "Bỏ qua" + "Mời" + "Mời ai đó" + "Mời ai đó vào %1$s" + "Mời ai đó vào %1$s" + "Lời mời" + "Tham gia" + "Tìm hiểu thêm" + "Rời" + "Rời khỏi cuộc trò chuyện" + "Rời phòng" + "Rời space" + "Tải thêm" + "Quản lý tài khoản" + "Quản lý tài khoản và thiết bị" + "Quản lý thiết bị" + "Quản lý phòng trò chuyện" + "Tin nhắn" + "Thu nhỏ" + "Tiếp" + "Không" + "Không phải lúc này" + "OK" + "Mở context menu" + "Cài đặt" + "Mở bằng" + "Ghim" + "Trả lời nhanh" + "Trích dẫn" + "Phản ứng" + "Từ chối" + "Xoá" + "Xóa chú thích" + "Xóa tin nhắn" + "Trả lời" + "Trả lời trong thread" + "Báo cáo" + "Báo lỗi" + "Báo cáo nội dung" + "Báo cáo cuộc trò chuyện" + "Báo cáo phòng" + "Đặt lại" + "Đặt lại danh tính" + "Thử lại" + "Thử giải mã lại" + "Lưu" + "Tìm kiếm" + "Chọn tất cả" + "Gửi" + "Gửi tin nhắn đã chỉnh sửa" + "Gửi tin nhắn" + "Gửi tin nhắn thoại" + "Chia sẻ" + "Chia sẻ liên kết" + "Chia sẻ vị trí trong thời gian thực" + "Hiện" + "Đăng nhập lại" + "Gỡ bỏ thiết bị này" + "Vẫn gỡ bỏ thiết bị này" + "Bỏ qua" + "Bắt đầu" + "Bắt đầu trò truyện" + "Bắt đầu lại" + "Bắt đầu xác thực" + "Nhấn để tải bản đồ" + "Dừng" + "Chụp ảnh" + "Nhấn để hiện tùy chọn" + "Dịch" + "Thử lại" + "Bỏ ghim" + "Xem" + "Xem trong dòng thời gian" + "Xem mã nguồn" + "Có" + "Có, thử lại" + "Máy chủ của bạn hiện đã hỗ trợ một giao thức mới, nhanh hơn. Hãy đăng xuất và đăng nhập lại để nâng cấp ngay. Việc này sẽ giúp bạn tránh bị buộc đăng xuất khi giao thức cũ bị loại bỏ sau này." + "Có thể nâng cấp" + "Giới thiệu" + "Quy định sử dụng dịch vụ" + "Thêm tài khoản" + "Thêm tài khoản khác" + "Thêm chú thích" + "Cài đặt nâng cao" + "một hình ảnh" + "Phân tích" + "Đang đồng bộ thông báo…" + "Bạn rời phòng" + "Bạn đã bị đăng xuất" + "Giao diện" + "Âm thanh" + "Thử nghiệm" + "Người dùng bị chặn" + "Bong bóng" + "Cuộc gọi bắt đầu" + "Sao lưu cuộc trò chuyện" + "Đã sao chép" + "Bản quyền" + "Đang tạo phòng…" + "Tạo không gian…" + "Yêu cầu bị hủy bỏ" + "Đã rời khỏi phòng" + "Rời khỏi không gian" + "Lời mời bị từ chối" + "Tối" + "Lỗi khi giải mã" + "Mô tả" + "Tùy chọn nhà phát triển" + "ID thiết bị" + "Chat trực tiếp" + "Không hiển thị lại" + "Tải xuống thất bại" + "Đang tải xuống" + "(đã sửa)" + "Đang chỉnh sửa" + "Chỉnh sửa chú thích" + "*%1$s%2$s" + "Tệp trống." + "Mã hóa" + "Đã bật mã hoá" + "Kết thúc lúc %1$s" + "Nhập mã PIN của bạn" + "Lỗi" + "Đã xảy ra lỗi, bạn có thể không nhận được thông báo cho tin nhắn mới. Vui lòng khắc phục sự cố thông báo trong phần cài đặt. + +Lý do: %1$s ." + "Mọi người" + "Thất bại" + "Yêu thích" + "Được yêu thích" + "Tập tin" + "Tệp đã bị xóa" + "Tệp đã được lưu" + "Tệp đã được lưu vào thư mục Tải xuống" + "Chuyển tiếp tin nhắn" + "Thường được sử dụng" + "Ảnh động" + "Ảnh" + "Trả lời %1$s" + "Cài đặt APK" + "Không tìm thấy Matrix ID này, nên lời mời có thể chưa được nhận." + "Rời khỏi phòng" + "Rời khỏi không gian" + "Sáng" + "Đã sao chép dòng" + "Đã chép liên kết vào bộ nhớ tạm" + "Liên kết thiết bị mới" + "Vị trí trong thời gian thực" + "Chia sẻ vị trí trực tiếp đã kết thúc" + "Đang tải" + "Đang tải thêm…" + + "%d người khác" + + + "%1$d số thành viên" + + "Tin nhắn" + "Thao tác tin nhắn" + "Không gửi được tin nhắn" + "Bố cục tin nhắn" + "Tin nhắn bị xoá" + "Hiện đại" + "Tắt tiếng" + "Tên" + "Không có kết quả" + "Không được mã hóa" + "Ngoại tuyến" + "hoặc" + "Mật khẩu" + "Danh bạ" + "Liên kết cố định" + "Quyền truy cập" + "Bạn có chắc chắn muốn kết thúc cuộc thăm dò này không?" + "Khảo sát: %1$s" + "Tổng số phiếu: %1$s" + "Kết quả sẽ hiển thị sau khi cuộc thăm dò kết thúc" + + "%d lượt bình chọn" + + "Chính sách bảo mật" + "Phòng riêng tư" + "Biểu cảm" + "Cảm xúc" + "Khóa khôi phục." + "Đang làm mới…" + "Đang trả lời cho %1$s" + "Báo cáo lỗi" + "Báo cáo sự cố" + "Đã gửi báo cáo" + "Trình soạn thảo văn bản nâng cao" + "Phòng" + "Tên phòng" + "ví dụ: tên dự án của bạn" + "Đã lưu thay đổi" + "Đang lưu" + "Khóa màn hình" + "Tìm kiếm ai đó" + "Kết quả tìm kiếm" + "Bảo mật" + "Được xem bởi" + "Đang gửi…" + "Không gửi được" + "Đã gửi" + "Máy chủ không được hỗ trợ" + "Không thể kết nối với máy chủ" + "URL máy chủ" + "Cài đặt" + "Chia sẻ không gian" + "Thành viên mới có thể xem lịch sử." + "Chia sẻ vị trí trực tiếp" + "Vị trí được chia sẻ" + "Không gian chung" + "Đang gỡ thiết bị" + "Đã xảy ra sự cố" + "Đã xảy ra lỗi. Vui lòng thử lại." + "Không gian" + "Thành viên không gian" + "Không gian này dùng để làm gì?" + "Đang bắt đầu cuộc trò chuyện…" + "Sticker" + "Thành công" + "Gợi ý" + "Gợi ý" + "Đang đồng bộ" + "Hệ thống" + "Văn bản" + "Thông báo từ bên thứ ba" + "Chủ đề" + "Chủ đề" + "Chủ đề" + "Phòng này dùng để làm gì?" + "Không thể giải mã" + "Bạn không thể xem tin nhắn này" + "Không thể gửi lời mời đến một hoặc nhiều người dùng." + "Không thể gửi lời mời" + "Mở khóa" + "Bật tiếng" + "Cuộc gọi không được hỗ trợ" + "Sự kiện không được hỗ trợ" + "Tên người dùng" + "Đã hủy xác thực" + "Xác minh hoàn tất" + "Xác minh thất bại" + "Đã xác minh" + "Xác minh thiết bị" + "Xác minh danh tính" + "Xác minh người dùng" + "Video" + "Chất lượng cao" + "Chất lượng tốt nhất nhưng dung lượng tệp lớn hơn" + "Chất lượng thấp" + "Tốc độ tải lên nhanh nhất và kích thước tệp nhỏ nhất" + "Chất lượng tiêu chuẩn" + "Cân bằng giữa chất lượng và tốc độ tải lên" + "Tin nhắn thoại" + "Đang chờ…" + "Đang chờ tin nhắn này" + "Đang chờ vị trí trực tiếp…" + "Ai cũng có thể xem lịch sử" + "Bạn" + "%1$s (%2$s) đã chia sẻ tin nhắn này vì bạn không có trong phòng khi nó được gửi." + "%1$s đã chia sẻ tin nhắn này vì bạn không có trong phòng khi nó được gửi." + "Phòng chat này đã được thiết lập để các thành viên mới có thể xem lịch sử trò chuyện. %1$s" + "Danh tính số của %1$s đã được đặt lại. %2$s" + "Danh tính số %2$s của %1$s đã được đặt lại. %3$s" + "(%1$s )" + "Danh tính số của %1$s đã được đặt lại." + "Danh tính số %2$s của %1$s đã được đặt lại. %3$s" + "Hủy xác minh" + "Cho phép truy cập" + "Liên kết %1$s sẽ đưa bạn đến một trang khác %2$s + +Bạn có chắc muốn tiếp tục không?" + "Kiểm tra lại liên kết này" + "Chọn chất lượng mặc định cho video bạn tải lên." + "Kích thước tệp tối đa cho phép là: %1$s" + "Kích thước tệp quá lớn để tải lên" + "Phòng đã được báo cáo" + "Xác nhận" + "Lỗi" + "Thành công" + "Cảnh báo" + "Bạn có thay đổi chưa được lưu." + "Các thay đổi của bạn chưa được lưu. Bạn có chắc muốn quay lại không?" + "Lưu thay đổi?" + "Kích thước tệp tối đa cho phép là: %1$s" + "Chọn chất lượng video bạn muốn tải lên." + "Chọn chất lượng tải lên video" + "Không tạo được liên kết cố định" + "%1$s không thể tải bản đồ. Vui lòng thử lại sau." + "Không tải được tin nhắn" + "%1$s không thể truy cập vị trí của bạn. Vui lòng thử lại sau." + "Không thể tải lên tin nhắn thoại của bạn." + "%1$s không có quyền truy cập vị trí của bạn. Bạn có thể bật quyền trong Cài đặt." + "%1$s chưa được phép truy cập vị trí. Bật quyền dưới đây." + "%1$s không có quyền truy cập micro của bạn. Hãy bật quyền để ghi tin nhắn thoại." + "Một số tin nhắn chưa được gửi" + "Rất tiếc, đã có lỗi xảy ra." + "🔐️ Tham gia cùng tôi trên %1$s" + "Xin chào, hãy trò chuyện với tôi trên %1$s bằng đường liên kết sau: %2$s" + "%1$s Android" + "Lắc điện thoại để báo cáo lỗi" + "Không thể chọn tệp phương tiện. Vui lòng thử lại." + "Tin nhắn được ghim" + "Xử lý phương tiện tải lên không thành công, vui lòng thử lại." + "Không thể lấy thông tin người dùng" + "%1$s Tin nhắn được ghim" + "Chia sẻ vị trí" + "Chia sẻ vị trí của tôi" + "Mở trong Apple Maps" + "Mở trong Google Maps" + "Mở trong OpenStreetMap" + "Chia sẻ vị trí đã chọn" + "Vị trí" + "Phiên bản: %1$s (%2$s )" + "en" + "Bạn không thể xem tin nhắn này" + diff --git a/libraries/ui-strings/src/main/res/values-zh/translations.xml b/libraries/ui-strings/src/main/res/values-zh/translations.xml index 44f7c1c1e6..1ce3a75ced 100644 --- a/libraries/ui-strings/src/main/res/values-zh/translations.xml +++ b/libraries/ui-strings/src/main/res/values-zh/translations.xml @@ -9,7 +9,7 @@ "已输入 %1$d 个数字" "编辑头像" - "完整地址为%1$s" + "完整地址为 %1$s" "加密详情" "展开消息文本框" "隐藏密码" @@ -26,11 +26,12 @@ "暂停" "语音消息,时长:%1$s,当前位置:%2$s" "PIN 字段" + "已钉住的位置" "播放" "播放速度" "投票" "投票已结束" - "QR 码" + "二维码" "使用 %1$s 回应" "使用其他表情符号回应" "%1$s 和 %2$s 已读" @@ -43,10 +44,12 @@ "移除表情符号%1$s" "房间头像" "发送文件" + "发送方位置" "限时操作,您有一分钟的时间来验证" "显示密码" "开始通话" - "墓碑聊天室" + "发起语音通话" + "已封存的聊天室" "用户头像" "用户菜单" "查看头像" @@ -122,7 +125,7 @@ "下一步" "否" "以后再说" - "好" + "确定" "打开上下文菜单" "打开设置" "用其他方式打开" @@ -154,10 +157,11 @@ "发送语音消息" "分享" "分享链接" + "共享实时位置" "显示" "再次登录" - "登出" - "仍然登出" + "删除此设备" + "仍要删除此设备" "跳过" "开始" "开始聊天" @@ -185,7 +189,7 @@ "一张图片" "分析" "你离开了聊天室" - "您已被注销当前会话" + "您已退出会话" "外观" "音频" "测试版" @@ -267,6 +271,7 @@ "离线" "开源许可证" "或" + "其他选项" "密码" "用户" "固定链接" @@ -276,7 +281,7 @@ "请稍候……" "确定要结束这个投票吗?" "投票:%1$s" - "总票数: %1$s" + "总票数:%1$s" "结果将在投票结束后显示" "%d 票" @@ -358,7 +363,7 @@ "无法解密" "从不安全的设备发送" "无权访问此消息" - "发送者的已验证身份已重置" + "发送者的已验证数字身份已重置" "无法向部分用户发送邀请。" "无法发送邀请" "解锁" @@ -388,12 +393,13 @@ "%1$s (%2$s) 由于您当时不在聊天室内,系统已将消息共享给您。" "%1$s 由于您当时不在聊天室内,系统已将此消息共享给您。" "本聊天室已配置为允许新成员阅读历史记录。%1$s" - "%1$s的身份已重置。%2$s" - "%1$s %2$s 的身份已重置。%3$s" + "%1$s的数字身份已重置。%2$s" + "%1$s %2$s 的数字身份已重置。%3$s" "(%1$s)" - "%1$s 的身份已重置。" - "%1$s %2$s 的身份已重置。%3$s" + "%1$s 的数字身份已重置。" + "%1$s %2$s 的数字身份已重置。%3$s" "撤回验证" + "允许访问" "链接 %1$s 将跳转至外部网站 %2$s 确定要继续吗?" @@ -423,6 +429,7 @@ "%1$s 无法访问您的位置,请稍后再试。" "无法上传语音消息。" "该房间已不存在或邀请已失效。" + "请开启 GPS 以使用基于位置的功能。" "找不到消息" "%1$s 没有权限访问您的位置。您可以在设置中启用位置权限。" "%1$s 没有权限访问您的位置。在下方启用位置权限。" @@ -442,17 +449,18 @@ "移除%1$s" "设置" "选择媒体失败,请重试。" + "欢迎回来" "按下消息并选择 “%1$s” 将其包含在此处。" "固定重要消息,以便轻松发现它们" "%1$d 置顶消息" "置顶消息" - "您将要转到您的%1$s帐户来重置您的身份信息。之后,您将被带回该应用。" - "无法确认?请前往您的帐户重置您的身份。" + "您将要转到您的%1$s帐户来重置您的数字身份。之后,您将被带回该应用。" + "无法确认?请前往您的帐户重置您的数字身份。" "撤回验证并发送" "您可以撤回验证并仍然发送此消息;也可以暂时取消验证,在重新验证 %1$s 后重试。" - "您的消息未发送,因为%1$s的已验证身份已被重置" + "您的消息未发送,因为%1$s的已验证数字身份已被重置" "仍然发送消息" "%1$s 正在使用一个或多个未经验证的设备。您还是可以继续发送信息;也可以暂时取消,等 %2$s 验证了所有设备后重试。" "您的消息未发送,因为%1$s尚未验证所有设备" @@ -475,13 +483,16 @@ "在 Apple Maps 中打开" "在 Google Maps 中打开" "在 OpenStreetMap 中打开" - "分享这个位置" + "分享选定的位置" + "共享选项" "您创建或加入的空间。" "%1$s • %2$s" "创建空间以组织聊天室" "%1$s空间" "空间" - "消息未发送,因为%1$s的已验证身份已被重置。" + "共享于%1$s" + "在地图上" + "消息未发送,因为%1$s的已验证数字身份已被重置。" "消息未发送,因为%1$s尚未验证所有设备。" "消息未发送,因为您有尚未验证的设备。" "位置" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 457aec7d80..f91e3a85b0 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -50,6 +50,7 @@ "Time limited action required, you have one minute to verify" "Show password" "Start a call" + "Start a video call" "Start a voice call" "Tombstoned room" "User avatar" @@ -467,6 +468,14 @@ Are you sure you want to continue?" "Remove %1$s" "Settings" "Failed selecting media, please try again." + "Open Element Classic" + "Open Element Classic on your device" + "Go to Settings > Security & Privacy" + "In Cryptography keys management, select Encrypted messages recovery" + "Follow the instructions to enable your key storage" + "Come back to %1$s" + "Enable your key storage before proceeding to %1$s" + "Checking account" "Welcome back" "Press on a message and choose “%1$s” to include here." "Pin important messages so that they can be easily discovered" @@ -491,6 +500,7 @@ Are you sure you want to continue?" "Message in %1$s" "Expand" "Reduce" + "Sharing live location" "Already viewing this room!" "%1$s of %2$s" "%1$s Pinned messages" diff --git a/libraries/voiceplayer/impl/src/test/kotlin/io/element/android/libraries/voiceplayer/impl/DefaultVoiceMessageMediaRepoTest.kt b/libraries/voiceplayer/impl/src/test/kotlin/io/element/android/libraries/voiceplayer/impl/DefaultVoiceMessageMediaRepoTest.kt index 5df259fb5b..fad530a3e8 100644 --- a/libraries/voiceplayer/impl/src/test/kotlin/io/element/android/libraries/voiceplayer/impl/DefaultVoiceMessageMediaRepoTest.kt +++ b/libraries/voiceplayer/impl/src/test/kotlin/io/element/android/libraries/voiceplayer/impl/DefaultVoiceMessageMediaRepoTest.kt @@ -32,6 +32,9 @@ class DefaultVoiceMessageMediaRepoTest { val repo = createDefaultVoiceMessageMediaRepo( temporaryFolder = temporaryFolder, matrixMediaLoader = matrixMediaLoader, + mxcUri2FilePathResult = { + "matrix.org/1234567890abcdefg" + }, ) repo.getMediaFile().let { result -> @@ -76,6 +79,9 @@ class DefaultVoiceMessageMediaRepoTest { val repo = createDefaultVoiceMessageMediaRepo( temporaryFolder = temporaryFolder, matrixMediaLoader = matrixMediaLoader, + mxcUri2FilePathResult = { + "matrix.org/1234567890abcdefg" + }, ) repo.getMediaFile().let { result -> @@ -98,6 +104,9 @@ class DefaultVoiceMessageMediaRepoTest { val repo = createDefaultVoiceMessageMediaRepo( temporaryFolder = temporaryFolder, matrixMediaLoader = matrixMediaLoader, + mxcUri2FilePathResult = { + "matrix.org/1234567890abcdefg" + }, ) repo.getMediaFile().let { result -> @@ -128,10 +137,13 @@ class DefaultVoiceMessageMediaRepoTest { private fun createDefaultVoiceMessageMediaRepo( temporaryFolder: TemporaryFolder, matrixMediaLoader: MatrixMediaLoader = FakeMatrixMediaLoader(), + mxcUri2FilePathResult: (String) -> String? = { null }, mxcUri: String = MXC_URI, ) = DefaultVoiceMessageMediaRepo( cacheDir = temporaryFolder.root, - mxcTools = FakeMxcTools(), + mxcTools = FakeMxcTools( + mxcUri2FilePathResult = mxcUri2FilePathResult, + ), matrixMediaLoader = matrixMediaLoader, mediaSource = MediaSource( url = mxcUri, diff --git a/plugins/src/main/kotlin/Versions.kt b/plugins/src/main/kotlin/Versions.kt index f46abc71bf..93e92b81ce 100644 --- a/plugins/src/main/kotlin/Versions.kt +++ b/plugins/src/main/kotlin/Versions.kt @@ -45,7 +45,7 @@ private const val versionMonth = 4 * Release number in the month. Value must be in [0,99]. * Do not update this value. it is updated by the release script. */ -private const val versionReleaseNumber = 2 +private const val versionReleaseNumber = 3 object Versions { /** diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index 8a5dfa2c4a..ce5c324ff4 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -49,6 +49,7 @@ fun DependencyHandlerScope.testCommonDependencies( testImplementation(libs.test.arch.core) testImplementation(libs.test.junit) testImplementation(libs.test.mockk) + testImplementation(libs.test.parameter.injector) testImplementation(libs.test.robolectric) testImplementation(libs.test.truth) testImplementation(libs.test.turbine) diff --git a/plugins/src/main/kotlin/extension/locales.kt b/plugins/src/main/kotlin/extension/locales.kt index 1af7146681..650fbc4d72 100644 --- a/plugins/src/main/kotlin/extension/locales.kt +++ b/plugins/src/main/kotlin/extension/locales.kt @@ -22,6 +22,7 @@ val locales = setOf( "hu", "in", "it", + "ja", "ka", "ko", "lt", @@ -38,6 +39,7 @@ val locales = setOf( "uk", "ur", "uz", + "vi", "zh-rCN", "zh-rTW", ) diff --git a/screenshots/de/features.home.impl.spaces_HomeSpacesView_Day_2_de.png b/screenshots/de/features.home.impl.spaces_HomeSpacesView_Day_2_de.png index 0d394678ce..f0e44135ec 100644 --- a/screenshots/de/features.home.impl.spaces_HomeSpacesView_Day_2_de.png +++ b/screenshots/de/features.home.impl.spaces_HomeSpacesView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c50c374b59c957f4e007d9548c2e03ffb622fcba3de9bb0adb2e36336114263b -size 39053 +oid sha256:c7f97df762e010742fb2627a83e0939f4e190195200c53004412e01c6361cb43 +size 28480 diff --git a/screenshots/de/features.home.impl.spaces_HomeSpacesView_Day_3_de.png b/screenshots/de/features.home.impl.spaces_HomeSpacesView_Day_3_de.png deleted file mode 100644 index f0e44135ec..0000000000 --- a/screenshots/de/features.home.impl.spaces_HomeSpacesView_Day_3_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c7f97df762e010742fb2627a83e0939f4e190195200c53004412e01c6361cb43 -size 28480 diff --git a/screenshots/de/features.location.impl.share_ShareLocationView_Day_5_de.png b/screenshots/de/features.location.impl.share_ShareLocationView_Day_5_de.png index bdd4cde724..18dc2c74b7 100644 --- a/screenshots/de/features.location.impl.share_ShareLocationView_Day_5_de.png +++ b/screenshots/de/features.location.impl.share_ShareLocationView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e17424dbbfb6a537bcd285ab3d4a4a8567a61cba89b17b7ac1caddbc26dfde6 -size 17320 +oid sha256:6edd90a847c7fc84c25545313f90ec41b8769d4a9b758476725f7ec5255fc167 +size 21838 diff --git a/screenshots/de/features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Day_0_de.png b/screenshots/de/features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Day_0_de.png index 057548a09c..ee068ebc52 100644 --- a/screenshots/de/features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Day_0_de.png +++ b/screenshots/de/features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7fd7dc09638aef0559bd4b1c75800648d9f37c67d96fde244eb064a4374c67e8 -size 21574 +oid sha256:a237e342ba5bce3980d0f0164afe658993741ff8238e7cbdf9c73f7eb83f2f4c +size 36792 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_de.png index 538acbb4fd..36358041e6 100644 --- a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_de.png +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:180a33d511c0bcea775972eb4c0f80856d40657d0683eaee67e3d9b58687ef3e -size 365444 +oid sha256:4f44887b6c1d8c3de4e1bc4ae42f3999b99db906062ba95df22c88a2951f10d1 +size 364996 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_de.png index 7d0a21c3d6..dd1594478c 100644 --- a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_de.png +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e2d3bbb762ad093c0e60d1bb1f838d078449a1f3d733771f35660c3bbe5b374b -size 370934 +oid sha256:335e62d5380a9f6eba40017ed5f8bcfde6bacc7aaf29995852bf2c9c38960a09 +size 370563 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_de.png index de58d2b1bf..6f4a81b5bb 100644 --- a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_de.png +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:29c9f1d6d7972e8fb27701407eeb6febae153e92e60ac21f60a5bcb01beeede8 -size 364778 +oid sha256:6b6a749b9aefbf37b9bb4d9edcbf91f857acc395344a58439079268c73eb173f +size 364271 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_de.png index 40d6fe691b..bce9fe091c 100644 --- a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_de.png +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1fb43a66477ee416f6e8bea4acdecea322fc9cac09c5cc4c8fe6644fdefe78df -size 366590 +oid sha256:caf04029d097a3fcd937ecee55381b15353ca70b180629f3cc6b35dcde671c45 +size 366135 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_de.png index 9b074e7cae..77f972cdd8 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5aea70f07d78c618613628503eae01783fef2561e963e4249ab89813886ab8f4 -size 73530 +oid sha256:49de6907472a673c5b79ae0f20a9939f9f6786fc757b9e1ac96077be34557d06 +size 73249 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_de.png index c5b0d39e9d..ac61252e96 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:41a9554ef3d3c8a9cf0b2234335728528b212caf954086ac7f8767fe795ec0c0 -size 60076 +oid sha256:29a442ba2414d64a550f4bded4835e11daeff0bd55031689af7072fba0fadcfd +size 59788 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_de.png index bcc10c35e7..e9b333dc05 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4bf87c47d0cee012847b5f0a0264f6f4e4cb948f965ad28af58fa5dac6b7e3a6 -size 73133 +oid sha256:e5493a95039dae9babedde3214acbbff01c7b53a3b454251f234c6cdb09bbd35 +size 72797 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_de.png index 7218918f91..03a3fc0a40 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:abd564eae0efae348f0cafbb5749879b29f2d2b0a82eb645cd0db4e75ada4ff3 -size 81908 +oid sha256:e817846aa9983e8e38ba6c8ff29ec205636e7b9a72b461215c16eb06e22ceb0e +size 81614 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_de.png index e3d9c74293..bef0dfd03d 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1aa1be60455968000254246a1b307c8336e2c8e36cee5122e159254adeacf78e -size 62919 +oid sha256:ddf5dbaf0ad4247513d38a34e89897d099daa8e3f6c0bf9495ac65dad0771f34 +size 62532 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_de.png index af6d69fde2..9267d1cb6c 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1bb0d795e03931704043807bedc666f129ca23aa60b2690957588b15c2a6b5ff -size 61770 +oid sha256:35b64678b72a2e2433d278f717d859dc6f53414db565ed325604e0911e8a7e4b +size 61375 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_de.png index 33ff1188ae..a43cafd104 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a40c856d3959ea432fa4a5bc5624e10005d8f405c5b5c199acfa38c3107035c -size 67354 +oid sha256:59b8beb90fbe005555a4de1d67230362623ab9b715796abb3a223f028bf89d5a +size 66981 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_de.png index b100db999d..1f186b58be 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:797c78b06b51daa6088b94f38e536d59b619340a270bf7a4f8cbbf08c6c44be8 -size 90472 +oid sha256:0ad07ef8db1754dadbca19207273bbbffc83a591b615c22a6d38fcb8322af072 +size 90155 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_de.png index 2c0063362c..558f92c29b 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9c431aa38578a656d8f1bf8f58d2fe8534772ed127c2ce102b377052b007306e -size 60994 +oid sha256:e130d13f832caaa3b4016c2a915610bde46f0d34e7b394d52ccfb96f367afa74 +size 60683 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_de.png index 2141974bd3..0db525b4a1 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ecddf9e9d5c75c2675235b5399d899cb8ae24383d32ea1bdf1899cb95fb8be54 -size 61138 +oid sha256:72db00abddd84b586596e1dc61fe89b27096fbc08ba82d23eaf703c5c83173fd +size 60746 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_de.png index d0d960153f..b14578a682 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:beb5791aeff09f5e757c8f560cc9b705c208eb886462a840bfcab7d78e622c15 -size 69793 +oid sha256:926284ff1d11c483707b568100fbdc8382a1a61ba146a0ef23594089bb3038c8 +size 69406 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_de.png index 22917ebb0f..340cea1e36 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f3285a648f80647c159933a083458fefd4d7f59687b74baacfd4a8dc7296ff05 -size 60504 +oid sha256:2cf0dd833df3ace9700622b31f64513a3c76908d6651dbb0ffcc3b5a6a4dce9e +size 60201 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_0_de.png index 632d13a773..ef613cf899 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_0_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6f61101788599662175adfd0b8e3fffa7d0152db1019e8424b2a3011572d1a01 -size 73241 +oid sha256:164558c7d8cdd701a1d11e4f3df0448818a47eecb06024904e14c29651800f56 +size 72929 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_10_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_10_de.png index 05dae33d65..a16f0aaf45 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_10_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_10_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7d5e0c9b9c13b2e3d1cdfb1cbd666ced079e0faf230febc2377b9e10fab58be -size 56519 +oid sha256:b801d74b3feee574ba81d9d2e270090c86e3c007daa9b96674c752e6428a5d9e +size 56175 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_11_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_11_de.png index 2443e232c8..39c6f3f77f 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_11_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_11_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:81030d66c1ddbdf113dd49ea1a566cff5e6591d4733d27925673a99ad2f413f8 -size 71646 +oid sha256:e088310a4666c06e55fa88ef9d209e24ff681f9caaf148b1f1cbf4e0a8954206 +size 71274 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_1_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_1_de.png index 5b4f8dddd2..a906ed4d88 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_1_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3c7ef69c814541c026cf01b4466cb10f8737db3f673e81dbb57d1e5e8902f972 -size 83116 +oid sha256:8ffff1b75987e4b692fbec95fdc6bac243fdfb741e24d56f2e0f3b05c1b9a9a2 +size 82793 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_2_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_2_de.png index e6cd15541d..6ffea11e25 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_2_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:081a69d09ed70a4bbba08690df9dab86d4a10a980f1330b3b27943cb1c10dff5 -size 59746 +oid sha256:17b44b1796d438eb804fec2aa1935c9ef0fbac16b90c55ee57ae0393862391c8 +size 59342 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_3_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_3_de.png index c8eec3eb85..0bcdff0b59 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_3_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:893e712436eafea1791ad4bde6ef16b6e8389ed6dfcf6f5cd206f4e5cb25b44a -size 58863 +oid sha256:467e422cef79f2be11df55814794472113b434822434301e52bdfd12c094deae +size 58453 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_4_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_4_de.png index 23c18ff2f0..d28b54f3cc 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_4_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:72906d598feb20fba5d764485aa54ec3b3eb49ba9574dea5f97fefe8a5519a45 -size 66137 +oid sha256:e15f573b4d081b17f91ad2e19cc4920063dad46755217145fb8b13288eb2ad3b +size 65754 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_5_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_5_de.png index 29d057fff0..1e87d303a9 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_5_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fb9602983bbbf519d40a602fd7670585cf376aeb9a72a95e6da4077aae2d340b -size 101576 +oid sha256:d80a1a50b289631c432e1247af2ec1a41a9cb233f4ef9c5f6c964784113ff401 +size 101387 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_6_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_6_de.png index ba9ec7f709..7903b1ba42 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_6_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b8e52bb1582b86c3ba756282de449d61c673d7750c35813a712061456f246e4a -size 57799 +oid sha256:157c70292051f5dd737f309b2bdadddbe6cbf7ec3b322a3c47b937edc4df72e0 +size 57461 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_7_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_7_de.png index 6313c48a1e..a997076a87 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_7_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9da73f77e0e7aa91c0620b6714dcf81234939f42773ef2afeb6bd920af6d3593 -size 57852 +oid sha256:44df9e6774110798c72042f560bcc344a5b292f11cba5893190ad78513a8d07e +size 57468 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_8_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_8_de.png index c115f5a9db..b198b87ff5 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_8_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ae4658c6ce320cf09ca92d8321736d5079e085d7d41791ff33a2811a48fe2da -size 68025 +oid sha256:8323ee0e5869c5408cc17476fb603af972fa570d1e6896bc063ce401f4d1582f +size 67617 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_9_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_9_de.png index 0b321d53de..5515d78ee1 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_9_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_9_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ae8ddc2b5c7794bebb89110d9538c86f9ad90e1edc6df837b1710f19bd13aed -size 57175 +oid sha256:31e974364b144d6270b890bf7293ca84a8680105d3914d5d6d2c1fa766b463a1 +size 56853 diff --git a/screenshots/de/services.apperror.impl_AppErrorView_Day_0_de.png b/screenshots/de/services.apperror.api_AppErrorView_Day_0_de.png similarity index 100% rename from screenshots/de/services.apperror.impl_AppErrorView_Day_0_de.png rename to screenshots/de/services.apperror.api_AppErrorView_Day_0_de.png diff --git a/screenshots/html/data.js b/screenshots/html/data.js index 1ecfab0491..df612e28d9 100644 --- a/screenshots/html/data.js +++ b/screenshots/html/data.js @@ -1,87 +1,87 @@ // Generated file, do not edit export const screenshots = [ ["en","en-dark","de",], -["features.preferences.impl.about_AboutView_Day_0_en","features.preferences.impl.about_AboutView_Night_0_en",20539,], +["features.preferences.impl.about_AboutView_Day_0_en","features.preferences.impl.about_AboutView_Night_0_en",20553,], ["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_0_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_0_en",0,], -["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_1_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_1_en",20539,], -["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_2_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_2_en",20539,], -["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_3_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_3_en",20539,], -["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_4_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_4_en",20539,], -["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_5_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_5_en",20539,], -["features.logout.impl_AccountDeactivationView_Day_0_en","features.logout.impl_AccountDeactivationView_Night_0_en",20539,], -["features.logout.impl_AccountDeactivationView_Day_1_en","features.logout.impl_AccountDeactivationView_Night_1_en",20539,], -["features.logout.impl_AccountDeactivationView_Day_2_en","features.logout.impl_AccountDeactivationView_Night_2_en",20539,], -["features.logout.impl_AccountDeactivationView_Day_3_en","features.logout.impl_AccountDeactivationView_Night_3_en",20539,], -["features.logout.impl_AccountDeactivationView_Day_4_en","features.logout.impl_AccountDeactivationView_Night_4_en",20539,], -["features.login.impl.accountprovider_AccountProviderOtherView_Day_0_en","features.login.impl.accountprovider_AccountProviderOtherView_Night_0_en",20539,], +["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_1_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_1_en",20553,], +["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_2_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_2_en",20553,], +["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_3_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_3_en",20553,], +["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_4_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_4_en",20553,], +["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_5_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_5_en",20553,], +["features.logout.impl_AccountDeactivationView_Day_0_en","features.logout.impl_AccountDeactivationView_Night_0_en",20553,], +["features.logout.impl_AccountDeactivationView_Day_1_en","features.logout.impl_AccountDeactivationView_Night_1_en",20553,], +["features.logout.impl_AccountDeactivationView_Day_2_en","features.logout.impl_AccountDeactivationView_Night_2_en",20553,], +["features.logout.impl_AccountDeactivationView_Day_3_en","features.logout.impl_AccountDeactivationView_Night_3_en",20553,], +["features.logout.impl_AccountDeactivationView_Day_4_en","features.logout.impl_AccountDeactivationView_Night_4_en",20553,], +["features.login.impl.accountprovider_AccountProviderOtherView_Day_0_en","features.login.impl.accountprovider_AccountProviderOtherView_Night_0_en",20553,], ["features.login.impl.accountprovider_AccountProviderView_Day_0_en","features.login.impl.accountprovider_AccountProviderView_Night_0_en",0,], ["features.login.impl.accountprovider_AccountProviderView_Day_1_en","features.login.impl.accountprovider_AccountProviderView_Night_1_en",0,], ["features.login.impl.accountprovider_AccountProviderView_Day_2_en","features.login.impl.accountprovider_AccountProviderView_Night_2_en",0,], ["features.login.impl.accountprovider_AccountProviderView_Day_3_en","features.login.impl.accountprovider_AccountProviderView_Night_3_en",0,], -["libraries.accountselect.impl_AccountSelectView_Day_0_en","libraries.accountselect.impl_AccountSelectView_Night_0_en",20539,], -["libraries.accountselect.impl_AccountSelectView_Day_1_en","libraries.accountselect.impl_AccountSelectView_Night_1_en",20539,], +["libraries.accountselect.impl_AccountSelectView_Day_0_en","libraries.accountselect.impl_AccountSelectView_Night_0_en",20553,], +["libraries.accountselect.impl_AccountSelectView_Day_1_en","libraries.accountselect.impl_AccountSelectView_Night_1_en",20553,], ["features.messages.impl.actionlist_ActionListViewContent_Day_0_en","features.messages.impl.actionlist_ActionListViewContent_Night_0_en",0,], -["features.messages.impl.actionlist_ActionListViewContent_Day_10_en","features.messages.impl.actionlist_ActionListViewContent_Night_10_en",20539,], -["features.messages.impl.actionlist_ActionListViewContent_Day_11_en","features.messages.impl.actionlist_ActionListViewContent_Night_11_en",20539,], -["features.messages.impl.actionlist_ActionListViewContent_Day_12_en","features.messages.impl.actionlist_ActionListViewContent_Night_12_en",20539,], +["features.messages.impl.actionlist_ActionListViewContent_Day_10_en","features.messages.impl.actionlist_ActionListViewContent_Night_10_en",20553,], +["features.messages.impl.actionlist_ActionListViewContent_Day_11_en","features.messages.impl.actionlist_ActionListViewContent_Night_11_en",20553,], +["features.messages.impl.actionlist_ActionListViewContent_Day_12_en","features.messages.impl.actionlist_ActionListViewContent_Night_12_en",20553,], ["features.messages.impl.actionlist_ActionListViewContent_Day_1_en","features.messages.impl.actionlist_ActionListViewContent_Night_1_en",0,], -["features.messages.impl.actionlist_ActionListViewContent_Day_2_en","features.messages.impl.actionlist_ActionListViewContent_Night_2_en",20539,], -["features.messages.impl.actionlist_ActionListViewContent_Day_3_en","features.messages.impl.actionlist_ActionListViewContent_Night_3_en",20539,], -["features.messages.impl.actionlist_ActionListViewContent_Day_4_en","features.messages.impl.actionlist_ActionListViewContent_Night_4_en",20539,], -["features.messages.impl.actionlist_ActionListViewContent_Day_5_en","features.messages.impl.actionlist_ActionListViewContent_Night_5_en",20539,], -["features.messages.impl.actionlist_ActionListViewContent_Day_6_en","features.messages.impl.actionlist_ActionListViewContent_Night_6_en",20539,], -["features.messages.impl.actionlist_ActionListViewContent_Day_7_en","features.messages.impl.actionlist_ActionListViewContent_Night_7_en",20539,], -["features.messages.impl.actionlist_ActionListViewContent_Day_8_en","features.messages.impl.actionlist_ActionListViewContent_Night_8_en",20539,], -["features.messages.impl.actionlist_ActionListViewContent_Day_9_en","features.messages.impl.actionlist_ActionListViewContent_Night_9_en",20539,], -["features.createroom.impl.addpeople_AddPeopleView_Day_0_en","features.createroom.impl.addpeople_AddPeopleView_Night_0_en",20539,], -["features.createroom.impl.addpeople_AddPeopleView_Day_1_en","features.createroom.impl.addpeople_AddPeopleView_Night_1_en",20539,], -["features.createroom.impl.addpeople_AddPeopleView_Day_2_en","features.createroom.impl.addpeople_AddPeopleView_Night_2_en",20539,], -["features.createroom.impl.addpeople_AddPeopleView_Day_3_en","features.createroom.impl.addpeople_AddPeopleView_Night_3_en",20539,], -["features.space.impl.addroom_AddRoomToSpaceView_Day_0_en","features.space.impl.addroom_AddRoomToSpaceView_Night_0_en",20539,], -["features.space.impl.addroom_AddRoomToSpaceView_Day_1_en","features.space.impl.addroom_AddRoomToSpaceView_Night_1_en",20539,], -["features.space.impl.addroom_AddRoomToSpaceView_Day_2_en","features.space.impl.addroom_AddRoomToSpaceView_Night_2_en",20539,], -["features.space.impl.addroom_AddRoomToSpaceView_Day_3_en","features.space.impl.addroom_AddRoomToSpaceView_Night_3_en",20539,], -["features.space.impl.addroom_AddRoomToSpaceView_Day_4_en","features.space.impl.addroom_AddRoomToSpaceView_Night_4_en",20539,], -["features.space.impl.addroom_AddRoomToSpaceView_Day_5_en","features.space.impl.addroom_AddRoomToSpaceView_Night_5_en",20539,], -["features.space.impl.addroom_AddRoomToSpaceView_Day_6_en","features.space.impl.addroom_AddRoomToSpaceView_Night_6_en",20539,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_0_en","",20539,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_1_en","",20539,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_2_en","",20539,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_3_en","",20539,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_4_en","",20539,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_5_en","",20539,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_6_en","",20539,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_7_en","",20539,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_8_en","",20539,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_0_en","",20539,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_1_en","",20539,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_2_en","",20539,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_3_en","",20539,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_4_en","",20539,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_5_en","",20539,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_6_en","",20539,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_7_en","",20539,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_8_en","",20539,], -["libraries.designsystem.components.dialogs_AlertDialogContent_Dialogs_en","",20539,], -["libraries.designsystem.components.dialogs_AlertDialog_Day_0_en","libraries.designsystem.components.dialogs_AlertDialog_Night_0_en",20539,], +["features.messages.impl.actionlist_ActionListViewContent_Day_2_en","features.messages.impl.actionlist_ActionListViewContent_Night_2_en",20553,], +["features.messages.impl.actionlist_ActionListViewContent_Day_3_en","features.messages.impl.actionlist_ActionListViewContent_Night_3_en",20553,], +["features.messages.impl.actionlist_ActionListViewContent_Day_4_en","features.messages.impl.actionlist_ActionListViewContent_Night_4_en",20553,], +["features.messages.impl.actionlist_ActionListViewContent_Day_5_en","features.messages.impl.actionlist_ActionListViewContent_Night_5_en",20553,], +["features.messages.impl.actionlist_ActionListViewContent_Day_6_en","features.messages.impl.actionlist_ActionListViewContent_Night_6_en",20553,], +["features.messages.impl.actionlist_ActionListViewContent_Day_7_en","features.messages.impl.actionlist_ActionListViewContent_Night_7_en",20553,], +["features.messages.impl.actionlist_ActionListViewContent_Day_8_en","features.messages.impl.actionlist_ActionListViewContent_Night_8_en",20553,], +["features.messages.impl.actionlist_ActionListViewContent_Day_9_en","features.messages.impl.actionlist_ActionListViewContent_Night_9_en",20553,], +["features.createroom.impl.addpeople_AddPeopleView_Day_0_en","features.createroom.impl.addpeople_AddPeopleView_Night_0_en",20553,], +["features.createroom.impl.addpeople_AddPeopleView_Day_1_en","features.createroom.impl.addpeople_AddPeopleView_Night_1_en",20553,], +["features.createroom.impl.addpeople_AddPeopleView_Day_2_en","features.createroom.impl.addpeople_AddPeopleView_Night_2_en",20553,], +["features.createroom.impl.addpeople_AddPeopleView_Day_3_en","features.createroom.impl.addpeople_AddPeopleView_Night_3_en",20553,], +["features.space.impl.addroom_AddRoomToSpaceView_Day_0_en","features.space.impl.addroom_AddRoomToSpaceView_Night_0_en",20553,], +["features.space.impl.addroom_AddRoomToSpaceView_Day_1_en","features.space.impl.addroom_AddRoomToSpaceView_Night_1_en",20553,], +["features.space.impl.addroom_AddRoomToSpaceView_Day_2_en","features.space.impl.addroom_AddRoomToSpaceView_Night_2_en",20553,], +["features.space.impl.addroom_AddRoomToSpaceView_Day_3_en","features.space.impl.addroom_AddRoomToSpaceView_Night_3_en",20553,], +["features.space.impl.addroom_AddRoomToSpaceView_Day_4_en","features.space.impl.addroom_AddRoomToSpaceView_Night_4_en",20553,], +["features.space.impl.addroom_AddRoomToSpaceView_Day_5_en","features.space.impl.addroom_AddRoomToSpaceView_Night_5_en",20553,], +["features.space.impl.addroom_AddRoomToSpaceView_Day_6_en","features.space.impl.addroom_AddRoomToSpaceView_Night_6_en",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_0_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_1_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_2_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_3_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_4_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_5_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_6_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_7_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_8_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_0_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_1_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_2_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_3_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_4_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_5_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_6_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_7_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_8_en","",20553,], +["libraries.designsystem.components.dialogs_AlertDialogContent_Dialogs_en","",20553,], +["libraries.designsystem.components.dialogs_AlertDialog_Day_0_en","libraries.designsystem.components.dialogs_AlertDialog_Night_0_en",20553,], ["libraries.designsystem.theme.components_AllIcons_Icons_en","",0,], -["features.analytics.impl_AnalyticsOptInView_Day_0_en","features.analytics.impl_AnalyticsOptInView_Night_0_en",20539,], -["features.analytics.impl_AnalyticsOptInView_Day_1_en","features.analytics.impl_AnalyticsOptInView_Night_1_en",20539,], -["features.analytics.api.preferences_AnalyticsPreferencesView_Day_0_en","features.analytics.api.preferences_AnalyticsPreferencesView_Night_0_en",20539,], -["features.analytics.api.preferences_AnalyticsPreferencesView_Day_1_en","features.analytics.api.preferences_AnalyticsPreferencesView_Night_1_en",20539,], -["features.preferences.impl.analytics_AnalyticsSettingsView_Day_0_en","features.preferences.impl.analytics_AnalyticsSettingsView_Night_0_en",20539,], +["features.analytics.impl_AnalyticsOptInView_Day_0_en","features.analytics.impl_AnalyticsOptInView_Night_0_en",20553,], +["features.analytics.impl_AnalyticsOptInView_Day_1_en","features.analytics.impl_AnalyticsOptInView_Night_1_en",20553,], +["features.analytics.api.preferences_AnalyticsPreferencesView_Day_0_en","features.analytics.api.preferences_AnalyticsPreferencesView_Night_0_en",20553,], +["features.analytics.api.preferences_AnalyticsPreferencesView_Day_1_en","features.analytics.api.preferences_AnalyticsPreferencesView_Night_1_en",20553,], +["features.preferences.impl.analytics_AnalyticsSettingsView_Day_0_en","features.preferences.impl.analytics_AnalyticsSettingsView_Night_0_en",20553,], ["libraries.designsystem.components_Announcement_Day_0_en","libraries.designsystem.components_Announcement_Night_0_en",0,], -["services.apperror.impl_AppErrorView_Day_0_en","services.apperror.impl_AppErrorView_Night_0_en",20539,], +["services.apperror.api_AppErrorView_Day_0_en","services.apperror.api_AppErrorView_Night_0_en",20556,], ["libraries.designsystem.components.async_AsyncActionView_Day_0_en","libraries.designsystem.components.async_AsyncActionView_Night_0_en",0,], -["libraries.designsystem.components.async_AsyncActionView_Day_1_en","libraries.designsystem.components.async_AsyncActionView_Night_1_en",20539,], +["libraries.designsystem.components.async_AsyncActionView_Day_1_en","libraries.designsystem.components.async_AsyncActionView_Night_1_en",20553,], ["libraries.designsystem.components.async_AsyncActionView_Day_2_en","libraries.designsystem.components.async_AsyncActionView_Night_2_en",0,], -["libraries.designsystem.components.async_AsyncActionView_Day_3_en","libraries.designsystem.components.async_AsyncActionView_Night_3_en",20539,], +["libraries.designsystem.components.async_AsyncActionView_Day_3_en","libraries.designsystem.components.async_AsyncActionView_Night_3_en",20553,], ["libraries.designsystem.components.async_AsyncActionView_Day_4_en","libraries.designsystem.components.async_AsyncActionView_Night_4_en",0,], -["libraries.designsystem.components.async_AsyncFailure_Day_0_en","libraries.designsystem.components.async_AsyncFailure_Night_0_en",20539,], +["libraries.designsystem.components.async_AsyncFailure_Day_0_en","libraries.designsystem.components.async_AsyncFailure_Night_0_en",20553,], ["libraries.designsystem.components.async_AsyncIndicatorFailure_Day_0_en","libraries.designsystem.components.async_AsyncIndicatorFailure_Night_0_en",0,], ["libraries.designsystem.components.async_AsyncIndicatorLoading_Day_0_en","libraries.designsystem.components.async_AsyncIndicatorLoading_Night_0_en",0,], ["libraries.designsystem.components.async_AsyncLoading_Day_0_en","libraries.designsystem.components.async_AsyncLoading_Night_0_en",0,], -["features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Day_0_en","features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Night_0_en",20539,], +["features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Day_0_en","features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Night_0_en",20553,], ["libraries.matrix.ui.components_AttachmentThumbnail_Day_0_en","libraries.matrix.ui.components_AttachmentThumbnail_Night_0_en",0,], ["libraries.matrix.ui.components_AttachmentThumbnail_Day_1_en","libraries.matrix.ui.components_AttachmentThumbnail_Night_1_en",0,], ["libraries.matrix.ui.components_AttachmentThumbnail_Day_2_en","libraries.matrix.ui.components_AttachmentThumbnail_Night_2_en",0,], @@ -91,19 +91,19 @@ export const screenshots = [ ["libraries.matrix.ui.components_AttachmentThumbnail_Day_6_en","libraries.matrix.ui.components_AttachmentThumbnail_Night_6_en",0,], ["libraries.matrix.ui.components_AttachmentThumbnail_Day_7_en","libraries.matrix.ui.components_AttachmentThumbnail_Night_7_en",0,], ["libraries.matrix.ui.components_AttachmentThumbnail_Day_8_en","libraries.matrix.ui.components_AttachmentThumbnail_Night_8_en",0,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_0_en","",20539,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_1_en","",20539,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_2_en","",20539,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_3_en","",20539,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_4_en","",20539,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_5_en","",20539,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_6_en","",20539,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_7_en","",20539,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_8_en","",20539,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_0_en","",20553,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_1_en","",20553,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_2_en","",20553,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_3_en","",20553,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_4_en","",20553,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_5_en","",20553,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_6_en","",20553,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_7_en","",20553,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_8_en","",20553,], ["libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_0_en","libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_0_en",0,], ["libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_1_en","libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_1_en",0,], ["libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_2_en","libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_2_en",0,], -["libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_en","libraries.matrix.ui.components_AvatarActionBottomSheet_Night_0_en",20539,], +["libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_en","libraries.matrix.ui.components_AvatarActionBottomSheet_Night_0_en",20553,], ["libraries.designsystem.components.avatar.internal_AvatarCluster_Avatars_en","",0,], ["libraries.matrix.ui.components_AvatarPickerSizes_Day_0_en","libraries.matrix.ui.components_AvatarPickerSizes_Night_0_en",0,], ["libraries.matrix.ui.components_AvatarPickerViewRtl_Day_0_en","libraries.matrix.ui.components_AvatarPickerViewRtl_Night_0_en",0,], @@ -133,22 +133,22 @@ export const screenshots = [ ["libraries.designsystem.modifiers_BackgroundVerticalGradientDisabled_Day_0_en","libraries.designsystem.modifiers_BackgroundVerticalGradientDisabled_Night_0_en",0,], ["libraries.designsystem.modifiers_BackgroundVerticalGradient_Day_0_en","libraries.designsystem.modifiers_BackgroundVerticalGradient_Night_0_en",0,], ["libraries.designsystem.components_Badge_Day_0_en","libraries.designsystem.components_Badge_Night_0_en",0,], -["features.home.impl.components_BatteryOptimizationBanner_Day_0_en","features.home.impl.components_BatteryOptimizationBanner_Night_0_en",20539,], +["features.home.impl.components_BatteryOptimizationBanner_Day_0_en","features.home.impl.components_BatteryOptimizationBanner_Night_0_en",20553,], ["libraries.designsystem.atomic.atoms_BetaLabel_Day_0_en","libraries.designsystem.atomic.atoms_BetaLabel_Night_0_en",0,], ["libraries.designsystem.components_BigIcon_Day_0_en","libraries.designsystem.components_BigIcon_Night_0_en",0,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_0_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_0_en",20539,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_1_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_1_en",20539,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_2_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_2_en",20539,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_3_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_3_en",20539,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_4_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_4_en",20539,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_5_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_5_en",20539,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_6_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_6_en",20539,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_0_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_0_en",20553,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_1_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_1_en",20553,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_2_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_2_en",20553,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_3_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_3_en",20553,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_4_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_4_en",20553,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_5_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_5_en",20553,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_6_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_6_en",20553,], ["libraries.designsystem.theme.components_BottomSheetDragHandle_Day_0_en","libraries.designsystem.theme.components_BottomSheetDragHandle_Night_0_en",0,], -["features.rageshake.impl.bugreport_BugReportViewDay_0_en","",20539,], -["features.rageshake.impl.bugreport_BugReportViewDay_1_en","",20539,], -["features.rageshake.impl.bugreport_BugReportViewDay_2_en","",20539,], -["features.rageshake.impl.bugreport_BugReportViewDay_3_en","",20539,], -["features.rageshake.impl.bugreport_BugReportViewDay_4_en","",20539,], +["features.rageshake.impl.bugreport_BugReportViewDay_0_en","",20553,], +["features.rageshake.impl.bugreport_BugReportViewDay_1_en","",20553,], +["features.rageshake.impl.bugreport_BugReportViewDay_2_en","",20553,], +["features.rageshake.impl.bugreport_BugReportViewDay_3_en","",20553,], +["features.rageshake.impl.bugreport_BugReportViewDay_4_en","",20553,], ["features.rageshake.impl.bugreport_BugReportViewNight_0_en","",0,], ["features.rageshake.impl.bugreport_BugReportViewNight_1_en","",0,], ["features.rageshake.impl.bugreport_BugReportViewNight_2_en","",0,], @@ -159,141 +159,141 @@ export const screenshots = [ ["features.messages.impl.timeline.components_CallMenuItem_Day_0_en","features.messages.impl.timeline.components_CallMenuItem_Night_0_en",0,], ["features.messages.impl.timeline.components_CallMenuItem_Day_1_en","features.messages.impl.timeline.components_CallMenuItem_Night_1_en",0,], ["features.messages.impl.timeline.components_CallMenuItem_Day_2_en","features.messages.impl.timeline.components_CallMenuItem_Night_2_en",0,], -["features.messages.impl.timeline.components_CallMenuItem_Day_3_en","features.messages.impl.timeline.components_CallMenuItem_Night_3_en",20539,], -["features.messages.impl.timeline.components_CallMenuItem_Day_4_en","features.messages.impl.timeline.components_CallMenuItem_Night_4_en",20539,], +["features.messages.impl.timeline.components_CallMenuItem_Day_3_en","features.messages.impl.timeline.components_CallMenuItem_Night_3_en",20553,], +["features.messages.impl.timeline.components_CallMenuItem_Day_4_en","features.messages.impl.timeline.components_CallMenuItem_Night_4_en",20553,], ["features.messages.impl.timeline.components_CallMenuItem_Day_5_en","features.messages.impl.timeline.components_CallMenuItem_Night_5_en",0,], -["features.messages.impl.timeline.components_CallMenuItem_Day_6_en","features.messages.impl.timeline.components_CallMenuItem_Night_6_en",20539,], +["features.messages.impl.timeline.components_CallMenuItem_Day_6_en","features.messages.impl.timeline.components_CallMenuItem_Night_6_en",20553,], ["features.messages.impl.timeline.components_CallMenuItem_Day_7_en","features.messages.impl.timeline.components_CallMenuItem_Night_7_en",0,], ["features.call.impl.ui_CallScreenView_Day_0_en","features.call.impl.ui_CallScreenView_Night_0_en",0,], -["features.call.impl.ui_CallScreenView_Day_1_en","features.call.impl.ui_CallScreenView_Night_1_en",20539,], -["features.call.impl.ui_CallScreenView_Day_2_en","features.call.impl.ui_CallScreenView_Night_2_en",20539,], -["features.call.impl.ui_CallScreenView_Day_3_en","features.call.impl.ui_CallScreenView_Night_3_en",20539,], -["libraries.textcomposer_CaptionWarningBottomSheet_Day_0_en","libraries.textcomposer_CaptionWarningBottomSheet_Night_0_en",20539,], -["features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_en","features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_0_en",20539,], -["features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_1_en","features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_1_en",20539,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_0_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_0_en",20539,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_10_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_10_en",20539,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_11_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_11_en",20539,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_12_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_12_en",20539,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_13_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_13_en",20539,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_1_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_1_en",20539,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_2_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_2_en",20539,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_3_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_3_en",20539,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_4_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_4_en",20539,], +["features.call.impl.ui_CallScreenView_Day_1_en","features.call.impl.ui_CallScreenView_Night_1_en",20553,], +["features.call.impl.ui_CallScreenView_Day_2_en","features.call.impl.ui_CallScreenView_Night_2_en",20553,], +["features.call.impl.ui_CallScreenView_Day_3_en","features.call.impl.ui_CallScreenView_Night_3_en",20553,], +["libraries.textcomposer_CaptionWarningBottomSheet_Day_0_en","libraries.textcomposer_CaptionWarningBottomSheet_Night_0_en",20553,], +["features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_en","features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_0_en",20553,], +["features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_1_en","features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_1_en",20553,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_0_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_0_en",20553,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_10_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_10_en",20553,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_11_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_11_en",20553,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_12_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_12_en",20553,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_13_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_13_en",20553,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_1_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_1_en",20553,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_2_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_2_en",20553,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_3_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_3_en",20553,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_4_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_4_en",20553,], ["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_5_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_5_en",0,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_6_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_6_en",20539,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_7_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_7_en",20539,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_8_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_8_en",20539,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_9_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_9_en",20539,], -["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_0_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_0_en",20539,], -["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_1_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_1_en",20539,], -["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_2_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_2_en",20539,], -["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_3_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_3_en",20539,], -["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_4_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_4_en",20539,], -["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_5_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_5_en",20539,], -["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_6_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_6_en",20539,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_6_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_6_en",20553,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_7_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_7_en",20553,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_8_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_8_en",20553,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_9_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_9_en",20553,], +["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_0_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_0_en",20553,], +["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_1_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_1_en",20553,], +["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_2_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_2_en",20553,], +["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_3_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_3_en",20553,], +["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_4_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_4_en",20553,], +["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_5_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_5_en",20553,], +["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_6_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_6_en",20553,], ["features.login.impl.changeserver_ChangeServerView_Day_0_en","features.login.impl.changeserver_ChangeServerView_Night_0_en",0,], -["features.login.impl.changeserver_ChangeServerView_Day_1_en","features.login.impl.changeserver_ChangeServerView_Night_1_en",20539,], -["features.login.impl.changeserver_ChangeServerView_Day_2_en","features.login.impl.changeserver_ChangeServerView_Night_2_en",20539,], -["features.login.impl.changeserver_ChangeServerView_Day_3_en","features.login.impl.changeserver_ChangeServerView_Night_3_en",20539,], -["features.login.impl.changeserver_ChangeServerView_Day_4_en","features.login.impl.changeserver_ChangeServerView_Night_4_en",20539,], -["features.login.impl.changeserver_ChangeServerView_Day_5_en","features.login.impl.changeserver_ChangeServerView_Night_5_en",20539,], +["features.login.impl.changeserver_ChangeServerView_Day_1_en","features.login.impl.changeserver_ChangeServerView_Night_1_en",20553,], +["features.login.impl.changeserver_ChangeServerView_Day_2_en","features.login.impl.changeserver_ChangeServerView_Night_2_en",20553,], +["features.login.impl.changeserver_ChangeServerView_Day_3_en","features.login.impl.changeserver_ChangeServerView_Night_3_en",20553,], +["features.login.impl.changeserver_ChangeServerView_Day_4_en","features.login.impl.changeserver_ChangeServerView_Night_4_en",20553,], +["features.login.impl.changeserver_ChangeServerView_Day_5_en","features.login.impl.changeserver_ChangeServerView_Night_5_en",20553,], ["libraries.matrix.ui.components_CheckableResolvedUserRow_en","",0,], -["libraries.matrix.ui.components_CheckableUnresolvedUserRow_en","",20539,], +["libraries.matrix.ui.components_CheckableUnresolvedUserRow_en","",20553,], ["libraries.designsystem.theme.components_Checkboxes_Toggles_en","",0,], -["features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_0_en","features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_0_en",20539,], -["features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_1_en","features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_1_en",20539,], -["features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_2_en","features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_2_en",20539,], -["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_0_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_0_en",20539,], -["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_1_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_1_en",20539,], -["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_2_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_2_en",20539,], -["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_3_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_3_en",20539,], -["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_4_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_4_en",20539,], +["features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_0_en","features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_0_en",20553,], +["features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_1_en","features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_1_en",20553,], +["features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_2_en","features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_2_en",20553,], +["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_0_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_0_en",20553,], +["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_1_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_1_en",20553,], +["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_2_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_2_en",20553,], +["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_3_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_3_en",20553,], +["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_4_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_4_en",20553,], ["libraries.designsystem.theme.components_CircularProgressIndicator_Progress_Indicators_en","",0,], ["libraries.designsystem.components_ClickableLinkText_Text_en","",0,], ["libraries.designsystem.theme_ColorAliases_Day_0_en","libraries.designsystem.theme_ColorAliases_Night_0_en",0,], -["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_0_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_0_en",20539,], -["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_1_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_1_en",20539,], -["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_2_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_2_en",20539,], -["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_3_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_3_en",20539,], -["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_4_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_4_en",20539,], -["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_5_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_5_en",20539,], -["libraries.textcomposer_ComposerModeView_Day_0_en","libraries.textcomposer_ComposerModeView_Night_0_en",20539,], +["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_0_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_0_en",20553,], +["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_1_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_1_en",20553,], +["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_2_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_2_en",20553,], +["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_3_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_3_en",20553,], +["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_4_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_4_en",20553,], +["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_5_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_5_en",20553,], +["libraries.textcomposer_ComposerModeView_Day_0_en","libraries.textcomposer_ComposerModeView_Night_0_en",20553,], ["libraries.textcomposer_ComposerModeView_Day_1_en","libraries.textcomposer_ComposerModeView_Night_1_en",0,], ["libraries.textcomposer_ComposerModeView_Day_2_en","libraries.textcomposer_ComposerModeView_Night_2_en",0,], ["libraries.textcomposer_ComposerModeView_Day_3_en","libraries.textcomposer_ComposerModeView_Night_3_en",0,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_0_en","",20539,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_1_en","",20539,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_2_en","",20539,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_3_en","",20539,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_4_en","",20539,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_5_en","",20539,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_6_en","",20539,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_7_en","",20539,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_8_en","",20539,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_0_en","",20539,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_1_en","",20539,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_2_en","",20539,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_3_en","",20539,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_4_en","",20539,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_5_en","",20539,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_6_en","",20539,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_7_en","",20539,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_8_en","",20539,], -["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_0_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_0_en",20539,], -["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_1_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_1_en",20539,], -["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_2_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_2_en",20539,], -["features.home.impl.components_ConfirmRecoveryKeyBanner_Day_0_en","features.home.impl.components_ConfirmRecoveryKeyBanner_Night_0_en",20539,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_0_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_1_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_2_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_3_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_4_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_5_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_6_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_7_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_8_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_0_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_1_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_2_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_3_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_4_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_5_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_6_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_7_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_8_en","",20553,], +["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_0_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_0_en",20553,], +["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_1_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_1_en",20553,], +["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_2_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_2_en",20553,], +["features.home.impl.components_ConfirmRecoveryKeyBanner_Day_0_en","features.home.impl.components_ConfirmRecoveryKeyBanner_Night_0_en",20553,], ["libraries.designsystem.components.dialogs_ConfirmationDialogContent_Dialogs_en","",0,], ["libraries.designsystem.components.dialogs_ConfirmationDialog_Day_0_en","libraries.designsystem.components.dialogs_ConfirmationDialog_Night_0_en",0,], ["features.networkmonitor.api.ui_ConnectivityIndicator_Day_0_en","features.networkmonitor.api.ui_ConnectivityIndicator_Night_0_en",0,], ["libraries.designsystem.atomic.atoms_CounterAtom_Day_0_en","libraries.designsystem.atomic.atoms_CounterAtom_Night_0_en",0,], -["features.rageshake.api.crash_CrashDetectionView_Day_0_en","features.rageshake.api.crash_CrashDetectionView_Night_0_en",20539,], -["features.login.impl.screens.createaccount_CreateAccountView_Day_0_en","features.login.impl.screens.createaccount_CreateAccountView_Night_0_en",20539,], -["features.login.impl.screens.createaccount_CreateAccountView_Day_1_en","features.login.impl.screens.createaccount_CreateAccountView_Night_1_en",20539,], -["features.login.impl.screens.createaccount_CreateAccountView_Day_2_en","features.login.impl.screens.createaccount_CreateAccountView_Night_2_en",20539,], -["features.login.impl.screens.createaccount_CreateAccountView_Day_3_en","features.login.impl.screens.createaccount_CreateAccountView_Night_3_en",20539,], -["libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_0_en","libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_0_en",20539,], -["libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_en","libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_1_en",20539,], -["features.poll.impl.create_CreatePollView_Day_0_en","features.poll.impl.create_CreatePollView_Night_0_en",20539,], -["features.poll.impl.create_CreatePollView_Day_1_en","features.poll.impl.create_CreatePollView_Night_1_en",20539,], -["features.poll.impl.create_CreatePollView_Day_2_en","features.poll.impl.create_CreatePollView_Night_2_en",20539,], -["features.poll.impl.create_CreatePollView_Day_3_en","features.poll.impl.create_CreatePollView_Night_3_en",20539,], -["features.poll.impl.create_CreatePollView_Day_4_en","features.poll.impl.create_CreatePollView_Night_4_en",20539,], -["features.poll.impl.create_CreatePollView_Day_5_en","features.poll.impl.create_CreatePollView_Night_5_en",20539,], -["features.poll.impl.create_CreatePollView_Day_6_en","features.poll.impl.create_CreatePollView_Night_6_en",20539,], -["features.poll.impl.create_CreatePollView_Day_7_en","features.poll.impl.create_CreatePollView_Night_7_en",20539,], -["libraries.dateformatter.impl.previews_DateFormatterModeView_0_en","",20539,], -["libraries.dateformatter.impl.previews_DateFormatterModeView_1_en","",20539,], -["libraries.dateformatter.impl.previews_DateFormatterModeView_2_en","",20539,], -["libraries.dateformatter.impl.previews_DateFormatterModeView_3_en","",20539,], -["libraries.dateformatter.impl.previews_DateFormatterModeView_4_en","",20539,], +["features.rageshake.api.crash_CrashDetectionView_Day_0_en","features.rageshake.api.crash_CrashDetectionView_Night_0_en",20553,], +["features.login.impl.screens.createaccount_CreateAccountView_Day_0_en","features.login.impl.screens.createaccount_CreateAccountView_Night_0_en",20553,], +["features.login.impl.screens.createaccount_CreateAccountView_Day_1_en","features.login.impl.screens.createaccount_CreateAccountView_Night_1_en",20553,], +["features.login.impl.screens.createaccount_CreateAccountView_Day_2_en","features.login.impl.screens.createaccount_CreateAccountView_Night_2_en",20553,], +["features.login.impl.screens.createaccount_CreateAccountView_Day_3_en","features.login.impl.screens.createaccount_CreateAccountView_Night_3_en",20553,], +["libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_0_en","libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_0_en",20553,], +["libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_en","libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_1_en",20553,], +["features.poll.impl.create_CreatePollView_Day_0_en","features.poll.impl.create_CreatePollView_Night_0_en",20553,], +["features.poll.impl.create_CreatePollView_Day_1_en","features.poll.impl.create_CreatePollView_Night_1_en",20553,], +["features.poll.impl.create_CreatePollView_Day_2_en","features.poll.impl.create_CreatePollView_Night_2_en",20553,], +["features.poll.impl.create_CreatePollView_Day_3_en","features.poll.impl.create_CreatePollView_Night_3_en",20553,], +["features.poll.impl.create_CreatePollView_Day_4_en","features.poll.impl.create_CreatePollView_Night_4_en",20553,], +["features.poll.impl.create_CreatePollView_Day_5_en","features.poll.impl.create_CreatePollView_Night_5_en",20553,], +["features.poll.impl.create_CreatePollView_Day_6_en","features.poll.impl.create_CreatePollView_Night_6_en",20553,], +["features.poll.impl.create_CreatePollView_Day_7_en","features.poll.impl.create_CreatePollView_Night_7_en",20553,], +["libraries.dateformatter.impl.previews_DateFormatterModeView_0_en","",20553,], +["libraries.dateformatter.impl.previews_DateFormatterModeView_1_en","",20553,], +["libraries.dateformatter.impl.previews_DateFormatterModeView_2_en","",20553,], +["libraries.dateformatter.impl.previews_DateFormatterModeView_3_en","",20553,], +["libraries.dateformatter.impl.previews_DateFormatterModeView_4_en","",20553,], ["libraries.mediaviewer.impl.gallery.ui_DateItemView_Day_0_en","libraries.mediaviewer.impl.gallery.ui_DateItemView_Night_0_en",0,], ["libraries.mediaviewer.impl.gallery.ui_DateItemView_Day_1_en","libraries.mediaviewer.impl.gallery.ui_DateItemView_Night_1_en",0,], -["libraries.designsystem.theme.components.previews_DatePickerDark_DateTime_pickers_en","",20539,], -["libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_en","",20539,], -["features.invite.impl.declineandblock_DeclineAndBlockView_Day_0_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_0_en",20539,], -["features.invite.impl.declineandblock_DeclineAndBlockView_Day_1_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_1_en",20539,], -["features.invite.impl.declineandblock_DeclineAndBlockView_Day_2_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_2_en",20539,], -["features.invite.impl.declineandblock_DeclineAndBlockView_Day_3_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_3_en",20539,], -["features.invite.impl.declineandblock_DeclineAndBlockView_Day_4_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_4_en",20539,], +["libraries.designsystem.theme.components.previews_DatePickerDark_DateTime_pickers_en","",20553,], +["libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_en","",20553,], +["features.invite.impl.declineandblock_DeclineAndBlockView_Day_0_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_0_en",20553,], +["features.invite.impl.declineandblock_DeclineAndBlockView_Day_1_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_1_en",20553,], +["features.invite.impl.declineandblock_DeclineAndBlockView_Day_2_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_2_en",20553,], +["features.invite.impl.declineandblock_DeclineAndBlockView_Day_3_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_3_en",20553,], +["features.invite.impl.declineandblock_DeclineAndBlockView_Day_4_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_4_en",20553,], ["features.logout.impl.direct_DefaultDirectLogoutView_Day_0_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_0_en",0,], -["features.logout.impl.direct_DefaultDirectLogoutView_Day_1_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_1_en",20539,], -["features.logout.impl.direct_DefaultDirectLogoutView_Day_2_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_2_en",20539,], -["features.logout.impl.direct_DefaultDirectLogoutView_Day_3_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_3_en",20539,], +["features.logout.impl.direct_DefaultDirectLogoutView_Day_1_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_1_en",20553,], +["features.logout.impl.direct_DefaultDirectLogoutView_Day_2_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_2_en",20553,], +["features.logout.impl.direct_DefaultDirectLogoutView_Day_3_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_3_en",20553,], ["features.logout.impl.direct_DefaultDirectLogoutView_Day_4_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_4_en",0,], -["features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Day_0_en","features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Night_0_en",20539,], +["features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Day_0_en","features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Night_0_en",20553,], ["features.licenses.impl.details_DependenciesDetailsView_Day_0_en","features.licenses.impl.details_DependenciesDetailsView_Night_0_en",0,], -["features.licenses.impl.list_DependencyLicensesListView_Day_0_en","features.licenses.impl.list_DependencyLicensesListView_Night_0_en",20539,], -["features.licenses.impl.list_DependencyLicensesListView_Day_1_en","features.licenses.impl.list_DependencyLicensesListView_Night_1_en",20539,], -["features.licenses.impl.list_DependencyLicensesListView_Day_2_en","features.licenses.impl.list_DependencyLicensesListView_Night_2_en",20539,], -["features.licenses.impl.list_DependencyLicensesListView_Day_3_en","features.licenses.impl.list_DependencyLicensesListView_Night_3_en",20539,], -["features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Day_0_en","features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Night_0_en",20539,], -["features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Day_1_en","features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Night_1_en",20539,], -["features.preferences.impl.developer_DeveloperSettingsView_Day_0_en","features.preferences.impl.developer_DeveloperSettingsView_Night_0_en",20539,], -["features.preferences.impl.developer_DeveloperSettingsView_Day_1_en","features.preferences.impl.developer_DeveloperSettingsView_Night_1_en",20539,], -["features.preferences.impl.developer_DeveloperSettingsView_Day_2_en","features.preferences.impl.developer_DeveloperSettingsView_Night_2_en",20539,], -["features.preferences.impl.developer_DeveloperSettingsView_Day_3_en","features.preferences.impl.developer_DeveloperSettingsView_Night_3_en",20539,], +["features.licenses.impl.list_DependencyLicensesListView_Day_0_en","features.licenses.impl.list_DependencyLicensesListView_Night_0_en",20553,], +["features.licenses.impl.list_DependencyLicensesListView_Day_1_en","features.licenses.impl.list_DependencyLicensesListView_Night_1_en",20553,], +["features.licenses.impl.list_DependencyLicensesListView_Day_2_en","features.licenses.impl.list_DependencyLicensesListView_Night_2_en",20553,], +["features.licenses.impl.list_DependencyLicensesListView_Day_3_en","features.licenses.impl.list_DependencyLicensesListView_Night_3_en",20553,], +["features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Day_0_en","features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Night_0_en",20553,], +["features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Day_1_en","features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Night_1_en",20553,], +["features.preferences.impl.developer_DeveloperSettingsView_Day_0_en","features.preferences.impl.developer_DeveloperSettingsView_Night_0_en",20553,], +["features.preferences.impl.developer_DeveloperSettingsView_Day_1_en","features.preferences.impl.developer_DeveloperSettingsView_Night_1_en",20553,], +["features.preferences.impl.developer_DeveloperSettingsView_Day_2_en","features.preferences.impl.developer_DeveloperSettingsView_Night_2_en",20553,], +["features.preferences.impl.developer_DeveloperSettingsView_Day_3_en","features.preferences.impl.developer_DeveloperSettingsView_Night_3_en",20553,], ["libraries.designsystem.theme.components_DialogWithDestructiveButton_Dialog_with_destructive_button_Dialogs_en","",0,], ["libraries.designsystem.theme.components_DialogWithOnlyMessageAndOkButton_Dialog_with_only_message_and_ok_button_Dialogs_en","",0,], ["libraries.designsystem.theme.components_DialogWithThirdButton_Dialog_with_third_button_Dialogs_en","",0,], @@ -308,19 +308,19 @@ export const screenshots = [ ["libraries.designsystem.text_DpScale_1_0f__en","",0,], ["libraries.designsystem.text_DpScale_1_5f__en","",0,], ["libraries.designsystem.theme.components_DropdownMenuItem_Menus_en","",0,], -["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_0_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_0_en",20539,], -["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_1_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_1_en",20539,], -["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_2_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_2_en",20539,], -["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_3_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_3_en",20539,], -["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_4_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_4_en",20539,], -["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_0_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_0_en",20539,], -["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_1_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_1_en",20539,], -["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_2_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_2_en",20539,], -["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_3_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_3_en",20539,], -["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_4_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_4_en",20539,], -["features.preferences.impl.user.editprofile_EditUserProfileView_Day_0_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_0_en",20539,], -["features.preferences.impl.user.editprofile_EditUserProfileView_Day_1_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_1_en",20539,], -["features.preferences.impl.user.editprofile_EditUserProfileView_Day_2_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_2_en",20539,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_0_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_0_en",20553,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_1_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_1_en",20553,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_2_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_2_en",20553,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_3_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_3_en",20553,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_4_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_4_en",20553,], +["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_0_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_0_en",20553,], +["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_1_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_1_en",20553,], +["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_2_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_2_en",20553,], +["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_3_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_3_en",20553,], +["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_4_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_4_en",20553,], +["features.preferences.impl.user.editprofile_EditUserProfileView_Day_0_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_0_en",20553,], +["features.preferences.impl.user.editprofile_EditUserProfileView_Day_1_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_1_en",20553,], +["features.preferences.impl.user.editprofile_EditUserProfileView_Day_2_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_2_en",20553,], ["libraries.matrix.ui.components_EditableOrgAvatarRtl_Day_0_en","libraries.matrix.ui.components_EditableOrgAvatarRtl_Night_0_en",0,], ["libraries.matrix.ui.components_EditableOrgAvatar_Day_0_en","libraries.matrix.ui.components_EditableOrgAvatar_Night_0_en",0,], ["libraries.designsystem.atomic.atoms_ElementLogoAtomLargeNoBlurShadow_Day_0_en","libraries.designsystem.atomic.atoms_ElementLogoAtomLargeNoBlurShadow_Night_0_en",0,], @@ -328,28 +328,28 @@ export const screenshots = [ ["libraries.designsystem.atomic.atoms_ElementLogoAtomMediumNoBlurShadow_Day_0_en","libraries.designsystem.atomic.atoms_ElementLogoAtomMediumNoBlurShadow_Night_0_en",0,], ["libraries.designsystem.atomic.atoms_ElementLogoAtomMedium_Day_0_en","libraries.designsystem.atomic.atoms_ElementLogoAtomMedium_Night_0_en",0,], ["features.messages.impl.timeline.components.customreaction_EmojiItem_Day_0_en","features.messages.impl.timeline.components.customreaction_EmojiItem_Night_0_en",0,], -["features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_0_en","features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_0_en",20539,], -["features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_1_en","features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_1_en",20539,], +["features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_0_en","features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_0_en",20553,], +["features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_1_en","features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_1_en",20553,], ["features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_2_en","features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_2_en",0,], ["features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_3_en","features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_3_en",0,], ["libraries.ui.common.nodes_EmptyView_Day_0_en","libraries.ui.common.nodes_EmptyView_Night_0_en",0,], -["features.linknewdevice.impl.screens.number_EnterNumberView_Day_0_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_0_en",20539,], -["features.linknewdevice.impl.screens.number_EnterNumberView_Day_1_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_1_en",20539,], -["features.linknewdevice.impl.screens.number_EnterNumberView_Day_2_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_2_en",20539,], -["features.linknewdevice.impl.screens.number_EnterNumberView_Day_3_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_3_en",20539,], -["features.linknewdevice.impl.screens.number_EnterNumberView_Day_4_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_4_en",20539,], -["features.linknewdevice.impl.screens.number_EnterNumberView_Day_5_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_5_en",20539,], -["libraries.designsystem.components.dialogs_ErrorDialogContent_Dialogs_en","",20539,], -["libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Day_0_en","libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Night_0_en",20539,], -["libraries.designsystem.components.dialogs_ErrorDialog_Day_0_en","libraries.designsystem.components.dialogs_ErrorDialog_Night_0_en",20539,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_0_en","features.linknewdevice.impl.screens.error_ErrorView_Night_0_en",20539,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_1_en","features.linknewdevice.impl.screens.error_ErrorView_Night_1_en",20539,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_2_en","features.linknewdevice.impl.screens.error_ErrorView_Night_2_en",20539,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_3_en","features.linknewdevice.impl.screens.error_ErrorView_Night_3_en",20539,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_4_en","features.linknewdevice.impl.screens.error_ErrorView_Night_4_en",20539,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_5_en","features.linknewdevice.impl.screens.error_ErrorView_Night_5_en",20539,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_6_en","features.linknewdevice.impl.screens.error_ErrorView_Night_6_en",20539,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_7_en","features.linknewdevice.impl.screens.error_ErrorView_Night_7_en",20539,], +["features.linknewdevice.impl.screens.number_EnterNumberView_Day_0_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_0_en",20553,], +["features.linknewdevice.impl.screens.number_EnterNumberView_Day_1_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_1_en",20553,], +["features.linknewdevice.impl.screens.number_EnterNumberView_Day_2_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_2_en",20553,], +["features.linknewdevice.impl.screens.number_EnterNumberView_Day_3_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_3_en",20553,], +["features.linknewdevice.impl.screens.number_EnterNumberView_Day_4_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_4_en",20553,], +["features.linknewdevice.impl.screens.number_EnterNumberView_Day_5_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_5_en",20553,], +["libraries.designsystem.components.dialogs_ErrorDialogContent_Dialogs_en","",20553,], +["libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Day_0_en","libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Night_0_en",20553,], +["libraries.designsystem.components.dialogs_ErrorDialog_Day_0_en","libraries.designsystem.components.dialogs_ErrorDialog_Night_0_en",20553,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_0_en","features.linknewdevice.impl.screens.error_ErrorView_Night_0_en",20553,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_1_en","features.linknewdevice.impl.screens.error_ErrorView_Night_1_en",20553,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_2_en","features.linknewdevice.impl.screens.error_ErrorView_Night_2_en",20553,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_3_en","features.linknewdevice.impl.screens.error_ErrorView_Night_3_en",20553,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_4_en","features.linknewdevice.impl.screens.error_ErrorView_Night_4_en",20553,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_5_en","features.linknewdevice.impl.screens.error_ErrorView_Night_5_en",20553,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_6_en","features.linknewdevice.impl.screens.error_ErrorView_Night_6_en",20553,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_7_en","features.linknewdevice.impl.screens.error_ErrorView_Night_7_en",20553,], ["features.messages.impl.timeline.debug_EventDebugInfoView_Day_0_en","features.messages.impl.timeline.debug_EventDebugInfoView_Night_0_en",0,], ["libraries.designsystem.components_ExpandableBottomSheetLayout_en","",0,], ["libraries.featureflag.ui_FeatureListView_Day_0_en","libraries.featureflag.ui_FeatureListView_Night_0_en",0,], @@ -366,50 +366,50 @@ export const screenshots = [ ["libraries.designsystem.theme.components_FilledTextFieldValueLight_TextFields_en","",0,], ["libraries.designsystem.theme.components_FilledTextFieldValueTextFieldDark_TextFields_en","",0,], ["libraries.designsystem.theme.components_FloatingActionButton_Floating_Action_Buttons_en","",0,], +["features.messages.impl.timeline.components_FloatingDateBadge_Day_0_en","features.messages.impl.timeline.components_FloatingDateBadge_Night_0_en",0,], ["libraries.designsystem.atomic.pages_FlowStepPage_Day_0_en","libraries.designsystem.atomic.pages_FlowStepPage_Night_0_en",0,], ["features.messages.impl.timeline.focus_FocusRequestStateView_Day_0_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_0_en",0,], -["features.messages.impl.timeline.focus_FocusRequestStateView_Day_1_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_1_en",20539,], -["features.messages.impl.timeline.focus_FocusRequestStateView_Day_2_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_2_en",20539,], -["features.messages.impl.timeline.focus_FocusRequestStateView_Day_3_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_3_en",20539,], +["features.messages.impl.timeline.focus_FocusRequestStateView_Day_1_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_1_en",20553,], +["features.messages.impl.timeline.focus_FocusRequestStateView_Day_2_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_2_en",20553,], +["features.messages.impl.timeline.focus_FocusRequestStateView_Day_3_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_3_en",20553,], ["features.messages.impl.timeline.components_FocusedEvent_Day_0_en","features.messages.impl.timeline.components_FocusedEvent_Night_0_en",0,], ["libraries.textcomposer.components_FormattingOption_Day_0_en","libraries.textcomposer.components_FormattingOption_Night_0_en",0,], ["features.forward.impl_ForwardMessagesView_Day_0_en","features.forward.impl_ForwardMessagesView_Night_0_en",0,], ["features.forward.impl_ForwardMessagesView_Day_1_en","features.forward.impl_ForwardMessagesView_Night_1_en",0,], ["features.forward.impl_ForwardMessagesView_Day_2_en","features.forward.impl_ForwardMessagesView_Night_2_en",0,], -["features.forward.impl_ForwardMessagesView_Day_3_en","features.forward.impl_ForwardMessagesView_Night_3_en",20539,], -["features.home.impl.components_FullScreenIntentPermissionBanner_Day_0_en","features.home.impl.components_FullScreenIntentPermissionBanner_Night_0_en",20539,], +["features.forward.impl_ForwardMessagesView_Day_3_en","features.forward.impl_ForwardMessagesView_Night_3_en",20553,], +["features.home.impl.components_FullScreenIntentPermissionBanner_Day_0_en","features.home.impl.components_FullScreenIntentPermissionBanner_Night_0_en",20553,], ["libraries.designsystem.components.button_GradientFloatingActionButtonCircleShape_Day_0_en","libraries.designsystem.components.button_GradientFloatingActionButtonCircleShape_Night_0_en",0,], ["libraries.designsystem.components.button_GradientFloatingActionButton_Day_0_en","libraries.designsystem.components.button_GradientFloatingActionButton_Night_0_en",0,], ["features.messages.impl.timeline.components.group_GroupHeaderView_Day_0_en","features.messages.impl.timeline.components.group_GroupHeaderView_Night_0_en",0,], ["libraries.designsystem.atomic.pages_HeaderFooterPageScrollable_Day_0_en","libraries.designsystem.atomic.pages_HeaderFooterPageScrollable_Night_0_en",0,], ["libraries.designsystem.atomic.pages_HeaderFooterPage_Day_0_en","libraries.designsystem.atomic.pages_HeaderFooterPage_Night_0_en",0,], -["features.home.impl.spaces_HomeSpacesView_Day_0_en","features.home.impl.spaces_HomeSpacesView_Night_0_en",20539,], -["features.home.impl.spaces_HomeSpacesView_Day_1_en","features.home.impl.spaces_HomeSpacesView_Night_1_en",20539,], -["features.home.impl.spaces_HomeSpacesView_Day_2_en","features.home.impl.spaces_HomeSpacesView_Night_2_en",20539,], -["features.home.impl.spaces_HomeSpacesView_Day_3_en","features.home.impl.spaces_HomeSpacesView_Night_3_en",20539,], -["features.home.impl.components_HomeTopBarMultiAccount_Day_0_en","features.home.impl.components_HomeTopBarMultiAccount_Night_0_en",20539,], -["features.home.impl.components_HomeTopBarSpaceFiltersSelected_Day_0_en","features.home.impl.components_HomeTopBarSpaceFiltersSelected_Night_0_en",20539,], +["features.home.impl.spaces_HomeSpacesView_Day_0_en","features.home.impl.spaces_HomeSpacesView_Night_0_en",20553,], +["features.home.impl.spaces_HomeSpacesView_Day_1_en","features.home.impl.spaces_HomeSpacesView_Night_1_en",20553,], +["features.home.impl.spaces_HomeSpacesView_Day_2_en","features.home.impl.spaces_HomeSpacesView_Night_2_en",20553,], +["features.home.impl.components_HomeTopBarMultiAccount_Day_0_en","features.home.impl.components_HomeTopBarMultiAccount_Night_0_en",20553,], +["features.home.impl.components_HomeTopBarSpaceFiltersSelected_Day_0_en","features.home.impl.components_HomeTopBarSpaceFiltersSelected_Night_0_en",20553,], ["features.home.impl.components_HomeTopBarSpaces_Day_0_en","features.home.impl.components_HomeTopBarSpaces_Night_0_en",0,], -["features.home.impl.components_HomeTopBarWithIndicator_Day_0_en","features.home.impl.components_HomeTopBarWithIndicator_Night_0_en",20539,], -["features.home.impl.components_HomeTopBar_Day_0_en","features.home.impl.components_HomeTopBar_Night_0_en",20539,], +["features.home.impl.components_HomeTopBarWithIndicator_Day_0_en","features.home.impl.components_HomeTopBarWithIndicator_Night_0_en",20553,], +["features.home.impl.components_HomeTopBar_Day_0_en","features.home.impl.components_HomeTopBar_Night_0_en",20553,], ["features.home.impl_HomeViewA11y_en","",0,], -["features.home.impl_HomeView_Day_0_en","features.home.impl_HomeView_Night_0_en",20539,], -["features.home.impl_HomeView_Day_10_en","features.home.impl_HomeView_Night_10_en",20539,], +["features.home.impl_HomeView_Day_0_en","features.home.impl_HomeView_Night_0_en",20553,], +["features.home.impl_HomeView_Day_10_en","features.home.impl_HomeView_Night_10_en",20553,], ["features.home.impl_HomeView_Day_11_en","features.home.impl_HomeView_Night_11_en",0,], ["features.home.impl_HomeView_Day_12_en","features.home.impl_HomeView_Night_12_en",0,], -["features.home.impl_HomeView_Day_13_en","features.home.impl_HomeView_Night_13_en",20539,], -["features.home.impl_HomeView_Day_14_en","features.home.impl_HomeView_Night_14_en",20539,], -["features.home.impl_HomeView_Day_15_en","features.home.impl_HomeView_Night_15_en",20539,], -["features.home.impl_HomeView_Day_16_en","features.home.impl_HomeView_Night_16_en",20539,], -["features.home.impl_HomeView_Day_1_en","features.home.impl_HomeView_Night_1_en",20539,], -["features.home.impl_HomeView_Day_2_en","features.home.impl_HomeView_Night_2_en",20539,], -["features.home.impl_HomeView_Day_3_en","features.home.impl_HomeView_Night_3_en",20539,], -["features.home.impl_HomeView_Day_4_en","features.home.impl_HomeView_Night_4_en",20539,], -["features.home.impl_HomeView_Day_5_en","features.home.impl_HomeView_Night_5_en",20539,], -["features.home.impl_HomeView_Day_6_en","features.home.impl_HomeView_Night_6_en",20539,], -["features.home.impl_HomeView_Day_7_en","features.home.impl_HomeView_Night_7_en",20539,], -["features.home.impl_HomeView_Day_8_en","features.home.impl_HomeView_Night_8_en",20539,], -["features.home.impl_HomeView_Day_9_en","features.home.impl_HomeView_Night_9_en",20539,], +["features.home.impl_HomeView_Day_13_en","features.home.impl_HomeView_Night_13_en",20553,], +["features.home.impl_HomeView_Day_14_en","features.home.impl_HomeView_Night_14_en",20553,], +["features.home.impl_HomeView_Day_15_en","features.home.impl_HomeView_Night_15_en",20553,], +["features.home.impl_HomeView_Day_16_en","features.home.impl_HomeView_Night_16_en",20553,], +["features.home.impl_HomeView_Day_1_en","features.home.impl_HomeView_Night_1_en",20553,], +["features.home.impl_HomeView_Day_2_en","features.home.impl_HomeView_Night_2_en",20553,], +["features.home.impl_HomeView_Day_3_en","features.home.impl_HomeView_Night_3_en",20553,], +["features.home.impl_HomeView_Day_4_en","features.home.impl_HomeView_Night_4_en",20553,], +["features.home.impl_HomeView_Day_5_en","features.home.impl_HomeView_Night_5_en",20553,], +["features.home.impl_HomeView_Day_6_en","features.home.impl_HomeView_Night_6_en",20553,], +["features.home.impl_HomeView_Day_7_en","features.home.impl_HomeView_Night_7_en",20553,], +["features.home.impl_HomeView_Day_8_en","features.home.impl_HomeView_Night_8_en",20553,], +["features.home.impl_HomeView_Day_9_en","features.home.impl_HomeView_Night_9_en",20553,], ["libraries.designsystem.theme.components_HorizontalDivider_Dividers_en","",0,], ["libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Day_0_en","libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Night_0_en",0,], ["libraries.designsystem.theme.components_HorizontalFloatingToolbar_Day_0_en","libraries.designsystem.theme.components_HorizontalFloatingToolbar_Night_0_en",0,], @@ -420,12 +420,12 @@ export const screenshots = [ ["libraries.designsystem.atomic.molecules_IconTitlePlaceholdersRowMolecule_Day_0_en","libraries.designsystem.atomic.molecules_IconTitlePlaceholdersRowMolecule_Night_0_en",0,], ["libraries.designsystem.atomic.molecules_IconTitleSubtitleMolecule_Day_0_en","libraries.designsystem.atomic.molecules_IconTitleSubtitleMolecule_Night_0_en",0,], ["libraries.designsystem.theme.components_IconToggleButton_Toggles_en","",0,], -["appicon.element_Icon_en","",0,], ["appicon.enterprise_Icon_en","",0,], +["appicon.element_Icon_en","",0,], ["libraries.designsystem.icons_IconsOther_Day_0_en","libraries.designsystem.icons_IconsOther_Night_0_en",0,], ["features.messages.impl.crypto.identity_IdentityChangeStateView_Day_0_en","features.messages.impl.crypto.identity_IdentityChangeStateView_Night_0_en",0,], -["features.messages.impl.crypto.identity_IdentityChangeStateView_Day_1_en","features.messages.impl.crypto.identity_IdentityChangeStateView_Night_1_en",20539,], -["features.messages.impl.crypto.identity_IdentityChangeStateView_Day_2_en","features.messages.impl.crypto.identity_IdentityChangeStateView_Night_2_en",20539,], +["features.messages.impl.crypto.identity_IdentityChangeStateView_Day_1_en","features.messages.impl.crypto.identity_IdentityChangeStateView_Night_1_en",20553,], +["features.messages.impl.crypto.identity_IdentityChangeStateView_Day_2_en","features.messages.impl.crypto.identity_IdentityChangeStateView_Night_2_en",20553,], ["libraries.mediaviewer.impl.gallery.ui_ImageItemView_Day_0_en","libraries.mediaviewer.impl.gallery.ui_ImageItemView_Night_0_en",0,], ["libraries.matrix.ui.messages.reply_InReplyToView_Day_0_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_0_en",0,], ["libraries.matrix.ui.messages.reply_InReplyToView_Day_10_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_10_en",0,], @@ -433,117 +433,117 @@ export const screenshots = [ ["libraries.matrix.ui.messages.reply_InReplyToView_Day_1_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_1_en",0,], ["libraries.matrix.ui.messages.reply_InReplyToView_Day_2_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_2_en",0,], ["libraries.matrix.ui.messages.reply_InReplyToView_Day_3_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_3_en",0,], -["libraries.matrix.ui.messages.reply_InReplyToView_Day_4_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_4_en",20539,], +["libraries.matrix.ui.messages.reply_InReplyToView_Day_4_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_4_en",20553,], ["libraries.matrix.ui.messages.reply_InReplyToView_Day_5_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_5_en",0,], ["libraries.matrix.ui.messages.reply_InReplyToView_Day_6_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_6_en",0,], ["libraries.matrix.ui.messages.reply_InReplyToView_Day_7_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_7_en",0,], -["libraries.matrix.ui.messages.reply_InReplyToView_Day_8_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_8_en",20539,], +["libraries.matrix.ui.messages.reply_InReplyToView_Day_8_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_8_en",20553,], ["libraries.matrix.ui.messages.reply_InReplyToView_Day_9_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_9_en",0,], -["features.call.impl.ui_IncomingCallScreen_Day_0_en","features.call.impl.ui_IncomingCallScreen_Night_0_en",20539,], -["features.call.impl.ui_IncomingCallScreen_Day_1_en","features.call.impl.ui_IncomingCallScreen_Night_1_en",20539,], +["features.call.impl.ui_IncomingCallScreen_Day_0_en","features.call.impl.ui_IncomingCallScreen_Night_0_en",20553,], +["features.call.impl.ui_IncomingCallScreen_Day_1_en","features.call.impl.ui_IncomingCallScreen_Night_1_en",20553,], ["features.verifysession.impl.incoming_IncomingVerificationViewA11y_en","",0,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_0_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_0_en",20539,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_10_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_10_en",20539,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_11_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_11_en",20539,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_12_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_12_en",20539,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_13_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_13_en",20539,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_1_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_1_en",20539,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_2_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_2_en",20539,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_3_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_3_en",20539,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_4_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_4_en",20539,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_5_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_5_en",20539,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_6_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_6_en",20539,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_7_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_7_en",20539,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_8_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_8_en",20539,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_9_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_9_en",20539,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_0_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_0_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_10_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_10_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_11_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_11_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_12_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_12_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_13_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_13_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_1_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_1_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_2_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_2_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_3_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_3_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_4_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_4_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_5_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_5_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_6_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_6_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_7_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_7_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_8_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_8_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_9_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_9_en",20553,], ["libraries.designsystem.atomic.molecules_InfoListItemMolecule_Day_0_en","libraries.designsystem.atomic.molecules_InfoListItemMolecule_Night_0_en",0,], ["libraries.designsystem.atomic.organisms_InfoListOrganism_Day_0_en","libraries.designsystem.atomic.organisms_InfoListOrganism_Night_0_en",0,], ["libraries.matrix.ui.media_InitialsAvatarBitmapGenerator_Day_0_en","libraries.matrix.ui.media_InitialsAvatarBitmapGenerator_Night_0_en",0,], -["features.call.impl.ui_InvalidAudioDeviceDialog_Day_0_en","features.call.impl.ui_InvalidAudioDeviceDialog_Night_0_en",20539,], -["features.invitepeople.impl_InvitePeopleView_Day_0_en","features.invitepeople.impl_InvitePeopleView_Night_0_en",20539,], -["features.invitepeople.impl_InvitePeopleView_Day_1_en","features.invitepeople.impl_InvitePeopleView_Night_1_en",20539,], +["features.call.impl.ui_InvalidAudioDeviceDialog_Day_0_en","features.call.impl.ui_InvalidAudioDeviceDialog_Night_0_en",20553,], +["features.invitepeople.impl_InvitePeopleView_Day_0_en","features.invitepeople.impl_InvitePeopleView_Night_0_en",20553,], +["features.invitepeople.impl_InvitePeopleView_Day_1_en","features.invitepeople.impl_InvitePeopleView_Night_1_en",20553,], ["features.invitepeople.impl_InvitePeopleView_Day_2_en","features.invitepeople.impl_InvitePeopleView_Night_2_en",0,], ["features.invitepeople.impl_InvitePeopleView_Day_3_en","features.invitepeople.impl_InvitePeopleView_Night_3_en",0,], -["features.invitepeople.impl_InvitePeopleView_Day_4_en","features.invitepeople.impl_InvitePeopleView_Night_4_en",20539,], -["features.invitepeople.impl_InvitePeopleView_Day_5_en","features.invitepeople.impl_InvitePeopleView_Night_5_en",20539,], -["features.invitepeople.impl_InvitePeopleView_Day_6_en","features.invitepeople.impl_InvitePeopleView_Night_6_en",20539,], -["features.invitepeople.impl_InvitePeopleView_Day_7_en","features.invitepeople.impl_InvitePeopleView_Night_7_en",20539,], +["features.invitepeople.impl_InvitePeopleView_Day_4_en","features.invitepeople.impl_InvitePeopleView_Night_4_en",20553,], +["features.invitepeople.impl_InvitePeopleView_Day_5_en","features.invitepeople.impl_InvitePeopleView_Night_5_en",20553,], +["features.invitepeople.impl_InvitePeopleView_Day_6_en","features.invitepeople.impl_InvitePeopleView_Night_6_en",20553,], +["features.invitepeople.impl_InvitePeopleView_Day_7_en","features.invitepeople.impl_InvitePeopleView_Night_7_en",20553,], ["features.invitepeople.impl_InvitePeopleView_Day_8_en","features.invitepeople.impl_InvitePeopleView_Night_8_en",0,], -["features.invitepeople.impl_InvitePeopleView_Day_9_en","features.invitepeople.impl_InvitePeopleView_Night_9_en",20539,], -["libraries.matrix.ui.components_InviteSenderView_Day_0_en","libraries.matrix.ui.components_InviteSenderView_Night_0_en",20539,], -["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_0_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_0_en",20539,], -["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_1_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_1_en",20539,], -["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_2_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_2_en",20539,], -["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_3_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_3_en",20539,], -["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_4_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_4_en",20539,], -["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_5_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_5_en",20539,], +["features.invitepeople.impl_InvitePeopleView_Day_9_en","features.invitepeople.impl_InvitePeopleView_Night_9_en",20553,], +["libraries.matrix.ui.components_InviteSenderView_Day_0_en","libraries.matrix.ui.components_InviteSenderView_Night_0_en",20553,], +["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_0_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_0_en",20553,], +["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_1_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_1_en",20553,], +["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_2_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_2_en",20553,], +["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_3_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_3_en",20553,], +["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_4_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_4_en",20553,], +["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_5_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_5_en",20553,], ["features.joinroom.impl_JoinRoomView_Day_0_en","features.joinroom.impl_JoinRoomView_Night_0_en",0,], -["features.joinroom.impl_JoinRoomView_Day_10_en","features.joinroom.impl_JoinRoomView_Night_10_en",20539,], -["features.joinroom.impl_JoinRoomView_Day_11_en","features.joinroom.impl_JoinRoomView_Night_11_en",20539,], -["features.joinroom.impl_JoinRoomView_Day_12_en","features.joinroom.impl_JoinRoomView_Night_12_en",20539,], -["features.joinroom.impl_JoinRoomView_Day_13_en","features.joinroom.impl_JoinRoomView_Night_13_en",20539,], -["features.joinroom.impl_JoinRoomView_Day_14_en","features.joinroom.impl_JoinRoomView_Night_14_en",20539,], -["features.joinroom.impl_JoinRoomView_Day_15_en","features.joinroom.impl_JoinRoomView_Night_15_en",20539,], -["features.joinroom.impl_JoinRoomView_Day_16_en","features.joinroom.impl_JoinRoomView_Night_16_en",20539,], -["features.joinroom.impl_JoinRoomView_Day_1_en","features.joinroom.impl_JoinRoomView_Night_1_en",20539,], -["features.joinroom.impl_JoinRoomView_Day_2_en","features.joinroom.impl_JoinRoomView_Night_2_en",20539,], -["features.joinroom.impl_JoinRoomView_Day_3_en","features.joinroom.impl_JoinRoomView_Night_3_en",20539,], -["features.joinroom.impl_JoinRoomView_Day_4_en","features.joinroom.impl_JoinRoomView_Night_4_en",20539,], -["features.joinroom.impl_JoinRoomView_Day_5_en","features.joinroom.impl_JoinRoomView_Night_5_en",20539,], -["features.joinroom.impl_JoinRoomView_Day_6_en","features.joinroom.impl_JoinRoomView_Night_6_en",20539,], -["features.joinroom.impl_JoinRoomView_Day_7_en","features.joinroom.impl_JoinRoomView_Night_7_en",20539,], -["features.joinroom.impl_JoinRoomView_Day_8_en","features.joinroom.impl_JoinRoomView_Night_8_en",20539,], -["features.joinroom.impl_JoinRoomView_Day_9_en","features.joinroom.impl_JoinRoomView_Night_9_en",20539,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_0_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_0_en",20539,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_1_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_1_en",20539,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_2_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_2_en",20539,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_3_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_3_en",20539,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_4_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_4_en",20539,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_5_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_5_en",20539,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_6_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_6_en",20539,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_0_en","features.knockrequests.impl.list_KnockRequestsListView_Night_0_en",20539,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_10_en","features.knockrequests.impl.list_KnockRequestsListView_Night_10_en",20539,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_1_en","features.knockrequests.impl.list_KnockRequestsListView_Night_1_en",20539,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_2_en","features.knockrequests.impl.list_KnockRequestsListView_Night_2_en",20539,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_3_en","features.knockrequests.impl.list_KnockRequestsListView_Night_3_en",20539,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_4_en","features.knockrequests.impl.list_KnockRequestsListView_Night_4_en",20539,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_5_en","features.knockrequests.impl.list_KnockRequestsListView_Night_5_en",20539,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_6_en","features.knockrequests.impl.list_KnockRequestsListView_Night_6_en",20539,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_7_en","features.knockrequests.impl.list_KnockRequestsListView_Night_7_en",20539,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_8_en","features.knockrequests.impl.list_KnockRequestsListView_Night_8_en",20539,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_9_en","features.knockrequests.impl.list_KnockRequestsListView_Night_9_en",20539,], +["features.joinroom.impl_JoinRoomView_Day_10_en","features.joinroom.impl_JoinRoomView_Night_10_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_11_en","features.joinroom.impl_JoinRoomView_Night_11_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_12_en","features.joinroom.impl_JoinRoomView_Night_12_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_13_en","features.joinroom.impl_JoinRoomView_Night_13_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_14_en","features.joinroom.impl_JoinRoomView_Night_14_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_15_en","features.joinroom.impl_JoinRoomView_Night_15_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_16_en","features.joinroom.impl_JoinRoomView_Night_16_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_1_en","features.joinroom.impl_JoinRoomView_Night_1_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_2_en","features.joinroom.impl_JoinRoomView_Night_2_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_3_en","features.joinroom.impl_JoinRoomView_Night_3_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_4_en","features.joinroom.impl_JoinRoomView_Night_4_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_5_en","features.joinroom.impl_JoinRoomView_Night_5_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_6_en","features.joinroom.impl_JoinRoomView_Night_6_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_7_en","features.joinroom.impl_JoinRoomView_Night_7_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_8_en","features.joinroom.impl_JoinRoomView_Night_8_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_9_en","features.joinroom.impl_JoinRoomView_Night_9_en",20553,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_0_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_0_en",20553,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_1_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_1_en",20553,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_2_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_2_en",20553,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_3_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_3_en",20553,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_4_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_4_en",20553,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_5_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_5_en",20553,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_6_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_6_en",20553,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_0_en","features.knockrequests.impl.list_KnockRequestsListView_Night_0_en",20553,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_10_en","features.knockrequests.impl.list_KnockRequestsListView_Night_10_en",20553,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_1_en","features.knockrequests.impl.list_KnockRequestsListView_Night_1_en",20553,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_2_en","features.knockrequests.impl.list_KnockRequestsListView_Night_2_en",20553,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_3_en","features.knockrequests.impl.list_KnockRequestsListView_Night_3_en",20553,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_4_en","features.knockrequests.impl.list_KnockRequestsListView_Night_4_en",20553,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_5_en","features.knockrequests.impl.list_KnockRequestsListView_Night_5_en",20553,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_6_en","features.knockrequests.impl.list_KnockRequestsListView_Night_6_en",20553,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_7_en","features.knockrequests.impl.list_KnockRequestsListView_Night_7_en",20553,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_8_en","features.knockrequests.impl.list_KnockRequestsListView_Night_8_en",20553,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_9_en","features.knockrequests.impl.list_KnockRequestsListView_Night_9_en",20553,], ["libraries.designsystem.components_LabelledCheckbox_Toggles_en","",0,], -["features.preferences.impl.labs_LabsView_Day_0_en","features.preferences.impl.labs_LabsView_Night_0_en",20539,], -["features.preferences.impl.labs_LabsView_Day_1_en","features.preferences.impl.labs_LabsView_Night_1_en",20539,], +["features.preferences.impl.labs_LabsView_Day_0_en","features.preferences.impl.labs_LabsView_Night_0_en",20553,], +["features.preferences.impl.labs_LabsView_Day_1_en","features.preferences.impl.labs_LabsView_Night_1_en",20553,], ["features.leaveroom.impl_LeaveRoomView_Day_0_en","features.leaveroom.impl_LeaveRoomView_Night_0_en",0,], -["features.leaveroom.impl_LeaveRoomView_Day_1_en","features.leaveroom.impl_LeaveRoomView_Night_1_en",20539,], -["features.leaveroom.impl_LeaveRoomView_Day_2_en","features.leaveroom.impl_LeaveRoomView_Night_2_en",20539,], -["features.leaveroom.impl_LeaveRoomView_Day_3_en","features.leaveroom.impl_LeaveRoomView_Night_3_en",20539,], -["features.leaveroom.impl_LeaveRoomView_Day_4_en","features.leaveroom.impl_LeaveRoomView_Night_4_en",20539,], -["features.leaveroom.impl_LeaveRoomView_Day_5_en","features.leaveroom.impl_LeaveRoomView_Night_5_en",20539,], -["features.leaveroom.impl_LeaveRoomView_Day_6_en","features.leaveroom.impl_LeaveRoomView_Night_6_en",20539,], -["features.leaveroom.impl_LeaveRoomView_Day_7_en","features.leaveroom.impl_LeaveRoomView_Night_7_en",20539,], -["features.space.impl.leave_LeaveSpaceView_Day_0_en","features.space.impl.leave_LeaveSpaceView_Night_0_en",20539,], -["features.space.impl.leave_LeaveSpaceView_Day_10_en","features.space.impl.leave_LeaveSpaceView_Night_10_en",20539,], -["features.space.impl.leave_LeaveSpaceView_Day_1_en","features.space.impl.leave_LeaveSpaceView_Night_1_en",20539,], -["features.space.impl.leave_LeaveSpaceView_Day_2_en","features.space.impl.leave_LeaveSpaceView_Night_2_en",20539,], -["features.space.impl.leave_LeaveSpaceView_Day_3_en","features.space.impl.leave_LeaveSpaceView_Night_3_en",20539,], -["features.space.impl.leave_LeaveSpaceView_Day_4_en","features.space.impl.leave_LeaveSpaceView_Night_4_en",20539,], -["features.space.impl.leave_LeaveSpaceView_Day_5_en","features.space.impl.leave_LeaveSpaceView_Night_5_en",20539,], -["features.space.impl.leave_LeaveSpaceView_Day_6_en","features.space.impl.leave_LeaveSpaceView_Night_6_en",20539,], -["features.space.impl.leave_LeaveSpaceView_Day_7_en","features.space.impl.leave_LeaveSpaceView_Night_7_en",20539,], -["features.space.impl.leave_LeaveSpaceView_Day_8_en","features.space.impl.leave_LeaveSpaceView_Night_8_en",20539,], -["features.space.impl.leave_LeaveSpaceView_Day_9_en","features.space.impl.leave_LeaveSpaceView_Night_9_en",20539,], +["features.leaveroom.impl_LeaveRoomView_Day_1_en","features.leaveroom.impl_LeaveRoomView_Night_1_en",20553,], +["features.leaveroom.impl_LeaveRoomView_Day_2_en","features.leaveroom.impl_LeaveRoomView_Night_2_en",20553,], +["features.leaveroom.impl_LeaveRoomView_Day_3_en","features.leaveroom.impl_LeaveRoomView_Night_3_en",20553,], +["features.leaveroom.impl_LeaveRoomView_Day_4_en","features.leaveroom.impl_LeaveRoomView_Night_4_en",20553,], +["features.leaveroom.impl_LeaveRoomView_Day_5_en","features.leaveroom.impl_LeaveRoomView_Night_5_en",20553,], +["features.leaveroom.impl_LeaveRoomView_Day_6_en","features.leaveroom.impl_LeaveRoomView_Night_6_en",20553,], +["features.leaveroom.impl_LeaveRoomView_Day_7_en","features.leaveroom.impl_LeaveRoomView_Night_7_en",20553,], +["features.space.impl.leave_LeaveSpaceView_Day_0_en","features.space.impl.leave_LeaveSpaceView_Night_0_en",20553,], +["features.space.impl.leave_LeaveSpaceView_Day_10_en","features.space.impl.leave_LeaveSpaceView_Night_10_en",20553,], +["features.space.impl.leave_LeaveSpaceView_Day_1_en","features.space.impl.leave_LeaveSpaceView_Night_1_en",20553,], +["features.space.impl.leave_LeaveSpaceView_Day_2_en","features.space.impl.leave_LeaveSpaceView_Night_2_en",20553,], +["features.space.impl.leave_LeaveSpaceView_Day_3_en","features.space.impl.leave_LeaveSpaceView_Night_3_en",20553,], +["features.space.impl.leave_LeaveSpaceView_Day_4_en","features.space.impl.leave_LeaveSpaceView_Night_4_en",20553,], +["features.space.impl.leave_LeaveSpaceView_Day_5_en","features.space.impl.leave_LeaveSpaceView_Night_5_en",20553,], +["features.space.impl.leave_LeaveSpaceView_Day_6_en","features.space.impl.leave_LeaveSpaceView_Night_6_en",20553,], +["features.space.impl.leave_LeaveSpaceView_Day_7_en","features.space.impl.leave_LeaveSpaceView_Night_7_en",20553,], +["features.space.impl.leave_LeaveSpaceView_Day_8_en","features.space.impl.leave_LeaveSpaceView_Night_8_en",20553,], +["features.space.impl.leave_LeaveSpaceView_Day_9_en","features.space.impl.leave_LeaveSpaceView_Night_9_en",20553,], ["libraries.designsystem.background_LightGradientBackground_Day_0_en","libraries.designsystem.background_LightGradientBackground_Night_0_en",0,], ["libraries.designsystem.theme.components_LinearProgressIndicator_Progress_Indicators_en","",0,], -["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_0_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_0_en",20539,], -["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_1_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_1_en",20539,], -["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_2_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_2_en",20539,], -["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_3_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_3_en",20539,], -["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_4_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_4_en",20539,], -["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_5_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_5_en",20539,], +["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_0_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_0_en",20553,], +["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_1_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_1_en",20553,], +["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_2_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_2_en",20553,], +["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_3_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_3_en",20553,], +["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_4_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_4_en",20553,], +["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_5_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_5_en",20553,], ["features.messages.impl.link_LinkView_Day_0_en","features.messages.impl.link_LinkView_Night_0_en",0,], -["features.messages.impl.link_LinkView_Day_1_en","features.messages.impl.link_LinkView_Night_1_en",20539,], +["features.messages.impl.link_LinkView_Day_1_en","features.messages.impl.link_LinkView_Night_1_en",20553,], ["libraries.designsystem.components.dialogs_ListDialogContent_Dialogs_en","",0,], ["libraries.designsystem.components.dialogs_ListDialog_Day_0_en","libraries.designsystem.components.dialogs_ListDialog_Night_0_en",0,], ["libraries.designsystem.theme.components_ListItemPrimaryActionWithIcon_List_item_-_Primary_action_&_Icon_List_items_en","",0,], @@ -598,43 +598,43 @@ export const screenshots = [ ["libraries.designsystem.theme.components_ListSupportingTextSmallPadding_List_supporting_text_-_small_padding_List_sections_en","",0,], ["libraries.textcomposer.components_LiveWaveformView_Day_0_en","libraries.textcomposer.components_LiveWaveformView_Night_0_en",0,], ["appnav.room.joined_LoadingRoomNodeView_Day_0_en","appnav.room.joined_LoadingRoomNodeView_Night_0_en",0,], -["appnav.room.joined_LoadingRoomNodeView_Day_1_en","appnav.room.joined_LoadingRoomNodeView_Night_1_en",20539,], +["appnav.room.joined_LoadingRoomNodeView_Day_1_en","appnav.room.joined_LoadingRoomNodeView_Night_1_en",20553,], ["libraries.designsystem.components_LocationPin_Day_0_en","libraries.designsystem.components_LocationPin_Night_0_en",0,], ["features.location.impl.common.ui_LocationShareRow_Day_0_en","features.location.impl.common.ui_LocationShareRow_Night_0_en",0,], -["features.lockscreen.impl.settings_LockScreenSettingsView_Day_0_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_0_en",20539,], -["features.lockscreen.impl.settings_LockScreenSettingsView_Day_1_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_1_en",20539,], -["features.lockscreen.impl.settings_LockScreenSettingsView_Day_2_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_2_en",20539,], +["features.lockscreen.impl.settings_LockScreenSettingsView_Day_0_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_0_en",20553,], +["features.lockscreen.impl.settings_LockScreenSettingsView_Day_1_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_1_en",20553,], +["features.lockscreen.impl.settings_LockScreenSettingsView_Day_2_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_2_en",20553,], ["appnav.loggedin_LoggedInView_Day_0_en","appnav.loggedin_LoggedInView_Night_0_en",0,], -["appnav.loggedin_LoggedInView_Day_1_en","appnav.loggedin_LoggedInView_Night_1_en",20539,], -["appnav.loggedin_LoggedInView_Day_2_en","appnav.loggedin_LoggedInView_Night_2_en",20539,], -["appnav.loggedin_LoggedInView_Day_3_en","appnav.loggedin_LoggedInView_Night_3_en",20539,], -["features.login.impl.login_LoginModeView_Day_0_en","features.login.impl.login_LoginModeView_Night_0_en",20539,], -["features.login.impl.login_LoginModeView_Day_1_en","features.login.impl.login_LoginModeView_Night_1_en",20539,], -["features.login.impl.login_LoginModeView_Day_2_en","features.login.impl.login_LoginModeView_Night_2_en",20539,], -["features.login.impl.login_LoginModeView_Day_3_en","features.login.impl.login_LoginModeView_Night_3_en",20539,], -["features.login.impl.login_LoginModeView_Day_4_en","features.login.impl.login_LoginModeView_Night_4_en",20539,], -["features.login.impl.login_LoginModeView_Day_5_en","features.login.impl.login_LoginModeView_Night_5_en",20539,], -["features.login.impl.login_LoginModeView_Day_6_en","features.login.impl.login_LoginModeView_Night_6_en",20539,], -["features.login.impl.screens.loginpassword_LoginPasswordView_Day_0_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_0_en",20539,], -["features.login.impl.screens.loginpassword_LoginPasswordView_Day_1_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_1_en",20539,], -["features.login.impl.screens.loginpassword_LoginPasswordView_Day_2_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_2_en",20539,], -["features.logout.impl_LogoutView_Day_0_en","features.logout.impl_LogoutView_Night_0_en",20539,], -["features.logout.impl_LogoutView_Day_10_en","features.logout.impl_LogoutView_Night_10_en",20539,], -["features.logout.impl_LogoutView_Day_11_en","features.logout.impl_LogoutView_Night_11_en",20539,], -["features.logout.impl_LogoutView_Day_1_en","features.logout.impl_LogoutView_Night_1_en",20539,], -["features.logout.impl_LogoutView_Day_2_en","features.logout.impl_LogoutView_Night_2_en",20539,], -["features.logout.impl_LogoutView_Day_3_en","features.logout.impl_LogoutView_Night_3_en",20539,], -["features.logout.impl_LogoutView_Day_4_en","features.logout.impl_LogoutView_Night_4_en",20539,], -["features.logout.impl_LogoutView_Day_5_en","features.logout.impl_LogoutView_Night_5_en",20539,], -["features.logout.impl_LogoutView_Day_6_en","features.logout.impl_LogoutView_Night_6_en",20539,], -["features.logout.impl_LogoutView_Day_7_en","features.logout.impl_LogoutView_Night_7_en",20539,], -["features.logout.impl_LogoutView_Day_8_en","features.logout.impl_LogoutView_Night_8_en",20539,], -["features.logout.impl_LogoutView_Day_9_en","features.logout.impl_LogoutView_Night_9_en",20539,], +["appnav.loggedin_LoggedInView_Day_1_en","appnav.loggedin_LoggedInView_Night_1_en",20553,], +["appnav.loggedin_LoggedInView_Day_2_en","appnav.loggedin_LoggedInView_Night_2_en",20553,], +["appnav.loggedin_LoggedInView_Day_3_en","appnav.loggedin_LoggedInView_Night_3_en",20553,], +["features.login.impl.login_LoginModeView_Day_0_en","features.login.impl.login_LoginModeView_Night_0_en",20553,], +["features.login.impl.login_LoginModeView_Day_1_en","features.login.impl.login_LoginModeView_Night_1_en",20553,], +["features.login.impl.login_LoginModeView_Day_2_en","features.login.impl.login_LoginModeView_Night_2_en",20553,], +["features.login.impl.login_LoginModeView_Day_3_en","features.login.impl.login_LoginModeView_Night_3_en",20553,], +["features.login.impl.login_LoginModeView_Day_4_en","features.login.impl.login_LoginModeView_Night_4_en",20553,], +["features.login.impl.login_LoginModeView_Day_5_en","features.login.impl.login_LoginModeView_Night_5_en",20553,], +["features.login.impl.login_LoginModeView_Day_6_en","features.login.impl.login_LoginModeView_Night_6_en",20553,], +["features.login.impl.screens.loginpassword_LoginPasswordView_Day_0_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_0_en",20553,], +["features.login.impl.screens.loginpassword_LoginPasswordView_Day_1_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_1_en",20553,], +["features.login.impl.screens.loginpassword_LoginPasswordView_Day_2_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_2_en",20553,], +["features.logout.impl_LogoutView_Day_0_en","features.logout.impl_LogoutView_Night_0_en",20553,], +["features.logout.impl_LogoutView_Day_10_en","features.logout.impl_LogoutView_Night_10_en",20553,], +["features.logout.impl_LogoutView_Day_11_en","features.logout.impl_LogoutView_Night_11_en",20553,], +["features.logout.impl_LogoutView_Day_1_en","features.logout.impl_LogoutView_Night_1_en",20553,], +["features.logout.impl_LogoutView_Day_2_en","features.logout.impl_LogoutView_Night_2_en",20553,], +["features.logout.impl_LogoutView_Day_3_en","features.logout.impl_LogoutView_Night_3_en",20553,], +["features.logout.impl_LogoutView_Day_4_en","features.logout.impl_LogoutView_Night_4_en",20553,], +["features.logout.impl_LogoutView_Day_5_en","features.logout.impl_LogoutView_Night_5_en",20553,], +["features.logout.impl_LogoutView_Day_6_en","features.logout.impl_LogoutView_Night_6_en",20553,], +["features.logout.impl_LogoutView_Day_7_en","features.logout.impl_LogoutView_Night_7_en",20553,], +["features.logout.impl_LogoutView_Day_8_en","features.logout.impl_LogoutView_Night_8_en",20553,], +["features.logout.impl_LogoutView_Day_9_en","features.logout.impl_LogoutView_Night_9_en",20553,], ["libraries.designsystem.components.button_MainActionButton_Buttons_en","",0,], -["features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_0_en","features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_0_en",20539,], -["features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_1_en","features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_1_en",20539,], -["features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_2_en","features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_2_en",20539,], -["libraries.textcomposer_MarkdownTextComposerEdit_Day_0_en","libraries.textcomposer_MarkdownTextComposerEdit_Night_0_en",20539,], +["features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_0_en","features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_0_en",20553,], +["features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_1_en","features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_1_en",20553,], +["features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_2_en","features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_2_en",20553,], +["libraries.textcomposer_MarkdownTextComposerEdit_Day_0_en","libraries.textcomposer_MarkdownTextComposerEdit_Night_0_en",20553,], ["libraries.textcomposer.components.markdown_MarkdownTextInput_Day_0_en","libraries.textcomposer.components.markdown_MarkdownTextInput_Night_0_en",0,], ["libraries.designsystem.atomic.atoms_MatrixBadgeAtomInfo_Day_0_en","libraries.designsystem.atomic.atoms_MatrixBadgeAtomInfo_Night_0_en",0,], ["libraries.designsystem.atomic.atoms_MatrixBadgeAtomNegative_Day_0_en","libraries.designsystem.atomic.atoms_MatrixBadgeAtomNegative_Night_0_en",0,], @@ -648,22 +648,22 @@ export const screenshots = [ ["libraries.matrix.ui.components_MatrixUserRow_Day_1_en","libraries.matrix.ui.components_MatrixUserRow_Night_1_en",0,], ["libraries.mediaviewer.impl.local.audio_MediaAudioView_Day_0_en","libraries.mediaviewer.impl.local.audio_MediaAudioView_Night_0_en",0,], ["libraries.mediaviewer.impl.local.audio_MediaAudioView_Day_1_en","libraries.mediaviewer.impl.local.audio_MediaAudioView_Night_1_en",0,], -["libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_0_en","libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Night_0_en",20539,], -["libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_en","libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_0_en",20539,], +["libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_0_en","libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Night_0_en",20553,], +["libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_en","libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_0_en",20553,], ["libraries.mediaviewer.impl.local.file_MediaFileView_Day_0_en","libraries.mediaviewer.impl.local.file_MediaFileView_Night_0_en",0,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_0_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_0_en",20539,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_10_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_10_en",20539,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_11_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_11_en",20539,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_12_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_12_en",20539,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_1_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_1_en",20539,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_2_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_2_en",20539,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_3_en",20539,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_4_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_4_en",20539,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_5_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_5_en",20539,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_6_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_6_en",20539,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_7_en",20539,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_8_en",20539,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_9_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_9_en",20539,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_0_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_0_en",20553,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_10_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_10_en",20553,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_11_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_11_en",20553,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_12_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_12_en",20553,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_1_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_1_en",20553,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_2_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_2_en",20553,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_3_en",20553,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_4_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_4_en",20553,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_5_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_5_en",20553,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_6_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_6_en",20553,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_7_en",20553,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_8_en",20553,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_9_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_9_en",20553,], ["libraries.mediaviewer.impl.local.image_MediaImageView_Day_0_en","libraries.mediaviewer.impl.local.image_MediaImageView_Night_0_en",0,], ["libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Day_0_en","libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Night_0_en",0,], ["libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Day_1_en","libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Night_1_en",0,], @@ -671,14 +671,14 @@ export const screenshots = [ ["libraries.mediaviewer.impl.local.video_MediaVideoView_Day_0_en","libraries.mediaviewer.impl.local.video_MediaVideoView_Night_0_en",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_0_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_10_en","",0,], -["libraries.mediaviewer.impl.viewer_MediaViewerView_11_en","",20539,], -["libraries.mediaviewer.impl.viewer_MediaViewerView_12_en","",20539,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_11_en","",20553,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_12_en","",20553,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_13_en","",0,], -["libraries.mediaviewer.impl.viewer_MediaViewerView_14_en","",20539,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_14_en","",20553,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_15_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_16_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_1_en","",0,], -["libraries.mediaviewer.impl.viewer_MediaViewerView_2_en","",20539,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_2_en","",20553,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_3_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_4_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_5_en","",0,], @@ -692,7 +692,7 @@ export const screenshots = [ ["libraries.textcomposer.mentions_MentionSpanTheme_Day_0_en","libraries.textcomposer.mentions_MentionSpanTheme_Night_0_en",0,], ["libraries.designsystem.theme.components.previews_Menu_Menus_en","",0,], ["features.messages.impl.messagecomposer_MessageComposerViewVoice_Day_0_en","features.messages.impl.messagecomposer_MessageComposerViewVoice_Night_0_en",0,], -["features.messages.impl.messagecomposer_MessageComposerView_Day_0_en","features.messages.impl.messagecomposer_MessageComposerView_Night_0_en",20539,], +["features.messages.impl.messagecomposer_MessageComposerView_Day_0_en","features.messages.impl.messagecomposer_MessageComposerView_Night_0_en",20553,], ["features.messages.impl.timeline.components_MessageEventBubble_Day_0_en","features.messages.impl.timeline.components_MessageEventBubble_Night_0_en",0,], ["features.messages.impl.timeline.components_MessageEventBubble_Day_1_en","features.messages.impl.timeline.components_MessageEventBubble_Night_1_en",0,], ["features.messages.impl.timeline.components_MessageEventBubble_Day_2_en","features.messages.impl.timeline.components_MessageEventBubble_Night_2_en",0,], @@ -701,7 +701,7 @@ export const screenshots = [ ["features.messages.impl.timeline.components_MessageEventBubble_Day_5_en","features.messages.impl.timeline.components_MessageEventBubble_Night_5_en",0,], ["features.messages.impl.timeline.components_MessageEventBubble_Day_6_en","features.messages.impl.timeline.components_MessageEventBubble_Night_6_en",0,], ["features.messages.impl.timeline.components_MessageEventBubble_Day_7_en","features.messages.impl.timeline.components_MessageEventBubble_Night_7_en",0,], -["features.messages.impl.timeline.components_MessageShieldView_Day_0_en","features.messages.impl.timeline.components_MessageShieldView_Night_0_en",20539,], +["features.messages.impl.timeline.components_MessageShieldView_Day_0_en","features.messages.impl.timeline.components_MessageShieldView_Night_0_en",20553,], ["features.messages.impl.timeline.components_MessageStateEventContainer_Day_0_en","features.messages.impl.timeline.components_MessageStateEventContainer_Night_0_en",0,], ["features.messages.impl.timeline.components_MessagesReactionButtonAdd_Day_0_en","features.messages.impl.timeline.components_MessagesReactionButtonAdd_Night_0_en",0,], ["features.messages.impl.timeline.components_MessagesReactionButtonExtra_Day_0_en","features.messages.impl.timeline.components_MessagesReactionButtonExtra_Night_0_en",0,], @@ -710,23 +710,23 @@ export const screenshots = [ ["features.messages.impl.timeline.components_MessagesReactionButton_Day_2_en","features.messages.impl.timeline.components_MessagesReactionButton_Night_2_en",0,], ["features.messages.impl.timeline.components_MessagesReactionButton_Day_3_en","features.messages.impl.timeline.components_MessagesReactionButton_Night_3_en",0,], ["features.messages.impl_MessagesViewA11y_en","",0,], -["features.messages.impl.topbars_MessagesViewTopBar_Day_0_en","features.messages.impl.topbars_MessagesViewTopBar_Night_0_en",20539,], -["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_0_en",20539,], -["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_1_en",20539,], -["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_2_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_2_en",20539,], -["features.messages.impl_MessagesView_Day_0_en","features.messages.impl_MessagesView_Night_0_en",20539,], -["features.messages.impl_MessagesView_Day_10_en","features.messages.impl_MessagesView_Night_10_en",20539,], -["features.messages.impl_MessagesView_Day_1_en","features.messages.impl_MessagesView_Night_1_en",20539,], -["features.messages.impl_MessagesView_Day_2_en","features.messages.impl_MessagesView_Night_2_en",20539,], -["features.messages.impl_MessagesView_Day_3_en","features.messages.impl_MessagesView_Night_3_en",20539,], -["features.messages.impl_MessagesView_Day_4_en","features.messages.impl_MessagesView_Night_4_en",20539,], -["features.messages.impl_MessagesView_Day_5_en","features.messages.impl_MessagesView_Night_5_en",20539,], -["features.messages.impl_MessagesView_Day_6_en","features.messages.impl_MessagesView_Night_6_en",20539,], -["features.messages.impl_MessagesView_Day_7_en","features.messages.impl_MessagesView_Night_7_en",20539,], -["features.messages.impl_MessagesView_Day_8_en","features.messages.impl_MessagesView_Night_8_en",20539,], -["features.messages.impl_MessagesView_Day_9_en","features.messages.impl_MessagesView_Night_9_en",20539,], +["features.messages.impl.topbars_MessagesViewTopBar_Day_0_en","features.messages.impl.topbars_MessagesViewTopBar_Night_0_en",20553,], +["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_0_en",20553,], +["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_1_en",20553,], +["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_2_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_2_en",20553,], +["features.messages.impl_MessagesView_Day_0_en","features.messages.impl_MessagesView_Night_0_en",20553,], +["features.messages.impl_MessagesView_Day_10_en","features.messages.impl_MessagesView_Night_10_en",20553,], +["features.messages.impl_MessagesView_Day_1_en","features.messages.impl_MessagesView_Night_1_en",20553,], +["features.messages.impl_MessagesView_Day_2_en","features.messages.impl_MessagesView_Night_2_en",20553,], +["features.messages.impl_MessagesView_Day_3_en","features.messages.impl_MessagesView_Night_3_en",20553,], +["features.messages.impl_MessagesView_Day_4_en","features.messages.impl_MessagesView_Night_4_en",20553,], +["features.messages.impl_MessagesView_Day_5_en","features.messages.impl_MessagesView_Night_5_en",20553,], +["features.messages.impl_MessagesView_Day_6_en","features.messages.impl_MessagesView_Night_6_en",20553,], +["features.messages.impl_MessagesView_Day_7_en","features.messages.impl_MessagesView_Night_7_en",20553,], +["features.messages.impl_MessagesView_Day_8_en","features.messages.impl_MessagesView_Night_8_en",20553,], +["features.messages.impl_MessagesView_Day_9_en","features.messages.impl_MessagesView_Night_9_en",20553,], ["features.migration.impl_MigrationView_Day_0_en","features.migration.impl_MigrationView_Night_0_en",0,], -["features.migration.impl_MigrationView_Day_1_en","features.migration.impl_MigrationView_Night_1_en",20539,], +["features.migration.impl_MigrationView_Day_1_en","features.migration.impl_MigrationView_Night_1_en",20553,], ["libraries.designsystem.theme.components_ModalBottomSheetDark_Bottom_Sheets_en","",0,], ["libraries.designsystem.theme.components_ModalBottomSheetLight_Bottom_Sheets_en","",0,], ["appicon.element_MonochromeIcon_en","",0,], @@ -737,112 +737,112 @@ export const screenshots = [ ["libraries.designsystem.components.list_MutipleSelectionListItemSelected_Multiple_selection_List_item_-_selection_in_supporting_text_List_items_en","",0,], ["libraries.designsystem.components.list_MutipleSelectionListItem_Multiple_selection_List_item_-_no_selection_List_items_en","",0,], ["libraries.designsystem.theme.components_NavigationBar_App_Bars_en","",0,], -["features.home.impl.components_NewNotificationSoundBanner_Day_0_en","features.home.impl.components_NewNotificationSoundBanner_Night_0_en",20539,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_0_en","features.preferences.impl.notifications_NotificationSettingsView_Night_0_en",20539,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_10_en","features.preferences.impl.notifications_NotificationSettingsView_Night_10_en",20539,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_11_en","features.preferences.impl.notifications_NotificationSettingsView_Night_11_en",20539,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_12_en","features.preferences.impl.notifications_NotificationSettingsView_Night_12_en",20539,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_13_en","features.preferences.impl.notifications_NotificationSettingsView_Night_13_en",20539,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_1_en","features.preferences.impl.notifications_NotificationSettingsView_Night_1_en",20539,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_2_en","features.preferences.impl.notifications_NotificationSettingsView_Night_2_en",20539,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_3_en","features.preferences.impl.notifications_NotificationSettingsView_Night_3_en",20539,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_4_en","features.preferences.impl.notifications_NotificationSettingsView_Night_4_en",20539,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_5_en","features.preferences.impl.notifications_NotificationSettingsView_Night_5_en",20539,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_6_en","features.preferences.impl.notifications_NotificationSettingsView_Night_6_en",20539,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_7_en","features.preferences.impl.notifications_NotificationSettingsView_Night_7_en",20539,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_8_en","features.preferences.impl.notifications_NotificationSettingsView_Night_8_en",20539,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_9_en","features.preferences.impl.notifications_NotificationSettingsView_Night_9_en",20539,], -["features.ftue.impl.notifications_NotificationsOptInView_Day_0_en","features.ftue.impl.notifications_NotificationsOptInView_Night_0_en",20539,], +["features.home.impl.components_NewNotificationSoundBanner_Day_0_en","features.home.impl.components_NewNotificationSoundBanner_Night_0_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_0_en","features.preferences.impl.notifications_NotificationSettingsView_Night_0_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_10_en","features.preferences.impl.notifications_NotificationSettingsView_Night_10_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_11_en","features.preferences.impl.notifications_NotificationSettingsView_Night_11_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_12_en","features.preferences.impl.notifications_NotificationSettingsView_Night_12_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_13_en","features.preferences.impl.notifications_NotificationSettingsView_Night_13_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_1_en","features.preferences.impl.notifications_NotificationSettingsView_Night_1_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_2_en","features.preferences.impl.notifications_NotificationSettingsView_Night_2_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_3_en","features.preferences.impl.notifications_NotificationSettingsView_Night_3_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_4_en","features.preferences.impl.notifications_NotificationSettingsView_Night_4_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_5_en","features.preferences.impl.notifications_NotificationSettingsView_Night_5_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_6_en","features.preferences.impl.notifications_NotificationSettingsView_Night_6_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_7_en","features.preferences.impl.notifications_NotificationSettingsView_Night_7_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_8_en","features.preferences.impl.notifications_NotificationSettingsView_Night_8_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_9_en","features.preferences.impl.notifications_NotificationSettingsView_Night_9_en",20553,], +["features.ftue.impl.notifications_NotificationsOptInView_Day_0_en","features.ftue.impl.notifications_NotificationsOptInView_Night_0_en",20553,], ["features.linknewdevice.impl.screens.number.component_NumberTextField_Day_0_en","features.linknewdevice.impl.screens.number.component_NumberTextField_Night_0_en",0,], ["libraries.designsystem.atomic.pages_OnBoardingPage_Day_0_en","libraries.designsystem.atomic.pages_OnBoardingPage_Night_0_en",0,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_0_en","features.login.impl.screens.onboarding_OnBoardingView_Night_0_en",20539,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_1_en","features.login.impl.screens.onboarding_OnBoardingView_Night_1_en",20539,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_2_en","features.login.impl.screens.onboarding_OnBoardingView_Night_2_en",20539,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_3_en","features.login.impl.screens.onboarding_OnBoardingView_Night_3_en",20539,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_4_en","features.login.impl.screens.onboarding_OnBoardingView_Night_4_en",20539,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_5_en","features.login.impl.screens.onboarding_OnBoardingView_Night_5_en",20539,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_6_en","features.login.impl.screens.onboarding_OnBoardingView_Night_6_en",20539,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_7_en","features.login.impl.screens.onboarding_OnBoardingView_Night_7_en",20539,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_0_en","features.login.impl.screens.onboarding_OnBoardingView_Night_0_en",20553,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_1_en","features.login.impl.screens.onboarding_OnBoardingView_Night_1_en",20553,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_2_en","features.login.impl.screens.onboarding_OnBoardingView_Night_2_en",20553,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_3_en","features.login.impl.screens.onboarding_OnBoardingView_Night_3_en",20553,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_4_en","features.login.impl.screens.onboarding_OnBoardingView_Night_4_en",20553,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_5_en","features.login.impl.screens.onboarding_OnBoardingView_Night_5_en",20553,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_6_en","features.login.impl.screens.onboarding_OnBoardingView_Night_6_en",20553,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_7_en","features.login.impl.screens.onboarding_OnBoardingView_Night_7_en",20553,], ["libraries.designsystem.background_OnboardingBackground_Day_0_en","libraries.designsystem.background_OnboardingBackground_Night_0_en",0,], -["libraries.matrix.ui.components_OrganizationHeader_Day_0_en","libraries.matrix.ui.components_OrganizationHeader_Night_0_en",20539,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_0_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_0_en",20539,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_10_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_10_en",20539,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_11_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_11_en",20539,], +["libraries.matrix.ui.components_OrganizationHeader_Day_0_en","libraries.matrix.ui.components_OrganizationHeader_Night_0_en",20553,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_0_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_0_en",20553,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_10_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_10_en",20553,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_11_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_11_en",20553,], ["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_12_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_12_en",0,], ["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_13_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_13_en",0,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_1_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_1_en",20539,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_2_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_2_en",20539,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_3_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_3_en",20539,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_4_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_4_en",20539,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_5_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_5_en",20539,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_6_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_6_en",20539,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_7_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_7_en",20539,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_8_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_8_en",20539,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_9_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_9_en",20539,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_1_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_1_en",20553,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_2_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_2_en",20553,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_3_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_3_en",20553,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_4_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_4_en",20553,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_5_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_5_en",20553,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_6_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_6_en",20553,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_7_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_7_en",20553,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_8_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_8_en",20553,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_9_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_9_en",20553,], ["libraries.designsystem.theme.components_OutlinedButtonLargeLowPadding_Buttons_en","",0,], ["libraries.designsystem.theme.components_OutlinedButtonLarge_Buttons_en","",0,], ["libraries.designsystem.theme.components_OutlinedButtonMediumLowPadding_Buttons_en","",0,], ["libraries.designsystem.theme.components_OutlinedButtonMedium_Buttons_en","",0,], ["libraries.designsystem.theme.components_OutlinedButtonSmall_Buttons_en","",0,], -["libraries.mediaviewer.impl.local.pdf_PdfPagesErrorView_Day_0_en","libraries.mediaviewer.impl.local.pdf_PdfPagesErrorView_Night_0_en",20539,], -["features.rolesandpermissions.impl.roles_PendingMemberRowWithLongName_Day_0_en","features.rolesandpermissions.impl.roles_PendingMemberRowWithLongName_Night_0_en",20539,], -["libraries.permissions.api_PermissionsView_Day_0_en","libraries.permissions.api_PermissionsView_Night_0_en",20539,], -["libraries.permissions.api_PermissionsView_Day_1_en","libraries.permissions.api_PermissionsView_Night_1_en",20539,], -["libraries.permissions.api_PermissionsView_Day_2_en","libraries.permissions.api_PermissionsView_Night_2_en",20539,], -["libraries.permissions.api_PermissionsView_Day_3_en","libraries.permissions.api_PermissionsView_Night_3_en",20539,], +["libraries.mediaviewer.impl.local.pdf_PdfPagesErrorView_Day_0_en","libraries.mediaviewer.impl.local.pdf_PdfPagesErrorView_Night_0_en",20553,], +["features.rolesandpermissions.impl.roles_PendingMemberRowWithLongName_Day_0_en","features.rolesandpermissions.impl.roles_PendingMemberRowWithLongName_Night_0_en",20553,], +["libraries.permissions.api_PermissionsView_Day_0_en","libraries.permissions.api_PermissionsView_Night_0_en",20553,], +["libraries.permissions.api_PermissionsView_Day_1_en","libraries.permissions.api_PermissionsView_Night_1_en",20553,], +["libraries.permissions.api_PermissionsView_Day_2_en","libraries.permissions.api_PermissionsView_Night_2_en",20553,], +["libraries.permissions.api_PermissionsView_Day_3_en","libraries.permissions.api_PermissionsView_Night_3_en",20553,], ["features.lockscreen.impl.components_PinEntryTextField_Day_0_en","features.lockscreen.impl.components_PinEntryTextField_Night_0_en",0,], ["features.lockscreen.impl.unlock.keypad_PinKeypad_Day_0_en","features.lockscreen.impl.unlock.keypad_PinKeypad_Night_0_en",0,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_0_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_0_en",20539,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_1_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_1_en",20539,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_2_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_2_en",20539,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_3_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_3_en",20539,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_4_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_4_en",20539,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_5_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_5_en",20539,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_6_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_6_en",20539,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_7_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_7_en",20539,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_0_en","features.lockscreen.impl.unlock_PinUnlockView_Night_0_en",20539,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_1_en","features.lockscreen.impl.unlock_PinUnlockView_Night_1_en",20539,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_2_en","features.lockscreen.impl.unlock_PinUnlockView_Night_2_en",20539,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_3_en","features.lockscreen.impl.unlock_PinUnlockView_Night_3_en",20539,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_4_en","features.lockscreen.impl.unlock_PinUnlockView_Night_4_en",20539,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_5_en","features.lockscreen.impl.unlock_PinUnlockView_Night_5_en",20539,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_6_en","features.lockscreen.impl.unlock_PinUnlockView_Night_6_en",20539,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_7_en","features.lockscreen.impl.unlock_PinUnlockView_Night_7_en",20539,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_0_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_0_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_1_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_1_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_2_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_2_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_3_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_3_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_4_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_4_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_5_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_5_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_6_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_6_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_7_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_7_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_0_en","features.lockscreen.impl.unlock_PinUnlockView_Night_0_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_1_en","features.lockscreen.impl.unlock_PinUnlockView_Night_1_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_2_en","features.lockscreen.impl.unlock_PinUnlockView_Night_2_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_3_en","features.lockscreen.impl.unlock_PinUnlockView_Night_3_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_4_en","features.lockscreen.impl.unlock_PinUnlockView_Night_4_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_5_en","features.lockscreen.impl.unlock_PinUnlockView_Night_5_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_6_en","features.lockscreen.impl.unlock_PinUnlockView_Night_6_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_7_en","features.lockscreen.impl.unlock_PinUnlockView_Night_7_en",20553,], ["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_0_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_0_en",0,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_10_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_10_en",20539,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_1_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_1_en",20539,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_2_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_2_en",20539,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_3_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_3_en",20539,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_4_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_4_en",20539,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_5_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_5_en",20539,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_6_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_6_en",20539,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_7_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_7_en",20539,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_8_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_8_en",20539,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_9_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_9_en",20539,], -["features.messages.impl.pinned.list_PinnedMessagesListView_Day_0_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_0_en",20539,], -["features.messages.impl.pinned.list_PinnedMessagesListView_Day_1_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_1_en",20539,], -["features.messages.impl.pinned.list_PinnedMessagesListView_Day_2_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_2_en",20539,], -["features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_3_en",20539,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_10_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_10_en",20553,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_1_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_1_en",20553,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_2_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_2_en",20553,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_3_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_3_en",20553,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_4_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_4_en",20553,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_5_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_5_en",20553,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_6_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_6_en",20553,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_7_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_7_en",20553,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_8_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_8_en",20553,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_9_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_9_en",20553,], +["features.messages.impl.pinned.list_PinnedMessagesListView_Day_0_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_0_en",20553,], +["features.messages.impl.pinned.list_PinnedMessagesListView_Day_1_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_1_en",20553,], +["features.messages.impl.pinned.list_PinnedMessagesListView_Day_2_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_2_en",20553,], +["features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_3_en",20553,], ["libraries.designsystem.atomic.atoms_PlaceholderAtom_Day_0_en","libraries.designsystem.atomic.atoms_PlaceholderAtom_Night_0_en",0,], ["libraries.designsystem.atomic.atoms_PlaybackSpeedButton_Day_0_en","libraries.designsystem.atomic.atoms_PlaybackSpeedButton_Night_0_en",0,], -["features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Night_0_en",20539,], -["features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Night_0_en",20539,], -["features.poll.api.pollcontent_PollAnswerViewEndedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedSelected_Night_0_en",20539,], -["features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Night_0_en",20539,], -["features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Night_0_en",20539,], +["features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Night_0_en",20553,], +["features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Night_0_en",20553,], +["features.poll.api.pollcontent_PollAnswerViewEndedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedSelected_Night_0_en",20553,], +["features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Night_0_en",20553,], +["features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Night_0_en",20553,], ["features.poll.api.pollcontent_PollAnswerViewUndisclosedNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewUndisclosedNotSelected_Night_0_en",0,], ["features.poll.api.pollcontent_PollAnswerViewUndisclosedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewUndisclosedSelected_Night_0_en",0,], -["features.poll.api.pollcontent_PollContentViewCreatorEditable_Day_0_en","features.poll.api.pollcontent_PollContentViewCreatorEditable_Night_0_en",20539,], -["features.poll.api.pollcontent_PollContentViewCreatorEnded_Day_0_en","features.poll.api.pollcontent_PollContentViewCreatorEnded_Night_0_en",20539,], -["features.poll.api.pollcontent_PollContentViewCreator_Day_0_en","features.poll.api.pollcontent_PollContentViewCreator_Night_0_en",20539,], -["features.poll.api.pollcontent_PollContentViewDisclosed_Day_0_en","features.poll.api.pollcontent_PollContentViewDisclosed_Night_0_en",20539,], -["features.poll.api.pollcontent_PollContentViewEnded_Day_0_en","features.poll.api.pollcontent_PollContentViewEnded_Night_0_en",20539,], -["features.poll.api.pollcontent_PollContentViewUndisclosed_Day_0_en","features.poll.api.pollcontent_PollContentViewUndisclosed_Night_0_en",20539,], -["features.poll.impl.history_PollHistoryView_Day_0_en","features.poll.impl.history_PollHistoryView_Night_0_en",20539,], -["features.poll.impl.history_PollHistoryView_Day_1_en","features.poll.impl.history_PollHistoryView_Night_1_en",20539,], -["features.poll.impl.history_PollHistoryView_Day_2_en","features.poll.impl.history_PollHistoryView_Night_2_en",20539,], -["features.poll.impl.history_PollHistoryView_Day_3_en","features.poll.impl.history_PollHistoryView_Night_3_en",20539,], -["features.poll.impl.history_PollHistoryView_Day_4_en","features.poll.impl.history_PollHistoryView_Night_4_en",20539,], +["features.poll.api.pollcontent_PollContentViewCreatorEditable_Day_0_en","features.poll.api.pollcontent_PollContentViewCreatorEditable_Night_0_en",20553,], +["features.poll.api.pollcontent_PollContentViewCreatorEnded_Day_0_en","features.poll.api.pollcontent_PollContentViewCreatorEnded_Night_0_en",20553,], +["features.poll.api.pollcontent_PollContentViewCreator_Day_0_en","features.poll.api.pollcontent_PollContentViewCreator_Night_0_en",20553,], +["features.poll.api.pollcontent_PollContentViewDisclosed_Day_0_en","features.poll.api.pollcontent_PollContentViewDisclosed_Night_0_en",20553,], +["features.poll.api.pollcontent_PollContentViewEnded_Day_0_en","features.poll.api.pollcontent_PollContentViewEnded_Night_0_en",20553,], +["features.poll.api.pollcontent_PollContentViewUndisclosed_Day_0_en","features.poll.api.pollcontent_PollContentViewUndisclosed_Night_0_en",20553,], +["features.poll.impl.history_PollHistoryView_Day_0_en","features.poll.impl.history_PollHistoryView_Night_0_en",20553,], +["features.poll.impl.history_PollHistoryView_Day_1_en","features.poll.impl.history_PollHistoryView_Night_1_en",20553,], +["features.poll.impl.history_PollHistoryView_Day_2_en","features.poll.impl.history_PollHistoryView_Night_2_en",20553,], +["features.poll.impl.history_PollHistoryView_Day_3_en","features.poll.impl.history_PollHistoryView_Night_3_en",20553,], +["features.poll.impl.history_PollHistoryView_Day_4_en","features.poll.impl.history_PollHistoryView_Night_4_en",20553,], ["features.poll.api.pollcontent_PollTitleView_Day_0_en","features.poll.api.pollcontent_PollTitleView_Night_0_en",0,], ["libraries.designsystem.components.preferences_PreferenceCategory_Preferences_en","",0,], ["libraries.designsystem.components.preferences_PreferenceCheckbox_Preferences_en","",0,], @@ -856,215 +856,215 @@ export const screenshots = [ ["libraries.designsystem.components.preferences_PreferenceRow_Preferences_en","",0,], ["libraries.designsystem.components.preferences_PreferenceSlide_Preferences_en","",0,], ["libraries.designsystem.components.preferences_PreferenceSwitch_Preferences_en","",0,], -["features.preferences.impl.root_PreferencesRootViewDark_0_en","",20539,], -["features.preferences.impl.root_PreferencesRootViewDark_1_en","",20539,], -["features.preferences.impl.root_PreferencesRootViewLight_0_en","",20539,], -["features.preferences.impl.root_PreferencesRootViewLight_1_en","",20539,], +["features.preferences.impl.root_PreferencesRootViewDark_0_en","",20553,], +["features.preferences.impl.root_PreferencesRootViewDark_1_en","",20553,], +["features.preferences.impl.root_PreferencesRootViewLight_0_en","",20553,], +["features.preferences.impl.root_PreferencesRootViewLight_1_en","",20553,], ["features.messages.impl.timeline.components.event_ProgressButton_Day_0_en","features.messages.impl.timeline.components.event_ProgressButton_Night_0_en",0,], -["libraries.designsystem.components_ProgressDialogContent_Dialogs_en","",20539,], -["libraries.designsystem.components_ProgressDialogWithContent_Day_0_en","libraries.designsystem.components_ProgressDialogWithContent_Night_0_en",20539,], +["libraries.designsystem.components_ProgressDialogContent_Dialogs_en","",20553,], +["libraries.designsystem.components_ProgressDialogWithContent_Day_0_en","libraries.designsystem.components_ProgressDialogWithContent_Night_0_en",20553,], ["libraries.designsystem.components_ProgressDialogWithTextAndContent_Day_0_en","libraries.designsystem.components_ProgressDialogWithTextAndContent_Night_0_en",0,], -["libraries.designsystem.components_ProgressDialog_Day_0_en","libraries.designsystem.components_ProgressDialog_Night_0_en",20539,], -["features.messages.impl.timeline.protection_ProtectedView_Day_0_en","features.messages.impl.timeline.protection_ProtectedView_Night_0_en",20539,], -["features.messages.impl.timeline.protection_ProtectedView_Day_1_en","features.messages.impl.timeline.protection_ProtectedView_Night_1_en",20539,], -["features.messages.impl.timeline.protection_ProtectedView_Day_2_en","features.messages.impl.timeline.protection_ProtectedView_Night_2_en",20539,], -["features.messages.impl.timeline.protection_ProtectedView_Day_3_en","features.messages.impl.timeline.protection_ProtectedView_Night_3_en",20539,], -["libraries.troubleshoot.impl.history_PushHistoryView_Day_0_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_0_en",20539,], -["libraries.troubleshoot.impl.history_PushHistoryView_Day_1_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_1_en",20539,], -["libraries.troubleshoot.impl.history_PushHistoryView_Day_2_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_2_en",20539,], -["libraries.troubleshoot.impl.history_PushHistoryView_Day_3_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_3_en",20539,], -["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_0_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_0_en",20539,], -["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_1_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_1_en",20539,], -["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_2_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_2_en",20539,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_0_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_0_en",20539,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_1_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_1_en",20539,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_2_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_2_en",20539,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_3_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_3_en",20539,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_4_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_4_en",20539,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_5_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_5_en",20539,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_6_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_6_en",20539,], -["features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_0_en","features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_0_en",20539,], -["features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_1_en","features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_1_en",20539,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_0_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_0_en",20539,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_1_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_1_en",20539,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_2_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_2_en",20539,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_3_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_3_en",20539,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_4_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_4_en",20539,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_5_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_5_en",20539,], +["libraries.designsystem.components_ProgressDialog_Day_0_en","libraries.designsystem.components_ProgressDialog_Night_0_en",20553,], +["features.messages.impl.timeline.protection_ProtectedView_Day_0_en","features.messages.impl.timeline.protection_ProtectedView_Night_0_en",20553,], +["features.messages.impl.timeline.protection_ProtectedView_Day_1_en","features.messages.impl.timeline.protection_ProtectedView_Night_1_en",20553,], +["features.messages.impl.timeline.protection_ProtectedView_Day_2_en","features.messages.impl.timeline.protection_ProtectedView_Night_2_en",20553,], +["features.messages.impl.timeline.protection_ProtectedView_Day_3_en","features.messages.impl.timeline.protection_ProtectedView_Night_3_en",20553,], +["libraries.troubleshoot.impl.history_PushHistoryView_Day_0_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_0_en",20553,], +["libraries.troubleshoot.impl.history_PushHistoryView_Day_1_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_1_en",20553,], +["libraries.troubleshoot.impl.history_PushHistoryView_Day_2_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_2_en",20553,], +["libraries.troubleshoot.impl.history_PushHistoryView_Day_3_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_3_en",20553,], +["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_0_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_0_en",20553,], +["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_1_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_1_en",20553,], +["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_2_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_2_en",20553,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_0_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_0_en",20553,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_1_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_1_en",20553,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_2_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_2_en",20553,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_3_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_3_en",20553,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_4_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_4_en",20553,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_5_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_5_en",20553,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_6_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_6_en",20553,], +["features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_0_en","features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_0_en",20553,], +["features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_1_en","features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_1_en",20553,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_0_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_0_en",20553,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_1_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_1_en",20553,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_2_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_2_en",20553,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_3_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_3_en",20553,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_4_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_4_en",20553,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_5_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_5_en",20553,], ["libraries.qrcode_QrCodeView_en","",0,], ["libraries.designsystem.theme.components_RadioButton_Toggles_en","",0,], -["features.rageshake.api.detection_RageshakeDialogContent_Day_0_en","features.rageshake.api.detection_RageshakeDialogContent_Night_0_en",20539,], -["features.rageshake.api.preferences_RageshakePreferencesView_Day_0_en","features.rageshake.api.preferences_RageshakePreferencesView_Night_0_en",20539,], +["features.rageshake.api.detection_RageshakeDialogContent_Day_0_en","features.rageshake.api.detection_RageshakeDialogContent_Night_0_en",20553,], +["features.rageshake.api.preferences_RageshakePreferencesView_Day_0_en","features.rageshake.api.preferences_RageshakePreferencesView_Night_0_en",20553,], ["features.rageshake.api.preferences_RageshakePreferencesView_Day_1_en","features.rageshake.api.preferences_RageshakePreferencesView_Night_1_en",0,], ["features.messages.impl.timeline.components.reactionsummary_ReactionSummaryViewContent_Day_0_en","features.messages.impl.timeline.components.reactionsummary_ReactionSummaryViewContent_Night_0_en",0,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_0_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_0_en",20539,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_1_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_1_en",20539,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_2_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_2_en",20539,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_3_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_3_en",20539,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_4_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_4_en",20539,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_5_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_5_en",20539,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_0_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_0_en",20539,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_10_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_10_en",20539,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_11_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_11_en",20539,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_12_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_12_en",20539,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_13_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_13_en",20539,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_14_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_14_en",20539,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_1_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_1_en",20539,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_2_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_2_en",20539,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_3_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_3_en",20539,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_4_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_4_en",20539,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_5_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_5_en",20539,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_6_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_6_en",20539,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_7_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_7_en",20539,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_8_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_8_en",20539,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_9_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_9_en",20539,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_0_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_0_en",20553,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_1_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_1_en",20553,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_2_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_2_en",20553,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_3_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_3_en",20553,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_4_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_4_en",20553,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_5_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_5_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_0_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_0_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_10_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_10_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_11_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_11_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_12_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_12_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_13_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_13_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_14_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_14_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_1_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_1_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_2_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_2_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_3_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_3_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_4_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_4_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_5_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_5_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_6_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_6_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_7_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_7_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_8_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_8_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_9_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_9_en",20553,], ["libraries.designsystem.atomic.atoms_RedIndicatorAtom_Day_0_en","libraries.designsystem.atomic.atoms_RedIndicatorAtom_Night_0_en",0,], ["features.messages.impl.timeline.components_ReplySwipeIndicator_Day_0_en","features.messages.impl.timeline.components_ReplySwipeIndicator_Night_0_en",0,], -["features.messages.impl.report_ReportMessageView_Day_0_en","features.messages.impl.report_ReportMessageView_Night_0_en",20539,], -["features.messages.impl.report_ReportMessageView_Day_1_en","features.messages.impl.report_ReportMessageView_Night_1_en",20539,], -["features.messages.impl.report_ReportMessageView_Day_2_en","features.messages.impl.report_ReportMessageView_Night_2_en",20539,], -["features.messages.impl.report_ReportMessageView_Day_3_en","features.messages.impl.report_ReportMessageView_Night_3_en",20539,], -["features.messages.impl.report_ReportMessageView_Day_4_en","features.messages.impl.report_ReportMessageView_Night_4_en",20539,], -["features.messages.impl.report_ReportMessageView_Day_5_en","features.messages.impl.report_ReportMessageView_Night_5_en",20539,], -["features.reportroom.impl_ReportRoomView_Day_0_en","features.reportroom.impl_ReportRoomView_Night_0_en",20539,], -["features.reportroom.impl_ReportRoomView_Day_1_en","features.reportroom.impl_ReportRoomView_Night_1_en",20539,], -["features.reportroom.impl_ReportRoomView_Day_2_en","features.reportroom.impl_ReportRoomView_Night_2_en",20539,], -["features.reportroom.impl_ReportRoomView_Day_3_en","features.reportroom.impl_ReportRoomView_Night_3_en",20539,], -["features.reportroom.impl_ReportRoomView_Day_4_en","features.reportroom.impl_ReportRoomView_Night_4_en",20539,], -["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_0_en",20539,], -["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_1_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_1_en",20539,], -["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_2_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_2_en",20539,], -["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_3_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_3_en",20539,], -["features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en","features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en",20539,], -["features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en","features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en",20539,], +["features.messages.impl.report_ReportMessageView_Day_0_en","features.messages.impl.report_ReportMessageView_Night_0_en",20553,], +["features.messages.impl.report_ReportMessageView_Day_1_en","features.messages.impl.report_ReportMessageView_Night_1_en",20553,], +["features.messages.impl.report_ReportMessageView_Day_2_en","features.messages.impl.report_ReportMessageView_Night_2_en",20553,], +["features.messages.impl.report_ReportMessageView_Day_3_en","features.messages.impl.report_ReportMessageView_Night_3_en",20553,], +["features.messages.impl.report_ReportMessageView_Day_4_en","features.messages.impl.report_ReportMessageView_Night_4_en",20553,], +["features.messages.impl.report_ReportMessageView_Day_5_en","features.messages.impl.report_ReportMessageView_Night_5_en",20553,], +["features.reportroom.impl_ReportRoomView_Day_0_en","features.reportroom.impl_ReportRoomView_Night_0_en",20553,], +["features.reportroom.impl_ReportRoomView_Day_1_en","features.reportroom.impl_ReportRoomView_Night_1_en",20553,], +["features.reportroom.impl_ReportRoomView_Day_2_en","features.reportroom.impl_ReportRoomView_Night_2_en",20553,], +["features.reportroom.impl_ReportRoomView_Day_3_en","features.reportroom.impl_ReportRoomView_Night_3_en",20553,], +["features.reportroom.impl_ReportRoomView_Day_4_en","features.reportroom.impl_ReportRoomView_Night_4_en",20553,], +["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_0_en",20553,], +["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_1_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_1_en",20553,], +["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_2_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_2_en",20553,], +["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_3_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_3_en",20553,], +["features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en","features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en",20553,], +["features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en","features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en",20553,], ["features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_0_en","features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_0_en",0,], -["features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_1_en","features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_1_en",20539,], -["features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_en","features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_2_en",20539,], -["libraries.designsystem.components.dialogs_RetryDialogContent_Dialogs_en","",20539,], -["libraries.designsystem.components.dialogs_RetryDialog_Day_0_en","libraries.designsystem.components.dialogs_RetryDialog_Night_0_en",20539,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_0_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_0_en",20539,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_1_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_1_en",20539,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_2_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_2_en",20539,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_3_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_3_en",20539,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_4_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_4_en",20539,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_5_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_5_en",20539,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_6_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_6_en",20539,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_7_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_7_en",20539,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_8_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_8_en",20539,], +["features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_1_en","features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_1_en",20553,], +["features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_en","features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_2_en",20553,], +["libraries.designsystem.components.dialogs_RetryDialogContent_Dialogs_en","",20553,], +["libraries.designsystem.components.dialogs_RetryDialog_Day_0_en","libraries.designsystem.components.dialogs_RetryDialog_Night_0_en",20553,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_0_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_0_en",20553,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_1_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_1_en",20553,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_2_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_2_en",20553,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_3_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_3_en",20553,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_4_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_4_en",20553,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_5_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_5_en",20553,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_6_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_6_en",20553,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_7_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_7_en",20553,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_8_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_8_en",20553,], ["libraries.matrix.ui.room.address_RoomAddressField_Day_0_en","libraries.matrix.ui.room.address_RoomAddressField_Night_0_en",0,], ["features.roomaliasresolver.impl_RoomAliasResolverView_Day_0_en","features.roomaliasresolver.impl_RoomAliasResolverView_Night_0_en",0,], -["features.roomaliasresolver.impl_RoomAliasResolverView_Day_1_en","features.roomaliasresolver.impl_RoomAliasResolverView_Night_1_en",20539,], -["features.roomaliasresolver.impl_RoomAliasResolverView_Day_2_en","features.roomaliasresolver.impl_RoomAliasResolverView_Night_2_en",20539,], +["features.roomaliasresolver.impl_RoomAliasResolverView_Day_1_en","features.roomaliasresolver.impl_RoomAliasResolverView_Night_1_en",20553,], +["features.roomaliasresolver.impl_RoomAliasResolverView_Day_2_en","features.roomaliasresolver.impl_RoomAliasResolverView_Night_2_en",20553,], ["features.roomdetails.impl_RoomDetailsA11y_en","",0,], -["features.roomdetails.impl_RoomDetailsDark_0_en","",20539,], -["features.roomdetails.impl_RoomDetailsDark_10_en","",20539,], -["features.roomdetails.impl_RoomDetailsDark_11_en","",20539,], -["features.roomdetails.impl_RoomDetailsDark_12_en","",20539,], -["features.roomdetails.impl_RoomDetailsDark_13_en","",20539,], -["features.roomdetails.impl_RoomDetailsDark_14_en","",20539,], -["features.roomdetails.impl_RoomDetailsDark_15_en","",20539,], -["features.roomdetails.impl_RoomDetailsDark_16_en","",20539,], -["features.roomdetails.impl_RoomDetailsDark_17_en","",20539,], -["features.roomdetails.impl_RoomDetailsDark_18_en","",20539,], -["features.roomdetails.impl_RoomDetailsDark_19_en","",20539,], -["features.roomdetails.impl_RoomDetailsDark_1_en","",20539,], -["features.roomdetails.impl_RoomDetailsDark_20_en","",20539,], -["features.roomdetails.impl_RoomDetailsDark_21_en","",20539,], -["features.roomdetails.impl_RoomDetailsDark_22_en","",20539,], -["features.roomdetails.impl_RoomDetailsDark_2_en","",20539,], -["features.roomdetails.impl_RoomDetailsDark_3_en","",20539,], -["features.roomdetails.impl_RoomDetailsDark_4_en","",20539,], -["features.roomdetails.impl_RoomDetailsDark_5_en","",20539,], -["features.roomdetails.impl_RoomDetailsDark_6_en","",20539,], -["features.roomdetails.impl_RoomDetailsDark_7_en","",20539,], -["features.roomdetails.impl_RoomDetailsDark_8_en","",20539,], -["features.roomdetails.impl_RoomDetailsDark_9_en","",20539,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_0_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_0_en",20539,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_1_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_1_en",20539,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_2_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_2_en",20539,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_3_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_3_en",20539,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_4_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_4_en",20539,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_5_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_5_en",20539,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_6_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_6_en",20539,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_7_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_7_en",20539,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_8_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_8_en",20539,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_9_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_9_en",20539,], -["features.roomdetails.impl_RoomDetails_0_en","",20539,], -["features.roomdetails.impl_RoomDetails_10_en","",20539,], -["features.roomdetails.impl_RoomDetails_11_en","",20539,], -["features.roomdetails.impl_RoomDetails_12_en","",20539,], -["features.roomdetails.impl_RoomDetails_13_en","",20539,], -["features.roomdetails.impl_RoomDetails_14_en","",20539,], -["features.roomdetails.impl_RoomDetails_15_en","",20539,], -["features.roomdetails.impl_RoomDetails_16_en","",20539,], -["features.roomdetails.impl_RoomDetails_17_en","",20539,], -["features.roomdetails.impl_RoomDetails_18_en","",20539,], -["features.roomdetails.impl_RoomDetails_19_en","",20539,], -["features.roomdetails.impl_RoomDetails_1_en","",20539,], -["features.roomdetails.impl_RoomDetails_20_en","",20539,], -["features.roomdetails.impl_RoomDetails_21_en","",20539,], -["features.roomdetails.impl_RoomDetails_22_en","",20539,], -["features.roomdetails.impl_RoomDetails_2_en","",20539,], -["features.roomdetails.impl_RoomDetails_3_en","",20539,], -["features.roomdetails.impl_RoomDetails_4_en","",20539,], -["features.roomdetails.impl_RoomDetails_5_en","",20539,], -["features.roomdetails.impl_RoomDetails_6_en","",20539,], -["features.roomdetails.impl_RoomDetails_7_en","",20539,], -["features.roomdetails.impl_RoomDetails_8_en","",20539,], -["features.roomdetails.impl_RoomDetails_9_en","",20539,], -["features.roomdirectory.impl.root_RoomDirectoryView_Day_0_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_0_en",20539,], -["features.roomdirectory.impl.root_RoomDirectoryView_Day_1_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_1_en",20539,], -["features.roomdirectory.impl.root_RoomDirectoryView_Day_2_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_2_en",20539,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_0_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_0_en",20539,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_1_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_1_en",20539,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_2_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_2_en",20539,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_3_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_3_en",20539,], -["features.home.impl.components_RoomListContentView_Day_0_en","features.home.impl.components_RoomListContentView_Night_0_en",20539,], -["features.home.impl.components_RoomListContentView_Day_1_en","features.home.impl.components_RoomListContentView_Night_1_en",20539,], +["features.roomdetails.impl_RoomDetailsDark_0_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_10_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_11_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_12_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_13_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_14_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_15_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_16_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_17_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_18_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_19_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_1_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_20_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_21_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_22_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_2_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_3_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_4_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_5_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_6_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_7_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_8_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_9_en","",20553,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_0_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_0_en",20553,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_1_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_1_en",20553,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_2_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_2_en",20553,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_3_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_3_en",20553,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_4_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_4_en",20553,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_5_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_5_en",20553,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_6_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_6_en",20553,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_7_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_7_en",20553,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_8_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_8_en",20553,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_9_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_9_en",20553,], +["features.roomdetails.impl_RoomDetails_0_en","",20553,], +["features.roomdetails.impl_RoomDetails_10_en","",20553,], +["features.roomdetails.impl_RoomDetails_11_en","",20553,], +["features.roomdetails.impl_RoomDetails_12_en","",20553,], +["features.roomdetails.impl_RoomDetails_13_en","",20553,], +["features.roomdetails.impl_RoomDetails_14_en","",20553,], +["features.roomdetails.impl_RoomDetails_15_en","",20553,], +["features.roomdetails.impl_RoomDetails_16_en","",20553,], +["features.roomdetails.impl_RoomDetails_17_en","",20553,], +["features.roomdetails.impl_RoomDetails_18_en","",20553,], +["features.roomdetails.impl_RoomDetails_19_en","",20553,], +["features.roomdetails.impl_RoomDetails_1_en","",20553,], +["features.roomdetails.impl_RoomDetails_20_en","",20553,], +["features.roomdetails.impl_RoomDetails_21_en","",20553,], +["features.roomdetails.impl_RoomDetails_22_en","",20553,], +["features.roomdetails.impl_RoomDetails_2_en","",20553,], +["features.roomdetails.impl_RoomDetails_3_en","",20553,], +["features.roomdetails.impl_RoomDetails_4_en","",20553,], +["features.roomdetails.impl_RoomDetails_5_en","",20553,], +["features.roomdetails.impl_RoomDetails_6_en","",20553,], +["features.roomdetails.impl_RoomDetails_7_en","",20553,], +["features.roomdetails.impl_RoomDetails_8_en","",20553,], +["features.roomdetails.impl_RoomDetails_9_en","",20553,], +["features.roomdirectory.impl.root_RoomDirectoryView_Day_0_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_0_en",20553,], +["features.roomdirectory.impl.root_RoomDirectoryView_Day_1_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_1_en",20553,], +["features.roomdirectory.impl.root_RoomDirectoryView_Day_2_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_2_en",20553,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_0_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_0_en",20553,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_1_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_1_en",20553,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_2_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_2_en",20553,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_3_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_3_en",20553,], +["features.home.impl.components_RoomListContentView_Day_0_en","features.home.impl.components_RoomListContentView_Night_0_en",20553,], +["features.home.impl.components_RoomListContentView_Day_1_en","features.home.impl.components_RoomListContentView_Night_1_en",20553,], ["features.home.impl.components_RoomListContentView_Day_2_en","features.home.impl.components_RoomListContentView_Night_2_en",0,], -["features.home.impl.components_RoomListContentView_Day_3_en","features.home.impl.components_RoomListContentView_Night_3_en",20539,], -["features.home.impl.components_RoomListContentView_Day_4_en","features.home.impl.components_RoomListContentView_Night_4_en",20539,], -["features.home.impl.components_RoomListContentView_Day_5_en","features.home.impl.components_RoomListContentView_Night_5_en",20539,], -["features.home.impl.roomlist_RoomListDeclineInviteMenuContent_Day_0_en","features.home.impl.roomlist_RoomListDeclineInviteMenuContent_Night_0_en",20539,], -["features.home.impl.filters_RoomListFiltersView_Day_0_en","features.home.impl.filters_RoomListFiltersView_Night_0_en",20539,], -["features.home.impl.filters_RoomListFiltersView_Day_1_en","features.home.impl.filters_RoomListFiltersView_Night_1_en",20539,], -["features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_0_en","features.home.impl.roomlist_RoomListModalBottomSheetContent_Night_0_en",20539,], -["features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_1_en","features.home.impl.roomlist_RoomListModalBottomSheetContent_Night_1_en",20539,], -["features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_2_en","features.home.impl.roomlist_RoomListModalBottomSheetContent_Night_2_en",20539,], +["features.home.impl.components_RoomListContentView_Day_3_en","features.home.impl.components_RoomListContentView_Night_3_en",20553,], +["features.home.impl.components_RoomListContentView_Day_4_en","features.home.impl.components_RoomListContentView_Night_4_en",20553,], +["features.home.impl.components_RoomListContentView_Day_5_en","features.home.impl.components_RoomListContentView_Night_5_en",20553,], +["features.home.impl.roomlist_RoomListDeclineInviteMenuContent_Day_0_en","features.home.impl.roomlist_RoomListDeclineInviteMenuContent_Night_0_en",20553,], +["features.home.impl.filters_RoomListFiltersView_Day_0_en","features.home.impl.filters_RoomListFiltersView_Night_0_en",20553,], +["features.home.impl.filters_RoomListFiltersView_Day_1_en","features.home.impl.filters_RoomListFiltersView_Night_1_en",20553,], +["features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_0_en","features.home.impl.roomlist_RoomListModalBottomSheetContent_Night_0_en",20553,], +["features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_1_en","features.home.impl.roomlist_RoomListModalBottomSheetContent_Night_1_en",20553,], +["features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_2_en","features.home.impl.roomlist_RoomListModalBottomSheetContent_Night_2_en",20553,], ["features.home.impl.search_RoomListSearchContent_Day_0_en","features.home.impl.search_RoomListSearchContent_Night_0_en",0,], -["features.home.impl.search_RoomListSearchContent_Day_1_en","features.home.impl.search_RoomListSearchContent_Night_1_en",20539,], -["features.roomdetails.impl.members_RoomMemberListView_Day_0_en","features.roomdetails.impl.members_RoomMemberListView_Night_0_en",20539,], -["features.roomdetails.impl.members_RoomMemberListView_Day_1_en","features.roomdetails.impl.members_RoomMemberListView_Night_1_en",20539,], -["features.roomdetails.impl.members_RoomMemberListView_Day_2_en","features.roomdetails.impl.members_RoomMemberListView_Night_2_en",20539,], -["features.roomdetails.impl.members_RoomMemberListView_Day_3_en","features.roomdetails.impl.members_RoomMemberListView_Night_3_en",20539,], -["features.roomdetails.impl.members_RoomMemberListView_Day_4_en","features.roomdetails.impl.members_RoomMemberListView_Night_4_en",20539,], -["features.roomdetails.impl.members_RoomMemberListView_Day_5_en","features.roomdetails.impl.members_RoomMemberListView_Night_5_en",20539,], -["features.roomdetails.impl.members_RoomMemberListView_Day_6_en","features.roomdetails.impl.members_RoomMemberListView_Night_6_en",20539,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_0_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_0_en",20539,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_1_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_1_en",20539,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_2_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_2_en",20539,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_3_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_3_en",20539,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_4_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_4_en",20539,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_5_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_5_en",20539,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_6_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_6_en",20539,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_7_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_7_en",20539,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_8_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_8_en",20539,], +["features.home.impl.search_RoomListSearchContent_Day_1_en","features.home.impl.search_RoomListSearchContent_Night_1_en",20553,], +["features.roomdetails.impl.members_RoomMemberListView_Day_0_en","features.roomdetails.impl.members_RoomMemberListView_Night_0_en",20553,], +["features.roomdetails.impl.members_RoomMemberListView_Day_1_en","features.roomdetails.impl.members_RoomMemberListView_Night_1_en",20553,], +["features.roomdetails.impl.members_RoomMemberListView_Day_2_en","features.roomdetails.impl.members_RoomMemberListView_Night_2_en",20553,], +["features.roomdetails.impl.members_RoomMemberListView_Day_3_en","features.roomdetails.impl.members_RoomMemberListView_Night_3_en",20553,], +["features.roomdetails.impl.members_RoomMemberListView_Day_4_en","features.roomdetails.impl.members_RoomMemberListView_Night_4_en",20553,], +["features.roomdetails.impl.members_RoomMemberListView_Day_5_en","features.roomdetails.impl.members_RoomMemberListView_Night_5_en",20553,], +["features.roomdetails.impl.members_RoomMemberListView_Day_6_en","features.roomdetails.impl.members_RoomMemberListView_Night_6_en",20553,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_0_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_0_en",20553,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_1_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_1_en",20553,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_2_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_2_en",20553,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_3_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_3_en",20553,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_4_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_4_en",20553,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_5_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_5_en",20553,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_6_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_6_en",20553,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_7_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_7_en",20553,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_8_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_8_en",20553,], ["features.roommembermoderation.impl_RoomMemberModerationView_Day_9_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_9_en",0,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Day_0_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Night_0_en",20539,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_0_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_0_en",20539,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_1_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_1_en",20539,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_2_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_2_en",20539,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_3_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_3_en",20539,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_4_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_4_en",20539,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_5_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_5_en",20539,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_6_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_6_en",20539,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Day_0_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Night_0_en",20553,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_0_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_0_en",20553,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_1_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_1_en",20553,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_2_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_2_en",20553,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_3_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_3_en",20553,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_4_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_4_en",20553,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_5_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_5_en",20553,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_6_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_6_en",20553,], ["libraries.designsystem.atomic.atoms_RoomPreviewAliasAtom_Day_0_en","libraries.designsystem.atomic.atoms_RoomPreviewAliasAtom_Night_0_en",0,], -["libraries.roomselect.impl_RoomSelectView_Day_0_en","libraries.roomselect.impl_RoomSelectView_Night_0_en",20539,], -["libraries.roomselect.impl_RoomSelectView_Day_1_en","libraries.roomselect.impl_RoomSelectView_Night_1_en",20539,], -["libraries.roomselect.impl_RoomSelectView_Day_2_en","libraries.roomselect.impl_RoomSelectView_Night_2_en",20539,], -["libraries.roomselect.impl_RoomSelectView_Day_3_en","libraries.roomselect.impl_RoomSelectView_Night_3_en",20539,], -["libraries.roomselect.impl_RoomSelectView_Day_4_en","libraries.roomselect.impl_RoomSelectView_Night_4_en",20539,], -["libraries.roomselect.impl_RoomSelectView_Day_5_en","libraries.roomselect.impl_RoomSelectView_Night_5_en",20539,], +["libraries.roomselect.impl_RoomSelectView_Day_0_en","libraries.roomselect.impl_RoomSelectView_Night_0_en",20553,], +["libraries.roomselect.impl_RoomSelectView_Day_1_en","libraries.roomselect.impl_RoomSelectView_Night_1_en",20553,], +["libraries.roomselect.impl_RoomSelectView_Day_2_en","libraries.roomselect.impl_RoomSelectView_Night_2_en",20553,], +["libraries.roomselect.impl_RoomSelectView_Day_3_en","libraries.roomselect.impl_RoomSelectView_Night_3_en",20553,], +["libraries.roomselect.impl_RoomSelectView_Day_4_en","libraries.roomselect.impl_RoomSelectView_Night_4_en",20553,], +["libraries.roomselect.impl_RoomSelectView_Day_5_en","libraries.roomselect.impl_RoomSelectView_Night_5_en",20553,], ["features.home.impl.components_RoomSummaryPlaceholderRow_Day_0_en","features.home.impl.components_RoomSummaryPlaceholderRow_Night_0_en",0,], ["features.home.impl.components_RoomSummaryRow_Day_0_en","features.home.impl.components_RoomSummaryRow_Night_0_en",0,], ["features.home.impl.components_RoomSummaryRow_Day_10_en","features.home.impl.components_RoomSummaryRow_Night_10_en",0,], @@ -1087,16 +1087,16 @@ export const screenshots = [ ["features.home.impl.components_RoomSummaryRow_Day_26_en","features.home.impl.components_RoomSummaryRow_Night_26_en",0,], ["features.home.impl.components_RoomSummaryRow_Day_27_en","features.home.impl.components_RoomSummaryRow_Night_27_en",0,], ["features.home.impl.components_RoomSummaryRow_Day_28_en","features.home.impl.components_RoomSummaryRow_Night_28_en",0,], -["features.home.impl.components_RoomSummaryRow_Day_29_en","features.home.impl.components_RoomSummaryRow_Night_29_en",20539,], -["features.home.impl.components_RoomSummaryRow_Day_2_en","features.home.impl.components_RoomSummaryRow_Night_2_en",20539,], -["features.home.impl.components_RoomSummaryRow_Day_30_en","features.home.impl.components_RoomSummaryRow_Night_30_en",20539,], -["features.home.impl.components_RoomSummaryRow_Day_31_en","features.home.impl.components_RoomSummaryRow_Night_31_en",20539,], -["features.home.impl.components_RoomSummaryRow_Day_32_en","features.home.impl.components_RoomSummaryRow_Night_32_en",20539,], -["features.home.impl.components_RoomSummaryRow_Day_33_en","features.home.impl.components_RoomSummaryRow_Night_33_en",20539,], -["features.home.impl.components_RoomSummaryRow_Day_34_en","features.home.impl.components_RoomSummaryRow_Night_34_en",20539,], -["features.home.impl.components_RoomSummaryRow_Day_35_en","features.home.impl.components_RoomSummaryRow_Night_35_en",20539,], +["features.home.impl.components_RoomSummaryRow_Day_29_en","features.home.impl.components_RoomSummaryRow_Night_29_en",20553,], +["features.home.impl.components_RoomSummaryRow_Day_2_en","features.home.impl.components_RoomSummaryRow_Night_2_en",20553,], +["features.home.impl.components_RoomSummaryRow_Day_30_en","features.home.impl.components_RoomSummaryRow_Night_30_en",20553,], +["features.home.impl.components_RoomSummaryRow_Day_31_en","features.home.impl.components_RoomSummaryRow_Night_31_en",20553,], +["features.home.impl.components_RoomSummaryRow_Day_32_en","features.home.impl.components_RoomSummaryRow_Night_32_en",20553,], +["features.home.impl.components_RoomSummaryRow_Day_33_en","features.home.impl.components_RoomSummaryRow_Night_33_en",20553,], +["features.home.impl.components_RoomSummaryRow_Day_34_en","features.home.impl.components_RoomSummaryRow_Night_34_en",20553,], +["features.home.impl.components_RoomSummaryRow_Day_35_en","features.home.impl.components_RoomSummaryRow_Night_35_en",20553,], ["features.home.impl.components_RoomSummaryRow_Day_36_en","features.home.impl.components_RoomSummaryRow_Night_36_en",0,], -["features.home.impl.components_RoomSummaryRow_Day_37_en","features.home.impl.components_RoomSummaryRow_Night_37_en",20539,], +["features.home.impl.components_RoomSummaryRow_Day_37_en","features.home.impl.components_RoomSummaryRow_Night_37_en",20553,], ["features.home.impl.components_RoomSummaryRow_Day_3_en","features.home.impl.components_RoomSummaryRow_Night_3_en",0,], ["features.home.impl.components_RoomSummaryRow_Day_4_en","features.home.impl.components_RoomSummaryRow_Night_4_en",0,], ["features.home.impl.components_RoomSummaryRow_Day_5_en","features.home.impl.components_RoomSummaryRow_Night_5_en",0,], @@ -1104,118 +1104,118 @@ export const screenshots = [ ["features.home.impl.components_RoomSummaryRow_Day_7_en","features.home.impl.components_RoomSummaryRow_Night_7_en",0,], ["features.home.impl.components_RoomSummaryRow_Day_8_en","features.home.impl.components_RoomSummaryRow_Night_8_en",0,], ["features.home.impl.components_RoomSummaryRow_Day_9_en","features.home.impl.components_RoomSummaryRow_Night_9_en",0,], -["appnav.root_RootView_Day_0_en","appnav.root_RootView_Night_0_en",20539,], -["appnav.root_RootView_Day_1_en","appnav.root_RootView_Night_1_en",20539,], -["appnav.root_RootView_Day_2_en","appnav.root_RootView_Night_2_en",20539,], -["appicon.enterprise_RoundIcon_en","",0,], +["appnav.root_RootView_Day_0_en","appnav.root_RootView_Night_0_en",20553,], +["appnav.root_RootView_Day_1_en","appnav.root_RootView_Night_1_en",20553,], +["appnav.root_RootView_Day_2_en","appnav.root_RootView_Night_2_en",20553,], ["appicon.element_RoundIcon_en","",0,], +["appicon.enterprise_RoundIcon_en","",0,], ["libraries.designsystem.atomic.atoms_RoundedIconAtom_Day_0_en","libraries.designsystem.atomic.atoms_RoundedIconAtom_Night_0_en",0,], -["features.verifysession.impl.emoji_SasEmojis_Day_0_en","features.verifysession.impl.emoji_SasEmojis_Night_0_en",20539,], -["libraries.designsystem.components.dialogs_SaveChangesDialog_Day_0_en","libraries.designsystem.components.dialogs_SaveChangesDialog_Night_0_en",20539,], -["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_0_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_0_en",20539,], -["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_1_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_1_en",20539,], -["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_2_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_2_en",20539,], -["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_3_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_3_en",20539,], -["features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_0_en","features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_0_en",20539,], -["features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_en","features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_1_en",20539,], +["features.verifysession.impl.emoji_SasEmojis_Day_0_en","features.verifysession.impl.emoji_SasEmojis_Night_0_en",20553,], +["libraries.designsystem.components.dialogs_SaveChangesDialog_Day_0_en","libraries.designsystem.components.dialogs_SaveChangesDialog_Night_0_en",20553,], +["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_0_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_0_en",20553,], +["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_1_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_1_en",20553,], +["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_2_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_2_en",20553,], +["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_3_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_3_en",20553,], +["features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_0_en","features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_0_en",20553,], +["features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_en","features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_1_en",20553,], ["libraries.designsystem.theme.components_SearchBarActiveNoneQuery_Search_views_en","",0,], ["libraries.designsystem.theme.components_SearchBarActiveWithContent_Search_views_en","",0,], -["libraries.designsystem.theme.components_SearchBarActiveWithNoResults_Search_views_en","",20539,], +["libraries.designsystem.theme.components_SearchBarActiveWithNoResults_Search_views_en","",20553,], ["libraries.designsystem.theme.components_SearchBarActiveWithQueryNoBackButton_Search_views_en","",0,], ["libraries.designsystem.theme.components_SearchBarActiveWithQuery_Search_views_en","",0,], ["libraries.designsystem.theme.components_SearchBarInactive_Search_views_en","",0,], ["libraries.designsystem.theme.components_SearchFieldsDark_Search_views_en","",0,], ["libraries.designsystem.theme.components_SearchFieldsLight_Search_views_en","",0,], -["features.startchat.impl.components_SearchMultipleUsersResultItem_en","",20539,], -["features.startchat.impl.components_SearchSingleUserResultItem_en","",20539,], -["features.securebackup.impl.disable_SecureBackupDisableView_Day_0_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_0_en",20539,], -["features.securebackup.impl.disable_SecureBackupDisableView_Day_1_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_1_en",20539,], -["features.securebackup.impl.disable_SecureBackupDisableView_Day_2_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_2_en",20539,], -["features.securebackup.impl.disable_SecureBackupDisableView_Day_3_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_3_en",20539,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_0_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_0_en",20539,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_1_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_1_en",20539,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_2_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_2_en",20539,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_3_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_3_en",20539,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_4_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_4_en",20539,], -["features.securebackup.impl.root_SecureBackupRootView_Day_0_en","features.securebackup.impl.root_SecureBackupRootView_Night_0_en",20539,], -["features.securebackup.impl.root_SecureBackupRootView_Day_10_en","features.securebackup.impl.root_SecureBackupRootView_Night_10_en",20539,], -["features.securebackup.impl.root_SecureBackupRootView_Day_11_en","features.securebackup.impl.root_SecureBackupRootView_Night_11_en",20539,], -["features.securebackup.impl.root_SecureBackupRootView_Day_12_en","features.securebackup.impl.root_SecureBackupRootView_Night_12_en",20539,], -["features.securebackup.impl.root_SecureBackupRootView_Day_13_en","features.securebackup.impl.root_SecureBackupRootView_Night_13_en",20539,], -["features.securebackup.impl.root_SecureBackupRootView_Day_14_en","features.securebackup.impl.root_SecureBackupRootView_Night_14_en",20539,], -["features.securebackup.impl.root_SecureBackupRootView_Day_15_en","features.securebackup.impl.root_SecureBackupRootView_Night_15_en",20539,], -["features.securebackup.impl.root_SecureBackupRootView_Day_16_en","features.securebackup.impl.root_SecureBackupRootView_Night_16_en",20539,], -["features.securebackup.impl.root_SecureBackupRootView_Day_17_en","features.securebackup.impl.root_SecureBackupRootView_Night_17_en",20539,], -["features.securebackup.impl.root_SecureBackupRootView_Day_1_en","features.securebackup.impl.root_SecureBackupRootView_Night_1_en",20539,], -["features.securebackup.impl.root_SecureBackupRootView_Day_2_en","features.securebackup.impl.root_SecureBackupRootView_Night_2_en",20539,], -["features.securebackup.impl.root_SecureBackupRootView_Day_3_en","features.securebackup.impl.root_SecureBackupRootView_Night_3_en",20539,], -["features.securebackup.impl.root_SecureBackupRootView_Day_4_en","features.securebackup.impl.root_SecureBackupRootView_Night_4_en",20539,], -["features.securebackup.impl.root_SecureBackupRootView_Day_5_en","features.securebackup.impl.root_SecureBackupRootView_Night_5_en",20539,], -["features.securebackup.impl.root_SecureBackupRootView_Day_6_en","features.securebackup.impl.root_SecureBackupRootView_Night_6_en",20539,], -["features.securebackup.impl.root_SecureBackupRootView_Day_7_en","features.securebackup.impl.root_SecureBackupRootView_Night_7_en",20539,], -["features.securebackup.impl.root_SecureBackupRootView_Day_8_en","features.securebackup.impl.root_SecureBackupRootView_Night_8_en",20539,], -["features.securebackup.impl.root_SecureBackupRootView_Day_9_en","features.securebackup.impl.root_SecureBackupRootView_Night_9_en",20539,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_0_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_0_en",20539,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_1_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_1_en",20539,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_2_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_2_en",20539,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_3_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_3_en",20539,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_4_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_4_en",20539,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_5_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_5_en",20539,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_0_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_0_en",20539,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_1_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_1_en",20539,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_2_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_2_en",20539,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_3_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_3_en",20539,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_4_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_4_en",20539,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_5_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_5_en",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_0_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_10_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_11_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_12_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_13_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_14_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_15_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_16_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_17_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_18_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_19_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_1_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_20_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_21_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_22_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_23_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_2_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_3_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_4_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_5_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_6_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_7_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_8_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_9_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_0_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_10_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_11_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_12_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_13_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_14_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_15_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_16_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_17_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_18_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_19_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_1_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_20_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_21_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_22_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_23_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_2_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_3_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_4_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_5_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_6_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_7_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_8_en","",20539,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_9_en","",20539,], -["features.createroom.impl.configureroom_SelectParentSpaceBottomSheet_Day_0_en","features.createroom.impl.configureroom_SelectParentSpaceBottomSheet_Night_0_en",20539,], +["features.startchat.impl.components_SearchMultipleUsersResultItem_en","",20553,], +["features.startchat.impl.components_SearchSingleUserResultItem_en","",20553,], +["features.securebackup.impl.disable_SecureBackupDisableView_Day_0_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_0_en",20553,], +["features.securebackup.impl.disable_SecureBackupDisableView_Day_1_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_1_en",20553,], +["features.securebackup.impl.disable_SecureBackupDisableView_Day_2_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_2_en",20553,], +["features.securebackup.impl.disable_SecureBackupDisableView_Day_3_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_3_en",20553,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_0_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_0_en",20553,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_1_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_1_en",20553,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_2_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_2_en",20553,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_3_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_3_en",20553,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_4_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_4_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_0_en","features.securebackup.impl.root_SecureBackupRootView_Night_0_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_10_en","features.securebackup.impl.root_SecureBackupRootView_Night_10_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_11_en","features.securebackup.impl.root_SecureBackupRootView_Night_11_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_12_en","features.securebackup.impl.root_SecureBackupRootView_Night_12_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_13_en","features.securebackup.impl.root_SecureBackupRootView_Night_13_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_14_en","features.securebackup.impl.root_SecureBackupRootView_Night_14_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_15_en","features.securebackup.impl.root_SecureBackupRootView_Night_15_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_16_en","features.securebackup.impl.root_SecureBackupRootView_Night_16_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_17_en","features.securebackup.impl.root_SecureBackupRootView_Night_17_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_1_en","features.securebackup.impl.root_SecureBackupRootView_Night_1_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_2_en","features.securebackup.impl.root_SecureBackupRootView_Night_2_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_3_en","features.securebackup.impl.root_SecureBackupRootView_Night_3_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_4_en","features.securebackup.impl.root_SecureBackupRootView_Night_4_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_5_en","features.securebackup.impl.root_SecureBackupRootView_Night_5_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_6_en","features.securebackup.impl.root_SecureBackupRootView_Night_6_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_7_en","features.securebackup.impl.root_SecureBackupRootView_Night_7_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_8_en","features.securebackup.impl.root_SecureBackupRootView_Night_8_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_9_en","features.securebackup.impl.root_SecureBackupRootView_Night_9_en",20553,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_0_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_0_en",20553,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_1_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_1_en",20553,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_2_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_2_en",20553,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_3_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_3_en",20553,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_4_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_4_en",20553,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_5_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_5_en",20553,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_0_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_0_en",20553,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_1_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_1_en",20553,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_2_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_2_en",20553,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_3_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_3_en",20553,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_4_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_4_en",20553,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_5_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_5_en",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_0_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_10_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_11_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_12_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_13_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_14_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_15_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_16_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_17_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_18_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_19_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_1_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_20_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_21_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_22_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_23_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_2_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_3_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_4_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_5_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_6_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_7_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_8_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_9_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_0_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_10_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_11_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_12_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_13_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_14_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_15_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_16_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_17_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_18_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_19_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_1_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_20_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_21_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_22_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_23_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_2_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_3_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_4_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_5_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_6_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_7_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_8_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_9_en","",20553,], +["features.createroom.impl.configureroom_SelectParentSpaceBottomSheet_Day_0_en","features.createroom.impl.configureroom_SelectParentSpaceBottomSheet_Night_0_en",20553,], ["libraries.designsystem.atomic.atoms_SelectedIndicatorAtom_Day_0_en","libraries.designsystem.atomic.atoms_SelectedIndicatorAtom_Night_0_en",0,], ["libraries.matrix.ui.components_SelectedRoomRtl_Day_0_en","libraries.matrix.ui.components_SelectedRoomRtl_Night_0_en",0,], ["libraries.matrix.ui.components_SelectedRoomRtl_Day_1_en","libraries.matrix.ui.components_SelectedRoomRtl_Night_1_en",0,], @@ -1238,33 +1238,33 @@ export const screenshots = [ ["libraries.matrix.ui.messages.sender_SenderName_Day_6_en","libraries.matrix.ui.messages.sender_SenderName_Night_6_en",0,], ["libraries.matrix.ui.messages.sender_SenderName_Day_7_en","libraries.matrix.ui.messages.sender_SenderName_Night_7_en",0,], ["libraries.matrix.ui.messages.sender_SenderName_Day_8_en","libraries.matrix.ui.messages.sender_SenderName_Night_8_en",0,], -["features.verifysession.impl.incoming.ui_SessionDetailsView_Day_0_en","features.verifysession.impl.incoming.ui_SessionDetailsView_Night_0_en",20539,], -["features.home.impl.components_SetUpRecoveryKeyBanner_Day_0_en","features.home.impl.components_SetUpRecoveryKeyBanner_Night_0_en",20539,], -["features.lockscreen.impl.setup.biometric_SetupBiometricView_Day_0_en","features.lockscreen.impl.setup.biometric_SetupBiometricView_Night_0_en",20539,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_0_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_0_en",20539,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_1_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_1_en",20539,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_2_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_2_en",20539,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_3_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_3_en",20539,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_4_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_4_en",20539,], -["features.location.impl.share_ShareLocationView_Day_0_en","features.location.impl.share_ShareLocationView_Night_0_en",20542,], -["features.location.impl.share_ShareLocationView_Day_1_en","features.location.impl.share_ShareLocationView_Night_1_en",20542,], -["features.location.impl.share_ShareLocationView_Day_2_en","features.location.impl.share_ShareLocationView_Night_2_en",20542,], -["features.location.impl.share_ShareLocationView_Day_3_en","features.location.impl.share_ShareLocationView_Night_3_en",20542,], -["features.location.impl.share_ShareLocationView_Day_4_en","features.location.impl.share_ShareLocationView_Night_4_en",20542,], -["features.location.impl.share_ShareLocationView_Day_5_en","features.location.impl.share_ShareLocationView_Night_5_en",20542,], -["features.location.impl.share_ShareLocationView_Day_6_en","features.location.impl.share_ShareLocationView_Night_6_en",20542,], +["features.verifysession.impl.incoming.ui_SessionDetailsView_Day_0_en","features.verifysession.impl.incoming.ui_SessionDetailsView_Night_0_en",20553,], +["features.home.impl.components_SetUpRecoveryKeyBanner_Day_0_en","features.home.impl.components_SetUpRecoveryKeyBanner_Night_0_en",20553,], +["features.lockscreen.impl.setup.biometric_SetupBiometricView_Day_0_en","features.lockscreen.impl.setup.biometric_SetupBiometricView_Night_0_en",20553,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_0_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_0_en",20553,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_1_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_1_en",20553,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_2_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_2_en",20553,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_3_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_3_en",20553,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_4_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_4_en",20553,], +["features.location.impl.share_ShareLocationView_Day_0_en","features.location.impl.share_ShareLocationView_Night_0_en",20553,], +["features.location.impl.share_ShareLocationView_Day_1_en","features.location.impl.share_ShareLocationView_Night_1_en",20553,], +["features.location.impl.share_ShareLocationView_Day_2_en","features.location.impl.share_ShareLocationView_Night_2_en",20553,], +["features.location.impl.share_ShareLocationView_Day_3_en","features.location.impl.share_ShareLocationView_Night_3_en",20553,], +["features.location.impl.share_ShareLocationView_Day_4_en","features.location.impl.share_ShareLocationView_Night_4_en",20553,], +["features.location.impl.share_ShareLocationView_Day_5_en","features.location.impl.share_ShareLocationView_Night_5_en",20553,], +["features.location.impl.share_ShareLocationView_Day_6_en","features.location.impl.share_ShareLocationView_Night_6_en",20553,], ["features.share.impl_ShareView_Day_0_en","features.share.impl_ShareView_Night_0_en",0,], ["features.share.impl_ShareView_Day_1_en","features.share.impl_ShareView_Night_1_en",0,], ["features.share.impl_ShareView_Day_2_en","features.share.impl_ShareView_Night_2_en",0,], -["features.share.impl_ShareView_Day_3_en","features.share.impl_ShareView_Night_3_en",20539,], -["features.location.impl.show_ShowLocationView_Day_0_en","features.location.impl.show_ShowLocationView_Night_0_en",20539,], -["features.location.impl.show_ShowLocationView_Day_1_en","features.location.impl.show_ShowLocationView_Night_1_en",20539,], -["features.location.impl.show_ShowLocationView_Day_2_en","features.location.impl.show_ShowLocationView_Night_2_en",20539,], -["features.location.impl.show_ShowLocationView_Day_3_en","features.location.impl.show_ShowLocationView_Night_3_en",20539,], -["features.location.impl.show_ShowLocationView_Day_4_en","features.location.impl.show_ShowLocationView_Night_4_en",20539,], -["features.location.impl.show_ShowLocationView_Day_5_en","features.location.impl.show_ShowLocationView_Night_5_en",20539,], -["features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_0_en","features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Night_0_en",20539,], -["features.signedout.impl_SignedOutView_Day_0_en","features.signedout.impl_SignedOutView_Night_0_en",20539,], +["features.share.impl_ShareView_Day_3_en","features.share.impl_ShareView_Night_3_en",20553,], +["features.location.impl.show_ShowLocationView_Day_0_en","features.location.impl.show_ShowLocationView_Night_0_en",20553,], +["features.location.impl.show_ShowLocationView_Day_1_en","features.location.impl.show_ShowLocationView_Night_1_en",20553,], +["features.location.impl.show_ShowLocationView_Day_2_en","features.location.impl.show_ShowLocationView_Night_2_en",20553,], +["features.location.impl.show_ShowLocationView_Day_3_en","features.location.impl.show_ShowLocationView_Night_3_en",20553,], +["features.location.impl.show_ShowLocationView_Day_4_en","features.location.impl.show_ShowLocationView_Night_4_en",20553,], +["features.location.impl.show_ShowLocationView_Day_5_en","features.location.impl.show_ShowLocationView_Night_5_en",20553,], +["features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_0_en","features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Night_0_en",20553,], +["features.signedout.impl_SignedOutView_Day_0_en","features.signedout.impl_SignedOutView_Night_0_en",20553,], ["libraries.designsystem.components_SimpleModalBottomSheet_Day_0_en","libraries.designsystem.components_SimpleModalBottomSheet_Night_0_en",0,], ["libraries.designsystem.components.dialogs_SingleSelectionDialogContent_Dialogs_en","",0,], ["libraries.designsystem.components.dialogs_SingleSelectionDialog_Day_0_en","libraries.designsystem.components.dialogs_SingleSelectionDialog_Night_0_en",0,], @@ -1274,107 +1274,107 @@ export const screenshots = [ ["libraries.designsystem.components.list_SingleSelectionListItemUnselectedWithSupportingText_Single_selection_List_item_-_no_selection,_supporting_text_List_items_en","",0,], ["libraries.designsystem.components.list_SingleSelectionListItem_Single_selection_List_item_-_no_selection_List_items_en","",0,], ["libraries.designsystem.theme.components_Sliders_Sliders_en","",0,], -["features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Day_0_en","features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Night_0_en",20539,], +["features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Day_0_en","features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Night_0_en",20553,], ["libraries.designsystem.theme.components_SnackbarWithActionAndCloseButton_Snackbar_with_action_and_close_button_Snackbars_en","",0,], ["libraries.designsystem.theme.components_SnackbarWithActionOnNewLineAndCloseButton_Snackbar_with_action_and_close_button_on_new_line_Snackbars_en","",0,], ["libraries.designsystem.theme.components_SnackbarWithActionOnNewLine_Snackbar_with_action_on_new_line_Snackbars_en","",0,], ["libraries.designsystem.theme.components_SnackbarWithAction_Snackbar_with_action_Snackbars_en","",0,], ["libraries.designsystem.theme.components_Snackbar_Snackbar_Snackbars_en","",0,], -["features.announcement.impl.spaces_SpaceAnnouncementView_Day_0_en","features.announcement.impl.spaces_SpaceAnnouncementView_Night_0_en",20539,], +["features.announcement.impl.spaces_SpaceAnnouncementView_Day_0_en","features.announcement.impl.spaces_SpaceAnnouncementView_Night_0_en",20553,], ["libraries.designsystem.components.avatar.internal_SpaceAvatar_Avatars_en","",0,], -["features.home.impl.spacefilters_SpaceFiltersView_Day_0_en","features.home.impl.spacefilters_SpaceFiltersView_Night_0_en",20539,], -["features.home.impl.spacefilters_SpaceFiltersView_Day_1_en","features.home.impl.spacefilters_SpaceFiltersView_Night_1_en",20539,], -["libraries.matrix.ui.components_SpaceHeaderRootView_Day_0_en","libraries.matrix.ui.components_SpaceHeaderRootView_Night_0_en",20539,], +["features.home.impl.spacefilters_SpaceFiltersView_Day_0_en","features.home.impl.spacefilters_SpaceFiltersView_Night_0_en",20553,], +["features.home.impl.spacefilters_SpaceFiltersView_Day_1_en","features.home.impl.spacefilters_SpaceFiltersView_Night_1_en",20553,], +["libraries.matrix.ui.components_SpaceHeaderRootView_Day_0_en","libraries.matrix.ui.components_SpaceHeaderRootView_Night_0_en",20553,], ["libraries.matrix.ui.components_SpaceHeaderView_Day_0_en","libraries.matrix.ui.components_SpaceHeaderView_Night_0_en",0,], -["libraries.matrix.ui.components_SpaceInfoRow_Day_0_en","libraries.matrix.ui.components_SpaceInfoRow_Night_0_en",20539,], +["libraries.matrix.ui.components_SpaceInfoRow_Day_0_en","libraries.matrix.ui.components_SpaceInfoRow_Night_0_en",20553,], ["libraries.matrix.ui.components_SpaceMembersViewNoHeroes_Day_0_en","libraries.matrix.ui.components_SpaceMembersViewNoHeroes_Night_0_en",0,], ["libraries.matrix.ui.components_SpaceMembersView_Day_0_en","libraries.matrix.ui.components_SpaceMembersView_Night_0_en",0,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_0_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_0_en",20539,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_1_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_1_en",20539,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_2_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_2_en",20539,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_3_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_3_en",20539,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_4_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_4_en",20539,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_5_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_5_en",20539,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_6_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_6_en",20539,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_7_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_7_en",20539,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_8_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_8_en",20539,], -["features.space.impl.settings_SpaceSettingsView_Day_0_en","features.space.impl.settings_SpaceSettingsView_Night_0_en",20539,], -["features.space.impl.settings_SpaceSettingsView_Day_1_en","features.space.impl.settings_SpaceSettingsView_Night_1_en",20539,], -["features.space.impl.settings_SpaceSettingsView_Day_2_en","features.space.impl.settings_SpaceSettingsView_Night_2_en",20539,], -["features.space.impl.settings_SpaceSettingsView_Day_3_en","features.space.impl.settings_SpaceSettingsView_Night_3_en",20539,], -["features.space.impl.root_SpaceView_Day_0_en","features.space.impl.root_SpaceView_Night_0_en",20539,], -["features.space.impl.root_SpaceView_Day_1_en","features.space.impl.root_SpaceView_Night_1_en",20539,], -["features.space.impl.root_SpaceView_Day_2_en","features.space.impl.root_SpaceView_Night_2_en",20539,], -["features.space.impl.root_SpaceView_Day_3_en","features.space.impl.root_SpaceView_Night_3_en",20539,], -["features.space.impl.root_SpaceView_Day_4_en","features.space.impl.root_SpaceView_Night_4_en",20539,], -["features.space.impl.root_SpaceView_Day_5_en","features.space.impl.root_SpaceView_Night_5_en",20539,], -["features.space.impl.root_SpaceView_Day_6_en","features.space.impl.root_SpaceView_Night_6_en",20539,], -["features.space.impl.root_SpaceView_Day_7_en","features.space.impl.root_SpaceView_Night_7_en",20539,], -["features.space.impl.root_SpaceView_Day_8_en","features.space.impl.root_SpaceView_Night_8_en",20539,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_0_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_0_en",20553,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_1_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_1_en",20553,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_2_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_2_en",20553,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_3_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_3_en",20553,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_4_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_4_en",20553,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_5_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_5_en",20553,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_6_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_6_en",20553,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_7_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_7_en",20553,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_8_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_8_en",20553,], +["features.space.impl.settings_SpaceSettingsView_Day_0_en","features.space.impl.settings_SpaceSettingsView_Night_0_en",20553,], +["features.space.impl.settings_SpaceSettingsView_Day_1_en","features.space.impl.settings_SpaceSettingsView_Night_1_en",20553,], +["features.space.impl.settings_SpaceSettingsView_Day_2_en","features.space.impl.settings_SpaceSettingsView_Night_2_en",20553,], +["features.space.impl.settings_SpaceSettingsView_Day_3_en","features.space.impl.settings_SpaceSettingsView_Night_3_en",20553,], +["features.space.impl.root_SpaceView_Day_0_en","features.space.impl.root_SpaceView_Night_0_en",20553,], +["features.space.impl.root_SpaceView_Day_1_en","features.space.impl.root_SpaceView_Night_1_en",20553,], +["features.space.impl.root_SpaceView_Day_2_en","features.space.impl.root_SpaceView_Night_2_en",20553,], +["features.space.impl.root_SpaceView_Day_3_en","features.space.impl.root_SpaceView_Night_3_en",20553,], +["features.space.impl.root_SpaceView_Day_4_en","features.space.impl.root_SpaceView_Night_4_en",20553,], +["features.space.impl.root_SpaceView_Day_5_en","features.space.impl.root_SpaceView_Night_5_en",20553,], +["features.space.impl.root_SpaceView_Day_6_en","features.space.impl.root_SpaceView_Night_6_en",20553,], +["features.space.impl.root_SpaceView_Day_7_en","features.space.impl.root_SpaceView_Night_7_en",20553,], +["features.space.impl.root_SpaceView_Day_8_en","features.space.impl.root_SpaceView_Night_8_en",20553,], ["libraries.designsystem.modifiers_SquareSizeModifierInsideSquare_en","",0,], ["libraries.designsystem.modifiers_SquareSizeModifierLargeHeight_en","",0,], ["libraries.designsystem.modifiers_SquareSizeModifierLargeWidth_en","",0,], -["features.startchat.impl.root_StartChatView_Day_0_en","features.startchat.impl.root_StartChatView_Night_0_en",20539,], -["features.startchat.impl.root_StartChatView_Day_1_en","features.startchat.impl.root_StartChatView_Night_1_en",20539,], -["features.startchat.impl.root_StartChatView_Day_2_en","features.startchat.impl.root_StartChatView_Night_2_en",20539,], -["features.startchat.impl.root_StartChatView_Day_3_en","features.startchat.impl.root_StartChatView_Night_3_en",20539,], -["features.startchat.impl.root_StartChatView_Day_4_en","features.startchat.impl.root_StartChatView_Night_4_en",20539,], -["features.startchat.impl.root_StartChatView_Day_5_en","features.startchat.impl.root_StartChatView_Night_5_en",20539,], -["features.location.api.internal_StaticMapPlaceholder_Day_0_en","features.location.api.internal_StaticMapPlaceholder_Night_0_en",20539,], +["features.startchat.impl.root_StartChatView_Day_0_en","features.startchat.impl.root_StartChatView_Night_0_en",20553,], +["features.startchat.impl.root_StartChatView_Day_1_en","features.startchat.impl.root_StartChatView_Night_1_en",20553,], +["features.startchat.impl.root_StartChatView_Day_2_en","features.startchat.impl.root_StartChatView_Night_2_en",20553,], +["features.startchat.impl.root_StartChatView_Day_3_en","features.startchat.impl.root_StartChatView_Night_3_en",20553,], +["features.startchat.impl.root_StartChatView_Day_4_en","features.startchat.impl.root_StartChatView_Night_4_en",20553,], +["features.startchat.impl.root_StartChatView_Day_5_en","features.startchat.impl.root_StartChatView_Night_5_en",20553,], +["features.location.api.internal_StaticMapPlaceholder_Day_0_en","features.location.api.internal_StaticMapPlaceholder_Night_0_en",20553,], ["features.location.api_StaticMapView_Day_0_en","features.location.api_StaticMapView_Night_0_en",0,], -["features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Day_0_en","features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Night_0_en",20539,], +["features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Day_0_en","features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Night_0_en",20553,], ["libraries.designsystem.atomic.pages_SunsetPage_Day_0_en","libraries.designsystem.atomic.pages_SunsetPage_Night_0_en",0,], ["libraries.designsystem.components.button_SuperButton_Day_0_en","libraries.designsystem.components.button_SuperButton_Night_0_en",0,], ["libraries.designsystem.theme.components_Surface_en","",0,], ["libraries.designsystem.theme.components_Switch_Toggles_en","",0,], -["appnav.loggedin_SyncStateView_Day_0_en","appnav.loggedin_SyncStateView_Night_0_en",20539,], +["appnav.loggedin_SyncStateView_Day_0_en","appnav.loggedin_SyncStateView_Night_0_en",20553,], ["libraries.designsystem.components.avatar.internal_TextAvatar_Avatars_en","",0,], ["libraries.designsystem.theme.components_TextButtonLargeLowPadding_Buttons_en","",0,], ["libraries.designsystem.theme.components_TextButtonLarge_Buttons_en","",0,], ["libraries.designsystem.theme.components_TextButtonMediumLowPadding_Buttons_en","",0,], ["libraries.designsystem.theme.components_TextButtonMedium_Buttons_en","",0,], ["libraries.designsystem.theme.components_TextButtonSmall_Buttons_en","",0,], -["libraries.textcomposer_TextComposerAddCaption_Day_0_en","libraries.textcomposer_TextComposerAddCaption_Night_0_en",20539,], -["libraries.textcomposer_TextComposerCaption_Day_0_en","libraries.textcomposer_TextComposerCaption_Night_0_en",20539,], -["libraries.textcomposer_TextComposerEditCaption_Day_0_en","libraries.textcomposer_TextComposerEditCaption_Night_0_en",20539,], -["libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerEditNotEncrypted_Night_0_en",20539,], -["libraries.textcomposer_TextComposerEdit_Day_0_en","libraries.textcomposer_TextComposerEdit_Night_0_en",20539,], -["libraries.textcomposer_TextComposerFormattingNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerFormattingNotEncrypted_Night_0_en",20539,], -["libraries.textcomposer_TextComposerFormatting_Day_0_en","libraries.textcomposer_TextComposerFormatting_Night_0_en",20539,], -["libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Day_0_en","libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Night_0_en",20539,], -["libraries.textcomposer_TextComposerLinkDialogCreateLink_Day_0_en","libraries.textcomposer_TextComposerLinkDialogCreateLink_Night_0_en",20539,], -["libraries.textcomposer_TextComposerLinkDialogEditLink_Day_0_en","libraries.textcomposer_TextComposerLinkDialogEditLink_Night_0_en",20539,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_0_en",20539,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_10_en",20539,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_11_en",20539,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_1_en",20539,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_2_en",20539,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_3_en",20539,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_4_en",20539,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_5_en",20539,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_6_en",20539,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_7_en",20539,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_8_en",20539,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_9_en",20539,], -["libraries.textcomposer_TextComposerReply_Day_0_en","libraries.textcomposer_TextComposerReply_Night_0_en",20539,], -["libraries.textcomposer_TextComposerReply_Day_10_en","libraries.textcomposer_TextComposerReply_Night_10_en",20539,], -["libraries.textcomposer_TextComposerReply_Day_11_en","libraries.textcomposer_TextComposerReply_Night_11_en",20539,], -["libraries.textcomposer_TextComposerReply_Day_1_en","libraries.textcomposer_TextComposerReply_Night_1_en",20539,], -["libraries.textcomposer_TextComposerReply_Day_2_en","libraries.textcomposer_TextComposerReply_Night_2_en",20539,], -["libraries.textcomposer_TextComposerReply_Day_3_en","libraries.textcomposer_TextComposerReply_Night_3_en",20539,], -["libraries.textcomposer_TextComposerReply_Day_4_en","libraries.textcomposer_TextComposerReply_Night_4_en",20539,], -["libraries.textcomposer_TextComposerReply_Day_5_en","libraries.textcomposer_TextComposerReply_Night_5_en",20539,], -["libraries.textcomposer_TextComposerReply_Day_6_en","libraries.textcomposer_TextComposerReply_Night_6_en",20539,], -["libraries.textcomposer_TextComposerReply_Day_7_en","libraries.textcomposer_TextComposerReply_Night_7_en",20539,], -["libraries.textcomposer_TextComposerReply_Day_8_en","libraries.textcomposer_TextComposerReply_Night_8_en",20539,], -["libraries.textcomposer_TextComposerReply_Day_9_en","libraries.textcomposer_TextComposerReply_Night_9_en",20539,], -["libraries.textcomposer_TextComposerSimpleNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerSimpleNotEncrypted_Night_0_en",20539,], -["libraries.textcomposer_TextComposerSimple_Day_0_en","libraries.textcomposer_TextComposerSimple_Night_0_en",20539,], -["libraries.textcomposer_TextComposerVoiceNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerVoiceNotEncrypted_Night_0_en",20539,], +["libraries.textcomposer_TextComposerAddCaption_Day_0_en","libraries.textcomposer_TextComposerAddCaption_Night_0_en",20553,], +["libraries.textcomposer_TextComposerCaption_Day_0_en","libraries.textcomposer_TextComposerCaption_Night_0_en",20553,], +["libraries.textcomposer_TextComposerEditCaption_Day_0_en","libraries.textcomposer_TextComposerEditCaption_Night_0_en",20553,], +["libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerEditNotEncrypted_Night_0_en",20553,], +["libraries.textcomposer_TextComposerEdit_Day_0_en","libraries.textcomposer_TextComposerEdit_Night_0_en",20553,], +["libraries.textcomposer_TextComposerFormattingNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerFormattingNotEncrypted_Night_0_en",20553,], +["libraries.textcomposer_TextComposerFormatting_Day_0_en","libraries.textcomposer_TextComposerFormatting_Night_0_en",20553,], +["libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Day_0_en","libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Night_0_en",20553,], +["libraries.textcomposer_TextComposerLinkDialogCreateLink_Day_0_en","libraries.textcomposer_TextComposerLinkDialogCreateLink_Night_0_en",20553,], +["libraries.textcomposer_TextComposerLinkDialogEditLink_Day_0_en","libraries.textcomposer_TextComposerLinkDialogEditLink_Night_0_en",20553,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_0_en",20553,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_10_en",20553,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_11_en",20553,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_1_en",20553,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_2_en",20553,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_3_en",20553,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_4_en",20553,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_5_en",20553,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_6_en",20553,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_7_en",20553,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_8_en",20553,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_9_en",20553,], +["libraries.textcomposer_TextComposerReply_Day_0_en","libraries.textcomposer_TextComposerReply_Night_0_en",20553,], +["libraries.textcomposer_TextComposerReply_Day_10_en","libraries.textcomposer_TextComposerReply_Night_10_en",20553,], +["libraries.textcomposer_TextComposerReply_Day_11_en","libraries.textcomposer_TextComposerReply_Night_11_en",20553,], +["libraries.textcomposer_TextComposerReply_Day_1_en","libraries.textcomposer_TextComposerReply_Night_1_en",20553,], +["libraries.textcomposer_TextComposerReply_Day_2_en","libraries.textcomposer_TextComposerReply_Night_2_en",20553,], +["libraries.textcomposer_TextComposerReply_Day_3_en","libraries.textcomposer_TextComposerReply_Night_3_en",20553,], +["libraries.textcomposer_TextComposerReply_Day_4_en","libraries.textcomposer_TextComposerReply_Night_4_en",20553,], +["libraries.textcomposer_TextComposerReply_Day_5_en","libraries.textcomposer_TextComposerReply_Night_5_en",20553,], +["libraries.textcomposer_TextComposerReply_Day_6_en","libraries.textcomposer_TextComposerReply_Night_6_en",20553,], +["libraries.textcomposer_TextComposerReply_Day_7_en","libraries.textcomposer_TextComposerReply_Night_7_en",20553,], +["libraries.textcomposer_TextComposerReply_Day_8_en","libraries.textcomposer_TextComposerReply_Night_8_en",20553,], +["libraries.textcomposer_TextComposerReply_Day_9_en","libraries.textcomposer_TextComposerReply_Night_9_en",20553,], +["libraries.textcomposer_TextComposerSimpleNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerSimpleNotEncrypted_Night_0_en",20553,], +["libraries.textcomposer_TextComposerSimple_Day_0_en","libraries.textcomposer_TextComposerSimple_Night_0_en",20553,], +["libraries.textcomposer_TextComposerVoiceNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerVoiceNotEncrypted_Night_0_en",20553,], ["libraries.textcomposer_TextComposerVoice_Day_0_en","libraries.textcomposer_TextComposerVoice_Night_0_en",0,], ["libraries.designsystem.theme.components_TextDark_Text_en","",0,], -["libraries.designsystem.components.dialogs_TextFieldDialogWithError_Day_0_en","libraries.designsystem.components.dialogs_TextFieldDialogWithError_Night_0_en",20539,], -["libraries.designsystem.components.dialogs_TextFieldDialog_Day_0_en","libraries.designsystem.components.dialogs_TextFieldDialog_Night_0_en",20539,], +["libraries.designsystem.components.dialogs_TextFieldDialogWithError_Day_0_en","libraries.designsystem.components.dialogs_TextFieldDialogWithError_Night_0_en",20553,], +["libraries.designsystem.components.dialogs_TextFieldDialog_Day_0_en","libraries.designsystem.components.dialogs_TextFieldDialog_Night_0_en",20553,], ["libraries.designsystem.components.list_TextFieldListItemEmpty_Text_field_List_item_-_empty_List_items_en","",0,], ["libraries.designsystem.components.list_TextFieldListItemTextFieldValue_Text_field_List_item_-_textfieldvalue_List_items_en","",0,], ["libraries.designsystem.components.list_TextFieldListItem_Text_field_List_item_-_text_List_items_en","",0,], @@ -1386,16 +1386,16 @@ export const screenshots = [ ["libraries.mediaviewer.impl.local.txt_TextFileContentView_Day_3_en","libraries.mediaviewer.impl.local.txt_TextFileContentView_Night_3_en",0,], ["libraries.textcomposer.components_TextFormatting_Day_0_en","libraries.textcomposer.components_TextFormatting_Night_0_en",0,], ["libraries.designsystem.theme.components_TextLight_Text_en","",0,], -["features.messages.impl.timeline.components_ThreadSummaryView_Day_0_en","features.messages.impl.timeline.components_ThreadSummaryView_Night_0_en",20539,], -["features.messages.impl.topbars_ThreadTopBar_Day_0_en","features.messages.impl.topbars_ThreadTopBar_Night_0_en",20539,], -["libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime_pickers_en","",20539,], -["libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime_pickers_en","",20539,], -["libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime_pickers_en","",20539,], +["features.messages.impl.timeline.components_ThreadSummaryView_Day_0_en","features.messages.impl.timeline.components_ThreadSummaryView_Night_0_en",20553,], +["features.messages.impl.topbars_ThreadTopBar_Day_0_en","features.messages.impl.topbars_ThreadTopBar_Night_0_en",20553,], +["libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime_pickers_en","",20553,], +["libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime_pickers_en","",20553,], +["libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime_pickers_en","",20553,], ["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_0_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_0_en",0,], ["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_1_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_1_en",0,], ["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_2_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_2_en",0,], -["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_3_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_3_en",20539,], -["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_4_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_4_en",20539,], +["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_3_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_3_en",20553,], +["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_4_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_4_en",20553,], ["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_5_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_5_en",0,], ["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_6_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_6_en",0,], ["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_7_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_7_en",0,], @@ -1405,18 +1405,18 @@ export const screenshots = [ ["features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_2_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_3_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_4_en",0,], -["features.messages.impl.timeline.components_TimelineItemCallNotifyView_Day_0_en","features.messages.impl.timeline.components_TimelineItemCallNotifyView_Night_0_en",20539,], +["features.messages.impl.timeline.components_TimelineItemCallNotifyView_Day_0_en","features.messages.impl.timeline.components_TimelineItemCallNotifyView_Night_0_en",20553,], ["features.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_Night_0_en",0,], ["features.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_Day_1_en","features.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_Night_1_en",0,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_0_en",20539,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_1_en",20539,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_2_en",20539,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_3_en",20539,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_4_en",20539,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_5_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_5_en",20539,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_6_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_6_en",20539,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_7_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_7_en",20539,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_8_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_8_en",20539,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_0_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_1_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_2_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_3_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_4_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_5_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_5_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_6_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_6_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_7_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_7_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_8_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_8_en",20553,], ["features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Night_0_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowForDirectRoom_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowForDirectRoom_Night_0_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowLongSenderName_en","",0,], @@ -1424,18 +1424,18 @@ export const screenshots = [ ["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_0_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_1_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_2_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_2_en",0,], -["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_3_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_3_en",20539,], -["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_4_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_4_en",20539,], +["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_3_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_3_en",20553,], +["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_4_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_4_en",20553,], ["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_5_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_5_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_6_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_6_en",0,], -["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_7_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_7_en",20539,], -["features.messages.impl.timeline.components_TimelineItemEventRowUtd_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowUtd_Night_0_en",20539,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Night_0_en",20539,], +["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_7_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_7_en",20553,], +["features.messages.impl.timeline.components_TimelineItemEventRowUtd_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowUtd_Night_0_en",20553,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Night_0_en",20553,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Night_0_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Night_1_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Day_2_en","features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Night_2_en",0,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en",20539,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en",20539,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en",20553,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en",20553,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_1_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en",0,], @@ -1444,42 +1444,42 @@ export const screenshots = [ ["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_1_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_2_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_3_en",0,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en",20539,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en",20553,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_5_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_5_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_6_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_7_en",0,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en",20539,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en",20553,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_9_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_9_en",0,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithThreadSummary_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithThreadSummary_Night_0_en",20539,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithThreadSummary_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithThreadSummary_Night_0_en",20553,], ["features.messages.impl.timeline.components_TimelineItemEventRow_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRow_Night_0_en",0,], -["features.messages.impl.timeline.components_TimelineItemEventTimestampBelow_en","",20539,], +["features.messages.impl.timeline.components_TimelineItemEventTimestampBelow_en","",20553,], ["features.messages.impl.timeline.components.event_TimelineItemFileView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemFileView_Night_0_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemFileView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemFileView_Night_1_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemFileView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemFileView_Night_2_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemFileView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemFileView_Night_3_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemFileView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemFileView_Night_4_en",0,], -["features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Day_0_en","features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Night_0_en",20539,], -["features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Day_0_en","features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Night_0_en",20539,], -["features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Night_0_en",20539,], +["features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Day_0_en","features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Night_0_en",20553,], +["features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Day_0_en","features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Night_0_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Night_0_en",20553,], ["features.messages.impl.timeline.components.event_TimelineItemImageView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemImageView_Night_0_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemImageView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemImageView_Night_1_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemImageView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemImageView_Night_2_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemImageView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemImageView_Night_3_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemInformativeView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemInformativeView_Night_0_en",0,], -["features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Night_0_en",20539,], +["features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Night_0_en",20553,], ["features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_0_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_1_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_2_en",0,], -["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_0_en",20539,], -["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_1_en",20539,], -["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_2_en",20539,], -["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_3_en",20539,], -["features.messages.impl.timeline.components_TimelineItemReactionsLayout_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsLayout_Night_0_en",20539,], +["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_0_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_1_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_2_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_3_en",20553,], +["features.messages.impl.timeline.components_TimelineItemReactionsLayout_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsLayout_Night_0_en",20553,], ["features.messages.impl.timeline.components_TimelineItemReactionsViewFew_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewFew_Night_0_en",0,], -["features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Night_0_en",20539,], -["features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Night_0_en",20539,], +["features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Night_0_en",20553,], +["features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Night_0_en",20553,], ["features.messages.impl.timeline.components_TimelineItemReactionsView_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsView_Night_0_en",0,], -["features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Night_0_en",20539,], +["features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Night_0_en",20553,], ["features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_0_en","features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_0_en",0,], ["features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_1_en","features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_1_en",0,], ["features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_2_en","features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_2_en",0,], @@ -1488,8 +1488,8 @@ export const screenshots = [ ["features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_5_en","features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_5_en",0,], ["features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_6_en","features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_6_en",0,], ["features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_7_en","features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_7_en",0,], -["features.messages.impl.timeline.components.event_TimelineItemRedactedView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemRedactedView_Night_0_en",20539,], -["features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Night_0_en",20539,], +["features.messages.impl.timeline.components.event_TimelineItemRedactedView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemRedactedView_Night_0_en",20553,], +["features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Night_0_en",20553,], ["features.messages.impl.timeline.components_TimelineItemStateEventRow_Day_0_en","features.messages.impl.timeline.components_TimelineItemStateEventRow_Night_0_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemStateView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemStateView_Night_0_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_0_en",0,], @@ -1504,8 +1504,8 @@ export const screenshots = [ ["features.messages.impl.timeline.components.event_TimelineItemTextView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemTextView_Night_3_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemTextView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemTextView_Night_4_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemTextView_Day_5_en","features.messages.impl.timeline.components.event_TimelineItemTextView_Night_5_en",0,], -["features.messages.impl.timeline.components.event_TimelineItemUnknownView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemUnknownView_Night_0_en",20539,], -["features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Night_0_en",20539,], +["features.messages.impl.timeline.components.event_TimelineItemUnknownView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemUnknownView_Night_0_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Night_0_en",20553,], ["features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_0_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_1_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_2_en",0,], @@ -1528,85 +1528,85 @@ export const screenshots = [ ["features.messages.impl.timeline.components.event_TimelineItemVoiceView_Day_9_en","features.messages.impl.timeline.components.event_TimelineItemVoiceView_Night_9_en",0,], ["features.messages.impl.timeline.components.virtual_TimelineLoadingMoreIndicator_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineLoadingMoreIndicator_Night_0_en",0,], ["features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Day_0_en","features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Night_0_en",0,], -["features.messages.impl.timeline_TimelineViewMessageShield_Day_0_en","features.messages.impl.timeline_TimelineViewMessageShield_Night_0_en",20539,], -["features.messages.impl.timeline_TimelineView_Day_0_en","features.messages.impl.timeline_TimelineView_Night_0_en",20539,], -["features.messages.impl.timeline_TimelineView_Day_10_en","features.messages.impl.timeline_TimelineView_Night_10_en",20542,], -["features.messages.impl.timeline_TimelineView_Day_11_en","features.messages.impl.timeline_TimelineView_Night_11_en",20539,], -["features.messages.impl.timeline_TimelineView_Day_12_en","features.messages.impl.timeline_TimelineView_Night_12_en",20539,], -["features.messages.impl.timeline_TimelineView_Day_13_en","features.messages.impl.timeline_TimelineView_Night_13_en",20539,], -["features.messages.impl.timeline_TimelineView_Day_14_en","features.messages.impl.timeline_TimelineView_Night_14_en",20539,], -["features.messages.impl.timeline_TimelineView_Day_15_en","features.messages.impl.timeline_TimelineView_Night_15_en",20539,], -["features.messages.impl.timeline_TimelineView_Day_16_en","features.messages.impl.timeline_TimelineView_Night_16_en",20539,], +["features.messages.impl.timeline_TimelineViewMessageShield_Day_0_en","features.messages.impl.timeline_TimelineViewMessageShield_Night_0_en",20553,], +["features.messages.impl.timeline_TimelineView_Day_0_en","features.messages.impl.timeline_TimelineView_Night_0_en",20553,], +["features.messages.impl.timeline_TimelineView_Day_10_en","features.messages.impl.timeline_TimelineView_Night_10_en",20553,], +["features.messages.impl.timeline_TimelineView_Day_11_en","features.messages.impl.timeline_TimelineView_Night_11_en",20553,], +["features.messages.impl.timeline_TimelineView_Day_12_en","features.messages.impl.timeline_TimelineView_Night_12_en",20553,], +["features.messages.impl.timeline_TimelineView_Day_13_en","features.messages.impl.timeline_TimelineView_Night_13_en",20553,], +["features.messages.impl.timeline_TimelineView_Day_14_en","features.messages.impl.timeline_TimelineView_Night_14_en",20553,], +["features.messages.impl.timeline_TimelineView_Day_15_en","features.messages.impl.timeline_TimelineView_Night_15_en",20553,], +["features.messages.impl.timeline_TimelineView_Day_16_en","features.messages.impl.timeline_TimelineView_Night_16_en",20553,], ["features.messages.impl.timeline_TimelineView_Day_17_en","features.messages.impl.timeline_TimelineView_Night_17_en",0,], -["features.messages.impl.timeline_TimelineView_Day_1_en","features.messages.impl.timeline_TimelineView_Night_1_en",20539,], +["features.messages.impl.timeline_TimelineView_Day_1_en","features.messages.impl.timeline_TimelineView_Night_1_en",20553,], ["features.messages.impl.timeline_TimelineView_Day_2_en","features.messages.impl.timeline_TimelineView_Night_2_en",0,], ["features.messages.impl.timeline_TimelineView_Day_3_en","features.messages.impl.timeline_TimelineView_Night_3_en",0,], -["features.messages.impl.timeline_TimelineView_Day_4_en","features.messages.impl.timeline_TimelineView_Night_4_en",20539,], +["features.messages.impl.timeline_TimelineView_Day_4_en","features.messages.impl.timeline_TimelineView_Night_4_en",20553,], ["features.messages.impl.timeline_TimelineView_Day_5_en","features.messages.impl.timeline_TimelineView_Night_5_en",0,], -["features.messages.impl.timeline_TimelineView_Day_6_en","features.messages.impl.timeline_TimelineView_Night_6_en",20539,], +["features.messages.impl.timeline_TimelineView_Day_6_en","features.messages.impl.timeline_TimelineView_Night_6_en",20553,], ["features.messages.impl.timeline_TimelineView_Day_7_en","features.messages.impl.timeline_TimelineView_Night_7_en",0,], ["features.messages.impl.timeline_TimelineView_Day_8_en","features.messages.impl.timeline_TimelineView_Night_8_en",0,], ["features.messages.impl.timeline_TimelineView_Day_9_en","features.messages.impl.timeline_TimelineView_Night_9_en",0,], ["libraries.designsystem.components.avatar.internal_TombstonedRoomAvatar_Avatars_en","",0,], ["libraries.designsystem.theme.components_TopAppBarStr_App_Bars_en","",0,], ["libraries.designsystem.theme.components_TopAppBar_App_Bars_en","",0,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_0_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_0_en",20539,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_1_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_1_en",20539,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_2_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_2_en",20539,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_3_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_3_en",20539,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_4_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_4_en",20539,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_5_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_5_en",20539,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_6_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_6_en",20539,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_7_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_7_en",20539,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_0_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_0_en",20553,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_1_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_1_en",20553,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_2_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_2_en",20553,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_3_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_3_en",20553,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_4_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_4_en",20553,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_5_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_5_en",20553,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_6_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_6_en",20553,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_7_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_7_en",20553,], ["features.messages.impl.typing_TypingNotificationView_Day_0_en","features.messages.impl.typing_TypingNotificationView_Night_0_en",0,], -["features.messages.impl.typing_TypingNotificationView_Day_1_en","features.messages.impl.typing_TypingNotificationView_Night_1_en",20539,], -["features.messages.impl.typing_TypingNotificationView_Day_2_en","features.messages.impl.typing_TypingNotificationView_Night_2_en",20539,], -["features.messages.impl.typing_TypingNotificationView_Day_3_en","features.messages.impl.typing_TypingNotificationView_Night_3_en",20539,], -["features.messages.impl.typing_TypingNotificationView_Day_4_en","features.messages.impl.typing_TypingNotificationView_Night_4_en",20539,], -["features.messages.impl.typing_TypingNotificationView_Day_5_en","features.messages.impl.typing_TypingNotificationView_Night_5_en",20539,], -["features.messages.impl.typing_TypingNotificationView_Day_6_en","features.messages.impl.typing_TypingNotificationView_Night_6_en",20539,], +["features.messages.impl.typing_TypingNotificationView_Day_1_en","features.messages.impl.typing_TypingNotificationView_Night_1_en",20553,], +["features.messages.impl.typing_TypingNotificationView_Day_2_en","features.messages.impl.typing_TypingNotificationView_Night_2_en",20553,], +["features.messages.impl.typing_TypingNotificationView_Day_3_en","features.messages.impl.typing_TypingNotificationView_Night_3_en",20553,], +["features.messages.impl.typing_TypingNotificationView_Day_4_en","features.messages.impl.typing_TypingNotificationView_Night_4_en",20553,], +["features.messages.impl.typing_TypingNotificationView_Day_5_en","features.messages.impl.typing_TypingNotificationView_Night_5_en",20553,], +["features.messages.impl.typing_TypingNotificationView_Day_6_en","features.messages.impl.typing_TypingNotificationView_Night_6_en",20553,], ["features.messages.impl.typing_TypingNotificationView_Day_7_en","features.messages.impl.typing_TypingNotificationView_Night_7_en",0,], ["features.messages.impl.typing_TypingNotificationView_Day_8_en","features.messages.impl.typing_TypingNotificationView_Night_8_en",0,], ["libraries.designsystem.atomic.atoms_UnreadIndicatorAtom_Day_0_en","libraries.designsystem.atomic.atoms_UnreadIndicatorAtom_Night_0_en",0,], -["libraries.matrix.ui.components_UnresolvedUserRow_en","",20539,], +["libraries.matrix.ui.components_UnresolvedUserRow_en","",20553,], ["libraries.designsystem.components.avatar.internal_UserAvatarColors_Day_0_en","libraries.designsystem.components.avatar.internal_UserAvatarColors_Night_0_en",0,], -["features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Day_0_en","features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Night_0_en",20539,], -["features.startchat.impl.components_UserListView_Day_0_en","features.startchat.impl.components_UserListView_Night_0_en",20539,], -["features.startchat.impl.components_UserListView_Day_1_en","features.startchat.impl.components_UserListView_Night_1_en",20539,], -["features.startchat.impl.components_UserListView_Day_2_en","features.startchat.impl.components_UserListView_Night_2_en",20539,], +["features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Day_0_en","features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Night_0_en",20553,], +["features.startchat.impl.components_UserListView_Day_0_en","features.startchat.impl.components_UserListView_Night_0_en",20553,], +["features.startchat.impl.components_UserListView_Day_1_en","features.startchat.impl.components_UserListView_Night_1_en",20553,], +["features.startchat.impl.components_UserListView_Day_2_en","features.startchat.impl.components_UserListView_Night_2_en",20553,], ["features.startchat.impl.components_UserListView_Day_3_en","features.startchat.impl.components_UserListView_Night_3_en",0,], ["features.startchat.impl.components_UserListView_Day_4_en","features.startchat.impl.components_UserListView_Night_4_en",0,], ["features.startchat.impl.components_UserListView_Day_5_en","features.startchat.impl.components_UserListView_Night_5_en",0,], ["features.startchat.impl.components_UserListView_Day_6_en","features.startchat.impl.components_UserListView_Night_6_en",0,], -["features.startchat.impl.components_UserListView_Day_7_en","features.startchat.impl.components_UserListView_Night_7_en",20539,], +["features.startchat.impl.components_UserListView_Day_7_en","features.startchat.impl.components_UserListView_Night_7_en",20553,], ["features.startchat.impl.components_UserListView_Day_8_en","features.startchat.impl.components_UserListView_Night_8_en",0,], -["features.startchat.impl.components_UserListView_Day_9_en","features.startchat.impl.components_UserListView_Night_9_en",20539,], +["features.startchat.impl.components_UserListView_Day_9_en","features.startchat.impl.components_UserListView_Night_9_en",20553,], ["features.preferences.impl.user_UserPreferences_Day_0_en","features.preferences.impl.user_UserPreferences_Night_0_en",0,], ["features.preferences.impl.user_UserPreferences_Day_1_en","features.preferences.impl.user_UserPreferences_Night_1_en",0,], ["features.preferences.impl.user_UserPreferences_Day_2_en","features.preferences.impl.user_UserPreferences_Night_2_en",0,], -["features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Day_0_en","features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Night_0_en",20539,], -["features.userprofile.shared_UserProfileHeaderSection_Day_0_en","features.userprofile.shared_UserProfileHeaderSection_Night_0_en",20539,], -["features.userprofile.shared_UserProfileMainActionsSection_Day_0_en","features.userprofile.shared_UserProfileMainActionsSection_Night_0_en",20539,], -["features.userprofile.shared_UserProfileView_Day_0_en","features.userprofile.shared_UserProfileView_Night_0_en",20539,], -["features.userprofile.shared_UserProfileView_Day_1_en","features.userprofile.shared_UserProfileView_Night_1_en",20539,], -["features.userprofile.shared_UserProfileView_Day_2_en","features.userprofile.shared_UserProfileView_Night_2_en",20539,], -["features.userprofile.shared_UserProfileView_Day_3_en","features.userprofile.shared_UserProfileView_Night_3_en",20539,], -["features.userprofile.shared_UserProfileView_Day_4_en","features.userprofile.shared_UserProfileView_Night_4_en",20539,], -["features.userprofile.shared_UserProfileView_Day_5_en","features.userprofile.shared_UserProfileView_Night_5_en",20539,], -["features.userprofile.shared_UserProfileView_Day_6_en","features.userprofile.shared_UserProfileView_Night_6_en",20539,], -["features.userprofile.shared_UserProfileView_Day_7_en","features.userprofile.shared_UserProfileView_Night_7_en",20539,], -["features.userprofile.shared_UserProfileView_Day_8_en","features.userprofile.shared_UserProfileView_Night_8_en",20539,], -["features.userprofile.shared_UserProfileView_Day_9_en","features.userprofile.shared_UserProfileView_Night_9_en",20539,], +["features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Day_0_en","features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Night_0_en",20553,], +["features.userprofile.shared_UserProfileHeaderSection_Day_0_en","features.userprofile.shared_UserProfileHeaderSection_Night_0_en",20553,], +["features.userprofile.shared_UserProfileMainActionsSection_Day_0_en","features.userprofile.shared_UserProfileMainActionsSection_Night_0_en",20553,], +["features.userprofile.shared_UserProfileView_Day_0_en","features.userprofile.shared_UserProfileView_Night_0_en",20553,], +["features.userprofile.shared_UserProfileView_Day_1_en","features.userprofile.shared_UserProfileView_Night_1_en",20553,], +["features.userprofile.shared_UserProfileView_Day_2_en","features.userprofile.shared_UserProfileView_Night_2_en",20553,], +["features.userprofile.shared_UserProfileView_Day_3_en","features.userprofile.shared_UserProfileView_Night_3_en",20553,], +["features.userprofile.shared_UserProfileView_Day_4_en","features.userprofile.shared_UserProfileView_Night_4_en",20553,], +["features.userprofile.shared_UserProfileView_Day_5_en","features.userprofile.shared_UserProfileView_Night_5_en",20553,], +["features.userprofile.shared_UserProfileView_Day_6_en","features.userprofile.shared_UserProfileView_Night_6_en",20553,], +["features.userprofile.shared_UserProfileView_Day_7_en","features.userprofile.shared_UserProfileView_Night_7_en",20553,], +["features.userprofile.shared_UserProfileView_Day_8_en","features.userprofile.shared_UserProfileView_Night_8_en",20553,], +["features.userprofile.shared_UserProfileView_Day_9_en","features.userprofile.shared_UserProfileView_Night_9_en",20553,], ["features.verifysession.impl.ui_VerificationUserProfileContent_Day_0_en","features.verifysession.impl.ui_VerificationUserProfileContent_Night_0_en",0,], ["libraries.designsystem.ruler_VerticalRuler_Day_0_en","libraries.designsystem.ruler_VerticalRuler_Night_0_en",0,], ["libraries.mediaviewer.impl.gallery.ui_VideoItemView_Day_0_en","libraries.mediaviewer.impl.gallery.ui_VideoItemView_Night_0_en",0,], ["libraries.mediaviewer.impl.gallery.ui_VideoItemView_Day_1_en","libraries.mediaviewer.impl.gallery.ui_VideoItemView_Night_1_en",0,], -["features.preferences.impl.advanced_VideoQualitySelectorDialog_Day_0_en","features.preferences.impl.advanced_VideoQualitySelectorDialog_Night_0_en",20539,], -["features.messages.impl.attachments.preview_VideoQualitySelectorDialog_Day_0_en","features.messages.impl.attachments.preview_VideoQualitySelectorDialog_Night_0_en",20539,], +["features.preferences.impl.advanced_VideoQualitySelectorDialog_Day_0_en","features.preferences.impl.advanced_VideoQualitySelectorDialog_Night_0_en",20553,], +["features.messages.impl.attachments.preview_VideoQualitySelectorDialog_Day_0_en","features.messages.impl.attachments.preview_VideoQualitySelectorDialog_Night_0_en",20553,], ["features.viewfolder.impl.file_ViewFileView_Day_0_en","features.viewfolder.impl.file_ViewFileView_Night_0_en",0,], ["features.viewfolder.impl.file_ViewFileView_Day_1_en","features.viewfolder.impl.file_ViewFileView_Night_1_en",0,], ["features.viewfolder.impl.file_ViewFileView_Day_2_en","features.viewfolder.impl.file_ViewFileView_Night_2_en",0,], -["features.viewfolder.impl.file_ViewFileView_Day_3_en","features.viewfolder.impl.file_ViewFileView_Night_3_en",20539,], +["features.viewfolder.impl.file_ViewFileView_Day_3_en","features.viewfolder.impl.file_ViewFileView_Night_3_en",20553,], ["features.viewfolder.impl.file_ViewFileView_Day_4_en","features.viewfolder.impl.file_ViewFileView_Night_4_en",0,], ["features.viewfolder.impl.file_ViewFileView_Day_5_en","features.viewfolder.impl.file_ViewFileView_Night_5_en",0,], ["features.viewfolder.impl.folder_ViewFolderView_Day_0_en","features.viewfolder.impl.folder_ViewFolderView_Night_0_en",0,], diff --git a/services/apperror/api/build.gradle.kts b/services/apperror/api/build.gradle.kts index c0893bd7e8..62d3501655 100644 --- a/services/apperror/api/build.gradle.kts +++ b/services/apperror/api/build.gradle.kts @@ -16,4 +16,5 @@ android { dependencies { implementation(libs.coroutines.core) + implementation(projects.libraries.designsystem) } diff --git a/services/apperror/impl/src/main/kotlin/io/element/android/services/apperror/impl/AppErrorView.kt b/services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorView.kt similarity index 86% rename from services/apperror/impl/src/main/kotlin/io/element/android/services/apperror/impl/AppErrorView.kt rename to services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorView.kt index 3794234c79..5a2a5110f5 100644 --- a/services/apperror/impl/src/main/kotlin/io/element/android/services/apperror/impl/AppErrorView.kt +++ b/services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorView.kt @@ -6,14 +6,12 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.services.apperror.impl +package io.element.android.services.apperror.api import androidx.compose.runtime.Composable 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.services.apperror.api.AppErrorState -import io.element.android.services.apperror.api.aAppErrorState @Composable fun AppErrorView( diff --git a/services/apperror/impl/build.gradle.kts b/services/apperror/impl/build.gradle.kts index b72b54c198..75440dc5a3 100644 --- a/services/apperror/impl/build.gradle.kts +++ b/services/apperror/impl/build.gradle.kts @@ -10,7 +10,7 @@ import extension.testCommonDependencies */ plugins { - id("io.element.android-compose-library") + id("io.element.android-library") } setupDependencyInjection() @@ -22,8 +22,6 @@ android { dependencies { implementation(projects.libraries.core) implementation(projects.libraries.di) - implementation(projects.libraries.designsystem) - implementation(projects.libraries.uiStrings) implementation(projects.services.toolbox.api) implementation(libs.coroutines.core) diff --git a/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/FakeActiveRoomsHolder.kt b/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/FakeActiveRoomsHolder.kt new file mode 100644 index 0000000000..7561932fc2 --- /dev/null +++ b/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/FakeActiveRoomsHolder.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026 Element Creations 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.services.appnavstate.test + +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.room.JoinedRoom +import io.element.android.services.appnavstate.api.ActiveRoomsHolder + +class FakeActiveRoomsHolder : ActiveRoomsHolder { + private var room: JoinedRoom? = null + + override fun addRoom(room: JoinedRoom) { + this.room = room + } + + override fun getActiveRoom(sessionId: SessionId): JoinedRoom? { + return room + } + + override fun getActiveRoomMatching(sessionId: SessionId, roomId: RoomId): JoinedRoom? { + return null + } + + override fun removeRoom(sessionId: SessionId, roomId: RoomId) { + room = null + } + + override fun clear(sessionId: SessionId) { + } +} diff --git a/tests/uitests/src/test/kotlin/base/ScreenshotTest.kt b/tests/uitests/src/test/kotlin/base/ScreenshotTest.kt index 0b088b5de0..a611fa83cf 100644 --- a/tests/uitests/src/test/kotlin/base/ScreenshotTest.kt +++ b/tests/uitests/src/test/kotlin/base/ScreenshotTest.kt @@ -23,6 +23,7 @@ import app.cash.paparazzi.Paparazzi import app.cash.paparazzi.RenderExtension import app.cash.paparazzi.TestName import com.android.resources.NightMode +import com.android.resources.ScreenOrientation import io.element.android.compound.theme.ElementTheme import sergio.sastre.composable.preview.scanner.android.AndroidPreviewInfo import sergio.sastre.composable.preview.scanner.core.preview.ComposablePreview @@ -122,6 +123,7 @@ object PaparazziPreviewRule { ): Paparazzi { val densityScale = deviceConfig.density.dpiValue / 160f val customScreenHeight = preview.previewInfo.heightDp.takeIf { it >= 0 }?.let { it * densityScale }?.toInt() + val isLandscape = preview.previewInfo.device.contains("landscape") return Paparazzi( deviceConfig = deviceConfig.copy( nightMode = when (preview.previewInfo.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES) { @@ -131,6 +133,7 @@ object PaparazziPreviewRule { locale = locale, softButtons = false, screenHeight = customScreenHeight ?: deviceConfig.screenHeight, + orientation = if (isLandscape) ScreenOrientation.LANDSCAPE else ScreenOrientation.PORTRAIT, ), maxPercentDifference = 0.01, renderExtensions = renderExtensions, diff --git a/tests/uitests/src/test/snapshots/images/features.announcement.impl.fullscreen_FullscreenAnnouncementView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.announcement.impl.fullscreen_FullscreenAnnouncementView_Day_0_en.png new file mode 100644 index 0000000000..1b6fb4bab8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.announcement.impl.fullscreen_FullscreenAnnouncementView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:96a867cb12498cbdc97957bee07855dfaa13602baddaf933aff2b666ef4c7650 +size 3642 diff --git a/tests/uitests/src/test/snapshots/images/features.announcement.impl.spaces_SpaceAnnouncementView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.announcement.impl.fullscreen_FullscreenAnnouncementView_Day_1_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.announcement.impl.spaces_SpaceAnnouncementView_Day_0_en.png rename to tests/uitests/src/test/snapshots/images/features.announcement.impl.fullscreen_FullscreenAnnouncementView_Day_1_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.announcement.impl.fullscreen_FullscreenAnnouncementView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.announcement.impl.fullscreen_FullscreenAnnouncementView_Night_0_en.png new file mode 100644 index 0000000000..d6fd8eeb70 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.announcement.impl.fullscreen_FullscreenAnnouncementView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5bb36ccd718f3fec5b04f1bc812dc7718b5ea7fa4619c8b031466297a8d016fd +size 3659 diff --git a/tests/uitests/src/test/snapshots/images/features.announcement.impl.spaces_SpaceAnnouncementView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.announcement.impl.fullscreen_FullscreenAnnouncementView_Night_1_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.announcement.impl.spaces_SpaceAnnouncementView_Night_0_en.png rename to tests/uitests/src/test/snapshots/images/features.announcement.impl.fullscreen_FullscreenAnnouncementView_Night_1_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_38_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_38_en.png new file mode 100644 index 0000000000..473fd3f275 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_38_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26071a6b16f4446526f6abf28274580eb0f385bd0e74f4f2b0479da5f5d4f6f2 +size 12914 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_38_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_38_en.png new file mode 100644 index 0000000000..14c08f0b62 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_38_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ddef7c50d6a791c6a32bae8506a999b198bc601b70b6ffb5e5e7a8d5a2f1543 +size 12869 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_2_en.png index de86c4285d..03d7d97186 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d69d687090971ab7e9b87f959521109fbed48413ef217e1be3da8d7d985038b -size 38751 +oid sha256:ecd79d3f8c4efbb54efa6a5b8dee79274ae9957b01146025dab42a45fc328fc0 +size 24740 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_3_en.png deleted file mode 100644 index 03d7d97186..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ecd79d3f8c4efbb54efa6a5b8dee79274ae9957b01146025dab42a45fc328fc0 -size 24740 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_2_en.png index 07e9ef1e28..049aa93a8a 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2a6a7f3188eb9635ade7c0bbc2239257cfbe3166f4a6fb7917a1e5578f51d9e -size 37436 +oid sha256:0fd37517b8913e2bac3275f717aa15bed18055a7525e0404b25b43cb8bd8422d +size 23840 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_3_en.png deleted file mode 100644 index 049aa93a8a..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0fd37517b8913e2bac3275f717aa15bed18055a7525e0404b25b43cb8bd8422d -size 23840 diff --git a/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Day_10_en.png new file mode 100644 index 0000000000..8c6b1bd7cc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Day_10_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8ee76c2369a9671cbe370f367718fcda5bb08a89ed5116accc96928a64e9724 +size 55689 diff --git a/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Day_11_en.png new file mode 100644 index 0000000000..35f633b3a1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Day_11_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:029b19808dd54b74ef30737242a10af31b80e579b313d001dd9fa377bc2cca58 +size 58319 diff --git a/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Night_10_en.png new file mode 100644 index 0000000000..7983ef5e8a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Night_10_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:21a24fade9819efdb9114ec0ba3db21ec87cf93e32d896e22117fcd4f23e07ce +size 53601 diff --git a/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Night_11_en.png new file mode 100644 index 0000000000..1c50d23117 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Night_11_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eca54577cffddb66921623ede7ab39e017f5cd95e5049d6ad2763fa4f1f88ad4 +size 58133 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Day_0_en.png new file mode 100644 index 0000000000..adbcd16ec1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d861dd7397c0e15091e022d956c1955d86529fa0cc39e08c3c645d91e5023e0e +size 77677 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Day_1_en.png new file mode 100644 index 0000000000..df7c95510d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c0acaebca757642346f3381601f044a55d02749575150364e232f772ba0167e1 +size 73811 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Night_0_en.png new file mode 100644 index 0000000000..7bed006a97 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54c841ffefb3d053bb74e6f88a9aa7cff5d39b4854c92de3b507552a9a4226bc +size 68693 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Night_1_en.png new file mode 100644 index 0000000000..94a1749517 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eba1f4395b4c32e7edead84b1e22957cc8f973121207d7000419a8f4c314f5b8 +size 65936 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.missingkeybackup_MissingKeyBackupView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.missingkeybackup_MissingKeyBackupView_Day_0_en.png new file mode 100644 index 0000000000..4a5a13a36a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.missingkeybackup_MissingKeyBackupView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ef35fd3f5346870f11120a37c9db969453b7594bf9a0ccc71fe43e7fdade488 +size 62532 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.missingkeybackup_MissingKeyBackupView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.missingkeybackup_MissingKeyBackupView_Night_0_en.png new file mode 100644 index 0000000000..1b69f8f6a1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.missingkeybackup_MissingKeyBackupView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1501c2591f7df68404285770b1dad67360dddce074d4ce1c71223ea0baa0d1e4 +size 60873 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.root_RootView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.root_RootView_Day_0_en.png new file mode 100644 index 0000000000..e3e5480add --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.root_RootView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72e73b036458ee32e207f711cf6656fe7646b23d3d9e096e62932c828dd53189 +size 5244 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.root_RootView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.root_RootView_Night_0_en.png new file mode 100644 index 0000000000..6582264383 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.root_RootView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:518818c549548b6304d2960242ce7251bb609fa439928539a7556c33223ca8ba +size 5251 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_8_en.png new file mode 100644 index 0000000000..133535c6d5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:01fa1c9b917b65afc2d1464fad177f7420dea1625eeb7c8335d8105664134e67 +size 312145 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_8_en.png new file mode 100644 index 0000000000..30d5478a90 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e622d9b43664c5a31b83b41801ed07769384ab9ab84aad57605cdb67b16c58d +size 392254 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.threads.list_ThreadListItemRow_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.threads.list_ThreadListItemRow_Day_0_en.png new file mode 100644 index 0000000000..ac6836af2b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.threads.list_ThreadListItemRow_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c523f3a502600b837c07ecd5804831da2d9aba5a74886b7001affbb90169112a +size 12455 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.threads.list_ThreadListItemRow_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.threads.list_ThreadListItemRow_Night_0_en.png new file mode 100644 index 0000000000..05f2571669 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.threads.list_ThreadListItemRow_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:528c2f3183a153b9129606806fc457265819ececd71cf5021d1d843970c0b774 +size 12366 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.threads.list_ThreadsListView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.threads.list_ThreadsListView_Day_0_en.png new file mode 100644 index 0000000000..adeb4f7c4d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.threads.list_ThreadsListView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c02999b2d0eba92f1e9b1f5fac52bcff303401f177459d1a439b7db906e5c2a +size 64622 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.threads.list_ThreadsListView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.threads.list_ThreadsListView_Night_0_en.png new file mode 100644 index 0000000000..d89f062c11 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.threads.list_ThreadsListView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7f6b64bfe0b47546a009efe23060ba091adbad32288ce546cb3b030d7a9ec88 +size 66177 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Day_0_en.png index c70050658f..0435f55de6 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bd81cbc3db5d329fcc1f0d4866c220af7488bb68929d0e75b983a170d15a7ab7 -size 380173 +oid sha256:ff839731834d7d0c94fa9a2c412014ddbaeaa053c5af1c8cbd00a3f3a583f364 +size 379511 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Night_0_en.png index 57e2226f1f..f0b5506b89 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3be2bb0c8e344780d59b67a4ba23e50890b24a8b8ad1c48673e34403ff8db496 -size 378130 +oid sha256:04ea6c2947a21d3d4a3d43e9c4750c4f2a49056536a4da3f34c95df8353e9de3 +size 377990 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en.png index 662ccbf182..3ac59caeea 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da65fa129fdfe903f3e466f4e1dc6c70a6930e0388a3f6d3fb116e5b91411cc3 -size 365497 +oid sha256:1a94d5198e27338675c0e15df5874ba3272ee315b8d77291dc7ccc7c754ed1f4 +size 365082 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en.png index 11289371f2..86018764ac 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cd797555bb72d7bc1b874df24ca5793df5550b235fe9a28d20aaa04bf5f3d0fb -size 370592 +oid sha256:8344427a0fe87cbf0883ff53a9ee4dcf90976a7011f6618d85961df48b2c53a9 +size 370228 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en.png index 64bcaa05f9..058a3992c3 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be969a809f346799887fe031e139dcc47db8bc851ca4ccfae8dad4ad9dc523f7 -size 363498 +oid sha256:00fe1e676086766a3eac3e4457eff52c989789fc5d8d0df2624c019ed25ce9af +size 363322 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en.png index 33ea565e67..db4ea8296a 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:48c0af64a6aab3a28ac0493bb86cb92424555d6793a0a5c6cd446c25a20ce915 -size 368757 +oid sha256:2f0d24b8011564ee7a8b2f8c2e6a24eda668ee42d451d862f884acb7926aa06c +size 368529 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en.png index 1a345fdd13..94428abb56 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b4bef0095e989bd411426a2626155f1b75931f05c32d2632144f3c232f3aa93 -size 348379 +oid sha256:36d7cf2510d546d2b330b1d1b1674ce58df0558c547e443794a1e7b2222add87 +size 347897 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_1_en.png index 7ef8d729ef..7ddac02717 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:30db6503b7dde29553b5f806924565b1d60c13edeeaa982dd2ec7d2a5c8f1162 -size 364508 +oid sha256:a4ac79f3b8fb4e231244bd96b244a4f2526d6ed28d558f9d50734d83bbb74992 +size 363909 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en.png index 72f5c9a9e5..bac23aab98 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:15ad31eb2b457790913442d3186fae215f77f345470748cd64a345849b9de9d0 -size 346837 +oid sha256:e30686c7c037ef102717e0316543faf4d3ed2671c21eca96a667150daab380d6 +size 346614 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_1_en.png index 5a6ad7fc23..b6e4650d9d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e72a79c374fc23a1325e1bd294584b8b7f0b312c804b932144bc2e5d8911bd9 -size 361950 +oid sha256:2cd357a1fbaa425eca95587bff2f768a45e773f983f75664875653ba1900e534 +size 361625 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en.png index ca682211d3..67490ba700 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:092a0d4c37d47c84c790a767b9acc77f38878876e609b986f80e5fb461e0a340 -size 370156 +oid sha256:f4431e2d9f30123949ea5676ce60f642234adcf9b5a33f2acc3f80ebf6609a66 +size 369730 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_10_en.png index 51a93f68e5..f0dfbc3321 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:097fe6208d9cadc707e6866c151a2ab843550a9fab4d2cf4908514ceafc4e396 -size 355327 +oid sha256:f7a6302320bd1e4258e03487539bf46b0b12781dfc8a22b3f9f267017a6146fa +size 354751 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_11_en.png index e60716fb3b..c4943b892c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f61a7a62c6fc96e23c8aaa793f5fff68e2197a067ad8f1614d1103e833b3230c -size 368585 +oid sha256:4c410e853089cc2c4a2c48a00dd9a8babc8095aed72bb4625057444710559c6c +size 368129 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_1_en.png index 73be879502..68f8148879 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:091352ebd6bf2abfac5dfd99be5edc94159a5a45c1d3137c7271e0031116a254 -size 345649 +oid sha256:6ca8cf82aa8ab1bdad56a27584b27789402aeb7ad8fadd038280a376fb75a72c +size 345094 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_en.png index 4ec235074c..8aa2cca147 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:842632747c13219ee6c7a50d9003c9e07c622393ae4b066e86eacf04f807b181 -size 358344 +oid sha256:02ecb903124273e24151ea2088f6b99144f7dcadba00a582b6210dd23835b5c9 +size 357743 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_en.png index bc6cb62827..cececc229a 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f43d615fd1aec05ee483d3047e99c1ecc89a6e5a7e56e6cf7e1ea4c108be413 -size 357473 +oid sha256:f0c6dafaa41849af00d5097a85bd88e3a054a0a9e41c0466a08e6ff8dd6102b2 +size 356857 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en.png index d39b6ec114..14a40a5676 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:898a1e8f0277d43a1127d99ec50687c24fe1154ceb290832a14fa5a43f12647a -size 365153 +oid sha256:a11044fe90227f1f316a1ade8587e545bb620e11cca76865fc2eb83cdb817fc5 +size 364655 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_5_en.png index 0617c06d2f..133fd3903f 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4cce6211c0a33407498145cb03a780d80553983146f487ba3e26f794ddf12369 -size 397594 +oid sha256:426d1c844a8c88281c5fa4c61916d93278fdfd38f8cdf2dc54bf1a821f2de3eb +size 397167 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_en.png index 838c70dcdc..0041a870b9 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7e91b7aec4c43e2028a2e69a02f66e04f6a3188a42387013e3deb1faa7a108e -size 356627 +oid sha256:a2bd77c08be705c66225f16c29fd6826decd92b41922d0f8ffc0d1a63d8c59d2 +size 356193 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_en.png index 44e5341280..9fcf389708 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:80c2e99ce6f851994da38077acc30ff386867f2093a8db4de3c1b82b0b6c1a8e -size 356307 +oid sha256:9daeee1aaa04247da3c8bb5aaae9947aab1ed2d2057995e53d6ebdb63e5cd356 +size 355848 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en.png index 6a0ee13891..d235670def 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b4fd5470ac81fbdaf471e297c0f5bd9c6b9945e006ab88079568848befe1049d -size 365366 +oid sha256:a00bd766a00b7e7c6615fc53bf469b3ca5056a6c514c096431ae38d99d604306 +size 364845 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_9_en.png index cede2d333e..b10f9741b9 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a75fb4510d405b29b120fa400d4f69a89965c11a2228b5cc2e772200a12227e1 -size 355912 +oid sha256:3ec1b0244dab22414c6719b3309426e13128d0c70fb67dc66cd8a60d42521fd0 +size 355411 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en.png index ebe440bfdd..422c324695 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a3f7e96bbbb4b3661514713514d7f6cb27c6634ba2621c22c0dd4ff730e5e24a -size 368043 +oid sha256:a1df8d22766981fc76d6d767dc3f40d98cadc7c51be20417b067b91af604232b +size 367884 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_10_en.png index bd841f6f75..1d34761a9b 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:449cc2fa5ce39163a5d5eea3430c7dc457579d41b0213b8b46a61acaa76b3ccc -size 353304 +oid sha256:f7d9b5fc0ff5d7828ad6f4dc0c9bd9520b4920553da97ed7b4e030ee844c0208 +size 353054 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_11_en.png index 2867c5dfd4..c081fa16a5 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4cb93499ea8f21d3fb79aa81b2746972f57a4732296bb421897e93a47ddf7d06 -size 366544 +oid sha256:6a92bcf5885322e11a706071969fb674b1f6e60e021af153aaa125baa4a602ae +size 366285 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_1_en.png index 83d0f043e4..b6d1f07279 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a6bb6a35c2e1d2090e6e6e5b3a1733d16fd433e63925082b2ea32293ecc639fd -size 343140 +oid sha256:4bee12a6bdb18fea5ac6997671bfcb903296da7278cf69b2456330066ca31dfb +size 342870 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_2_en.png index e178f75d91..ceca184bd7 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a1067826b5b1e3220eef3ee3b9a8c9f77bebb248ba2378cc2e7029b86c585c48 -size 356339 +oid sha256:8e82011a72f36889c53ab61ed2b9947c7fb5f868c9b7010904fc10f4ca587082 +size 356082 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_3_en.png index f43199a9d8..5cc5cfe4fc 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:437f71e1845447b8f09905b4d97dd46677d8d8db669a317f8cba9ada6f5f1aa8 -size 355551 +oid sha256:6524cc87257cc9d3f135904f4e14594b56df695e71fe576418d2319c472d710b +size 355285 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en.png index 80db679074..e9d85990e8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ffa5216fcff101f8ab1047ac9808f98749144e1b1197bc01905cd6ad251d31b7 -size 363081 +oid sha256:66d4b6ea4a697735bbb0fac936b89cde99c5077973ba828894bf50b5dae233b3 +size 362848 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_5_en.png index b7a7e2020b..0eb106f4a9 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:222460ba0557c96c2ad0e8aab675a254f515281d06bf4a7b642ea7ef79d5171d -size 395589 +oid sha256:217ac95bf476c53d2dc6d3d0143b21312bf38b8ab86c8dfd4f9360582b584f9d +size 395476 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_6_en.png index 75bb1086fe..ae8495618a 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb0a738e57b657bda2454526dcd78a37d38839183a5feae99bf7f107c12ccded -size 354661 +oid sha256:08057fdc1e1184720b438ddd2a8753f00791baf2456cc30b45de8e6bdeb57e8e +size 354404 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_7_en.png index a93768052e..40105c1498 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f98146a417290fcc741ef676fdb60f36822931358e943ad12faace7f96e995f -size 354422 +oid sha256:5ed24d190f10c079f8cf176404372d40eb25cc78ce595a22febc540315057b86 +size 354075 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en.png index c09d98903c..0cfd1dc985 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:54c9d1f121d83f1a161f6483a6b4bbefa220c235ae88097ed4d03baf10bbcf9c -size 363289 +oid sha256:545c9e8933e420cc3f8fbba0fbd5162c946c65a20721faeb77c568596ca720cd +size 363045 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_9_en.png index 0e2c06b0f3..c8306263a0 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:932e5b29f2a64d70956470bee1be397775c6205f590081d3b90c98cf47845577 -size 353993 +oid sha256:756f0cff90ddd61adce2f467c0384d74d430f6d51c091e4980e45bcc9ee6f0fd +size 353705 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_MessagesViewTopBar_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_MessagesViewTopBar_Day_0_en.png index 8c023c5e17..b2c131b124 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_MessagesViewTopBar_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_MessagesViewTopBar_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3136c95bc9134eba4cfdfe8d552473a54287c356bd3895291b9cd4ec11969d9c -size 52706 +oid sha256:e26887bd81e10726414e1833029b4b51e22e534684a230777ca50f86024af994 +size 56430 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_MessagesViewTopBar_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_MessagesViewTopBar_Night_0_en.png index 3b3133cf67..41b8b16bcc 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_MessagesViewTopBar_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_MessagesViewTopBar_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6770e720477c2547f593c626cfe3bdafb9b7c78d0b66e910fb9eb1163730045f -size 51707 +oid sha256:ce44cf850169736008a3f3fc21a2be4fb044badfae631b0284ce379b325879df +size 55533 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer.appsettings_AppDeveloperSettingsPage_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer.appsettings_AppDeveloperSettingsPage_Day_0_en.png new file mode 100644 index 0000000000..708313c475 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer.appsettings_AppDeveloperSettingsPage_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:62068492969ad00e1a8e4a44189f93d83c98fee40612e4eecb68d3076d00ed07 +size 56425 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer.appsettings_AppDeveloperSettingsPage_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer.appsettings_AppDeveloperSettingsPage_Night_0_en.png new file mode 100644 index 0000000000..b69265cf41 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer.appsettings_AppDeveloperSettingsPage_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d92e220d675097c37b60312ca4c4e821eb6ff6475bcd004437dde0d2e964cce +size 54756 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Day_0_en.png new file mode 100644 index 0000000000..b56ed34048 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf121c0ea1fb3cc7a47a292877535c9c0a1ae1ac11bf7f5ce169d0cd79844246 +size 53775 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Day_1_en.png new file mode 100644 index 0000000000..d0a5500209 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:37ae25b9f9c659164c006c9bdb9f94379398ee8215e8a2dd9167620ef994b6d1 +size 52347 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Night_0_en.png new file mode 100644 index 0000000000..d2929c43be --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db439869d2b8843cd58251bbca638e0dad7ed2b1ae4b22507e052e3d5521214d +size 52090 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Night_1_en.png new file mode 100644 index 0000000000..540b910b54 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cae56bb14ae7bc1f6c73884c2efb333b7da22c82d69026854a59c380657a5bbe +size 50705 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_0_en.png index 4026c0e658..503b9ad61c 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e24299f25ffd7095887bb52214c25de8de2cf0380b374d11f65d78c86f49dae1 -size 45507 +oid sha256:d18a2e9ba19401f958483bf06ecd7b311ed7383de0df3d89f3bb4d3a57f8e9e7 +size 54191 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_1_en.png index dd9d30850a..16be314a0d 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d84781b107e2f25bdc88cbfe84a1933dd20bf4c1dd372cb69f136f36df2607c0 -size 41951 +oid sha256:aafbc1f791f067fd0084db3bf293511e3cf7557329e7599c3f3e3c66f01435c4 +size 45472 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_2_en.png index dff4e9fa71..503b9ad61c 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:577c00e6e45e1da5ac1b1deee380d7a087b1f32e077f8e5b9430497bf6f7012e -size 44083 +oid sha256:d18a2e9ba19401f958483bf06ecd7b311ed7383de0df3d89f3bb4d3a57f8e9e7 +size 54191 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_3_en.png deleted file mode 100644 index 4026c0e658..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e24299f25ffd7095887bb52214c25de8de2cf0380b374d11f65d78c86f49dae1 -size 45507 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_0_en.png index c8188fbaaa..430756b5b3 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9750c0ae52dce1ba63c1cff7a22a3e3c75c15b5a556ba7fff49815b55836372b -size 44198 +oid sha256:dcca87aa6eee7e45bedb8f58c3b776e3932d94843573eea70aa686c3de82e03f +size 52290 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_1_en.png index d3c89a0735..4225562f66 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:31a3e5f9abaed21c87052ef7642dc8456d75580b79988ebe271f09d1381e9a03 -size 40820 +oid sha256:25e311c9bd46defd9659004d2d36088985c0578189a41906b51bfe683ddb6488 +size 43889 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_2_en.png index 5bfd54ce11..430756b5b3 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a7aab145e8ca2cd9de64a145c7966420a474b3500016a46100dad798f33acba9 -size 42792 +oid sha256:dcca87aa6eee7e45bedb8f58c3b776e3932d94843573eea70aa686c3de82e03f +size 52290 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_3_en.png deleted file mode 100644 index c8188fbaaa..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9750c0ae52dce1ba63c1cff7a22a3e3c75c15b5a556ba7fff49815b55836372b -size 44198 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.user.editprofile_EditUserProfileView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.user.editprofile_EditUserProfileView_Day_3_en.png new file mode 100644 index 0000000000..8517440a90 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.user.editprofile_EditUserProfileView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d4dbcac95e6fd72ee2eb683a4f25d49f4d181be9b1386ec01670fb078bc46a52 +size 18966 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.user.editprofile_EditUserProfileView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.user.editprofile_EditUserProfileView_Night_3_en.png new file mode 100644 index 0000000000..75ce9869d4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.user.editprofile_EditUserProfileView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:901c40d524de6de6fd70cbfcfc8b1d86cc896734254452535a07830587adafcf +size 18987 diff --git a/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.folder_ViewFolderView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.folder_ViewFolderView_Day_1_en.png index 035b39c739..9b53e56d9f 100644 --- a/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.folder_ViewFolderView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.folder_ViewFolderView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0cdaf746151224c1889c60232b55cb8c9d8b3c07cd341b1bb8ad6fb7abff97ed -size 9538 +oid sha256:a7983d27647e60d385703d5bb91763ab787ee69e0a0ef04acd13ca86d5d5b1ec +size 9560 diff --git a/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.folder_ViewFolderView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.folder_ViewFolderView_Night_1_en.png index 2a46e52aa3..ced849e58a 100644 --- a/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.folder_ViewFolderView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.folder_ViewFolderView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:072d91aa19fa0c5ec80ca565e1529988598cd54afcf6f593c6dcf17882170d0c -size 9393 +oid sha256:c32280e645ff1459b179423f67077dfc75093f5e6c40fff659769d164f391aeb +size 9408 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_AllIcons_Icons_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_AllIcons_Icons_en.png index e3cda24208..79992a9708 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_AllIcons_Icons_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_AllIcons_Icons_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ca6ffe10dd122a2c2df99160b4e318e591ef6d3b10b6173d36f6c9959a93277f -size 114895 +oid sha256:30b39281fd597089c94c8ff1680297143989d6da97d6f5b15198be5f92469483 +size 114653 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_en.png index acec20812f..c7e3599c58 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c5a4487507334ec43c9d659f57f2ec0d86856d941f8b1b437c101b696a5b49d -size 24223 +oid sha256:bb4d6bfb9c412de00a2b4956032dd42906b5451eb99e6ebb1880dc01f6b55af5 +size 26077 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_2_en.png new file mode 100644 index 0000000000..d7ff4a1d2f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26bf76ccdb56d042422553f557d91d0f26d874a710f696ac106c5c2b5590d332 +size 38833 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_1_en.png index 0c60a3da07..fe44b8941c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ab0ba9a693ede4106d09170710f215bccfd82dbfbadfdafa5fb49fe39a03c25d -size 23471 +oid sha256:b8c422787b67d477d3b7c8d5dee8879f33d47153dc93dd29bb3883e4ed863a41 +size 25232 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_2_en.png new file mode 100644 index 0000000000..f5ff7856b2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8413aed02383572cfe8c481c6ba8b0db4cfb3402334c37f2d8b54a73fe4bf594 +size 37343 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_0_en.png new file mode 100644 index 0000000000..60c5e812ba --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e514d7f36fe25150d66c2d2092982a696196a5a0a2674eef97b7231c0c03bef +size 699410 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_10_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_10_en.png new file mode 100644 index 0000000000..5a96554c0a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_10_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:712af2723a0656ffe927f1cd488d3117f5b450fa405dad307b0c88a9f2483f9c +size 698704 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_11_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_11_en.png new file mode 100644 index 0000000000..e4424188e6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_11_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d6ab1331e65a4d02accb4d20c899a06c37a022d9f9080133799e30763969300 +size 26774 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_12_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_12_en.png new file mode 100644 index 0000000000..3dccb4c068 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_12_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:010120cbcbfed1e7bfa3fb4c88df6e1098d0382cb1834f1a8046e312434f201d +size 26549 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_13_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_13_en.png new file mode 100644 index 0000000000..d0139ec184 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_13_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:52174b55b1737787260a454c59f243b0e3f6327ef5ec71464744def928d165d4 +size 206785 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_14_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_14_en.png new file mode 100644 index 0000000000..71ca7c633d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_14_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47c2e2194283803c55553c0220400ed8991539f25b772e0dbdde6d5defa3d3d4 +size 7343 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_15_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_15_en.png new file mode 100644 index 0000000000..4e8d03c6a4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_15_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f1e56f491ccc4c9ab82dde36a8fca588bdf037fb59b70ea47ffe13a56e22f801 +size 4457 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_16_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_16_en.png new file mode 100644 index 0000000000..a32a45029c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_16_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99478fe3d9d44e9cda39f33748c70017203a2cce5cb855605740a29360221bd5 +size 184910 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_17_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_17_en.png new file mode 100644 index 0000000000..a855818ac6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_17_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:062f8342a3a3715498ca34488c2d9ffe09c1e6dbbe04df09e88fd107a33b174a +size 653228 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_1_en.png new file mode 100644 index 0000000000..accbd26985 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3effa629295c12d2248924cf298599f3bd28975ecc76fac84f771c16266d738e +size 698895 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_2_en.png new file mode 100644 index 0000000000..b9dabc97ee --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1fd3b7065860d1e51fe51a0a6e3f4a9e61a77ea91cefe85cc3aa3560c4cc6bb2 +size 252253 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_3_en.png new file mode 100644 index 0000000000..f11d4fc3fc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f9ee78e2a034f34d77a438d305a5cc63ea583df80f83a14b1d81fcf74ca93f4 +size 665416 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_4_en.png new file mode 100644 index 0000000000..5e081d3c4e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d1e91c66363a5af74d4345f7837db178410c078e390b0cf9296d0ba4b3dfd7cb +size 207004 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_5_en.png new file mode 100644 index 0000000000..d61e0a50b9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9eb950f3c0d34a796ebd7635ff736023742d5e6a243912f3ad2234ecf08694b2 +size 183220 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_6_en.png new file mode 100644 index 0000000000..f2352c88f8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f6c84fbd21949a3d9d355bfb627b8a70f65fc669cb6b6818a325fe4577351f9 +size 196092 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_7_en.png new file mode 100644 index 0000000000..bd37e3eb79 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26bd927acc578cb3585f6e101c066f0a6adb6a6e424dcd196388d5a838d8b22b +size 196396 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_8_en.png new file mode 100644 index 0000000000..69515d4326 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c4440f5ed01f2c1b405bb2d444c52455ea8ee1c094baf7d2f85bd9a9ff98b02 +size 210117 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_9_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_9_en.png new file mode 100644 index 0000000000..5d254988c9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_9_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11f140569061e21e6b546118b57340a0830779580498a31627acb2e6c7313471 +size 210490 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_17_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_17_en.png new file mode 100644 index 0000000000..dc06b5afc9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_17_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e353d24c2b9b49abaa11e064b1581215ad65154d0e67372fe24684ba2695a0e +size 442063 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_1_en.png index a5db879ab2..1a6f5d660b 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9775ee85b895e1eb6b58841a5232de980f44bf4a20c0f2e2bcab731f503fc762 -size 11000 +oid sha256:30ae0b86a9834a90990a42f6bad0fc9010b4109bfbbe85072f00d61b0664e6e2 +size 10926 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_2_en.png index c186a409ad..1c65fadbf4 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7ab6bf5d8255d61569c4400f5a911400f415a4e228de41e181ccb42e2742e47 -size 18637 +oid sha256:d6c87eb2b2da4afdadd949bd39e88389c34053adb19d34f8958f97ae1faf90da +size 18557 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_3_en.png index c229dd85d8..42f4e1a941 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c54410a3733a3cecd1b161eb524e1809c270b8d2dd814c095eb73e013b486e0 -size 7840 +oid sha256:3c49dbe21e9c465adc1f12a57b87dcb06569936e80d047e0db77314cefce4691 +size 7756 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_1_en.png index 609162f4bc..41e20b4266 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:793eea182afcb72be59a090d3c3e622edcd6f9509271bef2e75a2f45f47a8fe4 -size 10567 +oid sha256:7523f982565c692f9986d11bba14ecc51a3401700de5a752fe5d2b667d0ea236 +size 10560 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_2_en.png index 874da23636..be3a01d3d2 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5e1cd401ab146d2d15e94a591625ab40440e32030f4acfd4ed9bd18dce01a1f0 -size 17814 +oid sha256:73769f2c3ced2e20f6a3a91cd8cd040c14e93a19c923d5784e2081cf4baf2d44 +size 17790 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_3_en.png index c4e3bc9a39..90d85686d7 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0e47e98d0ace1eb50dfeb9935c27206abee28d42d0e092d410de4239dc6ac293 -size 7519 +oid sha256:ef3e1d6d11520f6475059032d53c9c0a6e80c133318a0ae44dfc67d9b69c31c1 +size 7494 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_en.png index b51a04e421..df3af23261 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2cf5e60d289daa62dff1c07469227acffd275a1e4a3534b60cbe37c89a2c4bb7 -size 72136 +oid sha256:2703da4167bd0198f9666c2160bf95e4c99ac718864ae67d277fbbb4fd50014f +size 71876 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_en.png index 217b3181b7..6d8f6e1a3e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af4895f8222d984437a5d7427268d198345ad690161789d7bbb0340eeda916f7 -size 58660 +oid sha256:86f6e18357569284c223afb87195be656e6e6c90794e64ef6a13346c8de6c496 +size 58367 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_en.png index 046d651b57..2704eb3847 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6049adcc9de51802804d0224bd7d5a7ee8ae2a754411e4ca5685df8028d51480 -size 71727 +oid sha256:05f9317281ed84751b9f168c4495e94d1f68a9f9e7946270402db59e259d1ac5 +size 71392 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_en.png index 26ba390595..6cc38e5ad5 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e339b203a782f7d73195b1cd6961c67dac8b313a6a965515e4bc2d2b83a2118d -size 80524 +oid sha256:be4cda10c2b416d70d020c46abb00e41a2802e5d71f2dc416c506cffae147af1 +size 80226 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_en.png index 0a18ae1074..a74e2710c8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:22cf555cb18cea56f2e6bd9f91dbf70353916f196ceba84b11ea017b9e95d794 -size 61511 +oid sha256:3d29017d9dca92779074b963ba29681bb6cc83728a15ba2f55846c8fb2751d1d +size 61119 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_en.png index 063006fb88..f3472de5f0 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de1e98fb04a4188e2c6b795a3cf0b21129d0e99f8ea2748137e9f8122d413c6a -size 60353 +oid sha256:a8d1701f1be6087e93d15bbeed97659e57e0d58b9d5087fad0253d5d3dd60194 +size 59972 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_en.png index 9bfef2c6a2..be14ebd9b2 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1861fe30f2c653911cfe193d69c21affea73f458132d6a1b6381aa504d4cce74 -size 67327 +oid sha256:d2d984f17ca414c523d6a2cdcf9fc4f82c0759b22b72b60e8f5bb28776350a72 +size 66958 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_en.png index dcc6b6c915..c0ee61b312 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b5f1d42f06c4c5949f1fb1d216c7af45c4606a782178c21b58d0a0e7504a333 -size 89006 +oid sha256:eeb25a0b7738b0a8b12a2fbdcfc469dcfc2c893ebfa4a50a3047491f817ff981 +size 88665 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_en.png index df1c374209..05cb3a4a89 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7074c637e0a3e9320f86a4f417931d59c2d5e199996e8e533f692a82ab0d8d2 -size 59581 +oid sha256:f1f008fc8bc035bf9256e19e831691d3ac7c3347e83aaed2ae8d4b7a7f5b4213 +size 59285 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_en.png index 442ea4ff99..63622b0027 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0f084ce1bca37f796e7f50d78a4f03485b77fa742efd688d6b8accc2b41f5e9 -size 59734 +oid sha256:e9294ee360463d72968ef89eeed7ff52c80da29e4a59f36f00422f1fe3cb7d9f +size 59350 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_en.png index 4e4fd5476c..62268cf4f8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20f92a0cbc8a60bb057b131e17300653ce21367627f611193b6b9ddeba5d4b37 -size 66908 +oid sha256:5dd13cfda2b0f029a46477cbe9f8db1e3a191890072665adda02ec0750cddcd6 +size 66535 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_en.png index c5324db3d6..da1080256f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a0be3b08f1923c223eb433cda9d8c1bd86ba2dff1bce31e069b29953b13a084 -size 59085 +oid sha256:9b32f9442e9088eec377f74127c99002aaf2c5668b0550047dd7b1c9e73ad619 +size 58783 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_0_en.png index 8fb1c22b45..9e1047504c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:17bc1cac4537bd0b9a2dcd91838ff7c20de7e6076dbb9b174ad801af76f234f8 -size 69080 +oid sha256:24baa66c2fced922b328987b51c1045af7111fb3c7e4bafdc46046eae61d0f87 +size 68773 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_10_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_10_en.png index c6b221f461..70a49794e7 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_10_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:caceabb04420f3675940e3b95ec685cb2b02c69d87fd19843640a5e882dfa1ca -size 56124 +oid sha256:e343b71b9f170fa7879510544cfc95af1ba15ebc128bc5d78afd49d10c70dc66 +size 55802 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_11_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_11_en.png index b5e644918e..c04c1cae02 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_11_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d46716354df7925d15345b61d8e16d80c0b3552b687a896004f4174286ad18ef -size 68625 +oid sha256:d76f37cc5bd2d6ee9b7c87bfe7b656b0095ce6ded4d07cdc48d255039aed3520 +size 68313 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_1_en.png index c2bbd56d35..98a5dfd03c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a0b4ad61421f6b142e51200fb40df935027962145fa09b9871a11cdfdea8c0f -size 77316 +oid sha256:d24998e960d2d4d36c1ef0f589e9094d972203016b0a923268c1894bd09f267f +size 76999 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_2_en.png index 3bb5259f05..ba691c8f65 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:89b7d536f86c72378714e4fab14b24671a86726abdcd63a1e15abcefde6a6abb -size 58721 +oid sha256:62592587ff02f0d36cb40f0b59fa3e7c6925cd42622ac76c4d561f6df999eba9 +size 58404 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_3_en.png index 4af97d2cc8..7ee8d6355c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d314aa8f8572a099c4aa0f96de8dfade75c57edee4732c3d1300e5356c0bed20 -size 57649 +oid sha256:fc54d34649b067af6e0daa7d2e1c8661572fe0139ea9aed209b28c3a99f44f12 +size 57353 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_4_en.png index 918c7fa1b5..80a0c981fb 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf3bc1027fdbbb194d1a0120123872f0ce75c41c43d5c73c72908ddbffaaebb1 -size 64314 +oid sha256:10f02c7de0575dc30c65cafa4adeb265d6ea1e487db240f1d4052be138a8b0b0 +size 64014 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_5_en.png index c5eb73aa1c..3c4f0dff27 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:92273288bdfe895fe0664533f60e322893ece33d271259303e35d9358fbae5df -size 85238 +oid sha256:a196fe9e7c551fa7cc1145c9f8df12c5905678fae285884b71e7ea15cea1bf5b +size 85088 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_6_en.png index 4449d490db..616dacf9a0 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d04a904fb5f98c075ed844668a06af29d03e7d1adf8bab7706d1cea39abcceb6 -size 57003 +oid sha256:60b3b04167249a6283dccae1450645853df5e05ac7a31f419c674be0594dd413 +size 56710 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_7_en.png index ff54d6d0c5..256094a264 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:896b4497818ded4ce18ad6089fd436160f87189d79da1cb63ea652818de10041 -size 57009 +oid sha256:d303c75fe252ee9d3fb0bd91131ed44b731ea6fbe120dc1379aeac4c4d43c467 +size 56693 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_8_en.png index 6d72c0f7cc..9aa831dde8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:256349593b50a7e1f40db6c032aa39a640136934eadde8883936b0b1885f81b0 -size 63867 +oid sha256:81d23de4f3afac7993f42ea8030f6a2392768b343761408d6737382492443267 +size 63586 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_9_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_9_en.png index f05bee7bd5..3c59d53b44 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:25b5d39992361966545a3c04b3de7b20569a1e12800b3ecf38e090ceb2afa7c9 -size 56567 +oid sha256:7c3a1636c58e8243f971f69ea3575e75a12526863f1304a32f65fc2991fd403f +size 56250 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_0_en.png index d9d7e2913e..93c1f0a9ce 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bfb2c4674cd8bd0bcd434a2790f3e121dbc6a6c39e41d0417c80cdfb44be91ae -size 73627 +oid sha256:8eb6049cc99813238995f936022fd9fd5a43d1e917bfc69ab1069fe3da456cb5 +size 73316 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_10_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_10_en.png index 7891573b3d..fe7b50f2e8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_10_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be931ad71b0562cd96456124c987ed79709f3ec7c55d736f4f71da1fdb036309 -size 56863 +oid sha256:611527e8c0bce7a9fb48b5eff42170cb9da5f40cf020c78209dee84615d300ba +size 56514 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_11_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_11_en.png index 2e5b1dc647..dd8f8d2a66 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_11_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f3f24723f76f444a6503ee01399298ae4258a190cd6657dd1bf5308ea528d55f -size 72035 +oid sha256:77b28be1b95709082e0302cca5b65c2d57ab48786e6a22facb8c86116f058b11 +size 71667 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_1_en.png index 584285c1be..be45e720dd 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cfb33cec43d32917cb8f59e012244fa0c8824421cbedac516e002862f28f93db -size 83506 +oid sha256:31875ed4c6d9443418899de47eeb5b61d2246d8321e83579681cfa152ff4a056 +size 83174 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_2_en.png index 7414d959a4..e7efad21d5 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:94d01e112a860c145c6c09d1d3a9bf948fe73e7b939d7f602704a3b4853de9f5 -size 60076 +oid sha256:5cc4f388f677bf04259344b5bfc9033ef6a4a76ad161012abcd0239ef520a8d8 +size 59670 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_3_en.png index b6e7b4e2a5..638e2761d7 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b064ba533e8bfa225f0548fc6e13d811a0ab0dddcd500cfbb72ed199a268c1b9 -size 59202 +oid sha256:e57a614424396e36229d7877a0bb222a51e9e955a42e180acc1d43c048684da2 +size 58787 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_4_en.png index c223cabf8f..6c510542bb 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e0cdd88f5776e92aa0063c09f4c7c1c099d2b8cb0f4a13066925c1ca105b37a -size 66800 +oid sha256:4546f32ff1eeae0a923d473491926a6037eddf59c0c88c516c504b9875874c8a +size 66402 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_5_en.png index 042c2792ce..d472874510 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fa0e313a3d429ddfba85b62f4d855489cb5cca62bc6693dcaa33f4d0ac622b85 -size 101888 +oid sha256:acd25c00d9127c2c78f48b22296a5ab33f1e16ec5698c93dd29adc9172467aac +size 101706 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_6_en.png index d1b0e795b5..48f0ad6881 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e3210208348505b93f4d673977f237bd7859b054699c1d66ece28591d20640b -size 58145 +oid sha256:d066e82aba5742c7e84a260de0e4578bb3bc8faac097225271cfdb14cd52f5f0 +size 57816 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_7_en.png index a014be7a5a..f8f0337792 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0c9e887cd48c5aee046649a2ca5949abe763363659e29dd523869dc615e98f2 -size 58186 +oid sha256:2c037b4de113387ac7e28d256d28ce9308be184b4ebff988ed96760623acfafd +size 57776 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_8_en.png index 4f07ac8b8f..6278391831 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d5febf4de05a7794160b567dec3b00e818a6e3d4d7830d3dba97f72498aa5a3c -size 67172 +oid sha256:81883325e2e450c33f38b3143c43a3fa3d659a210ff6e9ef40de71b1d0522f50 +size 66781 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_9_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_9_en.png index 2d5e2c39da..07c20262cd 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f213b779238ac8536e4f0115d8de444a21c3b8ce31aac8aebd5415d8f80504a4 -size 57501 +oid sha256:b6b5808bdde620cac3fe980aee1f8162a854965113d99d074442a2baa818d712 +size 57198 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_0_en.png index 362033d451..c41f41d043 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:27cc53e2336c9ccf90d97d2aed98f94f83315dcb9fe4bd489958b8df2366262c -size 70207 +oid sha256:7174f340d6e16be525da9a06270f07abc7496fc6c41a5670a71bd1b23f4a72c2 +size 69957 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_10_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_10_en.png index aa712c48ec..5d3b894ebc 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_10_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bd6c1a82daedd923982d81956924eeaa37857d54dee55f31fda4520071b4f66b -size 53675 +oid sha256:dbb0c22089c063c18042c8778c1b69edb441af8071f7dfb9a3ffdaeab4ba4c71 +size 53340 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_11_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_11_en.png index 804b238d8d..61a6581ec4 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_11_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ffce97ff7f1d6a2641d761aad6457883babe333075a454b5860d266719bdb3f9 -size 68525 +oid sha256:e9a561ca3aa61371cc1bd0c1b51508469a0343de3bd8f404055041591c60d42f +size 68231 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_1_en.png index ec221ef43a..7c25276223 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:58718344783197c680e18a7a3c97a3531293b6e2442ca5701d4605ecb567e70a -size 79724 +oid sha256:c739add59dd40303652319c0b789ec4d0d2224cc097598ef717e8fb101031ad7 +size 79471 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_2_en.png index 8369183006..a3fcdfc5f0 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d84207a6b1bf4815d2515974c721a0ba26fb406a31e96f3566b90e548c7bf49 -size 57077 +oid sha256:61d30aca9b213938462659c2dcb8bc8ef10b28821f8492715003262f4cae142c +size 56761 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_3_en.png index 7cd4c1a580..e10bdbdf26 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2056d90bbf2676fae7d88b326c496aae95a1c31924c8b5d13ca363f486c72968 -size 56206 +oid sha256:a84be2101e96ab4e7566e4fe1af14243466c7dba7e540fb7502acbbf70d33aaa +size 55835 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_4_en.png index 6b23cf2db8..29ac8cd6f3 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5bca7347130b0b449ef2c917359753660446e4be7d2d4a13323489c63dbf28a4 -size 63589 +oid sha256:9f168b3af0e3679a2d7a5f2baddc1523c1afe9dc433d25c267572097d9ab7d15 +size 63284 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_5_en.png index f388e96dac..f405aa34cb 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4fffdb6a8f159af3104d9f2e8edbc11395854f6ac6a558e303d20c79ea91857b -size 98128 +oid sha256:41799e15fad757a976b278d5d722fc755dcfdda3ad0880a1f4058a337b96d470 +size 97907 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_6_en.png index 37890657c9..9b72a8c4fd 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:534628991ee750aa4f4b4e3edda1ea26cf648eb74d06610d4207266b659d4421 -size 55083 +oid sha256:c3d27473a9375edc590aeedf8dcb188e1c23785a8f8c62900aa439c33f86ca13 +size 54772 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_7_en.png index cb402d830b..7a87bb30d2 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9de6ef77a1d574da317c3d1c3258f3068e79613387d0bc32e925f516338da9e9 -size 55092 +oid sha256:52a125268370bb369c53271f204b5f942da75f01639a39ae6b935d3f3ab4ef00 +size 54705 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_8_en.png index 06a6ee8488..d0e908caff 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:350c89c3491e08a80f9910121140d56f2cf28fdc0b8f6ced380d1ec34b116c52 -size 63975 +oid sha256:56d531df665ae3baa6400b0fc83b1fe756ec0f452ddd2423457ed26ea18aed98 +size 63628 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_9_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_9_en.png index e698f48771..29df858167 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8659bbb89e5c26caea7f1d759e1c9bbd6ecedc525dcc2cb6b1cc844c835aeec1 -size 54455 +oid sha256:d4e66a9e00b7aa05294522055cf6a89e68d409496c1a24ef0b6d841221f6883b +size 54122 diff --git a/tests/uitests/src/test/snapshots/images/services.apperror.impl_AppErrorView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/services.apperror.api_AppErrorView_Day_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/services.apperror.impl_AppErrorView_Day_0_en.png rename to tests/uitests/src/test/snapshots/images/services.apperror.api_AppErrorView_Day_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/services.apperror.impl_AppErrorView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/services.apperror.api_AppErrorView_Night_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/services.apperror.impl_AppErrorView_Night_0_en.png rename to tests/uitests/src/test/snapshots/images/services.apperror.api_AppErrorView_Night_0_en.png diff --git a/tools/localazy/checkForbiddenTerms.py b/tools/localazy/checkForbiddenTerms.py index e190fcea68..123246ffd0 100755 --- a/tools/localazy/checkForbiddenTerms.py +++ b/tools/localazy/checkForbiddenTerms.py @@ -31,6 +31,9 @@ forbiddenTerms = { # We explicitly want to mention Element Pro in these 2: "screen_change_server_error_element_pro_required_title", "screen_change_server_error_element_pro_required_message", + # Contains "Element Classic" + "screen_missing_key_backup_open_element_classic", + "screen_missing_key_backup_step_1", ] } diff --git a/tools/localazy/config.json b/tools/localazy/config.json index d3e44b7c08..b38268e5f7 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -170,6 +170,8 @@ "name" : ":features:login:impl", "includeRegex" : [ "screen_onboarding_.*", + "screen\\.onboarding\\..*", + "screen\\.missing_key_backup\\..*", "screen_login_.*", "screen_server_confirmation_.*", "screen_change_server_.*",