Merge branch 'release/26.03.0'

This commit is contained in:
Jorge Martín 2026-02-24 17:06:22 +01:00
commit 01aeca7121
1265 changed files with 5114 additions and 3964 deletions

View file

@ -25,6 +25,20 @@ jobs:
group: ${{ github.ref == 'refs/heads/develop' && format('build-develop-{0}-{1}', matrix.variant, github.sha) || format('build-{0}-{1}', matrix.variant, github.ref) }}
cancel-in-progress: true
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
with:
# This might remove tools that are actually needed, if set to "true" but frees about 6 GB
tool-cache: true
# All of these default to true, but we should only need the 'android' one (and maybe swap-storage?)
android: false
dotnet: true
haskell: true
# This takes way too long to run (~2 minutes) and it saves only ~5.5GB
large-packages: false
docker-images: true
swap-storage: false
- uses: actions/checkout@v6
with:
# Ensure we are building the branch and not the branch after being merged on develop
@ -60,16 +74,6 @@ jobs:
path: |
app/build/outputs/apk/gplay/debug/*-universal-debug.apk
app/build/outputs/apk/fdroid/debug/*-universal-debug.apk
- name: Upload x86_64 APK for Maestro
if: ${{ matrix.variant == 'debug' }}
uses: actions/upload-artifact@v6
with:
name: elementx-apk-maestro
path: |
app/build/outputs/apk/gplay/debug/app-gplay-x86_64-debug.apk
retention-days: 5
overwrite: true
if-no-files-found: error
- uses: rnkdsh/action-upload-diawi@4e1421305be7cfc510d05f47850262eeaf345108 # v1.5.12
id: diawi
# Do not fail the whole build if Diawi upload fails

View file

@ -27,6 +27,20 @@ jobs:
group: ${{ github.ref == 'refs/heads/develop' && format('build-develop-enterprise-{0}-{1}', matrix.variant, github.sha) || format('build-enterprise-{0}-{1}', matrix.variant, github.ref) }}
cancel-in-progress: true
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
with:
# This might remove tools that are actually needed, if set to "true" but frees about 6 GB
tool-cache: true
# All of these default to true, but we should only need the 'android' one (and maybe swap-storage?)
android: false
dotnet: true
haskell: true
# This takes way too long to run (~2 minutes) and it saves only ~5.5GB
large-packages: false
docker-images: true
swap-storage: false
- uses: actions/checkout@v6
with:
# Ensure we are building the branch and not the branch after being merged on develop

View file

@ -18,11 +18,24 @@ jobs:
build-apk:
name: Build APK
runs-on: ubuntu-latest
# Allow one per PR.
concurrency:
group: ${{ format('maestro-{0}', github.ref) }}
group: ${{ format('maestro-build-{0}', github.ref) }}
cancel-in-progress: true
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
with:
# This might remove tools that are actually needed, if set to "true" but frees about 6 GB
tool-cache: true
# All of these default to true, but we should only need the 'android' one (and maybe swap-storage?)
android: false
dotnet: true
haskell: true
# This takes way too long to run (~2 minutes) and it saves only ~5.5GB
large-packages: false
docker-images: true
swap-storage: false
- uses: actions/checkout@v6
with:
# Ensure we are building the branch and not the branch after being merged on develop
@ -57,10 +70,10 @@ jobs:
name: Maestro test suite
runs-on: ubuntu-latest
needs: [ build-apk ]
# Allow one per PR.
# Allow only one to run at a time, since they use the same environment.
# Otherwise, tests running in parallel can break each other.
concurrency:
group: ${{ format('maestro-{0}', github.ref) }}
cancel-in-progress: true
group: maestro-test
steps:
- uses: actions/checkout@v6
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
@ -110,6 +123,21 @@ jobs:
retention-days: 5
overwrite: true
if-no-files-found: error
- name: Update summary (success)
if: steps.maestro_test.outcome == 'success'
run: |
echo "### Maestro tests worked :rocket:!"
- name: Update summary (failure)
if: steps.maestro_test.outcome != 'success'
run: |
LOG_FILE=$(find ~/.maestro/tests/ -name maestro.log)
echo "Log file: $LOG_FILE"
LOG_LINES="$(tail -n 30 $LOG_FILE)"
echo "### :x: Maestro tests failed...
\`\`\`
$LOG_LINES
\`\`\`" >> $GITHUB_STEP_SUMMARY
- name: Fail the workflow in case of error in test
if: steps.maestro_test.outcome != 'success'
run: |

View file

@ -16,6 +16,20 @@ jobs:
runs-on: ubuntu-latest
if: ${{ github.repository == 'element-hq/element-x-android' }}
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
with:
# This might remove tools that are actually needed, if set to "true" but frees about 6 GB
tool-cache: true
# All of these default to true, but we should only need the 'android' one (and maybe swap-storage?)
android: false
dotnet: true
haskell: true
# This takes way too long to run (~2 minutes) and it saves only ~5.5GB
large-packages: false
docker-images: true
swap-storage: false
- uses: actions/checkout@v6
- name: Use JDK 21
uses: actions/setup-java@v5

View file

@ -17,6 +17,20 @@ jobs:
runs-on: ubuntu-latest
if: ${{ github.repository == 'element-hq/element-x-android' }}
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
with:
# This might remove tools that are actually needed, if set to "true" but frees about 6 GB
tool-cache: true
# All of these default to true, but we should only need the 'android' one (and maybe swap-storage?)
android: false
dotnet: true
haskell: true
# This takes way too long to run (~2 minutes) and it saves only ~5.5GB
large-packages: false
docker-images: true
swap-storage: false
- name: ⏬ Checkout with LFS
uses: nschloe/action-cached-lfs-checkout@f46300cd8952454b9f0a21a3d133d4bd5684cfc2 # v1.2.3

View file

@ -17,6 +17,20 @@ jobs:
name: Search for forbidden patterns
runs-on: ubuntu-latest
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
with:
# This might remove tools that are actually needed, if set to "true" but frees about 6 GB
tool-cache: true
# All of these default to true, but we should only need the 'android' one (and maybe swap-storage?)
android: false
dotnet: true
haskell: true
# This takes way too long to run (~2 minutes) and it saves only ~5.5GB
large-packages: false
docker-images: true
swap-storage: false
- uses: actions/checkout@v6
- name: Add SSH private keys for submodule repositories
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1

View file

@ -17,6 +17,20 @@ jobs:
if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'Record-Screenshots'
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
with:
# This might remove tools that are actually needed, if set to "true" but frees about 6 GB
tool-cache: true
# All of these default to true, but we should only need the 'android' one (and maybe swap-storage?)
android: false
dotnet: true
haskell: true
# This takes way too long to run (~2 minutes) and it saves only ~5.5GB
large-packages: false
docker-images: true
swap-storage: false
- name: Remove Record-Screenshots label
if: github.event.label.name == 'Record-Screenshots'
uses: actions-ecosystem/action-remove-labels@2ce5d41b4b6aa8503e285553f75ed56e0a40bae0 # v1.3.0

View file

@ -18,6 +18,20 @@ jobs:
group: ${{ format('build-release-main-gplay-{0}', github.sha) }}
cancel-in-progress: true
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
with:
# This might remove tools that are actually needed, if set to "true" but frees about 6 GB
tool-cache: true
# All of these default to true, but we should only need the 'android' one (and maybe swap-storage?)
android: false
dotnet: true
haskell: true
# This takes way too long to run (~2 minutes) and it saves only ~5.5GB
large-packages: false
docker-images: true
swap-storage: false
- uses: actions/checkout@v6
- name: Use JDK 21
uses: actions/setup-java@v5
@ -88,6 +102,20 @@ jobs:
group: ${{ format('build-release-main-fdroid-{0}', github.sha) }}
cancel-in-progress: true
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
with:
# This might remove tools that are actually needed, if set to "true" but frees about 6 GB
tool-cache: true
# All of these default to true, but we should only need the 'android' one (and maybe swap-storage?)
android: false
dotnet: true
haskell: true
# This takes way too long to run (~2 minutes) and it saves only ~5.5GB
large-packages: false
docker-images: true
swap-storage: false
- uses: actions/checkout@v6
- name: Use JDK 21
uses: actions/setup-java@v5

View file

@ -8,6 +8,13 @@
# Please see LICENSE in the repository root for full details.
#
# First we disable the onboarding flow on Chrome, which is a source of issues
# (see https://stackoverflow.com/a/64629745)
echo "Disabling Chrome onboarding flow"
adb shell am set-debug-app --persistent com.android.chrome
adb shell 'echo "chrome --disable-fre --no-default-browser-check --no-first-run" > /data/local/tmp/chrome-command-line'
adb shell am start -n com.android.chrome/com.google.android.apps.chrome.Main
adb install -r $1
echo "Starting the screen recording..."
adb push .github/workflows/scripts/maestro/local-recording.sh /data/local/tmp/

View file

@ -22,6 +22,20 @@ jobs:
group: ${{ format('sonar-{0}', github.ref) }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }}
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
with:
# This might remove tools that are actually needed, if set to "true" but frees about 6 GB
tool-cache: true
# All of these default to true, but we should only need the 'android' one (and maybe swap-storage?)
android: false
dotnet: true
haskell: true
# This takes way too long to run (~2 minutes) and it saves only ~5.5GB
large-packages: false
docker-images: true
swap-storage: false
- uses: actions/checkout@v6
with:
# Ensure we are building the branch and not the branch after being merged on develop

View file

@ -22,6 +22,20 @@ jobs:
group: ${{ github.ref == 'refs/heads/main' && format('unit-tests-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('unit-tests-develop-{0}', github.sha) || format('unit-tests-{0}', github.ref) }}
cancel-in-progress: true
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
with:
# This might remove tools that are actually needed, if set to "true" but frees about 6 GB
tool-cache: true
# All of these default to true, but we should only need the 'android' one (and maybe swap-storage?)
android: false
dotnet: true
haskell: true
# This takes way too long to run (~2 minutes) and it saves only ~5.5GB
large-packages: false
docker-images: true
swap-storage: false
# Increase swapfile size to prevent screenshot tests getting terminated
# https://github.com/actions/runner-images/discussions/7188#discussioncomment-6750749
- name: 💽 Increase swapfile size

View file

@ -8,27 +8,6 @@ appId: ${MAESTRO_APP_ID}
- tapOn:
id: "login-continue"
## MAS page
## Conditional workflow to pass the Chrome first launch welcome page.
- retry:
maxRetries: 3
commands:
- runFlow:
when:
visible: 'Use without an account'
commands:
- tapOn: "Use without an account"
## For older chrome versions
- runFlow:
when:
visible: 'Accept & continue'
commands:
- tapOn: "Accept & continue"
- runFlow:
when:
visible: 'No thanks'
commands:
- tapOn: "No thanks"
## Working when running Maestro locally, but not on the CI yet.
- retry:
maxRetries: 3
commands:

View file

@ -1,5 +1,8 @@
appId: ${MAESTRO_APP_ID}
---
- extendedWaitUntil:
visible: "Enter recovery key"
timeout: 30000
- takeScreenshot: build/maestro/150-Verify
- tapOn: "Enter recovery key"
- tapOn:
@ -7,7 +10,10 @@ appId: ${MAESTRO_APP_ID}
- inputText: ${MAESTRO_RECOVERY_KEY}
- hideKeyboard
- tapOn: "Continue"
- extendedWaitUntil:
visible: "Device verified"
timeout: 30000
- retry:
maxRetries: 3
commands:
- extendedWaitUntil:
visible: "Device verified"
timeout: 30000
- tapOn: "Continue"

View file

@ -2,4 +2,4 @@ appId: ${MAESTRO_APP_ID}
---
- extendedWaitUntil:
visible: "Be in your element"
timeout: 10000
timeout: 30000

View file

@ -2,4 +2,4 @@ appId: ${MAESTRO_APP_ID}
---
- extendedWaitUntil:
visible: "Confirm your identity"
timeout: 20000
timeout: 60000

View file

@ -1,3 +1,77 @@
Changes in Element X v26.02.0
=============================
<!-- Release notes generated using configuration in .github/release.yml at v26.02.0 -->
## What's Changed
### ✨ Features
* When a background SDK task fails, react in the client by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6166
* Enable space feature flags by default by @ganfra in https://github.com/element-hq/element-x-android/pull/6171
### 🙌 Improvements
* Improve space management with pagination and partial failure handling by @ganfra in https://github.com/element-hq/element-x-android/pull/6099
* Iterate on QrCode login error buttons by @bmarty in https://github.com/element-hq/element-x-android/pull/6101
* Update icon shown for world_readable rooms by @richvdh in https://github.com/element-hq/element-x-android/pull/6111
* QRCode login: treat not found error as expired error. by @bmarty in https://github.com/element-hq/element-x-android/pull/6161
* Iterate on Space related UI by @ganfra in https://github.com/element-hq/element-x-android/pull/6150
### 🔒 Security
* Ensure aspect ratio of images in the timeline is restricted by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6168
### 🐛 Bugfixes
* Ensure that Element Call activity is not closed when using an external link by @bmarty in https://github.com/element-hq/element-x-android/pull/6114
* Refresh a Space's room list after creating a room in it by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6135
* When creating a DM, set room history visibility to `invited` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6138
* Fix back navigation after creating a room in a space by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6134
* Fix `LinkifyHelper` index out of bounds with parenthesis by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6140
* Change role screen won't be dismissed until changes take effect by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6141
### 🗣 Translations
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/6122
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/6155
### 🧱 Build
* Try fixing Maestro tests (again) by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6149
* Add a stale bot for X-Needs-Info issues. by @bmarty in https://github.com/element-hq/element-x-android/pull/6153
* [Release script] Ensure that the release version will match the next Monday date by @bmarty in https://github.com/element-hq/element-x-android/pull/6152
### 🚧 In development 🚧
* Add Space Filters feature for Room List by @ganfra in https://github.com/element-hq/element-x-android/pull/6136
* Add history sharing badges to room details by @kaylendog in https://github.com/element-hq/element-x-android/pull/6132
### Dependency upgrades
* Update dependency androidx.work:work-runtime-ktx to v2.11.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6105
* Update metro to v0.10.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6106
* Update camera to v1.5.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6103
* Update activity to v1.12.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6104
* Update dependency com.posthog:posthog-android to v3.30.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6120
* Update kotlin by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6102
* Update roborazzi to v1.58.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6124
* Update kover by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6139
* Update dependency com.posthog:posthog-android to v3.31.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6145
* Update kotlin by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6142
* Update media3 to v1.9.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6151
* Update dependency org.matrix.rustcomponents:sdk-android to v26.02.6 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6144
* Update firebaseAppDistribution to v5.2.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6146
* Update dependency com.google.firebase:firebase-bom to v34.9.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6148
* Update dependency io.sentry:sentry-android to v8.32.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6157
* Update metro to v0.10.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6164
* Update dependency org.matrix.rustcomponents:sdk-android to v26.2.10 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6169
* chore(deps): update plugin paparazzi to v2.0.0-alpha04 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6048
* fix(deps): update dependency org.jetbrains.kotlinx:kover-gradle-plugin to v0.9.7 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6173
* fix(deps): update haze to v1.7.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6175
### Others
* Improve favorite wording and icon of room by @bmarty in https://github.com/element-hq/element-x-android/pull/6097
* Add special flow for leaving a space as the last owner by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6112
* Remove `runBlocking` in `ThreadedMessagesNode` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6108
* Revert "Add "call.pro.element.io" in the list of known hosts for Element Call." by @bmarty in https://github.com/element-hq/element-x-android/pull/6118
* Refactor room list filtering to use Rust SDK by @ganfra in https://github.com/element-hq/element-x-android/pull/6117
* Ensure http 429 are retried 3 times before failing. by @bmarty in https://github.com/element-hq/element-x-android/pull/6119
* Remove `JoinRule.Private` from the codebase by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6129
* Fix voice message recording not starting after permission is granted by @kknappe in https://github.com/element-hq/element-x-android/pull/6109
* Use correct bg color. by @bmarty in https://github.com/element-hq/element-x-android/pull/6165
* Document "Developer options" and remove outdated instructions by @MadLittleMods in https://github.com/element-hq/element-x-android/pull/6162
* Update SpaceFilterButton selected state color by @ganfra in https://github.com/element-hq/element-x-android/pull/6178
## New Contributors
* @kknappe made their first contribution in https://github.com/element-hq/element-x-android/pull/6109
* @MadLittleMods made their first contribution in https://github.com/element-hq/element-x-android/pull/6162
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.01.2...v26.02.0
Changes in Element X v26.01.2
=============================

View file

@ -79,7 +79,6 @@ import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.MAIN_SPACE
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.UserId
@ -113,6 +112,10 @@ import kotlin.time.Duration.Companion.seconds
import kotlin.time.toKotlinDuration
import im.vector.app.features.analytics.plan.JoinedRoom as JoinedRoomAnalyticsEvent
// The maximum number of room nodes that should be kept in the backstack at the same time.
// Having 5 rooms in the backstack seems reasonable and shouldn't grow the saved state size too much.
private const val MAX_ROOM_NODE_COUNT = 5
@ContributesNode(SessionScope::class)
@AssistedInject
class LoggedInFlowNode(
@ -211,8 +214,6 @@ class LoggedInFlowNode(
onCreate = {
analyticsRoomListStateWatcher.start()
appNavigationStateService.onNavigateToSession(id, matrixClient.sessionId)
// TODO We do not support Space yet, so directly navigate to main space
appNavigationStateService.onNavigateToSpace(id, MAIN_SPACE)
loggedInFlowProcessor.observeEvents(sessionCoroutineScope)
matrixClient.sessionVerificationService.setListener(verificationListener)
mediaPreviewConfigMigration()
@ -242,7 +243,6 @@ class LoggedInFlowNode(
}
},
onDestroy = {
appNavigationStateService.onLeavingSpace(id)
appNavigationStateService.onLeavingSession(id)
loggedInFlowProcessor.stopObserving()
matrixClient.sessionVerificationService.setListener(null)
@ -327,12 +327,13 @@ class LoggedInFlowNode(
NavTarget.Home -> {
val callback = object : HomeEntryPoint.Callback {
override fun navigateToRoom(roomId: RoomId, joinedRoom: JoinedRoom?) {
backstack.push(
NavTarget.Room(
lifecycleScope.launch {
attachRoom(
roomIdOrAlias = roomId.toRoomIdOrAlias(),
initialElement = RoomNavigationTarget.Root(joinedRoom = joinedRoom)
initialElement = RoomNavigationTarget.Root(joinedRoom = joinedRoom),
clearBackstack = false,
)
)
}
}
override fun navigateToSettings() {
@ -356,7 +357,13 @@ class LoggedInFlowNode(
}
override fun navigateToRoomSettings(roomId: RoomId) {
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.Details))
lifecycleScope.launch {
attachRoom(
roomIdOrAlias = roomId.toRoomIdOrAlias(),
initialElement = RoomNavigationTarget.Details,
clearBackstack = false
)
}
}
override fun navigateToBugReport() {
@ -372,7 +379,9 @@ class LoggedInFlowNode(
is NavTarget.Room -> {
val joinedRoomCallback = object : JoinedRoomLoadedFlowNode.Callback {
override fun navigateToRoom(roomId: RoomId, serverNames: List<String>) {
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), serverNames))
lifecycleScope.launch {
attachRoom(roomIdOrAlias = roomId.toRoomIdOrAlias(), serverNames = serverNames, clearBackstack = false)
}
}
override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) {
@ -382,16 +391,25 @@ class LoggedInFlowNode(
Timber.e("User link clicked: ${data.userId}.")
}
is PermalinkData.RoomLink -> {
val target = NavTarget.Room(
roomIdOrAlias = data.roomIdOrAlias,
serverNames = data.viaParameters,
trigger = JoinedRoomAnalyticsEvent.Trigger.Timeline,
initialElement = RoomNavigationTarget.Root(data.eventId),
)
if (pushToBackstack) {
backstack.push(target)
lifecycleScope.launch {
attachRoom(
roomIdOrAlias = data.roomIdOrAlias,
serverNames = data.viaParameters,
trigger = JoinedRoomAnalyticsEvent.Trigger.Timeline,
initialElement = RoomNavigationTarget.Root(data.eventId),
clearBackstack = false
)
}
} else {
backstack.replace(target)
backstack.replace(
NavTarget.Room(
roomIdOrAlias = data.roomIdOrAlias,
serverNames = data.viaParameters,
trigger = JoinedRoomAnalyticsEvent.Trigger.Timeline,
initialElement = RoomNavigationTarget.Root(data.eventId),
)
)
}
}
is PermalinkData.FallbackLink,
@ -417,7 +435,9 @@ class LoggedInFlowNode(
is NavTarget.UserProfile -> {
val callback = object : UserProfileEntryPoint.Callback {
override fun navigateToRoom(roomId: RoomId) {
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
lifecycleScope.launch {
attachRoom(roomIdOrAlias = roomId.toRoomIdOrAlias(), clearBackstack = false)
}
}
}
userProfileEntryPoint.createNode(
@ -446,11 +466,22 @@ class LoggedInFlowNode(
}
override fun navigateToRoomNotificationSettings(roomId: RoomId) {
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.NotificationSettings))
lifecycleScope.launch {
attachRoom(
roomIdOrAlias = roomId.toRoomIdOrAlias(),
initialElement = RoomNavigationTarget.NotificationSettings,
)
}
}
override fun navigateToEvent(roomId: RoomId, eventId: EventId) {
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.Root(eventId)))
lifecycleScope.launch {
attachRoom(
roomIdOrAlias = roomId.toRoomIdOrAlias(),
initialElement = RoomNavigationTarget.Root(eventId),
clearBackstack = false
)
}
}
}
val inputs = PreferencesEntryPoint.Params(navTarget.initialElement)
@ -481,7 +512,13 @@ class LoggedInFlowNode(
is NavTarget.CreateSpace -> {
val callback = object : CreateRoomEntryPoint.Callback {
override fun onRoomCreated(roomId: RoomId) {
backstack.replace(NavTarget.Room(roomIdOrAlias = RoomIdOrAlias.Id(roomId), serverNames = emptyList()))
lifecycleScope.launch {
attachRoom(
roomIdOrAlias = roomId.toRoomIdOrAlias(),
serverNames = emptyList(),
clearBackstack = false,
)
}
}
}
createRoomEntryPoint
@ -518,13 +555,13 @@ class LoggedInFlowNode(
buildContext = buildContext,
callback = object : RoomDirectoryEntryPoint.Callback {
override fun navigateToRoom(roomDescription: RoomDescription) {
backstack.push(
NavTarget.Room(
lifecycleScope.launch {
attachRoom(
roomIdOrAlias = roomDescription.roomId.toRoomIdOrAlias(),
roomDescription = roomDescription,
trigger = JoinedRoomAnalyticsEvent.Trigger.RoomDirectory,
)
)
}
}
},
)
@ -541,7 +578,7 @@ class LoggedInFlowNode(
// Navigate to the room if the text/media was shared to a single one
roomIds.singleOrNull()?.let { roomId ->
sessionCoroutineScope.launch {
lifecycleScope.launch {
// Wait until the incoming share screen is removed
backstack.elements.first { it.lastOrNull()?.key?.navTarget !is NavTarget.IncomingShare }
@ -572,8 +609,9 @@ class LoggedInFlowNode(
roomIdOrAlias: RoomIdOrAlias,
serverNames: List<String> = emptyList(),
trigger: JoinedRoomAnalyticsEvent.Trigger? = null,
eventId: EventId? = null,
clearBackstack: Boolean,
roomDescription: RoomDescription? = null,
initialElement: RoomNavigationTarget = RoomNavigationTarget.Root(),
clearBackstack: Boolean = false,
): RoomFlowNode {
waitForNavTargetAttached { navTarget ->
navTarget is NavTarget.Home
@ -582,8 +620,9 @@ class LoggedInFlowNode(
val roomNavTarget = NavTarget.Room(
roomIdOrAlias = roomIdOrAlias,
serverNames = serverNames,
roomDescription = roomDescription,
trigger = trigger,
initialElement = RoomNavigationTarget.Root(eventId = eventId)
initialElement = initialElement,
)
backstack.accept(AttachRoomOperation(roomNavTarget, clearBackstack))
}
@ -593,8 +632,7 @@ class LoggedInFlowNode(
return waitForChildAttached<RoomFlowNode, NavTarget> {
it is NavTarget.Room &&
it.roomIdOrAlias == roomIdOrAlias &&
it.initialElement is RoomNavigationTarget.Root &&
it.initialElement.eventId == eventId
it.initialElement == initialElement
}
}
@ -640,7 +678,7 @@ class LoggedInFlowNode(
) { contentModifier ->
Box(modifier = contentModifier) {
val ftueState by ftueService.state.collectAsState()
BackstackView()
BackstackView(transitionHandler = rememberLoggedInFlowTransitionHandler(backstack))
if (ftueState is FtueState.Complete) {
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
}
@ -655,6 +693,15 @@ private class AttachRoomOperation(
val roomTarget: LoggedInFlowNode.NavTarget.Room,
val clearBackstack: Boolean,
) : BackStackOperation<LoggedInFlowNode.NavTarget> {
/**
* Returns a list containing last [count] elements that match [predicate] while preserving other elements.
*/
private fun <T> List<T>.keepingLast(count: Int, predicate: (T) -> Boolean): List<T> {
val matchingIndices = indices.filter { predicate(this[it]) }
val indicesToRemove = matchingIndices.dropLast(count).toSet()
return filterIndexed { index, _ -> index !in indicesToRemove }
}
override fun isApplicable(elements: NavElements<LoggedInFlowNode.NavTarget, BackStack.State>) = true
override fun invoke(elements: BackStackElements<LoggedInFlowNode.NavTarget>): BackStackElements<LoggedInFlowNode.NavTarget> {
@ -677,16 +724,34 @@ private class AttachRoomOperation(
val roomNavTarget = it.key.navTarget as? LoggedInFlowNode.NavTarget.Room
roomNavTarget?.roomIdOrAlias == roomTarget.roomIdOrAlias
}
// Make sure the backstack of rooms can't grow indefinitely when opening permalinks.
val roomElementCount = elements.count { it.key.navTarget is LoggedInFlowNode.NavTarget.Room }
Timber.d("Current room nodes: $roomElementCount/$MAX_ROOM_NODE_COUNT")
// Crate a new list keeping all the elements, but for Room ones just keep the last MAX_ROOM_NODE_COUNT
val currentElements = elements.keepingLast(MAX_ROOM_NODE_COUNT) { element ->
element.key.navTarget is LoggedInFlowNode.NavTarget.Room
}
// If the room already existed, remove it from the stack and add a new node at the end
if (existingRoomElement != null) {
elements.mapNotNull { element ->
currentElements.mapNotNull { element ->
if (element == existingRoomElement) {
null
} else {
element.transitionTo(STASHED, this)
}
} + existingRoomElement.transitionTo(ACTIVE, this)
} + // Always create a new element, otherwise we wouldn't be navigating to the target event id or child node
BackStackElement(
key = NavKey(roomTarget),
fromState = CREATED,
targetState = ACTIVE,
operation = this
)
} else {
Push<LoggedInFlowNode.NavTarget>(roomTarget).invoke(elements)
// Otherwise, just push the new node to the end of the backstack
Push<LoggedInFlowNode.NavTarget>(roomTarget).invoke(currentElements)
}
}
}

View file

@ -0,0 +1,58 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.appnav
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.Transition
import androidx.compose.animation.core.spring
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.navigation.transition.ModifierTransitionHandler
import com.bumble.appyx.core.navigation.transition.TransitionDescriptor
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.transitionhandler.rememberBackstackFader
import com.bumble.appyx.navmodel.backstack.transitionhandler.rememberBackstackSlider
/**
* A TransitionHandler that uses fade transition when Placeholder is being removed,
* and slide transition for all other cases.
*/
class LoggedInFlowTransitionHandler(
private val backstack: BackStack<LoggedInFlowNode.NavTarget>,
private val slider: ModifierTransitionHandler<LoggedInFlowNode.NavTarget, BackStack.State>,
private val fader: ModifierTransitionHandler<LoggedInFlowNode.NavTarget, BackStack.State>,
) : ModifierTransitionHandler<LoggedInFlowNode.NavTarget, BackStack.State>() {
override fun createModifier(
modifier: Modifier,
transition: Transition<BackStack.State>,
descriptor: TransitionDescriptor<LoggedInFlowNode.NavTarget, BackStack.State>
): Modifier {
val isPlaceholderBeingRemoved = backstack.elements.value.any { element ->
element.key.navTarget == LoggedInFlowNode.NavTarget.Placeholder &&
element.targetState != BackStack.State.ACTIVE
}
val handler = if (isPlaceholderBeingRemoved) fader else slider
return handler.createModifier(modifier, transition, descriptor)
}
}
@Composable
fun rememberLoggedInFlowTransitionHandler(
backstack: BackStack<LoggedInFlowNode.NavTarget>,
): ModifierTransitionHandler<LoggedInFlowNode.NavTarget, BackStack.State> {
val slider = rememberBackstackSlider<LoggedInFlowNode.NavTarget>(
transitionSpec = { spring(stiffness = Spring.StiffnessMediumLow) },
)
val fader = rememberBackstackFader<LoggedInFlowNode.NavTarget>(
transitionSpec = { spring(stiffness = Spring.StiffnessMediumLow) },
)
return remember(backstack, slider, fader) {
LoggedInFlowTransitionHandler(backstack, slider, fader)
}
}

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.appnav
import com.bumble.appyx.core.navigation.NavElements
import com.bumble.appyx.core.navigation.Operation
import com.bumble.appyx.navmodel.backstack.BackStack
import kotlinx.parcelize.Parcelize
/**
* Replaces all the current elements with the provided [navElements], keeping their [BackStack.State] too.
*/
@Parcelize
class ReplaceAllOperation<NavTarget : Any>(
private val navElements: NavElements<NavTarget, BackStack.State>
) : Operation<NavTarget, BackStack.State> {
override fun isApplicable(elements: NavElements<NavTarget, BackStack.State>): Boolean {
return true
}
override fun invoke(existing: NavElements<NavTarget, BackStack.State>): NavElements<NavTarget, BackStack.State> {
return navElements
}
}

View file

@ -16,9 +16,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.navigation.NavElements
import com.bumble.appyx.core.navigation.NavKey
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.state.MutableSavedStateMap
import com.bumble.appyx.core.state.SavedStateMap
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push
@ -33,6 +36,7 @@ import io.element.android.appnav.di.MatrixSessionCache
import io.element.android.appnav.intent.IntentResolver
import io.element.android.appnav.intent.ResolvedIntent
import io.element.android.appnav.room.RoomFlowNode
import io.element.android.appnav.room.RoomNavigationTarget
import io.element.android.appnav.root.RootNavStateFlowFactory
import io.element.android.appnav.root.RootPresenter
import io.element.android.appnav.root.RootView
@ -49,6 +53,7 @@ import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.waitForChildAttached
import io.element.android.libraries.core.uri.ensureProtocol
import io.element.android.libraries.deeplink.api.DeeplinkData
import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.core.EventId
@ -66,7 +71,9 @@ import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.api.watchers.AnalyticsColdStartWatcher
import io.element.android.services.appnavstate.api.ROOM_OPENED_FROM_NOTIFICATION
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@ -92,19 +99,27 @@ class RootFlowNode(
private val announcementService: AnnouncementService,
private val analyticsService: AnalyticsService,
private val analyticsColdStartWatcher: AnalyticsColdStartWatcher,
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
) : BaseFlowNode<RootFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.SplashScreen,
savedStateMap = buildContext.savedStateMap,
savedStateMap = null,
),
buildContext = buildContext,
plugins = plugins
) {
override fun onBuilt() {
analyticsColdStartWatcher.start()
matrixSessionCache.restoreWithSavedState(buildContext.savedStateMap)
appCoroutineScope.launch {
matrixSessionCache.restoreWithSavedState(buildContext.savedStateMap)
if (buildContext.savedStateMap != null) {
restoreSavedState(buildContext.savedStateMap)
observeNavState(true)
} else {
observeNavState(false)
}
}
super.onBuilt()
observeNavState()
}
override fun onSaveInstanceState(state: MutableSavedStateMap) {
@ -113,25 +128,68 @@ class RootFlowNode(
navStateFlowFactory.saveIntoSavedState(state)
}
private fun observeNavState() {
navStateFlowFactory.create(buildContext.savedStateMap).distinctUntilChanged().onEach { navState ->
Timber.v("navState=$navState")
when (navState.loggedInState) {
is LoggedInState.LoggedIn -> {
if (navState.loggedInState.isTokenValid) {
tryToRestoreLatestSession(
onSuccess = { sessionId -> switchToLoggedInFlow(sessionId, navState.cacheIndex) },
onFailure = { switchToNotLoggedInFlow(null) }
)
} else {
switchToSignedOutFlow(SessionId(navState.loggedInState.sessionId))
private fun observeNavState(skipFirst: Boolean) {
navStateFlowFactory.create(buildContext.savedStateMap)
.distinctUntilChanged()
.drop(if (skipFirst) 1 else 0)
.onEach { navState ->
Timber.v("navState=$navState")
when (navState.loggedInState) {
is LoggedInState.LoggedIn -> {
if (navState.loggedInState.isTokenValid) {
val sessionId = SessionId(navState.loggedInState.sessionId)
if (matrixSessionCache.getOrNull(sessionId) != null) {
switchToLoggedInFlow(sessionId, navState.cacheIndex)
} else {
tryToRestoreLatestSession(
onSuccess = { sessionId -> switchToLoggedInFlow(sessionId, navState.cacheIndex) },
onFailure = { switchToNotLoggedInFlow(null) }
)
}
} else {
switchToSignedOutFlow(SessionId(navState.loggedInState.sessionId))
}
}
LoggedInState.NotLoggedIn -> {
switchToNotLoggedInFlow(null)
}
}
LoggedInState.NotLoggedIn -> {
switchToNotLoggedInFlow(null)
}
}
}.launchIn(lifecycleScope)
.launchIn(lifecycleScope)
}
/**
* Restore the saved state for navigation in the current backstack.
*
* **WARNING:** this is an unsafe operation abusing the internals of the Appyx library, but it's the only way allow async state
* restoration and not having to block the main thread when the app starts.
*
* Modify with utmost care and double check any possible Appyx updates that might break this.
*/
@Suppress("UNCHECKED_CAST")
private fun restoreSavedState(savedStateMap: SavedStateMap?) {
if (savedStateMap == null) return
// 'NavModel' is the key used for storing the nav model state data in the map in Appyx
val savedElements = buildContext.savedStateMap?.get("NavModel") as? NavElements<NavTarget, BackStack.State>
if (savedElements != null) {
backstack.accept(ReplaceAllOperation(savedElements))
}
}
/**
* Extract the saved state for navigation in the [navTarget].
*
* **WARNING:** this is an unsafe operation abusing the internals of the Appyx library, but it's the only way allow async state
* restoration and not having to block the main thread when the app starts.
*
* Modify with utmost care and double check any possible Appyx updates that might break this.
*/
@Suppress("UNCHECKED_CAST")
private fun extractSavedStateForNavTarget(navTarget: NavTarget, savedStateMap: SavedStateMap?): SavedStateMap? {
// 'ChildrenState' is the key used for storing the children state data in the map in Appyx
val childrenState = savedStateMap?.get("ChildrenState") as? Map<NavKey<NavTarget>, SavedStateMap> ?: return null
return childrenState.entries.find { (key, _) -> key.navTarget == navTarget }?.value
}
private fun switchToLoggedInFlow(sessionId: SessionId, navId: Int) {
@ -243,6 +301,13 @@ class RootFlowNode(
backstack.push(NavTarget.NotLoggedInFlow(null))
}
}
val savedNavState = extractSavedStateForNavTarget(navTarget, this.buildContext.savedStateMap)
val buildContext = if (savedNavState != null) {
Timber.d("Creating a $navTarget with restored saved state")
buildContext.copy(savedStateMap = savedNavState)
} else {
buildContext.copy(savedStateMap = savedNavState)
}
createNode<LoggedInAppScopeFlowNode>(buildContext, plugins = listOf(inputs, callback))
}
is NavTarget.NotLoggedInFlow -> {
@ -430,7 +495,7 @@ class RootFlowNode(
roomIdOrAlias = permalinkData.roomIdOrAlias,
trigger = JoinedRoom.Trigger.MobilePermalink,
serverNames = permalinkData.viaParameters,
eventId = focusedEventId,
initialElement = RoomNavigationTarget.Root(eventId = focusedEventId),
clearBackstack = true
).maybeAttachThread(permalinkData.threadId, permalinkData.eventId)
}
@ -454,7 +519,7 @@ class RootFlowNode(
is DeeplinkData.Room -> {
loggedInFlowNode.attachRoom(
roomIdOrAlias = deeplinkData.roomId.toRoomIdOrAlias(),
eventId = if (deeplinkData.threadId != null) deeplinkData.threadId?.asEventId() else deeplinkData.eventId,
initialElement = RoomNavigationTarget.Root(eventId = deeplinkData.threadId?.asEventId() ?: deeplinkData.eventId),
clearBackstack = true,
).maybeAttachThread(deeplinkData.threadId, deeplinkData.eventId)
}

View file

@ -21,7 +21,6 @@ import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analyticsproviders.api.AnalyticsUserData
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
@ -77,20 +76,18 @@ class MatrixSessionCache(
}
@Suppress("UNCHECKED_CAST")
fun restoreWithSavedState(state: SavedStateMap?) {
suspend fun restoreWithSavedState(state: SavedStateMap?) {
Timber.d("Restore state")
if (state == null || sessionIdsToMatrixSession.isNotEmpty()) {
Timber.w("Restore with non-empty map")
Timber.w("No need to restore saved state")
return
}
val sessionIds = state[SAVE_INSTANCE_KEY] as? Array<SessionId>
Timber.d("Restore matrix session keys = ${sessionIds?.map { it.value }}")
if (sessionIds.isNullOrEmpty()) return
// Not ideal but should only happens in case of process recreation. This ensure we restore all the active sessions before restoring the node graphs.
runBlocking {
sessionIds.forEach { sessionId ->
getOrRestore(sessionId)
}
sessionIds.forEach { sessionId ->
getOrRestore(sessionId)
}
}

@ -1 +1 @@
Subproject commit 6207ddc1cb7dac7fdb6212c0158497a1d9752c75
Subproject commit 1fd0d297d944186e3af2773e1c5db2938d60f74b

View file

@ -0,0 +1,2 @@
Main changes in this version: bug fixes and improvements.
Full changelog: https://github.com/element-hq/element-x-android/releases

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_space_announcement_item1">"Ver espaços que criaste ou nos quais entraste"</string>
<string name="screen_space_announcement_item2">"Aceitar ou recusar convites para espaços"</string>
<string name="screen_space_announcement_item3">"Descobrir todas as salas dos seus espaços nas quais podes entrar"</string>
<string name="screen_space_announcement_item4">"Entrar em espaços públicos"</string>
<string name="screen_space_announcement_item5">"Deixar todos os espaços em que entraste"</string>
<string name="screen_space_announcement_notice">"Em breve, será possível filtrar, criar e gerir espaços."</string>
<string name="screen_space_announcement_subtitle">"Eis a versão beta dos Espaços! Nesta primeira versão, podes:"</string>
<string name="screen_space_announcement_title">"Apresentamos os Espaços"</string>
</resources>

View file

@ -29,6 +29,7 @@ class DeclineCallBroadcastReceiver : BroadcastReceiver() {
companion object {
const val EXTRA_NOTIFICATION_DATA = "EXTRA_NOTIFICATION_DATA"
}
@Inject
lateinit var activeCallManager: ActiveCallManager
@ -40,7 +41,13 @@ class DeclineCallBroadcastReceiver : BroadcastReceiver() {
?: return
context.bindings<CallBindings>().inject(this)
appCoroutineScope.launch {
activeCallManager.hungUpCall(callType = CallType.RoomCall(notificationData.sessionId, notificationData.roomId))
activeCallManager.hangUpCall(
callType = CallType.RoomCall(
sessionId = notificationData.sessionId,
roomId = notificationData.roomId,
),
notificationData = notificationData,
)
}
}
}

View file

@ -100,7 +100,7 @@ class CallScreenPresenter(
)
}
onDispose {
appCoroutineScope.launch { activeCallManager.hungUpCall(callType) }
appCoroutineScope.launch { activeCallManager.hangUpCall(callType) }
}
}

View file

@ -118,7 +118,7 @@ class IncomingCallActivity : AppCompatActivity() {
private fun onCancel() {
val activeCall = activeCallManager.activeCall.value ?: return
appCoroutineScope.launch {
activeCallManager.hungUpCall(callType = activeCall.callType)
activeCallManager.hangUpCall(callType = activeCall.callType)
}
}
}

View file

@ -51,6 +51,9 @@ import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.ui.strings.CommonStrings
/**
* Ref: https://www.figma.com/design/0MMNu7cTOzLOlWb7ctTkv3/Element-X?node-id=16501-5740
*/
@Composable
internal fun IncomingCallScreen(
notificationData: CallNotificationData,
@ -94,11 +97,8 @@ internal fun IncomingCallScreen(
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 24.dp, end = 24.dp, bottom = 64.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
modifier = Modifier.padding(bottom = 64.dp),
horizontalArrangement = Arrangement.spacedBy(48.dp),
) {
ActionButton(
size = 64.dp,
@ -108,7 +108,6 @@ internal fun IncomingCallScreen(
backgroundColor = ElementTheme.colors.iconSuccessPrimary,
borderColor = ElementTheme.colors.borderSuccessSubtle
)
ActionButton(
size = 64.dp,
onClick = onCancel,
@ -143,7 +142,7 @@ private fun ActionButton(
onClick = onClick,
colors = IconButtonDefaults.filledIconButtonColors(
containerColor = backgroundColor,
contentColor = Color.White,
contentColor = ElementTheme.colors.iconOnSolidPrimary,
)
) {
Icon(

View file

@ -72,10 +72,14 @@ interface ActiveCallManager {
suspend fun registerIncomingCall(notificationData: CallNotificationData)
/**
* Called when the active call has been hung up. It will remove any existing UI and the active call.
* @param callType The type of call that the user hung up, either an external url one or a room one.
* Called to hang up the active call. It will hang up the call and remove any existing UI and the active call.
* @param callType The type of call that the user hangs up, either an external url one or a room one.
* @param notificationData The data for the incoming call notification.
*/
suspend fun hungUpCall(callType: CallType)
suspend fun hangUpCall(
callType: CallType,
notificationData: CallNotificationData? = null,
)
/**
* Called after the user joined a call. It will remove any existing UI and set the call state as [CallState.InCall].
@ -192,12 +196,28 @@ class DefaultActiveCallManager(
}
}
override suspend fun hungUpCall(callType: CallType) = mutex.withLock {
Timber.tag(tag).d("Hung up call: $callType")
override suspend fun hangUpCall(
callType: CallType,
notificationData: CallNotificationData?,
) = mutex.withLock {
Timber.tag(tag).d("Hang up call: $callType")
cancelIncomingCallNotification()
val currentActiveCall = activeCall.value ?: run {
// activeCall.value can be null if the application has been killed while the call was ringing
// Build a currentActiveCall with the provided parameters.
notificationData?.let {
ActiveCall(
callType = callType,
callState = CallState.Ringing(
notificationData = notificationData,
)
)
}
} ?: run {
Timber.tag(tag).w("No active call, ignoring hang up")
return@withLock
}
if (currentActiveCall.callType != callType) {
Timber.tag(tag).w("Call type $callType does not match the active call type, ignoring")
return@withLock
@ -208,9 +228,13 @@ class DefaultActiveCallManager(
matrixClientProvider.getOrRestore(notificationData.sessionId).getOrNull()
?.getRoom(notificationData.roomId)
?.declineCall(notificationData.eventId)
?.onFailure {
Timber.e(it, "Failed to decline incoming call")
}
?: run {
Timber.tag(tag).d("Couldn't find session or room to decline call for incoming call")
}
}
cancelIncomingCallNotification()
if (activeWakeLock?.isHeld == true) {
Timber.tag(tag).d("Releasing partial wakelock after hang up")
activeWakeLock.release()
@ -221,7 +245,6 @@ class DefaultActiveCallManager(
override suspend fun joinedCall(callType: CallType) = mutex.withLock {
Timber.tag(tag).d("Joined call: $callType")
cancelIncomingCallNotification()
if (activeWakeLock?.isHeld == true) {
Timber.tag(tag).d("Releasing partial wakelock after joining call")

View file

@ -18,6 +18,7 @@ import android.webkit.JavascriptInterface
import android.webkit.WebView
import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService
import io.element.android.libraries.core.extensions.runCatchingExceptions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@ -82,6 +83,13 @@ class WebViewAudioManager(
AudioDeviceInfo.TYPE_BUILTIN_EARPIECE,
)
private val audioDeviceComparator = Comparator<AudioDeviceInfo> { a, b ->
// If the device type is not in the wantedDeviceTypes list, we give it a high index, (i.e. low priority)
val indexOfA = wantedDeviceTypes.indexOf(a.type).let { if (it == -1) Int.MAX_VALUE else it }
val indexOfB = wantedDeviceTypes.indexOf(b.type).let { if (it == -1) Int.MAX_VALUE else it }
indexOfA.compareTo(indexOfB)
}
private val audioManager = webView.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
/**
@ -134,7 +142,7 @@ class WebViewAudioManager(
if (validNewDevices.isEmpty()) return
// We need to calculate the available devices ourselves, since calling `listAudioDevices` will return an outdated list
val audioDevices = (listAudioDevices() + validNewDevices).distinctBy { it.id }
val audioDevices = (listAudioDevices() + validNewDevices).distinctBy { it.id }.sortedWith(audioDeviceComparator)
setAvailableAudioDevices(audioDevices.map(SerializableAudioDevice::fromAudioDeviceInfo))
// This should automatically switch to a new device if it has a higher priority than the current one
selectDefaultAudioDevice(audioDevices)
@ -294,7 +302,7 @@ class WebViewAudioManager(
}
/**
* Returns the list of available audio devices.
* Returns the list of available audio devices, sorted by likelihood of it being used for communication.
*
* On Android 11 ([Build.VERSION_CODES.R]) and lower, it returns the list of output devices as a fallback.
*/
@ -304,7 +312,7 @@ class WebViewAudioManager(
} else {
val rawAudioDevices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
rawAudioDevices.filter { it.type in wantedDeviceTypes && it.isSink }
}
}.sortedWith(audioDeviceComparator)
}
/**
@ -323,19 +331,12 @@ class WebViewAudioManager(
}
/**
* Selects the default audio device based on the available devices.
* Selects the default audio device based on the sorted available devices.
*
* @param availableDevices The list of available audio devices to select from. If not provided, it will use the current list of audio devices.
*/
private fun selectDefaultAudioDevice(availableDevices: List<AudioDeviceInfo> = listAudioDevices()) {
val selectedDevice = availableDevices
.minByOrNull {
wantedDeviceTypes.indexOf(it.type).let { index ->
// If the device type is not in the wantedDeviceTypes list, we give it a low priority
if (index == -1) Int.MAX_VALUE else index
}
}
val selectedDevice = availableDevices.firstOrNull()
expectedNewCommunicationDeviceId = selectedDevice?.id
audioManager.selectAudioDevice(selectedDevice)
@ -385,10 +386,18 @@ class WebViewAudioManager(
currentDeviceId = device?.id
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (device != null) {
Timber.d("Setting communication device: ${device.id} - ${deviceName(device.type, device.productName.toString())}")
setCommunicationDevice(device)
runCatchingExceptions {
Timber.d("Setting communication device: ${device.id} - ${deviceName(device.type, device.productName.toString())}")
setCommunicationDevice(device)
}.onFailure {
Timber.e(it, "Could not set communication device.")
}
} else {
audioManager.clearCommunicationDevice()
runCatchingExceptions {
clearCommunicationDevice()
}.onFailure {
Timber.e(it, "Could not clear communication device.")
}
}
} else {
// On Android 11 and lower, we don't have the concept of communication devices

View file

@ -155,7 +155,7 @@ class DefaultActiveCallManagerTest {
}
@Test
fun `hungUpCall - removes existing call if the CallType matches`() = runTest {
fun `hangUpCall - removes existing call if the CallType matches`() = runTest {
setupShadowPowerManager()
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
@ -165,7 +165,7 @@ class DefaultActiveCallManagerTest {
assertThat(manager.activeCall.value).isNotNull()
assertThat(manager.activeWakeLock?.isHeld).isTrue()
manager.hungUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId))
manager.hangUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId))
assertThat(manager.activeCall.value).isNull()
assertThat(manager.activeWakeLock?.isHeld).isFalse()
@ -192,13 +192,41 @@ class DefaultActiveCallManagerTest {
val notificationData = aCallNotificationData(roomId = A_ROOM_ID)
manager.registerIncomingCall(notificationData)
manager.hungUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId))
manager.hangUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId))
coVerify {
room.declineCall(notificationEventId = notificationData.eventId)
}
}
@Test
fun `Decline event - Hangup on a unknown call should send a decline event`() = runTest {
setupShadowPowerManager()
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
val room = mockk<JoinedRoom>(relaxed = true)
val matrixClient = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, room)
}
val clientProvider = FakeMatrixClientProvider({ Result.success(matrixClient) })
val manager = createActiveCallManager(
matrixClientProvider = clientProvider,
notificationManagerCompat = notificationManagerCompat
)
val notificationData = aCallNotificationData(roomId = A_ROOM_ID)
// Do not register the incoming call, so the manager doesn't know about it
manager.hangUpCall(
callType = CallType.RoomCall(notificationData.sessionId, notificationData.roomId),
notificationData = notificationData,
)
coVerify {
room.declineCall(notificationEventId = notificationData.eventId)
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `Decline event - Declining from another session should stop ringing`() = runTest {
@ -269,7 +297,7 @@ class DefaultActiveCallManagerTest {
}
@Test
fun `hungUpCall - does nothing if the CallType doesn't match`() = runTest {
fun `hangUpCall - does nothing if the CallType doesn't match`() = runTest {
setupShadowPowerManager()
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
@ -278,11 +306,12 @@ class DefaultActiveCallManagerTest {
assertThat(manager.activeCall.value).isNotNull()
assertThat(manager.activeWakeLock?.isHeld).isTrue()
manager.hungUpCall(CallType.ExternalUrl("https://example.com"))
manager.hangUpCall(CallType.ExternalUrl("https://example.com"))
assertThat(manager.activeCall.value).isNotNull()
assertThat(manager.activeWakeLock?.isHeld).isTrue()
verify(exactly = 0) { notificationManagerCompat.cancel(notificationId) }
// The notification is always cancelled do not block the user
verify(exactly = 1) { notificationManagerCompat.cancel(notificationId) }
}
@OptIn(ExperimentalCoroutinesApi::class)

View file

@ -17,7 +17,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
class FakeActiveCallManager(
var registerIncomingCallResult: (CallNotificationData) -> Unit = {},
var hungUpCallResult: (CallType) -> Unit = {},
var hangUpCallResult: (CallType, CallNotificationData?) -> Unit = { _, _ -> },
var joinedCallResult: (CallType) -> Unit = {},
) : ActiveCallManager {
override val activeCall = MutableStateFlow<ActiveCall?>(null)
@ -26,8 +26,8 @@ class FakeActiveCallManager(
registerIncomingCallResult(notificationData)
}
override suspend fun hungUpCall(callType: CallType) = simulateLongTask {
hungUpCallResult(callType)
override suspend fun hangUpCall(callType: CallType, notificationData: CallNotificationData?) = simulateLongTask {
hangUpCallResult(callType, notificationData)
}
override suspend fun joinedCall(callType: CallType) = simulateLongTask {

View file

@ -6,8 +6,6 @@
<string name="screen_create_room_private_option_description">"Толькі запрошаныя людзі могуць атрымаць доступ да гэтага пакоя. Усе паведамленні абаронены end-to-end шыфраваннем."</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_access_section_knocking_option_title">"Папрасіце далучыцца"</string>
<string name="screen_create_room_room_access_section_public_option_title">"Хто заўгодна"</string>
<string name="screen_create_room_topic_label">"Тэма (неабавязкова)"</string>
</resources>

View file

@ -6,9 +6,7 @@
<string name="screen_create_room_private_option_description">"Само поканени хора имат достъп до тази стая. Всички съобщения са шифровани от край до край."</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_access_section_public_option_description">"Всеки може да се присъедини към тази стая"</string>
<string name="screen_create_room_room_access_section_public_option_title">"Всеки"</string>
<string name="screen_create_room_room_address_section_footer">"За да бъде тази стая видима в директорията на общодостъпните стаи, ще ви е необходим адрес на стаята."</string>
<string name="screen_create_room_room_visibility_section_title">"Видимост на стаята"</string>
<string name="screen_create_room_topic_label">"Тема за разговор (незадължително)"</string>

View file

@ -8,19 +8,19 @@
<string name="screen_create_room_new_room_title">"Nová místnost"</string>
<string name="screen_create_room_new_space_title">"Nový prostor"</string>
<string name="screen_create_room_private_option_description">"Do této místnosti mohou vstoupit pouze pozvaní."</string>
<string name="screen_create_room_private_option_title">"Soukromý"</string>
<string name="screen_create_room_private_option_title">"Soukromé"</string>
<string name="screen_create_room_public_option_description">"Tuto místnost může najít kdokoli.
To můžete kdykoli změnit v nastavení místnosti."</string>
<string name="screen_create_room_public_option_short_description">"Vstoupit může kdokoli."</string>
<string name="screen_create_room_public_option_title">"Veřejná místnost"</string>
<string name="screen_create_room_public_option_title">"Veřejné"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Kdokoli může požádat o vstup do místnosti, ale správce nebo moderátor bude muset žádost přijmout."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Povolit žádost o vstup"</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"Kdokoli v %1$s může vstoupit, ale všichni ostatní si musí o přístup požádat."</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"Požádat o vstup"</string>
<string name="screen_create_room_room_access_section_private_option_description">"Vstoupit mohou pouze pozvaní lidé."</string>
<string name="screen_create_room_room_access_section_private_option_title">"Soukromý"</string>
<string name="screen_create_room_room_access_section_private_option_title">"Soukromé"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Vstoupit může kdokoli."</string>
<string name="screen_create_room_room_access_section_public_option_title">"Kdokoliv"</string>
<string name="screen_create_room_room_access_section_public_option_title">"Veřejné"</string>
<string name="screen_create_room_room_access_section_restricted_option_description">"Kdokoli může vstoupit do %1$s."</string>
<string name="screen_create_room_room_access_section_restricted_option_title">"Standard"</string>
<string name="screen_create_room_room_access_section_title">"Kdo má přístup"</string>
@ -28,7 +28,8 @@ To můžete kdykoli změnit v nastavení místnosti."</string>
<string name="screen_create_room_room_address_section_title">"Adresa"</string>
<string name="screen_create_room_room_visibility_section_title">"Viditelnost místnosti"</string>
<string name="screen_create_room_space_selection_no_space_description">"(bez prostoru)"</string>
<string name="screen_create_room_space_selection_no_space_title">"Domov"</string>
<string name="screen_create_room_space_selection_no_space_option">"Nepřidávejte do prostoru"</string>
<string name="screen_create_room_space_selection_no_space_title">"Není vybrán žádný prostor"</string>
<string name="screen_create_room_space_selection_sheet_title">"Přidat do prostoru"</string>
<string name="screen_create_room_topic_label">"Téma (nepovinné)"</string>
<string name="screen_create_room_topic_placeholder">"Přidat popis…"</string>

View file

@ -6,11 +6,9 @@
<string name="screen_create_room_private_option_description">"Dim ond pobl wahoddwyd all gael mynediad i\'r ystafell hon. Mae pob neges wedi\'i hamgryptio o\'r dechrau i\'r diwedd."</string>
<string name="screen_create_room_public_option_description">"Gall unrhyw un ddod o hyd i\'r ystafell hon.
Gallwch newid hyn unrhyw bryd yng ngosodiadau ystafell."</string>
<string name="screen_create_room_public_option_title">"Ystafell gyhoeddus"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Gall unrhyw un ofyn am gael ymuno â\'r ystafell ond bydd rhaid i weinyddwr neu gymedrolwr dderbyn y cais"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Gofyn i gael ymuno"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Gall unrhyw un ymuno â\'r ystafell hon"</string>
<string name="screen_create_room_room_access_section_public_option_title">"Unrhyw un"</string>
<string name="screen_create_room_room_address_section_footer">"Er mwyn i\'r ystafell hon fod yn weladwy yn y cyfeiriadur ystafelloedd cyhoeddus, bydd angen cyfeiriad ystafell arnoch."</string>
<string name="screen_create_room_room_address_section_title">"Cyfeiriad yr ystafell"</string>
<string name="screen_create_room_room_visibility_section_title">"Gwelededd yr ystafell"</string>

View file

@ -8,19 +8,15 @@
<string name="screen_create_room_new_room_title">"Nyt rum"</string>
<string name="screen_create_room_new_space_title">"Ny gruppe"</string>
<string name="screen_create_room_private_option_description">"Kun inviterede personer kan deltage."</string>
<string name="screen_create_room_private_option_title">"Privat"</string>
<string name="screen_create_room_public_option_description">"Alle kan finde dette rum.
Du kan ændre dette når som helst i rummets indstillinger."</string>
<string name="screen_create_room_public_option_short_description">"Alle kan deltage."</string>
<string name="screen_create_room_public_option_title">"Offentlig"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Alle kan bede om at deltage i rummet, men en administrator eller en moderator skal acceptere anmodningen"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Tillad at man kan anmode om deltagelse"</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"Enhver i %1$s kan deltage, men alle andre skal anmode om adgang."</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"Anmod om at deltage"</string>
<string name="screen_create_room_room_access_section_private_option_description">"Kun inviterede brugere kan deltage."</string>
<string name="screen_create_room_room_access_section_private_option_title">"Privat"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Alle kan deltage i dette rum"</string>
<string name="screen_create_room_room_access_section_public_option_title">"Offentlig"</string>
<string name="screen_create_room_room_access_section_restricted_option_description">"Alle i %1$s kan deltage."</string>
<string name="screen_create_room_room_access_section_restricted_option_title">"Standard"</string>
<string name="screen_create_room_room_access_section_title">"Hvem har adgang"</string>

View file

@ -8,19 +8,15 @@
<string name="screen_create_room_new_room_title">"Neuer Chat"</string>
<string name="screen_create_room_new_space_title">"Neuer Space"</string>
<string name="screen_create_room_private_option_description">"Nur eingeladene Personen haben Zutritt zu diesem Chat."</string>
<string name="screen_create_room_private_option_title">"Privat"</string>
<string name="screen_create_room_public_option_description">"Jeder kann diesen Chat finden.
Du kannst dies jederzeit in den Einstellungen des Chats ändern."</string>
<string name="screen_create_room_public_option_short_description">"Jeder kann beitreten."</string>
<string name="screen_create_room_public_option_title">"Öffentlicher Chatroom"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Jeder kann den Beitritt zum Chat erbitten, aber ein Admin oder Moderator muss die Anfrage akzeptieren."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Anfrage zum Beitritt zulassen"</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"Jeder in %1$s kann beitreten, aber alle anderen müssen den Beitritt anfragen."</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"Beitritt anfragen"</string>
<string name="screen_create_room_room_access_section_private_option_description">"Nur eingeladene Personen können beitreten."</string>
<string name="screen_create_room_room_access_section_private_option_title">"Privat"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Jeder darf diesem Chat beitreten."</string>
<string name="screen_create_room_room_access_section_public_option_title">"Jeder"</string>
<string name="screen_create_room_room_access_section_restricted_option_description">"Jeder in %1$s kann beitreten."</string>
<string name="screen_create_room_room_access_section_restricted_option_title">"Standard"</string>
<string name="screen_create_room_room_access_section_title">"Wer hat Zugang"</string>

View file

@ -6,11 +6,9 @@
<string name="screen_create_room_private_option_description">"Μόνο τα άτομα που έχουν προσκληθεί μπορούν να έχουν πρόσβαση σε αυτή την αίθουσα. Όλα τα μηνύματα είναι κρυπτογραφημένα από άκρο σε άκρο."</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_access_section_knocking_option_description">"Οποιοσδήποτε μπορεί να ζητήσει να συμμετάσχει στην αίθουσα, αλλά ένας διαχειριστής ή ένας συντονιστής θα πρέπει να αποδεχτεί το αίτημα"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Αίτημα συμμετοχής"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Οποιοσδήποτε μπορεί να συμμετάσχει σε αυτή την αίθουσα"</string>
<string name="screen_create_room_room_access_section_public_option_title">"Οποιοσδήποτε"</string>
<string name="screen_create_room_room_address_section_footer">"Για να είναι ορατή αυτή η αίθουσα στον δημόσιο κατάλογο αιθουσών, θα χρειαστείτε μια διεύθυνση αίθουσας."</string>
<string name="screen_create_room_room_address_section_title">"Διεύθυνση δωματίου"</string>
<string name="screen_create_room_room_visibility_section_title">"Ορατότητα αίθουσας"</string>

View file

@ -6,11 +6,9 @@
<string name="screen_create_room_private_option_description">"Solo las personas invitadas pueden acceder a esta sala. Todos los mensajes están cifrados de extremo a extremo."</string>
<string name="screen_create_room_public_option_description">"Cualquiera puede encontrar esta sala.
Puedes cambiar esto en cualquier momento en los ajustes de la sala."</string>
<string name="screen_create_room_public_option_title">"Sala pública (cualquiera)"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Cualquiera puede solicitar unirse a la sala, pero un administrador o un moderador tendrá que aceptar la solicitud"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Solicitud para unirse"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Cualquiera puede unirse a esta sala"</string>
<string name="screen_create_room_room_access_section_public_option_title">"Cualquiera"</string>
<string name="screen_create_room_room_address_section_footer">"Para que esta sala sea visible en el directorio de salas públicas, necesitarás una dirección de sala."</string>
<string name="screen_create_room_room_visibility_section_title">"Visibilidad de la sala"</string>
<string name="screen_create_room_topic_label">"Tema (opcional)"</string>

View file

@ -12,7 +12,7 @@
<string name="screen_create_room_public_option_description">"Kõik saavad seda jututuba leida.
Sa võid seda jututoa seadistustest alati muuta."</string>
<string name="screen_create_room_public_option_short_description">"Kõik võivad selle jututoaga liituda."</string>
<string name="screen_create_room_public_option_title">"Avalik jututuba"</string>
<string name="screen_create_room_public_option_title">"Avalik"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Kõik võivad paluda selle jututoaga liitumist, kuid peakasutaja või moderaator peavad selle kinnitama."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Luba küsida liitumisvõimalust"</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"Kõik „%1$s“ kogukonna liikmed võivad liituda, kuid kõik teised peavad liitumiseks küsima luba."</string>
@ -20,7 +20,7 @@ Sa võid seda jututoa seadistustest alati muuta."</string>
<string name="screen_create_room_room_access_section_private_option_description">"Ligipääs siia jututuppa on vaid kutse alusel."</string>
<string name="screen_create_room_room_access_section_private_option_title">"Privaatne"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Kõik võivad selle jututoaga liituda."</string>
<string name="screen_create_room_room_access_section_public_option_title">"Kõik kasutajad"</string>
<string name="screen_create_room_room_access_section_public_option_title">"Avalik"</string>
<string name="screen_create_room_room_access_section_restricted_option_description">"Liituda võivad kõik „%1$s“ kogukonna liikmed."</string>
<string name="screen_create_room_room_access_section_restricted_option_title">"Standardne"</string>
<string name="screen_create_room_room_access_section_title">"Kellel on ligipääs"</string>
@ -28,7 +28,8 @@ Sa võid seda jututoa seadistustest alati muuta."</string>
<string name="screen_create_room_room_address_section_title">"Aadress"</string>
<string name="screen_create_room_room_visibility_section_title">"Jututoa nähtavus"</string>
<string name="screen_create_room_space_selection_no_space_description">"(kogukonda pole)"</string>
<string name="screen_create_room_space_selection_no_space_title">"Avaleht"</string>
<string name="screen_create_room_space_selection_no_space_option">"Ära lisa kogukonda"</string>
<string name="screen_create_room_space_selection_no_space_title">"Ühtegi kogukonda pole valitud"</string>
<string name="screen_create_room_space_selection_sheet_title">"Lisa kogukonda"</string>
<string name="screen_create_room_topic_label">"Teema (kui soovid lisada)"</string>
<string name="screen_create_room_topic_placeholder">"Lisa kirjeldus…"</string>

View file

@ -6,9 +6,7 @@
<string name="screen_create_room_private_option_description">"Gonbidatutako jendea soilik sar daiteke gelara. Mezu guztiak daude ertzetik ertzera zifratuta."</string>
<string name="screen_create_room_public_option_description">"Edonork aurki dezake gela hau.
Gelaren ezarpenetan aldatu dezakezu hobespena."</string>
<string name="screen_create_room_public_option_title">"Gela publikoa"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Edonor sar daiteke gela honetara"</string>
<string name="screen_create_room_room_access_section_public_option_title">"Edonork"</string>
<string name="screen_create_room_room_address_section_title">"Gelaren helbidea"</string>
<string name="screen_create_room_room_visibility_section_title">"Gelaren ikusgarritasuna"</string>
<string name="screen_create_room_topic_label">"Mintzagaia (aukerakoa)"</string>

View file

@ -4,13 +4,10 @@
<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_access_section_knocking_option_title">"درخواست دعوت"</string>
<string name="screen_create_room_room_access_section_public_option_description">"هرکسی می‌تواند به این اتاق بپیوندد"</string>
<string name="screen_create_room_room_access_section_public_option_title">"هرکسی"</string>
<string name="screen_create_room_room_address_section_title">"نشانی اتاق"</string>
<string name="screen_create_room_room_visibility_section_title">"نمایانی اتاق"</string>
<string name="screen_create_room_topic_label">"موضوع (اختیاری)"</string>

View file

@ -6,11 +6,9 @@
<string name="screen_create_room_private_option_description">"Vain kutsutut henkilöt pääsevät tähän huoneeseen. Kaikki viestit ovat päästä päähän salattuja."</string>
<string name="screen_create_room_public_option_description">"Kuka tahansa voi löytää tämän huoneen.
Voit muuttaa tämän milloin tahansa huoneen asetuksista."</string>
<string name="screen_create_room_public_option_title">"Julkinen huone"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Kuka tahansa voi pyytää saada liittyä huoneeseen, mutta ylläpitäjän tai valvojan on hyväksyttävä pyyntö"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Pyydä liittymistä"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Kuka tahansa voi liittyä tähän huoneeseen"</string>
<string name="screen_create_room_room_access_section_public_option_title">"Kuka tahansa"</string>
<string name="screen_create_room_room_address_section_footer">"Jotta tämä huone näkyisi julkisessa huonehakemistossa, tarvitset huoneen osoitteen."</string>
<string name="screen_create_room_room_address_section_title">"Huoneen osoite"</string>
<string name="screen_create_room_room_visibility_section_title">"Huoneen näkyvyys"</string>

View file

@ -12,7 +12,7 @@
<string name="screen_create_room_public_option_description">"Nimporte qui peut trouver ce salon.
Vous pouvez modifier cela à tout moment dans les paramètres du salon."</string>
<string name="screen_create_room_public_option_short_description">"Tout le monde peut joindre"</string>
<string name="screen_create_room_public_option_title">"Salon public"</string>
<string name="screen_create_room_public_option_title">"Public"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Tout le monde peut demander à joindre, mais un administrateur ou un modérateur devra accepter la demande"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Autoriser la demande à joindre"</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"Tout membre de %1$s peut joindre, mais les autres doivent demander un accès."</string>

View file

@ -9,7 +9,6 @@ To možete u svakom trenutku promijeniti u postavkama sobe."</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Svatko može zatražiti pridruživanje sobi, ali administrator ili moderator morat će prihvatiti zahtjev."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Zatraži pridruživanje"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Svatko se može pridružiti ovoj sobi"</string>
<string name="screen_create_room_room_access_section_public_option_title">"Svatko"</string>
<string name="screen_create_room_room_address_section_footer">"Da bi ova soba bila vidljiva u javnom direktoriju soba, trebat će vam adresa sobe."</string>
<string name="screen_create_room_room_address_section_title">"Adresa sobe"</string>
<string name="screen_create_room_room_visibility_section_title">"Vidljivost sobe"</string>

View file

@ -8,19 +8,15 @@
<string name="screen_create_room_new_room_title">"Új szoba"</string>
<string name="screen_create_room_new_space_title">"Új tér"</string>
<string name="screen_create_room_private_option_description">"Csak a meghívottak léphetnek be."</string>
<string name="screen_create_room_private_option_title">"Privát"</string>
<string name="screen_create_room_public_option_description">"Bárki megtalálhatja ezt a szobát.
Ezt bármikor módosíthatja a szobabeállításokban."</string>
<string name="screen_create_room_public_option_short_description">"Bárki csatlakozhat."</string>
<string name="screen_create_room_public_option_title">"Nyilvános szoba"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Bárki kérheti, hogy csatlakozhasson a szobához, de egy adminisztrátornak vagy moderátornak el kell fogadnia a kérést."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Csatlakozás kérése"</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"Bárki csatlakozhat innen: %1$s, de mindenki másnak hozzáférést kell kérnie."</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"Csatlakozás kérése"</string>
<string name="screen_create_room_room_access_section_private_option_description">"Csak a meghívottak léphetnek be."</string>
<string name="screen_create_room_room_access_section_private_option_title">"Privát"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Bárki csatlakozhat."</string>
<string name="screen_create_room_room_access_section_public_option_title">"Nyilvános"</string>
<string name="screen_create_room_room_access_section_restricted_option_description">"Bárki csatlakozhat innen: %1$s."</string>
<string name="screen_create_room_room_access_section_restricted_option_title">"Szokásos"</string>
<string name="screen_create_room_room_access_section_title">"Hozzáférésre jogosultak"</string>

View file

@ -6,11 +6,9 @@
<string name="screen_create_room_private_option_description">"Hanya orang-orang yang diundang dapat mengakses ruangan ini. Semua pesan terenkripsi secara ujung ke ujung."</string>
<string name="screen_create_room_public_option_description">"Siapa pun dapat mencari ruangan ini.
Anda dapat mengubah ini kapan pun dalam pengaturan ruangan."</string>
<string name="screen_create_room_public_option_title">"Ruangan publik"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Siapa pun dapat meminta untuk bergabung dengan ruangan tetapi administrator atau moderator harus menerima permintaan tersebut"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Minta untuk bergabung"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Siapa pun dapat bergabung dengan ruangan ini"</string>
<string name="screen_create_room_room_access_section_public_option_title">"Siapa pun"</string>
<string name="screen_create_room_room_address_section_footer">"Supaya ruangan ini terlihat di direktori ruangan publik, Anda memerlukan alamat ruangan."</string>
<string name="screen_create_room_room_address_section_title">"Alamat ruangan"</string>
<string name="screen_create_room_room_visibility_section_title">"Keterlihatan ruangan"</string>

View file

@ -6,11 +6,9 @@
<string name="screen_create_room_private_option_description">"Solo le persone invitate possono accedere a questa stanza. Tutti i messaggi sono cifrati end-to-end."</string>
<string name="screen_create_room_public_option_description">"Chiunque può trovare questa stanza.
Puoi modificarlo in qualsiasi momento nelle impostazioni della stanza."</string>
<string name="screen_create_room_public_option_title">"Stanza pubblica"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Chiunque può chiedere di entrare nella stanza, ma un amministratore o un moderatore dovrà accettare la richiesta"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Chiedi di entrare"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Chiunque può entrare in questa stanza"</string>
<string name="screen_create_room_room_access_section_public_option_title">"Chiunque"</string>
<string name="screen_create_room_room_address_section_footer">"Affinché questa stanza sia visibile nell\'elenco delle stanze pubbliche, è necessario un indirizzo della stanza."</string>
<string name="screen_create_room_room_address_section_title">"Indirizzo della stanza"</string>
<string name="screen_create_room_room_visibility_section_title">"Visibilità della stanza"</string>

View file

@ -6,6 +6,5 @@
<string name="screen_create_room_private_option_description">"ამ ოთახში შეტყობინებები დაშიფრულია. შემდგომ დაშიფვრის გამორთვა შეუძლებელია."</string>
<string name="screen_create_room_public_option_description">"ყველას ამ ოთახის მოძებნა შეუძლია.
თქვენ ნებისმიერ დროს შეგიძლიათ ამის შეცვლა ოთახის პარამეტრებში."</string>
<string name="screen_create_room_public_option_title">"საჯარო ოთახი"</string>
<string name="screen_create_room_topic_label">"თემა (სურვილისამებრ)"</string>
</resources>

View file

@ -6,11 +6,9 @@
<string name="screen_create_room_private_option_description">"초대받은 사람만 이 방에 액세스할 수 있습니다. 모든 메시지는 종단 간 암호화됩니다."</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_access_section_knocking_option_description">"누구나 방에 참여 요청을 할 수 있지만, 관리자나 운영자가 요청을 수락해야 합니다."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"참가 요청"</string>
<string name="screen_create_room_room_access_section_public_option_description">"누구나 이 방에 참여할 수 있습니다."</string>
<string name="screen_create_room_room_access_section_public_option_title">"누구나"</string>
<string name="screen_create_room_room_address_section_footer">"이 방이 공개 방 디렉토리에 표시되려면 방 주소가 필요합니다."</string>
<string name="screen_create_room_room_visibility_section_title">"방 표시 여부"</string>
<string name="screen_create_room_topic_label">"주제 (선택)"</string>

View file

@ -7,17 +7,13 @@
<string name="screen_create_room_new_room_title">"Nytt rom"</string>
<string name="screen_create_room_new_space_title">"Nytt område"</string>
<string name="screen_create_room_private_option_description">"Bare inviterte personer kan bli med."</string>
<string name="screen_create_room_private_option_title">"Privat"</string>
<string name="screen_create_room_public_option_description">"Alle kan finne dette rommet.
Du kan endre dette når som helst i rominnstillingene."</string>
<string name="screen_create_room_public_option_short_description">"Alle kan bli med."</string>
<string name="screen_create_room_public_option_title">"Offentlig rom"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Alle kan be om å få bli med, men en administrator eller moderator må godta forespørselen."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Be om å bli med"</string>
<string name="screen_create_room_room_access_section_private_option_description">"Bare inviterte personer kan bli med."</string>
<string name="screen_create_room_room_access_section_private_option_title">"Privat"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Alle kan bli med."</string>
<string name="screen_create_room_room_access_section_public_option_title">"Alle"</string>
<string name="screen_create_room_room_access_section_title">"Hvem har tilgang"</string>
<string name="screen_create_room_room_address_section_footer">"Du trenger en adresse for å gjøre den synlig i den offentlige katalogen."</string>
<string name="screen_create_room_room_address_section_title">"Adresse"</string>

View file

@ -6,10 +6,8 @@
<string name="screen_create_room_private_option_description">"Alleen uitgenodigde personen hebben toegang tot deze kamer. Alle berichten zijn end-to-end versleuteld."</string>
<string name="screen_create_room_public_option_description">"Iedereen kan deze kamer vinden.
Je kunt dit op elk gewenst moment wijzigen in de kamerinstellingen."</string>
<string name="screen_create_room_public_option_title">"Openbare kamer"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Iedereen kan vragen om toe te treden tot de kamer, maar een beheerder of moderator moet het verzoek accepteren"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Vraag om toe te treden"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Iedereen kan toetreden tot deze kamer"</string>
<string name="screen_create_room_room_access_section_public_option_title">"Iedereen"</string>
<string name="screen_create_room_topic_label">"Onderwerp (optioneel)"</string>
</resources>

View file

@ -6,11 +6,9 @@
<string name="screen_create_room_private_option_description">"Tylko zaproszone osoby mogą dołączyć do tego pokoju. Wszystkie wiadomości są szyfrowane end-to-end."</string>
<string name="screen_create_room_public_option_description">"Każdy może znaleźć ten pokój.
Możesz to zmienić w ustawieniach pokoju."</string>
<string name="screen_create_room_public_option_title">"Pokój publiczny"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Każdy może poprosić o dołączenie do pokoju, ale administrator lub moderator będzie musiał zatwierdzić prośbę"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Poproś o dołączenie"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Każdy może dołączyć do tego pokoju"</string>
<string name="screen_create_room_room_access_section_public_option_title">"Wszyscy"</string>
<string name="screen_create_room_room_address_section_footer">"Aby ten pokój był widoczny w katalogu pomieszczeń publicznych, będziesz potrzebował adres pokoju."</string>
<string name="screen_create_room_room_address_section_title">"Adres pokoju"</string>
<string name="screen_create_room_room_visibility_section_title">"Widoczność pomieszczenia"</string>

View file

@ -8,19 +8,15 @@
<string name="screen_create_room_new_room_title">"Nova sala"</string>
<string name="screen_create_room_new_space_title">"Novo espaço"</string>
<string name="screen_create_room_private_option_description">"Apenas pessoas convidadas podem entrar."</string>
<string name="screen_create_room_private_option_title">"Privada"</string>
<string name="screen_create_room_public_option_description">"Qualquer um pode encontrar esta sala.
Você pode mudar isso a qualquer momento nas configurações da sala."</string>
<string name="screen_create_room_public_option_short_description">"Qualquer um pode entrar."</string>
<string name="screen_create_room_public_option_title">"Publica"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Qualquer um pode pedir para entrar, mas um administrador ou moderador deve aceitar a solicitação"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Permitir pedir para entrar"</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"Qualquer um em %1$s pode entrar, mas todos os outros devem pedir acesso."</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"Pedir para entrar"</string>
<string name="screen_create_room_room_access_section_private_option_description">"Apenas pessoas convidadas podem entrar."</string>
<string name="screen_create_room_room_access_section_private_option_title">"Privada"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Qualquer um pode entrar."</string>
<string name="screen_create_room_room_access_section_public_option_title">"Qualquer pessoa"</string>
<string name="screen_create_room_room_access_section_restricted_option_description">"Qualquer um em %1$s pode participar."</string>
<string name="screen_create_room_room_access_section_restricted_option_title">"Normal"</string>
<string name="screen_create_room_room_access_section_title">"Quem tem acesso"</string>

View file

@ -4,14 +4,11 @@
<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">"Apenas as pessoas convidadas podem entrar."</string>
<string name="screen_create_room_private_option_title">"Privada"</string>
<string name="screen_create_room_public_option_description">"Qualquer um pode encontrar esta sala.
Pode alterar esta opção nas definições da sala."</string>
<string name="screen_create_room_public_option_title">"Sala pública"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Qualquer pessoa pode pedir para entrar, mas um administrador ou um moderador terá de aceitar o pedido."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Pedir para participar"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Qualquer pessoa pode entrar."</string>
<string name="screen_create_room_room_access_section_public_option_title">"Pública"</string>
<string name="screen_create_room_room_address_section_footer">"Para que esta sala seja visível no diretório público, precisa de ter um endereço."</string>
<string name="screen_create_room_room_address_section_title">"Endereço"</string>
<string name="screen_create_room_room_visibility_section_title">"Visibilidade da sala"</string>

View file

@ -6,11 +6,9 @@
<string name="screen_create_room_private_option_description">"Doar persoanele invitate pot accesa această cameră. Toate mesajele sunt criptate end-to-end."</string>
<string name="screen_create_room_public_option_description">"Oricine poate găsi această cameră.
Puteți modifica acest lucru oricând în setări."</string>
<string name="screen_create_room_public_option_title">"Cameră publică"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Oricine poate cere să se alăture camerei, dar un administrator sau un moderator va trebui să accepte cererea"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Cereți să vă alăturați"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Oricine se poate alătura acestei camere"</string>
<string name="screen_create_room_room_access_section_public_option_title">"Oricine"</string>
<string name="screen_create_room_room_address_section_footer">"Pentru ca această cameră să fie vizibilă în directorul de camere publice, veți avea nevoie de o adresă de cameră."</string>
<string name="screen_create_room_room_address_section_title">"Adresa camerei"</string>
<string name="screen_create_room_room_visibility_section_title">"Vizibilitatea camerei"</string>

View file

@ -8,17 +8,13 @@
<string name="screen_create_room_new_room_title">"Новая комната"</string>
<string name="screen_create_room_new_space_title">"Новое пространство"</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_short_description">"Присоединиться может любой."</string>
<string name="screen_create_room_public_option_title">"Общедоступная комната"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Любой желающий может подать заявку на присоединение к комнате, но администратор или модератор должен будет принять запрос."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Разрешить запрос на присоединение"</string>
<string name="screen_create_room_room_access_section_private_option_description">"Присоединиться могут только приглашенные."</string>
<string name="screen_create_room_room_access_section_private_option_title">"Частный"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Присоединиться может любой желающий."</string>
<string name="screen_create_room_room_access_section_public_option_title">"Публичный"</string>
<string name="screen_create_room_room_access_section_title">"Кто имеет доступ"</string>
<string name="screen_create_room_room_address_section_footer">"Вам понадобится адрес комнаты, чтобы сделать ее видимой в каталоге."</string>
<string name="screen_create_room_room_address_section_title">"Адрес"</string>

View file

@ -8,19 +8,15 @@
<string name="screen_create_room_new_room_title">"Nová miestnosť"</string>
<string name="screen_create_room_new_space_title">"Nový priestor"</string>
<string name="screen_create_room_private_option_description">"Pripojiť sa môžu iba pozvaní ľudia."</string>
<string name="screen_create_room_private_option_title">"Súkromná"</string>
<string name="screen_create_room_public_option_description">"Túto miestnosť môže nájsť ktokoľvek.
Môžete to kedykoľvek zmeniť v nastaveniach miestnosti."</string>
<string name="screen_create_room_public_option_short_description">"Pripojiť sa môže ktokoľvek."</string>
<string name="screen_create_room_public_option_title">"Verejná miestnosť"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"O pripojenie sa môže požiadať ktokoľvek, ale žiadosť musí schváliť správca alebo moderátor."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Povoliť požiadať o vstup"</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"Ktokoľvek v %1$s sa môžu pripojiť, ale všetci ostatní musia požiadať o prístup."</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"Požiadať o pripojenie"</string>
<string name="screen_create_room_room_access_section_private_option_description">"Pripojiť sa môžu iba pozvaní ľudia."</string>
<string name="screen_create_room_room_access_section_private_option_title">"Súkromná"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Pripojiť sa môže ktokoľvek."</string>
<string name="screen_create_room_room_access_section_public_option_title">"Ktokoľvek"</string>
<string name="screen_create_room_room_access_section_restricted_option_description">"Ktokoľvek v %1$s sa môže pripojiť."</string>
<string name="screen_create_room_room_access_section_restricted_option_title">"Štandardná"</string>
<string name="screen_create_room_room_access_section_title">"Kto má prístup"</string>

View file

@ -6,11 +6,9 @@
<string name="screen_create_room_private_option_description">"Endast inbjudna personer kan gå med."</string>
<string name="screen_create_room_public_option_description">"Vem som helst kan hitta det här rummet.
Du kan ändra detta när som helst i rumsinställningarna."</string>
<string name="screen_create_room_public_option_title">"Offentligt rum"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Vem som helst kan be om att gå med men en administratör eller en moderator måste acceptera begäran"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Tillåt att be om att gå med"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Vem som helst kan gå med."</string>
<string name="screen_create_room_room_access_section_public_option_title">"Offentligt"</string>
<string name="screen_create_room_room_address_section_footer">"Du behöver en adress för att den ska synas i den offentliga katalogen."</string>
<string name="screen_create_room_room_address_section_title">"Adress"</string>
<string name="screen_create_room_room_visibility_section_title">"Rumssynlighet"</string>

View file

@ -6,11 +6,9 @@
<string name="screen_create_room_private_option_description">"Bu odaya yalnızca davet edilen kişiler erişebilir. Tüm mesajlar uçtan uca şifrelenir."</string>
<string name="screen_create_room_public_option_description">"Bu odayı herkes bulabilir.
Bunu istediğiniz zaman oda ayarlarından değiştirebilirsiniz."</string>
<string name="screen_create_room_public_option_title">"Herkese açık oda"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Herkes odaya katılmayı isteyebilir ancak bir yönetici veya moderatörün isteği kabul etmesi gerekecektir"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Katılmak için sor"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Bu odaya herkes katılabilir"</string>
<string name="screen_create_room_room_access_section_public_option_title">"Herkes"</string>
<string name="screen_create_room_room_address_section_footer">"Bu odanın genel oda dizininde görünür olması için bir oda adresine ihtiyacınız olacaktır."</string>
<string name="screen_create_room_room_address_section_title">"Oda adresi"</string>
<string name="screen_create_room_room_visibility_section_title">"Oda görünürlüğü"</string>

View file

@ -6,11 +6,9 @@
<string name="screen_create_room_private_option_description">"Лише запрошені люди мають доступ до цієї кімнати. Усі повідомлення захищені наскрізним шифруванням."</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_access_section_knocking_option_description">"Будь-хто може попросити приєднатися до кімнати, але адміністратор або модератор повинен буде прийняти запит"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Запросити приєднатися"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Будь-хто може приєднатися до цієї кімнати"</string>
<string name="screen_create_room_room_access_section_public_option_title">"Кожний"</string>
<string name="screen_create_room_room_address_section_footer">"Щоб цю кімнату було видно в каталозі загальнодоступних кімнат, вам знадобиться її адреса."</string>
<string name="screen_create_room_room_address_section_title">"Адреса кімнати"</string>
<string name="screen_create_room_room_visibility_section_title">"Видимість кімнати"</string>

View file

@ -6,11 +6,9 @@
<string name="screen_create_room_private_option_description">"Faqat taklif etilgan shaxslargina bu xonaga kira oladi. Barcha xabarlar boshdan-oxirigacha shifrlanadi."</string>
<string name="screen_create_room_public_option_description">"Bu xonani har kim topishi mumkin.
Buni xona sozlamalaridan istalgan vaqtda oʻzgartirishingiz mumkin."</string>
<string name="screen_create_room_public_option_title">"Jamoat xonasi (har kim)"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Xonaga qoshilishni istalgan kishi sorashi mumkin, lekin administrator yoki moderator sorovni qabul qilishi kerak"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Qoshilishni sorang"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Bu xonaga istalgan kishi qoshilishi mumkin"</string>
<string name="screen_create_room_room_access_section_public_option_title">"Har kim"</string>
<string name="screen_create_room_room_address_section_footer">"Ushbu xona ommaviy xonalar royxatida korinishi uchun sizga xona manzili kerak boladi."</string>
<string name="screen_create_room_room_visibility_section_title">"Xonaning korinishi"</string>
<string name="screen_create_room_topic_label">"Mavzu (ixtiyoriy)"</string>

View file

@ -6,11 +6,9 @@
<string name="screen_create_room_private_option_description">"僅被邀請的人才能存取此聊天室。所有訊息均會端到端加密。"</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_access_section_knocking_option_description">"任何人都可以要求加入聊天室,但管理員或版主必須接受該請求"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"要求加入"</string>
<string name="screen_create_room_room_access_section_public_option_description">"任何人都可以加入此聊天室"</string>
<string name="screen_create_room_room_access_section_public_option_title">"任何人"</string>
<string name="screen_create_room_room_address_section_footer">"為了讓此聊天室在公開聊天室目錄中可見,您需要聊天室地址。"</string>
<string name="screen_create_room_room_address_section_title">"聊天室地址"</string>
<string name="screen_create_room_room_visibility_section_title">"聊天室能見度"</string>

View file

@ -6,11 +6,9 @@
<string name="screen_create_room_private_option_description">"只有受邀用户才能访问此聊天室。所有消息均经过端到端加密。"</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_access_section_knocking_option_description">"任何人都可以请求加入房间,但必须由管理员或审核人接受"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"请求加入"</string>
<string name="screen_create_room_room_access_section_public_option_description">"任何人都可以加入此房间"</string>
<string name="screen_create_room_room_access_section_public_option_title">"任何人"</string>
<string name="screen_create_room_room_address_section_footer">"要使该房间在公开房间目录中可见,您需要一个房间地址。"</string>
<string name="screen_create_room_room_address_section_title">"房间地址"</string>
<string name="screen_create_room_room_visibility_section_title">"房间可见性"</string>

View file

@ -35,6 +35,11 @@ interface EnterpriseService {
fun bugReportUrlFlow(sessionId: SessionId?): Flow<BugReportUrl>
/**
* Gets Notification Channel to use for the noisy notifications of the provided session.
*/
fun getNoisyNotificationChannelId(sessionId: SessionId): String?
companion object {
const val ANY_ACCOUNT_PROVIDER = "*"
}

View file

@ -43,4 +43,6 @@ class DefaultEnterpriseService : EnterpriseService {
override fun bugReportUrlFlow(sessionId: SessionId?): Flow<BugReportUrl> {
return flowOf(BugReportUrl.UseDefault)
}
override fun getNoisyNotificationChannelId(sessionId: SessionId): String? = null
}

View file

@ -98,4 +98,10 @@ class DefaultEnterpriseServiceTest {
awaitComplete()
}
}
@Test
fun `getNoisyNotificationChannelId returns null`() = runTest {
val defaultEnterpriseService = DefaultEnterpriseService()
assertThat(defaultEnterpriseService.getNoisyNotificationChannelId(A_SESSION_ID)).isNull()
}
}

View file

@ -29,6 +29,7 @@ class FakeEnterpriseService(
private val overrideBrandColorResult: (SessionId?, String?) -> Unit = { _, _ -> lambdaError() },
private val firebasePushGatewayResult: () -> String? = { lambdaError() },
private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() },
private val getNoisyNotificationChannelIdResult: (SessionId?) -> String? = { lambdaError() },
) : EnterpriseService {
private val brandColorState = MutableStateFlow(initialBrandColor)
private val semanticColorsState = MutableStateFlow(initialSemanticColors)
@ -69,4 +70,8 @@ class FakeEnterpriseService(
override fun bugReportUrlFlow(sessionId: SessionId?): Flow<BugReportUrl> {
return bugReportUrlMutableFlow.asStateFlow()
}
override fun getNoisyNotificationChannelId(sessionId: SessionId): String? {
return getNoisyNotificationChannelIdResult(sessionId)
}
}

View file

@ -3,6 +3,8 @@
<string name="banner_battery_optimization_content_android">"Desativa as otimizações de bateria para esta aplicação, de modo a garantir que todas as notificações chegam."</string>
<string name="banner_battery_optimization_submit_android">"Desativar otimizações"</string>
<string name="banner_battery_optimization_title_android">"As notificações não chegam?"</string>
<string name="banner_new_sound_message">"O toque de notificação foi atualizado — mais claro, mais rápido e menos perturbador."</string>
<string name="banner_new_sound_title">"Atualizámos os seus sons"</string>
<string name="banner_set_up_recovery_content">"Recupera a tua identidade criptográfica e o histórico de mensagens com uma chave de recuperação se tiveres perdido todos os teus dispositivos existentes."</string>
<string name="banner_set_up_recovery_submit">"Configurar recuperação"</string>
<string name="banner_set_up_recovery_title">"Configurar a recuperação"</string>

View file

@ -1,3 +1,4 @@
import extension.buildConfigFieldStr
import extension.setupDependencyInjection
import extension.testCommonDependencies
@ -23,6 +24,30 @@ android {
isIncludeAndroidResources = true
}
}
buildFeatures {
buildConfig = true
}
buildTypes {
val elementClassicPackageKey = "elementClassicPackage"
val elementClassicPackage = "im.vector.app"
val elementClassicPackageDebug = "$elementClassicPackage.debug"
val elementClassicPackageNightly = "$elementClassicPackage.nightly"
getByName("release") {
manifestPlaceholders[elementClassicPackageKey] = elementClassicPackage
buildConfigFieldStr(elementClassicPackageKey, elementClassicPackage)
}
getByName("debug") {
manifestPlaceholders[elementClassicPackageKey] = elementClassicPackageDebug
buildConfigFieldStr(elementClassicPackageKey, elementClassicPackageDebug)
}
register("nightly") {
matchingFallbacks += listOf("release")
manifestPlaceholders[elementClassicPackageKey] = elementClassicPackageNightly
buildConfigFieldStr(elementClassicPackageKey, elementClassicPackageNightly)
}
}
}
setupDependencyInjection()

View file

@ -13,8 +13,9 @@
<intent>
<action android:name="android.support.customtabs.action.CustomTabsService" />
</intent>
<!-- To be able to start the service exported by Element Classic -->
<package android:name="${elementClassicPackage}" />
</queries>
<!-- Permission to read data from Element classic -->
<uses-permission android:name="im.vector.app.READ_DATA" />
</manifest>

View file

@ -20,9 +20,8 @@ import android.os.Messenger
import android.os.RemoteException
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.login.impl.BuildConfig
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.matrix.api.core.UserId
@ -44,7 +43,11 @@ sealed interface ElementClassicConnectionState {
object Idle : ElementClassicConnectionState
object ElementClassicNotFound : ElementClassicConnectionState
object ElementClassicReadyNoSession : ElementClassicConnectionState
data class ElementClassicReady(val userId: UserId) : ElementClassicConnectionState
data class ElementClassicReady(
val userId: UserId,
val secrets: String,
) : ElementClassicConnectionState
data class Error(val error: String) : ElementClassicConnectionState
}
@ -56,7 +59,6 @@ class DefaultElementClassicConnection(
private val context: Context,
@AppCoroutineScope
private val coroutineScope: CoroutineScope,
private val buildMeta: BuildMeta,
) : ElementClassicConnection {
// Messenger for communicating with the service.
private var messenger: Messenger? = null
@ -101,7 +103,7 @@ class DefaultElementClassicConnection(
// applications replace our component.
try {
val intentService = Intent()
intentService.setComponent(getElementClassicComponent(buildMeta))
intentService.setComponent(getElementClassicComponent())
if (context.bindService(intentService, serviceConnection, BIND_AUTO_CREATE)) {
Timber.tag(loggerTag.value).d("Binding returned true")
} else {
@ -198,17 +200,8 @@ class DefaultElementClassicConnection(
}
}
private fun getElementClassicComponent(buildMeta: BuildMeta) = ComponentName(
buildString {
append(ELEMENT_CLASSIC_APP_ID)
append(
when (buildMeta.buildType) {
BuildType.DEBUG -> ELEMENT_CLASSIC_APP_ID_DEBUG_SUFFIX
BuildType.NIGHTLY -> ELEMENT_CLASSIC_APP_ID_NIGHTLY_SUFFIX
BuildType.RELEASE -> ELEMENT_CLASSIC_APP_ID_RELEASE_SUFFIX
}
)
},
private fun getElementClassicComponent() = ComponentName(
BuildConfig.elementClassicPackage,
ELEMENT_CLASSIC_SERVICE_FULL_CLASS_NAME,
)
@ -220,9 +213,14 @@ class DefaultElementClassicConnection(
if (error != null) {
ElementClassicConnectionState.Error(error)
} else {
val userId = getString(KEY_USER_ID_STR)?.let(::UserId)
val userId = getString(KEY_USER_ID_STR)?.takeIf { it.isNotEmpty() }?.let(::UserId)
if (userId != null) {
ElementClassicConnectionState.ElementClassicReady(userId)
val secrets = getString(KEY_SECRETS_STR)?.takeIf { it.isNotEmpty() }
if (secrets == null) {
ElementClassicConnectionState.Error("No secrets received from Element Classic")
} else {
ElementClassicConnectionState.ElementClassicReady(userId, secrets)
}
} else {
ElementClassicConnectionState.ElementClassicReadyNoSession
}
@ -232,18 +230,31 @@ class DefaultElementClassicConnection(
// Everything in this companion object must match what is defined in Element Classic
private companion object {
const val ELEMENT_CLASSIC_SERVICE_FULL_CLASS_NAME = "im.vector.app.features.importer.ImporterService"
// Command to the service to get the data.
const val MSG_GET_DATA = 1
const val ELEMENT_CLASSIC_APP_ID = "im.vector.app"
const val ELEMENT_CLASSIC_APP_ID_DEBUG_SUFFIX = ".debug"
const val ELEMENT_CLASSIC_APP_ID_NIGHTLY_SUFFIX = ".nightly"
const val ELEMENT_CLASSIC_APP_ID_RELEASE_SUFFIX = ""
const val ELEMENT_CLASSIC_SERVICE_FULL_CLASS_NAME = "im.vector.app.features.importer.ImporterService"
// Keys for the bundle returned from the service
const val KEY_ERROR_STR = "error"
const val KEY_USER_ID_STR = "userId"
/**
* Key to extract the secrets from the bundle, as a Json string.
* Json will have this format:
* {
* "cross_signing" : {
* "master_key" : "z8RUxnaAGu___REDACTED___k+BQL9o",
* "user_signing_key" : "baJHzA___REDACTED___xMLbSUAXw9QUzqms",
* "self_signing_key" : "DU0CvLtR2G/___REDACTED___dV/MONNq4nsQhM"
* },
* "backup" : {
* "algorithm" : "m.megolm_backup.v1.curve25519-aes-sha2",
* "key" : "VzncmQ+UOV___REDACTED___patxDz7m0Nc",
* "backup_version" : "1"
* }
* }
*/
const val KEY_SECRETS_STR = "secrets"
}
}

View file

@ -13,6 +13,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.test.A_SECRET
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
@ -114,7 +115,7 @@ class LoginWithClassicPresenterTest {
presenter.test {
skipItems(2)
elementClassicConnection.emitState(
ElementClassicConnectionState.ElementClassicReady(userId = A_USER_ID)
ElementClassicConnectionState.ElementClassicReady(userId = A_USER_ID, secrets = A_SECRET)
)
val readyState = awaitItem()
assertThat(readyState.canLoginWithClassic).isTrue()
@ -140,7 +141,7 @@ class LoginWithClassicPresenterTest {
presenter.test {
skipItems(2)
elementClassicConnection.emitState(
ElementClassicConnectionState.ElementClassicReady(userId = A_USER_ID)
ElementClassicConnectionState.ElementClassicReady(userId = A_USER_ID, secrets = A_SECRET)
)
val readyState = awaitItem()
assertThat(readyState.canLoginWithClassic).isTrue()
@ -175,7 +176,7 @@ class LoginWithClassicPresenterTest {
presenter.test {
skipItems(2)
elementClassicConnection.emitState(
ElementClassicConnectionState.ElementClassicReady(userId = A_USER_ID)
ElementClassicConnectionState.ElementClassicReady(userId = A_USER_ID, secrets = A_SECRET)
)
// No new item, because canLoginWithClassic is still false
}
@ -192,7 +193,7 @@ class LoginWithClassicPresenterTest {
skipItems(1)
// Note: it should not happen IRL
elementClassicConnection.emitState(
ElementClassicConnectionState.ElementClassicReady(userId = A_USER_ID)
ElementClassicConnectionState.ElementClassicReady(userId = A_USER_ID, secrets = A_SECRET)
)
// No new item, because canLoginWithClassic is still false
}

View file

@ -53,6 +53,7 @@ dependencies {
implementation(projects.libraries.preferences.api)
implementation(projects.libraries.recentemojis.api)
implementation(projects.libraries.roomselect.api)
implementation(projects.libraries.audio.api)
implementation(projects.libraries.voiceplayer.api)
implementation(projects.libraries.voicerecorder.api)
implementation(projects.libraries.mediaplayer.api)
@ -95,6 +96,7 @@ dependencies {
testImplementation(projects.libraries.mediapickers.test)
testImplementation(projects.libraries.permissions.test)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.audio.test)
testImplementation(projects.libraries.voicerecorder.test)
testImplementation(projects.libraries.mediaplayer.test)
testImplementation(projects.libraries.mediaviewer.test)

View file

@ -30,6 +30,8 @@ import io.element.android.features.messages.api.MessageComposerContext
import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerEvent
import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerPresenter
import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerState
import io.element.android.libraries.audio.api.AudioFocus
import io.element.android.libraries.audio.api.AudioFocusRequester
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.timeline.Timeline
@ -58,6 +60,7 @@ class DefaultVoiceMessageComposerPresenter(
@Assisted private val timelineMode: Timeline.Mode,
private val voiceRecorder: VoiceRecorder,
private val analyticsService: AnalyticsService,
private val audioFocus: AudioFocus,
mediaSenderFactory: MediaSenderFactory,
private val player: VoiceMessageComposerPlayer,
private val messageComposerContext: MessageComposerContext,
@ -246,8 +249,14 @@ class DefaultVoiceMessageComposerPresenter(
private fun CoroutineScope.startRecording() = launch {
try {
audioFocus.requestAudioFocus(AudioFocusRequester.RecordVoiceMessage) {
// something else grabbed focus (phone call, etc) - finish gracefully
// so the user keeps their partial recording
sessionCoroutineScope.finishRecording()
}
voiceRecorder.startRecord()
} catch (e: SecurityException) {
audioFocus.releaseAudioFocus()
Timber.e(e, "Voice message error")
analyticsService.trackError(VoiceMessageException.PermissionMissing("Expected permission to record but none", e))
}
@ -255,10 +264,12 @@ class DefaultVoiceMessageComposerPresenter(
private fun CoroutineScope.finishRecording() = launch {
voiceRecorder.stopRecord()
audioFocus.releaseAudioFocus()
}
private fun CoroutineScope.cancelRecording() = launch {
voiceRecorder.stopRecord(cancelled = true)
audioFocus.releaseAudioFocus()
}
private fun CoroutineScope.deleteRecording() = launch {

View file

@ -1238,7 +1238,7 @@ class MessagesPresenterTest {
}
@Test
fun `present - shows a "world_readable" icon if the room is encrypted and history is world_readable`() = runTest {
fun `present - shows a 'world_readable' icon if the room is encrypted and history is world_readable`() = runTest {
val presenter = createMessagesPresenter(
joinedRoom = FakeJoinedRoom(
baseRoom = FakeBaseRoom(

View file

@ -19,12 +19,15 @@ import io.element.android.features.messages.api.timeline.voicemessages.composer.
import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerState
import io.element.android.features.messages.impl.messagecomposer.aReplyMode
import io.element.android.features.messages.test.FakeMessageComposerContext
import io.element.android.libraries.audio.api.AudioFocus
import io.element.android.libraries.audio.api.AudioFocusRequester
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.media.AudioInfo
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.mediaplayer.test.FakeAudioFocus
import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfig
import io.element.android.libraries.mediaupload.impl.DefaultMediaSender
@ -80,6 +83,12 @@ class DefaultVoiceMessageComposerPresenterTest {
timelineMode = Timeline.Mode.Live,
mediaOptimizationConfigProvider = { MediaOptimizationConfig(compressImages = true, videoCompressionPreset = VideoCompressionPreset.STANDARD) },
)
private val requestAudioFocusResult = lambdaRecorder<AudioFocusRequester, () -> Unit, Unit> { _, _ -> }
private val releaseAudioFocusResult = lambdaRecorder<Unit> { }
private val audioFocus: AudioFocus = FakeAudioFocus(
requestAudioFocusResult = requestAudioFocusResult,
releaseAudioFocusResult = releaseAudioFocusResult,
)
private val messageComposerContext = FakeMessageComposerContext()
companion object {
@ -159,6 +168,61 @@ class DefaultVoiceMessageComposerPresenterTest {
}
}
@Test
fun `present - recording requests audio focus and releases on stop`() = runTest {
val presenter = createDefaultVoiceMessageComposerPresenter()
presenter.test {
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
val recordingState = awaitItem()
requestAudioFocusResult.assertions().isCalledOnce()
releaseAudioFocusResult.assertions().isNeverCalled()
recordingState.eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
awaitItem()
releaseAudioFocusResult.assertions().isCalledOnce()
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - cancelling recording releases audio focus`() = runTest {
val presenter = createDefaultVoiceMessageComposerPresenter()
presenter.test {
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Cancel))
awaitItem()
requestAudioFocusResult.assertions().isCalledOnce()
releaseAudioFocusResult.assertions().isCalledOnce()
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - audio focus loss during recording finishes gracefully`() = runTest {
var onFocusLost: (() -> Unit)? = null
val testAudioFocus = FakeAudioFocus(
requestAudioFocusResult = { _, callback -> onFocusLost = callback },
releaseAudioFocusResult = { },
)
val presenter = createDefaultVoiceMessageComposerPresenter(audioFocus = testAudioFocus)
presenter.test {
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
awaitItem()
// simulate focus loss (phone call, etc)
onFocusLost?.invoke()
advanceUntilIdle()
val finalState = awaitItem()
assertThat(finalState.voiceMessageState).isEqualTo(aPreviewState())
voiceRecorder.assertCalls(started = 1, stopped = 1)
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - abort recording`() = runTest {
val presenter = createDefaultVoiceMessageComposerPresenter()
@ -647,12 +711,14 @@ class DefaultVoiceMessageComposerPresenterTest {
private fun TestScope.createDefaultVoiceMessageComposerPresenter(
permissionsPresenter: PermissionsPresenter = createFakePermissionsPresenter(),
voiceRecorder: VoiceRecorder = this@DefaultVoiceMessageComposerPresenterTest.voiceRecorder,
audioFocus: AudioFocus = this@DefaultVoiceMessageComposerPresenterTest.audioFocus,
): DefaultVoiceMessageComposerPresenter {
return DefaultVoiceMessageComposerPresenter(
sessionCoroutineScope = backgroundScope,
timelineMode = Timeline.Mode.Live,
voiceRecorder = voiceRecorder,
analyticsService = analyticsService,
audioFocus = audioFocus,
mediaSenderFactory = { mediaSender },
player = VoiceMessageComposerPlayer(FakeMediaPlayer(), this),
messageComposerContext = messageComposerContext,

View file

@ -17,6 +17,7 @@ android {
dependencies {
api(projects.features.messages.impl)
implementation(projects.libraries.matrix.test)
implementation(projects.libraries.audio.test)
implementation(projects.libraries.mediaplayer.test)
implementation(projects.libraries.mediaupload.test)
implementation(projects.libraries.mediaviewer.api)

View file

@ -13,6 +13,7 @@ import io.element.android.features.messages.impl.voicemessages.composer.VoiceMes
import io.element.android.features.messages.test.FakeMessageComposerContext
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.mediaplayer.test.FakeAudioFocus
import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer
import io.element.android.libraries.mediaupload.api.MediaSender
import io.element.android.libraries.mediaupload.impl.DefaultMediaSender
@ -38,6 +39,10 @@ class FakeDefaultVoiceMessageComposerPresenterFactory(
timelineMode = timelineMode,
voiceRecorder = FakeVoiceRecorder(),
analyticsService = FakeAnalyticsService(),
audioFocus = FakeAudioFocus(
requestAudioFocusResult = { _, _ -> },
releaseAudioFocusResult = { },
),
mediaSenderFactory = { mediaSender },
player = VoiceMessageComposerPlayer(
mediaPlayer = FakeMediaPlayer(),

View file

@ -10,6 +10,7 @@
<string name="screen_advanced_settings_element_call_base_url_validation_error">"URL inválido, certifica-te de que incluis o protocolo (http/https) e o endereço correto."</string>
<string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"Ocultar avatares nos pedidos de acesso a salas"</string>
<string name="screen_advanced_settings_hide_timeline_media_toggle_title">"Ocultar pré-visualizações de multimédia na cronologia"</string>
<string name="screen_advanced_settings_labs">"Experiências"</string>
<string name="screen_advanced_settings_media_compression_description">"Carrega fotos e vídeos mais rapidamente e reduz a utilização de dados"</string>
<string name="screen_advanced_settings_media_compression_title">"Otimiza a qualidade da mídia"</string>
<string name="screen_advanced_settings_moderation_and_safety_section_title">"Moderação e Segurança"</string>
@ -43,6 +44,11 @@
<string name="screen_edit_profile_error_title">"Não foi possível atualizar o perfil"</string>
<string name="screen_edit_profile_title">"Editar perfil"</string>
<string name="screen_edit_profile_updating_details">"A atualizar o perfil…"</string>
<string name="screen_labs_enable_threads">"Ativar respostas em tópicos"</string>
<string name="screen_labs_enable_threads_description">"A aplicação será reiniciada para aplicar esta alteração."</string>
<string name="screen_labs_header_description">"Experimenta as nossas mais recentes ideias ainda em desenvolvimento. Estas funcionalidades não estão finalizadas e por isso podem ser instáveis ou acabarem alteradas."</string>
<string name="screen_labs_header_title">"E que tal umas experiências?"</string>
<string name="screen_labs_title">"Experiências"</string>
<string name="screen_notification_settings_additional_settings_section_title">"Configurações adicionais"</string>
<string name="screen_notification_settings_calls_label">"Chamadas de áudio e vídeo"</string>
<string name="screen_notification_settings_configuration_mismatch">"Incompatibilidade de configuração"</string>

View file

@ -429,7 +429,7 @@ class DefaultBugReporterTest {
assertThat((param as WriteToFilesConfiguration.Enabled).directory).endsWith("/cache/logs/server.org")
assertThat((param as WriteToFilesConfiguration.Enabled).filenamePrefix).isEqualTo("logs")
assertThat((param as WriteToFilesConfiguration.Enabled).numberOfFiles).isEqualTo(168)
assertThat((param as WriteToFilesConfiguration.Enabled).filenameSuffix).isEqualTo("log")
assertThat((param as WriteToFilesConfiguration.Enabled).filenameSuffix).isEqualTo(".log")
}
@OptIn(ExperimentalCoroutinesApi::class)
@ -491,7 +491,7 @@ class DefaultBugReporterTest {
assertThat((param as WriteToFilesConfiguration.Enabled).directory).endsWith("/cache/logs")
assertThat((param as WriteToFilesConfiguration.Enabled).filenamePrefix).isEqualTo("logs")
assertThat((param as WriteToFilesConfiguration.Enabled).numberOfFiles).isEqualTo(168)
assertThat((param as WriteToFilesConfiguration.Enabled).filenameSuffix).isEqualTo("log")
assertThat((param as WriteToFilesConfiguration.Enabled).filenameSuffix).isEqualTo(".log")
}
@Test

View file

@ -2,10 +2,13 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_room_change_permissions_administrators">"Administrador"</string>
<string name="screen_room_change_permissions_ban_people">"Banir pessoas"</string>
<string name="screen_room_change_permissions_change_settings">"Alterar configurações"</string>
<string name="screen_room_change_permissions_delete_messages">"Remover mensagens"</string>
<string name="screen_room_change_permissions_everyone">"Membro"</string>
<string name="screen_room_change_permissions_everyone">"Participante"</string>
<string name="screen_room_change_permissions_invite_people">"Convidar pessoas"</string>
<string name="screen_room_change_permissions_member_moderation">"Gerir membros"</string>
<string name="screen_room_change_permissions_manage_space">"Gerir espaço"</string>
<string name="screen_room_change_permissions_manage_space_rooms">"Gerir salas"</string>
<string name="screen_room_change_permissions_member_moderation">"Gerir participantes"</string>
<string name="screen_room_change_permissions_messages_and_content">"Mensagens e conteúdo"</string>
<string name="screen_room_change_permissions_moderators">"Moderador"</string>
<string name="screen_room_change_permissions_remove_people">"Remover pessoas"</string>
@ -14,6 +17,7 @@
<string name="screen_room_change_permissions_room_name">"Altera o nome da sala"</string>
<string name="screen_room_change_permissions_room_topic">"Alterar a descrição da sala"</string>
<string name="screen_room_change_permissions_send_messages">"Enviar mensagens"</string>
<string name="screen_room_change_permissions_title">"Permissões"</string>
<string name="screen_room_change_role_administrators_title">"Editar Administradores"</string>
<string name="screen_room_change_role_confirm_add_admin_description">"Não poderás desfazer esta ação. Estás a promover o utilizador para ter o mesmo nível de poder que tu."</string>
<string name="screen_room_change_role_confirm_add_admin_title">"Adicionar administrador?"</string>
@ -34,6 +38,12 @@
<string name="screen_room_change_role_unsaved_changes_description">"Tens alterações por guardar."</string>
<string name="screen_room_change_role_unsaved_changes_title">"Guardar alterações?"</string>
<string name="screen_room_member_list_banned_empty">"Não há nenhum utilizador banido."</string>
<plurals name="screen_room_member_list_banned_header_title">
<item quantity="one">"%1$d banido"</item>
<item quantity="other">"%1$d banidos"</item>
</plurals>
<string name="screen_room_member_list_empty_search_subtitle">"Verifica a ortografia ou tenta uma nova pesquisa"</string>
<string name="screen_room_member_list_empty_search_title">"Sem resultados para “%1$s”"</string>
<plurals name="screen_room_member_list_header_title">
<item quantity="one">"%1$d pessoa"</item>
<item quantity="other">"%1$d pessoas"</item>
@ -41,10 +51,15 @@
<string name="screen_room_member_list_manage_member_remove_confirmation_ban">"Remover e banir participante"</string>
<string name="screen_room_member_list_manage_member_remove_confirmation_kick">"Remover apenas"</string>
<string name="screen_room_member_list_manage_member_unban_action">"Anular banimento"</string>
<string name="screen_room_member_list_manage_member_unban_message">"Poderão juntar-se novamente a esta sala se forem convidados."</string>
<string name="screen_room_member_list_manage_member_unban_message">"Poderão entrar novamente nesta sala se forem convidados."</string>
<string name="screen_room_member_list_manage_member_unban_title">"Desbanir da sala"</string>
<string name="screen_room_member_list_mode_banned">"Banidos"</string>
<string name="screen_room_member_list_mode_members">"Participantes"</string>
<plurals name="screen_room_member_list_pending_header_title">
<item quantity="one">"%1$d convidado"</item>
<item quantity="other">"%1$d convidados"</item>
</plurals>
<string name="screen_room_member_list_pending_status">"Pendente"</string>
<string name="screen_room_member_list_role_administrator">"Administrador"</string>
<string name="screen_room_member_list_role_moderator">"Moderador"</string>
<string name="screen_room_member_list_role_owner">"Dono / Dona"</string>
@ -59,10 +74,12 @@
<string name="screen_room_roles_and_permissions_messages_and_content">"Mensagens e conteúdo"</string>
<string name="screen_room_roles_and_permissions_moderators">"Moderadores"</string>
<string name="screen_room_roles_and_permissions_owners">"Donos"</string>
<string name="screen_room_roles_and_permissions_permissions_header">"Permissões"</string>
<string name="screen_room_roles_and_permissions_reset">"Repor permissões"</string>
<string name="screen_room_roles_and_permissions_reset_confirm_description">"Ao repores as permissões, perderás as configurações atuais."</string>
<string name="screen_room_roles_and_permissions_reset_confirm_title">"Repor as permissões?"</string>
<string name="screen_room_roles_and_permissions_roles_header">"Cargos"</string>
<string name="screen_room_roles_and_permissions_room_details">"Detalhes da sala"</string>
<string name="screen_room_roles_and_permissions_space_details">"Detalhes do espaço"</string>
<string name="screen_room_roles_and_permissions_title">"Cargos e permissões"</string>
</resources>

View file

@ -88,7 +88,6 @@
<string name="screen_security_and_privacy_room_access_invite_only_option_description">"Хората могат да се присъединят само ако са поканени"</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_title">"Само с покана"</string>
<string name="screen_security_and_privacy_room_access_section_header">"Достъп до стаята"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_title">"Членове на пространството"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_unavailable_description">"Пространствата в момента не се поддържат"</string>
<string name="screen_security_and_privacy_room_directory_visibility_toggle_title">"Видима в директорията на обществените стаи"</string>
<string name="screen_security_and_privacy_room_history_section_header">"Кой може да чете историята"</string>

View file

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="crypto_history_sharing_room_info_hidden_badge_content">"Noví členové nevidí historii"</string>
<string name="crypto_history_sharing_room_info_shared_badge_content">"Noví členové vidí historii"</string>
<string name="crypto_history_sharing_room_info_world_readable_badge_content">"Každý může vidět historii"</string>
<string name="screen_edit_room_address_room_address_section_footer">"Budete potřebovat adresu místnosti, aby byla viditelná v adresáři místností."</string>
<string name="screen_edit_room_address_title">"Upravit adresu"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Při aktualizaci nastavení oznámení došlo k chybě."</string>

View file

@ -135,7 +135,6 @@ Nid ydym yn argymell galluogi amgryptio ar gyfer ystafelloedd y gall unrhyw un d
<string name="screen_security_and_privacy_room_access_invite_only_option_description">"Dim ond os cawn nhw wahoddiad gall pobl ymuno"</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_title">"Gwahoddiad yn unig"</string>
<string name="screen_security_and_privacy_room_access_section_header">"Mynediad ystafell"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_title">"Aelodau gofod"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_unavailable_description">"Nid yw gofodau\'n cael eu cefnogi ar hyn o bryd"</string>
<string name="screen_security_and_privacy_room_address_section_footer">"Bydd angen cyfeiriad ystafell arnoch i\'w wneud yn weladwy yn y cyfeiriadur."</string>
<string name="screen_security_and_privacy_room_address_section_header">"Cyfeiriad yr ystafell"</string>

View file

@ -150,7 +150,6 @@ Vi anbefaler ikke at aktivere kryptering for rum, som alle kan finde og deltage
<string name="screen_security_and_privacy_room_access_section_header">"Adgang"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_multiple_parents_description">"Alle i autoriserede grupper kan deltage."</string>
<string name="screen_security_and_privacy_room_access_space_members_option_single_parent_description">"Alle i %1$s kan deltage."</string>
<string name="screen_security_and_privacy_room_access_space_members_option_title">"Medlemmer af gruppen"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_unavailable_description">"Grupper understøttes ikke i øjeblikket"</string>
<string name="screen_security_and_privacy_room_address_section_footer">"Du skal bruge en adresse for at gøre det synligt i det offentlige register."</string>
<string name="screen_security_and_privacy_room_address_section_header">"Adresse"</string>

View file

@ -150,7 +150,6 @@ Wir empfehlen keine Verschlüsselung für Chats zu aktivieren, die jeder finden
<string name="screen_security_and_privacy_room_access_section_header">"Zugang"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_multiple_parents_description">"Jeder in autorisierten Spaces kann beitreten."</string>
<string name="screen_security_and_privacy_room_access_space_members_option_single_parent_description">"Jeder in %1$s kann beitreten."</string>
<string name="screen_security_and_privacy_room_access_space_members_option_title">"Spacemitglieder"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_unavailable_description">"Spaces werden zur Zeit nicht unterstützt."</string>
<string name="screen_security_and_privacy_room_address_section_footer">"Du benötigst eine Chat-Adresse, um den Chat im öffentlichen Verzeichnis sichtbar zu machen."</string>
<string name="screen_security_and_privacy_room_address_section_header">"Adresse"</string>

View file

@ -120,7 +120,6 @@
<string name="screen_security_and_privacy_room_access_invite_only_option_description">"Τα άτομα μπορούν να συμμετάσχουν μόνο εάν έχουν προσκληθεί"</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_title">"Μόνο πρόσκληση"</string>
<string name="screen_security_and_privacy_room_access_section_header">"Πρόσβαση στην αίθουσα"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_title">"Μέλη χώρου"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_unavailable_description">"Οι χώροι δεν υποστηρίζονται προς το παρόν"</string>
<string name="screen_security_and_privacy_room_address_section_footer">"Θα χρειαστείτε μια διεύθυνση αίθουσας για να την κάνετε ορατή στον κατάλογο."</string>
<string name="screen_security_and_privacy_room_directory_visibility_section_footer">"Επιστρέψτε την εύρεση αυτής της αίθουσας με αναζήτηση στον κατάλογο %1$s δημοσίων αιθουσών"</string>

View file

@ -118,7 +118,6 @@ No recomendamos habilitar el cifrado para las salas que cualquiera pueda encontr
<string name="screen_security_and_privacy_room_access_invite_only_option_description">"Las personas solo pueden unirse si están invitadas"</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_title">"Solo por invitación"</string>
<string name="screen_security_and_privacy_room_access_section_header">"Acceso a la sala"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_title">"Miembros del espacio"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_unavailable_description">"No se admiten los espacios por el momento."</string>
<string name="screen_security_and_privacy_room_address_section_footer">"Necesitarás una dirección de sala para que sea visible en el directorio."</string>
<string name="screen_security_and_privacy_room_directory_visibility_section_footer">"Permite encontrar esta sala buscando en el directorio de salas públicas de %1$s"</string>

View file

@ -106,7 +106,6 @@
<string name="screen_security_and_privacy_room_access_invite_only_option_description">"Gonbidatutako pertsonak bakarrik sartu ahal izango dira"</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_title">"Gonbidapen bidez"</string>
<string name="screen_security_and_privacy_room_access_section_header">"Gelarako sarbidea"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_title">"Guneko kideak"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_unavailable_description">"Gaur-gaurkoz ez da guneekin bateragarria"</string>
<string name="screen_security_and_privacy_room_address_section_header">"Gelaren helbidea"</string>
<string name="screen_security_and_privacy_room_directory_visibility_toggle_title">"Gela publikoen direktorioan ikusgai"</string>

View file

@ -123,7 +123,6 @@
<string name="screen_security_and_privacy_room_access_invite_only_option_description">"افراد فقط در صورت دعوت می‌توانند بپیوندند"</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_title">"فقط دعوتی"</string>
<string name="screen_security_and_privacy_room_access_section_header">"دسترسی اتاق"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_title">"اعضای فضا"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_unavailable_description">"در حال حاضر فضاها پشتیبانی نمی‌شوند"</string>
<string name="screen_security_and_privacy_room_address_section_header">"نشانی اتاق"</string>
<string name="screen_security_and_privacy_room_history_anyone_option_title">"هرکسی"</string>

View file

@ -132,7 +132,6 @@ Emme suosittele salauksen ottamista käyttöön huoneissa, jotka kuka tahansa vo
<string name="screen_security_and_privacy_room_access_invite_only_option_description">"Vain kutsutut henkilöt voivat liittyä."</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_title">"Vain kutsutut"</string>
<string name="screen_security_and_privacy_room_access_section_header">"Pääsy"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_title">"Tilan jäsenet"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_unavailable_description">"Tiloja ei tällä hetkellä tueta"</string>
<string name="screen_security_and_privacy_room_address_section_footer">"Tarvitset osoitteen, jotta se näkyy julkisessa hakemistossa."</string>
<string name="screen_security_and_privacy_room_address_section_header">"Osoite"</string>

View file

@ -151,7 +151,6 @@ Ne preporučujemo omogućavanje šifriranja za sobe koje svatko može pronaći i
<string name="screen_security_and_privacy_room_access_section_header">"Pristup"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_multiple_parents_description">"Svatko tko se nalazi u ovlaštenim prostorima može se pridružiti."</string>
<string name="screen_security_and_privacy_room_access_space_members_option_single_parent_description">"Svatko u %1$s može se pridružiti."</string>
<string name="screen_security_and_privacy_room_access_space_members_option_title">"Članovi prostora"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_unavailable_description">"Prostori trenutačno nisu podržani"</string>
<string name="screen_security_and_privacy_room_address_section_footer">"Trebat će vam adresa kako bi bila vidljiva u javnom direktoriju."</string>
<string name="screen_security_and_privacy_room_address_section_header">"Adresa"</string>

View file

@ -150,7 +150,6 @@ Nem javasoljuk a titkosítás engedélyezését az olyan szobákban, amelyeket b
<string name="screen_security_and_privacy_room_access_section_header">"Hozzáférés"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_multiple_parents_description">"Bárki csatlakozhat, az engedélyezett terekből."</string>
<string name="screen_security_and_privacy_room_access_space_members_option_single_parent_description">"Bárki csatlakozhatnak innen: %1$s."</string>
<string name="screen_security_and_privacy_room_access_space_members_option_title">"A tér tagjai"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_unavailable_description">"A terek jelenleg nem támogatottak"</string>
<string name="screen_security_and_privacy_room_address_section_footer">"Szüksége lesz egy szobacímre, hogy láthatóvá tegye a szobakatalógusban."</string>
<string name="screen_security_and_privacy_room_address_section_header">"Cím"</string>

View file

@ -117,7 +117,6 @@ Kami tidak menyarankan untuk mengaktifkan enkripsi untuk ruangan yang dapat dite
<string name="screen_security_and_privacy_room_access_invite_only_option_description">"Orang hanya dapat bergabung jika mereka diundang"</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_title">"Hanya undangan"</string>
<string name="screen_security_and_privacy_room_access_section_header">"Akses ruangan"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_title">"Anggota space"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_unavailable_description">"Space saat ini tidak didukung"</string>
<string name="screen_security_and_privacy_room_address_section_footer">"Anda akan memerlukan alamat ruangan untuk membuatnya terlihat dalam direktori."</string>
<string name="screen_security_and_privacy_room_directory_visibility_section_footer">"Izinkan ruangan ini ditemukan dengan mencari direktori ruangan %1$s publik"</string>

View file

@ -143,7 +143,6 @@ Non consigliamo di attivare la crittografia per le stanze che chiunque può trov
<string name="screen_security_and_privacy_room_access_invite_only_option_description">"Solo le persone invitate possono entrare."</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_title">"Solo su invito"</string>
<string name="screen_security_and_privacy_room_access_section_header">"Accesso"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_title">"Membri dello spazio"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_unavailable_description">"Gli spazi non sono attualmente supportati"</string>
<string name="screen_security_and_privacy_room_address_section_footer">"Per renderlo visibile nell\'elenco pubblico, avrai bisogno di un indirizzo."</string>
<string name="screen_security_and_privacy_room_address_section_header">"Indirizzo"</string>

View file

@ -124,7 +124,6 @@
<string name="screen_security_and_privacy_room_access_invite_only_option_description">"초대받은 사용자만 가입할 수 있습니다."</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_title">"초대 전용"</string>
<string name="screen_security_and_privacy_room_access_section_header">"방 액세스"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_title">"스페이스 멤버들"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_unavailable_description">"스페이스는 현재 지원되지 않습니다"</string>
<string name="screen_security_and_privacy_room_address_section_footer">"디렉토리에 표시하려면 방 주소가 필요합니다."</string>
<string name="screen_security_and_privacy_room_directory_visibility_section_footer">"%1$s 공개 방 디렉토리에서 이 방을 검색할 수 있도록 허용합니다"</string>

View file

@ -149,7 +149,6 @@ Vi anbefaler ikke å aktivere kryptering for rom som hvem som helst kan finne og
<string name="screen_security_and_privacy_room_access_section_header">"Tilgang"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_multiple_parents_description">"Alle i autoriserte områder kan bli med."</string>
<string name="screen_security_and_privacy_room_access_space_members_option_single_parent_description">"Alle i %1$s kan bli med."</string>
<string name="screen_security_and_privacy_room_access_space_members_option_title">"Medlemmer av område"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_unavailable_description">"Områder støttes ikke for øyeblikket"</string>
<string name="screen_security_and_privacy_room_address_section_footer">"Du trenger en adresse for å gjøre den synlig i den offentlige katalogen."</string>
<string name="screen_security_and_privacy_room_address_section_header">"Adresse"</string>

View file

@ -130,7 +130,6 @@ Odradzamy włączanie szyfrowania dla pokoi, które każdy może znaleźć i do
<string name="screen_security_and_privacy_room_access_invite_only_option_description">"Tylko osoby z zaproszeniem mogą dołączyć"</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_title">"Tylko zaproszenie"</string>
<string name="screen_security_and_privacy_room_access_section_header">"Dostęp do pokoju"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_title">"Członkowie przestrzeni"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_unavailable_description">"Przestrzenie nie są obecnie wspierane"</string>
<string name="screen_security_and_privacy_room_address_section_footer">"Aby pokój był widoczny w katalogu, potrzebny jest adres pokoju."</string>
<string name="screen_security_and_privacy_room_address_section_header">"Adres pokoju"</string>

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