Merge branch 'develop' into renovate/io.nlopez.compose.rules-detekt-0.x

This commit is contained in:
Benoit Marty 2024-05-28 08:59:36 +02:00 committed by GitHub
commit 8cd392a2b8
587 changed files with 9299 additions and 2438 deletions

View file

@ -11,7 +11,7 @@ jobs:
- run: | - run: |
npm install --save-dev @babel/plugin-transform-flow-strip-types npm install --save-dev @babel/plugin-transform-flow-strip-types
- name: Danger - name: Danger
uses: danger/danger-js@12.2.0 uses: danger/danger-js@12.3.0
with: with:
args: "--dangerfile ./tools/danger/dangerfile.js" args: "--dangerfile ./tools/danger/dangerfile.js"
env: env:

View file

@ -10,7 +10,7 @@ on:
# Enrich gradle.properties for CI/CD # Enrich gradle.properties for CI/CD
env: env:
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx6g -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -XX:+UseParallelGC GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx6g -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -XX:+UseParallelGC
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 8 --no-daemon --warn CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 8 --no-daemon
jobs: jobs:
checkScript: checkScript:
@ -33,12 +33,13 @@ jobs:
- name: Search for invalid screenshot files - name: Search for invalid screenshot files
run: ./tools/test/checkInvalidScreenshots.py run: ./tools/test/checkInvalidScreenshots.py
check: # Code checks
name: Project Check Suite konsist:
name: Konsist tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Allow all jobs on main and develop. Just one per PR. # Allow all jobs on main and develop. Just one per PR.
concurrency: concurrency:
group: ${{ github.ref == 'refs/heads/main' && format('check-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-develop-{0}', github.sha) || format('check-{0}', github.ref) }} group: ${{ github.ref == 'refs/heads/main' && format('check-konsist-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-konsist-develop-{0}', github.sha) || format('check-konsist-{0}', github.ref) }}
cancel-in-progress: true cancel-in-progress: true
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -55,8 +56,40 @@ jobs:
uses: gradle/actions/setup-gradle@v3 uses: gradle/actions/setup-gradle@v3
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run code quality check suite - name: Run Konsist tests
run: ./gradlew runQualityChecks $CI_GRADLE_ARG_PROPERTIES run: ./gradlew :tests:konsist:testDebugUnitTest $CI_GRADLE_ARG_PROPERTIES --no-daemon
- name: Upload reports
if: always()
uses: actions/upload-artifact@v4
with:
name: konsist-report
path: |
**/build/reports/**/*.*
lint:
name: Android lint check
runs-on: ubuntu-latest
# Allow all jobs on main and develop. Just one per PR.
concurrency:
group: ${{ github.ref == 'refs/heads/main' && format('check-lint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-lint-develop-{0}', github.sha) || format('check-lint-{0}', github.ref) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v4
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
- name: Use JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v3
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run lint
run: ./gradlew :app:lintGplayDebug :app:lintFdroidDebug $CI_GRADLE_ARG_PROPERTIES
- name: Upload reports - name: Upload reports
if: always() if: always()
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@ -64,6 +97,108 @@ jobs:
name: linting-report name: linting-report
path: | path: |
**/build/reports/**/*.* **/build/reports/**/*.*
detekt:
name: Detekt checks
runs-on: ubuntu-latest
# Allow all jobs on main and develop. Just one per PR.
concurrency:
group: ${{ github.ref == 'refs/heads/main' && format('check-detekt-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-detekt-develop-{0}', github.sha) || format('check-detekt-{0}', github.ref) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v4
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
- name: Use JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v3
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run Detekt
run: ./gradlew detekt $CI_GRADLE_ARG_PROPERTIES --no-daemon
- name: Upload reports
if: always()
uses: actions/upload-artifact@v4
with:
name: detekt-report
path: |
**/build/reports/**/*.*
ktlint:
name: Ktlint checks
runs-on: ubuntu-latest
# Allow all jobs on main and develop. Just one per PR.
concurrency:
group: ${{ github.ref == 'refs/heads/main' && format('check-ktlint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-ktlint-develop-{0}', github.sha) || format('check-ktlint-{0}', github.ref) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v4
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
- name: Use JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v3
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run Ktlint check
run: ./gradlew ktlintCheck $CI_GRADLE_ARG_PROPERTIES
- name: Upload reports
if: always()
uses: actions/upload-artifact@v4
with:
name: ktlint-report
path: |
**/build/reports/**/*.*
knit:
name: Knit checks
runs-on: ubuntu-latest
# Allow all jobs on main and develop. Just one per PR.
concurrency:
group: ${{ github.ref == 'refs/heads/main' && format('check-knit-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-knit-develop-{0}', github.sha) || format('check-knit-{0}', github.ref) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v4
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
- name: Use JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v3
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run Knit
run: ./gradlew knitCheck $CI_GRADLE_ARG_PROPERTIES
upload_reports:
name: Project Check Suite
runs-on: ubuntu-latest
needs: [konsist, lint, ktlint, detekt]
steps:
- uses: actions/checkout@v4
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
- name: Download reports from previous jobs
uses: actions/download-artifact@v4
- name: Prepare Danger - name: Prepare Danger
if: always() if: always()
run: | run: |
@ -72,7 +207,7 @@ jobs:
yarn add danger-plugin-lint-report --dev yarn add danger-plugin-lint-report --dev
- name: Danger lint - name: Danger lint
if: always() if: always()
uses: danger/danger-js@12.2.0 uses: danger/danger-js@12.3.0
with: with:
args: "--dangerfile ./tools/danger/dangerfile-lint.js" args: "--dangerfile ./tools/danger/dangerfile-lint.js"
env: env:

View file

@ -1,4 +1,4 @@
name: Create release App Bundle name: Create release App Bundle and APKs
on: on:
workflow_dispatch: workflow_dispatch:
@ -11,11 +11,11 @@ env:
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 8 --no-daemon CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 8 --no-daemon
jobs: jobs:
release: gplay:
name: Create App Bundle name: Create App Bundle (Gplay)
runs-on: ubuntu-latest runs-on: ubuntu-latest
concurrency: concurrency:
group: ${{ github.ref == 'refs/head/main' && format('build-release-main-{0}', github.sha) }} group: ${{ github.ref == 'refs/head/main' && format('build-release-main-gplay-{0}', github.sha) }}
cancel-in-progress: true cancel-in-progress: true
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -38,3 +38,31 @@ jobs:
name: elementx-app-gplay-bundle-unsigned name: elementx-app-gplay-bundle-unsigned
path: | path: |
app/build/outputs/bundle/gplayRelease/app-gplay-release.aab app/build/outputs/bundle/gplayRelease/app-gplay-release.aab
fdroid:
name: Create APKs (FDroid)
runs-on: ubuntu-latest
concurrency:
group: ${{ github.ref == 'refs/head/main' && format('build-release-main-fdroid-{0}', github.sha) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v4
- name: Use JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v3
- name: Create APKs
env:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
run: ./gradlew assembleFdroidRelease $CI_GRADLE_ARG_PROPERTIES
- name: Upload apks as artifact
uses: actions/upload-artifact@v4
with:
name: elementx-app-fdroid-apks-unsigned
path: |
app/build/outputs/apk/fdroid/release/*.apk

View file

@ -3,7 +3,9 @@
<words> <words>
<w>backstack</w> <w>backstack</w>
<w>blurhash</w> <w>blurhash</w>
<w>fdroid</w>
<w>ftue</w> <w>ftue</w>
<w>gplay</w>
<w>homeserver</w> <w>homeserver</w>
<w>konsist</w> <w>konsist</w>
<w>kover</w> <w>kover</w>

View file

@ -2,7 +2,7 @@ appId: ${MAESTRO_APP_ID}
--- ---
- takeScreenshot: build/maestro/510-Timeline - takeScreenshot: build/maestro/510-Timeline
- tapOn: - tapOn:
id: "rich_text_editor" id: "text_editor"
- inputText: "Hello world!" - inputText: "Hello world!"
- tapOn: "Send" - tapOn: "Send"
- hideKeyboard - hideKeyboard

View file

@ -34,7 +34,7 @@ appId: ${MAESTRO_APP_ID}
- tapOn: - tapOn:
text: "Advanced settings" text: "Advanced settings"
- assertVisible: "Rich text editor" - assertVisible: "View source"
- back - back
- tapOn: - tapOn:

View file

@ -1,3 +1,26 @@
Changes in Element X v0.4.13 (2024-05-22)
=========================================
Features ✨
----------
- Add plain text editor based on Markdown input. ([#2840](https://github.com/element-hq/element-x-android/issues/2840))
Bugfixes 🐛
----------
- Use members display names for their membership state events. ([#2286](https://github.com/element-hq/element-x-android/issues/2286))
- Make sure explicit links in messages take priority over links found by linkification (urls, emails, phone numbers, etc.) ([#2291](https://github.com/element-hq/element-x-android/issues/2291))
- Fix modal contents overlapping screen lock pin. ([#2692](https://github.com/element-hq/element-x-android/issues/2692))
- Fix a crash when trying to create an `EncryptedFile` in Android 6. ([#2846](https://github.com/element-hq/element-x-android/issues/2846))
- Session falsely displayed as 'verified' with no internet connection. ([#2884](https://github.com/element-hq/element-x-android/issues/2884))
Other changes
-------------
- Allow configuring push notification provider ([#2340](https://github.com/element-hq/element-x-android/issues/2340))
- UX cleanup: reorder text composer actions to prioritise camera ones. ([#2803](https://github.com/element-hq/element-x-android/issues/2803))
- Translation added into Portuguese and Simplified Chinese ([#2834](https://github.com/element-hq/element-x-android/issues/2834))
- Use via parameters when joining a room from permalink. ([#2843](https://github.com/element-hq/element-x-android/issues/2843))
Changes in Element X v0.4.12 (2024-05-13) Changes in Element X v0.4.12 (2024-05-13)
========================================= =========================================

View file

@ -36,6 +36,6 @@ class ElementXApplication : Application(), DaggerComponentOwner {
initializeComponent(TracingInitializer::class.java) initializeComponent(TracingInitializer::class.java)
initializeComponent(CacheCleanerInitializer::class.java) initializeComponent(CacheCleanerInitializer::class.java)
} }
logApplicationInfo() logApplicationInfo(this)
} }
} }

View file

@ -32,6 +32,9 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.bumble.appyx.core.integration.NodeHost import com.bumble.appyx.core.integration.NodeHost
import com.bumble.appyx.core.integrationpoint.NodeActivity import com.bumble.appyx.core.integrationpoint.NodeActivity
import com.bumble.appyx.core.plugin.NodeReadyObserver import com.bumble.appyx.core.plugin.NodeReadyObserver
@ -39,13 +42,16 @@ import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.theme.Theme import io.element.android.compound.theme.Theme
import io.element.android.compound.theme.isDark import io.element.android.compound.theme.isDark
import io.element.android.compound.theme.mapToTheme import io.element.android.compound.theme.mapToTheme
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
import io.element.android.features.lockscreen.api.LockScreenLockState
import io.element.android.features.lockscreen.api.LockScreenService
import io.element.android.features.lockscreen.api.handleSecureFlag import io.element.android.features.lockscreen.api.handleSecureFlag
import io.element.android.features.lockscreen.api.isLocked
import io.element.android.libraries.architecture.bindings import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.designsystem.utils.snackbar.LocalSnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.LocalSnackbarDispatcher
import io.element.android.x.di.AppBindings import io.element.android.x.di.AppBindings
import io.element.android.x.intent.SafeUriHandler import io.element.android.x.intent.SafeUriHandler
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
private val loggerTag = LoggerTag("MainActivity") private val loggerTag = LoggerTag("MainActivity")
@ -59,27 +65,13 @@ class MainActivity : NodeActivity() {
installSplashScreen() installSplashScreen()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
appBindings = bindings() appBindings = bindings()
appBindings.lockScreenService().handleSecureFlag(this) setupLockManagement(appBindings.lockScreenService(), appBindings.lockScreenEntryPoint())
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
MainContent(appBindings) MainContent(appBindings)
} }
} }
@Deprecated("")
override fun onBackPressed() {
// If the app is locked, we need to intercept onBackPressed before it goes to OnBackPressedDispatcher.
// Indeed, otherwise we would need to trick Appyx backstack management everywhere.
// Without this trick, we would get pop operations on the hidden backstack.
if (appBindings.lockScreenService().isLocked) {
// Do not kill the app in this case, just go to background.
moveTaskToBack(false)
} else {
@Suppress("DEPRECATION")
super.onBackPressed()
}
}
@Composable @Composable
private fun MainContent(appBindings: AppBindings) { private fun MainContent(appBindings: AppBindings) {
val theme by remember { val theme by remember {
@ -96,8 +88,8 @@ class MainActivity : NodeActivity() {
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(MaterialTheme.colorScheme.background), .background(MaterialTheme.colorScheme.background),
) { ) {
if (migrationState.migrationAction.isSuccess()) { if (migrationState.migrationAction.isSuccess()) {
MainNodeHost() MainNodeHost()
@ -131,6 +123,22 @@ class MainActivity : NodeActivity() {
} }
} }
private fun setupLockManagement(
lockScreenService: LockScreenService,
lockScreenEntryPoint: LockScreenEntryPoint
) {
lockScreenService.handleSecureFlag(this)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
lockScreenService.lockState.collect { state ->
if (state == LockScreenLockState.Locked) {
startActivity(lockScreenEntryPoint.pinUnlockIntent(this@MainActivity))
}
}
}
}
}
/** /**
* Called when: * Called when:
* - the launcher icon is clicked (if the app is already running); * - the launcher icon is clicked (if the app is already running);

View file

@ -18,6 +18,7 @@ package io.element.android.x.di
import com.squareup.anvil.annotations.ContributesTo import com.squareup.anvil.annotations.ContributesTo
import io.element.android.features.api.MigrationEntryPoint import io.element.android.features.api.MigrationEntryPoint
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
import io.element.android.features.lockscreen.api.LockScreenService import io.element.android.features.lockscreen.api.LockScreenService
import io.element.android.features.preferences.api.store.AppPreferencesStore import io.element.android.features.preferences.api.store.AppPreferencesStore
import io.element.android.features.rageshake.api.reporter.BugReporter import io.element.android.features.rageshake.api.reporter.BugReporter
@ -38,4 +39,6 @@ interface AppBindings {
fun preferencesStore(): AppPreferencesStore fun preferencesStore(): AppPreferencesStore
fun migrationEntryPoint(): MigrationEntryPoint fun migrationEntryPoint(): MigrationEntryPoint
fun lockScreenEntryPoint(): LockScreenEntryPoint
} }

View file

@ -26,6 +26,7 @@ import dagger.Provides
import io.element.android.appconfig.ApplicationConfig import io.element.android.appconfig.ApplicationConfig
import io.element.android.features.messages.impl.timeline.components.customreaction.DefaultEmojibaseProvider import io.element.android.features.messages.impl.timeline.components.customreaction.DefaultEmojibaseProvider
import io.element.android.features.messages.impl.timeline.components.customreaction.EmojibaseProvider import io.element.android.features.messages.impl.timeline.components.customreaction.EmojibaseProvider
import io.element.android.libraries.androidutils.system.getVersionCodeFromManifest
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.meta.BuildType import io.element.android.libraries.core.meta.BuildType
@ -87,7 +88,7 @@ object AppModule {
// TODO EAx Config.LOW_PRIVACY_LOG_ENABLE, // TODO EAx Config.LOW_PRIVACY_LOG_ENABLE,
lowPrivacyLoggingEnabled = false, lowPrivacyLoggingEnabled = false,
versionName = BuildConfig.VERSION_NAME, versionName = BuildConfig.VERSION_NAME,
versionCode = BuildConfig.VERSION_CODE, versionCode = context.getVersionCodeFromManifest(),
gitRevision = BuildConfig.GIT_REVISION, gitRevision = BuildConfig.GIT_REVISION,
gitBranchName = BuildConfig.GIT_BRANCH_NAME, gitBranchName = BuildConfig.GIT_BRANCH_NAME,
flavorDescription = BuildConfig.FLAVOR_DESCRIPTION, flavorDescription = BuildConfig.FLAVOR_DESCRIPTION,

View file

@ -16,17 +16,19 @@
package io.element.android.x.info package io.element.android.x.info
import android.content.Context
import io.element.android.libraries.androidutils.system.getVersionCodeFromManifest
import io.element.android.x.BuildConfig import io.element.android.x.BuildConfig
import timber.log.Timber import timber.log.Timber
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
fun logApplicationInfo() { fun logApplicationInfo(context: Context) {
val appVersion = buildString { val appVersion = buildString {
append(BuildConfig.VERSION_NAME) append(BuildConfig.VERSION_NAME)
append(" (") append(" (")
append(BuildConfig.VERSION_CODE) append(context.getVersionCodeFromManifest())
append(") - ") append(") - ")
append(BuildConfig.BUILD_TYPE) append(BuildConfig.BUILD_TYPE)
append(" / ") append(" / ")

View file

@ -10,7 +10,8 @@
<locale android:name="hu"/> <locale android:name="hu"/>
<locale android:name="in"/> <locale android:name="in"/>
<locale android:name="it"/> <locale android:name="it"/>
<locale android:name="pt"/> <locale android:name="ka"/>
<locale android:name="pt_BR"/>
<locale android:name="ro"/> <locale android:name="ro"/>
<locale android:name="ru"/> <locale android:name="ru"/>
<locale android:name="sk"/> <locale android:name="sk"/>

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2023 New Vector Ltd * Copyright (c) 2024 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,10 +14,11 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.libraries.designsystem.components.preferences package io.element.android.appconfig
import androidx.compose.ui.unit.dp object MessageComposerConfig {
/**
internal val preferenceMinHeightOnlyTitle = 56.dp * Enable the rich text editing in the composer.
internal val preferenceMinHeight = 56.dp */
internal val preferencePaddingHorizontal = 16.dp const val ENABLE_RICH_TEXT_EDITING = true
}

View file

@ -47,9 +47,6 @@ import io.element.android.features.createroom.api.CreateRoomEntryPoint
import io.element.android.features.ftue.api.FtueEntryPoint import io.element.android.features.ftue.api.FtueEntryPoint
import io.element.android.features.ftue.api.state.FtueService import io.element.android.features.ftue.api.state.FtueService
import io.element.android.features.ftue.api.state.FtueState import io.element.android.features.ftue.api.state.FtueState
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
import io.element.android.features.lockscreen.api.LockScreenLockState
import io.element.android.features.lockscreen.api.LockScreenService
import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.preferences.api.PreferencesEntryPoint import io.element.android.features.preferences.api.PreferencesEntryPoint
@ -100,8 +97,6 @@ class LoggedInFlowNode @AssistedInject constructor(
private val coroutineScope: CoroutineScope, private val coroutineScope: CoroutineScope,
private val networkMonitor: NetworkMonitor, private val networkMonitor: NetworkMonitor,
private val ftueService: FtueService, private val ftueService: FtueService,
private val lockScreenEntryPoint: LockScreenEntryPoint,
private val lockScreenStateService: LockScreenService,
private val roomDirectoryEntryPoint: RoomDirectoryEntryPoint, private val roomDirectoryEntryPoint: RoomDirectoryEntryPoint,
private val matrixClient: MatrixClient, private val matrixClient: MatrixClient,
snackbarDispatcher: SnackbarDispatcher, snackbarDispatcher: SnackbarDispatcher,
@ -111,7 +106,7 @@ class LoggedInFlowNode @AssistedInject constructor(
savedStateMap = buildContext.savedStateMap, savedStateMap = buildContext.savedStateMap,
), ),
permanentNavModel = PermanentNavModel( permanentNavModel = PermanentNavModel(
navTargets = setOf(NavTarget.LoggedInPermanent, NavTarget.LockPermanent), navTargets = setOf(NavTarget.LoggedInPermanent),
savedStateMap = buildContext.savedStateMap, savedStateMap = buildContext.savedStateMap,
), ),
buildContext = buildContext, buildContext = buildContext,
@ -189,9 +184,6 @@ class LoggedInFlowNode @AssistedInject constructor(
@Parcelize @Parcelize
data object LoggedInPermanent : NavTarget data object LoggedInPermanent : NavTarget
@Parcelize
data object LockPermanent : NavTarget
@Parcelize @Parcelize
data object RoomList : NavTarget data object RoomList : NavTarget
@ -235,11 +227,6 @@ class LoggedInFlowNode @AssistedInject constructor(
NavTarget.LoggedInPermanent -> { NavTarget.LoggedInPermanent -> {
createNode<LoggedInNode>(buildContext) createNode<LoggedInNode>(buildContext)
} }
NavTarget.LockPermanent -> {
lockScreenEntryPoint.nodeBuilder(this, buildContext)
.target(LockScreenEntryPoint.Target.Unlock)
.build()
}
NavTarget.RoomList -> { NavTarget.RoomList -> {
val callback = object : RoomListEntryPoint.Callback { val callback = object : RoomListEntryPoint.Callback {
override fun onRoomClicked(roomId: RoomId) { override fun onRoomClicked(roomId: RoomId) {
@ -430,15 +417,11 @@ class LoggedInFlowNode @AssistedInject constructor(
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
Box(modifier = modifier) { Box(modifier = modifier) {
val lockScreenState by lockScreenStateService.lockState.collectAsState()
val ftueState by ftueService.state.collectAsState() val ftueState by ftueService.state.collectAsState()
BackstackView() BackstackView()
if (ftueState is FtueState.Complete) { if (ftueState is FtueState.Complete) {
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent) PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
} }
if (lockScreenState == LockScreenLockState.Locked) {
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LockPermanent)
}
} }
} }

View file

@ -36,6 +36,7 @@ import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatu
import io.element.android.libraries.push.api.PushService import io.element.android.libraries.push.api.PushService
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class LoggedInPresenter @Inject constructor( class LoggedInPresenter @Inject constructor(
@ -55,10 +56,26 @@ class LoggedInPresenter @Inject constructor(
LaunchedEffect(isVerified) { LaunchedEffect(isVerified) {
if (isVerified) { if (isVerified) {
// Ensure pusher is registered // Ensure pusher is registered
// TODO Manually select push provider for now val currentPushProvider = pushService.getCurrentPushProvider()
val pushProvider = pushService.getAvailablePushProviders().firstOrNull() ?: return@LaunchedEffect val result = if (currentPushProvider == null) {
val distributor = pushProvider.getDistributors().firstOrNull() ?: return@LaunchedEffect // Register with the first available push provider
pushService.registerWith(matrixClient, pushProvider, distributor) val pushProvider = pushService.getAvailablePushProviders().firstOrNull() ?: return@LaunchedEffect
val distributor = pushProvider.getDistributors().firstOrNull() ?: return@LaunchedEffect
pushService.registerWith(matrixClient, pushProvider, distributor)
} else {
val currentPushDistributor = currentPushProvider.getCurrentDistributor(matrixClient)
if (currentPushDistributor == null) {
// Register with the first available distributor
val distributor = currentPushProvider.getDistributors().firstOrNull() ?: return@LaunchedEffect
pushService.registerWith(matrixClient, currentPushProvider, distributor)
} else {
// Re-register with the current distributor
pushService.registerWith(matrixClient, currentPushProvider, currentPushDistributor)
}
}
result.onFailure {
Timber.e(it, "Failed to register pusher")
}
} }
} }

View file

@ -20,21 +20,21 @@ import com.bumble.appyx.core.state.MutableSavedStateMapImpl
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
class MatrixClientsHolderTest { class MatrixClientsHolderTest {
@Test @Test
fun `test getOrNull`() { fun `test getOrNull`() {
val fakeAuthenticationService = FakeAuthenticationService() val fakeAuthenticationService = FakeMatrixAuthenticationService()
val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService) val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService)
assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isNull() assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isNull()
} }
@Test @Test
fun `test getOrRestore`() = runTest { fun `test getOrRestore`() = runTest {
val fakeAuthenticationService = FakeAuthenticationService() val fakeAuthenticationService = FakeMatrixAuthenticationService()
val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService) val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService)
val fakeMatrixClient = FakeMatrixClient() val fakeMatrixClient = FakeMatrixClient()
fakeAuthenticationService.givenMatrixClient(fakeMatrixClient) fakeAuthenticationService.givenMatrixClient(fakeMatrixClient)
@ -47,7 +47,7 @@ class MatrixClientsHolderTest {
@Test @Test
fun `test remove`() = runTest { fun `test remove`() = runTest {
val fakeAuthenticationService = FakeAuthenticationService() val fakeAuthenticationService = FakeMatrixAuthenticationService()
val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService) val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService)
val fakeMatrixClient = FakeMatrixClient() val fakeMatrixClient = FakeMatrixClient()
fakeAuthenticationService.givenMatrixClient(fakeMatrixClient) fakeAuthenticationService.givenMatrixClient(fakeMatrixClient)
@ -60,7 +60,7 @@ class MatrixClientsHolderTest {
@Test @Test
fun `test remove all`() = runTest { fun `test remove all`() = runTest {
val fakeAuthenticationService = FakeAuthenticationService() val fakeAuthenticationService = FakeMatrixAuthenticationService()
val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService) val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService)
val fakeMatrixClient = FakeMatrixClient() val fakeMatrixClient = FakeMatrixClient()
fakeAuthenticationService.givenMatrixClient(fakeMatrixClient) fakeAuthenticationService.givenMatrixClient(fakeMatrixClient)
@ -73,7 +73,7 @@ class MatrixClientsHolderTest {
@Test @Test
fun `test save and restore`() = runTest { fun `test save and restore`() = runTest {
val fakeAuthenticationService = FakeAuthenticationService() val fakeAuthenticationService = FakeMatrixAuthenticationService()
val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService) val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService)
val fakeMatrixClient = FakeMatrixClient() val fakeMatrixClient = FakeMatrixClient()
fakeAuthenticationService.givenMatrixClient(fakeMatrixClient) fakeAuthenticationService.givenMatrixClient(fakeMatrixClient)

View file

@ -33,6 +33,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_THREAD_ID import io.element.android.libraries.matrix.test.A_THREAD_ID
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.tests.testutils.lambda.lambdaError
import org.junit.Assert.assertThrows import org.junit.Assert.assertThrows
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -229,7 +230,7 @@ class IntentResolverTest {
} }
private fun createIntentResolver( private fun createIntentResolver(
permalinkParserResult: () -> PermalinkData = { throw NotImplementedError() } permalinkParserResult: () -> PermalinkData = { lambdaError() }
): IntentResolver { ): IntentResolver {
return IntentResolver( return IntentResolver(
deeplinkParser = DeeplinkParser(), deeplinkParser = DeeplinkParser(),

View file

@ -1 +0,0 @@
UX cleanup: reorder text composer actions to prioritise camera ones.

1
changelog.d/2809.bugfix Normal file
View file

@ -0,0 +1 @@
Render selected/deselected room list filters on top

View file

@ -1 +0,0 @@
Translation added into Portuguese and Simplified Chinese

View file

@ -1 +0,0 @@
Use via parameters when joining a room from permalink.

View file

@ -1 +0,0 @@
Fix a crash when trying to create an `EncryptedFile` in Android 6.

1
changelog.d/2893.misc Normal file
View file

@ -0,0 +1 @@
BugReporting | Add public device keys to rageshakes

1
changelog.d/2896.bugfix Normal file
View file

@ -0,0 +1 @@
Set auto captilization, multiline and autocompletion flags for the markdown EditText.

1
changelog.d/2898.bugfix Normal file
View file

@ -0,0 +1 @@
Restoree Markdown text input contents when returning to the room screen.

1
changelog.d/2917.bugfix Normal file
View file

@ -0,0 +1 @@
Fixed sending rich content from android keyboards on the markdown text input

View file

@ -0,0 +1,2 @@
Main changes in this version: Add plain text editor based on Markdown input.
Full changelog: https://github.com/element-hq/element-x-android/releases

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_settings_help_us_improve">"გააზიარეთ ანონიმური გამოყენების მონაცემები, რათა დაგვეხმაროთ პრობლემების იდენტიფიცირებაში."</string>
<string name="screen_analytics_settings_read_terms">"შეგიძლიათ, წაიკითხოთ ჩვენი ყველა პირობა %1$s."</string>
<string name="screen_analytics_settings_read_terms_content_link">"აქ"</string>
<string name="screen_analytics_settings_share_data">"გააზიარეთ ანალიტიკური მონაცემები"</string>
</resources>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_data_usage">"ჩვენ არ ჩავწერთ და არ დავაფიქსირებთ პერსონალურ მონაცემებს"</string>
<string name="screen_analytics_prompt_help_us_improve">"გააზიარეთ ანონიმური გამოყენების მონაცემები, რათა დაგვეხმაროთ პრობლემების იდენტიფიცირებაში."</string>
<string name="screen_analytics_prompt_read_terms">"შეგიძლიათ, წაიკითხოთ ჩვენი ყველა პირობა %1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"აქ"</string>
<string name="screen_analytics_prompt_settings">"ამის გამორთვა ნებისმიერ დროს შეგიძლიათ"</string>
<string name="screen_analytics_prompt_third_party_sharing">"თქვენს მონაცემებს მესამე პირს არ გადავცემთ"</string>
<string name="screen_analytics_prompt_title">"დაგვეხმარეთ, გავაუმჯობესოთ %1$s"</string>
</resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="call_foreground_service_channel_title_android">"მიმდინარე ზარი"</string>
<string name="call_foreground_service_message_android">"დააწკაპუნეთ ზარში დასაბრუნებლად"</string>
<string name="call_foreground_service_title_android">"☎️ ზარი მიმდინარეობს"</string>
</resources>

View file

@ -30,7 +30,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
import io.element.android.libraries.network.useragent.UserAgentProvider import io.element.android.libraries.network.useragent.UserAgentProvider
import io.element.android.services.toolbox.api.systemclock.SystemClock import io.element.android.services.toolbox.api.systemclock.SystemClock
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
@ -68,7 +68,7 @@ class CallScreenPresenterTest {
@Test @Test
fun `present - with CallType RoomCall loads URL and runs WidgetDriver`() = runTest { fun `present - with CallType RoomCall loads URL and runs WidgetDriver`() = runTest {
val widgetDriver = FakeWidgetDriver() val widgetDriver = FakeMatrixWidgetDriver()
val widgetProvider = FakeCallWidgetProvider(widgetDriver) val widgetProvider = FakeCallWidgetProvider(widgetDriver)
val presenter = createCallScreenPresenter( val presenter = createCallScreenPresenter(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
@ -91,7 +91,7 @@ class CallScreenPresenterTest {
@Test @Test
fun `present - set message interceptor, send and receive messages`() = runTest { fun `present - set message interceptor, send and receive messages`() = runTest {
val widgetDriver = FakeWidgetDriver() val widgetDriver = FakeMatrixWidgetDriver()
val presenter = createCallScreenPresenter( val presenter = createCallScreenPresenter(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
widgetDriver = widgetDriver, widgetDriver = widgetDriver,
@ -119,7 +119,7 @@ class CallScreenPresenterTest {
@Test @Test
fun `present - hang up event closes the screen and stops the widget driver`() = runTest(UnconfinedTestDispatcher()) { fun `present - hang up event closes the screen and stops the widget driver`() = runTest(UnconfinedTestDispatcher()) {
val navigator = FakeCallScreenNavigator() val navigator = FakeCallScreenNavigator()
val widgetDriver = FakeWidgetDriver() val widgetDriver = FakeMatrixWidgetDriver()
val presenter = createCallScreenPresenter( val presenter = createCallScreenPresenter(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
widgetDriver = widgetDriver, widgetDriver = widgetDriver,
@ -149,7 +149,7 @@ class CallScreenPresenterTest {
@Test @Test
fun `present - a received hang up message closes the screen and stops the widget driver`() = runTest(UnconfinedTestDispatcher()) { fun `present - a received hang up message closes the screen and stops the widget driver`() = runTest(UnconfinedTestDispatcher()) {
val navigator = FakeCallScreenNavigator() val navigator = FakeCallScreenNavigator()
val widgetDriver = FakeWidgetDriver() val widgetDriver = FakeMatrixWidgetDriver()
val presenter = createCallScreenPresenter( val presenter = createCallScreenPresenter(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
widgetDriver = widgetDriver, widgetDriver = widgetDriver,
@ -178,7 +178,7 @@ class CallScreenPresenterTest {
@Test @Test
fun `present - automatically starts the Matrix client sync when on RoomCall`() = runTest { fun `present - automatically starts the Matrix client sync when on RoomCall`() = runTest {
val navigator = FakeCallScreenNavigator() val navigator = FakeCallScreenNavigator()
val widgetDriver = FakeWidgetDriver() val widgetDriver = FakeMatrixWidgetDriver()
val matrixClient = FakeMatrixClient() val matrixClient = FakeMatrixClient()
val presenter = createCallScreenPresenter( val presenter = createCallScreenPresenter(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
@ -201,7 +201,7 @@ class CallScreenPresenterTest {
@Test @Test
fun `present - automatically stops the Matrix client sync on dispose`() = runTest { fun `present - automatically stops the Matrix client sync on dispose`() = runTest {
val navigator = FakeCallScreenNavigator() val navigator = FakeCallScreenNavigator()
val widgetDriver = FakeWidgetDriver() val widgetDriver = FakeMatrixWidgetDriver()
val matrixClient = FakeMatrixClient() val matrixClient = FakeMatrixClient()
val presenter = createCallScreenPresenter( val presenter = createCallScreenPresenter(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
@ -229,7 +229,7 @@ class CallScreenPresenterTest {
private fun TestScope.createCallScreenPresenter( private fun TestScope.createCallScreenPresenter(
callType: CallType, callType: CallType,
navigator: CallScreenNavigator = FakeCallScreenNavigator(), navigator: CallScreenNavigator = FakeCallScreenNavigator(),
widgetDriver: FakeWidgetDriver = FakeWidgetDriver(), widgetDriver: FakeMatrixWidgetDriver = FakeMatrixWidgetDriver(),
widgetProvider: FakeCallWidgetProvider = FakeCallWidgetProvider(widgetDriver), widgetProvider: FakeCallWidgetProvider = FakeCallWidgetProvider(widgetDriver),
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
matrixClientsProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(), matrixClientsProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),

View file

@ -26,7 +26,7 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.widget.FakeCallWidgetSettingsProvider import io.element.android.libraries.matrix.test.widget.FakeCallWidgetSettingsProvider
import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@ -76,7 +76,7 @@ class DefaultCallWidgetProviderTest {
fun `getWidget - returns a widget driver when all steps are successful`() = runTest { fun `getWidget - returns a widget driver when all steps are successful`() = runTest {
val room = FakeMatrixRoom().apply { val room = FakeMatrixRoom().apply {
givenGenerateWidgetWebViewUrlResult(Result.success("url")) givenGenerateWidgetWebViewUrlResult(Result.success("url"))
givenGetWidgetDriverResult(Result.success(FakeWidgetDriver())) givenGetWidgetDriverResult(Result.success(FakeMatrixWidgetDriver()))
} }
val client = FakeMatrixClient().apply { val client = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, room) givenGetRoomResult(A_ROOM_ID, room)
@ -89,7 +89,7 @@ class DefaultCallWidgetProviderTest {
fun `getWidget - will use a custom base url if it exists`() = runTest { fun `getWidget - will use a custom base url if it exists`() = runTest {
val room = FakeMatrixRoom().apply { val room = FakeMatrixRoom().apply {
givenGenerateWidgetWebViewUrlResult(Result.success("url")) givenGenerateWidgetWebViewUrlResult(Result.success("url"))
givenGetWidgetDriverResult(Result.success(FakeWidgetDriver())) givenGetWidgetDriverResult(Result.success(FakeMatrixWidgetDriver()))
} }
val client = FakeMatrixClient().apply { val client = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, room) givenGetRoomResult(A_ROOM_ID, room)

View file

@ -19,10 +19,10 @@ package io.element.android.features.call.utils
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
class FakeCallWidgetProvider( class FakeCallWidgetProvider(
private val widgetDriver: FakeWidgetDriver = FakeWidgetDriver(), private val widgetDriver: FakeMatrixWidgetDriver = FakeMatrixWidgetDriver(),
private val url: String = "https://call.element.io", private val url: String = "https://call.element.io",
) : CallWidgetProvider { ) : CallWidgetProvider {
var getWidgetCalled = false var getWidgetCalled = false

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_room_action_create_room">"ახალი ოთახი"</string>
<string name="screen_create_room_add_people_title">"ხალხის მოწვევა"</string>
<string name="screen_create_room_error_creating_room">"ოთახის შექმნისას შეცდომა მოხდა"</string>
<string name="screen_create_room_private_option_description">"ამ ოთახში შეტყობინებები დაშიფრულია. შემდგომ დაშიფვრის გამორთვა შეუძლებელია."</string>
<string name="screen_create_room_private_option_title">"კერძო ოთახი (მხოლოდ მოწვევა)"</string>
<string name="screen_create_room_public_option_description">"შეტყობინებები არ არის დაშიფრული და ყველას შეუძლია მათი წაკითხვა. შეგიძლიათ ჩართოთ დაშიფვრა მოგვიანებით."</string>
<string name="screen_create_room_public_option_title">"საჯარო ოთახი (ნებისმიერი)"</string>
<string name="screen_create_room_room_name_label">"ოთახის სახელი"</string>
<string name="screen_create_room_title">"ოთახის შექმნა"</string>
<string name="screen_create_room_topic_label">"თემა (სურვილისამებრ)"</string>
<string name="screen_start_chat_error_starting_chat">"ჩატის დაწყების მცდელობისას შეცდომა მოხდა"</string>
</resources>

View file

@ -132,9 +132,8 @@ class FtueFlowNode @AssistedInject constructor(
lifecycleScope.launch { moveToNextStep() } lifecycleScope.launch { moveToNextStep() }
} }
} }
lockScreenEntryPoint.nodeBuilder(this, buildContext) lockScreenEntryPoint.nodeBuilder(this, buildContext, LockScreenEntryPoint.Target.Setup)
.callback(callback) .callback(callback)
.target(LockScreenEntryPoint.Target.Setup)
.build() .build()
} }
} }

View file

@ -11,6 +11,18 @@
<string name="screen_qr_code_login_connection_note_secure_state_title">"Злучэнне небяспечнае"</string> <string name="screen_qr_code_login_connection_note_secure_state_title">"Злучэнне небяспечнае"</string>
<string name="screen_qr_code_login_device_code_subtitle">"Вам будзе прапанавана ўвесці дзве лічбы, паказаныя на гэтай прыладзе."</string> <string name="screen_qr_code_login_device_code_subtitle">"Вам будзе прапанавана ўвесці дзве лічбы, паказаныя на гэтай прыладзе."</string>
<string name="screen_qr_code_login_device_code_title">"Увядзіце наступны нумар на іншай прыладзе."</string> <string name="screen_qr_code_login_device_code_title">"Увядзіце наступны нумар на іншай прыладзе."</string>
<string name="screen_qr_code_login_error_cancelled_subtitle">"Уваход быў адменены на іншай прыладзе."</string>
<string name="screen_qr_code_login_error_cancelled_title">"Запыт на ўваход скасаваны"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"Запыт на іншай прыладзе не быў прыняты."</string>
<string name="screen_qr_code_login_error_declined_title">"Уваход адхілены"</string>
<string name="screen_qr_code_login_error_expired_subtitle">"Тэрмін уваходу скончыўся. Калі ласка, паспрабуйце яшчэ раз."</string>
<string name="screen_qr_code_login_error_expired_title">"Уваход у сістэму не быў завершаны своечасова"</string>
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Ваша іншая прылада не падтрымлівае ўваход у %s з дапамогай QR-кода.
Паспрабуйце ўвайсці ў сістэму ўручную або адскануйце QR-код з дапамогай іншай прылады."</string>
<string name="screen_qr_code_login_error_linking_not_suported_title">"QR-код не падтрымліваецца"</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Ваш правайдар уліковага запісу не падтрымлівае %1$s."</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s не падтрымліваецца"</string>
<string name="screen_qr_code_login_initial_state_button_title">"Гатовы да сканавання"</string> <string name="screen_qr_code_login_initial_state_button_title">"Гатовы да сканавання"</string>
<string name="screen_qr_code_login_initial_state_item_1">"Адкрыйце %1$s на настольнай прыладзе"</string> <string name="screen_qr_code_login_initial_state_item_1">"Адкрыйце %1$s на настольнай прыладзе"</string>
<string name="screen_qr_code_login_initial_state_item_2">"Націсніце на свой аватар"</string> <string name="screen_qr_code_login_initial_state_item_2">"Націсніце на свой аватар"</string>

View file

@ -11,6 +11,18 @@
<string name="screen_qr_code_login_connection_note_secure_state_title">"Připojení není zabezpečené"</string> <string name="screen_qr_code_login_connection_note_secure_state_title">"Připojení není zabezpečené"</string>
<string name="screen_qr_code_login_device_code_subtitle">"Budete požádáni o zadání dvou níže uvedených číslic."</string> <string name="screen_qr_code_login_device_code_subtitle">"Budete požádáni o zadání dvou níže uvedených číslic."</string>
<string name="screen_qr_code_login_device_code_title">"Zadejte níže uvedené číslo na svém dalším zařízení"</string> <string name="screen_qr_code_login_device_code_title">"Zadejte níže uvedené číslo na svém dalším zařízení"</string>
<string name="screen_qr_code_login_error_cancelled_subtitle">"Přihlášení bylo na druhém zařízení zrušeno."</string>
<string name="screen_qr_code_login_error_cancelled_title">"Žádost o přihlášení zrušena"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"Požadavek na vašem druhém zařízení nebyl přijat."</string>
<string name="screen_qr_code_login_error_declined_title">"Přihlášení odmítnuto"</string>
<string name="screen_qr_code_login_error_expired_subtitle">"Platnost přihlášení vypršela. Zkuste to prosím znovu."</string>
<string name="screen_qr_code_login_error_expired_title">"Přihlášení nebylo dokončeno včas"</string>
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Vaše druhé zařízení nepodporuje přihlášení k %su pomocí QR kódu.
Zkuste se přihlásit ručně nebo naskenujte QR kód pomocí jiného zařízení."</string>
<string name="screen_qr_code_login_error_linking_not_suported_title">"QR kód není podporován"</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Váš poskytovatel účtu nepodporuje %1$s."</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s není podporováno"</string>
<string name="screen_qr_code_login_initial_state_button_title">"Připraveno ke skenování"</string> <string name="screen_qr_code_login_initial_state_button_title">"Připraveno ke skenování"</string>
<string name="screen_qr_code_login_initial_state_item_1">"Otevřete %1$s na stolním počítači"</string> <string name="screen_qr_code_login_initial_state_item_1">"Otevřete %1$s na stolním počítači"</string>
<string name="screen_qr_code_login_initial_state_item_2">"Klikněte na svůj avatar"</string> <string name="screen_qr_code_login_initial_state_item_2">"Klikněte na svůj avatar"</string>

View file

@ -11,11 +11,22 @@
<string name="screen_qr_code_login_connection_note_secure_state_title">"La connexion nest pas sécurisée"</string> <string name="screen_qr_code_login_connection_note_secure_state_title">"La connexion nest pas sécurisée"</string>
<string name="screen_qr_code_login_device_code_subtitle">"Il vous sera demandé de saisir les deux chiffres affichés sur cet appareil."</string> <string name="screen_qr_code_login_device_code_subtitle">"Il vous sera demandé de saisir les deux chiffres affichés sur cet appareil."</string>
<string name="screen_qr_code_login_device_code_title">"Saisissez le nombre ci-dessous sur votre autre appareil"</string> <string name="screen_qr_code_login_device_code_title">"Saisissez le nombre ci-dessous sur votre autre appareil"</string>
<string name="screen_qr_code_login_error_cancelled_subtitle">"La connexion a été annulée sur lautre appareil."</string>
<string name="screen_qr_code_login_error_cancelled_title">"Demande de connexion annulée"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"La demande sur lautre appareil na pas été acceptée."</string>
<string name="screen_qr_code_login_error_declined_title">"Connexion refusée"</string>
<string name="screen_qr_code_login_error_expired_subtitle">"Connexion expirée. Veuillez essayer à nouveau."</string>
<string name="screen_qr_code_login_error_expired_title">"La connexion a pris trop de temps."</string>
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Votre autre appareil ne supporte pas la connexion à %s avec un code QR. Essayer de vous connecter manuellement, ou scanner le code QR avec un autre appareil."</string>
<string name="screen_qr_code_login_error_linking_not_suported_title">"Code QR non supporté"</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Votre fournisseur de compte ne supporte pas %1$s."</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s nest pas supporté"</string>
<string name="screen_qr_code_login_initial_state_button_title">"Prêt à scanner"</string>
<string name="screen_qr_code_login_initial_state_item_1">"Ouvrez %1$s sur un ordinateur"</string> <string name="screen_qr_code_login_initial_state_item_1">"Ouvrez %1$s sur un ordinateur"</string>
<string name="screen_qr_code_login_initial_state_item_2">"Cliquez sur votre image de profil"</string> <string name="screen_qr_code_login_initial_state_item_2">"Cliquez sur votre image de profil"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Choisissez %1$s"</string> <string name="screen_qr_code_login_initial_state_item_3">"Choisissez %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"“Associer une nouvelle session”"</string> <string name="screen_qr_code_login_initial_state_item_3_action">"“Associer une nouvelle session”"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Suivez les instructions affichées"</string> <string name="screen_qr_code_login_initial_state_item_4">"Scanner le code QR avec cet appareil"</string>
<string name="screen_qr_code_login_initial_state_title">"Ouvrez %1$s sur un autre appareil pour obtenir le QR code"</string> <string name="screen_qr_code_login_initial_state_title">"Ouvrez %1$s sur un autre appareil pour obtenir le QR code"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Scannez le QR code affiché sur lautre appareil."</string> <string name="screen_qr_code_login_invalid_scan_state_description">"Scannez le QR code affiché sur lautre appareil."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Essayer à nouveau"</string> <string name="screen_qr_code_login_invalid_scan_state_retry_button">"Essayer à nouveau"</string>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_notification_optin_subtitle">"თქვენ შეგიძლიათ შეცვალოთ თქვენი პარამეტრები მოგვიანებით."</string>
<string name="screen_notification_optin_title">"ყველა შეტყობინებაზე შეტყობინებების მიღება"</string>
<string name="screen_welcome_bullet_1">"ზარები, გამოკითხვები, ძიება და სხვა დაემატება ამ წლის ბოლოს."</string>
<string name="screen_welcome_bullet_2">"დაშიფრული ოთახებისთვის შეტყობინებების ისტორია ჯერ არ არის ხელმისაწვდომი."</string>
<string name="screen_welcome_bullet_3">"ჩვენ სიამოვნებით მოვისმინოთ თქვენგან, შეგვატყობინეთ რას ფიქრობთ პარამეტრების გვერდზე."</string>
<string name="screen_welcome_button">"დავიწყოთ!"</string>
<string name="screen_welcome_subtitle">"აი, რა უნდა იცოდეთ:"</string>
<string name="screen_welcome_title">"კეთილი იყოს თქვენი მობრძანება %1$s-ში!"</string>
</resources>

View file

@ -11,6 +11,7 @@
<string name="screen_qr_code_login_connection_note_secure_state_title">"Ligação insegura"</string> <string name="screen_qr_code_login_connection_note_secure_state_title">"Ligação insegura"</string>
<string name="screen_qr_code_login_device_code_subtitle">"Ser-te-á pedido que insiras os dois dígitos indicados neste dispositivo."</string> <string name="screen_qr_code_login_device_code_subtitle">"Ser-te-á pedido que insiras os dois dígitos indicados neste dispositivo."</string>
<string name="screen_qr_code_login_device_code_title">"Insere o número abaixo no teu dispositivo"</string> <string name="screen_qr_code_login_device_code_title">"Insere o número abaixo no teu dispositivo"</string>
<string name="screen_qr_code_login_error_cancelled_title">"Pedido de início de sessão cancelado"</string>
<string name="screen_qr_code_login_initial_state_button_title">"Pronto para ler"</string> <string name="screen_qr_code_login_initial_state_button_title">"Pronto para ler"</string>
<string name="screen_qr_code_login_initial_state_item_1">"Abre a %1$s num computador"</string> <string name="screen_qr_code_login_initial_state_item_1">"Abre a %1$s num computador"</string>
<string name="screen_qr_code_login_initial_state_item_2">"Carrega no teu avatar"</string> <string name="screen_qr_code_login_initial_state_item_2">"Carrega no teu avatar"</string>

View file

@ -11,11 +11,12 @@
<string name="screen_qr_code_login_connection_note_secure_state_title">"Conexiunea nu este sigură"</string> <string name="screen_qr_code_login_connection_note_secure_state_title">"Conexiunea nu este sigură"</string>
<string name="screen_qr_code_login_device_code_subtitle">"Vi se va cere să introduceți cele două cifre afișate pe acest dispozitiv."</string> <string name="screen_qr_code_login_device_code_subtitle">"Vi se va cere să introduceți cele două cifre afișate pe acest dispozitiv."</string>
<string name="screen_qr_code_login_device_code_title">"Introduceți numărul de mai jos pe celălalt dispozitiv"</string> <string name="screen_qr_code_login_device_code_title">"Introduceți numărul de mai jos pe celălalt dispozitiv"</string>
<string name="screen_qr_code_login_initial_state_button_title">"Gata de scanare"</string>
<string name="screen_qr_code_login_initial_state_item_1">"Deschideți %1$s pe un dispozitiv desktop"</string> <string name="screen_qr_code_login_initial_state_item_1">"Deschideți %1$s pe un dispozitiv desktop"</string>
<string name="screen_qr_code_login_initial_state_item_2">"Faceți clic pe avatarul dumneavoastră"</string> <string name="screen_qr_code_login_initial_state_item_2">"Faceți clic pe avatarul dumneavoastră"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Selectați %1$s"</string> <string name="screen_qr_code_login_initial_state_item_3">"Selectați %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"„Conectați un dispozitiv nou”"</string> <string name="screen_qr_code_login_initial_state_item_3_action">"„Conectați un dispozitiv nou”"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Urmați instrucțiunile afișate"</string> <string name="screen_qr_code_login_initial_state_item_4">"Scanați codul QR cu acest dispozitiv"</string>
<string name="screen_qr_code_login_initial_state_title">"Deschideți %1$s pe un alt dispozitiv pentru a obține codul QR"</string> <string name="screen_qr_code_login_initial_state_title">"Deschideți %1$s pe un alt dispozitiv pentru a obține codul QR"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Utilizați codul QR afișat pe celălalt dispozitiv."</string> <string name="screen_qr_code_login_invalid_scan_state_description">"Utilizați codul QR afișat pe celălalt dispozitiv."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Încercați din nou"</string> <string name="screen_qr_code_login_invalid_scan_state_retry_button">"Încercați din nou"</string>

View file

@ -11,11 +11,24 @@
<string name="screen_qr_code_login_connection_note_secure_state_title">"Соединение не защищено"</string> <string name="screen_qr_code_login_connection_note_secure_state_title">"Соединение не защищено"</string>
<string name="screen_qr_code_login_device_code_subtitle">"Вам нужно будет ввести две цифры, показанные на этом устройстве."</string> <string name="screen_qr_code_login_device_code_subtitle">"Вам нужно будет ввести две цифры, показанные на этом устройстве."</string>
<string name="screen_qr_code_login_device_code_title">"Введите показанный номер на своем другом устройстве"</string> <string name="screen_qr_code_login_device_code_title">"Введите показанный номер на своем другом устройстве"</string>
<string name="screen_qr_code_login_error_cancelled_subtitle">"Вход на другом устройстве был отменен."</string>
<string name="screen_qr_code_login_error_cancelled_title">"Запрос на вход отменен"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"Запрос не был принят на другом устройстве."</string>
<string name="screen_qr_code_login_error_declined_title">"Вход отклонен"</string>
<string name="screen_qr_code_login_error_expired_subtitle">"Срок действия входа истек. Пожалуйста, попробуйте еще раз."</string>
<string name="screen_qr_code_login_error_expired_title">"Вход в систему не был выполнен вовремя"</string>
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Другое устройство не поддерживает вход в %s с помощью QR-кода.
Попробуйте войти вручную или отсканируйте QR-код на другом устройстве."</string>
<string name="screen_qr_code_login_error_linking_not_suported_title">"QR-код не поддерживается"</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Поставщик учетной записи не поддерживает %1$s."</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s не поддерживается"</string>
<string name="screen_qr_code_login_initial_state_button_title">"Готово к сканированию"</string>
<string name="screen_qr_code_login_initial_state_item_1">"Откройте %1$s на настольном устройстве"</string> <string name="screen_qr_code_login_initial_state_item_1">"Откройте %1$s на настольном устройстве"</string>
<string name="screen_qr_code_login_initial_state_item_2">"Нажмите на свое изображение"</string> <string name="screen_qr_code_login_initial_state_item_2">"Нажмите на свое изображение"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Выбрать %1$s"</string> <string name="screen_qr_code_login_initial_state_item_3">"Выбрать %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Привязать новое устройство\""</string> <string name="screen_qr_code_login_initial_state_item_3_action">"\"Привязать новое устройство\""</string>
<string name="screen_qr_code_login_initial_state_item_4">"Соблюдайте показанную инструкцию"</string> <string name="screen_qr_code_login_initial_state_item_4">"Отсканируйте QR-код с помощью этого устройства"</string>
<string name="screen_qr_code_login_initial_state_title">"Откройте %1$s на другом устройстве, чтобы получить QR-код"</string> <string name="screen_qr_code_login_initial_state_title">"Откройте %1$s на другом устройстве, чтобы получить QR-код"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Используйте QR-код, показанный на другом устройстве."</string> <string name="screen_qr_code_login_invalid_scan_state_description">"Используйте QR-код, показанный на другом устройстве."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Повторить попытку"</string> <string name="screen_qr_code_login_invalid_scan_state_retry_button">"Повторить попытку"</string>

View file

@ -11,6 +11,18 @@
<string name="screen_qr_code_login_connection_note_secure_state_title">"Pripojenie nie je bezpečené"</string> <string name="screen_qr_code_login_connection_note_secure_state_title">"Pripojenie nie je bezpečené"</string>
<string name="screen_qr_code_login_device_code_subtitle">"Budete požiadaní o zadanie dvoch číslic zobrazených na tomto zariadení."</string> <string name="screen_qr_code_login_device_code_subtitle">"Budete požiadaní o zadanie dvoch číslic zobrazených na tomto zariadení."</string>
<string name="screen_qr_code_login_device_code_title">"Zadajte nižšie uvedené číslo na vašom druhom zariadení"</string> <string name="screen_qr_code_login_device_code_title">"Zadajte nižšie uvedené číslo na vašom druhom zariadení"</string>
<string name="screen_qr_code_login_error_cancelled_subtitle">"Prihlásenie bolo zrušené na druhom zariadení."</string>
<string name="screen_qr_code_login_error_cancelled_title">"Žiadosť o prihlásenie bola zrušená"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"Žiadosť na vašom druhom zariadení nebola prijatá."</string>
<string name="screen_qr_code_login_error_declined_title">"Prihlásenie bolo odmietnuté"</string>
<string name="screen_qr_code_login_error_expired_subtitle">"Platnosť prihlásenia vypršala. Skúste to prosím znova."</string>
<string name="screen_qr_code_login_error_expired_title">"Prihlásenie nebolo včas dokončené"</string>
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Vaše druhé zariadenie nepodporuje prihlásenie do aplikácie %s pomocou QR kódu.
Skúste sa prihlásiť manuálne alebo naskenujte QR kód pomocou iného zariadenia."</string>
<string name="screen_qr_code_login_error_linking_not_suported_title">"QR kód nie je podporovaný"</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Poskytovateľ vášho účtu nepodporuje %1$s."</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s nie je podporovaný"</string>
<string name="screen_qr_code_login_initial_state_button_title">"Pripravené na skenovanie"</string> <string name="screen_qr_code_login_initial_state_button_title">"Pripravené na skenovanie"</string>
<string name="screen_qr_code_login_initial_state_item_1">"Otvorte %1$s na stolnom zariadení"</string> <string name="screen_qr_code_login_initial_state_item_1">"Otvorte %1$s na stolnom zariadení"</string>
<string name="screen_qr_code_login_initial_state_item_2">"Kliknite na svoj obrázok"</string> <string name="screen_qr_code_login_initial_state_item_2">"Kliknite na svoj obrázok"</string>

View file

@ -11,6 +11,18 @@
<string name="screen_qr_code_login_connection_note_secure_state_title">"Connection not secure"</string> <string name="screen_qr_code_login_connection_note_secure_state_title">"Connection not secure"</string>
<string name="screen_qr_code_login_device_code_subtitle">"Youll be asked to enter the two digits shown on this device."</string> <string name="screen_qr_code_login_device_code_subtitle">"Youll be asked to enter the two digits shown on this device."</string>
<string name="screen_qr_code_login_device_code_title">"Enter the number below on your other device"</string> <string name="screen_qr_code_login_device_code_title">"Enter the number below on your other device"</string>
<string name="screen_qr_code_login_error_cancelled_subtitle">"The sign in was cancelled on the other device."</string>
<string name="screen_qr_code_login_error_cancelled_title">"Sign in request cancelled"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"The request on your other device was not accepted."</string>
<string name="screen_qr_code_login_error_declined_title">"Sign in declined"</string>
<string name="screen_qr_code_login_error_expired_subtitle">"Sign in expired. Please try again."</string>
<string name="screen_qr_code_login_error_expired_title">"The sign in was not completed in time"</string>
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Your other device does not support signing in to %s with a QR code.
Try signing in manually, or scan the QR code with another device."</string>
<string name="screen_qr_code_login_error_linking_not_suported_title">"QR code not supported"</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Your account provider does not support %1$s."</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s not supported"</string>
<string name="screen_qr_code_login_initial_state_button_title">"Ready to scan"</string> <string name="screen_qr_code_login_initial_state_button_title">"Ready to scan"</string>
<string name="screen_qr_code_login_initial_state_item_1">"Open %1$s on a desktop device"</string> <string name="screen_qr_code_login_initial_state_item_1">"Open %1$s on a desktop device"</string>
<string name="screen_qr_code_login_initial_state_item_2">"Click on your avatar"</string> <string name="screen_qr_code_login_initial_state_item_2">"Click on your avatar"</string>

View file

@ -16,7 +16,7 @@
package io.element.android.features.ftue.impl.welcome.state package io.element.android.features.ftue.impl.welcome.state
class FakeWelcomeState : WelcomeScreenState { class FakeWelcomeScreenState : WelcomeScreenState {
private var isWelcomeScreenNeeded = true private var isWelcomeScreenNeeded = true
override fun isWelcomeScreenNeeded(): Boolean { override fun isWelcomeScreenNeeded(): Boolean {

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invites_decline_chat_message">"დარწმუნებული ხართ, რომ გსურთ, უარი თქვათ მოწვევაზე %1$s-ში?"</string>
<string name="screen_invites_decline_chat_title">"მოწვევაზე უარის თქმა"</string>
<string name="screen_invites_decline_direct_chat_message">"დარწმუნებული ხართ, რომ გსურთ, უარი თქვათ ჩატზე %1$s-თან?"</string>
<string name="screen_invites_decline_direct_chat_title">"ჩატზე უარის თქვა"</string>
<string name="screen_invites_empty_list">"მოწვევები არ არის"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) მოგიწვიათ"</string>
</resources>

View file

@ -192,9 +192,11 @@ class AcceptDeclineInvitePresenterTest {
cancelAndConsumeRemainingEvents() cancelAndConsumeRemainingEvents()
} }
assert(joinRoomFailure) assert(joinRoomFailure)
.isCalledExactly(1) .isCalledOnce()
.withSequence( .with(
listOf(value(A_ROOM_ID), value(emptyList<String>()), value(JoinedRoom.Trigger.Invite)) value(A_ROOM_ID),
value(emptyList<String>()),
value(JoinedRoom.Trigger.Invite)
) )
} }
@ -221,9 +223,11 @@ class AcceptDeclineInvitePresenterTest {
cancelAndConsumeRemainingEvents() cancelAndConsumeRemainingEvents()
} }
assert(joinRoomSuccess) assert(joinRoomSuccess)
.isCalledExactly(1) .isCalledOnce()
.withSequence( .with(
listOf(value(A_ROOM_ID), value(emptyList<String>()), value(JoinedRoom.Trigger.Invite)) value(A_ROOM_ID),
value(emptyList<String>()),
value(JoinedRoom.Trigger.Invite)
) )
} }

View file

@ -42,7 +42,6 @@ import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.RoomType import io.element.android.libraries.matrix.api.room.RoomType
@ -96,7 +95,7 @@ class JoinRoomPresenter @AssistedInject constructor(
} }
else -> { else -> {
value = ContentState.Loading(roomIdOrAlias) value = ContentState.Loading(roomIdOrAlias)
val result = matrixClient.getRoomPreview(roomId.toRoomIdOrAlias()) val result = matrixClient.getRoomPreviewFromRoomId(roomId, serverNames)
value = result.fold( value = result.fold(
onSuccess = { roomPreview -> onSuccess = { roomPreview ->
roomPreview.toContentState() roomPreview.toContentState()

View file

@ -366,7 +366,7 @@ class JoinRoomPresenterTest {
@Test @Test
fun `present - when room is not known RoomPreview is loaded`() = runTest { fun `present - when room is not known RoomPreview is loaded`() = runTest {
val client = FakeMatrixClient( val client = FakeMatrixClient(
getRoomPreviewResult = { getRoomPreviewFromRoomIdResult = { _, _ ->
Result.success( Result.success(
RoomPreview( RoomPreview(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
@ -411,7 +411,7 @@ class JoinRoomPresenterTest {
@Test @Test
fun `present - when room is not known RoomPreview is loaded with error`() = runTest { fun `present - when room is not known RoomPreview is loaded with error`() = runTest {
val client = FakeMatrixClient( val client = FakeMatrixClient(
getRoomPreviewResult = { getRoomPreviewFromRoomIdResult = { _, _ ->
Result.failure(AN_EXCEPTION) Result.failure(AN_EXCEPTION)
} }
) )
@ -449,7 +449,7 @@ class JoinRoomPresenterTest {
@Test @Test
fun `present - when room is not known RoomPreview is loaded with error 403`() = runTest { fun `present - when room is not known RoomPreview is loaded with error 403`() = runTest {
val client = FakeMatrixClient( val client = FakeMatrixClient(
getRoomPreviewResult = { getRoomPreviewFromRoomIdResult = { _, _ ->
Result.failure(Exception("403")) Result.failure(Exception("403"))
} }
) )

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="leave_room_alert_empty_subtitle">"დარწმუნებული ბრძანდებით, რომ ამ ოთახის დატოვება გსურთ? თქვენ აქ მარტო ხართ და ჩატის დატოვებისას აქ თქვენს ჩათვლით ვერავინ ვერ გაწევრიანდება."</string>
<string name="leave_room_alert_private_subtitle">"დარწმუნებული ბრძანდებით, რომ ამ ოთახის დატოვება გსურთ? ეს ოთახი არ არის საჯარო და მოწვევის გარეშე ვერ შეძლებთ ხელახლა გაწევრიანებას."</string>
<string name="leave_room_alert_subtitle">"დარწმუნებული ბრძანდებით, რომ ოთახის დატოვება გსურთ?"</string>
</resources>

View file

@ -18,7 +18,7 @@ package io.element.android.features.location.impl.common.permissions
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
class PermissionsPresenterFake : PermissionsPresenter { class FakePermissionsPresenter : PermissionsPresenter {
val events = mutableListOf<PermissionsEvents>() val events = mutableListOf<PermissionsEvents>()
private fun handleEvent(event: PermissionsEvents) { private fun handleEvent(event: PermissionsEvents) {

View file

@ -24,9 +24,9 @@ import im.vector.app.features.analytics.plan.Composer
import io.element.android.features.location.api.Location import io.element.android.features.location.api.Location
import io.element.android.features.location.impl.aPermissionsState import io.element.android.features.location.impl.aPermissionsState
import io.element.android.features.location.impl.common.actions.FakeLocationActions import io.element.android.features.location.impl.common.actions.FakeLocationActions
import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter
import io.element.android.features.location.impl.common.permissions.PermissionsEvents import io.element.android.features.location.impl.common.permissions.PermissionsEvents
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
import io.element.android.features.location.impl.common.permissions.PermissionsPresenterFake
import io.element.android.features.location.impl.common.permissions.PermissionsState import io.element.android.features.location.impl.common.permissions.PermissionsState
import io.element.android.features.messages.test.FakeMessageComposerContext import io.element.android.features.messages.test.FakeMessageComposerContext
import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.room.location.AssetType
@ -45,7 +45,7 @@ class SendLocationPresenterTest {
@get:Rule @get:Rule
val warmUpRule = WarmUpRule() val warmUpRule = WarmUpRule()
private val permissionsPresenterFake = PermissionsPresenterFake() private val fakePermissionsPresenter = FakePermissionsPresenter()
private val fakeMatrixRoom = FakeMatrixRoom() private val fakeMatrixRoom = FakeMatrixRoom()
private val fakeAnalyticsService = FakeAnalyticsService() private val fakeAnalyticsService = FakeAnalyticsService()
private val fakeMessageComposerContext = FakeMessageComposerContext() private val fakeMessageComposerContext = FakeMessageComposerContext()
@ -53,7 +53,7 @@ class SendLocationPresenterTest {
private val fakeBuildMeta = aBuildMeta(applicationName = "app name") private val fakeBuildMeta = aBuildMeta(applicationName = "app name")
private val sendLocationPresenter: SendLocationPresenter = SendLocationPresenter( private val sendLocationPresenter: SendLocationPresenter = SendLocationPresenter(
permissionsPresenterFactory = object : PermissionsPresenter.Factory { permissionsPresenterFactory = object : PermissionsPresenter.Factory {
override fun create(permissions: List<String>): PermissionsPresenter = permissionsPresenterFake override fun create(permissions: List<String>): PermissionsPresenter = fakePermissionsPresenter
}, },
room = fakeMatrixRoom, room = fakeMatrixRoom,
analyticsService = fakeAnalyticsService, analyticsService = fakeAnalyticsService,
@ -64,7 +64,7 @@ class SendLocationPresenterTest {
@Test @Test
fun `initial state with permissions granted`() = runTest { fun `initial state with permissions granted`() = runTest {
permissionsPresenterFake.givenState( fakePermissionsPresenter.givenState(
aPermissionsState( aPermissionsState(
permissions = PermissionsState.Permissions.AllGranted, permissions = PermissionsState.Permissions.AllGranted,
shouldShowRationale = false, shouldShowRationale = false,
@ -90,7 +90,7 @@ class SendLocationPresenterTest {
@Test @Test
fun `initial state with permissions partially granted`() = runTest { fun `initial state with permissions partially granted`() = runTest {
permissionsPresenterFake.givenState( fakePermissionsPresenter.givenState(
aPermissionsState( aPermissionsState(
permissions = PermissionsState.Permissions.SomeGranted, permissions = PermissionsState.Permissions.SomeGranted,
shouldShowRationale = false, shouldShowRationale = false,
@ -116,7 +116,7 @@ class SendLocationPresenterTest {
@Test @Test
fun `initial state with permissions denied`() = runTest { fun `initial state with permissions denied`() = runTest {
permissionsPresenterFake.givenState( fakePermissionsPresenter.givenState(
aPermissionsState( aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted, permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = false, shouldShowRationale = false,
@ -142,7 +142,7 @@ class SendLocationPresenterTest {
@Test @Test
fun `initial state with permissions denied once`() = runTest { fun `initial state with permissions denied once`() = runTest {
permissionsPresenterFake.givenState( fakePermissionsPresenter.givenState(
aPermissionsState( aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted, permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = true, shouldShowRationale = true,
@ -168,7 +168,7 @@ class SendLocationPresenterTest {
@Test @Test
fun `rationale dialog dismiss`() = runTest { fun `rationale dialog dismiss`() = runTest {
permissionsPresenterFake.givenState( fakePermissionsPresenter.givenState(
aPermissionsState( aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted, permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = true, shouldShowRationale = true,
@ -199,7 +199,7 @@ class SendLocationPresenterTest {
@Test @Test
fun `rationale dialog continue`() = runTest { fun `rationale dialog continue`() = runTest {
permissionsPresenterFake.givenState( fakePermissionsPresenter.givenState(
aPermissionsState( aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted, permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = true, shouldShowRationale = true,
@ -221,13 +221,13 @@ class SendLocationPresenterTest {
// Continue the dialog sends permission request to the permissions presenter // Continue the dialog sends permission request to the permissions presenter
myLocationState.eventSink(SendLocationEvents.RequestPermissions) myLocationState.eventSink(SendLocationEvents.RequestPermissions)
assertThat(permissionsPresenterFake.events.last()).isEqualTo(PermissionsEvents.RequestPermissions) assertThat(fakePermissionsPresenter.events.last()).isEqualTo(PermissionsEvents.RequestPermissions)
} }
} }
@Test @Test
fun `permission denied dialog dismiss`() = runTest { fun `permission denied dialog dismiss`() = runTest {
permissionsPresenterFake.givenState( fakePermissionsPresenter.givenState(
aPermissionsState( aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted, permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = false, shouldShowRationale = false,
@ -258,7 +258,7 @@ class SendLocationPresenterTest {
@Test @Test
fun `share sender location`() = runTest { fun `share sender location`() = runTest {
permissionsPresenterFake.givenState( fakePermissionsPresenter.givenState(
aPermissionsState( aPermissionsState(
permissions = PermissionsState.Permissions.AllGranted, permissions = PermissionsState.Permissions.AllGranted,
shouldShowRationale = false, shouldShowRationale = false,
@ -314,7 +314,7 @@ class SendLocationPresenterTest {
@Test @Test
fun `share pin location`() = runTest { fun `share pin location`() = runTest {
permissionsPresenterFake.givenState( fakePermissionsPresenter.givenState(
aPermissionsState( aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted, permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = false, shouldShowRationale = false,
@ -370,7 +370,7 @@ class SendLocationPresenterTest {
@Test @Test
fun `composer context passes through analytics`() = runTest { fun `composer context passes through analytics`() = runTest {
permissionsPresenterFake.givenState( fakePermissionsPresenter.givenState(
aPermissionsState( aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted, permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = false, shouldShowRationale = false,
@ -418,7 +418,7 @@ class SendLocationPresenterTest {
@Test @Test
fun `open settings activity`() = runTest { fun `open settings activity`() = runTest {
permissionsPresenterFake.givenState( fakePermissionsPresenter.givenState(
aPermissionsState( aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted, permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = false, shouldShowRationale = false,

View file

@ -23,9 +23,9 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.location.api.Location import io.element.android.features.location.api.Location
import io.element.android.features.location.impl.aPermissionsState import io.element.android.features.location.impl.aPermissionsState
import io.element.android.features.location.impl.common.actions.FakeLocationActions import io.element.android.features.location.impl.common.actions.FakeLocationActions
import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter
import io.element.android.features.location.impl.common.permissions.PermissionsEvents import io.element.android.features.location.impl.common.permissions.PermissionsEvents
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
import io.element.android.features.location.impl.common.permissions.PermissionsPresenterFake
import io.element.android.features.location.impl.common.permissions.PermissionsState import io.element.android.features.location.impl.common.permissions.PermissionsState
import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
@ -38,13 +38,13 @@ class ShowLocationPresenterTest {
@get:Rule @get:Rule
val warmUpRule = WarmUpRule() val warmUpRule = WarmUpRule()
private val permissionsPresenterFake = PermissionsPresenterFake() private val fakePermissionsPresenter = FakePermissionsPresenter()
private val fakeLocationActions = FakeLocationActions() private val fakeLocationActions = FakeLocationActions()
private val fakeBuildMeta = aBuildMeta(applicationName = "app name") private val fakeBuildMeta = aBuildMeta(applicationName = "app name")
private val location = Location(1.23, 4.56, 7.8f) private val location = Location(1.23, 4.56, 7.8f)
private val presenter = ShowLocationPresenter( private val presenter = ShowLocationPresenter(
permissionsPresenterFactory = object : PermissionsPresenter.Factory { permissionsPresenterFactory = object : PermissionsPresenter.Factory {
override fun create(permissions: List<String>): PermissionsPresenter = permissionsPresenterFake override fun create(permissions: List<String>): PermissionsPresenter = fakePermissionsPresenter
}, },
fakeLocationActions, fakeLocationActions,
fakeBuildMeta, fakeBuildMeta,
@ -54,7 +54,7 @@ class ShowLocationPresenterTest {
@Test @Test
fun `emits initial state with no location permission`() = runTest { fun `emits initial state with no location permission`() = runTest {
permissionsPresenterFake.givenState( fakePermissionsPresenter.givenState(
aPermissionsState( aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted, permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = false, shouldShowRationale = false,
@ -74,7 +74,7 @@ class ShowLocationPresenterTest {
@Test @Test
fun `emits initial state location permission denied once`() = runTest { fun `emits initial state location permission denied once`() = runTest {
permissionsPresenterFake.givenState( fakePermissionsPresenter.givenState(
aPermissionsState( aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted, permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = true, shouldShowRationale = true,
@ -94,7 +94,7 @@ class ShowLocationPresenterTest {
@Test @Test
fun `emits initial state with location permission`() = runTest { fun `emits initial state with location permission`() = runTest {
permissionsPresenterFake.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted)) fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted))
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@ -109,7 +109,7 @@ class ShowLocationPresenterTest {
@Test @Test
fun `emits initial state with partial location permission`() = runTest { fun `emits initial state with partial location permission`() = runTest {
permissionsPresenterFake.givenState(aPermissionsState(permissions = PermissionsState.Permissions.SomeGranted)) fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.SomeGranted))
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@ -137,7 +137,7 @@ class ShowLocationPresenterTest {
@Test @Test
fun `centers on user location`() = runTest { fun `centers on user location`() = runTest {
permissionsPresenterFake.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted)) fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted))
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@ -165,7 +165,7 @@ class ShowLocationPresenterTest {
@Test @Test
fun `rationale dialog dismiss`() = runTest { fun `rationale dialog dismiss`() = runTest {
permissionsPresenterFake.givenState( fakePermissionsPresenter.givenState(
aPermissionsState( aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted, permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = true, shouldShowRationale = true,
@ -196,7 +196,7 @@ class ShowLocationPresenterTest {
@Test @Test
fun `rationale dialog continue`() = runTest { fun `rationale dialog continue`() = runTest {
permissionsPresenterFake.givenState( fakePermissionsPresenter.givenState(
aPermissionsState( aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted, permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = true, shouldShowRationale = true,
@ -218,13 +218,13 @@ class ShowLocationPresenterTest {
// Continue the dialog sends permission request to the permissions presenter // Continue the dialog sends permission request to the permissions presenter
trackLocationState.eventSink(ShowLocationEvents.RequestPermissions) trackLocationState.eventSink(ShowLocationEvents.RequestPermissions)
assertThat(permissionsPresenterFake.events.last()).isEqualTo(PermissionsEvents.RequestPermissions) assertThat(fakePermissionsPresenter.events.last()).isEqualTo(PermissionsEvents.RequestPermissions)
} }
} }
@Test @Test
fun `permission denied dialog dismiss`() = runTest { fun `permission denied dialog dismiss`() = runTest {
permissionsPresenterFake.givenState( fakePermissionsPresenter.givenState(
aPermissionsState( aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted, permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = false, shouldShowRationale = false,
@ -255,7 +255,7 @@ class ShowLocationPresenterTest {
@Test @Test
fun `open settings activity`() = runTest { fun `open settings activity`() = runTest {
permissionsPresenterFake.givenState( fakePermissionsPresenter.givenState(
aPermissionsState( aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted, permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = false, shouldShowRationale = false,

View file

@ -16,17 +16,19 @@
package io.element.android.features.lockscreen.api package io.element.android.features.lockscreen.api
import android.content.Context
import android.content.Intent
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import io.element.android.libraries.architecture.FeatureEntryPoint import io.element.android.libraries.architecture.FeatureEntryPoint
interface LockScreenEntryPoint : FeatureEntryPoint { interface LockScreenEntryPoint : FeatureEntryPoint {
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder fun nodeBuilder(parentNode: Node, buildContext: BuildContext, navTarget: Target): NodeBuilder
fun pinUnlockIntent(context: Context): Intent
interface NodeBuilder { interface NodeBuilder {
fun callback(callback: Callback): NodeBuilder fun callback(callback: Callback): NodeBuilder
fun target(target: Target): NodeBuilder
fun build(): Node fun build(): Node
} }
@ -37,6 +39,5 @@ interface LockScreenEntryPoint : FeatureEntryPoint {
enum class Target { enum class Target {
Settings, Settings,
Setup, Setup,
Unlock
} }
} }

View file

@ -0,0 +1,25 @@
<!--
~ Copyright (c) 2024 New Vector Ltd
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name="io.element.android.features.lockscreen.impl.unlock.activity.PinUnlockActivity"
android:configChanges="screenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode"/>
</application>
</manifest>

View file

@ -16,18 +16,20 @@
package io.element.android.features.lockscreen.impl package io.element.android.features.lockscreen.impl
import android.content.Context
import android.content.Intent
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.lockscreen.api.LockScreenEntryPoint import io.element.android.features.lockscreen.api.LockScreenEntryPoint
import io.element.android.features.lockscreen.impl.unlock.activity.PinUnlockActivity
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import javax.inject.Inject import javax.inject.Inject
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DefaultLockScreenEntryPoint @Inject constructor() : LockScreenEntryPoint { class DefaultLockScreenEntryPoint @Inject constructor() : LockScreenEntryPoint {
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): LockScreenEntryPoint.NodeBuilder { override fun nodeBuilder(parentNode: Node, buildContext: BuildContext, navTarget: LockScreenEntryPoint.Target): LockScreenEntryPoint.NodeBuilder {
var innerTarget: LockScreenEntryPoint.Target = LockScreenEntryPoint.Target.Unlock
val callbacks = mutableListOf<LockScreenEntryPoint.Callback>() val callbacks = mutableListOf<LockScreenEntryPoint.Callback>()
return object : LockScreenEntryPoint.NodeBuilder { return object : LockScreenEntryPoint.NodeBuilder {
@ -36,15 +38,9 @@ class DefaultLockScreenEntryPoint @Inject constructor() : LockScreenEntryPoint {
return this return this
} }
override fun target(target: LockScreenEntryPoint.Target): LockScreenEntryPoint.NodeBuilder {
innerTarget = target
return this
}
override fun build(): Node { override fun build(): Node {
val inputs = LockScreenFlowNode.Inputs( val inputs = LockScreenFlowNode.Inputs(
when (innerTarget) { when (navTarget) {
LockScreenEntryPoint.Target.Unlock -> LockScreenFlowNode.NavTarget.Unlock
LockScreenEntryPoint.Target.Setup -> LockScreenFlowNode.NavTarget.Setup LockScreenEntryPoint.Target.Setup -> LockScreenFlowNode.NavTarget.Setup
LockScreenEntryPoint.Target.Settings -> LockScreenFlowNode.NavTarget.Settings LockScreenEntryPoint.Target.Settings -> LockScreenFlowNode.NavTarget.Settings
} }
@ -54,4 +50,8 @@ class DefaultLockScreenEntryPoint @Inject constructor() : LockScreenEntryPoint {
} }
} }
} }
override fun pinUnlockIntent(context: Context): Intent {
return PinUnlockActivity.newIntent(context)
}
} }

View file

@ -30,7 +30,6 @@ import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.lockscreen.api.LockScreenEntryPoint import io.element.android.features.lockscreen.api.LockScreenEntryPoint
import io.element.android.features.lockscreen.impl.settings.LockScreenSettingsFlowNode import io.element.android.features.lockscreen.impl.settings.LockScreenSettingsFlowNode
import io.element.android.features.lockscreen.impl.setup.LockScreenSetupFlowNode import io.element.android.features.lockscreen.impl.setup.LockScreenSetupFlowNode
import io.element.android.features.lockscreen.impl.unlock.PinUnlockNode
import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
@ -44,20 +43,17 @@ class LockScreenFlowNode @AssistedInject constructor(
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
) : BaseFlowNode<LockScreenFlowNode.NavTarget>( ) : BaseFlowNode<LockScreenFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
initialElement = plugins.filterIsInstance(Inputs::class.java).first().initialNavTarget, initialElement = plugins.filterIsInstance<Inputs>().first().initialNavTarget,
savedStateMap = buildContext.savedStateMap, savedStateMap = buildContext.savedStateMap,
), ),
buildContext = buildContext, buildContext = buildContext,
plugins = plugins, plugins = plugins,
) { ) {
data class Inputs( data class Inputs(
val initialNavTarget: NavTarget = NavTarget.Unlock, val initialNavTarget: NavTarget,
) : NodeInputs ) : NodeInputs
sealed interface NavTarget : Parcelable { sealed interface NavTarget : Parcelable {
@Parcelize
data object Unlock : NavTarget
@Parcelize @Parcelize
data object Setup : NavTarget data object Setup : NavTarget
@ -75,10 +71,6 @@ class LockScreenFlowNode @AssistedInject constructor(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) { return when (navTarget) {
NavTarget.Unlock -> {
val inputs = PinUnlockNode.Inputs(isInAppUnlock = false)
createNode<PinUnlockNode>(buildContext, plugins = listOf(inputs))
}
NavTarget.Setup -> { NavTarget.Setup -> {
val callback = OnSetupDoneCallback(plugins()) val callback = OnSetupDoneCallback(plugins())
createNode<LockScreenSetupFlowNode>(buildContext, plugins = listOf(callback)) createNode<LockScreenSetupFlowNode>(buildContext, plugins = listOf(callback))

View file

@ -103,13 +103,12 @@ class LockScreenSettingsFlowNode @AssistedInject constructor(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) { return when (navTarget) {
NavTarget.Unlock -> { NavTarget.Unlock -> {
val inputs = PinUnlockNode.Inputs(isInAppUnlock = true)
val callback = object : PinUnlockNode.Callback { val callback = object : PinUnlockNode.Callback {
override fun onUnlock() { override fun onUnlock() {
backstack.newRoot(NavTarget.Settings) backstack.newRoot(NavTarget.Settings)
} }
} }
createNode<PinUnlockNode>(buildContext, plugins = listOf(inputs, callback)) createNode<PinUnlockNode>(buildContext, plugins = listOf(callback))
} }
NavTarget.SetupPin -> { NavTarget.SetupPin -> {
createNode<SetupPinNode>(buildContext) createNode<SetupPinNode>(buildContext)

View file

@ -43,7 +43,7 @@ fun LockScreenSettingsView(
onBackPressed = onBackPressed, onBackPressed = onBackPressed,
modifier = modifier modifier = modifier
) { ) {
PreferenceCategory(showDivider = false) { PreferenceCategory(showTopDivider = false) {
PreferenceText( PreferenceText(
title = stringResource(id = R.string.screen_app_lock_settings_change_pin), title = stringResource(id = R.string.screen_app_lock_settings_change_pin),
onClick = onChangePinClicked onClick = onChangePinClicked

View file

@ -26,8 +26,6 @@ import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class) @ContributesNode(SessionScope::class)
@ -40,12 +38,6 @@ class PinUnlockNode @AssistedInject constructor(
fun onUnlock() fun onUnlock()
} }
data class Inputs(
val isInAppUnlock: Boolean
) : NodeInputs
private val inputs: Inputs = inputs()
private fun onUnlock() { private fun onUnlock() {
plugins<Callback>().forEach { plugins<Callback>().forEach {
it.onUnlock() it.onUnlock()
@ -62,7 +54,9 @@ class PinUnlockNode @AssistedInject constructor(
} }
PinUnlockView( PinUnlockView(
state = state, state = state,
isInAppUnlock = inputs.isInAppUnlock, // UnlockNode is only used for in-app unlock, so we can safely set isInAppUnlock to true.
// It's set to false in PinUnlockActivity.
isInAppUnlock = true,
modifier = modifier modifier = modifier
) )
} }

View file

@ -29,11 +29,11 @@ import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockMana
import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.features.lockscreen.impl.pin.PinCodeManager
import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel
import io.element.android.features.lockscreen.impl.unlock.signout.SignOut
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.matrix.api.MatrixClient
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@ -41,7 +41,7 @@ import javax.inject.Inject
class PinUnlockPresenter @Inject constructor( class PinUnlockPresenter @Inject constructor(
private val pinCodeManager: PinCodeManager, private val pinCodeManager: PinCodeManager,
private val biometricUnlockManager: BiometricUnlockManager, private val biometricUnlockManager: BiometricUnlockManager,
private val matrixClient: MatrixClient, private val signOut: SignOut,
private val coroutineScope: CoroutineScope, private val coroutineScope: CoroutineScope,
private val pinUnlockHelper: PinUnlockHelper, private val pinUnlockHelper: PinUnlockHelper,
) : Presenter<PinUnlockState> { ) : Presenter<PinUnlockState> {
@ -179,7 +179,7 @@ class PinUnlockPresenter @Inject constructor(
private fun CoroutineScope.signOut(signOutAction: MutableState<AsyncData<String?>>) = launch { private fun CoroutineScope.signOut(signOutAction: MutableState<AsyncData<String?>>) = launch {
suspend { suspend {
matrixClient.logout(ignoreSdkError = true) signOut()
}.runCatchingUpdatingState(signOutAction) }.runCatchingUpdatingState(signOutAction)
} }
} }

View file

@ -0,0 +1,71 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.lockscreen.impl.unlock.activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.OnBackPressedCallback
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.lockscreen.api.LockScreenLockState
import io.element.android.features.lockscreen.api.LockScreenService
import io.element.android.features.lockscreen.impl.unlock.PinUnlockPresenter
import io.element.android.features.lockscreen.impl.unlock.PinUnlockView
import io.element.android.features.lockscreen.impl.unlock.di.PinUnlockBindings
import io.element.android.libraries.architecture.bindings
import kotlinx.coroutines.launch
import javax.inject.Inject
class PinUnlockActivity : AppCompatActivity() {
internal companion object {
fun newIntent(context: Context): Intent {
return Intent(context, PinUnlockActivity::class.java)
}
}
@Inject lateinit var presenter: PinUnlockPresenter
@Inject lateinit var lockScreenService: LockScreenService
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
bindings<PinUnlockBindings>().inject(this)
setContent {
ElementTheme {
val state = presenter.present()
PinUnlockView(state = state, isInAppUnlock = false)
}
}
lifecycleScope.launch {
lockScreenService.lockState.collect { state ->
if (state == LockScreenLockState.Unlocked) {
finish()
}
}
}
val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
moveTaskToBack(true)
}
}
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
}
}

View file

@ -0,0 +1,26 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.lockscreen.impl.unlock.di
import com.squareup.anvil.annotations.ContributesTo
import io.element.android.features.lockscreen.impl.unlock.activity.PinUnlockActivity
import io.element.android.libraries.di.AppScope
@ContributesTo(AppScope::class)
interface PinUnlockBindings {
fun inject(activity: PinUnlockActivity)
}

View file

@ -0,0 +1,40 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.lockscreen.impl.unlock.signout
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultSignOut @Inject constructor(
private val authenticationService: MatrixAuthenticationService,
private val matrixClientProvider: MatrixClientProvider,
) : SignOut {
override suspend fun invoke(): String? {
val currentSession = authenticationService.getLatestSessionId()
return if (currentSession != null) {
matrixClientProvider.getOrRestore(currentSession)
.getOrThrow()
.logout(ignoreSdkError = true)
} else {
error("No session to sign out")
}
}
}

View file

@ -0,0 +1,21 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.lockscreen.impl.unlock.signout
interface SignOut {
suspend operator fun invoke(): String?
}

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_forgot_pin">"დაგავიწყდათ PIN?"</string>
<string name="screen_app_lock_settings_change_pin">"PIN კოდის შეცვლა"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"ბიომეტრიული განბლოკვის დაშვება"</string>
<string name="screen_app_lock_settings_remove_pin">"პინ კოდის წაშლა"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"დარწმუნებული ხართ, რომ გსურთ PIN-ის წაშლა?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"გსურთ PIN-ის წაშლა?"</string>
<string name="screen_app_lock_setup_confirm_pin">"დაადასტურეთ PIN"</string>
<string name="screen_app_lock_signout_alert_message">"გასაგრძელებლად საჭიროა ხელახლა შესვლა და ახალი PIN-ის შექმნა"</string>
<string name="screen_app_lock_signout_alert_title">"თქვენ ახლა გადიხართ…"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"თქვენ გაქვთ %1$d მცდელობა განსაბლოკად"</item>
<item quantity="other">"თქვენ გაქვთ %1$d მცდელობა განსაბლოკად"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"არასწორი PIN. თქვენ %1$d შანსი დაგრჩათ"</item>
<item quantity="other">"არასწორი PIN. თქვენ %1$d შანსი დაგრჩათ"</item>
</plurals>
<string name="screen_signout_in_progress_dialog_content">"გასვლა…"</string>
</resources>

View file

@ -0,0 +1,61 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.lockscreen.impl.unlock
import com.google.common.truth.Truth.assertThat
import io.element.android.features.lockscreen.impl.unlock.signout.DefaultSignOut
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.test.runTest
import org.junit.Test
class DefaultSignOutTest {
private val matrixClient = FakeMatrixClient()
private val authenticationService = FakeMatrixAuthenticationService()
private val matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(matrixClient) })
private val sut = DefaultSignOut(authenticationService, matrixClientProvider)
@Test
fun `when no active session then it throws`() = runTest {
authenticationService.getLatestSessionIdLambda = { null }
val result = runCatching { sut.invoke() }
assertThat(result.isFailure).isTrue()
}
@Test
fun `with one active session and successful logout on client`() = runTest {
val logoutLambda = lambdaRecorder<Boolean, String?> { _: Boolean -> null }
authenticationService.getLatestSessionIdLambda = { matrixClient.sessionId }
matrixClient.logoutLambda = logoutLambda
val result = runCatching { sut.invoke() }
assertThat(result.isSuccess).isTrue()
assert(logoutLambda).isCalledOnce()
}
@Test
fun `with one active session and and failed logout on client`() = runTest {
val logoutLambda = lambdaRecorder<Boolean, String?> { _: Boolean -> error("Failed to logout") }
authenticationService.getLatestSessionIdLambda = { matrixClient.sessionId }
matrixClient.logoutLambda = logoutLambda
val result = runCatching { sut.invoke() }
assertThat(result.isFailure).isTrue()
assert(logoutLambda).isCalledOnce()
}
}

View file

@ -0,0 +1,28 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.lockscreen.impl.unlock
import io.element.android.features.lockscreen.impl.unlock.signout.SignOut
import io.element.android.tests.testutils.simulateLongTask
class FakeSignOut(
var lambda: () -> String? = { null }
) : SignOut {
override suspend fun invoke(): String? = simulateLongTask {
lambda()
}
}

View file

@ -28,8 +28,10 @@ import io.element.android.features.lockscreen.impl.pin.PinCodeManager
import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.features.lockscreen.impl.pin.model.assertText import io.element.android.features.lockscreen.impl.pin.model.assertText
import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel
import io.element.android.features.lockscreen.impl.unlock.signout.SignOut
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@ -104,7 +106,9 @@ class PinUnlockPresenterTest {
@Test @Test
fun `present - forgot pin flow`() = runTest { fun `present - forgot pin flow`() = runTest {
val presenter = createPinUnlockPresenter(this) val signOutLambda = lambdaRecorder<String?> { null }
val signOut = FakeSignOut(signOutLambda)
val presenter = createPinUnlockPresenter(this, signOut = signOut)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -131,6 +135,7 @@ class PinUnlockPresenterTest {
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.signOutAction).isInstanceOf(AsyncData.Success::class.java) assertThat(state.signOutAction).isInstanceOf(AsyncData.Success::class.java)
} }
assert(signOutLambda).isCalledOnce().withNoParameter()
} }
} }
@ -142,6 +147,7 @@ class PinUnlockPresenterTest {
scope: CoroutineScope, scope: CoroutineScope,
biometricUnlockManager: BiometricUnlockManager = FakeBiometricUnlockManager(), biometricUnlockManager: BiometricUnlockManager = FakeBiometricUnlockManager(),
callback: PinCodeManager.Callback = DefaultPinCodeManagerCallback(), callback: PinCodeManager.Callback = DefaultPinCodeManagerCallback(),
signOut: SignOut = FakeSignOut(),
): PinUnlockPresenter { ): PinUnlockPresenter {
val pinCodeManager = aPinCodeManager().apply { val pinCodeManager = aPinCodeManager().apply {
addCallback(callback) addCallback(callback)
@ -150,7 +156,7 @@ class PinUnlockPresenterTest {
return PinUnlockPresenter( return PinUnlockPresenter(
pinCodeManager = pinCodeManager, pinCodeManager = pinCodeManager,
biometricUnlockManager = biometricUnlockManager, biometricUnlockManager = biometricUnlockManager,
matrixClient = FakeMatrixClient(), signOut = signOut,
coroutineScope = scope, coroutineScope = scope,
pinUnlockHelper = PinUnlockHelper(biometricUnlockManager, pinCodeManager), pinUnlockHelper = PinUnlockHelper(biometricUnlockManager, pinCodeManager),
) )

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_account_provider_change">"ანგარიშის მიმწოდებლის შეცვლა"</string>
<string name="screen_account_provider_form_hint">"სახლის სერვერის მისამართი"</string>
<string name="screen_account_provider_form_notice">"შეიყვანეთ საძიებო სიტყვა ან დომენის მისამართი."</string>
<string name="screen_account_provider_form_subtitle">"მოძებნეთ კომპანია, საზოგადოება ან კერძო სერვერი."</string>
<string name="screen_account_provider_form_title">"ანგარიშის მომწოდებლის მოძებნა"</string>
<string name="screen_account_provider_signin_subtitle">"აქ იქნება თქვენი საუბრები - ისევე, როგორც თქვენ ელ. ფოსტაში ინახება თქვენი ელ.წერილები."</string>
<string name="screen_account_provider_signin_title">"თქვენ აპირებთ შესვლას %s-ში"</string>
<string name="screen_account_provider_signup_subtitle">"აქ იქნება თქვენი საუბრები - ისევე, როგორც თქვენ ელ. ფოსტაში ინახება თქვენი ელ.წერილები."</string>
<string name="screen_account_provider_signup_title">"თქვენ აპირებთ ანგარიშის შექმნას %s-ში"</string>
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org არის დიდი, უფასო სერვერი საჯარო Matrix ქსელში უსაფრთხო, დეცენტრალიზებული კომუნიკაციისთვის, რომელსაც მართავს Matrix.org ფონდი."</string>
<string name="screen_change_account_provider_other">"სხვა"</string>
<string name="screen_change_account_provider_subtitle">"გამოიყენეთ სხვა ანგარიშის პროვაიდერი, როგორიცაა თქვენი პირადი სერვერი ან სამუშაო ანგარიში."</string>
<string name="screen_change_account_provider_title">"შეცვალეთ ანგარიშის მომწოდებელი"</string>
<string name="screen_change_server_error_invalid_homeserver">"ჩვენ ვერ მივაღწიეთ ამ სახლის სერვერს. გთხოვთ, შეამოწმოთ, რომ სწორად შეიყვანეთ სახლის სერვერის URL. თუ URL სწორია, დაუკავშირდით თქვენი სახლის სერვერის ადმინისტრატორს დამატებითი დახმარებისთვის."</string>
<string name="screen_change_server_error_no_sliding_sync_message">"ამჟამად ეს სერვერი მხარს არ უჭერს \"sliding sync\"-ს."</string>
<string name="screen_change_server_form_header">"სახლის სერვერის URL"</string>
<string name="screen_change_server_form_notice">"თქვენ შეგიძლიათ დაუკავშირდეთ მხოლოდ იმ სერვერს, რომელიც მხარს უჭერს \"sliding sync\"-ს. თქვენი სახლის სერვერის ადმინისტრატორს დასჭირდება მისი კონფიგურაცია.%1$s"</string>
<string name="screen_change_server_subtitle">"რა არის თქვენი სერვერის მისამართი?"</string>
<string name="screen_change_server_title">"აირჩიეთ თქვენი სერვერი"</string>
<string name="screen_login_error_deactivated_account">"ეს ანგარიში დეაქტივირებულია."</string>
<string name="screen_login_error_invalid_credentials">"არასწორი მომხმარებლის სახელი და/ან პაროლი"</string>
<string name="screen_login_error_invalid_user_id">"მოცემული მომხმარებლის იდენტიფიკატორი არასწორია. დასაშვები ფორმატი: @user:homeserver.org"</string>
<string name="screen_login_error_unsupported_authentication">"მოცემული სახლის სერვერი მხარს არ უჭერს პაროლით ან OIDC-ით შესვლას. გთხოვთ, დაუკავშირდეთ თქვენს ადმინისტრატორს ან აარჩიეთ სხვა სახლის სერვერი."</string>
<string name="screen_login_form_header">"შეიყვანეთ თქვენი დეტალები"</string>
<string name="screen_login_subtitle">"Matrix არის ღია ქსელი უსაფრთხო, დეცენტრალიზებული კომუნიკაციისთვის."</string>
<string name="screen_login_title">"კეთილი იყოს თქვენი მობრძანება!"</string>
<string name="screen_login_title_with_homeserver">"შესვლა %1$s-ში"</string>
<string name="screen_server_confirmation_change_server">"შეცვალეთ ანგარიშის მომწოდებელი"</string>
<string name="screen_server_confirmation_message_login_element_dot_io">"კერძო სერვერი Element-ის თანამშრომლებისთვის."</string>
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix არის ღია ქსელი უსაფრთხო, დეცენტრალიზებული კომუნიკაციისთვის."</string>
<string name="screen_server_confirmation_message_register">"აქ იქნება თქვენი საუბრები - ისევე, როგორც თქვენ ელ. ფოსტაში ინახება თქვენი ელ.წერილები."</string>
<string name="screen_server_confirmation_title_login">"თქვენ აპირებთ შესვლას %1$s-ში"</string>
<string name="screen_server_confirmation_title_register">"თქვენ აპირებთ ანგარიშის შექმნას %1$s-ში"</string>
<string name="screen_waitlist_message">"ახლა დიდი მოთხოვნაა %1$s-ზე %2$s-ში. დაბრუნდით რამდენიმე დღეში და სცადეთ ერთხელაც.
მადლობა მოთმენისათვის!"</string>
<string name="screen_waitlist_message_success">"კეთილი იყოს თქვენი მობრძანება %1$s-ში!"</string>
<string name="screen_waitlist_title">"თითქმის მზადაა."</string>
<string name="screen_waitlist_title_success">"თქვენ შეხვედით."</string>
</resources>

View file

@ -14,6 +14,8 @@
<string name="screen_change_account_provider_subtitle">"Använd en annan kontoleverantör, till exempel din egen privata server eller ett jobbkonto."</string> <string name="screen_change_account_provider_subtitle">"Använd en annan kontoleverantör, till exempel din egen privata server eller ett jobbkonto."</string>
<string name="screen_change_account_provider_title">"Byt kontoleverantör"</string> <string name="screen_change_account_provider_title">"Byt kontoleverantör"</string>
<string name="screen_change_server_error_invalid_homeserver">"Vi kunde inte nå den här hemservern. Kontrollera att du har angett hemserverns URL korrekt. Om URL:en är korrekt kontaktar du administratören för hemservern för ytterligare hjälp."</string> <string name="screen_change_server_error_invalid_homeserver">"Vi kunde inte nå den här hemservern. Kontrollera att du har angett hemserverns URL korrekt. Om URL:en är korrekt kontaktar du administratören för hemservern för ytterligare hjälp."</string>
<string name="screen_change_server_error_invalid_well_known">"Sliding Sync är inte tillgängligt på grund av ett problem i well-known-filen:
%1$s"</string>
<string name="screen_change_server_error_no_sliding_sync_message">"Den här servern stöder för närvarande inte sliding sync."</string> <string name="screen_change_server_error_no_sliding_sync_message">"Den här servern stöder för närvarande inte sliding sync."</string>
<string name="screen_change_server_form_header">"Hemserverns URL"</string> <string name="screen_change_server_form_header">"Hemserverns URL"</string>
<string name="screen_change_server_form_notice">"Du kan bara ansluta till en befintlig server som stöder sliding sync. Din hemserveradministratör måste konfigurera det. %1$s"</string> <string name="screen_change_server_form_notice">"Du kan bara ansluta till en befintlig server som stöder sliding sync. Din hemserveradministratör måste konfigurera det. %1$s"</string>
@ -22,6 +24,7 @@
<string name="screen_login_error_deactivated_account">"Detta konto har avaktiverats."</string> <string name="screen_login_error_deactivated_account">"Detta konto har avaktiverats."</string>
<string name="screen_login_error_invalid_credentials">"Felaktigt användarnamn och/eller lösenord"</string> <string name="screen_login_error_invalid_credentials">"Felaktigt användarnamn och/eller lösenord"</string>
<string name="screen_login_error_invalid_user_id">"Detta är inte en giltig användaridentifierare. Förväntat format: \'@användare:hemserver.org\'"</string> <string name="screen_login_error_invalid_user_id">"Detta är inte en giltig användaridentifierare. Förväntat format: \'@användare:hemserver.org\'"</string>
<string name="screen_login_error_refresh_tokens">"Den här servern är konfigurerad för att använda uppdateringstokens. Dessa stöds inte när du använder lösenordsbaserad inloggning."</string>
<string name="screen_login_error_unsupported_authentication">"Den valda hemservern stöder inte lösenord eller OIDC-inloggning. Kontakta administratören eller välj en annan hemserver."</string> <string name="screen_login_error_unsupported_authentication">"Den valda hemservern stöder inte lösenord eller OIDC-inloggning. Kontakta administratören eller välj en annan hemserver."</string>
<string name="screen_login_form_header">"Ange dina uppgifter"</string> <string name="screen_login_form_header">"Ange dina uppgifter"</string>
<string name="screen_login_subtitle">"Matrix är ett öppet nätverk för säker, decentraliserad kommunikation."</string> <string name="screen_login_subtitle">"Matrix är ett öppet nätverk för säker, decentraliserad kommunikation."</string>

View file

@ -25,7 +25,7 @@ import io.element.android.features.login.impl.accountprovider.AccountProviderDat
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.test.A_HOMESERVER import io.element.android.libraries.matrix.test.A_HOMESERVER
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
@ -38,7 +38,7 @@ class ChangeServerPresenterTest {
@Test @Test
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
val presenter = ChangeServerPresenter( val presenter = ChangeServerPresenter(
FakeAuthenticationService(), FakeMatrixAuthenticationService(),
AccountProviderDataSource() AccountProviderDataSource()
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
@ -51,7 +51,7 @@ class ChangeServerPresenterTest {
@Test @Test
fun `present - change server ok`() = runTest { fun `present - change server ok`() = runTest {
val authenticationService = FakeAuthenticationService() val authenticationService = FakeMatrixAuthenticationService()
val presenter = ChangeServerPresenter( val presenter = ChangeServerPresenter(
authenticationService, authenticationService,
AccountProviderDataSource() AccountProviderDataSource()
@ -72,7 +72,7 @@ class ChangeServerPresenterTest {
@Test @Test
fun `present - change server error`() = runTest { fun `present - change server error`() = runTest {
val authenticationService = FakeAuthenticationService() val authenticationService = FakeMatrixAuthenticationService()
val presenter = ChangeServerPresenter( val presenter = ChangeServerPresenter(
authenticationService, authenticationService,
AccountProviderDataSource() AccountProviderDataSource()

View file

@ -26,7 +26,7 @@ import io.element.android.features.login.api.oidc.OidcAction
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.auth.A_OIDC_DATA import io.element.android.libraries.matrix.test.auth.A_OIDC_DATA
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -41,7 +41,7 @@ class OidcPresenterTest {
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
val presenter = OidcPresenter( val presenter = OidcPresenter(
A_OIDC_DATA, A_OIDC_DATA,
FakeAuthenticationService(), FakeMatrixAuthenticationService(),
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@ -56,7 +56,7 @@ class OidcPresenterTest {
fun `present - go back`() = runTest { fun `present - go back`() = runTest {
val presenter = OidcPresenter( val presenter = OidcPresenter(
A_OIDC_DATA, A_OIDC_DATA,
FakeAuthenticationService(), FakeMatrixAuthenticationService(),
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@ -72,7 +72,7 @@ class OidcPresenterTest {
@Test @Test
fun `present - go back with failure`() = runTest { fun `present - go back with failure`() = runTest {
val authenticationService = FakeAuthenticationService() val authenticationService = FakeMatrixAuthenticationService()
val presenter = OidcPresenter( val presenter = OidcPresenter(
A_OIDC_DATA, A_OIDC_DATA,
authenticationService, authenticationService,
@ -95,7 +95,7 @@ class OidcPresenterTest {
fun `present - user cancels from webview`() = runTest { fun `present - user cancels from webview`() = runTest {
val presenter = OidcPresenter( val presenter = OidcPresenter(
A_OIDC_DATA, A_OIDC_DATA,
FakeAuthenticationService(), FakeMatrixAuthenticationService(),
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@ -113,7 +113,7 @@ class OidcPresenterTest {
fun `present - login success`() = runTest { fun `present - login success`() = runTest {
val presenter = OidcPresenter( val presenter = OidcPresenter(
A_OIDC_DATA, A_OIDC_DATA,
FakeAuthenticationService(), FakeMatrixAuthenticationService(),
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@ -128,7 +128,7 @@ class OidcPresenterTest {
@Test @Test
fun `present - login error`() = runTest { fun `present - login error`() = runTest {
val authenticationService = FakeAuthenticationService() val authenticationService = FakeMatrixAuthenticationService()
val presenter = OidcPresenter( val presenter = OidcPresenter(
A_OIDC_DATA, A_OIDC_DATA,
authenticationService, authenticationService,

View file

@ -23,7 +23,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.changeserver.ChangeServerPresenter import io.element.android.features.login.impl.changeserver.ChangeServerPresenter
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
@ -36,7 +36,7 @@ class ChangeAccountProviderPresenterTest {
@Test @Test
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
val changeServerPresenter = ChangeServerPresenter( val changeServerPresenter = ChangeServerPresenter(
FakeAuthenticationService(), FakeMatrixAuthenticationService(),
AccountProviderDataSource() AccountProviderDataSource()
) )
val presenter = ChangeAccountProviderPresenter( val presenter = ChangeAccountProviderPresenter(

View file

@ -30,7 +30,7 @@ import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.test.A_HOMESERVER import io.element.android.libraries.matrix.test.A_HOMESERVER
import io.element.android.libraries.matrix.test.A_HOMESERVER_OIDC import io.element.android.libraries.matrix.test.A_HOMESERVER_OIDC
import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.waitForPredicate import io.element.android.tests.testutils.waitForPredicate
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -57,7 +57,7 @@ class ConfirmAccountProviderPresenterTest {
@Test @Test
fun `present - continue password login`() = runTest { fun `present - continue password login`() = runTest {
val authenticationService = FakeAuthenticationService() val authenticationService = FakeMatrixAuthenticationService()
val presenter = createConfirmAccountProviderPresenter( val presenter = createConfirmAccountProviderPresenter(
matrixAuthenticationService = authenticationService, matrixAuthenticationService = authenticationService,
) )
@ -79,7 +79,7 @@ class ConfirmAccountProviderPresenterTest {
@Test @Test
fun `present - continue oidc`() = runTest { fun `present - continue oidc`() = runTest {
val authenticationService = FakeAuthenticationService() val authenticationService = FakeMatrixAuthenticationService()
val presenter = createConfirmAccountProviderPresenter( val presenter = createConfirmAccountProviderPresenter(
matrixAuthenticationService = authenticationService, matrixAuthenticationService = authenticationService,
) )
@ -101,7 +101,7 @@ class ConfirmAccountProviderPresenterTest {
@Test @Test
fun `present - oidc - cancel with failure`() = runTest { fun `present - oidc - cancel with failure`() = runTest {
val authenticationService = FakeAuthenticationService() val authenticationService = FakeMatrixAuthenticationService()
val defaultOidcActionFlow = DefaultOidcActionFlow() val defaultOidcActionFlow = DefaultOidcActionFlow()
val presenter = createConfirmAccountProviderPresenter( val presenter = createConfirmAccountProviderPresenter(
matrixAuthenticationService = authenticationService, matrixAuthenticationService = authenticationService,
@ -129,7 +129,7 @@ class ConfirmAccountProviderPresenterTest {
@Test @Test
fun `present - oidc - cancel with success`() = runTest { fun `present - oidc - cancel with success`() = runTest {
val authenticationService = FakeAuthenticationService() val authenticationService = FakeMatrixAuthenticationService()
val defaultOidcActionFlow = DefaultOidcActionFlow() val defaultOidcActionFlow = DefaultOidcActionFlow()
val presenter = createConfirmAccountProviderPresenter( val presenter = createConfirmAccountProviderPresenter(
matrixAuthenticationService = authenticationService, matrixAuthenticationService = authenticationService,
@ -156,7 +156,7 @@ class ConfirmAccountProviderPresenterTest {
@Test @Test
fun `present - oidc - success with failure`() = runTest { fun `present - oidc - success with failure`() = runTest {
val authenticationService = FakeAuthenticationService() val authenticationService = FakeMatrixAuthenticationService()
val defaultOidcActionFlow = DefaultOidcActionFlow() val defaultOidcActionFlow = DefaultOidcActionFlow()
val presenter = createConfirmAccountProviderPresenter( val presenter = createConfirmAccountProviderPresenter(
matrixAuthenticationService = authenticationService, matrixAuthenticationService = authenticationService,
@ -186,7 +186,7 @@ class ConfirmAccountProviderPresenterTest {
@Test @Test
fun `present - oidc - success with success`() = runTest { fun `present - oidc - success with success`() = runTest {
val authenticationService = FakeAuthenticationService() val authenticationService = FakeMatrixAuthenticationService()
val defaultOidcActionFlow = DefaultOidcActionFlow() val defaultOidcActionFlow = DefaultOidcActionFlow()
val defaultLoginUserStory = DefaultLoginUserStory().apply { val defaultLoginUserStory = DefaultLoginUserStory().apply {
setLoginFlowIsDone(false) setLoginFlowIsDone(false)
@ -219,7 +219,7 @@ class ConfirmAccountProviderPresenterTest {
@Test @Test
fun `present - submit fails`() = runTest { fun `present - submit fails`() = runTest {
val authenticationService = FakeAuthenticationService() val authenticationService = FakeMatrixAuthenticationService()
val presenter = createConfirmAccountProviderPresenter( val presenter = createConfirmAccountProviderPresenter(
matrixAuthenticationService = authenticationService, matrixAuthenticationService = authenticationService,
) )
@ -238,7 +238,7 @@ class ConfirmAccountProviderPresenterTest {
@Test @Test
fun `present - clear error`() = runTest { fun `present - clear error`() = runTest {
val authenticationService = FakeAuthenticationService() val authenticationService = FakeMatrixAuthenticationService()
val presenter = createConfirmAccountProviderPresenter( val presenter = createConfirmAccountProviderPresenter(
matrixAuthenticationService = authenticationService, matrixAuthenticationService = authenticationService,
) )
@ -267,7 +267,7 @@ class ConfirmAccountProviderPresenterTest {
private fun createConfirmAccountProviderPresenter( private fun createConfirmAccountProviderPresenter(
params: ConfirmAccountProviderPresenter.Params = ConfirmAccountProviderPresenter.Params(isAccountCreation = false), params: ConfirmAccountProviderPresenter.Params = ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(), accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(),
matrixAuthenticationService: MatrixAuthenticationService = FakeAuthenticationService(), matrixAuthenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(),
defaultOidcActionFlow: DefaultOidcActionFlow = DefaultOidcActionFlow(), defaultOidcActionFlow: DefaultOidcActionFlow = DefaultOidcActionFlow(),
defaultLoginUserStory: DefaultLoginUserStory = DefaultLoginUserStory(), defaultLoginUserStory: DefaultLoginUserStory = DefaultLoginUserStory(),
) = ConfirmAccountProviderPresenter( ) = ConfirmAccountProviderPresenter(

View file

@ -30,7 +30,7 @@ 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_SESSION_ID
import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
@ -42,7 +42,7 @@ class LoginPasswordPresenterTest {
@Test @Test
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
val authenticationService = FakeAuthenticationService() val authenticationService = FakeMatrixAuthenticationService()
val accountProviderDataSource = AccountProviderDataSource() val accountProviderDataSource = AccountProviderDataSource()
val loginUserStory = DefaultLoginUserStory() val loginUserStory = DefaultLoginUserStory()
val presenter = LoginPasswordPresenter( val presenter = LoginPasswordPresenter(
@ -63,7 +63,7 @@ class LoginPasswordPresenterTest {
@Test @Test
fun `present - enter login and password`() = runTest { fun `present - enter login and password`() = runTest {
val authenticationService = FakeAuthenticationService() val authenticationService = FakeMatrixAuthenticationService()
val accountProviderDataSource = AccountProviderDataSource() val accountProviderDataSource = AccountProviderDataSource()
val loginUserStory = DefaultLoginUserStory() val loginUserStory = DefaultLoginUserStory()
val presenter = LoginPasswordPresenter( val presenter = LoginPasswordPresenter(
@ -89,7 +89,7 @@ class LoginPasswordPresenterTest {
@Test @Test
fun `present - submit`() = runTest { fun `present - submit`() = runTest {
val authenticationService = FakeAuthenticationService() val authenticationService = FakeMatrixAuthenticationService()
val accountProviderDataSource = AccountProviderDataSource() val accountProviderDataSource = AccountProviderDataSource()
val loginUserStory = DefaultLoginUserStory().apply { setLoginFlowIsDone(false) } val loginUserStory = DefaultLoginUserStory().apply { setLoginFlowIsDone(false) }
val presenter = LoginPasswordPresenter( val presenter = LoginPasswordPresenter(
@ -118,7 +118,7 @@ class LoginPasswordPresenterTest {
@Test @Test
fun `present - submit with error`() = runTest { fun `present - submit with error`() = runTest {
val authenticationService = FakeAuthenticationService() val authenticationService = FakeMatrixAuthenticationService()
val accountProviderDataSource = AccountProviderDataSource() val accountProviderDataSource = AccountProviderDataSource()
val loginUserStory = DefaultLoginUserStory() val loginUserStory = DefaultLoginUserStory()
val presenter = LoginPasswordPresenter( val presenter = LoginPasswordPresenter(
@ -146,7 +146,7 @@ class LoginPasswordPresenterTest {
@Test @Test
fun `present - clear error`() = runTest { fun `present - clear error`() = runTest {
val authenticationService = FakeAuthenticationService() val authenticationService = FakeMatrixAuthenticationService()
val accountProviderDataSource = AccountProviderDataSource() val accountProviderDataSource = AccountProviderDataSource()
val loginUserStory = DefaultLoginUserStory() val loginUserStory = DefaultLoginUserStory()
val presenter = LoginPasswordPresenter( val presenter = LoginPasswordPresenter(

View file

@ -29,7 +29,7 @@ import io.element.android.features.login.impl.resolver.network.WellKnownBaseConf
import io.element.android.features.login.impl.resolver.network.WellKnownSlidingSyncConfig import io.element.android.features.login.impl.resolver.network.WellKnownSlidingSyncConfig
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -44,7 +44,7 @@ class SearchAccountProviderPresenterTest {
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
val fakeWellknownRequest = FakeWellknownRequest() val fakeWellknownRequest = FakeWellknownRequest()
val changeServerPresenter = ChangeServerPresenter( val changeServerPresenter = ChangeServerPresenter(
FakeAuthenticationService(), FakeMatrixAuthenticationService(),
AccountProviderDataSource() AccountProviderDataSource()
) )
val presenter = SearchAccountProviderPresenter( val presenter = SearchAccountProviderPresenter(
@ -64,7 +64,7 @@ class SearchAccountProviderPresenterTest {
fun `present - enter text no result`() = runTest { fun `present - enter text no result`() = runTest {
val fakeWellknownRequest = FakeWellknownRequest() val fakeWellknownRequest = FakeWellknownRequest()
val changeServerPresenter = ChangeServerPresenter( val changeServerPresenter = ChangeServerPresenter(
FakeAuthenticationService(), FakeMatrixAuthenticationService(),
AccountProviderDataSource() AccountProviderDataSource()
) )
val presenter = SearchAccountProviderPresenter( val presenter = SearchAccountProviderPresenter(
@ -88,7 +88,7 @@ class SearchAccountProviderPresenterTest {
fun `present - enter valid url no wellknown`() = runTest { fun `present - enter valid url no wellknown`() = runTest {
val fakeWellknownRequest = FakeWellknownRequest() val fakeWellknownRequest = FakeWellknownRequest()
val changeServerPresenter = ChangeServerPresenter( val changeServerPresenter = ChangeServerPresenter(
FakeAuthenticationService(), FakeMatrixAuthenticationService(),
AccountProviderDataSource() AccountProviderDataSource()
) )
val presenter = SearchAccountProviderPresenter( val presenter = SearchAccountProviderPresenter(
@ -123,7 +123,7 @@ class SearchAccountProviderPresenterTest {
) )
) )
val changeServerPresenter = ChangeServerPresenter( val changeServerPresenter = ChangeServerPresenter(
FakeAuthenticationService(), FakeMatrixAuthenticationService(),
AccountProviderDataSource() AccountProviderDataSource()
) )
val presenter = SearchAccountProviderPresenter( val presenter = SearchAccountProviderPresenter(
@ -158,7 +158,7 @@ class SearchAccountProviderPresenterTest {
) )
) )
val changeServerPresenter = ChangeServerPresenter( val changeServerPresenter = ChangeServerPresenter(
FakeAuthenticationService(), FakeMatrixAuthenticationService(),
AccountProviderDataSource() AccountProviderDataSource()
) )
val presenter = SearchAccountProviderPresenter( val presenter = SearchAccountProviderPresenter(

View file

@ -28,7 +28,7 @@ import io.element.android.libraries.matrix.test.A_HOMESERVER
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -41,7 +41,7 @@ class WaitListPresenterTest {
@Test @Test
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
val authenticationService = FakeAuthenticationService().apply { val authenticationService = FakeMatrixAuthenticationService().apply {
givenHomeserver(A_HOMESERVER) givenHomeserver(A_HOMESERVER)
} }
val loginUserStory = DefaultLoginUserStory() val loginUserStory = DefaultLoginUserStory()
@ -63,7 +63,7 @@ class WaitListPresenterTest {
@Test @Test
fun `present - attempt login with error`() = runTest { fun `present - attempt login with error`() = runTest {
val authenticationService = FakeAuthenticationService().apply { val authenticationService = FakeMatrixAuthenticationService().apply {
givenLoginError(A_THROWABLE) givenLoginError(A_THROWABLE)
} }
val loginUserStory = DefaultLoginUserStory() val loginUserStory = DefaultLoginUserStory()
@ -94,7 +94,7 @@ class WaitListPresenterTest {
@Test @Test
fun `present - attempt login with success`() = runTest { fun `present - attempt login with success`() = runTest {
val authenticationService = FakeAuthenticationService() val authenticationService = FakeMatrixAuthenticationService()
val loginUserStory = DefaultLoginUserStory().apply { setLoginFlowIsDone(false) } val loginUserStory = DefaultLoginUserStory().apply { setLoginFlowIsDone(false) }
val presenter = WaitListPresenter( val presenter = WaitListPresenter(
LoginFormState.Default, LoginFormState.Default,

View file

@ -55,7 +55,6 @@ dependencies {
testImplementation(libs.test.truth) testImplementation(libs.test.truth)
testImplementation(libs.test.turbine) testImplementation(libs.test.turbine)
testImplementation(libs.test.robolectric) testImplementation(libs.test.robolectric)
testImplementation(libs.test.junitext)
testImplementation(libs.androidx.compose.ui.test.junit) testImplementation(libs.androidx.compose.ui.test.junit)
testReleaseImplementation(libs.androidx.compose.ui.test.manifest) testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"დარწმუნებული ხართ, რომ გსურთ გამოსვლა?"</string>
<string name="screen_signout_confirmation_dialog_submit">"გამოსვლა"</string>
<string name="screen_signout_confirmation_dialog_title">"გამოსვლა"</string>
<string name="screen_signout_in_progress_dialog_content">"გასვლა…"</string>
<string name="screen_signout_preference_item">"გამოსვლა"</string>
</resources>

View file

@ -144,7 +144,9 @@ class LogoutPresenterTest {
@Test @Test
fun `present - logout with error then cancel`() = runTest { fun `present - logout with error then cancel`() = runTest {
val matrixClient = FakeMatrixClient().apply { val matrixClient = FakeMatrixClient().apply {
givenLogoutError(A_THROWABLE) logoutLambda = { _ ->
throw A_THROWABLE
}
} }
val presenter = createLogoutPresenter( val presenter = createLogoutPresenter(
matrixClient, matrixClient,
@ -170,7 +172,13 @@ class LogoutPresenterTest {
@Test @Test
fun `present - logout with error then force`() = runTest { fun `present - logout with error then force`() = runTest {
val matrixClient = FakeMatrixClient().apply { val matrixClient = FakeMatrixClient().apply {
givenLogoutError(A_THROWABLE) logoutLambda = { ignoreSdkError ->
if (!ignoreSdkError) {
throw A_THROWABLE
} else {
null
}
}
} }
val presenter = createLogoutPresenter( val presenter = createLogoutPresenter(
matrixClient, matrixClient,

View file

@ -125,7 +125,9 @@ class DefaultDirectLogoutPresenterTest {
@Test @Test
fun `present - logout with error then cancel`() = runTest { fun `present - logout with error then cancel`() = runTest {
val matrixClient = FakeMatrixClient().apply { val matrixClient = FakeMatrixClient().apply {
givenLogoutError(A_THROWABLE) logoutLambda = { _ ->
throw A_THROWABLE
}
} }
val presenter = createDefaultDirectLogoutPresenter( val presenter = createDefaultDirectLogoutPresenter(
matrixClient, matrixClient,
@ -151,7 +153,13 @@ class DefaultDirectLogoutPresenterTest {
@Test @Test
fun `present - logout with error then force`() = runTest { fun `present - logout with error then force`() = runTest {
val matrixClient = FakeMatrixClient().apply { val matrixClient = FakeMatrixClient().apply {
givenLogoutError(A_THROWABLE) logoutLambda = { ignoreSdkError ->
if (!ignoreSdkError) {
throw A_THROWABLE
} else {
null
}
}
} }
val presenter = createDefaultDirectLogoutPresenter( val presenter = createDefaultDirectLogoutPresenter(
matrixClient, matrixClient,

View file

@ -99,7 +99,6 @@ dependencies {
testImplementation(projects.libraries.mediaviewer.test) testImplementation(projects.libraries.mediaviewer.test)
testImplementation(projects.libraries.testtags) testImplementation(projects.libraries.testtags)
testImplementation(libs.test.mockk) testImplementation(libs.test.mockk)
testImplementation(libs.test.junitext)
testImplementation(libs.test.robolectric) testImplementation(libs.test.robolectric)
testImplementation(projects.features.poll.test) testImplementation(projects.features.poll.test)
testImplementation(projects.features.poll.impl) testImplementation(projects.features.poll.impl)

View file

@ -31,6 +31,7 @@ import androidx.compose.runtime.setValue
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.appconfig.MessageComposerConfig
import io.element.android.features.messages.api.timeline.HtmlConverterProvider import io.element.android.features.messages.api.timeline.HtmlConverterProvider
import io.element.android.features.messages.impl.actionlist.ActionListEvents import io.element.android.features.messages.impl.actionlist.ActionListEvents
import io.element.android.features.messages.impl.actionlist.ActionListPresenter import io.element.android.features.messages.impl.actionlist.ActionListPresenter
@ -66,7 +67,6 @@ import io.element.android.features.messages.impl.utils.messagesummary.MessageSum
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter
import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
@ -113,7 +113,6 @@ class MessagesPresenter @AssistedInject constructor(
private val messageSummaryFormatter: MessageSummaryFormatter, private val messageSummaryFormatter: MessageSummaryFormatter,
private val dispatchers: CoroutineDispatchers, private val dispatchers: CoroutineDispatchers,
private val clipboardHelper: ClipboardHelper, private val clipboardHelper: ClipboardHelper,
private val appPreferencesStore: AppPreferencesStore,
private val featureFlagsService: FeatureFlagService, private val featureFlagsService: FeatureFlagService,
private val htmlConverterProvider: HtmlConverterProvider, private val htmlConverterProvider: HtmlConverterProvider,
@Assisted private val navigator: MessagesNavigator, @Assisted private val navigator: MessagesNavigator,
@ -171,17 +170,15 @@ class MessagesPresenter @AssistedInject constructor(
val inviteProgress = remember { mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized) } val inviteProgress = remember { mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized) }
var showReinvitePrompt by remember { mutableStateOf(false) } var showReinvitePrompt by remember { mutableStateOf(false) }
LaunchedEffect(hasDismissedInviteDialog, composerState.hasFocus, syncUpdateFlow.value) { LaunchedEffect(hasDismissedInviteDialog, composerState.textEditorState.hasFocus(), syncUpdateFlow.value) {
withContext(dispatchers.io) { withContext(dispatchers.io) {
showReinvitePrompt = !hasDismissedInviteDialog && composerState.hasFocus && room.isDirect && room.activeMemberCount == 1L showReinvitePrompt = !hasDismissedInviteDialog && composerState.textEditorState.hasFocus() && room.isDirect && room.activeMemberCount == 1L
} }
} }
val networkConnectionStatus by networkMonitor.connectivity.collectAsState() val networkConnectionStatus by networkMonitor.connectivity.collectAsState()
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState() val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
val enableTextFormatting by appPreferencesStore.isRichTextEditorEnabledFlow().collectAsState(initial = true)
var enableVoiceMessages by remember { mutableStateOf(false) } var enableVoiceMessages by remember { mutableStateOf(false) }
LaunchedEffect(featureFlagsService) { LaunchedEffect(featureFlagsService) {
enableVoiceMessages = featureFlagsService.isFeatureEnabled(FeatureFlags.VoiceMessages) enableVoiceMessages = featureFlagsService.isFeatureEnabled(FeatureFlags.VoiceMessages)
@ -194,7 +191,7 @@ class MessagesPresenter @AssistedInject constructor(
action = event.action, action = event.action,
targetEvent = event.event, targetEvent = event.event,
composerState = composerState, composerState = composerState,
enableTextFormatting = enableTextFormatting, enableTextFormatting = composerState.showTextFormatting,
timelineState = timelineState, timelineState = timelineState,
) )
} }
@ -239,7 +236,7 @@ class MessagesPresenter @AssistedInject constructor(
snackbarMessage = snackbarMessage, snackbarMessage = snackbarMessage,
showReinvitePrompt = showReinvitePrompt, showReinvitePrompt = showReinvitePrompt,
inviteProgress = inviteProgress.value, inviteProgress = inviteProgress.value,
enableTextFormatting = enableTextFormatting, enableTextFormatting = MessageComposerConfig.ENABLE_RICH_TEXT_EDITING,
enableVoiceMessages = enableVoiceMessages, enableVoiceMessages = enableVoiceMessages,
appName = buildMeta.applicationName, appName = buildMeta.applicationName,
callState = callState, callState = callState,

View file

@ -45,6 +45,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.textcomposer.aRichTextEditorState import io.element.android.libraries.textcomposer.aRichTextEditorState
import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.libraries.textcomposer.model.TextEditorState
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.persistentSetOf
@ -99,9 +100,9 @@ fun aMessagesState(
userHasPermissionToRedactOther: Boolean = false, userHasPermissionToRedactOther: Boolean = false,
userHasPermissionToSendReaction: Boolean = true, userHasPermissionToSendReaction: Boolean = true,
composerState: MessageComposerState = aMessageComposerState( composerState: MessageComposerState = aMessageComposerState(
richTextEditorState = aRichTextEditorState(initialText = "Hello", initialFocus = true), textEditorState = TextEditorState.Rich(aRichTextEditorState(initialText = "Hello", initialFocus = true)),
isFullScreen = false, isFullScreen = false,
mode = MessageComposerMode.Normal, mode = MessageComposerMode.Normal,
), ),
voiceMessageComposerState: VoiceMessageComposerState = aVoiceMessageComposerState(), voiceMessageComposerState: VoiceMessageComposerState = aVoiceMessageComposerState(),
timelineState: TimelineState = aTimelineState( timelineState: TimelineState = aTimelineState(

View file

@ -362,7 +362,7 @@ private fun MessagesViewContent(
// Any state change that should trigger a height size should be added to the list of remembered values here. // Any state change that should trigger a height size should be added to the list of remembered values here.
val sheetResizeContentKey = remember { mutableIntStateOf(0) } val sheetResizeContentKey = remember { mutableIntStateOf(0) }
LaunchedEffect( LaunchedEffect(
state.composerState.richTextEditorState.lineCount, state.composerState.textEditorState.lineCount,
state.composerState.showTextFormatting, state.composerState.showTextFormatting,
) { ) {
sheetResizeContentKey.intValue = Random.nextInt() sheetResizeContentKey.intValue = Random.nextInt()
@ -439,7 +439,6 @@ private fun MessagesViewComposerBottomSheetContents(
state = state.composerState, state = state.composerState,
voiceMessageState = state.voiceMessageComposerState, voiceMessageState = state.voiceMessageComposerState,
subcomposing = subcomposing, subcomposing = subcomposing,
enableTextFormatting = state.enableTextFormatting,
enableVoiceMessages = state.enableVoiceMessages, enableVoiceMessages = state.enableVoiceMessages,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )

View file

@ -43,6 +43,7 @@ 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.core.UserId
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.textcomposer.mentions.ResolvedMentionSuggestion
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -51,8 +52,8 @@ fun MentionSuggestionsPickerView(
roomId: RoomId, roomId: RoomId,
roomName: String?, roomName: String?,
roomAvatarData: AvatarData?, roomAvatarData: AvatarData?,
memberSuggestions: ImmutableList<MentionSuggestion>, memberSuggestions: ImmutableList<ResolvedMentionSuggestion>,
onSuggestionSelected: (MentionSuggestion) -> Unit, onSuggestionSelected: (ResolvedMentionSuggestion) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
LazyColumn( LazyColumn(
@ -62,8 +63,8 @@ fun MentionSuggestionsPickerView(
memberSuggestions, memberSuggestions,
key = { suggestion -> key = { suggestion ->
when (suggestion) { when (suggestion) {
is MentionSuggestion.Room -> "@room" is ResolvedMentionSuggestion.AtRoom -> "@room"
is MentionSuggestion.Member -> suggestion.roomMember.userId.value is ResolvedMentionSuggestion.Member -> suggestion.roomMember.userId.value
} }
} }
) { ) {
@ -84,18 +85,18 @@ fun MentionSuggestionsPickerView(
@Composable @Composable
private fun RoomMemberSuggestionItemView( private fun RoomMemberSuggestionItemView(
memberSuggestion: MentionSuggestion, memberSuggestion: ResolvedMentionSuggestion,
roomId: String, roomId: String,
roomName: String?, roomName: String?,
roomAvatar: AvatarData?, roomAvatar: AvatarData?,
onSuggestionSelected: (MentionSuggestion) -> Unit, onSuggestionSelected: (ResolvedMentionSuggestion) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Row(modifier = modifier.clickable { onSuggestionSelected(memberSuggestion) }, horizontalArrangement = Arrangement.spacedBy(16.dp)) { Row(modifier = modifier.clickable { onSuggestionSelected(memberSuggestion) }, horizontalArrangement = Arrangement.spacedBy(16.dp)) {
val avatarSize = AvatarSize.TimelineRoom val avatarSize = AvatarSize.TimelineRoom
val avatarData = when (memberSuggestion) { val avatarData = when (memberSuggestion) {
is MentionSuggestion.Room -> roomAvatar?.copy(size = avatarSize) ?: AvatarData(roomId, roomName, null, avatarSize) is ResolvedMentionSuggestion.AtRoom -> roomAvatar?.copy(size = avatarSize) ?: AvatarData(roomId, roomName, null, avatarSize)
is MentionSuggestion.Member -> AvatarData( is ResolvedMentionSuggestion.Member -> AvatarData(
memberSuggestion.roomMember.userId.value, memberSuggestion.roomMember.userId.value,
memberSuggestion.roomMember.displayName, memberSuggestion.roomMember.displayName,
memberSuggestion.roomMember.avatarUrl, memberSuggestion.roomMember.avatarUrl,
@ -103,13 +104,13 @@ private fun RoomMemberSuggestionItemView(
) )
} }
val title = when (memberSuggestion) { val title = when (memberSuggestion) {
is MentionSuggestion.Room -> stringResource(R.string.screen_room_mentions_at_room_title) is ResolvedMentionSuggestion.AtRoom -> stringResource(R.string.screen_room_mentions_at_room_title)
is MentionSuggestion.Member -> memberSuggestion.roomMember.displayName is ResolvedMentionSuggestion.Member -> memberSuggestion.roomMember.displayName
} }
val subtitle = when (memberSuggestion) { val subtitle = when (memberSuggestion) {
is MentionSuggestion.Room -> "@room" is ResolvedMentionSuggestion.AtRoom -> "@room"
is MentionSuggestion.Member -> memberSuggestion.roomMember.userId.value is ResolvedMentionSuggestion.Member -> memberSuggestion.roomMember.userId.value
} }
Avatar(avatarData = avatarData, modifier = Modifier.padding(start = 16.dp, top = 12.dp, bottom = 12.dp)) Avatar(avatarData = avatarData, modifier = Modifier.padding(start = 16.dp, top = 12.dp, bottom = 12.dp))
@ -159,9 +160,9 @@ internal fun MentionSuggestionsPickerViewPreview() {
roomName = "Room", roomName = "Room",
roomAvatarData = null, roomAvatarData = null,
memberSuggestions = persistentListOf( memberSuggestions = persistentListOf(
MentionSuggestion.Room, ResolvedMentionSuggestion.AtRoom,
MentionSuggestion.Member(roomMember), ResolvedMentionSuggestion.Member(roomMember),
MentionSuggestion.Member(roomMember.copy(userId = UserId("@bob:server.org"), displayName = "Bob")), ResolvedMentionSuggestion.Member(roomMember.copy(userId = UserId("@bob:server.org"), displayName = "Bob")),
), ),
onSuggestionSelected = {} onSuggestionSelected = {}
) )

View file

@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.api.room.roomMembers
import io.element.android.libraries.textcomposer.mentions.ResolvedMentionSuggestion
import io.element.android.libraries.textcomposer.model.Suggestion import io.element.android.libraries.textcomposer.model.Suggestion
import io.element.android.libraries.textcomposer.model.SuggestionType import io.element.android.libraries.textcomposer.model.SuggestionType
@ -45,7 +46,7 @@ object MentionSuggestionsProcessor {
roomMembersState: MatrixRoomMembersState, roomMembersState: MatrixRoomMembersState,
currentUserId: UserId, currentUserId: UserId,
canSendRoomMention: suspend () -> Boolean, canSendRoomMention: suspend () -> Boolean,
): List<MentionSuggestion> { ): List<ResolvedMentionSuggestion> {
val members = roomMembersState.roomMembers() val members = roomMembersState.roomMembers()
return when { return when {
members.isNullOrEmpty() || suggestion == null -> { members.isNullOrEmpty() || suggestion == null -> {
@ -78,7 +79,7 @@ object MentionSuggestionsProcessor {
roomMembers: List<RoomMember>?, roomMembers: List<RoomMember>?,
currentUserId: UserId, currentUserId: UserId,
canSendRoomMention: Boolean, canSendRoomMention: Boolean,
): List<MentionSuggestion> { ): List<ResolvedMentionSuggestion> {
return if (roomMembers.isNullOrEmpty()) { return if (roomMembers.isNullOrEmpty()) {
emptyList() emptyList()
} else { } else {
@ -96,10 +97,10 @@ object MentionSuggestionsProcessor {
.filterUpTo(MAX_BATCH_ITEMS) { member -> .filterUpTo(MAX_BATCH_ITEMS) { member ->
isJoinedMemberAndNotSelf(member) && memberMatchesQuery(member, query) isJoinedMemberAndNotSelf(member) && memberMatchesQuery(member, query)
} }
.map(MentionSuggestion::Member) .map(ResolvedMentionSuggestion::Member)
if ("room".contains(query) && canSendRoomMention) { if ("room".contains(query) && canSendRoomMention) {
listOf(MentionSuggestion.Room) + matchingMembers listOf(ResolvedMentionSuggestion.AtRoom) + matchingMembers
} else { } else {
matchingMembers matchingMembers
} }

View file

@ -18,15 +18,14 @@ package io.element.android.features.messages.impl.messagecomposer
import android.net.Uri import android.net.Uri
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import io.element.android.features.messages.impl.mentions.MentionSuggestion import io.element.android.libraries.textcomposer.mentions.ResolvedMentionSuggestion
import io.element.android.libraries.textcomposer.model.Message
import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.libraries.textcomposer.model.Suggestion import io.element.android.libraries.textcomposer.model.Suggestion
@Immutable @Immutable
sealed interface MessageComposerEvents { sealed interface MessageComposerEvents {
data object ToggleFullScreenState : MessageComposerEvents data object ToggleFullScreenState : MessageComposerEvents
data class SendMessage(val message: Message) : MessageComposerEvents data object SendMessage : MessageComposerEvents
data class SendUri(val uri: Uri) : MessageComposerEvents data class SendUri(val uri: Uri) : MessageComposerEvents
data object CloseSpecialMode : MessageComposerEvents data object CloseSpecialMode : MessageComposerEvents
data class SetMode(val composerMode: MessageComposerMode) : MessageComposerEvents data class SetMode(val composerMode: MessageComposerMode) : MessageComposerEvents
@ -45,5 +44,5 @@ sealed interface MessageComposerEvents {
data class Error(val error: Throwable) : MessageComposerEvents data class Error(val error: Throwable) : MessageComposerEvents
data class TypingNotice(val isTyping: Boolean) : MessageComposerEvents data class TypingNotice(val isTyping: Boolean) : MessageComposerEvents
data class SuggestionReceived(val suggestion: Suggestion?) : MessageComposerEvents data class SuggestionReceived(val suggestion: Suggestion?) : MessageComposerEvents
data class InsertMention(val mention: MentionSuggestion) : MessageComposerEvents data class InsertMention(val mention: ResolvedMentionSuggestion) : MessageComposerEvents
} }

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