Merge branch 'develop' into renovate/org.maplibre.gl-android-sdk-11.x

This commit is contained in:
ganfra 2024-05-29 15:43:25 +02:00 committed by GitHub
commit 395b3c4806
1249 changed files with 18041 additions and 8127 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

@ -33,7 +33,7 @@ jobs:
run: ./gradlew verifyPaparazziDebug $CI_GRADLE_ARG_PROPERTIES run: ./gradlew verifyPaparazziDebug $CI_GRADLE_ARG_PROPERTIES
- name: 📈 Generate kover report and verify coverage - name: 📈 Generate kover report and verify coverage
run: ./gradlew :app:koverXmlReportGplayDebug :app:koverHtmlReportGplayDebug :app:koverVerifyGplayDebug $CI_GRADLE_ARG_PROPERTIES run: ./gradlew :app:koverXmlReportGplayDebug :app:koverHtmlReportGplayDebug :app:koverVerifyAll $CI_GRADLE_ARG_PROPERTIES
- name: ✅ Upload kover report - name: ✅ Upload kover report
if: always() if: always()

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

@ -55,7 +55,7 @@ jobs:
run: ./gradlew verifyPaparazziDebug $CI_GRADLE_ARG_PROPERTIES run: ./gradlew verifyPaparazziDebug $CI_GRADLE_ARG_PROPERTIES
- name: 📈Generate kover report and verify coverage - name: 📈Generate kover report and verify coverage
run: ./gradlew :app:koverXmlReportGplayDebug :app:koverHtmlReportGplayDebug :app:koverVerifyGplayDebug $CI_GRADLE_ARG_PROPERTIES run: ./gradlew :app:koverXmlReportGplayDebug :app:koverHtmlReportGplayDebug :app:koverVerifyAll $CI_GRADLE_ARG_PROPERTIES
- name: 🚫 Upload kover failed coverage reports - name: 🚫 Upload kover failed coverage reports
if: failure() if: failure()

1
.gitignore vendored
View file

@ -45,6 +45,7 @@ captures/
.idea/assetWizardSettings.xml .idea/assetWizardSettings.xml
.idea/compiler.xml .idea/compiler.xml
.idea/deploymentTargetDropDown.xml .idea/deploymentTargetDropDown.xml
.idea/deploymentTargetSelector.xml
.idea/gradle.xml .idea/gradle.xml
.idea/jarRepositories.xml .idea/jarRepositories.xml
.idea/misc.xml .idea/misc.xml

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>

2
.idea/kotlinc.xml generated
View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="KotlinJpsPluginSettings"> <component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.23" /> <option name="version" value="1.9.24" />
</component> </component>
</project> </project>

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,64 @@
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)
=========================================
Features ✨
----------
- Add support for expected decryption errors due to membership (UX and analytics). ([#2754](https://github.com/element-hq/element-x-android/issues/2754))
- Handle permalink navigation to Events. ([#2759](https://github.com/element-hq/element-x-android/issues/2759))
- Pretty-print event JSON in debug viewer ([#2771](https://github.com/element-hq/element-x-android/issues/2771))
- Add support for external permalinks. ([#2776](https://github.com/element-hq/element-x-android/issues/2776))
- Enable support for Android per-app language preferences ([#2795](https://github.com/element-hq/element-x-android/issues/2795))
Bugfixes 🐛
----------
- Fix session verification being asked again for already verified users. ([#2718](https://github.com/element-hq/element-x-android/issues/2718))
- Instead of displaying 'create new recovery key' on the session verification screen when there is no other session active, display it always under the 'enter recovery key' screen. ([#2740](https://github.com/element-hq/element-x-android/issues/2740))
- Adjust the typography used in the selected user component so a user's display name fits better. ([#2760](https://github.com/element-hq/element-x-android/issues/2760))
- User display name overflows in timeline messages when it's way too long. ([#2761](https://github.com/element-hq/element-x-android/issues/2761))
- Ensure the application open the room when a notification is clicked. ([#2778](https://github.com/element-hq/element-x-android/issues/2778))
- Enforce mandatory session verification only for new logins. ([#2810](https://github.com/element-hq/element-x-android/issues/2810))
- Make log less verbose, make sure we upload as many log files as possible before reaching the request size limit of the bug reporting service, discard older logs if they don't fit. ([#2825](https://github.com/element-hq/element-x-android/issues/2825))
- Remove 'Join' button in room directory search results. ([#2827](https://github.com/element-hq/element-x-android/issues/2827))
- Add missing `app_id` and `Version` properties to bug reports. ([#2829](https://github.com/element-hq/element-x-android/issues/2829))
Other changes
-------------
- RoomMember screen: fallback to userProfile data, if the member is not a user of the room. ([#2721](https://github.com/element-hq/element-x-android/issues/2721))
- Migrate application data. ([#2749](https://github.com/element-hq/element-x-android/issues/2749))
- Let the SDK manage the file log cleanup, and keep one week of log. ([#2758](https://github.com/element-hq/element-x-android/issues/2758))
- UX cleanup: reorder options in the main settings screen. ([#2801](https://github.com/element-hq/element-x-android/issues/2801))
- Analytics: Add support to report current session verification and recovery state ([#2806](https://github.com/element-hq/element-x-android/issues/2806))
- UX cleanup: room details screen, add new CTA buttons for Invite and Call actions. ([#2814](https://github.com/element-hq/element-x-android/issues/2814))
- UX cleanup: user profile. Move send DM to a call to action button, add 'Call' CTA too. ([#2818](https://github.com/element-hq/element-x-android/issues/2818))
- Add room badges to room details screen. ([#2822](https://github.com/element-hq/element-x-android/issues/2822))
Security
-------------
- Bump the Rust SDK to `v0.2.18` to remediate [CVE-2024-34353 / GHSA-9ggc-845v-gcgv](https://github.com/matrix-org/matrix-rust-sdk/security/advisories/GHSA-9ggc-845v-gcgv).
Changes in Element X v0.4.10 (2024-04-17) Changes in Element X v0.4.10 (2024-04-17)
========================================= =========================================

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 {
@ -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,10 +10,13 @@
<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="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"/>
<locale android:name="sv"/> <locale android:name="sv"/>
<locale android:name="uk"/> <locale android:name="uk"/>
<locale android:name="zh-CN"/>
<locale android:name="zh-TW"/> <locale android:name="zh-TW"/>
</locale-config> </locale-config>

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2021 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,11 +14,11 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.libraries.push.impl.notifications.fake package io.element.android.appconfig
import io.element.android.libraries.push.impl.notifications.SummaryGroupMessageCreator object MessageComposerConfig {
import io.mockk.mockk /**
* Enable the rich text editing in the composer.
class MockkSummaryGroupMessageCreator { */
val instance = mockk<SummaryGroupMessageCreator>() const val ENABLE_RICH_TEXT_EDITING = true
} }

View file

@ -20,6 +20,9 @@ object NotificationConfig {
// TODO EAx Implement and set to true at some point // TODO EAx Implement and set to true at some point
const val SUPPORT_MARK_AS_READ_ACTION = false const val SUPPORT_MARK_AS_READ_ACTION = false
// TODO EAx Implement and set to true at some point
const val SUPPORT_JOIN_DECLINE_INVITE = false
// TODO EAx Implement and set to true at some point // TODO EAx Implement and set to true at some point
const val SUPPORT_QUICK_REPLY_ACTION = false const val SUPPORT_QUICK_REPLY_ACTION = false
} }

View file

@ -37,6 +37,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push
import com.bumble.appyx.navmodel.backstack.operation.replace import com.bumble.appyx.navmodel.backstack.operation.replace
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.appnav.loggedin.LoggedInNode import io.element.android.appnav.loggedin.LoggedInNode
import io.element.android.appnav.room.RoomFlowNode import io.element.android.appnav.room.RoomFlowNode
@ -46,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
@ -99,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,
@ -110,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,
@ -188,15 +184,14 @@ 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
@Parcelize @Parcelize
data class Room( data class Room(
val roomIdOrAlias: RoomIdOrAlias, val roomIdOrAlias: RoomIdOrAlias,
val serverNames: List<String> = emptyList(),
val trigger: JoinedRoom.Trigger? = null,
val roomDescription: RoomDescription? = null, val roomDescription: RoomDescription? = null,
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages() val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages()
) : NavTarget ) : NavTarget
@ -232,38 +227,33 @@ 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 onRoomClick(roomId: RoomId) {
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias())) backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
} }
override fun onSettingsClicked() { override fun onSettingsClick() {
backstack.push(NavTarget.Settings()) backstack.push(NavTarget.Settings())
} }
override fun onCreateRoomClicked() { override fun onCreateRoomClick() {
backstack.push(NavTarget.CreateRoom) backstack.push(NavTarget.CreateRoom)
} }
override fun onSessionConfirmRecoveryKeyClicked() { override fun onSessionConfirmRecoveryKeyClick() {
backstack.push(NavTarget.SecureBackup(initialElement = SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey)) backstack.push(NavTarget.SecureBackup(initialElement = SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey))
} }
override fun onRoomSettingsClicked(roomId: RoomId) { override fun onRoomSettingsClick(roomId: RoomId) {
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.Details)) backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.Details))
} }
override fun onReportBugClicked() { override fun onReportBugClick() {
plugins<Callback>().forEach { it.onOpenBugReport() } plugins<Callback>().forEach { it.onOpenBugReport() }
} }
override fun onRoomDirectorySearchClicked() { override fun onRoomDirectorySearchClick() {
backstack.push(NavTarget.RoomDirectorySearch) backstack.push(NavTarget.RoomDirectorySearch)
} }
} }
@ -282,7 +272,7 @@ class LoggedInFlowNode @AssistedInject constructor(
coroutineScope.launch { attachRoom(roomId.toRoomIdOrAlias()) } coroutineScope.launch { attachRoom(roomId.toRoomIdOrAlias()) }
} }
override fun onPermalinkClicked(data: PermalinkData) { override fun onPermalinkClick(data: PermalinkData) {
when (data) { when (data) {
is PermalinkData.UserLink -> { is PermalinkData.UserLink -> {
// Should not happen (handled by MessagesNode) // Should not happen (handled by MessagesNode)
@ -292,8 +282,9 @@ class LoggedInFlowNode @AssistedInject constructor(
backstack.push( backstack.push(
NavTarget.Room( NavTarget.Room(
roomIdOrAlias = data.roomIdOrAlias, roomIdOrAlias = data.roomIdOrAlias,
serverNames = data.viaParameters,
trigger = JoinedRoom.Trigger.Timeline,
initialElement = RoomNavigationTarget.Messages(data.eventId), initialElement = RoomNavigationTarget.Messages(data.eventId),
// TODO Use the viaParameters
) )
) )
} }
@ -311,6 +302,8 @@ class LoggedInFlowNode @AssistedInject constructor(
val inputs = RoomFlowNode.Inputs( val inputs = RoomFlowNode.Inputs(
roomIdOrAlias = navTarget.roomIdOrAlias, roomIdOrAlias = navTarget.roomIdOrAlias,
roomDescription = Optional.ofNullable(navTarget.roomDescription), roomDescription = Optional.ofNullable(navTarget.roomDescription),
serverNames = navTarget.serverNames,
trigger = Optional.ofNullable(navTarget.trigger),
initialElement = navTarget.initialElement initialElement = navTarget.initialElement
) )
createNode<RoomFlowNode>(buildContext, plugins = listOf(inputs, callback)) createNode<RoomFlowNode>(buildContext, plugins = listOf(inputs, callback))
@ -332,7 +325,7 @@ class LoggedInFlowNode @AssistedInject constructor(
plugins<Callback>().forEach { it.onOpenBugReport() } plugins<Callback>().forEach { it.onOpenBugReport() }
} }
override fun onSecureBackupClicked() { override fun onSecureBackupClick() {
backstack.push(NavTarget.SecureBackup()) backstack.push(NavTarget.SecureBackup())
} }
@ -370,12 +363,14 @@ class LoggedInFlowNode @AssistedInject constructor(
NavTarget.RoomDirectorySearch -> { NavTarget.RoomDirectorySearch -> {
roomDirectoryEntryPoint.nodeBuilder(this, buildContext) roomDirectoryEntryPoint.nodeBuilder(this, buildContext)
.callback(object : RoomDirectoryEntryPoint.Callback { .callback(object : RoomDirectoryEntryPoint.Callback {
override fun onRoomJoined(roomId: RoomId) { override fun onResultClick(roomDescription: RoomDescription) {
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias())) backstack.push(
} NavTarget.Room(
roomIdOrAlias = roomDescription.roomId.toRoomIdOrAlias(),
override fun onResultClicked(roomDescription: RoomDescription) { roomDescription = roomDescription,
backstack.push(NavTarget.Room(roomDescription.roomId.toRoomIdOrAlias(), roomDescription)) trigger = JoinedRoom.Trigger.RoomDirectory,
)
)
} }
}) })
.build() .build()
@ -383,7 +378,12 @@ class LoggedInFlowNode @AssistedInject constructor(
} }
} }
suspend fun attachRoom(roomIdOrAlias: RoomIdOrAlias, eventId: EventId? = null) { suspend fun attachRoom(
roomIdOrAlias: RoomIdOrAlias,
serverNames: List<String> = emptyList(),
trigger: JoinedRoom.Trigger? = null,
eventId: EventId? = null,
) {
waitForNavTargetAttached { navTarget -> waitForNavTargetAttached { navTarget ->
navTarget is NavTarget.RoomList navTarget is NavTarget.RoomList
} }
@ -391,6 +391,8 @@ class LoggedInFlowNode @AssistedInject constructor(
backstack.push( backstack.push(
NavTarget.Room( NavTarget.Room(
roomIdOrAlias = roomIdOrAlias, roomIdOrAlias = roomIdOrAlias,
serverNames = serverNames,
trigger = trigger,
initialElement = RoomNavigationTarget.Messages( initialElement = RoomNavigationTarget.Messages(
focusedEventId = eventId focusedEventId = eventId
) )
@ -415,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

@ -34,6 +34,7 @@ import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push import com.bumble.appyx.navmodel.backstack.operation.push
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.appnav.di.MatrixClientsHolder import io.element.android.appnav.di.MatrixClientsHolder
import io.element.android.appnav.intent.IntentResolver import io.element.android.appnav.intent.IntentResolver
@ -295,6 +296,8 @@ class RootFlowNode @AssistedInject constructor(
is PermalinkData.RoomLink -> { is PermalinkData.RoomLink -> {
attachRoom( attachRoom(
roomIdOrAlias = permalinkData.roomIdOrAlias, roomIdOrAlias = permalinkData.roomIdOrAlias,
trigger = JoinedRoom.Trigger.MobilePermalink,
serverNames = permalinkData.viaParameters,
eventId = permalinkData.eventId, eventId = permalinkData.eventId,
) )
} }

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 result = if (currentPushProvider == null) {
// Register with the first available push provider
val pushProvider = pushService.getAvailablePushProviders().firstOrNull() ?: return@LaunchedEffect val pushProvider = pushService.getAvailablePushProviders().firstOrNull() ?: return@LaunchedEffect
val distributor = pushProvider.getDistributors().firstOrNull() ?: return@LaunchedEffect val distributor = pushProvider.getDistributors().firstOrNull() ?: return@LaunchedEffect
pushService.registerWith(matrixClient, pushProvider, distributor) 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

@ -32,6 +32,7 @@ import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.newRoot import com.bumble.appyx.navmodel.backstack.operation.newRoot
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.appnav.room.joined.JoinedRoomFlowNode import io.element.android.appnav.room.joined.JoinedRoomFlowNode
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
@ -54,6 +55,7 @@ import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.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.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -83,6 +85,8 @@ class RoomFlowNode @AssistedInject constructor(
data class Inputs( data class Inputs(
val roomIdOrAlias: RoomIdOrAlias, val roomIdOrAlias: RoomIdOrAlias,
val roomDescription: Optional<RoomDescription>, val roomDescription: Optional<RoomDescription>,
val serverNames: List<String>,
val trigger: Optional<JoinedRoom.Trigger>,
val initialElement: RoomNavigationTarget, val initialElement: RoomNavigationTarget,
) : NodeInputs ) : NodeInputs
@ -96,7 +100,11 @@ class RoomFlowNode @AssistedInject constructor(
data class Resolving(val roomAlias: RoomAlias) : NavTarget data class Resolving(val roomAlias: RoomAlias) : NavTarget
@Parcelize @Parcelize
data class JoinRoom(val roomId: RoomId) : NavTarget data class JoinRoom(
val roomId: RoomId,
val serverNames: List<String>,
val trigger: im.vector.app.features.analytics.plan.JoinedRoom.Trigger,
) : NavTarget
@Parcelize @Parcelize
data class JoinedRoom(val roomId: RoomId) : NavTarget data class JoinedRoom(val roomId: RoomId) : NavTarget
@ -114,13 +122,13 @@ class RoomFlowNode @AssistedInject constructor(
backstack.newRoot(NavTarget.Resolving(i.roomAlias)) backstack.newRoot(NavTarget.Resolving(i.roomAlias))
} }
is RoomIdOrAlias.Id -> { is RoomIdOrAlias.Id -> {
subscribeToRoomInfoFlow(i.roomId) subscribeToRoomInfoFlow(i.roomId, inputs.serverNames)
} }
} }
} }
} }
private fun subscribeToRoomInfoFlow(roomId: RoomId) { private fun subscribeToRoomInfoFlow(roomId: RoomId, serverNames: List<String>) {
val roomInfoFlow = client.getRoomInfoFlow( val roomInfoFlow = client.getRoomInfoFlow(
roomId = roomId roomId = roomId
).map { it.getOrNull() } ).map { it.getOrNull() }
@ -136,7 +144,13 @@ class RoomFlowNode @AssistedInject constructor(
// we can have a space here in case the space has just been joined. // we can have a space here in case the space has just been joined.
// So navigate to the JoinRoom target for now, which will // So navigate to the JoinRoom target for now, which will
// handle the space not supported screen // handle the space not supported screen
backstack.newRoot(NavTarget.JoinRoom(roomId)) backstack.newRoot(
NavTarget.JoinRoom(
roomId = roomId,
serverNames = serverNames,
trigger = inputs.trigger.getOrNull() ?: JoinedRoom.Trigger.Invite,
)
)
} else { } else {
backstack.newRoot(NavTarget.JoinedRoom(roomId)) backstack.newRoot(NavTarget.JoinedRoom(roomId))
} }
@ -147,7 +161,13 @@ class RoomFlowNode @AssistedInject constructor(
} }
else -> { else -> {
// Was invited or the room is not known, display the join room screen // Was invited or the room is not known, display the join room screen
backstack.newRoot(NavTarget.JoinRoom(roomId)) backstack.newRoot(
NavTarget.JoinRoom(
roomId = roomId,
serverNames = serverNames,
trigger = inputs.trigger.getOrNull() ?: JoinedRoom.Trigger.Invite,
)
)
} }
} }
}.launchIn(lifecycleScope) }.launchIn(lifecycleScope)
@ -158,8 +178,11 @@ class RoomFlowNode @AssistedInject constructor(
is NavTarget.Loading -> loadingNode(buildContext) is NavTarget.Loading -> loadingNode(buildContext)
is NavTarget.Resolving -> { is NavTarget.Resolving -> {
val callback = object : RoomAliasResolverEntryPoint.Callback { val callback = object : RoomAliasResolverEntryPoint.Callback {
override fun onAliasResolved(roomId: RoomId) { override fun onAliasResolved(data: ResolvedRoomAlias) {
subscribeToRoomInfoFlow(roomId) subscribeToRoomInfoFlow(
roomId = data.roomId,
serverNames = data.servers,
)
} }
} }
val params = RoomAliasResolverEntryPoint.Params(navTarget.roomAlias) val params = RoomAliasResolverEntryPoint.Params(navTarget.roomAlias)
@ -173,6 +196,8 @@ class RoomFlowNode @AssistedInject constructor(
roomId = navTarget.roomId, roomId = navTarget.roomId,
roomIdOrAlias = inputs.roomIdOrAlias, roomIdOrAlias = inputs.roomIdOrAlias,
roomDescription = inputs.roomDescription, roomDescription = inputs.roomDescription,
serverNames = navTarget.serverNames,
trigger = navTarget.trigger,
) )
joinRoomEntryPoint.createNode(this, buildContext, inputs) joinRoomEntryPoint.createNode(this, buildContext, inputs)
} }
@ -192,7 +217,7 @@ class RoomFlowNode @AssistedInject constructor(
LoadingRoomNodeView( LoadingRoomNodeView(
state = LoadingRoomState.Loading, state = LoadingRoomState.Loading,
hasNetworkConnection = networkStatus == NetworkStatus.Online, hasNetworkConnection = networkStatus == NetworkStatus.Online,
onBackClicked = { navigateUp() }, onBackClick = { navigateUp() },
modifier = modifier, modifier = modifier,
) )
} }

View file

@ -121,14 +121,14 @@ class JoinedRoomFlowNode @AssistedInject constructor(
} }
} }
private fun loadingNode(buildContext: BuildContext, onBackClicked: () -> Unit) = node(buildContext) { modifier -> private fun loadingNode(buildContext: BuildContext, onBackClick: () -> Unit) = node(buildContext) { modifier ->
val loadingRoomState by loadingRoomStateStateFlow.collectAsState() val loadingRoomState by loadingRoomStateStateFlow.collectAsState()
val networkStatus by networkMonitor.connectivity.collectAsState() val networkStatus by networkMonitor.connectivity.collectAsState()
LoadingRoomNodeView( LoadingRoomNodeView(
state = loadingRoomState, state = loadingRoomState,
hasNetworkConnection = networkStatus == NetworkStatus.Online, hasNetworkConnection = networkStatus == NetworkStatus.Online,
modifier = modifier, modifier = modifier,
onBackClicked = onBackClicked onBackClick = onBackClick
) )
} }

View file

@ -77,7 +77,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
), DaggerComponentOwner { ), DaggerComponentOwner {
interface Callback : Plugin { interface Callback : Plugin {
fun onOpenRoom(roomId: RoomId) fun onOpenRoom(roomId: RoomId)
fun onPermalinkClicked(data: PermalinkData) fun onPermalinkClick(data: PermalinkData)
fun onForwardedToSingleRoom(roomId: RoomId) fun onForwardedToSingleRoom(roomId: RoomId)
fun onOpenGlobalNotificationSettings() fun onOpenGlobalNotificationSettings()
} }
@ -144,16 +144,16 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
return when (navTarget) { return when (navTarget) {
is NavTarget.Messages -> { is NavTarget.Messages -> {
val callback = object : MessagesEntryPoint.Callback { val callback = object : MessagesEntryPoint.Callback {
override fun onRoomDetailsClicked() { override fun onRoomDetailsClick() {
backstack.push(NavTarget.RoomDetails) backstack.push(NavTarget.RoomDetails)
} }
override fun onUserDataClicked(userId: UserId) { override fun onUserDataClick(userId: UserId) {
backstack.push(NavTarget.RoomMemberDetails(userId)) backstack.push(NavTarget.RoomMemberDetails(userId))
} }
override fun onPermalinkClicked(data: PermalinkData) { override fun onPermalinkClick(data: PermalinkData) {
callbacks.forEach { it.onPermalinkClicked(data) } callbacks.forEach { it.onPermalinkClick(data) }
} }
override fun onForwardedToSingleRoom(roomId: RoomId) { override fun onForwardedToSingleRoom(roomId: RoomId) {

View file

@ -46,7 +46,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
fun LoadingRoomNodeView( fun LoadingRoomNodeView(
state: LoadingRoomState, state: LoadingRoomState,
hasNetworkConnection: Boolean, hasNetworkConnection: Boolean,
onBackClicked: () -> Unit, onBackClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Scaffold( Scaffold(
@ -54,7 +54,7 @@ fun LoadingRoomNodeView(
topBar = { topBar = {
Column { Column {
ConnectivityIndicatorView(isOnline = hasNetworkConnection) ConnectivityIndicatorView(isOnline = hasNetworkConnection)
LoadingRoomTopBar(onBackClicked) LoadingRoomTopBar(onBackClick)
} }
}, },
content = { padding -> content = { padding ->
@ -83,11 +83,11 @@ fun LoadingRoomNodeView(
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun LoadingRoomTopBar( private fun LoadingRoomTopBar(
onBackClicked: () -> Unit, onBackClick: () -> Unit,
) { ) {
TopAppBar( TopAppBar(
navigationIcon = { navigationIcon = {
BackButton(onClick = onBackClicked) BackButton(onClick = onBackClick)
}, },
title = { title = {
IconTitlePlaceholdersRowMolecule(iconSize = AvatarSize.TimelineRoom.dp) IconTitlePlaceholdersRowMolecule(iconSize = AvatarSize.TimelineRoom.dp)
@ -101,7 +101,7 @@ private fun LoadingRoomTopBar(
internal fun LoadingRoomNodeViewPreview(@PreviewParameter(LoadingRoomStateProvider::class) state: LoadingRoomState) = ElementPreview { internal fun LoadingRoomNodeViewPreview(@PreviewParameter(LoadingRoomStateProvider::class) state: LoadingRoomState) = ElementPreview {
LoadingRoomNodeView( LoadingRoomNodeView(
state = state, state = state,
onBackClicked = {}, onBackClick = {},
hasNetworkConnection = false hasNetworkConnection = false
) )
} }

View file

@ -156,7 +156,7 @@ class JoinRoomLoadedFlowNodeTest {
) )
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper() val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
// WHEN // WHEN
fakeMessagesEntryPoint.callback?.onRoomDetailsClicked() fakeMessagesEntryPoint.callback?.onRoomDetailsClick()
// THEN // THEN
roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.RoomDetails, Lifecycle.State.CREATED) roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.RoomDetails, Lifecycle.State.CREATED)
val roomDetailsNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.RoomDetails)!! val roomDetailsNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.RoomDetails)!!

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

@ -60,7 +60,7 @@ allprojects {
config.from(files("$rootDir/tools/detekt/detekt.yml")) config.from(files("$rootDir/tools/detekt/detekt.yml"))
} }
dependencies { dependencies {
detektPlugins("io.nlopez.compose.rules:detekt:0.3.12") detektPlugins("io.nlopez.compose.rules:detekt:0.4.3")
} }
// KtLint // KtLint

View file

@ -1 +0,0 @@
Fix session verification being asked again for already verified users.

View file

@ -1 +0,0 @@
RoomMember screen: fallback to userProfile data, if the member is not a user of the room.

View file

@ -1 +0,0 @@
Instead of displaying 'create new recovery key' on the session verification screen when there is no other session active, display it always under the 'enter recovery key' screen.

View file

@ -1 +0,0 @@
Migrate application data.

View file

@ -1 +0,0 @@
Add support for expected decryption errors due to membership (UX and analytics).

View file

@ -1 +0,0 @@
Let the SDK manage the file log cleanup, and keep one week of log.

View file

@ -1 +0,0 @@
Handle permalink navigation to Events.

View file

@ -1 +0,0 @@
Adjust the typography used in the selected user component so a user's display name fits better.

View file

@ -1 +0,0 @@
User display name overflows in timeline messages when it's way too long.

View file

@ -1 +0,0 @@
Pretty-print event JSON in debug viewer

View file

@ -1 +0,0 @@
Add support for external permalinks.

View file

@ -1 +0,0 @@
Ensure the application open the room when a notification is clicked.

View file

@ -1 +0,0 @@
Enable support for Android per-app language preferences

View file

@ -1 +0,0 @@
UX cleanup: reorder options in the main settings screen.

View file

@ -1 +0,0 @@
Analytics: Add support to report current session verification and recovery state

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 @@
Enforce mandatory session verification only for new logins.

View file

@ -1 +0,0 @@
UX cleanup: room details screen, add new CTA buttons for Invite and Call actions.

View file

@ -1 +0,0 @@
UX cleanup: user profile. Move send DM to a call to action button, add 'Call' CTA too.

View file

@ -1 +0,0 @@
Add room badges to room details screen.

View file

@ -1 +0,0 @@
Make log less verbose, make sure we upload as many log files as possible before reaching the request size limit of the bug reporting service, discard older logs if they don't fit.

View file

@ -1 +0,0 @@
Remove 'Join' button in room directory search results.

View file

@ -1 +0,0 @@
Add missing `app_id` and `Version` properties to bug reports.

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/2912.misc Normal file
View file

@ -0,0 +1 @@
Move push provider setting to the "Notifications" screen and display it only when several push provider are available.

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

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

3
changelog.d/2924.misc Normal file
View file

@ -0,0 +1,3 @@
Simplify notifications by removing the custom persistence layer.
Bump minSdk to 24 (Android 7).

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

@ -0,0 +1 @@
Add a feature flag ShowBlockedUsersDetails, disabled by default to render display name and avatar of blocked users in the blocked users list.

View file

@ -0,0 +1,10 @@
Main changes in this version:
- Added support for opening matrix URLs inside the app and navigating to replied to messages.
- Added per-app language support for Android 13+.
- Session verification is no longer mandatory for already logged in users.
- Better log handling.
- Fixed CVE-2024-34353 / GHSA-9ggc-845v-gcgv.
- UX improvements.
Full changelog: https://github.com/element-hq/element-x-android/releases

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,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">"Partilhe dados de utilização anónimos para nos ajudar a identificar problemas."</string>
<string name="screen_analytics_settings_read_terms">"Podes ler todos os nossos termos %1$s."</string>
<string name="screen_analytics_settings_read_terms_content_link">"aqui"</string>
<string name="screen_analytics_settings_share_data">"Partilhar dados de utilização"</string>
</resources>

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

@ -63,15 +63,15 @@ fun AnalyticsOptInView(
) { ) {
val eventSink = state.eventSink val eventSink = state.eventSink
fun onTermsAccepted() { fun onAcceptTerms() {
eventSink(AnalyticsOptInEvents.EnableAnalytics(true)) eventSink(AnalyticsOptInEvents.EnableAnalytics(true))
} }
fun onTermsDeclined() { fun onDeclineTerms() {
eventSink(AnalyticsOptInEvents.EnableAnalytics(false)) eventSink(AnalyticsOptInEvents.EnableAnalytics(false))
} }
BackHandler(onBack = ::onTermsDeclined) BackHandler(onBack = ::onDeclineTerms)
HeaderFooterPage( HeaderFooterPage(
modifier = modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
@ -82,8 +82,8 @@ fun AnalyticsOptInView(
content = { AnalyticsOptInContent() }, content = { AnalyticsOptInContent() },
footer = { footer = {
AnalyticsOptInFooter( AnalyticsOptInFooter(
onTermsAccepted = ::onTermsAccepted, onAcceptTerms = ::onAcceptTerms,
onTermsDeclined = ::onTermsDeclined, onDeclineTerms = ::onDeclineTerms,
) )
} }
) )
@ -165,19 +165,19 @@ private fun AnalyticsOptInContent() {
@Composable @Composable
private fun AnalyticsOptInFooter( private fun AnalyticsOptInFooter(
onTermsAccepted: () -> Unit, onAcceptTerms: () -> Unit,
onTermsDeclined: () -> Unit, onDeclineTerms: () -> Unit,
) { ) {
ButtonColumnMolecule { ButtonColumnMolecule {
Button( Button(
text = stringResource(id = CommonStrings.action_ok), text = stringResource(id = CommonStrings.action_ok),
onClick = onTermsAccepted, onClick = onAcceptTerms,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
TextButton( TextButton(
text = stringResource(id = CommonStrings.action_not_now), text = stringResource(id = CommonStrings.action_not_now),
size = ButtonSize.Medium, size = ButtonSize.Medium,
onClick = onTermsDeclined, onClick = onDeclineTerms,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
} }

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,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">"Não recolheremos ou analisaremos quaisquer dados pessoais"</string>
<string name="screen_analytics_prompt_help_us_improve">"Partilhe dados de utilização anónimos para nos ajudar a identificar problemas."</string>
<string name="screen_analytics_prompt_read_terms">"Podes ler todos os nossos termos %1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"aqui"</string>
<string name="screen_analytics_prompt_settings">"Podes desligar qualquer momento"</string>
<string name="screen_analytics_prompt_third_party_sharing">"Não partilharemos os teus dados com terceiros"</string>
<string name="screen_analytics_prompt_title">"Ajude a melhorar a %1$s"</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

@ -72,15 +72,10 @@ class CallForegroundService : Service() {
startForeground(1, notification) startForeground(1, notification)
} }
@Suppress("DEPRECATION")
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
stopForeground(STOP_FOREGROUND_REMOVE) stopForeground(STOP_FOREGROUND_REMOVE)
} else {
stopForeground(true)
}
} }
override fun onBind(intent: Intent?): IBinder? { override fun onBind(intent: Intent?): IBinder? {

View file

@ -81,12 +81,12 @@ internal fun CallScreenView(
.fillMaxSize(), .fillMaxSize(),
url = state.urlState, url = state.urlState,
userAgent = state.userAgent, userAgent = state.userAgent,
onPermissionsRequested = { request -> onPermissionsRequest = { request ->
val androidPermissions = mapWebkitPermissions(request.resources) val androidPermissions = mapWebkitPermissions(request.resources)
val callback: RequestPermissionCallback = { request.grant(it) } val callback: RequestPermissionCallback = { request.grant(it) }
requestPermissions(androidPermissions.toTypedArray(), callback) requestPermissions(androidPermissions.toTypedArray(), callback)
}, },
onWebViewCreated = { webView -> onWebViewCreate = { webView ->
val interceptor = WebViewWidgetMessageInterceptor(webView) val interceptor = WebViewWidgetMessageInterceptor(webView)
state.eventSink(CallScreenEvents.SetupMessageChannels(interceptor)) state.eventSink(CallScreenEvents.SetupMessageChannels(interceptor))
} }
@ -98,8 +98,8 @@ internal fun CallScreenView(
private fun CallWebView( private fun CallWebView(
url: AsyncData<String>, url: AsyncData<String>,
userAgent: String, userAgent: String,
onPermissionsRequested: (PermissionRequest) -> Unit, onPermissionsRequest: (PermissionRequest) -> Unit,
onWebViewCreated: (WebView) -> Unit, onWebViewCreate: (WebView) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
if (LocalInspectionMode.current) { if (LocalInspectionMode.current) {
@ -111,8 +111,8 @@ private fun CallWebView(
modifier = modifier, modifier = modifier,
factory = { context -> factory = { context ->
WebView(context).apply { WebView(context).apply {
onWebViewCreated(this) onWebViewCreate(this)
setup(userAgent, onPermissionsRequested) setup(userAgent, onPermissionsRequest)
} }
}, },
update = { webView -> update = { webView ->

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

@ -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">"Chamada em curso"</string>
<string name="call_foreground_service_message_android">"Toca para voltar à chamada"</string>
<string name="call_foreground_service_title_android">"☎️ Chamada em curso"</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

@ -47,8 +47,8 @@ class AddPeopleNode @AssistedInject constructor(
AddPeopleView( AddPeopleView(
state = state, state = state,
modifier = modifier, modifier = modifier,
onBackPressed = this::navigateUp, onBackClick = this::navigateUp,
onNextPressed = this::onContinue, onNextClick = this::onContinue,
) )
} }
} }

View file

@ -42,8 +42,8 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
fun AddPeopleView( fun AddPeopleView(
state: UserListState, state: UserListState,
onBackPressed: () -> Unit, onBackClick: () -> Unit,
onNextPressed: () -> Unit, onNextClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Scaffold( Scaffold(
@ -51,14 +51,14 @@ fun AddPeopleView(
topBar = { topBar = {
AddPeopleViewTopBar( AddPeopleViewTopBar(
hasSelectedUsers = state.selectedUsers.isNotEmpty(), hasSelectedUsers = state.selectedUsers.isNotEmpty(),
onBackPressed = { onBackClick = {
if (state.isSearchActive) { if (state.isSearchActive) {
state.eventSink(UserListEvents.OnSearchActiveChanged(false)) state.eventSink(UserListEvents.OnSearchActiveChanged(false))
} else { } else {
onBackPressed() onBackClick()
} }
}, },
onNextPressed = onNextPressed, onNextClick = onNextClick,
) )
} }
) { padding -> ) { padding ->
@ -69,8 +69,8 @@ fun AddPeopleView(
.consumeWindowInsets(padding), .consumeWindowInsets(padding),
state = state, state = state,
showBackButton = false, showBackButton = false,
onUserSelected = {}, onSelectUser = {},
onUserDeselected = {}, onDeselectUser = {},
) )
} }
} }
@ -79,8 +79,8 @@ fun AddPeopleView(
@Composable @Composable
private fun AddPeopleViewTopBar( private fun AddPeopleViewTopBar(
hasSelectedUsers: Boolean, hasSelectedUsers: Boolean,
onBackPressed: () -> Unit, onBackClick: () -> Unit,
onNextPressed: () -> Unit, onNextClick: () -> Unit,
) { ) {
TopAppBar( TopAppBar(
title = { title = {
@ -89,12 +89,12 @@ private fun AddPeopleViewTopBar(
style = ElementTheme.typography.aliasScreenTitle style = ElementTheme.typography.aliasScreenTitle
) )
}, },
navigationIcon = { BackButton(onClick = onBackPressed) }, navigationIcon = { BackButton(onClick = onBackClick) },
actions = { actions = {
val textActionResId = if (hasSelectedUsers) CommonStrings.action_next else CommonStrings.action_skip val textActionResId = if (hasSelectedUsers) CommonStrings.action_next else CommonStrings.action_skip
TextButton( TextButton(
text = stringResource(id = textActionResId), text = stringResource(id = textActionResId),
onClick = onNextPressed, onClick = onNextClick,
) )
} }
) )
@ -105,7 +105,7 @@ private fun AddPeopleViewTopBar(
internal fun AddPeopleViewPreview(@PreviewParameter(AddPeopleUserListStateProvider::class) state: UserListState) = ElementPreview { internal fun AddPeopleViewPreview(@PreviewParameter(AddPeopleUserListStateProvider::class) state: UserListState) = ElementPreview {
AddPeopleView( AddPeopleView(
state = state, state = state,
onBackPressed = {}, onBackClick = {},
onNextPressed = {}, onNextClick = {},
) )
} }

View file

@ -41,7 +41,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
@Composable @Composable
fun RoomPrivacyOption( fun RoomPrivacyOption(
roomPrivacyItem: RoomPrivacyItem, roomPrivacyItem: RoomPrivacyItem,
onOptionSelected: (RoomPrivacyItem) -> Unit, onOptionClick: (RoomPrivacyItem) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
isSelected: Boolean = false, isSelected: Boolean = false,
) { ) {
@ -50,7 +50,7 @@ fun RoomPrivacyOption(
.fillMaxWidth() .fillMaxWidth()
.selectable( .selectable(
selected = isSelected, selected = isSelected,
onClick = { onOptionSelected(roomPrivacyItem) }, onClick = { onOptionClick(roomPrivacyItem) },
role = Role.RadioButton, role = Role.RadioButton,
) )
.padding(8.dp), .padding(8.dp),
@ -98,12 +98,12 @@ internal fun RoomPrivacyOptionPreview() = ElementPreview {
Column { Column {
RoomPrivacyOption( RoomPrivacyOption(
roomPrivacyItem = aRoomPrivacyItem, roomPrivacyItem = aRoomPrivacyItem,
onOptionSelected = {}, onOptionClick = {},
isSelected = true, isSelected = true,
) )
RoomPrivacyOption( RoomPrivacyOption(
roomPrivacyItem = aRoomPrivacyItem, roomPrivacyItem = aRoomPrivacyItem,
onOptionSelected = {}, onOptionClick = {},
isSelected = false, isSelected = false,
) )
} }

View file

@ -53,11 +53,11 @@ fun SearchUserBar(
showLoader: Boolean, showLoader: Boolean,
selectedUsers: ImmutableList<MatrixUser>, selectedUsers: ImmutableList<MatrixUser>,
active: Boolean, active: Boolean,
isMultiSelectionEnabled: Boolean, isMultiSelectionEnable: Boolean,
onActiveChanged: (Boolean) -> Unit, onActiveChange: (Boolean) -> Unit,
onTextChanged: (String) -> Unit, onTextChange: (String) -> Unit,
onUserSelected: (MatrixUser) -> Unit, onUserSelect: (MatrixUser) -> Unit,
onUserDeselected: (MatrixUser) -> Unit, onUserDeselect: (MatrixUser) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
showBackButton: Boolean = true, showBackButton: Boolean = true,
placeHolderTitle: String = stringResource(CommonStrings.common_search_for_someone), placeHolderTitle: String = stringResource(CommonStrings.common_search_for_someone),
@ -66,14 +66,14 @@ fun SearchUserBar(
SearchBar( SearchBar(
query = query, query = query,
onQueryChange = onTextChanged, onQueryChange = onTextChange,
active = active, active = active,
onActiveChange = onActiveChanged, onActiveChange = onActiveChange,
modifier = modifier, modifier = modifier,
placeHolderTitle = placeHolderTitle, placeHolderTitle = placeHolderTitle,
showBackButton = showBackButton, showBackButton = showBackButton,
contentPrefix = { contentPrefix = {
if (isMultiSelectionEnabled && active && selectedUsers.isNotEmpty()) { if (isMultiSelectionEnable && active && selectedUsers.isNotEmpty()) {
// We want the selected users to behave a bit like a top bar - when the list below is scrolled, the colour // We want the selected users to behave a bit like a top bar - when the list below is scrolled, the colour
// should change to indicate elevation. // should change to indicate elevation.
@ -96,7 +96,7 @@ fun SearchUserBar(
contentPadding = PaddingValues(16.dp), contentPadding = PaddingValues(16.dp),
selectedUsers = selectedUsers, selectedUsers = selectedUsers,
autoScroll = true, autoScroll = true,
onUserRemoved = onUserDeselected, onUserRemove = onUserDeselect,
modifier = Modifier.background(appBarContainerColor) modifier = Modifier.background(appBarContainerColor)
) )
} }
@ -109,7 +109,7 @@ fun SearchUserBar(
resultState = state, resultState = state,
resultHandler = { users -> resultHandler = { users ->
LazyColumn(state = columnState) { LazyColumn(state = columnState) {
if (isMultiSelectionEnabled) { if (isMultiSelectionEnable) {
itemsIndexed(users) { index, searchResult -> itemsIndexed(users) { index, searchResult ->
SearchMultipleUsersResultItem( SearchMultipleUsersResultItem(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@ -117,9 +117,9 @@ fun SearchUserBar(
isUserSelected = selectedUsers.contains(searchResult.matrixUser), isUserSelected = selectedUsers.contains(searchResult.matrixUser),
onCheckedChange = { checked -> onCheckedChange = { checked ->
if (checked) { if (checked) {
onUserSelected(searchResult.matrixUser) onUserSelect(searchResult.matrixUser)
} else { } else {
onUserDeselected(searchResult.matrixUser) onUserDeselect(searchResult.matrixUser)
} }
} }
) )
@ -132,7 +132,7 @@ fun SearchUserBar(
SearchSingleUserResultItem( SearchSingleUserResultItem(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
searchResult = searchResult, searchResult = searchResult,
onClick = { onUserSelected(searchResult.matrixUser) } onClick = { onUserSelect(searchResult.matrixUser) }
) )
if (index < users.lastIndex) { if (index < users.lastIndex) {
HorizontalDivider() HorizontalDivider()

View file

@ -44,8 +44,8 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
fun UserListView( fun UserListView(
state: UserListState, state: UserListState,
onUserSelected: (MatrixUser) -> Unit, onSelectUser: (MatrixUser) -> Unit,
onUserDeselected: (MatrixUser) -> Unit, onDeselectUser: (MatrixUser) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
showBackButton: Boolean = true, showBackButton: Boolean = true,
) { ) {
@ -59,17 +59,17 @@ fun UserListView(
selectedUsers = state.selectedUsers, selectedUsers = state.selectedUsers,
active = state.isSearchActive, active = state.isSearchActive,
showLoader = state.showSearchLoader, showLoader = state.showSearchLoader,
isMultiSelectionEnabled = state.isMultiSelectionEnabled, isMultiSelectionEnable = state.isMultiSelectionEnabled,
showBackButton = showBackButton, showBackButton = showBackButton,
onActiveChanged = { state.eventSink(UserListEvents.OnSearchActiveChanged(it)) }, onActiveChange = { state.eventSink(UserListEvents.OnSearchActiveChanged(it)) },
onTextChanged = { state.eventSink(UserListEvents.UpdateSearchQuery(it)) }, onTextChange = { state.eventSink(UserListEvents.UpdateSearchQuery(it)) },
onUserSelected = { onUserSelect = {
state.eventSink(UserListEvents.AddToSelection(it)) state.eventSink(UserListEvents.AddToSelection(it))
onUserSelected(it) onSelectUser(it)
}, },
onUserDeselected = { onUserDeselect = {
state.eventSink(UserListEvents.RemoveFromSelection(it)) state.eventSink(UserListEvents.RemoveFromSelection(it))
onUserDeselected(it) onDeselectUser(it)
}, },
) )
@ -78,9 +78,9 @@ fun UserListView(
contentPadding = PaddingValues(16.dp), contentPadding = PaddingValues(16.dp),
selectedUsers = state.selectedUsers, selectedUsers = state.selectedUsers,
autoScroll = true, autoScroll = true,
onUserRemoved = { onUserRemove = {
state.eventSink(UserListEvents.RemoveFromSelection(it)) state.eventSink(UserListEvents.RemoveFromSelection(it))
onUserDeselected(it) onDeselectUser(it)
}, },
) )
} }
@ -102,10 +102,10 @@ fun UserListView(
onCheckedChange = { onCheckedChange = {
if (isSelected) { if (isSelected) {
state.eventSink(UserListEvents.RemoveFromSelection(recentDirectRoom.matrixUser)) state.eventSink(UserListEvents.RemoveFromSelection(recentDirectRoom.matrixUser))
onUserDeselected(recentDirectRoom.matrixUser) onDeselectUser(recentDirectRoom.matrixUser)
} else { } else {
state.eventSink(UserListEvents.AddToSelection(recentDirectRoom.matrixUser)) state.eventSink(UserListEvents.AddToSelection(recentDirectRoom.matrixUser))
onUserSelected(recentDirectRoom.matrixUser) onSelectUser(recentDirectRoom.matrixUser)
} }
}, },
data = CheckableUserRowData.Resolved( data = CheckableUserRowData.Resolved(
@ -129,7 +129,7 @@ fun UserListView(
internal fun UserListViewPreview(@PreviewParameter(UserListStateProvider::class) state: UserListState) = ElementPreview { internal fun UserListViewPreview(@PreviewParameter(UserListStateProvider::class) state: UserListState) = ElementPreview {
UserListView( UserListView(
state = state, state = state,
onUserSelected = {}, onSelectUser = {},
onUserDeselected = {}, onDeselectUser = {},
) )
} }

View file

@ -50,7 +50,7 @@ class ConfigureRoomNode @AssistedInject constructor(
fun onCreateRoomSuccess(roomId: RoomId) fun onCreateRoomSuccess(roomId: RoomId)
} }
private fun onRoomCreated(roomId: RoomId) { private fun onCreateRoomSuccess(roomId: RoomId) {
plugins<Callback>().forEach { it.onCreateRoomSuccess(roomId) } plugins<Callback>().forEach { it.onCreateRoomSuccess(roomId) }
} }
@ -60,8 +60,8 @@ class ConfigureRoomNode @AssistedInject constructor(
ConfigureRoomView( ConfigureRoomView(
state = state, state = state,
modifier = modifier, modifier = modifier,
onBackPressed = this::navigateUp, onBackClick = this::navigateUp,
onRoomCreated = this::onRoomCreated, onCreateRoomSuccess = this::onCreateRoomSuccess,
) )
} }
} }

View file

@ -29,12 +29,10 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
@ -63,27 +61,20 @@ import io.element.android.libraries.matrix.ui.components.SelectedUsersRowList
import io.element.android.libraries.matrix.ui.components.UnsavedAvatar import io.element.android.libraries.matrix.ui.components.UnsavedAvatar
import io.element.android.libraries.permissions.api.PermissionsView import io.element.android.libraries.permissions.api.PermissionsView
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
fun ConfigureRoomView( fun ConfigureRoomView(
state: ConfigureRoomState, state: ConfigureRoomState,
onBackPressed: () -> Unit, onBackClick: () -> Unit,
onRoomCreated: (RoomId) -> Unit, onCreateRoomSuccess: (RoomId) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val coroutineScope = rememberCoroutineScope()
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val itemActionsBottomSheetState = rememberModalBottomSheetState( val isAvatarActionsSheetVisible = remember { mutableStateOf(false) }
initialValue = ModalBottomSheetValue.Hidden,
)
fun onAvatarClicked() { fun onAvatarClick() {
focusManager.clearFocus() focusManager.clearFocus()
coroutineScope.launch { isAvatarActionsSheetVisible.value = true
itemActionsBottomSheetState.show()
}
} }
Scaffold( Scaffold(
@ -91,8 +82,8 @@ fun ConfigureRoomView(
topBar = { topBar = {
ConfigureRoomToolbar( ConfigureRoomToolbar(
isNextActionEnabled = state.isCreateButtonEnabled, isNextActionEnabled = state.isCreateButtonEnabled,
onBackPressed = onBackPressed, onBackClick = onBackClick,
onNextPressed = { onNextClick = {
focusManager.clearFocus() focusManager.clearFocus()
state.eventSink(ConfigureRoomEvents.CreateRoom(state.config)) state.eventSink(ConfigureRoomEvents.CreateRoom(state.config))
}, },
@ -111,20 +102,20 @@ fun ConfigureRoomView(
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.padding(horizontal = 16.dp),
avatarUri = state.config.avatarUri, avatarUri = state.config.avatarUri,
roomName = state.config.roomName.orEmpty(), roomName = state.config.roomName.orEmpty(),
onAvatarClick = ::onAvatarClicked, onAvatarClick = ::onAvatarClick,
onRoomNameChanged = { state.eventSink(ConfigureRoomEvents.RoomNameChanged(it)) }, onChangeRoomName = { state.eventSink(ConfigureRoomEvents.RoomNameChanged(it)) },
) )
RoomTopic( RoomTopic(
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.padding(horizontal = 16.dp),
topic = state.config.topic.orEmpty(), topic = state.config.topic.orEmpty(),
onTopicChanged = { state.eventSink(ConfigureRoomEvents.TopicChanged(it)) }, onTopicChange = { state.eventSink(ConfigureRoomEvents.TopicChanged(it)) },
) )
if (state.config.invites.isNotEmpty()) { if (state.config.invites.isNotEmpty()) {
SelectedUsersRowList( SelectedUsersRowList(
modifier = Modifier.padding(bottom = 16.dp), modifier = Modifier.padding(bottom = 16.dp),
contentPadding = PaddingValues(horizontal = 24.dp), contentPadding = PaddingValues(horizontal = 24.dp),
selectedUsers = state.config.invites, selectedUsers = state.config.invites,
onUserRemoved = { onUserRemove = {
focusManager.clearFocus() focusManager.clearFocus()
state.eventSink(ConfigureRoomEvents.RemoveFromSelection(it)) state.eventSink(ConfigureRoomEvents.RemoveFromSelection(it))
}, },
@ -133,7 +124,7 @@ fun ConfigureRoomView(
RoomPrivacyOptions( RoomPrivacyOptions(
modifier = Modifier.padding(bottom = 40.dp), modifier = Modifier.padding(bottom = 40.dp),
selected = state.config.privacy, selected = state.config.privacy,
onOptionSelected = { onOptionClick = {
focusManager.clearFocus() focusManager.clearFocus()
state.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(it.privacy)) state.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(it.privacy))
}, },
@ -143,8 +134,9 @@ fun ConfigureRoomView(
AvatarActionBottomSheet( AvatarActionBottomSheet(
actions = state.avatarActions, actions = state.avatarActions,
modalBottomSheetState = itemActionsBottomSheetState, isVisible = isAvatarActionsSheetVisible.value,
onActionSelected = { state.eventSink(ConfigureRoomEvents.HandleAvatarAction(it)) } onDismiss = { isAvatarActionsSheetVisible.value = false },
onSelectAction = { state.eventSink(ConfigureRoomEvents.HandleAvatarAction(it)) }
) )
AsyncActionView( AsyncActionView(
@ -154,7 +146,7 @@ fun ConfigureRoomView(
progressText = stringResource(CommonStrings.common_creating_room), progressText = stringResource(CommonStrings.common_creating_room),
) )
}, },
onSuccess = { onRoomCreated(it) }, onSuccess = { onCreateRoomSuccess(it) },
errorMessage = { stringResource(R.string.screen_create_room_error_creating_room) }, errorMessage = { stringResource(R.string.screen_create_room_error_creating_room) },
onRetry = { state.eventSink(ConfigureRoomEvents.CreateRoom(state.config)) }, onRetry = { state.eventSink(ConfigureRoomEvents.CreateRoom(state.config)) },
onErrorDismiss = { state.eventSink(ConfigureRoomEvents.CancelCreateRoom) }, onErrorDismiss = { state.eventSink(ConfigureRoomEvents.CancelCreateRoom) },
@ -169,8 +161,8 @@ fun ConfigureRoomView(
@Composable @Composable
private fun ConfigureRoomToolbar( private fun ConfigureRoomToolbar(
isNextActionEnabled: Boolean, isNextActionEnabled: Boolean,
onBackPressed: () -> Unit, onBackClick: () -> Unit,
onNextPressed: () -> Unit, onNextClick: () -> Unit,
) { ) {
TopAppBar( TopAppBar(
title = { title = {
@ -179,12 +171,12 @@ private fun ConfigureRoomToolbar(
style = ElementTheme.typography.aliasScreenTitle, style = ElementTheme.typography.aliasScreenTitle,
) )
}, },
navigationIcon = { BackButton(onClick = onBackPressed) }, navigationIcon = { BackButton(onClick = onBackClick) },
actions = { actions = {
TextButton( TextButton(
text = stringResource(CommonStrings.action_create), text = stringResource(CommonStrings.action_create),
enabled = isNextActionEnabled, enabled = isNextActionEnabled,
onClick = onNextPressed, onClick = onNextClick,
) )
} }
) )
@ -195,7 +187,7 @@ private fun RoomNameWithAvatar(
avatarUri: Uri?, avatarUri: Uri?,
roomName: String, roomName: String,
onAvatarClick: () -> Unit, onAvatarClick: () -> Unit,
onRoomNameChanged: (String) -> Unit, onChangeRoomName: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Row( Row(
@ -213,7 +205,7 @@ private fun RoomNameWithAvatar(
value = roomName, value = roomName,
placeholder = stringResource(CommonStrings.common_room_name_placeholder), placeholder = stringResource(CommonStrings.common_room_name_placeholder),
singleLine = true, singleLine = true,
onValueChange = onRoomNameChanged, onValueChange = onChangeRoomName,
) )
} }
} }
@ -221,7 +213,7 @@ private fun RoomNameWithAvatar(
@Composable @Composable
private fun RoomTopic( private fun RoomTopic(
topic: String, topic: String,
onTopicChanged: (String) -> Unit, onTopicChange: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
LabelledTextField( LabelledTextField(
@ -229,7 +221,7 @@ private fun RoomTopic(
label = stringResource(R.string.screen_create_room_topic_label), label = stringResource(R.string.screen_create_room_topic_label),
value = topic, value = topic,
placeholder = stringResource(CommonStrings.common_topic_placeholder), placeholder = stringResource(CommonStrings.common_topic_placeholder),
onValueChange = onTopicChanged, onValueChange = onTopicChange,
maxLines = 3, maxLines = 3,
keyboardOptions = KeyboardOptions( keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.Sentences, capitalization = KeyboardCapitalization.Sentences,
@ -240,7 +232,7 @@ private fun RoomTopic(
@Composable @Composable
private fun RoomPrivacyOptions( private fun RoomPrivacyOptions(
selected: RoomPrivacy?, selected: RoomPrivacy?,
onOptionSelected: (RoomPrivacyItem) -> Unit, onOptionClick: (RoomPrivacyItem) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val items = roomPrivacyItems() val items = roomPrivacyItems()
@ -249,7 +241,7 @@ private fun RoomPrivacyOptions(
RoomPrivacyOption( RoomPrivacyOption(
roomPrivacyItem = item, roomPrivacyItem = item,
isSelected = selected == item.privacy, isSelected = selected == item.privacy,
onOptionSelected = onOptionSelected, onOptionClick = onOptionClick,
) )
} }
} }
@ -260,7 +252,7 @@ private fun RoomPrivacyOptions(
internal fun ConfigureRoomViewPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) = ElementPreview { internal fun ConfigureRoomViewPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) = ElementPreview {
ConfigureRoomView( ConfigureRoomView(
state = state, state = state,
onBackPressed = {}, onBackClick = {},
onRoomCreated = {}, onCreateRoomSuccess = {},
) )
} }

View file

@ -68,10 +68,10 @@ class CreateRoomRootNode @AssistedInject constructor(
CreateRoomRootView( CreateRoomRootView(
state = state, state = state,
modifier = modifier, modifier = modifier,
onClosePressed = this::navigateUp, onCloseClick = this::navigateUp,
onNewRoomClicked = ::onCreateNewRoom, onNewRoomClick = ::onCreateNewRoom,
onOpenDM = ::onStartChatSuccess, onOpenDM = ::onStartChatSuccess,
onInviteFriendsClicked = { invitePeople(activity) } onInviteFriendsClick = { invitePeople(activity) }
) )
} }

View file

@ -59,17 +59,17 @@ import kotlinx.collections.immutable.persistentListOf
@Composable @Composable
fun CreateRoomRootView( fun CreateRoomRootView(
state: CreateRoomRootState, state: CreateRoomRootState,
onClosePressed: () -> Unit, onCloseClick: () -> Unit,
onNewRoomClicked: () -> Unit, onNewRoomClick: () -> Unit,
onOpenDM: (RoomId) -> Unit, onOpenDM: (RoomId) -> Unit,
onInviteFriendsClicked: () -> Unit, onInviteFriendsClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Scaffold( Scaffold(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
topBar = { topBar = {
if (!state.userListState.isSearchActive) { if (!state.userListState.isSearchActive) {
CreateRoomRootViewTopBar(onClosePressed = onClosePressed) CreateRoomRootViewTopBar(onCloseClick = onCloseClick)
} }
} }
) { paddingValues -> ) { paddingValues ->
@ -86,18 +86,18 @@ fun CreateRoomRootView(
state = state.userListState.copy( state = state.userListState.copy(
recentDirectRooms = persistentListOf(), recentDirectRooms = persistentListOf(),
), ),
onUserSelected = { onSelectUser = {
state.eventSink(CreateRoomRootEvents.StartDM(it)) state.eventSink(CreateRoomRootEvents.StartDM(it))
}, },
onUserDeselected = { }, onDeselectUser = { },
) )
if (!state.userListState.isSearchActive) { if (!state.userListState.isSearchActive) {
CreateRoomActionButtonsList( CreateRoomActionButtonsList(
state = state, state = state,
onNewRoomClicked = onNewRoomClicked, onNewRoomClick = onNewRoomClick,
onInvitePeopleClicked = onInviteFriendsClicked, onInvitePeopleClick = onInviteFriendsClick,
onDmClicked = onOpenDM, onDmClick = onOpenDM,
) )
} }
} }
@ -125,7 +125,7 @@ fun CreateRoomRootView(
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun CreateRoomRootViewTopBar( private fun CreateRoomRootViewTopBar(
onClosePressed: () -> Unit, onCloseClick: () -> Unit,
) { ) {
TopAppBar( TopAppBar(
title = { title = {
@ -137,7 +137,7 @@ private fun CreateRoomRootViewTopBar(
navigationIcon = { navigationIcon = {
BackButton( BackButton(
imageVector = CompoundIcons.Close(), imageVector = CompoundIcons.Close(),
onClick = onClosePressed, onClick = onCloseClick,
) )
} }
) )
@ -146,23 +146,23 @@ private fun CreateRoomRootViewTopBar(
@Composable @Composable
private fun CreateRoomActionButtonsList( private fun CreateRoomActionButtonsList(
state: CreateRoomRootState, state: CreateRoomRootState,
onNewRoomClicked: () -> Unit, onNewRoomClick: () -> Unit,
onInvitePeopleClicked: () -> Unit, onInvitePeopleClick: () -> Unit,
onDmClicked: (RoomId) -> Unit, onDmClick: (RoomId) -> Unit,
) { ) {
LazyColumn { LazyColumn {
item { item {
CreateRoomActionButton( CreateRoomActionButton(
iconRes = CompoundDrawables.ic_compound_plus, iconRes = CompoundDrawables.ic_compound_plus,
text = stringResource(id = R.string.screen_create_room_action_create_room), text = stringResource(id = R.string.screen_create_room_action_create_room),
onClick = onNewRoomClicked, onClick = onNewRoomClick,
) )
} }
item { item {
CreateRoomActionButton( CreateRoomActionButton(
iconRes = CompoundDrawables.ic_compound_share_android, iconRes = CompoundDrawables.ic_compound_share_android,
text = stringResource(id = CommonStrings.action_invite_friends_to_app, state.applicationName), text = stringResource(id = CommonStrings.action_invite_friends_to_app, state.applicationName),
onClick = onInvitePeopleClicked, onClick = onInvitePeopleClick,
) )
} }
if (state.userListState.recentDirectRooms.isNotEmpty()) { if (state.userListState.recentDirectRooms.isNotEmpty()) {
@ -177,7 +177,7 @@ private fun CreateRoomActionButtonsList(
MatrixUserRow( MatrixUserRow(
modifier = Modifier.clickable( modifier = Modifier.clickable(
onClick = { onClick = {
onDmClicked(recentDirectRoom.roomId) onDmClick(recentDirectRoom.roomId)
} }
), ),
matrixUser = recentDirectRoom.matrixUser, matrixUser = recentDirectRoom.matrixUser,
@ -222,9 +222,9 @@ internal fun CreateRoomRootViewPreview(@PreviewParameter(CreateRoomRootStateProv
ElementPreview { ElementPreview {
CreateRoomRootView( CreateRoomRootView(
state = state, state = state,
onClosePressed = {}, onCloseClick = {},
onNewRoomClicked = {}, onNewRoomClick = {},
onOpenDM = {}, onOpenDM = {},
onInviteFriendsClicked = {}, onInviteFriendsClick = {},
) )
} }

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

@ -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">"Nova sala"</string>
<string name="screen_create_room_add_people_title">"Convidar pessoas"</string>
<string name="screen_create_room_error_creating_room">"Ocorreu um erro ao criar a sala"</string>
<string name="screen_create_room_private_option_description">"As mensagens serão cifradas. Uma vez ativada, não é possível desativar a cifragem."</string>
<string name="screen_create_room_private_option_title">"Sala privada (entrada apenas por convite)"</string>
<string name="screen_create_room_public_option_description">"As mensagens não serão cifradas e qualquer um as poderá ler. É possível ativar a cifragem posteriormente."</string>
<string name="screen_create_room_public_option_title">"Sala pública (entrada livre)"</string>
<string name="screen_create_room_room_name_label">"Nome da sala"</string>
<string name="screen_create_room_title">"Criar uma sala"</string>
<string name="screen_create_room_topic_label">"Descrição (opcional)"</string>
<string name="screen_start_chat_error_starting_chat">"Ocorreu um erro ao tentar iniciar uma conversa"</string>
</resources>

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

@ -47,7 +47,7 @@ class AddPeopleViewTest {
aUserListState( aUserListState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onBackPressed = it onBackClick = it
) )
rule.pressBack() rule.pressBack()
} }
@ -75,7 +75,7 @@ class AddPeopleViewTest {
aUserListState( aUserListState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onNextPressed = it onNextClick = it
) )
rule.clickOn(CommonStrings.action_skip) rule.clickOn(CommonStrings.action_skip)
} }
@ -85,14 +85,14 @@ class AddPeopleViewTest {
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setAddPeopleView( private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setAddPeopleView(
state: UserListState, state: UserListState,
onBackPressed: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(),
onNextPressed: () -> Unit = EnsureNeverCalled(), onNextClick: () -> Unit = EnsureNeverCalled(),
) { ) {
setContent { setContent {
AddPeopleView( AddPeopleView(
state = state, state = state,
onBackPressed = onBackPressed, onBackClick = onBackClick,
onNextPressed = onNextPressed, onNextClick = onNextClick,
) )
} }
} }

View file

@ -54,7 +54,7 @@ class CreateRoomRootViewTest {
aCreateRoomRootState( aCreateRoomRootState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onClosePressed = it onCloseClick = it
) )
rule.pressBack() rule.pressBack()
} }
@ -68,7 +68,7 @@ class CreateRoomRootViewTest {
aCreateRoomRootState( aCreateRoomRootState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onNewRoomClicked = it onNewRoomClick = it
) )
rule.clickOn(R.string.screen_create_room_action_create_room) rule.clickOn(R.string.screen_create_room_action_create_room)
} }
@ -84,7 +84,7 @@ class CreateRoomRootViewTest {
applicationName = "test", applicationName = "test",
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onInviteFriendsClicked = it onInviteFriendsClick = it
) )
val text = rule.activity.getString(CommonStrings.action_invite_friends_to_app, "test") val text = rule.activity.getString(CommonStrings.action_invite_friends_to_app, "test")
rule.onNodeWithText(text).performClick() rule.onNodeWithText(text).performClick()
@ -114,18 +114,18 @@ class CreateRoomRootViewTest {
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setCreateRoomRootView( private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setCreateRoomRootView(
state: CreateRoomRootState, state: CreateRoomRootState,
onClosePressed: () -> Unit = EnsureNeverCalled(), onCloseClick: () -> Unit = EnsureNeverCalled(),
onNewRoomClicked: () -> Unit = EnsureNeverCalled(), onNewRoomClick: () -> Unit = EnsureNeverCalled(),
onOpenDM: (RoomId) -> Unit = EnsureNeverCalledWithParam(), onOpenDM: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
onInviteFriendsClicked: () -> Unit = EnsureNeverCalled(), onInviteFriendsClick: () -> Unit = EnsureNeverCalled(),
) { ) {
setContent { setContent {
CreateRoomRootView( CreateRoomRootView(
state = state, state = state,
onClosePressed = onClosePressed, onCloseClick = onCloseClick,
onNewRoomClicked = onNewRoomClicked, onNewRoomClick = onNewRoomClick,
onOpenDM = onOpenDM, onOpenDM = onOpenDM,
onInviteFriendsClicked = onInviteFriendsClicked, onInviteFriendsClick = onInviteFriendsClick,
) )
} }
} }

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

@ -34,18 +34,18 @@ class WelcomeNode @AssistedInject constructor(
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin { interface Callback : Plugin {
fun onContinueClicked() fun onContinueClick()
} }
private fun onContinueClicked() { private fun onContinueClick() {
plugins.filterIsInstance<Callback>().forEach { it.onContinueClicked() } plugins.filterIsInstance<Callback>().forEach { it.onContinueClick() }
} }
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
WelcomeView( WelcomeView(
applicationName = buildMeta.applicationName, applicationName = buildMeta.applicationName,
onContinueClicked = ::onContinueClicked, onContinueClick = ::onContinueClick,
modifier = modifier modifier = modifier
) )
} }

View file

@ -52,9 +52,9 @@ import kotlinx.collections.immutable.persistentListOf
fun WelcomeView( fun WelcomeView(
applicationName: String, applicationName: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onContinueClicked: () -> Unit, onContinueClick: () -> Unit,
) { ) {
BackHandler(onBack = onContinueClicked) BackHandler(onBack = onContinueClick)
OnBoardingPage( OnBoardingPage(
modifier = modifier modifier = modifier
.systemBarsPadding() .systemBarsPadding()
@ -90,7 +90,7 @@ fun WelcomeView(
Button( Button(
text = stringResource(CommonStrings.action_continue), text = stringResource(CommonStrings.action_continue),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
onClick = onContinueClicked onClick = onContinueClick
) )
Spacer(modifier = Modifier.height(32.dp)) Spacer(modifier = Modifier.height(32.dp))
} }
@ -113,6 +113,6 @@ private fun listItems() = persistentListOf(
@Composable @Composable
internal fun WelcomeViewPreview() { internal fun WelcomeViewPreview() {
ElementPreview { ElementPreview {
WelcomeView(applicationName = "Element X", onContinueClicked = {}) WelcomeView(applicationName = "Element X", onContinueClick = {})
} }
} }

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,11 +11,24 @@
<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_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>
<string name="screen_qr_code_login_initial_state_item_3">"Vybrat %1$s"</string> <string name="screen_qr_code_login_initial_state_item_3">"Vybrat %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Připojit nové zařízení\""</string> <string name="screen_qr_code_login_initial_state_item_3_action">"\"Připojit nové zařízení\""</string>
<string name="screen_qr_code_login_initial_state_item_4">"Postupujte podle uvedených pokynů"</string> <string name="screen_qr_code_login_initial_state_item_4">"Naskenujte QR kód pomocí tohoto zařízení"</string>
<string name="screen_qr_code_login_initial_state_title">"Otevřete %1$s na jiném zařízení pro získání QR kódu"</string> <string name="screen_qr_code_login_initial_state_title">"Otevřete %1$s na jiném zařízení pro získání QR kódu"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Použijte QR kód zobrazený na druhém zařízení."</string> <string name="screen_qr_code_login_invalid_scan_state_description">"Použijte QR kód zobrazený na druhém zařízení."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Zkusit znovu"</string> <string name="screen_qr_code_login_invalid_scan_state_retry_button">"Zkusit znovu"</string>

View file

@ -11,11 +11,12 @@
<string name="screen_qr_code_login_connection_note_secure_state_title">"Die Verbindung ist nicht sicher"</string> <string name="screen_qr_code_login_connection_note_secure_state_title">"Die Verbindung ist nicht sicher"</string>
<string name="screen_qr_code_login_device_code_subtitle">"Du wirst aufgefordert, die beiden unten abgebildeten Ziffern einzugeben."</string> <string name="screen_qr_code_login_device_code_subtitle">"Du wirst aufgefordert, die beiden unten abgebildeten Ziffern einzugeben."</string>
<string name="screen_qr_code_login_device_code_title">"Trage die unten angezeigte Zahl auf einem anderen Device ein"</string> <string name="screen_qr_code_login_device_code_title">"Trage die unten angezeigte Zahl auf einem anderen Device ein"</string>
<string name="screen_qr_code_login_initial_state_button_title">"Bereit zum Scannen"</string>
<string name="screen_qr_code_login_initial_state_item_1">"%1$s auf einem Desktop-Gerät öffnen"</string> <string name="screen_qr_code_login_initial_state_item_1">"%1$s auf einem Desktop-Gerät öffnen"</string>
<string name="screen_qr_code_login_initial_state_item_2">"Klick auf deinen Avatar"</string> <string name="screen_qr_code_login_initial_state_item_2">"Klick auf deinen Avatar"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Wähle %1$s"</string> <string name="screen_qr_code_login_initial_state_item_3">"Wähle %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Neues Gerät verknüpfen\""</string> <string name="screen_qr_code_login_initial_state_item_3_action">"\"Neues Gerät verknüpfen\""</string>
<string name="screen_qr_code_login_initial_state_item_4">"Befolge die angezeigten Anweisungen"</string> <string name="screen_qr_code_login_initial_state_item_4">"Scanne den QR-Code mit diesem Gerät"</string>
<string name="screen_qr_code_login_initial_state_title">"Öffne %1$s auf einem anderen Gerät, um den QR-Code zu erhalten"</string> <string name="screen_qr_code_login_initial_state_title">"Öffne %1$s auf einem anderen Gerät, um den QR-Code zu erhalten"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Verwende den QR-Code, der auf dem anderen Gerät angezeigt wird."</string> <string name="screen_qr_code_login_invalid_scan_state_description">"Verwende den QR-Code, der auf dem anderen Gerät angezeigt wird."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Erneut versuchen"</string> <string name="screen_qr_code_login_invalid_scan_state_retry_button">"Erneut versuchen"</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

@ -11,11 +11,12 @@
<string name="screen_qr_code_login_connection_note_secure_state_title">"A kapcsolat nem biztonságos"</string> <string name="screen_qr_code_login_connection_note_secure_state_title">"A kapcsolat nem biztonságos"</string>
<string name="screen_qr_code_login_device_code_subtitle">"A rendszer kérni fogja, hogy adja meg az alábbi két számjegyet az eszközén."</string> <string name="screen_qr_code_login_device_code_subtitle">"A rendszer kérni fogja, hogy adja meg az alábbi két számjegyet az eszközén."</string>
<string name="screen_qr_code_login_device_code_title">"Adja meg az alábbi számot a másik eszközén"</string> <string name="screen_qr_code_login_device_code_title">"Adja meg az alábbi számot a másik eszközén"</string>
<string name="screen_qr_code_login_initial_state_button_title">"Készen áll a beolvasásra"</string>
<string name="screen_qr_code_login_initial_state_item_1">"Nyissa meg az %1$set egy asztali eszközön"</string> <string name="screen_qr_code_login_initial_state_item_1">"Nyissa meg az %1$set egy asztali eszközön"</string>
<string name="screen_qr_code_login_initial_state_item_2">"Kattintson a profilképére"</string> <string name="screen_qr_code_login_initial_state_item_2">"Kattintson a profilképére"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Válassza ezt: %1$s"</string> <string name="screen_qr_code_login_initial_state_item_3">"Válassza ezt: %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"„Új eszköz összekapcsolása”"</string> <string name="screen_qr_code_login_initial_state_item_3_action">"„Új eszköz összekapcsolása”"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Kövesse a látható utasításokat"</string> <string name="screen_qr_code_login_initial_state_item_4">"Olvassa be a QR-kódot ezzel az eszközzel"</string>
<string name="screen_qr_code_login_initial_state_title">"Nyissa meg az %1$set egy másik eszközön a QR-kód lekéréséhez."</string> <string name="screen_qr_code_login_initial_state_title">"Nyissa meg az %1$set egy másik eszközön a QR-kód lekéréséhez."</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Használja a másik eszközön látható QR-kódot."</string> <string name="screen_qr_code_login_invalid_scan_state_description">"Használja a másik eszközön látható QR-kódot."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Próbálja újra"</string> <string name="screen_qr_code_login_invalid_scan_state_retry_button">"Próbálja újra"</string>

View file

@ -2,7 +2,33 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_notification_optin_subtitle">"Potrai modificare le tue impostazioni in seguito."</string> <string name="screen_notification_optin_subtitle">"Potrai modificare le tue impostazioni in seguito."</string>
<string name="screen_notification_optin_title">"Consenti le notifiche e non perdere mai un messaggio"</string> <string name="screen_notification_optin_title">"Consenti le notifiche e non perdere mai un messaggio"</string>
<string name="screen_qr_code_login_connecting_subtitle">"Stabilendo la connessione"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"Non è stato possibile stabilire una connessione sicura con il nuovo dispositivo. I tuoi dispositivi esistenti sono ancora al sicuro e non devi preoccuparti di loro."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"E adesso?"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Prova ad accedere di nuovo con un codice QR nel caso si sia verificato un problema di rete."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Se riscontri lo stesso problema, prova con un altra rete wifi o usa i dati mobili al posto del wifi."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Se il problema persiste, accedi manualmente"</string>
<string name="screen_qr_code_login_connection_note_secure_state_title">"La connessione non è sicura"</string>
<string name="screen_qr_code_login_device_code_subtitle">"Ti verrà chiesto di inserire le due cifre mostrate su questo dispositivo."</string>
<string name="screen_qr_code_login_device_code_title">"Inserisci il numero qui sotto sull\'altro dispositivo"</string>
<string name="screen_qr_code_login_initial_state_item_1">"Apri %1$s su un dispositivo desktop"</string>
<string name="screen_qr_code_login_initial_state_item_2">"Clicca sul tuo avatar"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Seleziona %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Collega un nuovo dispositivo\""</string>
<string name="screen_qr_code_login_initial_state_item_4">"Segui le istruzioni mostrate"</string>
<string name="screen_qr_code_login_initial_state_title">"Apri %1$s su un altro dispositivo per ottenere il codice QR"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Usa il codice QR mostrato sull\'altro dispositivo."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Riprova"</string> <string name="screen_qr_code_login_invalid_scan_state_retry_button">"Riprova"</string>
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Codice QR sbagliato"</string>
<string name="screen_qr_code_login_no_camera_permission_button">"Vai alle impostazioni della fotocamera"</string>
<string name="screen_qr_code_login_no_camera_permission_state_description">"Per continuare, è necessario fornire l\'autorizzazione a %1$s per utilizzare la fotocamera del dispositivo."</string>
<string name="screen_qr_code_login_no_camera_permission_state_title">"Consenti l\'accesso alla fotocamera per la scansione del codice QR"</string>
<string name="screen_qr_code_login_scanning_state_title">"Scansiona il codice QR"</string>
<string name="screen_qr_code_login_start_over_button">"Ricomincia"</string>
<string name="screen_qr_code_login_unknown_error_description">"Si è verificato un errore inatteso. Riprova."</string>
<string name="screen_qr_code_login_verify_code_loading">"In attesa dell\'altro dispositivo"</string>
<string name="screen_qr_code_login_verify_code_subtitle">"Il fornitore dell\'account potrebbe richiedere il seguente codice per verificare l\'accesso."</string>
<string name="screen_qr_code_login_verify_code_title">"Il tuo codice di verifica"</string>
<string name="screen_welcome_bullet_1">"Chiamate, sondaggi, ricerche e altro ancora saranno aggiunti nel corso dell\'anno."</string> <string name="screen_welcome_bullet_1">"Chiamate, sondaggi, ricerche e altro ancora saranno aggiunti nel corso dell\'anno."</string>
<string name="screen_welcome_bullet_2">"La cronologia dei messaggi per le stanze crittografate non è ancora disponibile."</string> <string name="screen_welcome_bullet_2">"La cronologia dei messaggi per le stanze crittografate non è ancora disponibile."</string>
<string name="screen_welcome_bullet_3">"Ci piacerebbe sentire il tuo parere, facci sapere cosa ne pensi tramite la pagina delle impostazioni."</string> <string name="screen_welcome_bullet_3">"Ci piacerebbe sentire il tuo parere, facci sapere cosa ne pensi tramite la pagina delle impostazioni."</string>

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