diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 1ab5d835b2..d25a322161 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -1,5 +1,5 @@ name: Bug report for the Element Android app -description: Report any issues that you have found with the Element app. Please [check open issues](https://github.com/vector-im/element-android/issues) first, in case it has already been reported. +description: Report any issues that you have found with the Element app. Please check open issues first, in case it has already been reported. labels: [T-Defect] body: - type: markdown diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..09848e9f88 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: Enhancement or feature request + url: https://github.com/vector-im/element-meta/discussions/categories/ideas + about: Do you have a suggestion or feature request? + - name: Element Android Community Support + url: https://matrix.to/#/#element-android:matrix.org + about: General Element Android support questions can be asked in the app Matrix room diff --git a/.github/ISSUE_TEMPLATE/enhancement.yml b/.github/ISSUE_TEMPLATE/enhancement.yml deleted file mode 100644 index 0e51d5155e..0000000000 --- a/.github/ISSUE_TEMPLATE/enhancement.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Enhancement request -description: Do you have a suggestion or feature request? -labels: [T-Enhancement] -body: - - type: markdown - attributes: - value: | - Thank you for taking the time to propose an enhancement to an existing feature. If you would like to propose a new feature or a major cross-platform change, please [start a discussion here](https://github.com/vector-im/element-meta/discussions/new?category=ideas). - - type: textarea - id: usecase - attributes: - label: Your use case - description: Please feel welcome to include screenshots or mock ups. - placeholder: Tell us what you would like to do! - value: | - #### What would you like to do? - - #### Why would you like to do it? - - #### How would you like to achieve it? - validations: - required: true - - type: textarea - id: alternative - attributes: - label: Have you considered any alternatives? - placeholder: A clear and concise description of any alternative solutions or features you've considered. - validations: - required: false - - type: textarea - id: additional-context - attributes: - label: Additional context - placeholder: Is there anything else you'd like to add? - validations: - required: false - - type: dropdown - id: pr - attributes: - label: Are you willing to provide a PR? - description: | - Don't worry, it's still OK to answer 'No' :). - options: - - 'Yes' - - 'No' - validations: - required: true diff --git a/.github/ISSUE_TEMPLATE/matrix-sdk.yml b/.github/ISSUE_TEMPLATE/matrix-sdk.yml index 4033423dd5..213a6696f7 100644 --- a/.github/ISSUE_TEMPLATE/matrix-sdk.yml +++ b/.github/ISSUE_TEMPLATE/matrix-sdk.yml @@ -1,5 +1,5 @@ name: Matrix SDK bug or enhancement -description: Report issue or ask for a feature in the [Android Matrix SDK](https://github.com/matrix-org/matrix-android-sdk2) +description: "Report issue or ask for a feature in the Android Matrix SDK: https://github.com/matrix-org/matrix-android-sdk2" title: "[SDK] " labels: [matrix-sdk] diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f3d14d9ecd..985da7d83a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,6 +25,9 @@ jobs: cancel-in-progress: true steps: - uses: actions/checkout@v3 + with: + # https://github.com/actions/checkout/issues/881 + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} - name: Configure gradle uses: gradle/gradle-build-action@v2 with: @@ -46,6 +49,9 @@ jobs: cancel-in-progress: ${{ github.ref != 'refs/head/main' }} steps: - uses: actions/checkout@v3 + with: + # https://github.com/actions/checkout/issues/881 + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} - name: Configure gradle uses: gradle/gradle-build-action@v2 with: diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml index 91352bb27b..4c00894a36 100644 --- a/.github/workflows/danger.yml +++ b/.github/workflows/danger.yml @@ -11,7 +11,7 @@ jobs: - run: | npm install --save-dev @babel/plugin-transform-flow-strip-types - name: Danger - uses: danger/danger-js@11.2.2 + uses: danger/danger-js@11.2.4 with: args: "--dangerfile ./tools/danger/dangerfile.js" env: diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 823af089fc..09b40a28ce 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -68,7 +68,7 @@ jobs: yarn add danger-plugin-lint-report --dev - name: Danger lint if: always() - uses: danger/danger-js@11.2.2 + uses: danger/danger-js@11.2.4 with: args: "--dangerfile ./tools/danger/dangerfile-lint.js" env: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a430b3d4e5..7af7ce2a36 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,6 +4,8 @@ on: pull_request: { } push: branches: [ main, develop ] + paths-ignore: + - '.github/**' # Enrich gradle.properties for CI/CD env: @@ -109,7 +111,7 @@ jobs: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} ORG_GRADLE_PROJECT_SONAR_LOGIN: ${{ secrets.SONAR_TOKEN }} if: ${{ always() && env.GITHUB_TOKEN != '' && env.SONAR_TOKEN != '' && env.ORG_GRADLE_PROJECT_SONAR_LOGIN != '' }} - run: ./gradlew sonarqube $CI_GRADLE_ARG_PROPERTIES + run: ./gradlew sonar $CI_GRADLE_ARG_PROPERTIES - name: Format unit test results if: always() diff --git a/.github/workflows/triage-labelled.yml b/.github/workflows/triage-labelled.yml index 036bc069ac..5abd284dcd 100644 --- a/.github/workflows/triage-labelled.yml +++ b/.github/workflows/triage-labelled.yml @@ -271,6 +271,31 @@ jobs: PROJECT_ID: "PVT_kwDOAM0swc4ABTXY" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + + ex_plorers: + name: Add labelled issues to X-Plorer project + runs-on: ubuntu-latest + if: > + contains(github.event.issue.labels.*.name, 'Team: Element X Feature') + steps: + - uses: octokit/graphql-action@v2.x + id: add_to_project + with: + headers: '{"GraphQL-Features": "projects_next_graphql"}' + query: | + mutation add_to_project($projectid:ID!,$contentid:ID!) { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { + id + } + } + } + projectid: ${{ env.PROJECT_ID }} + contentid: ${{ github.event.issue.node_id }} + env: + PROJECT_ID: "PVT_kwDOAM0swc4ALoFY" + GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + ps_features1: name: Add labelled issues to PS features team 1 runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index b7332e11f1..003765002d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ .idea/caches .idea/libraries .idea/inspectionProfiles +.idea/sonarlint .idea/*.xml .DS_Store /build diff --git a/CHANGES.md b/CHANGES.md index 76b46bbbe7..59e7c85715 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,98 @@ +Changes in Element v1.5.28 (2023-03-08) +======================================= + +Features ✨ +---------- + - [Poll] Error handling for push rules synchronization ([#8141](https://github.com/vector-im/element-android/issues/8141)) + - Add aggregated unread indicator for spaces in the new layout ([#8157](https://github.com/vector-im/element-android/issues/8157)) + - [Rich text editor] Add ability to insert GIFs from keyboard ([#8185](https://github.com/vector-im/element-android/issues/8185)) + +Bugfixes 🐛 +---------- + - Space setting category doesn't show up ([#7989](https://github.com/vector-im/element-android/issues/7989)) + - Fix timeline loading a wrong room on permalink if a matching event id is found in a different room ([#8168](https://github.com/vector-im/element-android/issues/8168)) + - Reapply local push rules after event decryption ([#8170](https://github.com/vector-im/element-android/issues/8170)) + - [Rich text editor] Fix code appearance ([#8171](https://github.com/vector-im/element-android/issues/8171)) + - Extend workaround for extra new lines in timeline ([#8187](https://github.com/vector-im/element-android/issues/8187)) + - [Poll history] Fixing small issue about font style ([#8190](https://github.com/vector-im/element-android/issues/8190)) + - Update room member shields behavior ([#8195](https://github.com/vector-im/element-android/issues/8195)) + +Other changes +------------- + - Direct Message: Manage encrypted DM in case of invite by email ([#6912](https://github.com/vector-im/element-android/issues/6912)) + + +Changes in Element v1.5.26 (2023-02-22) +======================================= + +Features ✨ +---------- + - Adds MSC3824 OIDC-awareness when talking to an OIDC-enabled homeservers ([#6367](https://github.com/vector-im/element-android/issues/6367)) + - [Poll] Synchronize polls push rules with message push rules ([#8007](https://github.com/vector-im/element-android/issues/8007)) + - [Rich text editor] Add code block, quote and indentation actions ([#8045](https://github.com/vector-im/element-android/issues/8045)) + - [Poll] History list: details screen of a poll + - [Poll] History list: enable the new settings entry in release mode ([#8056](https://github.com/vector-im/element-android/issues/8056)) + - [Location sharing] Show own location in map views ([#8110](https://github.com/vector-im/element-android/issues/8110)) + - Updates to protocol used for Sign in with QR code ([#8123](https://github.com/vector-im/element-android/issues/8123)) + - [Poll] Synchronize polls and message push rules ([#8130](https://github.com/vector-im/element-android/issues/8130)) + +Bugfixes 🐛 +---------- + - Android app does not show correct poll data ([#6121](https://github.com/vector-im/element-android/issues/6121)) + - Fix timeline always jumps to the bottom when screen goes back to foreground. ([#8090](https://github.com/vector-im/element-android/issues/8090)) + - [Poll] Improve rendering of poll end message when poll start event isn't available ([#8129](https://github.com/vector-im/element-android/issues/8129)) + - Replace hardcoded colors by theming colors on send button. ([#8142](https://github.com/vector-im/element-android/issues/8142)) + - [Timeline]: Editing a reply from iOS breaks the "in reply to" rendering ([#8150](https://github.com/vector-im/element-android/issues/8150)) + +Other changes +------------- + - Build unmerged APKs on pull request ([#8044](https://github.com/vector-im/element-android/issues/8044)) + - Replace 'Bots' with 'bots' inside terms_description_for_integration_manager ([#8115](https://github.com/vector-im/element-android/issues/8115)) + - Fix ktlint issue with fields and a new line. ([#8139](https://github.com/vector-im/element-android/issues/8139)) + + +Changes in Element v1.5.25 (2023-02-15) +======================================= + +Bugfixes 🐛 +---------- + - CountUpTimer - Fix StackOverFlow exception when stop action is called within the tick event ([#8127](https://github.com/vector-im/element-android/issues/8127)) + + +Changes in Element v1.5.24 (2023-02-08) +======================================= + +Features ✨ +---------- + - [Rich text editor] Add inline code to rich text editor ([#8011](https://github.com/vector-im/element-android/issues/8011)) + +Bugfixes 🐛 +---------- + - If media cache is large, Settings > General takes a long time to open ([#5918](https://github.com/vector-im/element-android/issues/5918)) + - Fix that replies to @roomba would be highlighted as a room ping. Contributed by Nico. ([#6457](https://github.com/vector-im/element-android/issues/6457)) + - Cannot select text properly in plain text mode when using Rich Text Editor. ([#7801](https://github.com/vector-im/element-android/issues/7801)) + - Fix the next button disabled issue after going to change homeserver screen ([#7928](https://github.com/vector-im/element-android/issues/7928)) + - Fix extra new lines added to inline code ([#7975](https://github.com/vector-im/element-android/issues/7975)) + - [Voice Broadcast] Use internal playback timer to compute the current playback position ([#8012](https://github.com/vector-im/element-android/issues/8012)) + - Do not send any request to Posthog if no consent is provided. ([#8031](https://github.com/vector-im/element-android/issues/8031)) + - [Voice Broadcast] We should not be able to start broadcasting if there is already a live broadcast in the Room ([#8062](https://github.com/vector-im/element-android/issues/8062)) + +In development 🚧 +---------------- + - [Poll] History list: unmock data ([#7864](https://github.com/vector-im/element-android/issues/7864)) + +SDK API changes ⚠️ +------------------ + - [Poll] Adding PollHistoryService ([#7864](https://github.com/vector-im/element-android/issues/7864)) + - [Push rules] Call /actions api before /enabled api ([#8005](https://github.com/vector-im/element-android/issues/8005)) + +Other changes +------------- + - Let the user know when we are not able to decrypt the voice broadcast chunks ([#7820](https://github.com/vector-im/element-android/issues/7820)) + - [Voice Broadcast] Show Live broadcast in the room list only if the feature flag is enabled in the lab ([#8042](https://github.com/vector-im/element-android/issues/8042)) + - Improve the `CountUpTimer` implementation ([#8058](https://github.com/vector-im/element-android/issues/8058)) + + Changes in Element v1.5.22 (2023-01-25) ======================================= diff --git a/build.gradle b/build.gradle index 9ea996e70b..4cbf84e6a1 100644 --- a/build.gradle +++ b/build.gradle @@ -26,10 +26,10 @@ buildscript { classpath libs.gradle.hiltPlugin classpath 'com.google.firebase:firebase-appdistribution-gradle:3.2.0' classpath 'com.google.gms:google-services:4.3.15' - classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.5.0.2730' + classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.0.0.2929' classpath 'com.google.android.gms:oss-licenses-plugin:0.10.6' classpath "com.likethesalad.android:stem-plugin:2.3.0" - classpath 'org.owasp:dependency-check-gradle:8.0.1' + classpath 'org.owasp:dependency-check-gradle:8.1.2' classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.7.20" classpath "org.jetbrains.kotlinx:kotlinx-knit:0.4.0" classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.3' @@ -41,14 +41,14 @@ buildscript { plugins { // ktlint Plugin - id "org.jlleitschuh.gradle.ktlint" version "11.1.0" + id "org.jlleitschuh.gradle.ktlint" version "11.3.1" // Detekt id "io.gitlab.arturbosch.detekt" version "1.22.0" // Ksp - id "com.google.devtools.ksp" version "1.8.0-1.0.8" + id "com.google.devtools.ksp" version "1.8.10-1.0.9" // Dependency Analysis - id 'com.autonomousapps.dependency-analysis' version "1.18.0" + id 'com.autonomousapps.dependency-analysis' version "1.19.0" // Gradle doctor id "com.osacky.doctor" version "0.8.1" } @@ -212,10 +212,10 @@ if (launchTask.contains("coverage".toLowerCase())) { apply plugin: 'org.sonarqube' // To run a sonar analysis: -// Run './gradlew sonarqube -Dsonar.login=' +// Run './gradlew sonar -Dsonar.login=' // The SONAR_KEY is stored in passbolt as Token Sonar Cloud Bma -sonarqube { +sonar { properties { property "sonar.projectName", "element-android" property "sonar.projectKey", "vector-im_element-android" @@ -234,7 +234,7 @@ sonarqube { } project(":vector") { - sonarqube { + sonar { properties { property "sonar.sources", project(":vector").android.sourceSets.main.java.srcDirs // exclude source code from analyses separated by a colon (:) @@ -245,13 +245,13 @@ project(":vector") { } project(":library:external:diff-match-patch") { - sonarqube { + sonar { skipProject = true } } //project(":matrix-sdk-android") { -// sonarqube { +// sonar { // properties { // property "sonar.sources", project(":matrix-sdk-android").android.sourceSets.main.java.srcDirs // // exclude source code from analyses separated by a colon (:) diff --git a/changelog.d/6457.bugfix b/changelog.d/6457.bugfix deleted file mode 100644 index 89ba075378..0000000000 --- a/changelog.d/6457.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix that replies to @roomba would be highlighted as a room ping. Contributed by Nico. diff --git a/changelog.d/7864.sdk b/changelog.d/7864.sdk deleted file mode 100644 index b7c6a5b339..0000000000 --- a/changelog.d/7864.sdk +++ /dev/null @@ -1 +0,0 @@ -[Poll] Adding PollHistoryService diff --git a/changelog.d/7864.wip b/changelog.d/7864.wip deleted file mode 100644 index da04806b8b..0000000000 --- a/changelog.d/7864.wip +++ /dev/null @@ -1 +0,0 @@ -[Poll] History list: unmock data diff --git a/changelog.d/7975.bugfix b/changelog.d/7975.bugfix deleted file mode 100644 index b34c784b27..0000000000 --- a/changelog.d/7975.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix extra new lines added to inline code diff --git a/changelog.d/8005.sdk b/changelog.d/8005.sdk deleted file mode 100644 index 1849776d50..0000000000 --- a/changelog.d/8005.sdk +++ /dev/null @@ -1 +0,0 @@ -[Push rules] Call /actions api before /enabled api diff --git a/changelog.d/8011.feature b/changelog.d/8011.feature deleted file mode 100644 index 700a528fc1..0000000000 --- a/changelog.d/8011.feature +++ /dev/null @@ -1 +0,0 @@ -[Rich text editor] Add inline code to rich text editor \ No newline at end of file diff --git a/changelog.d/8012.bugfix b/changelog.d/8012.bugfix deleted file mode 100644 index bd2ee3dd08..0000000000 --- a/changelog.d/8012.bugfix +++ /dev/null @@ -1 +0,0 @@ -[Voice Broadcast] Use internal playback timer to compute the current playback position diff --git a/changelog.d/8031.bugfix b/changelog.d/8031.bugfix deleted file mode 100644 index 0e7ff28509..0000000000 --- a/changelog.d/8031.bugfix +++ /dev/null @@ -1 +0,0 @@ -Do not send any request to Posthog if no consent is provided. diff --git a/changelog.d/8042.misc b/changelog.d/8042.misc deleted file mode 100644 index dbfe98140c..0000000000 --- a/changelog.d/8042.misc +++ /dev/null @@ -1 +0,0 @@ -[Voice Broadcast] Show Live broadcast in the room list only if the feature flag is enabled in the lab diff --git a/changelog.d/8058.misc b/changelog.d/8058.misc deleted file mode 100644 index d864b3c7a8..0000000000 --- a/changelog.d/8058.misc +++ /dev/null @@ -1 +0,0 @@ -Improve the `CountUpTimer` implementation diff --git a/changelog.d/8062.bugfix b/changelog.d/8062.bugfix deleted file mode 100644 index af1a370350..0000000000 --- a/changelog.d/8062.bugfix +++ /dev/null @@ -1,2 +0,0 @@ - [Voice Broadcast] We should not be able to start broadcasting if there is already a live broadcast in the Room - \ No newline at end of file diff --git a/changelog.d/8208.bugfix b/changelog.d/8208.bugfix new file mode 100644 index 0000000000..9131367c60 --- /dev/null +++ b/changelog.d/8208.bugfix @@ -0,0 +1 @@ +Replace hardcoded colors by theming colors on save button. \ No newline at end of file diff --git a/dependencies.gradle b/dependencies.gradle index 413199363a..0b37c8daa2 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -6,36 +6,36 @@ ext.versions = [ 'targetCompat' : JavaVersion.VERSION_11, ] -def gradle = "7.4.1" +def gradle = "7.4.2" // Ref: https://kotlinlang.org/releases.html -def kotlin = "1.8.0" +def kotlin = "1.8.10" def kotlinCoroutines = "1.6.4" -def dagger = "2.44.2" -def firebaseBom = "31.2.0" -def appDistribution = "16.0.0-beta05" +def dagger = "2.45" +def firebaseBom = "31.2.2" +def appDistribution = "16.0.0-beta06" def retrofit = "2.9.0" def markwon = "4.6.2" def moshi = "1.14.0" def lifecycle = "2.5.1" def flowBinding = "1.2.0" -def flipper = "0.177.0" +def flipper = "0.183.0" def epoxy = "5.0.0" def mavericks = "3.0.1" -def glide = "4.14.2" +def glide = "4.15.0" def bigImageViewer = "1.8.1" def jjwt = "0.11.5" // Temporary version to unblock #6929. Once 0.16.0 is released we should use it, and revert // the whole commit which set version 0.16.0-SNAPSHOT def vanniktechEmoji = "0.16.0-SNAPSHOT" -def sentry = "6.13.0" +def sentry = "6.15.0" // Use 1.6.0 alpha to fix issue with test -def fragment = "1.6.0-alpha04" +def fragment = "1.6.0-alpha06" // Testing def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819 def espresso = "3.5.1" def androidxTest = "1.5.0" def androidxOrchestrator = "1.4.2" -def paparazzi = "1.1.0" +def paparazzi = "1.2.0" ext.libs = [ gradle : [ @@ -50,16 +50,16 @@ ext.libs = [ ], androidx : [ 'activity' : "androidx.activity:activity-ktx:1.6.1", - 'appCompat' : "androidx.appcompat:appcompat:1.6.0", + 'appCompat' : "androidx.appcompat:appcompat:1.6.1", 'biometric' : "androidx.biometric:biometric:1.1.0", 'core' : "androidx.core:core-ktx:1.9.0", - 'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1", - 'exifinterface' : "androidx.exifinterface:exifinterface:1.3.5", + 'recyclerview' : "androidx.recyclerview:recyclerview:1.3.0", + 'exifinterface' : "androidx.exifinterface:exifinterface:1.3.6", 'fragmentKtx' : "androidx.fragment:fragment-ktx:$fragment", 'fragmentTesting' : "androidx.fragment:fragment-testing:$fragment", 'fragmentTestingManifest' : "androidx.fragment:fragment-testing-manifest:$fragment", 'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.4", - 'work' : "androidx.work:work-runtime-ktx:2.7.1", + 'work' : "androidx.work:work-runtime-ktx:2.8.0", 'autoFill' : "androidx.autofill:autofill:1.1.0", 'preferenceKtx' : "androidx.preference:preference-ktx:1.2.0", 'junit' : "androidx.test.ext:junit:1.1.5", @@ -70,7 +70,7 @@ ext.libs = [ 'datastore' : "androidx.datastore:datastore:1.0.0", 'datastorepreferences' : "androidx.datastore:datastore-preferences:1.0.0", 'pagingRuntimeKtx' : "androidx.paging:paging-runtime-ktx:2.1.2", - 'coreTesting' : "androidx.arch.core:core-testing:2.1.0", + 'coreTesting' : "androidx.arch.core:core-testing:2.2.0", 'testCore' : "androidx.test:core:$androidxTest", 'orchestrator' : "androidx.test:orchestrator:$androidxOrchestrator", 'testRunner' : "androidx.test:runner:$androidxTest", @@ -79,16 +79,16 @@ ext.libs = [ 'espressoContrib' : "androidx.test.espresso:espresso-contrib:$espresso", 'espressoIntents' : "androidx.test.espresso:espresso-intents:$espresso", 'viewpager2' : "androidx.viewpager2:viewpager2:1.0.0", - 'transition' : "androidx.transition:transition:1.2.0", + 'transition' : "androidx.transition:transition:1.4.1", ], google : [ - 'material' : "com.google.android.material:material:1.7.0", + 'material' : "com.google.android.material:material:1.8.0", 'firebaseBom' : "com.google.firebase:firebase-bom:$firebaseBom", 'messaging' : "com.google.firebase:firebase-messaging", 'appdistributionApi' : "com.google.firebase:firebase-appdistribution-api-ktx:$appDistribution", 'appdistribution' : "com.google.firebase:firebase-appdistribution:$appDistribution", // Phone number https://github.com/google/libphonenumber - 'phonenumber' : "com.googlecode.libphonenumber:libphonenumber:8.13.5" + 'phonenumber' : "com.googlecode.libphonenumber:libphonenumber:8.13.7" ], dagger : [ 'dagger' : "com.google.dagger:dagger:$dagger", @@ -103,7 +103,7 @@ ext.libs = [ ], element : [ 'opusencoder' : "io.element.android:opusencoder:1.1.0", - 'wysiwyg' : "io.element.android:wysiwyg:0.18.0" + 'wysiwyg' : "io.element.android:wysiwyg:1.1.1" ], squareup : [ 'moshi' : "com.squareup.moshi:moshi:$moshi", @@ -134,7 +134,7 @@ ext.libs = [ 'mavericksTesting' : "com.airbnb.android:mavericks-testing:$mavericks" ], maplibre : [ - 'androidSdk' : "org.maplibre.gl:android-sdk:9.6.0", + 'androidSdk' : "org.maplibre.gl:android-sdk:10.0.2", 'pluginAnnotation' : "org.maplibre.gl:android-plugin-annotation-v9:1.0.0" ], mockk : [ diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40105240.txt b/fastlane/metadata/android/cs-CZ/changelogs/40105240.txt new file mode 100644 index 0000000000..e0c33880d4 --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40105240.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Hlavně opravy chyb, zejména oprava zpráv, které se nezobrazovaly na časové ose. +Úplný seznam změn: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40105250.txt b/fastlane/metadata/android/cs-CZ/changelogs/40105250.txt new file mode 100644 index 0000000000..e0c33880d4 --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40105250.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Hlavně opravy chyb, zejména oprava zpráv, které se nezobrazovaly na časové ose. +Úplný seznam změn: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40105260.txt b/fastlane/metadata/android/cs-CZ/changelogs/40105260.txt new file mode 100644 index 0000000000..5c6d6cf466 --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40105260.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Hlavně opravy chyb. +Úplný seznam změn: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/de-DE/changelogs/40105240.txt b/fastlane/metadata/android/de-DE/changelogs/40105240.txt new file mode 100644 index 0000000000..6fce28362c --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40105240.txt @@ -0,0 +1,2 @@ +Die wichtigsten Änderungen in dieser Version: Hauptsächlich Fehlerbehebungen, insbesondere für nicht im Verlauf angezeigte Nachrichten. +Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/de-DE/changelogs/40105250.txt b/fastlane/metadata/android/de-DE/changelogs/40105250.txt new file mode 100644 index 0000000000..6fce28362c --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40105250.txt @@ -0,0 +1,2 @@ +Die wichtigsten Änderungen in dieser Version: Hauptsächlich Fehlerbehebungen, insbesondere für nicht im Verlauf angezeigte Nachrichten. +Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/de-DE/changelogs/40105260.txt b/fastlane/metadata/android/de-DE/changelogs/40105260.txt new file mode 100644 index 0000000000..71f4cbd484 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40105260.txt @@ -0,0 +1,2 @@ +Die wichtigsten Änderungen in dieser Version: Hauptsächlich Fehlerbehebungen. +Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/en-US/changelogs/40105240.txt b/fastlane/metadata/android/en-US/changelogs/40105240.txt new file mode 100644 index 0000000000..aaceef9ce6 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40105240.txt @@ -0,0 +1,2 @@ +Main changes in this version: Mainly bugfixing, in particular fix message not appearing on the timeline. +Full changelog: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/en-US/changelogs/40105250.txt b/fastlane/metadata/android/en-US/changelogs/40105250.txt new file mode 100644 index 0000000000..aaceef9ce6 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40105250.txt @@ -0,0 +1,2 @@ +Main changes in this version: Mainly bugfixing, in particular fix message not appearing on the timeline. +Full changelog: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/en-US/changelogs/40105260.txt b/fastlane/metadata/android/en-US/changelogs/40105260.txt new file mode 100644 index 0000000000..df2e163506 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40105260.txt @@ -0,0 +1,2 @@ +Main changes in this version: Mainly bugfixing. +Full changelog: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/en-US/changelogs/40105280.txt b/fastlane/metadata/android/en-US/changelogs/40105280.txt new file mode 100644 index 0000000000..df2e163506 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40105280.txt @@ -0,0 +1,2 @@ +Main changes in this version: Mainly bugfixing. +Full changelog: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/et/changelogs/40105240.txt b/fastlane/metadata/android/et/changelogs/40105240.txt new file mode 100644 index 0000000000..779a4428a6 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40105240.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: enamuses veaparandused, sh sõnumite kadumine ajajaoonelt. +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/et/changelogs/40105250.txt b/fastlane/metadata/android/et/changelogs/40105250.txt new file mode 100644 index 0000000000..779a4428a6 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40105250.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: enamuses veaparandused, sh sõnumite kadumine ajajaoonelt. +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/et/changelogs/40105260.txt b/fastlane/metadata/android/et/changelogs/40105260.txt new file mode 100644 index 0000000000..396e654160 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40105260.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: põhiliselt veaparandused. +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fa/changelogs/40105240.txt b/fastlane/metadata/android/fa/changelogs/40105240.txt new file mode 100644 index 0000000000..c6e3ddf8a9 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40105240.txt @@ -0,0 +1,2 @@ +تغییرات عمده در این نگارش: عموماً رفع اشکال. به طور خاص رفع نمایان نشدن پیام روی خط زمانی. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fa/changelogs/40105250.txt b/fastlane/metadata/android/fa/changelogs/40105250.txt new file mode 100644 index 0000000000..c6e3ddf8a9 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40105250.txt @@ -0,0 +1,2 @@ +تغییرات عمده در این نگارش: عموماً رفع اشکال. به طور خاص رفع نمایان نشدن پیام روی خط زمانی. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fa/changelogs/40105260.txt b/fastlane/metadata/android/fa/changelogs/40105260.txt new file mode 100644 index 0000000000..f3bb20cef8 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40105260.txt @@ -0,0 +1,2 @@ +تغییرات عمده در این نگارش: عمدتاً رفع اشکال. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fr-FR/changelogs/40105240.txt b/fastlane/metadata/android/fr-FR/changelogs/40105240.txt new file mode 100644 index 0000000000..187c8c8f72 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40105240.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : Principalement des corrections de bugs, notamment les messages non visibles dans l’historique +Intégralité des changements : https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fr-FR/changelogs/40105250.txt b/fastlane/metadata/android/fr-FR/changelogs/40105250.txt new file mode 100644 index 0000000000..187c8c8f72 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40105250.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : Principalement des corrections de bugs, notamment les messages non visibles dans l’historique +Intégralité des changements : https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fr-FR/changelogs/40105260.txt b/fastlane/metadata/android/fr-FR/changelogs/40105260.txt new file mode 100644 index 0000000000..2b066a3512 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40105260.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : Principalement des corrections de bugs. +Intégralité des changements : https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/hu-HU/changelogs/40105240.txt b/fastlane/metadata/android/hu-HU/changelogs/40105240.txt new file mode 100644 index 0000000000..592ef5e7ef --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/40105240.txt @@ -0,0 +1,2 @@ +Legnagyobb változtatás ebben a verzióban: Leginkább hibajavítások, mint amikor az üzenet nem jelenik meg az idővonalon. +Teljes változási napló: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/hu-HU/changelogs/40105250.txt b/fastlane/metadata/android/hu-HU/changelogs/40105250.txt new file mode 100644 index 0000000000..592ef5e7ef --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/40105250.txt @@ -0,0 +1,2 @@ +Legnagyobb változtatás ebben a verzióban: Leginkább hibajavítások, mint amikor az üzenet nem jelenik meg az idővonalon. +Teljes változási napló: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/hu-HU/changelogs/40105260.txt b/fastlane/metadata/android/hu-HU/changelogs/40105260.txt new file mode 100644 index 0000000000..9a73803dc4 --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/40105260.txt @@ -0,0 +1,2 @@ +Legnagyobb változtatás ebben a verzióban: Hibajavítások. +Teljes változási napló: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/id/changelogs/40105240.txt b/fastlane/metadata/android/id/changelogs/40105240.txt new file mode 100644 index 0000000000..d924da7ad6 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40105240.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Secara umum perbaikan kutu, terutama memperbaiki pesan-pesan tidak muncul di lini masa. +Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/id/changelogs/40105250.txt b/fastlane/metadata/android/id/changelogs/40105250.txt new file mode 100644 index 0000000000..d924da7ad6 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40105250.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Secara umum perbaikan kutu, terutama memperbaiki pesan-pesan tidak muncul di lini masa. +Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/id/changelogs/40105260.txt b/fastlane/metadata/android/id/changelogs/40105260.txt new file mode 100644 index 0000000000..329f1cc199 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40105260.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Kebanyakan perbaikan kutu. +Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/it-IT/changelogs/40105240.txt b/fastlane/metadata/android/it-IT/changelogs/40105240.txt new file mode 100644 index 0000000000..a3f49e5149 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40105240.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: correzione di errori, in particolare correzione dei messaggi che non comparivano nella linea temporale. +Cronologia completa: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/it-IT/changelogs/40105250.txt b/fastlane/metadata/android/it-IT/changelogs/40105250.txt new file mode 100644 index 0000000000..a3f49e5149 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40105250.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: correzione di errori, in particolare correzione dei messaggi che non comparivano nella linea temporale. +Cronologia completa: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/it-IT/changelogs/40105260.txt b/fastlane/metadata/android/it-IT/changelogs/40105260.txt new file mode 100644 index 0000000000..538dc33097 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40105260.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: correzioni di errori. +Cronologia completa: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40105240.txt b/fastlane/metadata/android/ja-JP/changelogs/40105240.txt new file mode 100644 index 0000000000..6feaf6245f --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40105240.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:バグの修正。特に、タイムラインにメッセージが表示されない不具合を修正。 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40105250.txt b/fastlane/metadata/android/ja-JP/changelogs/40105250.txt new file mode 100644 index 0000000000..6feaf6245f --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40105250.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:バグの修正。特に、タイムラインにメッセージが表示されない不具合を修正。 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sk/changelogs/40105240.txt b/fastlane/metadata/android/sk/changelogs/40105240.txt new file mode 100644 index 0000000000..5d293d4549 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40105240.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Hlavne opravy chýb, najmä oprava správy, ktorá sa nezobrazuje na časovej osi. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sk/changelogs/40105250.txt b/fastlane/metadata/android/sk/changelogs/40105250.txt new file mode 100644 index 0000000000..5d293d4549 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40105250.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Hlavne opravy chýb, najmä oprava správy, ktorá sa nezobrazuje na časovej osi. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sk/changelogs/40105260.txt b/fastlane/metadata/android/sk/changelogs/40105260.txt new file mode 100644 index 0000000000..f857b15b9f --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40105260.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Hlavne opravy chýb. +Celý zoznam zmien: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40105240.txt b/fastlane/metadata/android/sq/changelogs/40105240.txt new file mode 100644 index 0000000000..57b49e89c8 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40105240.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Kryesisht ndreqje të metash, veçanërisht ndreqje mosshfaqjeje mesazhesh te rrjedha kohore. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40105250.txt b/fastlane/metadata/android/sq/changelogs/40105250.txt new file mode 100644 index 0000000000..57b49e89c8 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40105250.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Kryesisht ndreqje të metash, veçanërisht ndreqje mosshfaqjeje mesazhesh te rrjedha kohore. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sv-SE/changelogs/40105240.txt b/fastlane/metadata/android/sv-SE/changelogs/40105240.txt new file mode 100644 index 0000000000..f6b6dd6f2c --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40105240.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i den här versionen: Huvudsakligen buggfixar, fixar speciellt meddelanden som inte visas i tidslinjen. +Full ändringslogg: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sv-SE/changelogs/40105250.txt b/fastlane/metadata/android/sv-SE/changelogs/40105250.txt new file mode 100644 index 0000000000..f6b6dd6f2c --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40105250.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i den här versionen: Huvudsakligen buggfixar, fixar speciellt meddelanden som inte visas i tidslinjen. +Full ändringslogg: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/uk/changelogs/40105240.txt b/fastlane/metadata/android/uk/changelogs/40105240.txt new file mode 100644 index 0000000000..c7e1351761 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40105240.txt @@ -0,0 +1,2 @@ +Основні зміни у цій версії: Переважно виправлення помилок, зокрема, виправлено повідомлення, що не з'являлися у стрічці. +Список усіх змін: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/uk/changelogs/40105250.txt b/fastlane/metadata/android/uk/changelogs/40105250.txt new file mode 100644 index 0000000000..c7e1351761 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40105250.txt @@ -0,0 +1,2 @@ +Основні зміни у цій версії: Переважно виправлення помилок, зокрема, виправлено повідомлення, що не з'являлися у стрічці. +Список усіх змін: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/uk/changelogs/40105260.txt b/fastlane/metadata/android/uk/changelogs/40105260.txt new file mode 100644 index 0000000000..608d8e7d37 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40105260.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: Виправлення помилок +Перелік усіх змін: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40100100.txt b/fastlane/metadata/android/zh-TW/changelogs/40100100.txt index 0ea092ba9a..982c4b3197 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40100100.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40100100.txt @@ -1,2 +1,2 @@ -這個新版本主要包含錯誤修復與改善。傳送訊息更快了。 -完整的變更紀錄請見:https://github.com/vector-im/element-android/releases/tag/v1.0.10 +新版本主要包含錯誤修復與改善。傳送訊息更快了。 +完整變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.0.10 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40100110.txt b/fastlane/metadata/android/zh-TW/changelogs/40100110.txt index 22dfe07097..20a1c1a8f9 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40100110.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40100110.txt @@ -1,2 +1,2 @@ -這個新版本主要包含使用者介面與使用者體驗改善。現在您可以邀請朋友,並透過掃描 QR code 來快速建立直接訊息了。 +新版本主要包含使用者介面與使用者體驗改善。現在您可以邀請朋友,並透過掃描 QR 碼來快速建立直接訊息了。 完整變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.0.11 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40100120.txt b/fastlane/metadata/android/zh-TW/changelogs/40100120.txt index 846126af63..efb372d583 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40100120.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40100120.txt @@ -1,2 +1,2 @@ -此版本中的主要變更:URL 預覽、新的表情符號鍵盤、新的聊天室設定功能以及聖誕節降雪! +此版本的主要變更:URL 預覽、新表情符號鍵盤、新聊天室設定功能以及聖誕節降雪! 完整變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.0.12 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40100130.txt b/fastlane/metadata/android/zh-TW/changelogs/40100130.txt index f42e9d3101..2909385827 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40100130.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40100130.txt @@ -1,2 +1,2 @@ -此版本中的主要變更:URL 預覽、新的表情符號鍵盤、新的聊天室設定功能以及聖誕節降雪! +此版本的主要變更:URL 預覽、新的表情符號鍵盤、新的聊天室設定功能以及聖誕節降雪! 完整變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.0.13 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40100140.txt b/fastlane/metadata/android/zh-TW/changelogs/40100140.txt index 9ed2152127..0bfed1ded5 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40100140.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40100140.txt @@ -1,2 +1,2 @@ -此版本的主要變動:編輯聊天室權限、自動淺色/深色佈景主題與許多臭蟲修復。 +此版本的主要變動:編輯聊天室權限、自動淺色/深色佈景主題與許多錯誤修復。 完整變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.0.14 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40100150.txt b/fastlane/metadata/android/zh-TW/changelogs/40100150.txt index 09a67d544b..4fd4f08ab8 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40100150.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40100150.txt @@ -1,2 +1,2 @@ -此版本的主要變動:社群網路登入支援。 +此版本的主要變動:支援社群網路登入。 完整變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.0.15 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40100160.txt b/fastlane/metadata/android/zh-TW/changelogs/40100160.txt index 77606636d3..07ab58e6fe 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40100160.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40100160.txt @@ -1,2 +1,2 @@ -此版本的主要變動:社群網路登入支援。 +此版本的主要變動:支援社群網路登入。 完整變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.0.15 以及 https://github.com/vector-im/element-android/releases/tag/v1.0.16 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40100170.txt b/fastlane/metadata/android/zh-TW/changelogs/40100170.txt index 35b8a54110..93c814a640 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40100170.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40100170.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:錯誤修復! +此版本的主要變動:錯誤修復! 完整變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.0.17 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40101020.txt b/fastlane/metadata/android/zh-TW/changelogs/40101020.txt index 90e76b074e..c4fc6abbd0 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40101020.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40101020.txt @@ -1,2 +1,2 @@ -此版本中的主要變更:效能改進與錯誤修復! +此版本的主要變更:效能改進與錯誤修復! 完整變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.1.2 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40101030.txt b/fastlane/metadata/android/zh-TW/changelogs/40101030.txt index c13d6ecfd4..ef69a00622 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40101030.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40101030.txt @@ -1,2 +1,2 @@ -此版本中的主要變更:效能改進與錯誤修復! +此版本的主要變更:效能改進與錯誤修復! 完整變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.1.3 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40101040.txt b/fastlane/metadata/android/zh-TW/changelogs/40101040.txt index 1786691c42..e6df797a05 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40101040.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40101040.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:效能改善與錯誤修復! +此版本的主要變動:效能改善與錯誤修復! 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.1.4 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40101050.txt b/fastlane/metadata/android/zh-TW/changelogs/40101050.txt index 899ce72c9a..6cd4319dc5 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40101050.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40101050.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:1.1.4 的快速修補 +此版本的主要變動:1.1.4 的熱修復 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.1.5 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40101060.txt b/fastlane/metadata/android/zh-TW/changelogs/40101060.txt index 838dc6d731..c3e8ff1a20 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40101060.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40101060.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:1.1.5 的快速修補 +此版本的主要變動:1.1.5 的熱修復 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.1.6 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40101070.txt b/fastlane/metadata/android/zh-TW/changelogs/40101070.txt index c62c7d5224..3e9d32268c 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40101070.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40101070.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:對「空間」的測試版支援。傳送前壓縮影片。 +此版本的主要變動:對「空間」的測試版支援。傳送前壓縮影片。 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.1.7 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40101080.txt b/fastlane/metadata/android/zh-TW/changelogs/40101080.txt index 4ed232fe70..37ef69ae6f 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40101080.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40101080.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:改善「空間」的功能。 +此版本的主要變動:改善「空間」功能。 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.1.8 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40101090.txt b/fastlane/metadata/android/zh-TW/changelogs/40101090.txt index 84e46bdd76..3df9fdbc45 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40101090.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40101090.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:新增對 gitter.im 網路的支援。 +此版本的主要變動:新增對 gitter.im 網路的支援。 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.1.9 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40101100.txt b/fastlane/metadata/android/zh-TW/changelogs/40101100.txt index 08e081fd8b..34c1cd3bdc 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40101100.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40101100.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:佈景主題與樣式更新,以及空間的新功能。 +此版本的主要變動:佈景主題與風格的更新,以及空間的新功能。 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.1.10 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40101110.txt b/fastlane/metadata/android/zh-TW/changelogs/40101110.txt index 91bbc18fff..092c695116 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40101110.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40101110.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:佈景主題與樣式更新,以及空間的新功能(1.1.10 的臭蟲修復版本) +此版本的主要變動:佈景主題與風格的更新,以及空間的新功能(1.1.10 的臭蟲修復版本) 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.1.11 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40101120.txt b/fastlane/metadata/android/zh-TW/changelogs/40101120.txt index 152173fe1b..4bfb315755 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40101120.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40101120.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:佈景主題與樣式更新,以及修復視訊通話後當機的問題 +此版本的主要變動:佈景主題與風格的更新,以及修復視訊通話後當機的問題 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.1.12 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40101130.txt b/fastlane/metadata/android/zh-TW/changelogs/40101130.txt index 88f439281f..f35789a222 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40101130.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40101130.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:主要是穩定性與臭蟲修復更新。 +此版本的主要變動:主要是穩定性與錯誤修復的更新。 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.1.13 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40101140.txt b/fastlane/metadata/android/zh-TW/changelogs/40101140.txt index 3eb5aa35ed..2a094dc292 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40101140.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40101140.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:修復關於加密訊息的問題。 +此版本的主要變動:修復關於加密訊息的問題。 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.1.14 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40101150.txt b/fastlane/metadata/android/zh-TW/changelogs/40101150.txt index c730151fe7..c87a69fb9e 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40101150.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40101150.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:實驗室設定下,語音訊息的實作。 +此版本的主要變動:實驗室設定下的語音訊息建置。 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.1.15 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40101160.txt b/fastlane/metadata/android/zh-TW/changelogs/40101160.txt index 364bec14b9..bf10f3425f 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40101160.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40101160.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:修復在聊天室中有人登出時傳送加密訊息所發生的問題。 +此版本的主要變動:修復聊天室中有人登出時傳送加密訊息所發生的問題。 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.1.16 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40102000.txt b/fastlane/metadata/android/zh-TW/changelogs/40102000.txt index 993a59c825..9248caf20c 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40102000.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40102000.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:語音訊息預設啟用。 +此版本的主要變動:語音訊息的預設為啟用。 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.2.0 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40102010.txt b/fastlane/metadata/android/zh-TW/changelogs/40102010.txt index b520266a78..6c5e63b960 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40102010.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40102010.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:對 VoIP 與空間功能的諸多改善(仍在測試中)。 +此版本的主要變動:對 VoIP 與空間功能的諸多改善(仍在測試中)。 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.2.1 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40103000.txt b/fastlane/metadata/android/zh-TW/changelogs/40103000.txt index fbae69cd21..6c9534077b 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40103000.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40103000.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:使用空間來整理您的聊天室! +此版本的主要變動:使用空間來整理您的聊天室! 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.3.0 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40103010.txt b/fastlane/metadata/android/zh-TW/changelogs/40103010.txt index 95dcd59e46..37ba0f53f8 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40103010.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40103010.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:使用空間來整理您的聊天室!v1.3.1 修復了在 v1.3.0 中遇到的當機問題。 +此版本的主要變動:使用空間來整理您的聊天室!v1.3.1 修復了在 v1.3.0 中遇到的當機問題。 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.3.1 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40103020.txt b/fastlane/metadata/android/zh-TW/changelogs/40103020.txt index 6a00bed1e7..5bdca7d2d5 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40103020.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40103020.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:新增對 Android Auto 的支援。以及許多錯誤修復! +此版本的主要變動:新增對 Android Auto 的支援。以及許多錯誤修復! 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.3.2 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40103030.txt b/fastlane/metadata/android/zh-TW/changelogs/40103030.txt index 7531d1d4a2..b4e477573e 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40103030.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40103030.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:讓身份伺服器政策在設定中可見。暫時移除 Android Auto 支援。 +此版本的主要變動:讓身分伺服器政策在設定中可見。暫時移除 Android Auto 支援。 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.3.3 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40103040.txt b/fastlane/metadata/android/zh-TW/changelogs/40103040.txt index bd82b54e45..d60363b798 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40103040.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40103040.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:為直接訊息聊天室新增 Presence 支援(請注意:此功能在 matrix.org 上停用)。加回 Android Auto 支援。 +此版本的主要變動:為直接訊息聊天室新增 Presence 支援(請注意:此功能在 matrix.org 上停用)。恢復對 Android Auto 的支援。 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.3.4 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40103050.txt b/fastlane/metadata/android/zh-TW/changelogs/40103050.txt index 659be479f5..37c3329178 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40103050.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40103050.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:為直接訊息聊天室新增 Presence 支援(請注意:此功能在 matrix.org 上停用)。加回 Android Auto 支援。 +此版本的主要變動:為直接訊息聊天室新增 Presence 支援(請注意:此功能在 matrix.org 上停用)。恢復對 Android Auto 的支援。 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.3.5 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40103060.txt b/fastlane/metadata/android/zh-TW/changelogs/40103060.txt index e1223a40e5..43caca7ed8 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40103060.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40103060.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:為直接訊息聊天室新增 Presence 支援(請注意:此功能在 matrix.org 上停用)。加回 Android Auto 支援。 +此版本的主要變動:為直接訊息聊天室新增 Presence 支援(請注意:此功能在 matrix.org 上停用)。加回 Android Auto 支援。 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.3.6 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40103070.txt b/fastlane/metadata/android/zh-TW/changelogs/40103070.txt index 1f9173fa1e..19dbaa623f 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40103070.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40103070.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:主要關於通知的臭蟲修復。 +此版本的主要變動:主要關於通知的錯誤修復。 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.3.7-RC2 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40103080.txt b/fastlane/metadata/android/zh-TW/changelogs/40103080.txt index 07689479a3..be0514e659 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40103080.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40103080.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:臭蟲修復! +此版本的主要變動:錯誤修復! 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.3.8 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40103090.txt b/fastlane/metadata/android/zh-TW/changelogs/40103090.txt index c74a27acbf..0ab8844b7e 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40103090.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40103090.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:新增對語音訊息草稿的支援。許多臭蟲修復! +此版本的主要變動:新增對語音訊息草稿的支援。許多錯誤修復! 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.3.9 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40103100.txt b/fastlane/metadata/android/zh-TW/changelogs/40103100.txt index 70d93e833d..17515e6710 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40103100.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40103100.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:新增對投票(在實驗室中)的支援。新的 URL 預覽設計。 +此版本的主要變動:新增(在實驗室中)對投票的支援。新的 URL 預覽設計。 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.3.10 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40103110.txt b/fastlane/metadata/android/zh-TW/changelogs/40103110.txt index d5450f4c6a..e0c8f48c4b 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40103110.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40103110.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:臭蟲修復! +此版本的主要變動:錯誤修復! 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.3.11 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40103120.txt b/fastlane/metadata/android/zh-TW/changelogs/40103120.txt index 0ee60318c1..3a102c6c4b 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40103120.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40103120.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:臭蟲修復! +此版本的主要變動:錯誤修復! 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.3.12 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40103130.txt b/fastlane/metadata/android/zh-TW/changelogs/40103130.txt index e0f9b47e16..0022bbc9b3 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40103130.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40103130.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:首次使用畫面的第一個變化,包含了選擇加入的分析功能。新增對數學活動的支援至實驗室中。 +此版本的主要變動:首次使用畫面的第一個變化,包含了選擇加入的分析功能。新增對數學活動的支援至實驗室中。 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.3.13 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40103140.txt b/fastlane/metadata/android/zh-TW/changelogs/40103140.txt index 8366a01265..9b9b234697 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40103140.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40103140.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:首次使用畫面的第一個變化,包含了選擇加入的分析功能。新增對數學活動的支援至實驗室中。 +此版本的主要變動:首次使用畫面的第一個變化,包含了選擇加入的分析功能。新增對數學活動的支援至實驗室中。 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.3.14 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40103150.txt b/fastlane/metadata/android/zh-TW/changelogs/40103150.txt index a6ddd5aa8c..e28e9aecc3 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40103150.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40103150.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:首次使用畫面的第一個變化,包含了選擇加入的分析功能。新增對數學活動的支援至實驗室中。 +此版本的主要變動:首次使用畫面的第一個變化,包含了選擇加入的分析功能。新增對數學活動的支援至實驗室中。 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.3.15 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40103160.txt b/fastlane/metadata/android/zh-TW/changelogs/40103160.txt index 04e51e013c..c44321d993 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40103160.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40103160.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:將您的位置傳送給任何聊天室。編輯投票。 +此版本的主要變動:將您的位置傳送給任何聊天室。編輯投票。 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.3.16 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40103170.txt b/fastlane/metadata/android/zh-TW/changelogs/40103170.txt index a8e353c2eb..257d8b0d8a 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40103170.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40103170.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:將您的位置傳送給任何聊天室。編輯投票。 +此版本的主要變動:將您的位置傳送給任何聊天室。編輯投票。 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.3.17 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40103180.txt b/fastlane/metadata/android/zh-TW/changelogs/40103180.txt index 3eabe8b7cc..e0fa18d687 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40103180.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40103180.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:將您的位置傳送給任何聊天室。編輯投票。 +此版本的主要變動:將您的位置傳送給任何聊天室。編輯投票。 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.3.18 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104000.txt b/fastlane/metadata/android/zh-TW/changelogs/40104000.txt index e61c235762..0870cb3787 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40104000.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40104000.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:訊息討論串的初始實作。訊息泡泡。 +此版本的主要變動:訊息討論串的初始建置。訊息泡泡。 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.4.0 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104020.txt b/fastlane/metadata/android/zh-TW/changelogs/40104020.txt index 92d0360bf1..77f570ce84 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40104020.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40104020.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:新增對 @room 的支援與未公開的投票,以及其他許多小變動。 +此版本的主要變動:新增對 @room 的支援與未公開的投票,以及其他許多小變動。 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.4.2 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104040.txt b/fastlane/metadata/android/zh-TW/changelogs/40104040.txt index 8949ec3486..fb4e0a62b8 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40104040.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40104040.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:輸入指示器使用者介面更新。許多臭蟲修復與穩定性改善。 +此版本的主要變動:輸入指示器使用者介面更新。修復多個錯誤與改善穩定性。 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.4.4 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104060.txt b/fastlane/metadata/android/zh-TW/changelogs/40104060.txt index 316fad3363..7e36657bcf 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40104060.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40104060.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:討論串時間軸現已更新,而且更快了。多個臭蟲修復與穩定性改善。 +此版本的主要變動:討論串時間軸現已上線,而且更快了。修復多個錯誤與改善穩定性。 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.4.6 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104070.txt b/fastlane/metadata/android/zh-TW/changelogs/40104070.txt index 2cd9da666e..7c5ade2085 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40104070.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40104070.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:多個臭蟲修復與穩定性改善。 +此版本的主要變動:修復多個錯誤與改善穩定性。 完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.4.7 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104080.txt b/fastlane/metadata/android/zh-TW/changelogs/40104080.txt index c036aa7d56..f3be961844 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40104080.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40104080.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:討論串時間軸現已更新,而且更快了。多個臭蟲修復與穩定性改善。 +此版本的主要變動:討論串時間軸現已上線,而且更快了。修復多個錯誤與改善穩定性。 完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104100.txt b/fastlane/metadata/android/zh-TW/changelogs/40104100.txt index c78ed7dd2d..9990a2d64b 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40104100.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40104100.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:捲動音訊訊息。多個臭蟲修復與穩定性改善。 +此版本的主要變動:捲動語音訊息。修復多個錯誤與改善穩定性。 完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104110.txt b/fastlane/metadata/android/zh-TW/changelogs/40104110.txt index 4bcca9a0b8..af2e29cf1c 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40104110.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40104110.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:多個臭蟲修復與穩定性改善。 +此版本的主要變動:修復多個錯誤與改善穩定性。 完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104120.txt b/fastlane/metadata/android/zh-TW/changelogs/40104120.txt index d3d48abab9..0b22b32ef0 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40104120.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40104120.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:允許使用者顯示為離線並為音訊附件新增音訊播放器 +此版本的主要變動:允許使用者顯示為離線並為音訊附件新增音訊播放器 完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104130.txt b/fastlane/metadata/android/zh-TW/changelogs/40104130.txt index d3d48abab9..0b22b32ef0 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40104130.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40104130.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:允許使用者顯示為離線並為音訊附件新增音訊播放器 +此版本的主要變動:允許使用者顯示為離線並為音訊附件新增音訊播放器 完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104140.txt b/fastlane/metadata/android/zh-TW/changelogs/40104140.txt index ff830dab7c..f0c5fa8268 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40104140.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40104140.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:改善被忽略使用者的管理。多個臭蟲修復與穩定性改善。 +此版本的主要變動:改善被忽略使用者的管理。修復多個錯誤與改善穩定性。 完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104160.txt b/fastlane/metadata/android/zh-TW/changelogs/40104160.txt index 0e64d36868..e2fd94140c 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40104160.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40104160.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:對於被加密的訊息有更好的管理方式。多個臭蟲修復與穩定性改善。 +此版本的主要變動:對於被加密的訊息有更好的管理方式。修復多個錯誤與改善穩定性。 完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104180.txt b/fastlane/metadata/android/zh-TW/changelogs/40104180.txt index 4bcca9a0b8..af2e29cf1c 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40104180.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40104180.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:多個臭蟲修復與穩定性改善。 +此版本的主要變動:修復多個錯誤與改善穩定性。 完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104190.txt b/fastlane/metadata/android/zh-TW/changelogs/40104190.txt index 4bcca9a0b8..af2e29cf1c 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40104190.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40104190.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:多個臭蟲修復與穩定性改善。 +此版本的主要變動:修復多個錯誤與改善穩定性。 完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104200.txt b/fastlane/metadata/android/zh-TW/changelogs/40104200.txt index 4bcca9a0b8..af2e29cf1c 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40104200.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40104200.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:多個臭蟲修復與穩定性改善。 +此版本的主要變動:修復多個錯誤與改善穩定性。 完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104220.txt b/fastlane/metadata/android/zh-TW/changelogs/40104220.txt index 4bcca9a0b8..af2e29cf1c 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40104220.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40104220.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:多個臭蟲修復與穩定性改善。 +此版本的主要變動:修復多個錯誤與改善穩定性。 完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104230.txt b/fastlane/metadata/android/zh-TW/changelogs/40104230.txt index 4bcca9a0b8..af2e29cf1c 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40104230.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40104230.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:多個臭蟲修復與穩定性改善。 +此版本的主要變動:修復多個錯誤與改善穩定性。 完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104240.txt b/fastlane/metadata/android/zh-TW/changelogs/40104240.txt index 4bcca9a0b8..af2e29cf1c 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40104240.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40104240.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:多個臭蟲修復與穩定性改善。 +此版本的主要變動:修復多個錯誤與改善穩定性。 完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104250.txt b/fastlane/metadata/android/zh-TW/changelogs/40104250.txt index 4bcca9a0b8..af2e29cf1c 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40104250.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40104250.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:多個臭蟲修復與穩定性改善。 +此版本的主要變動:修復多個錯誤與改善穩定性。 完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104260.txt b/fastlane/metadata/android/zh-TW/changelogs/40104260.txt index 7569b4f491..ff3722effc 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40104260.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40104260.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:使用 UnifiedPush 並允許使用者在沒有 FCM 的情況下推送。 +此版本的主要變動:使用 UnifiedPush 並允許使用者在沒有 FCM 的情況下推送。 完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104270.txt b/fastlane/metadata/android/zh-TW/changelogs/40104270.txt index 4bcca9a0b8..af2e29cf1c 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40104270.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40104270.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:多個臭蟲修復與穩定性改善。 +此版本的主要變動:修復多個錯誤與改善穩定性。 完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104280.txt b/fastlane/metadata/android/zh-TW/changelogs/40104280.txt index 4bcca9a0b8..af2e29cf1c 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40104280.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40104280.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:多個臭蟲修復與穩定性改善。 +此版本的主要變動:修復多個錯誤與改善穩定性。 完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104300.txt b/fastlane/metadata/android/zh-TW/changelogs/40104300.txt index 3055389b2b..4576d13849 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40104300.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40104300.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:啟用改善的登入與註冊流程。 +此版本的主要變動:啟用改善的登入與註冊流程。 完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104310.txt b/fastlane/metadata/android/zh-TW/changelogs/40104310.txt index 3055389b2b..4576d13849 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40104310.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40104310.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:啟用改善的登入與註冊流程。 +此版本的主要變動:啟用改善的登入與註冊流程。 完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104320.txt b/fastlane/metadata/android/zh-TW/changelogs/40104320.txt index 4bcca9a0b8..af2e29cf1c 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40104320.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40104320.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:多個臭蟲修復與穩定性改善。 +此版本的主要變動:修復多個錯誤與改善穩定性。 完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104340.txt b/fastlane/metadata/android/zh-TW/changelogs/40104340.txt index 4bcca9a0b8..af2e29cf1c 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40104340.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40104340.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:多個臭蟲修復與穩定性改善。 +此版本的主要變動:修復多個錯誤與改善穩定性。 完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40105000.txt b/fastlane/metadata/android/zh-TW/changelogs/40105000.txt index 7ab6a7a7bf..8b7e371d4f 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40105000.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40105000.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:預設啟用延遲直接訊息。 +此版本的主要變動:預設啟用延遲直接訊息。 完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40105020.txt b/fastlane/metadata/android/zh-TW/changelogs/40105020.txt index d83fd08a53..9112ae8f09 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40105020.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40105020.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:預設啟用新的應用程式佈局! +此版本的主要變動:預設啟用新的應用程式佈局! 完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40105040.txt b/fastlane/metadata/android/zh-TW/changelogs/40105040.txt index b35b1185b9..a0358632d4 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40105040.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40105040.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:實驗室設定下有新功能:格式化文字編輯器、新裝置管理、語音廣播。仍在積極開發中! +此版本的主要變動:實驗室設定下有新功能:富文本編輯器、新裝置管理、語音廣播。仍在積極開發中! 完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40105060.txt b/fastlane/metadata/android/zh-TW/changelogs/40105060.txt index 56667ccfc0..ac30daf3bf 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40105060.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40105060.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:選取附件的新使用者介面。 +此版本的主要變動:選取附件的新使用者介面。 完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40105070.txt b/fastlane/metadata/android/zh-TW/changelogs/40105070.txt index 56667ccfc0..ac30daf3bf 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40105070.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40105070.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:選取附件的新使用者介面。 +此版本的主要變動:選取附件的新使用者介面。 完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40105080.txt b/fastlane/metadata/android/zh-TW/changelogs/40105080.txt index 2a368ec8be..58176c43b8 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40105080.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40105080.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:臭蟲修復與改善。 +此版本的主要變動:修復錯誤與改善。 完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40105100.txt b/fastlane/metadata/android/zh-TW/changelogs/40105100.txt index 20341b84fe..4d1cbe2c2f 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40105100.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40105100.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:格式化文字編輯器的全螢幕模式新實作與臭蟲修復。 +此版本的主要變動:富文本編輯器全螢幕模式的全新建置與錯誤修復。 完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40105110.txt b/fastlane/metadata/android/zh-TW/changelogs/40105110.txt index 20341b84fe..37efa35f58 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40105110.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40105110.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:格式化文字編輯器的全螢幕模式新實作與臭蟲修復。 +此版本的主要變動:富文本編輯器全螢幕模式全新建置與錯誤修復。 完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40105120.txt b/fastlane/metadata/android/zh-TW/changelogs/40105120.txt index 9c66f3c2ad..4d6cd8c2ff 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/40105120.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/40105120.txt @@ -1,2 +1,2 @@ -此版本中的主要變動:討論串現在預設啟用。 +此版本的主要變動:預設開啟對話串功能。 完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40105240.txt b/fastlane/metadata/android/zh-TW/changelogs/40105240.txt new file mode 100644 index 0000000000..401877b730 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40105240.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:主要為臭蟲修復,特別是修復訊息不會出現在時間軸中的問題。 +完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40105250.txt b/fastlane/metadata/android/zh-TW/changelogs/40105250.txt new file mode 100644 index 0000000000..401877b730 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40105250.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:主要為臭蟲修復,特別是修復訊息不會出現在時間軸中的問題。 +完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40105260.txt b/fastlane/metadata/android/zh-TW/changelogs/40105260.txt new file mode 100644 index 0000000000..5346472634 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40105260.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:主要是臭蟲修復。 +完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/full_description.txt b/fastlane/metadata/android/zh-TW/full_description.txt index eda511c4af..99a711e34a 100644 --- a/fastlane/metadata/android/zh-TW/full_description.txt +++ b/fastlane/metadata/android/zh-TW/full_description.txt @@ -2,7 +2,7 @@ Element 同時是安全的通訊軟體,也是生產力團隊協作應用程式 Element 的功能包含了: - 進階線上通訊工具 -- 完全加密的訊息,即使對於遠端工作者,也可以有更安全的公司通訊 +- 完全加密的訊息,即使對於遠端工作者,也可以有更安全的企業通訊 - 以 Matrix 開放原始碼框架為基礎的去中心化的聊天 - 在管理專案時透過加密資料安全地分享檔案 - 包含了 VoIP 與畫面分享的視訊聊天 @@ -11,15 +11,15 @@ Element 同時是安全的通訊軟體,也是生產力團隊協作應用程式 Element 與其他訊息傳遞與協作應用程式完全不同。它在 Matrix(一個用於安全傳遞訊息與去中心化通訊的開放網路)上執行。其可以自架,讓使用者對他們的資料與訊息有最大的所有權與控制權。 隱私與加密訊息傳遞 -Element 保護您不受不想要的廣告、資料挖礦與圍牆花園侵擾。其也透過端到端加密與交叉簽章裝置驗證保護了您所有的資料,並提供一對一視訊以及語音通訊。 +Element 保護您不受不想要的廣告、資料探勘與圍牆花園侵擾。其也透過端到端加密與交叉簽章裝置驗證保護了您所有的資料,並提供一對一視訊以及語音通訊。 Element 透過與其他商業協作工具,如 Slack 等應用程式整合,讓您可以在控制您的隱私的同時,也可以與 Matrix 網路上的任何人安全地通訊。 Element 可以自架 -為了可以完整控制您的敏感資料與對話,Element 可以自架,您也可以選擇任何以 Matrix 為基礎的服務提供商,開放原始碼、去中心化的通訊標準。Element 為您提供隱私、安全合規與整合活性。 +為了可以完整控制您的敏感資料與對話,Element 可以自架,您也可以選擇任何以 Matrix 為基礎的服務提供商,來使用其開放原始碼、去中心化的通訊標準。Element 為您提供隱私、安全合規與整合活性。 擁有您的資料 -您可以決定將您的資料與訊息儲存在何處。沒有資料挖礦或被第三方存取的風險。 +您可以決定將您的資料與訊息儲存在何處。沒有資料探勘或被第三方存取的風險。 Element 透過不同的方式讓您掌控一切: 1. 在 Matrix 開發者架設的 matrix.org 公開伺服器上取得免費帳號,或是從數千個由志願者架設的公開伺服器中選擇 @@ -30,13 +30,13 @@ Element 透過不同的方式讓您掌控一切: 您可以與 Matrix 網路上的任何人聊天,不論他們是使用 Element、其他 Matrix 應用程式或其他通訊應用程式。 超級安全 -真的端到端加密(僅有那些在對話中的可以解密訊息)以及交叉簽章裝置驗證。 +真正的端到端加密(僅有那些在對話中的使用者可以解密訊息)以及交叉簽章裝置驗證。 -完整的通訊與整合Complete communication and integration +完整的通訊與整合 訊息傳遞、語音與視訊通話、檔案分享、畫面分享與超多的整合、機器人與小工具。建構聊天室、社群、保持聯絡並完成工作。 從上次離開的地方開始 -無論您身在何處,都可以透過在您所有裝置與網頁 https://app.element.io 間完全同步的訊息歷史保持聯絡 +無論您身在何處,都可以透過在您所有裝置與網頁 https://app.element.io 間完全同步的訊息歷史保持聯絡。 開放原始碼 -Android 版的 Element 是開放原始碼專案,託管於 GitHub 上。請在 https://github.com/vector-im/element-android 上回報臭蟲及/或貢獻其開發 +Android 版的 Element 是開放原始碼專案,託管於 GitHub 上。請在 https://github.com/vector-im/element-android 上回報錯誤及/或貢獻其開發 diff --git a/fastlane/metadata/android/zh-TW/short_description.txt b/fastlane/metadata/android/zh-TW/short_description.txt index 0d1f5bb7cd..3e13097738 100644 --- a/fastlane/metadata/android/zh-TW/short_description.txt +++ b/fastlane/metadata/android/zh-TW/short_description.txt @@ -1 +1 @@ -群組通訊軟體 - 訊息加密、群組聊天與視訊通話 +群組通訊軟體 - 加密訊息、群組聊天與視訊通話 diff --git a/library/core-utils/src/main/java/im/vector/lib/core/utils/timer/CountUpTimer.kt b/library/core-utils/src/main/java/im/vector/lib/core/utils/timer/CountUpTimer.kt index 3ed63a407b..369e96682a 100644 --- a/library/core-utils/src/main/java/im/vector/lib/core/utils/timer/CountUpTimer.kt +++ b/library/core-utils/src/main/java/im/vector/lib/core/utils/timer/CountUpTimer.kt @@ -34,11 +34,15 @@ class CountUpTimer( private val lastTime: AtomicLong = AtomicLong(clock.epochMillis()) private val elapsedTime: AtomicLong = AtomicLong(0) + // To ensure that the regular tick value is an exact multiple of `intervalInMs` + private val specialRound = SpecialRound(intervalInMs) + private fun startCounter() { + counterJob?.cancel() counterJob = coroutineScope.launch { while (true) { delay(intervalInMs - elapsedTime() % intervalInMs) - tickListener?.onTick(elapsedTime()) + tickListener?.onTick(specialRound.round(elapsedTime())) } } } @@ -54,29 +58,54 @@ class CountUpTimer( } } + /** + * Start a new timer with the initial given time, if any. + * If the timer is already started, it will be restarted. + */ fun start(initialTime: Long = 0L) { elapsedTime.set(initialTime) - resume() - } - - fun pause() { - tickListener?.onTick(elapsedTime()) - counterJob?.cancel() - counterJob = null - } - - fun resume() { lastTime.set(clock.epochMillis()) startCounter() } + /** + * Pause the timer at the current time. + */ + fun pause() { + pauseAndTick() + } + + /** + * Resume the timer from the current time. + * Does nothing if the timer is already running. + */ + fun resume() { + if (counterJob?.isActive != true) { + lastTime.set(clock.epochMillis()) + startCounter() + } + } + + /** + * Stop and reset the timer. + */ fun stop() { - tickListener?.onTick(elapsedTime()) - counterJob?.cancel() - counterJob = null + pauseAndTick() elapsedTime.set(0L) } + private fun pauseAndTick() { + if (counterJob?.isActive == true) { + // get the elapsed time before cancelling the timer + val elapsedTime = elapsedTime() + // cancel the timer before ticking + counterJob?.cancel() + counterJob = null + // tick with the computed elapsed time + tickListener?.onTick(elapsedTime) + } + } + fun interface TickListener { fun onTick(milliseconds: Long) } diff --git a/library/core-utils/src/main/java/im/vector/lib/core/utils/timer/SpecialRound.kt b/library/core-utils/src/main/java/im/vector/lib/core/utils/timer/SpecialRound.kt new file mode 100644 index 0000000000..82fead13e0 --- /dev/null +++ b/library/core-utils/src/main/java/im/vector/lib/core/utils/timer/SpecialRound.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.lib.core.utils.timer + +import kotlin.math.round + +class SpecialRound(private val step: Long) { + /** + * Round the provided value to the nearest multiple of `step`. + */ + fun round(value: Long): Long { + return round(value.toDouble() / step).toLong() * step + } +} diff --git a/library/core-utils/src/test/java/im/vector/lib/core/utils/timer/CountUpTimerTest.kt b/library/core-utils/src/test/java/im/vector/lib/core/utils/timer/CountUpTimerTest.kt index 83f11900b1..27d07bba66 100644 --- a/library/core-utils/src/test/java/im/vector/lib/core/utils/timer/CountUpTimerTest.kt +++ b/library/core-utils/src/test/java/im/vector/lib/core/utils/timer/CountUpTimerTest.kt @@ -19,6 +19,8 @@ package im.vector.lib.core.utils.timer import im.vector.lib.core.utils.test.fakes.FakeClock import io.mockk.every import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify import io.mockk.verifySequence import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceTimeBy @@ -36,6 +38,7 @@ internal class CountUpTimerTest { @Test fun `when pausing and resuming the timer, the timer ticks the right values at the right moments`() = runTest { + // Given every { fakeClock.epochMillis() } answers { currentTime } val tickListener = mockk(relaxed = true) val timer = CountUpTimer( @@ -44,6 +47,7 @@ internal class CountUpTimerTest { intervalInMs = AN_INTERVAL, ).also { it.tickListener = tickListener } + // When timer.start() advanceTimeBy(AN_INTERVAL / 2) // no tick timer.pause() // tick @@ -52,6 +56,7 @@ internal class CountUpTimerTest { advanceTimeBy(AN_INTERVAL * 4) // tick * 4 timer.stop() // tick + // Then verifySequence { tickListener.onTick(AN_INTERVAL / 2) tickListener.onTick(AN_INTERVAL) @@ -64,6 +69,7 @@ internal class CountUpTimerTest { @Test fun `given an initial time, the timer ticks the right values at the right moments`() = runTest { + // Given every { fakeClock.epochMillis() } answers { currentTime } val tickListener = mockk(relaxed = true) val timer = CountUpTimer( @@ -72,6 +78,7 @@ internal class CountUpTimerTest { intervalInMs = AN_INTERVAL, ).also { it.tickListener = tickListener } + // When timer.start(AN_INITIAL_TIME) advanceTimeBy(AN_INTERVAL) // tick timer.pause() // tick @@ -80,6 +87,7 @@ internal class CountUpTimerTest { advanceTimeBy(AN_INTERVAL * 4) // tick * 4 timer.stop() // tick + // Then val offset = AN_INITIAL_TIME % AN_INTERVAL verifySequence { tickListener.onTick(AN_INITIAL_TIME + AN_INTERVAL - offset) @@ -91,4 +99,54 @@ internal class CountUpTimerTest { tickListener.onTick(AN_INITIAL_TIME + AN_INTERVAL * 5) } } + + @Test + fun `when stopping the timer on tick, the stop action is called twice and the timer ticks twice`() = runTest { + // Given + every { fakeClock.epochMillis() } answers { currentTime } + val timer = spyk( + CountUpTimer( + coroutineScope = this, + clock = fakeClock, + intervalInMs = AN_INTERVAL, + ) + ) + val tickListener = mockk { + every { onTick(any()) } answers { timer.stop() } + } + timer.tickListener = tickListener + + // When + timer.start() + advanceTimeBy(AN_INTERVAL * 10) + + // Then + verify(exactly = 2) { timer.stop() } // one call at the first tick, a second time because of the tick of the first stop + verify(exactly = 2) { tickListener.onTick(any()) } // one after reaching the first interval, a second after the stop action + } + + @Test + fun `when pausing the timer on tick, the pause action is called twice and the timer ticks twice`() = runTest { + // Given + every { fakeClock.epochMillis() } answers { currentTime } + val timer = spyk( + CountUpTimer( + coroutineScope = this, + clock = fakeClock, + intervalInMs = AN_INTERVAL, + ) + ) + val tickListener = mockk { + every { onTick(any()) } answers { timer.pause() } + } + timer.tickListener = tickListener + + // When + timer.start() + advanceTimeBy(AN_INTERVAL * 10) + + // Then + verify(exactly = 2) { timer.pause() } // one call at the first tick, a second time because of the tick of the first pause + verify(exactly = 2) { tickListener.onTick(any()) } // one after reaching the first interval, a second after the pause action + } } diff --git a/library/core-utils/src/test/java/im/vector/lib/core/utils/timer/SpecialRoundTest.kt b/library/core-utils/src/test/java/im/vector/lib/core/utils/timer/SpecialRoundTest.kt new file mode 100644 index 0000000000..c41557ee2f --- /dev/null +++ b/library/core-utils/src/test/java/im/vector/lib/core/utils/timer/SpecialRoundTest.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.lib.core.utils.timer + +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test + +class SpecialRoundTest { + @Test + fun `test special round 500`() { + val sut = SpecialRound(500) + sut.round(1) shouldBeEqualTo 0 + sut.round(499) shouldBeEqualTo 500 + sut.round(500) shouldBeEqualTo 500 + sut.round(501) shouldBeEqualTo 500 + sut.round(999) shouldBeEqualTo 1_000 + sut.round(1000) shouldBeEqualTo 1_000 + sut.round(1001) shouldBeEqualTo 1_000 + sut.round(1499) shouldBeEqualTo 1_500 + sut.round(1500) shouldBeEqualTo 1_500 + sut.round(1501) shouldBeEqualTo 1_500 + } + + @Test + fun `test special round 1_000`() { + val sut = SpecialRound(1_000) + sut.round(1) shouldBeEqualTo 0 + sut.round(499) shouldBeEqualTo 0 + sut.round(500) shouldBeEqualTo 0 + sut.round(501) shouldBeEqualTo 1_000 + sut.round(999) shouldBeEqualTo 1_000 + sut.round(1000) shouldBeEqualTo 1_000 + sut.round(1001) shouldBeEqualTo 1_000 + sut.round(1499) shouldBeEqualTo 1_000 + sut.round(1500) shouldBeEqualTo 2_000 + sut.round(1501) shouldBeEqualTo 2_000 + } +} diff --git a/library/external/jsonviewer/build.gradle b/library/external/jsonviewer/build.gradle index a5d297b860..7b3b62c082 100644 --- a/library/external/jsonviewer/build.gradle +++ b/library/external/jsonviewer/build.gradle @@ -65,7 +65,7 @@ dependencies { implementation libs.jetbrains.coroutinesCore implementation libs.jetbrains.coroutinesAndroid - testImplementation 'org.json:json:20220924' + testImplementation 'org.json:json:20230227' testImplementation libs.tests.junit androidTestImplementation libs.androidx.junit androidTestImplementation libs.androidx.espressoCore diff --git a/library/ui-strings/src/main/res/values-ca/strings.xml b/library/ui-strings/src/main/res/values-ca/strings.xml index 1f2cf1983c..153036da0f 100644 --- a/library/ui-strings/src/main/res/values-ca/strings.xml +++ b/library/ui-strings/src/main/res/values-ca/strings.xml @@ -1160,7 +1160,7 @@ Tema "Tema: " Afegeix un tema - %s perquè la gent sàpiga de que tracta la sala. + %s perquè la gent sàpiga de què tracta la sala. Tema Tema de la sala (opcional) Esperant l\'històric xifrat @@ -2808,7 +2808,7 @@ Activa l\'editor de text enriquit Rep notificacions en aquesta sessió. Notificacions - Carregant + Carregant… Pausa l\'emissió de veu Reprodueix o reprèn l\'emissió de veu Atura l\'enregistrament d\'emissió de veu @@ -2833,4 +2833,19 @@ Format de text Enrere 30 segons Avança 30 segons - + No es pot iniciar la nova l\'emissió de veu + Emissió en directe + No es pot iniciar el missatge de veu + Comprova-ho per assegurar que el teu compte és segur + Tens sessions no verificades + Històric de l\'enquesta + Emissió de veu iniciada + Citant + Responent a %s + Editant + Mostra els xats recents al menú de compartició del sistema + Activa la compartició directa + Has finalitzat una emissió de veu. + %1$s a finalitzat una emissió de veu. + Sí, atura + \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-cs/strings.xml b/library/ui-strings/src/main/res/values-cs/strings.xml index b1f7df9bb4..00303591b7 100644 --- a/library/ui-strings/src/main/res/values-cs/strings.xml +++ b/library/ui-strings/src/main/res/values-cs/strings.xml @@ -2980,4 +2980,13 @@ Nelze spustit hlasovou zprávu Chyba připojení - nahrávání pozastaveno Použít formát inline kódu + Přepnout blok kódu + Přepnout citaci + Zrušit odsazení + Odsazení + Zobrazit hlasování na časové ose + Nelze dešifrovat toto hlasové vysílání. + Údaje o vašem účtu jsou spravovány odděleně na adrese %1$s. + Účet + Při aktualizaci předvoleb oznámení došlo k chybě. Zkuste to prosím znovu. \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-de/strings.xml b/library/ui-strings/src/main/res/values-de/strings.xml index 06c477b410..971344a863 100644 --- a/library/ui-strings/src/main/res/values-de/strings.xml +++ b/library/ui-strings/src/main/res/values-de/strings.xml @@ -941,7 +941,7 @@ Url: Nutzungsbedingungen Für andere auffindbar sein - Verwende Bots, Bridges, Widgets und Sticker-Pakete + Nutze Bots, Brücken, Widgets und Sticker-Pakete Identitäts-Server Verbindung zum Identitäts-Server trennen Identitäts-Server konfigurieren @@ -2783,7 +2783,7 @@ Öffne die App auf deinem anderen Gerät Die Anfrage wurde auf dem anderen Gerät abgelehnt. Die Verbindung konnte nicht in der erforderlichen Zeit hergestellt werden. - Verbindung mit diesem Gerät nicht unterstützt. + Die Verbindung mit diesem Gerät wird nicht unterstützt. Verbindung fehlgeschlagen Überprüfe dein angemeldetes Gerät. Der unten gezeigte Code sollte angezeigt werden. Bestätige, dass beide Codes übereinstimmen: Sichere Verbindung hergestellt @@ -2919,4 +2919,13 @@ Kann Sprachnachricht nicht beginnen Verbindungsfehler − Aufnahme pausiert Als Inline-Code formatieren + Einrücken + Umfrage im Verlauf anzeigen + Entschlüsseln der Sprachübertragung nicht möglich. + Zitat umschalten + Nicht einrücken + Codeblock umschalten + Konto + Deine Kontodetails werden separat verwaltet bei %1$s. + Ein Fehler ist während der Aktualisierung deiner Benachrichtigungseinstellungen aufgetreten. Bitte versuche es erneut. \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-enm/strings.xml b/library/ui-strings/src/main/res/values-enm/strings.xml new file mode 100644 index 0000000000..a6b3daec93 --- /dev/null +++ b/library/ui-strings/src/main/res/values-enm/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-et/strings.xml b/library/ui-strings/src/main/res/values-et/strings.xml index 0f005fe04e..8e304efbda 100644 --- a/library/ui-strings/src/main/res/values-et/strings.xml +++ b/library/ui-strings/src/main/res/values-et/strings.xml @@ -2891,7 +2891,7 @@ Sinu koduserver veel ei toeta jutulõngade loendit. Alustasime ringhäälingukõnega Selle ringhäälingukõne esitamine ei õnnestu. - Krüptimisvigade tõttu jääb osa hääli lugemata + Dekrüptimisvigade tõttu jääb osa hääli lugemata Möödunud päevas polnud ühtegi toimumas olnud küsitlust. \nVarasemate päevade vaatamiseks laadi veel küsitlusi. @@ -2911,4 +2911,13 @@ Kuna sa hetkel salvestad ringhäälingukõnet, siis häälsõnumi salvestamine või esitamine ei õnnestu. Selleks palun lõpeta ringhäälingukõne Viga võrguühenduses - salvestamine on peatatud Kasuta lõimitud koodi vormingut + Kasutajakonto + Sinu kasutajakonto andmeid hallatakse siin: %1$s. + Selle ringhäälingukõne dekrüptimine ei õnnestu. + Näita küsitlust ajajoonel + Lisa taandrida + Eemalda taandrida + Lülita tsiteerimine sisse/välja + Lülita koodiblokk sisse/välja + Sinu teavituste seadistuste muutmisel tekkis viga. Palu proovi uuesti. \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-fa/strings.xml b/library/ui-strings/src/main/res/values-fa/strings.xml index 9b1d367dde..8e15009c81 100644 --- a/library/ui-strings/src/main/res/values-fa/strings.xml +++ b/library/ui-strings/src/main/res/values-fa/strings.xml @@ -191,7 +191,7 @@ تنظیمات گزارش اشکال در حال بارگذاری… - باشه + قبول لغو ذخیره ترک کردن @@ -2920,4 +2920,13 @@ نمی‌توان پخش صوتی را آغاز کرد خطای اتّصال - ضبط مکث شد اعمال قالب کد درون‌خط + تغییر حالت بلوک کد + تغییر حالت نقل قول + تونرفتگی + تورفتگی + دیدن نظرسنجی در خط زمانی + حساب + ناتوان در رمزگشایی این پخش صوتی. + جزییات حسابتان جداگانه در %1$s مدیریت می‌شود. + هنگام به‌روز رسانی ترجیحات آگاهیتان خطایی رخ داد. لطفاً دوباره تلاش کنید. \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-fi/strings.xml b/library/ui-strings/src/main/res/values-fi/strings.xml index 66f333845f..96408da021 100644 --- a/library/ui-strings/src/main/res/values-fi/strings.xml +++ b/library/ui-strings/src/main/res/values-fi/strings.xml @@ -88,8 +88,8 @@ Peruutit eston %1$s. Syy: %2$s Poistit käyttäjän %1$s. Syy: %2$s Hylkäsit kutsun. Syy: %1$s - Lähdit. Syy: %1$s - %1$s lähti. Syy: %2$s + Poistuit. Syy: %1$s + %1$s poistui. Syy: %2$s Poistuit huoneesta. Syy: %1$s Liityit. Syy: %1$s %1$s liittyi. Syy: %2$s @@ -139,9 +139,9 @@ Päivitit tässä. %s päivitti täällä. Päivitit tämän huoneen. - Teit tulevista viesteistä näkyviä käyttäjälle %1$s - %1$s teki tulevista viesteistä näkyviä käyttäjälle %2$s - Teit tulevan huonehistorian näkyväksi %1$s + Teit tulevista viesteistä näkyviä seuraaville: %1$s + %1$s teki tulevista viesteistä näkyviä seuraaville: %2$s + Teit tulevan huonehistorian näkyväksi seuraaville: %1$s Lopetit puhelun. Vastasit puheluun. Lähetit tietoja puhelun valmistelemiseksi. @@ -168,7 +168,7 @@ Liityit %1$s liittyi Liityit huoneeseen - Kutsuit %1$s + Kutsuit käyttäjän %1$s Loit keskustelun %1$s loi keskustelun Loit huoneen @@ -1311,7 +1311,7 @@ QR-koodin skannaaminen vaatii luvan kameran käyttöön. Kysy varmistusta ennen puhelun aloittamista Estä vahinkopuhelut - SSL Virhe. + SSL-virhe. Takakamera Etukamera Vaihda kameraa @@ -2119,8 +2119,8 @@ Vahvistetu Suodata - Käyttämättä %1$d päivän tai pidempään - Käyttämättä %1$d päivää tai pidempään + Käyttämättä vuorokauden tai pidempään + Käyttämättä %1$d vuorokautta tai pidempään Käyttämätön Ei valmis turvallista viestintää varten @@ -2136,7 +2136,7 @@ Käyttämättä olevat istunnot Vahvista nämä istunnot tai kirjaudu niistä ulos. Vahvistamattomat istunnot - Paranna tilisi turvallisuutta seuraamalla näitä suosituksia. + Paranna tilisi turvallisuutta noudattamalla näitä suosituksia. Turvallisuussuositukset Käyttämättä %1$d+ päivän (%2$s) @@ -2164,8 +2164,8 @@ Tällä hetkellä käytössä %s. Menetelmä - Löytyi %d menetelmä. - Löytyi %d menetelmää. + Yksi menetelmä löytyi. + %d menetelmää löytyi. Saatavilla olevat menetelmät Ilmoitusmenetelmä @@ -2357,4 +2357,76 @@ Anna kameran käyttöoikeus järjestelmän asetuksista tämän toiminnon suorittamiseksi. Tämän toiminnon suorittaminen vaatii enemmän oikeuksia. Anna oikeudet järjestelmän asetuksista. Kuunnellaan ilmoituksia + Lukemattomat viestisi näkyvät tässä sitten kun saat niitä. + Ei ilmoitettavaa. + Kirjaudu QR-koodilla + Huomaa, että istuntojen nimet näkyvät ihmisille, joiden kanssa viestit. + Istunnon nimi + IP-osoite + Käyttöjärjestelmä + Malli + Selain + URL-osoite + Versio + Nimi + Istunnon nimi + Kirjaudu ulos tästä istunnosta + Piilota IP-osoite + Näytä IP-osoite + Kirjaudu ulos kaikista muista istunnoista + + Kirjaudu ulos yhdestä istunnosta + Kirjaudu ulos %1$d istunnosta + + Kirjaudu ulos + + Harkitse vanhoista (yksi vuorokausi tai vanhemmista), käyttämättömistä istunnoista uloskirjautumista. + Harkitse vanhoista (%1$d vuorokautta tai vanhemmista), käyttämättömistä istunnoista uloskirjautumista. + + + Harkitse vanhoista (yksi vuorokausi tai vanhemmista), käyttämättömistä istunnoista uloskirjautumista. + Harkitse vanhoista (%1$d vuorokautta tai vanhemmista), käyttämättömistä istunnoista uloskirjautumista. + + Tämä istunto on valmiina turvalliseen viestintään. + Nykyinen istuntosi on valmina turvalliseen viestintään. + Kamera + Sijainti + Kyselyt + Liitteet + Tarrat + Karttaa ei voida ladata +\nTätä kotipalvelinta ei välttämättä ole säädetty näyttämään karttoja. + Virhe kyselyjä noudettaessa. + Lataa lisää kyselyjä + + Viime vuorokaudelta ei ole menneitä kyselyjä. +\nLataa lisää kyselyjä nähdäksesi aiempien päivien kyselyt. + Viimeisiltä %1$d vuorokaudelta ei ole menneitä kyselyjä. +\nLataa lisää kyselyjä nähdäksesi aiempien päivien kyselyt. + + Tässä huoneessa ei ole menneitä kyselyjä + Menneet kyselyt + + Ei kyselyjä viimeisen vuorokauden ajalta. +\nLataa lisää kyselyjä nähdäksesi aiempien päivien kyselyt. + Ei kyselyjä viimeisen %1$d vuorokauden ajalta. +\nLataa lisää kyselyjä nähdäksesi aiempien päivien kyselyt. + + Tässä huoneessa ei ole aktiivisia kyselyjä + Aktiiviset kyselyt + Salauksen purkamisvirheistä johtuen joitakin ääniä ei ehkä lasketa + Tämä estää ihmisiä äänestämästä ja näyttää kyselyn lopulliset tulokset. + Ota LaTeX-matematiikka käyttöön + %1$s jäljellä + Äänitä pitämällä painettuna, lähetä päästämällä + Huoneen päivittäminen on edistynyt toiminto, jota yleensä suositellaan, kun huone on epävakaa virheistä, puuttuvista ominaisuuksita tai tietoturvahaavoittuvuuksista johtuen. +\nSe vaikuttaa yleensä vain siihen, miten huonetta käsitellään palvelimella. + Merkitse ei-ehdotetuksi + Merkitse ehdotetuksi + Viestien lähettäminen epäonnistui + Varattu + Virhe puhelua siirrettäessä + Yhdistä + Kumoa kutsu + Kotipalvelin ei hyväksy pelkistä numeroista koostuvaa käyttäjänimeä. \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-fr-rCA/strings.xml b/library/ui-strings/src/main/res/values-fr-rCA/strings.xml index cd39fa3381..9a9a288b99 100644 --- a/library/ui-strings/src/main/res/values-fr-rCA/strings.xml +++ b/library/ui-strings/src/main/res/values-fr-rCA/strings.xml @@ -296,7 +296,7 @@ code QR Ajouter avec un code QR Lien copié dans le presse-papiers - Ajouter un onglet dédié aux notifications non-lues sur l’écran principal. + Ajouter un onglet dédié aux notifications non lues sur l’écran principal. Activer le balayage pour répondre dans l’historique Nom ou identifiant (#exemple:matrix.org) Voir le répertoire des salons @@ -842,7 +842,7 @@ Le code est requis à l’ouverture d’${app_name}. Le code est demandé après 2 minutes d\'inutilisation d’${app_name}. Demander le code après 2 minutes - Afficher uniquement le numéro de messages non-lus dans une simple notification. + Afficher uniquement le numéro de messages non lus dans une simple notification. Afficher les détails comme les noms des salons et le contenu du message. Afficher le contenu dans les notifications Le code est la seule façon de déverrouiller ${app_name}. @@ -2068,4 +2068,4 @@ \n \nCette action va redémarrer l’application et pourra prendre du temps. Requête de synchronisation initiale - + \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-fr/strings.xml b/library/ui-strings/src/main/res/values-fr/strings.xml index 491a2660c4..3d08c58820 100644 --- a/library/ui-strings/src/main/res/values-fr/strings.xml +++ b/library/ui-strings/src/main/res/values-fr/strings.xml @@ -274,9 +274,9 @@ Rejoindre Rejeter Quitter le salon - Conversations privées + Messages directs Inviter - Bannir + Interdire l’accès au salon (définitif) Révoquer le bannissement Mentionner Faire confiance @@ -473,7 +473,7 @@ Chargement… Voulez-vous vraiment engager un nouvel appel audio \? Voulez-vous vraiment engager un nouvel appel vidéo \? - Bannir un utilisateur va l’expulser du salon et l’empêcher de le rejoindre à nouveau. + L’exclusion de l’utilisateur va l’expulser du salon et l’empêcher de le rejoindre à nouveau. Notification pour chaque message Ajouter à l’écran d’accueil Aperçu des liens @@ -579,7 +579,7 @@ réduire %1$s : %2$s +%d - Expulser + Retirer du salon (réversible) Afficher un aperçu des liens dans la discussion quand votre serveur d’accueil le permet. Envoyer des notifications de saisie Les autres utilisateurs pourront voir que vous êtes en train d’écrire. @@ -836,7 +836,7 @@ Changer de réseau Veuillez patienter… On dirait que vous avez déjà configuré une sauvegarde de clé depuis une autre session. Voulez-vous la remplacer par celle que vous êtes en train de créer \? - Vos conversations privées seront affichées ici. Appuyez sur « + » en bas à droite pour en démarrer une. + Vos messages directs seront affichés ici. Appuyez sur « + » en bas à droite pour en démarrer une. Salons Vos salons seront affichés ici. Appuyez sur le « + » en bas à droite pour trouver ceux existant ou en créer de nouveaux. Réactions @@ -852,7 +852,7 @@ Changer Impossible d’avoir un aperçu de ce salon Salons - Conversations privées + Messages directs CRÉER Nom Public @@ -884,7 +884,7 @@ Merci, la suggestion a bien été envoyée Échec d’envoi de la suggestion (%s) Afficher les évènements cachés dans les discussions - Conversations privées + Messages directs Attente… Chiffrement de la miniature… Envoi de la miniature (%1$s / %2$s) @@ -1410,7 +1410,7 @@ Le lien %1$s vous emmène sur un autre site : %2$s. \n \nVoulez-vous vraiment poursuivre \? - Nous n’avons pas pu créer votre conversation privée. Vérifiez les utilisateurs que vous souhaitez inviter et réessayez. + Nous n’avons pas pu créer votre message direct. Vérifiez les utilisateurs que vous souhaitez inviter et réessayez. Non chiffré Chiffré par un appareil non vérifié Vérifiez la nouvelle connexion accédant à votre compte : %1$s @@ -1570,7 +1570,7 @@ Le code est requis à l’ouverture de ${app_name}. Le code est demandé après 2 minutes d\'inutilisation de ${app_name}. Demander le code après 2 minutes - Afficher uniquement le numéro de messages non-lus dans une simple notification. + Afficher uniquement le numéro de messages non lus dans une simple notification. Afficher les détails comme les noms des salons et le contenu du message. Afficher le contenu dans les notifications Le code est la seule façon de déverrouiller ${app_name}. @@ -1607,7 +1607,7 @@ %1$s, %2$s et %3$d autre ont lu %1$s, %2$s et %3$d autres ont lu - Ajouter un onglet dédié aux notifications non-lues sur l’écran principal. + Ajouter un onglet dédié aux notifications non lues sur l’écran principal. Le salon a été créé, mais certaines invitations n’ont pas été envoyées pour la raison suivante : \n \n%s @@ -1716,12 +1716,12 @@ Sujet Sujet du salon (facultatif) Nom du salon - Conversation privée + Message direct Inclure l’historique d’échange de clés Plus aucun résultat Exporter le rapport d’audit %s pour permettre aux gens de connaître le sujet de ce salon. - Ceci est le début de l’historique de votre conversation privée avec %s. + Ceci est le début de l’historique de votre message direct avec %s. Ceci est le début de cette conversation. Ceci est le début de %s. Vous n\'avez pas le droit d’activer le chiffrement dans ce salon. @@ -2131,8 +2131,8 @@ Les invitations à des salons Les conversations de groupe chiffrées Les conversations de groupe - Les conversations privées chiffrées - Les conversations privées + Les conversations à deux chiffrées + Les messages directs Mon nom d’utilisateur Mon nom d’affichage Me notifier pour @@ -2614,7 +2614,7 @@ Préférences de présentation Parcourir les salons Créer un salon - Commencer une discussion + Commencer un message direct Non vérifiée · Dernière activité %1$s Vérifié · Dernière activité %1$s Tout voir (%1$d) @@ -2636,7 +2636,7 @@ Accéder aux espaces Pour simplifier Element, les onglets sont désormais facultatifs. Gérez les depuis le menu en haut à droite. Bienvenu dans une nouvelle vue ! - C\'est ici que vos messages non-lus s’afficheront lorsque vous en aurez. + C\'est ici que vos messages non lus s’afficheront lorsque vous en aurez. Rien à signaler. La messagerie sécurisée tout-en-un pour les équipes, les amis, et les organisations. Créez une discussion ou rejoignez un salon pour démarrer. Bienvenue dans ${app_name}, @@ -2920,4 +2920,13 @@ Impossible de démarrer un message vocal Erreur de connexion – Enregistrement en pause Appliquer le formatage de code en ligne + Bloc de code + Citation + Désindenter + Indenter + Consulter la chronologie des sondages + Impossible de déchiffrer cette diffusion audio. + Les détails de votre compte sont gérés séparément sur %1$s. + Compte + Nous avons rencontré une erreur lors de la mise-à-jour de vos préférences de notification. Veuillez réessayer. \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-hu/strings.xml b/library/ui-strings/src/main/res/values-hu/strings.xml index a44bc9b78b..6877571cce 100644 --- a/library/ui-strings/src/main/res/values-hu/strings.xml +++ b/library/ui-strings/src/main/res/values-hu/strings.xml @@ -2920,4 +2920,13 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze Hang üzenetet nem lehet elindítani Kapcsolódási hiba – Felvétel szüneteltetve Beágyazott kód formátum alkalmazása + Kód blokk be/ki + Idézet be/ki + Behúzás kijjebb + Behúzás + Szavazás megjelenítése az idővonalon + A hang közvetítés nem fejthető vissza. + A fiókadatok külön vannak kezelve itt: %1$s. + Fiók + Hiba történt az értesítések beállításának frissítésekor. Próbáld újra. \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-in/strings.xml b/library/ui-strings/src/main/res/values-in/strings.xml index ca871db81b..3d8d1b25be 100644 --- a/library/ui-strings/src/main/res/values-in/strings.xml +++ b/library/ui-strings/src/main/res/values-in/strings.xml @@ -2862,4 +2862,13 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Anda tidak dapat memulai sebuah pesan suara karena Anda saat ini merekam sebuah siaran langsung. Silakan mengakhiri siaran langsung Anda untuk memulai merekam sebuah pesan suara Tidak dapat memulai pesan suara Terapkan format kode dalam baris + Alih balok kode + Alih kutipan + Batalkan inden + Inden + Tampilkan pemungutan suara di lini masa + Tidak dapat mendekripsi siaran suara ini. + Detail akun Anda dikelola secara terpisah di %1$s. + Akun + Terjadi kesalahan saat memperbarui preferensi notifikasi Anda. Silakan coba lagi. \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-it/strings.xml b/library/ui-strings/src/main/res/values-it/strings.xml index 80a0b7045f..081a845ed2 100644 --- a/library/ui-strings/src/main/res/values-it/strings.xml +++ b/library/ui-strings/src/main/res/values-it/strings.xml @@ -2911,4 +2911,13 @@ Impossibile iniziare il messaggio vocale Applica formato codice interlinea Errore di connessione - Registrazione in pausa + Attiva/disattiva blocco di codice + Attiva/disattiva citazione + Rimuovi indentazione + Indenta + Vedi sondaggio nella linea temporale + Impossibile decifrare questa trasmissione vocale. + I dettagli del tuo account sono gestiti separatamente su %1$s. + Account + Si è verificato un errore aggiornando le tue preferenze di notifica. Riprova. \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-ja/strings.xml b/library/ui-strings/src/main/res/values-ja/strings.xml index d37cc518d7..cf28976e65 100644 --- a/library/ui-strings/src/main/res/values-ja/strings.xml +++ b/library/ui-strings/src/main/res/values-ja/strings.xml @@ -3,8 +3,8 @@ %sの招待 %1$sが%2$sを招待しました %1$sがあなたを招待しました - %1$sが参加しました - %1$sが退出しました + %1$sがルームに参加しました + %1$sがルームから退出しました %1$sが招待を拒否しました %1$sが%2$sを追放しました %1$sが%2$sのブロックを解除しました @@ -14,7 +14,7 @@ %1$sが表示名を%2$sに設定しました %1$sが表示名を%2$sから%3$sに変更しました %1$sが表示名(%2$s)を削除しました - %1$sがテーマを%2$sに変更しました + %1$sがトピックを%2$sに変更しました %1$sがルーム名を%2$sに変更しました %sがビデオ通話を発信しました。 %sが音声通話を発信しました。 @@ -30,7 +30,7 @@ 全員 (アバターも変更されました) %1$sがルーム名を削除しました - %1$sがルームの説明を削除しました + %1$sがルームのトピックを削除しました %1$sが%2$sにルームへの招待を送りました %1$sが%2$sの招待を受け入れました ** 復号化できません:%s ** @@ -58,8 +58,8 @@ %1$sが参加しました ルームに参加しました %1$sを招待しました - ディスカッションを作成しました - %1$sがディスカッションを作成しました + 会話を作成しました + %1$sが会話を作成しました ルームを作成しました %1$sがルームを作成しました 招待 @@ -87,33 +87,33 @@ 会話 不具合を報告 不具合の内容と状況の説明をお願いします。何をしましたか?何が起こるべきでしたか?実際に起こった事象は何でしょうか? - ここに不具合の内容を記述 + ここに不具合の内容を記述してください スクリーンショットの画像を送信 クラッシュ時のログを送信 ログを送信 開発者が問題を診断するために、このクライアントのログがバグレポートと一緒に送信されます。バグレポートは、ログとスクリーンショットを含めて、公開されることはありません。上記の説明文だけを送信したい場合は、以下のチェックを解除してください。 あなたは不満で端末を振っているようです。バグレポートの画面を開きますか? 前回アプリケーションは正常に停止しませんでした。クラッシュ報告の画面を開きますか? - 不具合を報告しました - 不具合の報告の送信に失敗しました (%s) + バグレポートを送信しました + バグレポートの送信に失敗しました(%s) ルームに参加 音声通話 ビデオ通話 検索 ファイルを送信 - ユーザー名かパスワードが正しくありません + ユーザー名とパスワードの一方あるいは両方が正しくありません メールアドレスの形式が正しくありません このメールアドレスは既に登録されています。 パスワードを忘れましたか? 正しいURLを入力してください 原寸 - 大き目 - 中程度 - 小さ目 - 通話終了 + + + + 通話が終了しました はい いいえ - 続行する + 続行 最新の未読へ移動 ルームから退出 このルームから退出してよろしいですか? @@ -130,7 +130,7 @@ ソースコードを表示 または 確認 - 送信中 (%s%%) + 送信中(%s%%) 削除 参加 このルームで発言する権限がありません。 @@ -143,15 +143,15 @@ このセッションで通知を有効にする 1対1のチャットでのメッセージ グループチャットでのメッセージ - ルームへ招待されたとき + ルームに招待されたとき 通話への招待 - 自動発言プログラム(Bot)が発言した時 + ボットによるメッセージ 端末起動時に開始 - アプリを閉じているときの動作 + バックグラウンド同期 同期のリクエストを失敗とするまでの時間 同期の間隔 - 一時保存を消去 - メディアの一時保存を消去 + キャッシュを消去 + メディアのキャッシュを消去 メディアファイルを保存 ユーザー設定 通知 @@ -160,7 +160,7 @@ 高度な設定 暗号 通知対象 - 端末の電話帳 + 端末の連絡先 端末の電話帳の使用を許可 電話帳の国番号 全てのメッセージにタイムスタンプを表示 @@ -168,29 +168,29 @@ ID(端末固有番号) 公開端末名 公開端末名の更新 - 最後のオンライン日時 + 直近のオンライン日時 %1$s @ %2$s 認証 ログイン中のアカウント 言語を選択 言語 インターフェース - 電子メールを確認して、本文中のURLをクリックしてください。完了したら「続行する」をクリックしてください。 - このメールアドレスは既に使われています。 - あなたのパスワードは更新されました + 電子メールを確認して、本文中のURLをクリックしてください。完了したら「続行」をクリックしてください。 + このメールアドレスは既に使用されています。 + パスワードを更新しました 国を選択 3日 1週間 1ヵ月 永久に - ルームの説明 - ルームの履歴の可視範囲 - ルームの履歴を読める人は\? + トピック + ルームの履歴の表示対象 + 履歴を閲覧できる人は? 誰でも メンバーのみ(この設定を選択した時点から) メンバーのみ(招待を送った時点から) メンバーのみ(参加した時点から) - ブロックされたユーザー + ブロックしたユーザー 高度な設定 このルームのサーバー内識別ID ラボ @@ -199,14 +199,14 @@ メインアドレスとしての設定を解除 セッションID フォントの大きさ - とても小さい - 小さい + 最小 + 標準 - 大きい - より大きい - とても大きい - 巨大 - 発言更新を確認しています + + 巨大 + 極大 + 最大 + イベントを待機しています 復号化されたソースコードを表示 名前変更 音声通話を開始 @@ -218,7 +218,7 @@ サインアウト 送信 このホームサーバーは、あなたがロボットではないことの確認を求めています - メールアドレスの認証に失敗しました:電子メールのリンクをクリックしたことを確認してください + メールアドレスの認証に失敗しました。電子メール内のリンクを開いたことを確認してください 不正な形式のJSON 有効なJSONを含んでいませんでした ログイン要求が多すぎます @@ -239,7 +239,7 @@ 結果がありません 利用規約 著作権 - 個人情報保護方針 + プライバシーポリシー ホームサーバー IDサーバー この電話番号は既に使用されています。 @@ -248,12 +248,12 @@ 新しいパスワード パスワードの更新に失敗しました %sの全てのメッセージを表示しますか? - 外観 + テーマ 公開端末名 ルームのエンドツーエンド暗号鍵をエクスポート 認証済 - このルームに参加していません。 - このルームで権限がありません。 + あなたはこのルームのメンバーではありません。 + このルームでそれを行う権限がありません。 ルーム %s は閲覧できません。 ユーザー名 ホームサーバーのURL @@ -261,13 +261,13 @@ Matrixアプリを追加 権限の数値は正の整数で入力してください。 Matrixの連絡先のみ - 通信先が通話の受取に失敗しました。 + 相手が電話に出られませんでした。 情報 - ${app_name}は、音声通話を実行するためにマイクへアクセスするための許可を必要としています。 - ${app_name}はビデオ通話を行うためにカメラとマイクにアクセスする許可を必要としています。 + ${app_name}は、音声通話を実行するためにマイクにアクセスする権限を必要としています。 + ${app_name}は、ビデオ通話を行うためにカメラとマイクにアクセスする権限を必要としています。 \n \n通話をするためには、次のポップアップでアクセスを許可してください。 - 発言を通報 + コンテンツを報告 写真を撮影 動画を撮影 認証を開始 @@ -275,7 +275,7 @@ リクエストにroom_idがありません。 リクエストの送信に失敗しました。 ウィジェットを作成できません。 - ウィジェットをこのルームから削除してもよろしいですか? + ウィジェットをこのルームから削除してよろしいですか? 一致していない場合は、コミュニケーションのセキュリティーが損なわれている可能性があります。 このセッションでは、未認証のセッションに対して暗号化されたメッセージを送信しない。 認証済のセッションにのみ暗号化 @@ -289,28 +289,28 @@ 鍵をローカルファイルにエクスポート ルームの暗号鍵をエクスポート 通話 - 通知あり(音量大) - 通知あり(サイレント) - 不具合の報告 + 通知(音量大) + 通知(サイレント) + バグレポート このユーザーにあなたと同じ権限レベルを与えようとしています。この変更は取り消せません。 \nよろしいですか? - 信用する - 信用しない + 信頼する + 信頼しない フィンガープリント(%s): リモートサーバーのIDを認証できませんでした。 - 誰かが不当にあなたの通信を傍受しているか、あなたの電話がリモートサーバーの証明書を信用していない可能性があります。 - サーバーの管理者が、これは想定されていることであると言っているのであれば、以下のフィンガープリントが、管理者によるフィンガープリントと一致していることを確認してください。 + これは、誰かがあなたのトラフィックを傍受しているか、あなたの電話機がリモートサーバーから提供された証明書を信頼していないことを意味している可能性があります。 + サーバーの管理者が、これは想定されていることであると述べた場合は、以下のフィンガープリントが、管理者によるフィンガープリントと一致することを確認してください。 証明書はあなたの電話により信頼されていたものから変更されています。これはきわめて異常な事態です。この新しい証明書を承認しないことを強く推奨します。 - 証明書は以前信頼されていたものから信頼されていないものへと変更されています。サーバーがその証明書を更新した可能性があります。サーバーの管理者に連絡して、適切なフィンガープリントを確認してください。 + 証明書が以前に信頼されたものから信頼されていないものに変更されました。サーバーが証明書を更新した可能性があります。サーバーの管理者に連絡して、適切なフィンガープリントを確認してください。 サーバーの管理者が上記のものと一致するフィンガープリントを発行した場合にのみ、証明書を承認してください。 検索 - このアプリの情報をシステム設定で表示。 + このアプリケーションの情報をシステム設定で表示。 アプリの情報 自分の表示名を含むメッセージ 自分のユーザー名を含むメッセージ バージョン olmのバージョン - サードパーティーの使用に関する掲示 + 外部ライブラリーのライセンス ホーム画面 逃した通知があるルームをピン止め 未読メッセージがあるルームをピン止め @@ -320,14 +320,14 @@ 未認証 認証 他のセッションのユーザー設定で、以下を比較して承認してください: - ルームのディレクトリを選択 + ルームのディレクトリーを選択 サーバー名 %sサーバー上の全てのルーム 全てのローカルの%sルーム 端末のカメラを使用 コマンドエラー 認識されないコマンド:%s - + オフ 音量大 暗号化されたメッセージ 読み込んでいます… @@ -337,8 +337,8 @@ 全てのメッセージ ホーム画面にショートカットを作成 インラインURLプレビュー - 暗号鍵を要求している新しいセッション \'%s\' を追加しました。 - 未認証のセッション \'%s\' が暗号鍵を要求しています。 + 暗号鍵を要求している新しいセッション\'%s\'を追加しました。 + 未認証のセッション\'%s\'が暗号鍵を要求しています。 作成 ホーム ルーム @@ -358,25 +358,25 @@ %d件の新しいメッセージ アバター - ステッカーを送る + ステッカーを送信 ダウンロード システムアラート - 可能であれば、英語で説明文を記述してください。 + 可能であれば、英語で詳細を記述してください。 音声を送信 - スタンプを送信 - 現在、有効なステッカーパックがありません。 + ステッカーを送信 + 現在、ステッカーパックが有効になっていません。 \n \nいくつか追加しますか? - 申し訳ありません、この操作を完了するための外部アプリが見つかりません。 - あなたの他のセッションに暗号鍵を再要求する。 + 申し訳ありません。この操作を完了するための外部アプリケーションが見つかりません。 + あなたの他のセッションに暗号鍵を再要求。 鍵をこのセッションに送信できるように、メッセージを復号化できる他の端末で${app_name}を起動してください。 %d個選択済 ユーザーをメンションするとき、バイブレーションで通知 送信の前にメディアをプレビュー - アカウントを停止 - 自分のアカウントを停止 + アカウントを無効化 + 自分のアカウントを無効化 分析データを送信 ${app_name}はアプリを改善するため、匿名の分析データを収集します。 @@ -390,36 +390,36 @@ %d個のウィジェットが使用中 必要な変数が見つかりません。 - 動作を表示 + アクションを表示 指定したIDのユーザーをブロック 指定したIDのユーザーのブロックを解除 ユーザーの権限レベルを規定 - 指定したIDのユーザーの管理者権限を取り消す - 指定したユーザーを現在のルームに招待 - 指定されたアドレスのルームに参加 + 指定したIDのユーザーの権限をリセット + 指定したIDのユーザーを現在のルームに招待 + 指定したアドレスのルームに参加 ルームから退出 - ルームの説明を設定 + ルームのトピックを設定 指定したIDのユーザーをこのルームから追放 表示するニックネームを変更 Markdown書式の入/切 Matrixアプリの管理を修正するには %1$sのホームサーバーを引き続き使用するには、利用規約を確認して同意する必要があります。 エラー - 今すぐ確認 - アカウントを停止 - この操作により、あなたのアカウントは永久に使えなくなります。あなたはログインできなくなり、誰も同じユーザーIDを再登録できなくなります。アカウントが参加している全てのルームから退出し、IDサーバーからアカウントの詳細は削除されます。 この操作は取り消せません。 + 確認 + アカウントの無効化 + この操作により、あなたのアカウントは永久に使えなくなります。ログインしたり同じユーザーIDを再登録したりすることはできなくなります。あなたのアカウントは参加している全てのルームから退出し、あなたのIDサーバーからアカウントの詳細が削除されます。<b>この操作は取り消せません。</b> \n -\nアカウントを停止しても、 デフォルトではあなたが送信したメッセージの履歴は消去されません。メッセージの履歴の消去を望む場合は、以下のボックスにチェックを入れてください。 +\nアカウントを無効化しても、<b>デフォルトではあなたが送信したメッセージの履歴は消去されません</b>。メッセージの履歴を消去する場合は、以下のボックスにチェックを入れてください。 \n -\nMatrixのメッセージの見え方は、電子メールと同様のものです。メッセージの履歴を消去すると、あなたが送信したメッセージは、新規または未登録のユーザーに共有されることはありませんが、既にメッセージを取得している登録ユーザーは、今後もそのコピーにアクセスできます。 - アカウントを停止するときに、自分の送信した全てのメッセージの履歴を消去してください(警告: この操作により、今後のユーザーは会話を不完全な形で見ることになります) - アカウントを停止 +\nMatrixのメッセージの見え方は、電子メールと同様のものです。メッセージの履歴を消去すると、あなたがこれまで送信したメッセージは、新規または未登録のユーザーに共有されることはありませんが、既にメッセージを取得している登録ユーザーは、今後もそのコピーにアクセスできます。 + アカウントを無効化する際、全ての送信済のメッセージを消去(警告:今後のユーザーには、不完全な会話が表示されます) + アカウントを無効化 パスワードを入力してください。 このルームは置き換えられており、アクティブではありません。 こちらから継続中の会話を確認 このルームは別の会話の続きです - 以前のメッセージを表示するには、ここをクリックしてください - サービス管理者に連絡してください + ここをクリックすると、以前のメッセージを表示します + サービス管理者に連絡 このホームサーバーはリソース制限の1つを超過しているため、 ユーザーがログインできなくなることがあります このホームサーバーはリソースの上限に達しました。 このホームサーバーは月間アクティブユーザーの上限に達しているため、 ユーザーがログインできなくなることがあります @@ -434,12 +434,12 @@ %d+ 展開 折りたたむ - 承諾 - このホームサーバーの方針を確認し承諾してください: + 同意 + このホームサーバーの運営方針を確認し、同意してください: 通話設定画面 着信に${app_name}の既定の着信音を使用 着信音 - 着信音を選んでください: + 着信音を選んでください: 会話から追放 鍵のバックアップ 鍵のバックアップを使用 @@ -449,7 +449,7 @@ リアルタイム性を重視して最適化 バックグラウンド同期を行わない 入力中通知を送信 - 文字入力中であることを他のメンバーに伝えます。 + 文字入力中であることを他のメンバーに表示。 開封確認メッセージを表示 開封確認メッセージをクリックすると、詳細な一覧を確認できます。 Enterキーでメッセージを送信 @@ -467,7 +467,7 @@ 暗号化されたメッセージにアクセスできなくなることを防ぐため、鍵の安全なバックアップはあなたのセッション全てで有効化してください。 暗号化されたメッセージは不要です 鍵をバックアップしています… - 続行しますか? + よろしいですか? バックアップ サインアウトする前に鍵をバックアップしないと、暗号化されたメッセージにアクセスできなくなります。 暗号鍵の管理 @@ -481,11 +481,11 @@ バージョン アルゴリズム 署名 - 通知に関する問題の解決 - システム設定。 + 通知に関する問題解決 + システムの設定。 アカウントの設定。 - カスタム設定。 - 起動時に実行 + ユーザー定義の設定。 + 端末起動時に開始 バックグラウンド制限の確認 編集 返信 @@ -501,14 +501,14 @@ 作成 名前 公開 - 誰でもこのルームに参加できるようになります + 誰でもこのルームに参加できます 一般 セキュリティーとプライバシー ヘルプと概要 ダイレクトメッセージ (編集済) - 会話を検索… - 全てのメッセージ (音量大) + 会話を絞り込む… + 全てのメッセージ(音量大) 全てのメッセージ メンションのみ ミュート @@ -525,7 +525,7 @@ いったん有効にすると、暗号化を無効にすることはできません。 セキュリティー 詳細を表示 - その他の設定 + その他 管理者としての操作 ルームの設定 通知 @@ -534,16 +534,16 @@ アップロード ルームから退出 - ルームから退室しています… + ルームから退出しています… 管理者 モデレーター - カスタム - 招待者 + ユーザー定義 + 招待中 ユーザー %1$sの管理者 %1$sのモデレーター - %1$sのデフォルトユーザー - %2$sのカスタム (%1$d) + %1$sの既定のユーザー + %2$sのユーザー定義(%1$d) タイムライン エンドツーエンド暗号化を有効にする… 暗号化を有効にする @@ -551,19 +551,19 @@ クロス署名は有効です \n秘密鍵は端末内にあります。 クロス署名は有効です -\n鍵は信頼されています +\n鍵は信頼されています。 \n秘密鍵は不明です - クロス署名は有効です + クロス署名は有効です。 \n鍵は信頼されていません クロス署名は無効です - 有効なセッション + 使用中のセッション 全てのセッションを表示 セッションを管理 このセッションからサインアウト %d件のアクティブなセッション - このログインを認証 + この端末を認証 QRコード はい いいえ @@ -572,11 +572,11 @@ アカウントデータ 削除… 削除の確認 - このイベントを削除してよろしいですか?ルーム名や説明の変更を削除すると、変更が取り消されますのでご注意ください。 - 暗号化は有効です - このルーム内でのメッセージはエンドツーエンド暗号化されます。詳細の確認や認証はユーザーのプロフィールをご確認ください。 + このイベントを削除してよろしいですか?ルームの名前やトピックの変更を削除すると、変更が取り消される可能性があります。 + 暗号化が有効です + このルーム内でのメッセージはエンドツーエンドで暗号化されます。詳細の確認や認証はユーザーのプロフィールをご確認ください。 暗号化が有効になっていません - 通知設定 + 通知の設定 切断 サインアウトしてよろしいですか? 既読にする @@ -599,10 +599,10 @@ カメラ ギャラリー ステッカー - スパムメッセージです + スパムです 不適切なメッセージです その他の報告… - コンテンツを報告 + このコンテンツを報告 このコンテンツを報告する理由 報告 ユーザーを無視 @@ -610,9 +610,9 @@ 元の大きさのまま画像を送信 - 自分に電話をかけることはできません + 自分に電話を発信することはできません マークダウン書式 - メッセージ送信前にマークダウン書式を適用します。これにより、アスタリスクを使用して斜体のテキストを表示するなどの高度な書式設定が利用できます。 + メッセージ送信前にマークダウン書式を適用します。アスタリスクを使用して斜字体のテキストを表示するなどの高度な書式設定が利用できます。 音声とビデオ 国際電話番号形式で入力してください(電話番号の最初に「+」を付けてください) メールアドレス @@ -621,24 +621,24 @@ 電話番号 あなたのMatrixアカウントに登録されたメールアドレスと電話番号を管理 メールアドレスと電話番号 - 有効化 - このセッションで通知が無効化されています。 -\n${app_name} の設定をご確認ください。 - このセッションで通知は有効化されています。 + 有効にする + このセッションで通知が無効になっています。 +\n${app_name}の設定をご確認ください。 + このセッションで通知は有効になっています。 セッションの設定。 - 有効化 - あなたのアカウントで通知が無効化されています。 + 有効にする + あなたのアカウントで通知が無効になっています。 \nアカウント設定をご確認ください。 - あなたのアカウントで通知は有効化されています。 + あなたのアカウントで通知は有効になっています。 設定を開く - システム設定で通知が無効化されています。 -\nシステム設定をご確認ください。 - システム設定で通知は有効化されています。 + システム設定で通知が無効になっています。 +\nシステム設定を確認してください。 + システム設定で通知は有効になっています。 バッテリー最適化 %d秒 - 拡張設定 + 高度な設定 現在の言語 他の利用可能な言語 メッセージエディター @@ -650,8 +650,8 @@ ルームを作成しています… 招待されています %sからの招待 - 概ね完了しました。%sの画面にも同じシールドアイコンが表示されていますか? - 相手ユーザーの端末のコードをスキャンし、相互に安全性を認証 + 概ね完了しました。%sにも同じマークが表示されていますか? + 相手のユーザーの端末のコードをスキャンし、安全に相互を認証 相手のコードをスキャン スキャンできません 拒否 @@ -660,7 +660,7 @@ 意図しない通話を防止 SSLエラー。 SSLエラー:相手のIDが認証されていません。 - このURLからホームサーバーに接続できませんでした、ご確認ください + このURLからホームサーバーに接続できませんでした。URLを確認してください 有効なMatrixサーバーのアドレスではありません この電話番号は既に登録されています。 シングルサインオンを使用してサインイン @@ -675,12 +675,12 @@ 電話 サウンドデバイスを選択 リアルタイム接続を確立できませんでした。 -\nホームサーバーの管理者に、通話が正常に動作するためにTURNを設定するようご連絡ください。 +\n安定した通話のために、ホームサーバーの管理者にTURNサーバーの設定を依頼してください。 電話を切る 拒否 - 承諾 - ウィジェットを削除できませんでした - ウィジェットを追加できませんでした + 同意 + ウィジェットの削除に失敗しました + ウィジェットの追加に失敗しました ビデオ通話を開始 通話を開始する権限がありません このルームで通話を開始する権限がありません @@ -689,10 +689,10 @@ なし トピック ルーム名 - このルーム内のメッセージはエンドツーエンドで暗号化されていません。 + このルームのメッセージはエンドツーエンドで暗号化されていません。 ここでのメッセージはエンドツーエンドで暗号化されていません。 設定 - あなたにはこのルームの暗号化を有効にする権限がありません。 + このルームの暗号化を有効にする権限がありません。 未読メッセージ タイムラインでのスワイプによる返信を有効にする タイムラインで非表示のイベントを表示 @@ -708,28 +708,28 @@ インテグレーションが無効になっています インテグレーションマネージャー インテグレーションを許可 - FCMトークンが正常に取得されました: + FCMトークンを正常に取得しました: \n%1$s Firebaseトークン Playサービスを修正 Google PlayサービスのAPKは利用可能で最新の状態になっています。 Playサービスのチェック - 一部の通知はカスタム設定で無効になっています。 + 一部の通知はユーザー定義の設定で無効になっています。 一部のメッセージがサイレントに設定されていることに注意してください(音を出さずに通知します)。 1つ以上のテストが失敗しました。調査用のバグレポートを送信してください。 1つ以上のテストが失敗しました。提案された修正を試してください。 基本的な診断はOKです。 それでも通知が届かない場合は、調査用のバグレポートを送信してください。 - 実行しています…(%1$dの%2$d) + 実行しています…(%2$d個のうち%1$d個目のテスト) テストを実行 - 診断トラブルシューティング + 問題解決に関する調査結果 イベントごとの通知の優先順位 - メールであなたに送ったリンクをクリックして確認してください。 + メールで送信したリンクをクリックしたことを確認してください。 %sを削除しますか? ブロックされたユーザーを絞り込む - トピックを変更 - ルームをアップグレード - m.room.server_acl eventsを送信 - 権限を変更 + トピックの変更 + ルームのアップグレード + m.room.server_acl eventsの送信 + 権限の変更 ルーム名の変更 履歴の見え方の変更 ルームの暗号化の有効化 @@ -748,7 +748,7 @@ ルームに関する変更を行うために必要な役割を選択 ルームの権限 権限 - ルームに関する変更を行うために必要な役割を表示し更新します。 + ルームに関する変更を行うために必要な役割を表示し更新。 ブロックを解除すると、ユーザーはルームに再び参加できるようになります。 ユーザーをブロック ブロックする理由 @@ -760,48 +760,48 @@ ユーザーを追放 このユーザーの招待をキャンセルしてよろしいですか? 招待をキャンセル - このユーザーを解除すると、そのユーザーからの全てのメッセージが再び表示されます。 + このユーザーの無視を解除すると、そのユーザーからの全てのメッセージが再び表示されます。 ユーザーの無視を解除 このユーザーを無視すると、あなたが共有しているルームからそのユーザーのメッセージが削除されます。 \n \nこの操作は、設定からいつでも元に戻すことができます。 ユーザーを無視 降格 - あなたは自分自身を降格させようとしています。この変更は取り消せません。あなたがルームの中で最後の特権ユーザーである場合、特権を再取得することはできません。 + あなたは自分自身を降格させようとしています。この変更は取り消せません。あなたがルームの中で最後の特権ユーザーである場合、特権を再取得することはできなくなります。 降格しますか? 招待をキャンセル - このルームは公開されていません。 招待がなければ再び参加することはできません。 + このルームは公開されていません。再度参加するには、招待が必要です。 連絡先へのアクセスを許可します。 QRコードをスキャンするには、カメラへのアクセスを許可する必要があります。 通話を保留しました %sが通話を保留しました 保留 - 通話をやり直す + 通話を再開 ビデオ通話が行われています… - 有効な認証情報がないため、権限がありません - ${app_name} 呼び出し失敗 - ルームディレクトリの全てのルームを表示(露骨なコンテンツのあるルームを含む)する。 + 有効な認証情報がないため、許可されていません + ${app_name}が呼び出しに失敗しました + ルームディレクトリーの全てのルームを表示(露骨なコンテンツのあるルームを含む)。 露骨なコンテンツのあるルームを表示 - ルームディレクトリ + ルームディレクトリー 新着情報 - 非公開 + 非公開にする 切り替える 追加 %1$sがエンドツーエンド暗号化(認識されていないアルゴリズム %2$s)を有効にしました。 エンドツーエンド暗号化(認識されていないアルゴリズム %1$s)を有効にしました。 - 会話を始める + 会話を開始 %1$sがエンドツーエンド暗号化を有効にしました。 エンドツーエンド暗号化を有効にしました。 - ゲストがルームに参加するのを拒否しました。 - %1$sはゲストがルームに参加するのを拒否しました。 - ゲストがルームに参加するのを拒否しました。 - %1$sはゲストがルームに参加するのを拒否しました。 + ゲストがルームに参加することを拒否しました。 + %1$sはゲストがルームに参加することを拒否しました。 + ゲストがルームに参加することを拒否しました。 + %1$sはゲストがルームに参加することを拒否しました。 ここにゲストが参加することを許可しました。 %1$sはここにゲストが参加することを許可しました。 - ゲストがルームに参加するのを許可しました。 - %1$sはゲストがルームに参加するのを許可しました。 - システムデフォルト - このルームのメインおよび代替のアドレスを変更しました。 + ゲストがルームに参加することを許可しました。 + %1$sはゲストがルームに参加することを許可しました。 + システムの既定 + このルームのメインおよび代替アドレスを変更しました。 このルームの代替アドレスを変更しました。 このルームの代替アドレス %1$s を削除しました。 @@ -850,7 +850,7 @@ %sがこのルームのサーバーのアクセス制御リストを変更しました。 ・IPリテラルに一致するサーバーはブロックされています。 - ・IPリテラルに一致するサーバーを許可します。 + ・IPリテラルに一致するサーバーを許可されています。 ・%sに一致するサーバーは許可されています。 ・%sに一致するサーバーはブロックされています。 %sがこのルームのサーバーアクセス制御リストを設定しました。 @@ -881,8 +881,8 @@ \n会話を読み込んでいます \n多くのルームに参加している場合、読み込みに時間がかかるかもしれません %1$sがこのルームから退出しました。理由:%2$s - このルームに参加しました。理由:%1$s - %1$sがこのルームに参加しました。理由:%2$s + 参加しました。理由:%1$s + %1$sが参加しました。理由:%2$s このルームに参加しました。理由:%1$s %1$sがこのルームに参加しました。理由:%2$s %1$sがあなたを招待しました。 理由:%2$s @@ -890,8 +890,8 @@ %1$sが%2$sを招待しました。 理由:%3$s あなたの招待です。理由:%1$s %1$sの招待です。理由:%2$s - メッセージを送っています… - メッセージを送りました + メッセージを送信しています… + メッセージを送信しました 初期同期: \nアカウントデータをインポートしています 初期同期: @@ -910,9 +910,9 @@ %1$s、%2$s、%3$sと%4$s %1$s、%2$sと%3$s - %1$sの権限レベルを%2$sから%3$sへ変更しました。 - カスタム - カスタム (%1$d) + %1$sの権限レベルを%2$sから%3$sへ + ユーザー定義 + ユーザー定義 (%1$d) 既定 モデレーター 管理者 @@ -933,30 +933,30 @@ %1$sにルームへの招待を送りました ルームのアバターを削除しました %1$sがルームのアバターを削除しました - ルームの説明を削除しました + ルームのトピックを削除しました ルーム名を削除しました - ディスカバリー設定を管理します。 + ディスカバリーの設定を管理。 ディスカバリー(発見) これにより、現在のキーまたはフレーズが置き換えられます。 - 新しいセキュリティーキーを生成するか、既存のバックアップに新しいセキュリティーフレーズを設定します。 + 新しいセキュリティーキーを生成するか、既存のバックアップに新しいセキュリティーフレーズを設定してください。 サーバー上の暗号鍵をバックアップして、暗号化されたメッセージとデータへのアクセスが失われるのを防ぎましょう。 - メッセージ作成画面に絵文字キーボードを開くためのボタンを追加 + メッセージ入力欄に絵文字キーボードを開くためのボタンを追加 絵文字キーボードを表示 アバターと表示名の変更を含む。 アカウントのイベントを表示 招待、追放、ブロックは影響を受けません。 参加・退出イベントを表示 - /confettiコマンドを使用するか、❄️または🎉を含むメッセージを送信 + /confettiコマンドを使用すると、❄️または🎉を含むメッセージを送信 チャットでエフェクトを表示 ホームサーバーがこの機能をサポートしている場合は、チャット内のリンクをプレビューします。 - ボット、ブリッジ、ウィジェット、ステッカーパックを管理します。 -\nインテグレーションマネージャーは、構成データを受信し、ユーザーに代わってウィジェットの変更や、ルーム招待の送信、権限の設定などを行うことができます。 + インテグレーションマネージャーを使用すると、ボット、ブリッジ、ウィジェット、ステッカーパックを管理できます。 +\n設定データを受信し、ユーザーに代わってウィジェットの変更、ルームへの招待の送信、権限レベルの設定を行うことができます。 インテグレーション(統合) - アプリがバックグラウンドにある場合、着信メッセージは通知されません。 - ${app_name}は正確な時間に定期的にバックグラウンドで同期します(構成可能)。 -\nこれは無線とバッテリーの使用量に影響し、${app_name}がイベントを待機していることを示す永続的な通知が表示されます。 - ${app_name}は、端末の限られたリソース(バッテリーの残量)を維持する方法でバックグラウンド同期をします。 -\n端末の状態によっては、OSによって同期が延期される場合があります。 + アプリがバックグラウンドにある場合、受信するメッセージは通知されません。 + ${app_name}は、正確な時間(設定可能)に定期的にバックグラウンドで同期します。 +\nこれは無線とバッテリーの使用量に影響します。また、${app_name}がイベントを待機していることを示す永続的な通知が表示されます。 + ${app_name}は、端末の限られたリソース(バッテリーの残量)を維持する方法でバックグラウンド同期を行います。 +\nバッテリーの状態によっては、OSによって同期が延期される場合があります。 LEDの色、振動、音を選択してください… 通知(サイレント)を設定 通話の通知を設定 @@ -973,17 +973,17 @@ \nこのエラーは${app_name}の管理外です。 これはいくつかの理由で発生する可能性があります。 後で再試行するとうまくいくかもしれません。システム設定でGoogle Playサービスのデータ使用量が制限されていないか、端末の時刻が正しいかどうかを確認してください。カスタムROMで生じることもあります。 ${app_name}はバッテリー最適化の影響を受けません。 制限を無効にする - 起動時の開始を有効にする + 端末起動時の開始を有効にする 端末を再起動するとサービスが開始します。 通知がクリックされました! 通知をクリックしてください。 通知が表示されない場合は、システム設定を確認してください。 - 通知を表示 + 通知の表示 通知を表示しています。 クリックしてください! プッシュ通知の受信に失敗しました。 アプリケーションを再インストールすると解決するかもしれません。 アプリケーションはプッシュ通知を受信しています アプリケーションはプッシュ通知を待機しています プッシュ通知のテスト - FCMトークンのホームサーバーへの登録に失敗しました: + FCMトークンのホームサーバーへの登録に失敗しました: \n%1$s FCMトークンがホームサーバーに登録されました。 トークンの登録 @@ -994,20 +994,20 @@ \nこのエラーは${app_name}の管理外です。Googleによると、このエラーは、FCMに登録されている端末上のアプリの数が多すぎることを示唆しています。 このエラーは、アプリの数が極端に多い場合にのみ発生するため、平均的なユーザーには影響しません。 ${app_name}はGoogle Playサービスを使用してプッシュメッセージを配信していますが、正しく設定されていないようです: \n%1$s - FCMトークンの取得に失敗しました: + FCMトークンの取得に失敗しました: \n%1$s 🎉全てのサーバーの参加がブロックされています!このルームは使用できなくなりました。 変更はありません。 - • サーバーにマッチするIPリテラルが禁止されています。 - • サーバーにマッチするIPリテラルが許可されるようになりました。 + • IPリテラルに一致するサーバーが禁止されるようになりました。 + • IPリテラルに一致するサーバーが許可されるようになりました。 • %sに一致するサーバーが許可リストから削除されました。 • %sに一致するサーバーが許可されるようになりました。 - • %sに一致するサーバーが禁止リストから削除されました。 - • %sに一致するサーバーは禁止されています。 + • %sに一致するサーバーがブロックリストから削除されました。 + • %sに一致するサーバーはブロックされています。 - %1$s、%2$s、他%3$d人のユーザーが読みました + %1$s、%2$s、他%3$d人のユーザーが閲覧済 - %1$s、%2$s、%3$sが読みました + %1$s、%2$s、%3$sが閲覧済 メッセージをマークダウンとして解釈せず、プレーンテキストとして送信 ファイルとして保存 共有 @@ -1023,7 +1023,7 @@ ユーザー名を入力してください。 無視 共有 - 続行するには利用規約を承認する必要があります。 + 続行するには、このサービスの利用規約に同意する必要があります。 全てブロック 許可 ルームID @@ -1041,7 +1041,7 @@ 新しいイベント 不明なIP - %2$d個の鍵のうち%1$d個のインポートに成功。 + %2$d個の鍵のうち%1$d個をインポートしました。 鍵のバックアップを管理 鍵のエクスポートに成功しました @@ -1061,7 +1061,7 @@ %sはあなたを招待しています このルームでグループ通話を開始する権限がありません 音声通話を開始 - 安全バックアップを設定 + セキュアバックアップを設定 鍵のバックアップで管理 鍵のバックアップを使用 暗号化されたメッセージとデータへのアクセスが失われるのを防ぎましょう @@ -1070,13 +1070,13 @@ バックアップの状態を確認しています バックアップを削除しています… このセッションで鍵のバックアップを使用するには、パスフレーズまたはリカバリーキーでバックアップを復元してください。 - バックアップには未認証のセッション %s による不正な署名があります - バックアップには認証済のセッション %s による不正な署名があります - バックアップには未認証のセッション %s による有効な署名があります + バックアップには未認証のセッション %s による不正な署名があります。 + バックアップには認証済のセッション %s による不正な署名があります。 + バックアップには未認証のセッション %s による有効な署名があります。 バックアップには認証済のセッション %s による署名があります。 バックアップにはこのセッションによる有効な署名があります。 バックアップには%sというIDの不明のセッションによる署名があります。 - このセッションでは鍵がバックアップされていません。 + 鍵はこのセッションからバックアップされていません。 このセッションでは鍵のバックアップが無効になっています。 このセッションでは鍵のバックアップが正しく設定されています。 最新の復号化キーのバージョンを取得するのに失敗しました(%s)。 @@ -1103,12 +1103,12 @@ リカバリーキーを入力 リカバリーキーを使用 ログアウトしたりこの端末を失くしたりすると、メッセージにアクセスできなくなる可能性があります。 - 続行しますか? + よろしいですか? 予期しないエラー リカバリーキー パスフレーズを使用してリカバリーキーを生成中です。数秒かかることがあります。 リカバリーキーを共有… - コピーをしてください + コピーしてください 中止 上書き 別のセッションで鍵のバックアップを既に設定しているようです。上書きしますか? @@ -1125,7 +1125,7 @@ 鍵のコピーを暗号化してホームサーバーに保存します。バックアップを保護するためにパスフレーズを設定してください。 \n \n最大限のセキュリティーを確保するために、アカウントのパスワードと異なるものに設定することが大切です。 - パスフレーズを使用してバックアップを保護します。 + パスフレーズを使用してバックアップを保護しましょう。 暗号化されたメッセージは、エンドツーエンドの暗号化によって保護されています。これらの暗号化されたメッセージを読むための鍵を持っているのは、あなたと受信者だけです。 \n \n鍵を失くさないよう、鍵を安全にバックアップしてください。 @@ -1134,36 +1134,36 @@ ${app_name}によるリカバリーキーの生成を望む場合、パスフレーズを削除してください。 マークダウンが無効です。 マークダウンが有効です。 - ”%s”とのコマンドはいくつかのパラメータが欠けているか不正です。 - 新しいセッションが暗号鍵を要請しています。 + コマンド\"%s\"はいくつかのパラメーターを欠いているか、パラメーターが正しくありません。 + 新しいセッションが暗号鍵を要求しています。 \nセッション名:%1$s -\n最後のオンライン日時:%2$s -\n新たにログインして新しいセッションを開始しなかった場合、この要求を無視してください。 +\n直近のオンライン日時:%2$s +\n新しいセッションにログインしなかった場合、この要求を無視してください。 未認証のセッションが暗号鍵を要求しています。 \nセッション名:%1$s -\n最後のオンライン日時:%2$s -\n新しいセッションにログインしていない場合、この要求を無視してください。 +\n直近のオンライン日時:%2$s +\n新しいセッションにログインしなかった場合、この要求を無視してください。 鍵の共有リクエスト - カスタムカメラ画面の代わりにシステムカメラを使用します。 + ユーザー定義のカメラ画面の代わりにシステムカメラを開始。 使用中のウィジェットがありません インテグレーションを管理 - DRM保護されているメディアを読み込む + DRMで保護されているメディアを読み込む マイクの使用 カメラの使用 - このウィジェットは次のリソースの使用を要求します: + このウィジェットは次のリソースの使用を要求しています: 現在の会議から退出し、もう一つの会議に参加しますか? - 申し訳ありませんが、ビデオ会議に参加する途中で問題が発生しました - 申し訳ありませんが、古い端末(Android OS 6.0以前)はJitsiを使用したビデオ会議をサポートしていません + 申し訳ありませんが、グループ通話に参加する際に問題が発生しました + 申し訳ありませんが、古い端末(Android OS 6.0以前)はJitsiを使用したグループ通話をサポートしていません あなたの表示名 ウィジェットの読み込みに失敗しました。 \n%s ウィジェットを再読み込み これを使用すると、クッキーが設定され、データが%sと共有される可能性があります: ウィジェットを追加した人: - **送信に失敗 - ルームを開いてください + **送信に失敗しました - ルームを開いてください 新しい招待 %1$sと%2$s - %1$sに%2$sと%3$s + %2$sと%3$sで%1$s %d件の通知 @@ -1181,33 +1181,33 @@ 暗号化されたメッセージの復元 ルームのバージョン - ブロックされたユーザー%d人 + %d人のブロックされたユーザー このルームのあるスペースのメンバーは、誰でもこのルームを発見し参加できます。ルームをスペースに追加できるのは、ルームの管理者だけです。 スペースのメンバーのみ 誰でもルームを発見し参加できます 公開 - 招待した人のみが検索・参加できます + 招待した人のみが検索し、参加できます 非公開 不明のアクセス設定(%s) 誰でもルームにノックができ、メンバーがその参加を承認または拒否できます - 現在のルームディレクトリの見え方を取得できません(%1$s)。 - このルームを%1$sのルームディレクトリに公開しますか? + 現在のルームディレクトリーの見え方を取得できません(%1$s)。 + このルームを%1$sのルームディレクトリーに公開しますか? このアドレスを非公開にする このアドレスを公開 - アドレスを設定すれば、他のユーザーがあなたのホームサーバー (%1$s) を通じてこのルームを見つけられるようになります。 + アドレスを設定すると、他のユーザーがあなたのホームサーバー(%1$s)を通じてこのルームを見つけられるようになります。 ローカルアドレス - 新しい公開アドレス(例: #alias:server) + 新しい公開アドレス(例:#alias:server) 他の公開アドレスはまだありません。以下から追加できます。 他の公開アドレスはまだありません。 - \"%1$s\"を非公開にしますか? + アドレス\"%1$s\"を非公開にしますか? 公開 手動で新しいアドレスを公開 他の公開アドレス: - 公開アドレスを通して、どのサーバーのどのユーザーでも、このルームに参加できます。アドレスを公開するには、まずローカルアドレスとして設定する必要があります。 + 公開アドレスを通して、どのサーバーのユーザーでも、このルームに参加できます。アドレスを公開するには、まずローカルアドレスとして設定する必要があります。 公開アドレス - このルームのアドレスと、ルームディレクトリにおける見え方を管理できます。 - スペースのアドレスを管理できます。 + このルームのアドレスと、ルームディレクトリーにおける見え方を管理。 + このスペースのアドレスを管理。 スペースのアドレス ルームのアドレス ゲストの参加を許可 @@ -1219,27 +1219,27 @@ ホームサーバーAPIのURL アクセスを取り消す 表示 - このルームのメッセージはエンドツーエンドで暗号化されています。 + このチャットのメッセージはエンドツーエンドで暗号化されています。 ダイレクトメッセージ 新しいダイレクトメッセージを送信 - メールアドレス(任意) - メールアドレス - アカウント復旧用のメールアドレスを設定します。後からオプションで知人に見つけてもらえるようにできます。 + 電子メール(任意) + 電子メール + アカウント復旧用のメールアドレスを設定します。後からこのメールアドレスによって知人に見つけてもらえるようにできます(任意)。 メールアドレスを設定 メールアドレスを確認しました 発見可能なメールアドレス 続行するには利用規約を承認してください ホームサーバーの利用規約を承認したら、再試行してください。 - 次に + 次へ 次へ - 次に - 次に - 次に + 次へ + 次へ + 次へ ユーザー名を選択してください。 - ユーザー名かパスワードが正しくありません。入力されたパスワードがスペースによって開始しているか終了しているので、確認してください。 + ユーザー名やパスワードが正しくありません。入力されたパスワードがスペースで開始または終了しています。確認してください。 そのユーザー名は既に使用されています ユーザー名 - ユーザー名またはメールアドレス + ユーザー名または電子メール %sでサインイン %sでサインアップ %sで続行 @@ -1252,11 +1252,11 @@ Matrix IDでサインイン 詳細を表示 その他 - カスタムと高度な設定 + ユーザー定義と高度な設定 組織向けのプレミアムホスティング 組織向けのプレミアムホスティング - 最大のパブリックサーバーで、数百万人に無料で参加 - メールと同じように、アカウントには1つのホームがありますが、誰とでも話すことができます + 最大の公開サーバーで、数百万人に無料で参加 + 電子メールと同じように、アカウントには1つのホームがありますが、誰とでも話すことができます サーバーを選択 始めましょう エクスペリエンスを拡張およびカスタマイズ @@ -1266,30 +1266,30 @@ ここが%sとのダイレクトメッセージのスタート地点です。 変更履歴はありません メッセージの変更履歴 - ファイル %1$s をダウンロードしました! + ファイル %1$s をダウンロードしました! ビデオを%d%%圧縮しています 画像を圧縮しています… 暗号化されたルームで完全な履歴を表示 フィードバックを送信 - フィードバックを送信できませんでした (%s) - ありがとうございます、あなたのフィードバックは正常に送信されました + フィードバックの送信に失敗しました(%s) + ありがとうございます。フィードバックを正常に送信しました 追加で確認が必要な事項がある場合は、連絡可 フィードバック 現在「スペース」のベータ版を使用しています。あなたのフィードバックは今後のバージョンに反映されます。ご意見を最大限に参考にさせていただくため、あなたのプラットフォームとユーザー名を記録させていただきます。 スペースについてのフィードバック - 提案の送信に失敗しました(%s) - ありがとうございます、提案は正常に送信されました + 提案の送信に失敗しました(%s) + ありがとうございます。提案を送信しました トークンの登録 - アプリケーションの表示名: - App ID: - Push Key: + アプリケーションの表示名: + App ID: + Push Key: 登録されたプッシュゲートウェイはありません プッシュ通知に関するルールが定義されていません プッシュ通知に関するルール - あなたは既にこのルームを見ています! - その他のサードパーティーの使用に関する掲示 + 既にこのルームを表示しています! + その他の外部ライブラリーのライセンス Matrix SDKのバージョン - ファイル\"%1$s\"からエンドツーエンド暗号鍵をインポートします。 + ファイル\"%1$s\"からエンドツーエンド暗号鍵をインポート。 鍵のバックアップデータの取得中にエラーが発生しました 信頼情報の取得中にエラーが発生しました ルームが作成されましたが、一部の招待が送信されていません。理由: @@ -1297,7 +1297,7 @@ \n%s ルームの設定 トピック - ルームの説明(任意) + ルームのトピック(任意) ルーム名 このルームはプレビューできません。参加しますか? 現在、このルームにはアクセスできません。 @@ -1308,7 +1308,7 @@ 不正な形式のイベントです。表示できません ルームの管理者によってモデレートされたイベント リアクション - リアクションを見る + リアクションを表示 リアクションを追加 同意 リアクション @@ -1319,28 +1319,28 @@ 未読メッセージはありません 未読はありません! %sがセッションの認証を要求しています - リトライ - 他のホームサーバーに接続しようとしているようですね。サインアウトしますか? + 再試行 + 他のホームサーバーに接続しようとしているようです。サインアウトしますか? IDサーバーを使用していません 不明なエラー このルームを含む参加済のスペース - このルームにアクセスできるスペースを決定します。スペースが選択されると、そのメンバーはルーム名を見つけて参加できます。 + このルームにアクセスできるスペースを選択してください。選択したスペースのメンバーはルーム名を検索し、参加できるようになります。 了解 - 完了しました! + 認証しました! メッセージの新しい鍵 暗号化されたメッセージを決して失わないために セキュアバックアップ これを使用するとデータが%sと共有される可能性があります: %1$s:%2$s %3$s %1$s:%2$s - あなたが知らないかもしれない他のスペースやルーム - 誰がこのルームを検索・参加できるか選択してください。 + 知らないかもしれない他のスペースやルーム + 誰がこのルームを検索し、参加できるか選択してください。 タップしスペースを編集 スペースを選択 アクセス可能なスペース スペースのメンバーに発見とアクセスを許可します。 スペース %s のメンバーが検索、プレビュー、参加できます。 - 非公開(招待のみ) + 非公開(招待者のみ参加可能) 既定のメディアソース 既定の圧縮率 ルームのアップグレード @@ -1354,9 +1354,9 @@ ダイレクトメッセージ 自分のユーザー名 自分の表示名 - グループチャットで暗号化されたメッセージ - 1対1のチャットで暗号化されたメッセージ - 以下がメッセージに含まれる場合に通知 + グループチャットでの暗号化されたメッセージ + 1対1のチャットでの暗号化されたメッセージ + 以下の場合に通知 その他 メンションとキーワード 通知のデフォルト @@ -1366,32 +1366,32 @@ %d件の不在着信(音声) - 既定に設定して次回から確認しない + 既定に設定し、次回から確認しない 鍵の共有リクエストの履歴を送信 結果がありません - 自分に電話をかけることはできません。参加者が招待を受け入れるまでお待ちください + 自分に電話を発信することはできません。参加者が招待を受け入れるまでお待ちください ミーティングはJitsiのセキュリティーとパーミッションポリシーを使用します。ミーティング中は、現在ルームにいる全ての人に招待が表示されます。 権限がありません 音声メッセージを送信するには、マイクの権限を許可してください。 この操作を実行するには、システム設定からカメラの権限を許可してください。 この操作を実行するための権限がありません。システム設定から権限を付与してください。 IDサーバーに接続できませんでした - IDサーバーのURLを入力 + IDサーバーのURLを入力してください 同意する - 同意を撤回 + 同意を取り消す あなたの連絡先から他のユーザーを発見するために、メールアドレスや電話番号をこのIDサーバーに送信することに同意しています。 メールと電話番号を送信 - %sにメールを送りました。メールを確認してリンクをクリックしてください - %sにメールを送りました。メールの確認リンクをクリックしてください + %sにメールを送りました。メールを確認して承認リンクをクリックしてください + %sにメールを送りました。メールを確認して承認リンクをクリックしてください 発見可能な電話番号 IDサーバーとの接続を解除すると、他のユーザーによって見つけられなくなり、また、メールアドレスや電話で他のユーザーを招待することもできなくなります。 電話番号を追加すると、発見可能に設定する電話番号を選択できるようになります。 メールアドレスを追加すると、発見可能に設定するメールアドレスを選択できるようになります。 - 現在、IDサーバーを使用していません。自分の連絡先を見つけたり、連絡先から見つけてもらったりするには、以下でIDサーバーを設定してください。 - 現在%1$sを使って自分の連絡先を見つけたり、連絡先から見つけてもらったりできるようにしています。 + 現在、IDサーバーを使用していません。連絡先を見つけたり、連絡先から見つけてもらったりするには、以下でIDサーバーを設定してください。 + 現在%1$sを使用して、自分の連絡先を見つけたり、連絡先から見つけてもらったりできるようにしています。 IDサーバーを変更 IDサーバーの設定 - IDサーバーの切断 + IDサーバーから切断 IDサーバー ボット、ブリッジ、ウィジェット、ステッカーパックを使用 他の人が見つけられるように @@ -1399,18 +1399,18 @@ 編集履歴を表示 提案 リンクをクリップボードにコピーしました - メイン画面に未読通知専用のタブを追加する。 + メイン画面に未読通知専用のタブを追加。 ルーム名を検索 - 名前もしくはID (#例えば:matrix.org) - ルームディレクトリを見る + 名前もしくはID(#example:matrix.org) + ルームディレクトリーを見る 新しいルームを作成 お探しのものが見つかりませんか? - あなたの提案をここに書いてください - ご意見・ご感想をお聞かせください。 + 提案をここに書いてください + 意見・感想を聞かせてください。 提案する - フォーマット: - URL: - セッションの表示名: + フォーマット: + URL: + セッションの表示名: 以下のうちいずれかが流出、あるいはハッキングされた恐れがあります。 \n \n- あなたのパスワード @@ -1419,9 +1419,9 @@ \n- あなたの端末が使用しているインターネット接続 \n \n設定画面からパスワードとリカバリーキーを早急に変更することを推奨します。 - メールアドレス + 電子メール アドレス - 続行する + 続行 ファイル このユーザーはスペースから追放されます。 \n @@ -1436,13 +1436,13 @@ %1$s個以上の選択肢が必要です - 呼び出したユーザーは話し中です。 - 話し中です + 呼び出したユーザーは通話中です。 + 通話中 %sとの音声通話 %sとのビデオ通話 呼び出しています… ホームサーバーを選択 - %sのURLにあるホームサーバーに接続できません。リンクを確認するか、手動でホームサーバーを選択してください。 + URL %s のホームサーバーに接続できません。リンクを確認するか、手動でホームサーバーを選択してください。 後で スペース スレッドから @@ -1452,7 +1452,7 @@ PINコードを有効にする これを「招待者のみ参加可能」に設定しました。 ルームの設定 - コンテンツが報告されました + コンテンツを報告しました ヘルプとサポート ヘルプ ${app_name}の運営方針 @@ -1477,7 +1477,7 @@ 種類 確認済 選択済 - ビデオ + 動画 画像 スクリーンショット 接続 @@ -1503,11 +1503,11 @@ 警告 警告 成功しました! - 続行する + 続行 警告! 位置情報 メディア - アンケート終了 + アンケートが終了しました アンケートを終了しますか? アンケートを削除 スペースを作成 @@ -1542,11 +1542,11 @@ 位置情報を共有 スペースに関する変更を行うために必要な役割を更新する権限がありません スペースに関する変更を行うために必要な役割を選択 - スペースに関する変更を行うために必要な役割を表示し更新します。 + スペースに関する変更を行うために必要な役割を表示し更新。 絞り込む スレッド スレッド - スペースをアップグレード + スペースのアップグレード スペース名の変更 スペースの権限 応答がありません @@ -1564,29 +1564,29 @@ ルーム名を設定 アカウントの設定 キーワード - ルームから退出しました! + ルームから退出しています! 吹き出しでメッセージを表示 電子メールによる通知 - セッションからサインアウトしました! + セッションからサインアウトしています! なし メンションとキーワードのみ ルームのスレッドを絞り込む 退出 携帯端末では、暗号化されたルームでのメンションとキーワードの通知は受信できません。 - ルームの暗号化の有効化 + スペースの暗号化の有効化 スペースのメインアドレスの変更 スペースのアバターの変更 アンケートを作成 アンケートを作成 - 暗号化が正しく設定されていないため、メッセージを送ることができません。クリックして設定を開いてください。 - 暗号化が正しく設定されていないため、メッセージを送ることができません。管理者に連絡して、暗号化を正しい状態に復元してください。 - %2$dの%1$d - あなたは既にこのスレッドを見ています! - ルームに表示 - ルームに表示 + 暗号化が正しく設定されていないため、メッセージを送信できません。クリックして設定を開いてください。 + 暗号化が正しく設定されていないため、メッセージを送信できません。管理者に連絡して、暗号化を正しい状態に復元してください。 + %2$d個のうち%1$d個 + 既にこのスレッドを表示しています! + ルーム内で表示 + ルーム内で表示 スレッドを表示 このルームへの参加は許可されていません - "トピック: " + "トピック: " トラブルシューティング 鍵のバックアップのバナーを閉じる キーワードに「%s」を含めることはできません @@ -1628,16 +1628,16 @@ 不適切として報告済 添付ファイルを送信 詳細なログを有効にする。 - メールアドレスか電話番号でアカウントを見つけてもらえるようにするには、IDサーバー(%s)の利用規約への同意が必要です。 + メールアドレスか電話番号でアカウントを検出可能にするには、IDサーバー(%s)の利用規約への同意が必要です。 ${app_name}は位置情報にアクセスできませんでした ${app_name}は位置情報にアクセスできませんでした。後でもう一度やり直してください。 音声メッセージ(%1$s) - 推奨のルームバージョンへとアップグレード + 推奨のルームバージョンにアップグレード 音声メッセージを録音 あなたのホームサーバーの運営方針 一番下に移動 - %sが読みました - %1$sと%2$sが読みました + %sが閲覧済 + %1$sと%2$sが閲覧済 ファイルを使用 暗号化を有効にしますか? キャンセルしました @@ -1667,20 +1667,20 @@ 新しいPINコード PINコードを再設定 PINコードを忘れましたか? - PINコードを入力 + PINコードを入力してください PINコードを確認 サードパーティー製ライブラリー これはいつでも設定から無効にできます 私たちは、情報を第三者と共有することはありません - 私たちは、アカウントのデータを記録したり分析したりすることはありません + 私たちは、アカウントのいかなるデータも記録したり分析したりすることは<b>ありません</b> ${app_name}の改善を手伝う このルームを「リンクを知っている人が参加可能」に設定しました。 - どのユーザーも無視していません + 無視しているユーザーはいません キーワードを入力するとリアクションを検索できます。 変更を加えませんでした %1$sは変更を加えませんでした - %d人のユーザーが読みました + %d人のユーザーが閲覧済 スペースへのアクセス このサーバーは運営方針を提供していません。 @@ -1697,7 +1697,7 @@ 連絡先を発見するには、連絡先のデータ(メールアドレスと電話番号)をあなたのIDサーバーに送信する必要があります。プライバシーの保護のため、データは送信前にハッシュ化されます。 メールアドレスと電話番号を%sに送信 このIDサーバーは運営方針を提供していません - IDサーバーの運営方針を隠す + IDサーバーの運営方針を表示しない IDサーバーの運営方針を表示 アカウントの新しいパスワードを設定… シェイクを検出しました! @@ -1707,21 +1707,21 @@ 素早くクラッシュ 開発者モードは隠された機能を有効にするため、アプリケーションが不安定になる恐れがあります。開発者向けです! 復号エラーが生じた際に、自動的にログを送信 - 復号エラーを自動的に報告する。 + 復号エラーを自動で報告。 変更を有効にするにはアプリケーションの再起動が必要です。 LaTeXによる数学表記を有効にする - 以下が含まれる場合に通知 + 以下の場合に通知 アップグレードすると、このルームの新しいバージョンが作成されます。今ある全てのメッセージは、アーカイブしたルームに残ります。 誰がアクセスできますか? 通知は%1$sで管理できます。 暗号化されたルームでのメンションとキーワードによる通知は、携帯端末では利用できません。 ユーザーを無視し、そのメッセージを非表示に設定 - %sとのコマンドは認識されていますが、スレッドではサポートされていません。 + コマンド\"%s\"は認識されていますが、スレッドではサポートされていません。 誰でもスペースを発見し参加できます 法的情報 ユーザーに関する情報を表示 - このルームにおいてのみアバターを変更 - このルームにおいてのみ表示名を変更 + このルームでのみアバターを変更 + このルームでのみ表示名を変更 ユーザーの無視を解除し、以後のメッセージを表示 続行するには%sを入力してください 有効なリカバリーキーではありません @@ -1731,14 +1731,14 @@ 投票する 投票した人には、投票の際に即座に結果が表示されます アンケートの終了後に結果を公開 - 結果はアンケートを終了した後でのみ明らかにされます + 結果はアンケートが終了した後で表示されます 以下で開く - 暗号化のアップグレードが利用可能です + 暗号化のアップグレードが利用できます SSSSキーをリカバリーキーから生成しています ${app_name} iOS \n${app_name} Android - ${app_name}ウェブ版 -\n${app_name}デスクトップ版 + ${app_name} ウェブ版 +\n${app_name} デスクトップ版 リカバリーキーを選択、直接入力、あるいはクリップボードからペースト リカバリーキーを使用 暗号化されたルームでのみサポート @@ -1775,8 +1775,8 @@ 録音を削除 音声メッセージがアクティブの間は返信や編集はできません このアンケートを削除してよろしいですか?一度削除すると復元することはできません。 - 共有データの取り扱いに失敗しました - 回転とクロップ + 共有データを取り扱えませんでした + 回転とトリミング ルームを探す 既存のルームとスペースを追加 スレッドのメッセージを有効にする @@ -1789,16 +1789,16 @@ \n既存のスペースを別のスペースに追加できます。 あなたのホームサーバーはまだスペースをサポートしていないようです 画像を追加 - このコンテンツは不適切な投稿として報告されています。 + このコンテンツを不適切な投稿として報告しました。 \n \nこのユーザーのコンテンツをこれ以上見たくなければ、ユーザーを無視してそのメッセージを非表示にできます。 - このコンテンツはスパムとして報告されています。 + このコンテンツをスパムとして報告しました。 \n \nこのユーザーのコンテンツをこれ以上見たくなければ、ユーザーを無視してそのメッセージを非表示にできます。 - このコンテンツが報告されています。 + このコンテンツを報告しました。 \n \nこのユーザーのコンテンツをこれ以上見たくなければ、ユーザーを無視してそのメッセージを非表示にできます。 - %1$sにより%2$sに + %1$sが%2$sにアップロード 質問あるいはトピック アンケートの質問あるいはトピック 少々お待ちください。少し時間がかかるかもしれません。 @@ -1812,7 +1812,7 @@ サーバーのバージョン サーバー名 電話番号が正しくないようです。確認してください - カスタムホームサーバーを選択 + ユーザー定義のホームサーバーを選択 Element Matrix Servicesを選択 再送信 コードを入力 @@ -1825,7 +1825,7 @@ 信頼済 認証済 未送信のメッセージを削除 - カスタムイベントを送信 + ユーザー定義のイベントを送信 ルームの状態を調査 開封確認メッセージを表示 通知しない @@ -1842,7 +1842,7 @@ 合計%1$d票。投票すると結果を確認できます - 未認証の端末で暗号化 + 未認証の端末による暗号化 メッセージを紙吹雪と共に送信 メッセージを降雪と共に送信 紙吹雪🎉を送る @@ -1851,19 +1851,19 @@ エンドツーエンドで暗号化されており、登録に電話番号は不要です。広告もデータ収集もありません。 会話の保存先を自分で決められ、自分で管理できる独立したコミュニケーション。Matrixをもとに。 オンライン上でも対面の会話と同じレベルでプライバシーを守る、安全で独立したコミュニケーション。 - 安全なメッセージ。 + 安全なメッセージのやりとり。 主導権を握るのは、あなたです。 ${app_name}の使用に関するヘルプ 詳細なログは、イライラシェイクでログを送信する際に、より多くのログを提供することで、開発者にとっての助けになります。有効にした場合でも、メッセージの内容やその他のプライベートな情報は記録されません。 ルームのアップグレードは高度な作業であり、不具合や欠けている機能、セキュリティー上の脆弱性がある場合に推奨されます。 \nアップグレードは通常、ルームがサーバー上で処理される仕方にだけ影響します。 - 一度有効にしたルームの暗号化は無効にすることはできません。暗号化されたルームで送信されたメッセージは、サーバーからは見ることができず、そのルームのメンバーだけが見ることができます。暗号化を有効にすると、多くのボットやブリッジが正常に動作しなくなる場合があります。 + 一度有効にしたルームの暗号化は無効にすることはできません。暗号化されたルームで送信されたメッセージは、サーバーからは閲覧できず、そのルームのメンバーだけが閲覧できます。暗号化を有効にすると、多くのボットやブリッジが正常に動作しなくなる可能性があります。 %sして、このルームを皆に紹介しましょう。 このコードを共有し、スキャンして追加してもらい、会話を始めましょう。 正しい参加者が%sにアクセスできるようにしましょう。 連絡先を追加 - %d人の知り合いがすでに参加しています + %d人の知人が既に参加しています %sに招待 ユーザー名かメールアドレスで招待 @@ -1874,18 +1874,18 @@ 注意:アプリケーションが再起動します ホームサーバーの管理者にお問い合わせください あなたが参加している全てのルームがホームに表示されます。 - 親のスペースを自動的に更新 + 上位のスペースを自動的に更新 残り%1$d秒 %sに属する人は、誰でもこのルームを検索し、参加することができます。手動で全員を招待する必要はありません。これはルームの設定からいつでも変更できます。 - 親のスペースに属する人は、誰でもこのルームを検索し、参加することができます。手動で全員を招待する必要はありません。これはルームの設定からいつでも変更できます。 + 上位のスペースに属する人は、誰でもこのルームを検索し、参加することができます。手動で全員を招待する必要はありません。これはルームの設定からいつでも変更できます。 %1$d個の投票に基づく - 合計%1$d票の投票に基づく最終結果 + 合計%1$d票に基づく最終結果 - 新しいセッションが認証されました。セッションは暗号化されたメッセージにアクセスでき、他のユーザーには信頼済として表示されます。 - このルームを同じホームサーバー上で組織内のチームとのコラボレーションにのみ使用するなら、このオプションを有効にするといいかもしれません。これは後から変更できません。 + 新しいセッションが認証されました。セッションは暗号化されたメッセージにアクセスすることができます。また、セッションは他のユーザーに「信頼済」として表示されます。 + このルームを、あなたのホームサーバーで、組織内のチームとのコラボレーションにのみ使用するなら、この設定を有効にするといいかもしれません。これは後から変更できません。 %sに属していないユーザーによるこのルームへの参加を、今後永久に拒否 プレーンテキストメッセージの前に ( ͡° ͜ʖ ͡°) を付ける このメールアドレスのドメインの登録は許可されていません @@ -1898,16 +1898,16 @@ デバッグ用の情報を画面に表示 初期同期中… 説明文が短すぎます - サインインして暗号鍵を取り戻さなければ、暗号化されたメッセージにアクセスできなくなります。 + サインインして暗号鍵を復旧しないと、暗号化されたメッセージにアクセスできなくなります。 再サインイン - 有効なホームサーバーを発見できません。識別子を確認してください + 有効なホームサーバーを発見できません。IDを確認してください どこかのホームサーバーで既にアカウントを登録している場合、以下でMatrix ID(例:@user:domain.com)とパスワードを使用してください。 入力したコードが正しくありません。確認してください。 - これは正しいユーザー識別子ではありません。正しいフォーマットは「@user:homeserver.org」です。 - パスワードをお忘れの場合、戻ってパスワードを再設定してください。 + これは正しいユーザーIDではありません。正しいフォーマットは「@user:homeserver.org」です。 + パスワードを忘れた場合、戻ってパスワードを再設定してください。 メールボックスを確認してください - カスタムサーバーに接続 - 既にアカウントを持っています + ユーザー定義のサーバーに接続 + 既にアカウントがあります 既存のサーバーに参加しますか? この質問をスキップ 友達と家族 @@ -1928,14 +1928,14 @@ 非公開で招待が必要なルームは表示されていません。 \nルームを追加する権限はありません。 非公開で招待が必要なルームは表示されていません。 - 知人に見つけてもらえるように電話番号を設定できます。任意です。 + 知人に見つけてもらえるように電話番号を設定できます(任意)。 メッセージキー 復旧用のパスフレーズ - ニックネームの色を変更 + 表示名の色を変更 パスワードはまだ変更されていません。 \n \n変更作業を中止しますか? - %1$sに確認メールを送信しました。 + %1$sに認証メールを送信しました。 メールボックスを確認してください サインインに戻る 元の大きさのままメディアファイルを送信 @@ -1948,15 +1948,15 @@ 信頼されていません 認証の要求 %sがキャンセルしました - 既読 + 閲覧済 認証 - メールアドレスが正しくないようです + メールアドレスの形式が正しくありません 国際電話番号の形式を使用してください。 国際電話番号は「+」から始めてください コードを%1$sに送信しました。以下に入力して認証してください。 このメールアドレスはどのアカウントにも登録されていません パスワードを変更すると、全てのセッションでのエンドツーエンド暗号鍵がリセットされ、暗号化されたメッセージ履歴が読めなくなります。パスワードを再設定する前に、鍵のバックアップを設定するか、他のセッションからルームの鍵をエクスポートしておいてください。 - パスワードの再設定を確認するために確認メールを送信します。 + パスワードの再設定を確認するために認証メールを送信します。 このメールアドレスはどのアカウントにも登録されていません。 このアプリでは、このホームサーバーにアカウントを作成できません。 \n @@ -1964,12 +1964,12 @@ 申し訳ありませんが、このサーバーはアカウントの新規登録を受け入れていません。 このアプリではこのホームサーバーにサインインできません。このホームサーバーは次のサインインの方法に対応しています:%1$s \n -\nウェブクライエントを使用してサインインしますか? - %1$sを読み込み中にエラーが発生しました(%2$d) - 利用したいサーバーのアドレスを入力してください - 利用したいModular Elementまたはサーバーのアドレスを入力してください +\nウェブクライアントを使用してサインインしますか? + ページ %1$s を読み込んでいる際にエラーが発生しました(%2$d) + 使用したいサーバーのアドレスを入力してください + 使用したいModular Elementまたはサーバーのアドレスを入力してください 迷っていますか?%s - みんなと繋がる手助けをいたします。 + みんなと繋がる手助けをいたします 自分のコード 招待を%1$sと他%2$d人に送信しました @@ -1991,7 +1991,7 @@ このルームはまだ作成されていません。キャンセルしますか? テキストメッセージで共有 保護を設定 - このルームのみ + このルームにのみ 誰でも参加可能。コミュニティー向け 既存のスペースに参加するには、招待が必要です。 これは後から変更できます @@ -2000,13 +2000,13 @@ QRコードがスキャンされていません! 内容を通知に表示 プッシュ通知は無効になっています - ユーザーのブロックを解除できませんでした + ユーザーのブロックの解除に失敗しました %1$sへの招待を取り消しますか? 連絡先を取得しています… RiotはElementになりました! このメッセージにアクセスできません アバターを設定 - IDサーバーのURLを入力 + IDサーバーのURLを入力してください マイクのミュートを解除 マイクをミュート %1$sを使用 @@ -2025,22 +2025,22 @@ \n - 認証している相手が接続しているホームサーバー \n - あなたか相手のインターネット接続 \n - あなたか相手の端末 - セキュアではない - 信頼できないサインイン + セキュアではありません + 信頼されていないサインイン 使用できない文字が含まれています ${app_name}の改善と課題抽出のために、匿名の使用状況データの送信をお願いします。複数の端末での使用を分析するために、あなたの全端末共通のランダムな識別子を生成します。 \n \n%sで利用規約を閲覧できます。 最初の検索結果のみ表示しています。文字をもっと入力してください… matrix.toリンクのフォーマットが正しくありませんでした - 注意!この端末には暗号鍵を含む個人情報が保存されています。 + 警告:あなたの個人データ(暗号化の鍵を含む)が、この端末に保存されています。 \n -\nこの端末での使用を終了、または他のアカウントにサインインしたい場合、このデータをクリアしてください。 +\nこの端末の使用を終了するか、他のアカウントにログインしたい場合は、そのデータを消去してください。 この端末に現在保存されている全てのデータをクリアしますか? \nアカウントデータとメッセージにアクセスするにはもう一度サインインしてください。 現在のセッションはユーザー %1$s のものですが、あなたが提供している認証情報はユーザー %2$s のものです。この操作は${app_name}ではサポートされていません。 -\nまずデータをクリアし、その後、別のアカウントにサインインしてください。 - 暗号化されたメッセージがどの端末でも読めるように、サインインしてこの端末にのみ保存されている暗号鍵を取り戻してください。 +\nデータをクリアし、その後、別のアカウントにサインインしてください。 + 暗号化されたメッセージがどの端末でも読めるように、サインインしてこの端末にのみ保存されている暗号鍵を復旧してください。 あなたのホームサーバー(%1$s)の管理者があなたを%2$sのアカウントからサインアウトしました(%3$s)。 いくつかの原因が考えられます: \n @@ -2052,10 +2052,10 @@ リクエストが多すぎます。%1$d秒後に再試行できます… - このホームサーバーは古いバージョンです。管理者にアップグレードを要請してください。続行できますが、いくつかの機能が正しく作動しない可能性があります。 + このホームサーバーは古いバージョンです。管理者にアップグレードを依頼してください。続行できますが、いくつかの機能が正しく作動しない可能性があります。 ホームサーバーのバージョンが古すぎます - ただいま%1$sにメールを送信しました。 -\nアカウント登録を続行するにはメール内のリンクをクリックしてください。 + %1$sにメールを送信しました。 +\nアカウント登録を続行するには、メール内のリンクをクリックしてください。 CAPTCHA認証を行ってください アカウントがまだ作成されていません。登録を中止しますか? %1$sにアカウント登録 @@ -2074,8 +2074,8 @@ サーバー上の暗号鍵をバックアップして、暗号化されたメッセージとデータへのアクセスが失われるのを防ぎましょう。 いまキャンセルすると、ログインできなくなった際に、暗号化されたメッセージとデータを失ってしまう可能性があります。 \n -\nまた、設定から、安全なバックアップの設定や鍵の管理を行うことができます。 - USBメモリーもしくはバックアップドライブに保存 +\n設定から、セキュアバックアップの設定や鍵の管理を行うこともできます。 + USBメモリーやバックアップ用のドライブに保存 鍵のバックアップの設定 自己署名キーを同期しています ユーザーキーを同期しています @@ -2087,12 +2087,12 @@ 鍵は既に最新です! 鍵をリセット 質問は空にできません - ここで送受信されるメッセージはエンドツーエンド暗号化されています。 + ここでのメッセージはエンドツーエンドで暗号化されています。 \n -\nメッセージは安全に保護されており、メッセージのロックを解除できる固有の鍵を持っているのはあなたと受信者だけです。 - この部屋のメッセージはエンドツーエンド暗号化されています。 +\nメッセージは安全に保護されており、メッセージのロックを解除するための固有の鍵は、あなたと受信者だけが持っています。 + このルームのメッセージはエンドツーエンドで暗号化されています。 \n -\nメッセージは安全に保護されており、メッセージのロックを解除できる固有の鍵を持っているのはあなたと受信者だけです。 +\nメッセージは安全に保護されており、メッセージのロックを解除するための固有の鍵は、あなたと受信者だけが持っています。 ステートキー リカバリーキーを以下に保存 送信者が意図的に鍵を送信しなかったため、このメッセージにアクセスすることができません @@ -2109,8 +2109,8 @@ ナビゲーションのメニューを開く 承諾しました %sが承諾しました - %sが認証済 - %sを認証する + %sを認証済 + %sを認証 絵文字を比較して認証 絵文字を比較して認証 対面でない場合は、代わりに絵文字を比較してください @@ -2124,31 +2124,31 @@ あなただけが知っている秘密のパスワードを入力してください。バックアップ用にセキュリティーキーを生成します。 暗号化されたメッセージにアクセスするには、ログインを認証し、本人確認を行う必要があります。 暗号化されたメッセージにアクセスするには、あなたの他のセッションからログインを認証し、本人確認を行う必要があります。 - 詳しく知る + 詳細を確認 セキュリティーを高めるために、使い捨てコードが一致しているのを確認して、%sを認証しましょう。 暗号化の設定が正しくありません。 暗号化を復元 - 暗号化を有効な状態に取り戻すために、管理者に連絡してください。 + 暗号化を正常な状態に戻すために、管理者に連絡してください。 このユーザーとのメッセージはエンドツーエンドで暗号化されており、第三者が解読することはできません。 このコードを相手の画面に現れているコードと比較してください。 絵文字を比較して、同じ順番で現れていることを確認してください。 セキュリティーを高めるために、対面で行うか、他の通信手段を利用しましょう。 - 選択されたエモートを虹色にして送信します - 選択されたテキストを虹色にして送信します - ${app_name}がID%1$sのイベントを処理中にエラーが発生しました - ${app_name}は%1$sという種類のイベントに対応していません + 指定したエモートを虹色で送信 + 指定したテキストを虹色で送信 + ${app_name}は、ID \'%1$s\'のイベントのコンテンツを描画している際にエラーに遭遇しました + ${app_name}は\'%1$s\'という種類のイベントに対応していません 既読通知へ移動 大切に保護しましょう 完了! アカウントパスワードと違うものにしてください。 続行するには%sを入力してください。 認証を中止しました - 今中止すると、%1$s(%2$s)を認証しません。認証は相手のユーザープロフィール画面からもう一度開始できます。 + 中止すると、%1$s(%2$s)を認証しません。認証は、相手のユーザープロフィール画面から改めて開始できます。 中止すると、新しい端末では暗号化されたメッセージが読めず、他のユーザーに信頼されません 中止すると、この端末では暗号化されたメッセージが読めず、他のユーザーに信頼されません - 自分ではない + ログインしていません 新しいセッションを認証して、暗号化されたメッセージにアクセスできるようにしましょう。 - 新しいログイン。あなたですか? + 新しいログインです。ログインしましたか? ${app_name} Android ルームの管理者によって削除されています。理由:%1$s ユーザーによって削除されています。理由:%1$s @@ -2157,17 +2157,17 @@ 既存のセッションにアクセスできない場合 %1$sというタイプのアカウントデータを削除しますか? \n -\n予期しないトラブルを起こす可能性があるので注意してください。 +\n予期しない動作が起こる可能性があるため、注意して使用してください。 %1$s(%2$s)が新しいセッションでサインインしました: - このセッションは%1$s(%2$s)によって認証されているので、メッセージのセキュリティは信頼できます。 - 既存のセッションでこのセッションを認証して、暗号化されたメッセージへアクセスできるようにしましょう。 - あなたはこのセッションを認証しているので、メッセージのセキュリティは信頼できます。 + このセッションは%1$s(%2$s)によって認証されているので、メッセージのセキュリティーは信頼できます。 + 既存のセッションでこのセッションを認証して、暗号化されたメッセージにアクセスできるようにしましょう。 + あなたはこのセッションを認証しているので、メッセージのセキュリティーは信頼できます。 利用可能な暗号情報がありません 既定のバージョン 非公開のルームとダイレクトメッセージにおけるエンドツーエンド暗号化は、あなたのサーバーの管理者により既定として無効にされています。 理由を含める - %1$sが%2$sの権限レベルを変更しました。 - %1$sの権限レベルを変更しました。 + %1$sが%2$s変更しました。 + %1$s変更しました。 誰と使いますか? 作成するスペースの種類を選択してください 自分とチームメイトの非公開のスペース @@ -2177,9 +2177,9 @@ もう少しです!確認を待機しています… あと少しです!もう一方の端末は同じマークを表示していますか? %sを待機しています… - このユーザーがこのセッションを認証するまで、送受信されるメッセージには警告マークが付きます。手動で認証することも可能です。 + このユーザーがこのセッションを認証するまで、送受信されるメッセージには警告マークが付きます。手動で認証することもできます。 セッションの取得に失敗しました - チームの仲間を招待しましょう + 誰がチームの仲間ですか? %sを探せるようになります 私のスペース %1$s %2$s に参加してください スキップ @@ -2188,10 +2188,10 @@ ディスカバリーの設定を終了します。 ここの参加者はあなただけです。退出すると、今後あなたを含めて誰も参加できなくなります。 再び招待されない限り、再参加することはできません。 - あなたはこのスペースの唯一の管理者です。退出すると、誰もそれをコントロールすることができなくなります。 + あなたはこのスペースの唯一の管理者です。退出すると、誰もそれを管理できなくなります。 名前のないルーム - このルームはホームサーバーが不安定と判断したルームバージョン%sで動作しています。 - 申し訳ありませんが、%sに参加する途中で問題が発生しました + このルームは、ホームサーバーが不安定と判断したルームバージョン%sで動作しています。 + 申し訳ありませんが、%sに参加する際に問題が発生しました このルームへの招待が、アカウントに関連付けられていないメールアドレス %s に送られました 投票がありません ルーム全体に通知 @@ -2207,14 +2207,14 @@ 再認証が必要です 全てリセット 連絡先 - 認証をキャンセルしました。あらためて開始してください。 + 認証をキャンセルしました。改めて開始してください。 押し続けて録音し、離すと送信 PINコードを設定してください %d個のサーバーアクセス制御リストの変更 置き換えられたルームに参加 - このルームが発見できません。存在することを確認してください。 + このルームを発見できません。存在することを確認してください。 指紋や顔画像など、端末に固有の生体認証を有効にする。 絵文字で認証 テキストを使って手動で認証 @@ -2234,11 +2234,11 @@ 未読のメッセージ数のみを通知に表示。 2分間${app_name}を使用しないと、PINコードが要求されます。 🔐️ ${app_name}で話しましょう - 個人情報保護の観点から、${app_name}はハッシュ化されたメールアドレスと電話番号の送信のみをサポートしています。 + プライバシーの保護の観点から、${app_name}はハッシュ化されたメールアドレスと電話番号の送信のみをサポートしています。 アプリの名前を変更しました!アプリは最新版で、アカウントにはログイン済です。 ステートイベントを送信 ステートイベント - カスタムのステートイベントを送信 + ユーザー定義のステートイベントを送信 ステートイベントを送信しました! 続行するには名前を設定してください。 どんな作業に取り組みますか? @@ -2259,7 +2259,7 @@ \n \n続行してよろしいですか? このリンクを再確認してください - ログインを認証してください:%1$s + 新しいログインがあなたのアカウントにアクセスしています。ログインを認証してください:%1$s 機密ストレージのアクセスに失敗しました この設定を有効にすると、全てのアクティビティーにFLAG_SECUREを追加します。変更を有効にするにはアプリケーションの再起動が必要です。 このアカウントは無効化されています。 @@ -2267,7 +2267,7 @@ 印刷して安全な場所に保管 %2$sと%1$sが設定されました。 \n -\n安全な場所で保管してください!それらは、アクティブなセッションを全て失ってしまった際、暗号化されたメッセージや安全な情報のロックを解除するために必要となります。 +\n安全な場所で保管してください!アクティブなセッションを全て失ってしまった際、暗号化されたメッセージや安全な情報のロックを解除するために必要となります。 作成したアイデンティティーキーを公開しています アプリケーションのスクリーンショットを防ぐ 続行するには%1$sか%2$sを使用してください。 @@ -2307,7 +2307,7 @@ ビデオ通話が拒否されました 音声通話が拒否されました %1$sは通話を拒否しました - このデバイスを認証可能な他の端末が全くない場合にのみ、続行してください。 + この端末を認証できる他の端末が全くない場合にのみ、続行してください。 このセッションを信頼済として認証すると、暗号化されたメッセージにアクセスすることができます。このアカウントにサインインしなかった場合は、あなたのアカウントのセキュリティーが破られている可能性があります: アカウントのセキュリティーが破られている可能性があります 選択したスペースに追加 @@ -2318,7 +2318,7 @@ 鍵のバックアップの機密情報をSSSSに保存しています あなたしか知らないセキュリティーフレーズを入力してください。サーバーで機密情報を保護するために使用します。 詳細を非表示 - 他の参加者はいません。%sに招待しましょう。 + まだ他の参加者はいません。%sに招待しましょう。 位置情報(ライブ)が有効です 現在の位置情報を共有 現在の位置情報を共有 @@ -2328,7 +2328,7 @@ 保存して続行 設定画面からいつでもプロフィールを更新できます - これは後から変更できます。 + 表示名にプロフィール画像を追加しましょう プロフィール画像を追加 これは後から変更できます 表示名 @@ -2340,14 +2340,14 @@ 表示名を選択 あなたのアカウント %s が作成されました おめでとうございます! - 近日中にスレッドはベータ版となります。 + 近日中にスレッド機能はベータ版となります。 \n \nその準備として、この時点以前に作成されたスレッドは、通常の返信として表示するように変更します。 \n -\nスレッドはMatrixの仕様の一部になったため、これは一度限りの変更です。 - スレッドはベータ版になります 🎉 +\nスレッド機能はMatrixの仕様の一部になったため、これは一度限りの変更です。 + スレッド機能はベータ版になります 🎉 無効にする - スレッドについてのフィードバック + スレッド機能についてのフィードバック フィードバックを送信 ベータ版 ベータ版 @@ -2363,7 +2363,7 @@ ${app_name}をシンプルにするために、タブはオプションになりました。右上のメニューから管理できます。 新しいレイアウトにようこそ! アニメーション画像を自動再生 - エンドポイントのホームサーバーへの登録に失敗しました: + エンドポイントのホームサーバーへの登録に失敗しました: \n%1$s エンドポイントがホームサーバーに登録されました。 エンドポイントの登録 @@ -2371,14 +2371,14 @@ ${app_name}は通知の表示に権限が必要です。 \n権限を与えてください。 - %1$sと他%2$d名 + %1$sと他%2$d人 %1$sと%2$s ホームサーバーがサポートしていないため、スレッド機能は不安定かもしれません。スレッドのメッセージが安定して表示されないおそれがあります。%sスレッド機能を有効にしてよろしいですか? - スレッド(ベータ版) - スレッドを用いると、会話のテーマを保ったり、会話を追跡したりするのが容易になります。%sスレッドを有効にするとアプリケーションが再起動します。再起動には時間がかかる可能性があります。 - スレッド(ベータ版) - ${app_name}は通知を表示するために許可を必要としています。通知にはメッセージや招待などが表示されます。 + スレッド機能(ベータ版) + スレッド機能を使うと、会話のテーマを維持したり、会話を簡単に追跡したりすることができます。%sスレッド機能を有効にするとアプリケーションが再起動します。再起動には時間がかかる可能性があります。 + スレッド機能(ベータ版) + ${app_name}は、通知を表示するための権限を必要としています。通知にはメッセージや招待などが表示されます。 \n \n通知を表示するには、次のポップアップでアクセスを許可してください。 メールアドレスが認証されていません。メールボックスを確認してください @@ -2399,7 +2399,7 @@ このステップをスキップ 問題ありません! 進みましょう - ユーザー名 / メールアドレス / 電話番号 + ユーザー名 / 電子メール / 電話番号 あなたは人間ですか? %sに送信された手順に従ってください。 パスワードを再設定 @@ -2410,7 +2410,7 @@ メールアドレスを認証 コードを再送信 コードが%sに送信されました - 電話番号を確認してください + 電話番号を確認 全ての端末からサインアウト パスワードを再設定 パスワードは8文字以上に設定してください。 @@ -2427,7 +2427,7 @@ リッチテキストエディターを有効にする 最初のメッセージを送信する際にダイレクトメッセージを作成 遅延DMを有効にする - スペースがありません。 + まだスペースがありません。 新しいレイアウトを有効にする アクティビティー順 アルファベット順 @@ -2445,12 +2445,12 @@ \n \nアプリケーションが再起動します。再起動には時間がかかる可能性があります。 初期同期のリクエスト - %sの子スペースを折りたたむ - %sの子スペースを展開 + %sのサブスペースを折りたたむ + %sのサブスペースを展開 ルームを探す スペースを変更 ルームを作成 - チャットを開始 + 会話を開始 全ての会話 ${app_name}にようこそ、 \n%s。 @@ -2481,7 +2481,7 @@ 音声配信を終了しました。 %1$sが音声配信を終了しました。 - %1$dを選択しました + %1$d個選択済 有効にすると、このアプリケーションを使用している際にも、他のユーザーにオフラインとして表示されます。 最近のチャットをシステムの共有メニューに表示 @@ -2490,7 +2490,7 @@ 自動的に設定 フォントの大きさを選択 ⚠ 未認証の端末がこのルームにあります。あなたが送信するメッセージを復号化することはできません。 - このルームの未認証のセッションに暗号化されたメッセージを送信しない。 + このルームの未認証のセッションに対して暗号化されたメッセージを送信しない。 あなたのホームサーバーはスレッドの一覧表示をまだサポートしていません。 ここに新しいリクエストと招待が表示されます。 リッチテキストエディターを試してみる(プレーンテキストモードは近日公開) @@ -2503,11 +2503,11 @@ %1$d日以上使用されていません(%2$s) 地図を読み込めません -\nこのホームサーバーは地図が読み込むよう設定されていないおそれがあります。 +\nこのホームサーバーは地図が読み込めるように設定されていないおそれがあります。 スペースは、ルームや連絡先をまとめる新しい方法です。右下のボタンを使うと、既存のルームを追加したり新たに作成したりできます。 セキュリティーに関する勧告 その他のセッション - セキュリティーを最大限に高めるには、不明なセッションや利用していないセッションからサインアウトしてください。 + セキュリティーを最大限に高めるには、不明なセッションや使用していないセッションからサインアウトしてください。 生体認証を有効にできませんでした。 関連付けに失敗しました。 おかえりなさい! @@ -2524,9 +2524,9 @@ ルームのタイムラインで音声配信を録音して送信することを可能にします。 音声配信を有効にする 未読のメッセージがある場合は、ここに表示されます。 - 報告することはありません。 + 未読はありません。 クライアントの情報の保存を有効にする - セッション名は連絡先にも表示されます。 + セッション名は連絡先に対しても表示されます。ご注意ください。 セッション名を設定すると、端末をより簡単に認識できるようになります。 このセッションでプッシュ通知を受信。 絞り込みを解除 @@ -2541,7 +2541,7 @@ 未認証のセッションはありません。 認証済のセッションはありません。 - 使用していない古いセッション(%1$d日以上使用されていません)からサインアウトすることを検討してください。 + 使用していない古いセッション(%1$d日以上使用されていません)からのサインアウトを検討してください。 非アクティブ セッションを認証すると、より安全なメッセージのやりとりが可能になります。見覚えのない、または使用していないセッションがあれば、サインアウトしましょう。 @@ -2614,7 +2614,7 @@ Googleサービス 通知の受信方法を選択してください 画面を共有しています - ${app_name}画面共有 + ${app_name}の画面共有 テキストの装飾 連絡先 カメラ @@ -2631,14 +2631,14 @@ 一時的な実装。位置情報がルームの履歴に残ります 位置情報(ライブ)の共有を有効にする 位置情報を共有しています - ${app_name}位置情報(ライブ) + ${app_name}の位置情報(ライブ) 残り%1$s 位置情報(ライブ)を表示 位置情報(ライブ)が終了しました 位置情報(ライブ)を読み込んでいます… 8時間 1時間 - 15分 + 15分間 位置情報(ライブ)を共有する時間 現在の位置にズーム 地図で選択した位置情報のピン @@ -2656,7 +2656,7 @@ ライブ配信を終了してよろしいですか?配信を終了し、録音をこのルームで利用できるよう設定します。 ライブ配信を停止しますか? 残り%1$s - 接続エラー - 録音を停止しました + 接続エラー - 録音を一時停止しました この音声配信を再生できません。 既に音声配信を録音しています。新しく始めるには今の音声配信を終了してください。 他の人が既に音声配信を録音しています。新しく始めるには音声配信が終わるまで待機してください。 @@ -2704,7 +2704,7 @@ %sの利用規約と運営方針を確認してください サーバーの運営方針 問い合わせる - 自分でサーバーを運営したいですか? + 自分でサーバーを運営しますか? サーバーのURL ホームサーバーのアドレスを入力してください あなたのホームサーバーのアドレスを入力してください。ここにあなたの全てのデータがホストされます @@ -2713,7 +2713,7 @@ 8文字以上にしてください アカウントを作成 ホームに移動 - プロフィールを変更 + プロフィールを設定 ${app_name}は職場利用にも最適です。世界で最も安全な組織によって信頼されています。 音声配信 スペースの一覧を開く @@ -2746,11 +2746,11 @@ このホームサーバーは数字だけからなるユーザー名を承諾しません。 最初のメッセージを送信すると、%sを会話に招待 Nightly build - あなたの他の端末でコードをスキャンするか、もしくは反対に、このデバイスでスキャンしてください + あなたの他の端末でコードをスキャンするか、もしくは反対に、この端末でスキャンしてください Element Matrix Services(EMS)は、高速、安全でリアルタイムのコミュニケーション向きの、堅牢で安定したホスティングサービスです。<a href=\"${ftue_ems_url}\">element.io/ems</a>で方法を調べましょう。 アカウントにサインインするサーバー アカウントを作成するサーバー - スレッドは、改良した通知など新機能の追加作業中です。フィードバックをお聞かせください! + スレッド機能については、改良した通知など新機能の追加などを行っています。フィードバックをお聞かせください! 🔒 セキュリティーの設定で、全てのルームに関して認証済のセッションにのみ暗号化を行うよう設定しました。 プレゼンス(ステータス表示) 取り込み中 @@ -2800,7 +2800,7 @@ %s \nは空です。 クライアントの名称、バージョン、URLを記録し、セッションマネージャーでより容易にセッションを認識できるよう設定。 - セッション名を変更 + セッション名の変更 絞り込む 直近のオンライン日時 %1$s @@ -2811,7 +2811,7 @@ 未認証・直近のオンライン日時 %1$s 認証済・直近のオンライン日時 %1$s 現在のセッションを認証すると、このセッションの認証の状態を確認できます。 - セキュリティーを最大限に高めるには、セッションを認証し、不明なセッションや利用していないセッションからサインアウトしてください。 + セキュリティーを最大限に高めるには、セッションを認証し、不明なセッションや使用していないセッションからサインアウトしてください。 Element Callウィジェットを自動で承認し、カメラまたはマイクのアクセス権を付与 Element Callの権限のショートカットを有効にする 現在のゲートウェイ:%s @@ -2854,4 +2854,18 @@ 認証済のセッションは、パスフレーズの入力、または他の認証済のセッションで本人確認を行ったセッションです。 \n \n認証済のセッションには、暗号化されたメッセージを復号化する際に使用する全ての鍵が備わっています。また、他のユーザーに対しては、あなたがこのセッションを信頼していることが表示されます。 + %1$s タップして戻る + もう一方の端末でサインインしてください。 + 携帯端末にサインインしますか? + サインインの画面で開始 + サインインの画面で開始 + セキュアなメッセージのやり取りを設定している際に、セキュリティー上の問題に遭遇しました。あなたのホームサーバー、インターネット接続、端末のいずれかの安全性が損なわれている可能性があります。 + コードブロックの表示を切り替える + あなたのアカウントの詳細は%1$sで管理されています。 + 引用の表示を切り替える + インデントを減らす + インデントを増やす + アンケートをタイムラインに表示 + この音声配信を復号化できません。 + アカウント \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-lv/strings.xml b/library/ui-strings/src/main/res/values-lv/strings.xml index 9201bf146a..81a7a10dcc 100644 --- a/library/ui-strings/src/main/res/values-lv/strings.xml +++ b/library/ui-strings/src/main/res/values-lv/strings.xml @@ -39,7 +39,6 @@ Uzaicinājums uz istabu %1$s un %2$s Tukša istaba - Jūs nomainījāt savu parādāmo vārdu no %1$s uz %2$s Jūs nomainījāt savu parādāmo vārdu uz %1$s Jūs nomainījāt savu avataru @@ -222,7 +221,6 @@ Dzēst Pārdēvēt Ziņot par saturu - vai Uzaicināt Izrakstīties @@ -245,7 +243,6 @@ Vienīgi Matrix kontakti Nav rezultātu Istabas - Nosūtīt logfailus Nosūtīt sistēmas avārijas logfailus Nosūtīt ekrānattēlu @@ -272,7 +269,7 @@ Gaiša tēma Tumša tēma Melna tēma - Notikumu monitorings + Uztver notikumus Skaņas paziņojumi Klusi paziņojumi Kļūdas atskaite @@ -285,10 +282,8 @@ Šķiet ievadīta nederīga epasta adrese Šī epasta adrese jau tiek izmantota. Aizmirsāt paroli\? - Mājasservers vēlas pārbaudīt, vai neesat robots Neizdevās verificēt epasta adresi: pārbaudiet, vai esi noklikšķinājis(usi) uz saiti atsūtītajā epastā - Ievadi korektu URL adresi Bojāts JSON Nav derīgs JSON @@ -305,15 +300,10 @@ Notiek zvans… Adresāts neatbildēja uz zvanu. Element informācija - - ${app_name}-am nepieciešama atļauja piekļūt mikrofonam, lai nodrošinātu audio zvanus. - ${app_name} nepieciešama atļauja piekļūt kamerai un mikrofonam, lai veiktu videozvanus. \n \nLūdzu, dodiet piekļuves atļauju nākamajā uznirstošajā logā, lai būtu iespēja veikt zvanus. - - Turpināt @@ -321,7 +311,6 @@ Pievienoties Noraidīt Pāriet uz pirmo neizlasīto ziņu - Pamest istabu Vai tiešām vēlies pamest istabu\? TIEŠIE ČATI @@ -349,7 +338,6 @@ Servera sertifikāts ir izmanījies un Tava ierīce tam tagad neuzticas. Tas ir ĻOTI NEPARASTI. Iesakām NEUZTICĒTIES šim jaunajam sertifikātam. Sertifikāts izmainījās no iepriekš uzticama uz neuzticamu. Iespējams, serveris ir aktualizējis savu sertifikātu. Sazinies ar servera administratoru, lai saņemtu sagaidāmo nospiedumu (fingerprint). Akceptē sertifikātu TIKAI tad, kad servera administrators publicējis sertifikāta nospiedumu, kurš atbilst augstāk redzamajam. - Meklēšana Istabas dalībnieku filtrs Rezultātu nav @@ -407,7 +395,6 @@ Ierīces nosaukums Pēdējo reizi manīts %1$s @ %2$s - Autentifikācija Pierakstījies kā Mājasserveris @@ -450,7 +437,6 @@ %d biedru izmaiņas Biedru katalogs - %d biedri %d biedrs @@ -461,11 +447,8 @@ %d jauna ziņa %d jaunas ziņas - - Iestatīt kā galveno adresi Atiestatīt kā galveno adresi - Tēma Atšifrēšanas kļūda Ierīces nosaukums @@ -475,9 +458,8 @@ Eksportēt istabas atslēgas Eksportēt atslēgas vietējā failā Eksportēt - Ievadiet frāzveida paroli - Apstiprināt frāzveida paroli - + Ievadīt paroles vārdkopu + Apstiprināt paroles vārdkopu Importēt E2E istabas atslēgas Importēt istabas atslēgas Importēt atslēgas no vietējā faila @@ -490,17 +472,15 @@ Apstipriniet, salīdzinot sekojošo ar lietotāja iestatījumiem citā savā sesijā: Ja tā sakrīt, nospied zemāk esošo verifikācijas pogu. Ja tā nesakrīt, tad kāds ir pārtvēris šo ierīci un Tu droši vien vēlies šo ierīci pievienot melnajam sarakstam. Nākotnē šī pārbaudes procedūra plānota sarežģītāka. - Izvēlies istabu katalogu Mājasservera nosaukums Visas istabas %s serverī Visas vietējās %s istabas - %d nelasīta paziņota ziņa - %d nelasītas paziņotas ziņas - %d nelasītu paziņotu ziņu + %d nelasītu paziņotu ziņu + %d nelasīta paziņota ziņa + %d nelasītas paziņotas ziņu - %d istaba %d istabas @@ -908,8 +888,6 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Nosūtīja jums uzaicinājumu %s vēlas verificēt jūsu sesiju Verifikācijas pieprasījums - - Nodrošinieties pret piekļuves zaudēšanu šifrētām ziņām un datiem Neparedzēta kļūda Izveidot frāzveida paroli @@ -936,7 +914,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Iztrūks obligāts parametrs. %1$s: %2$s %3$s %1$s: %2$s - ** Neizdevās nosūtīt - atveriet istabu + ** Neizdevās nosūtīt - lūgums atvērt istabu Es Jauns uzaicinājums Jaunas ziņas @@ -1036,7 +1014,6 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. %1$d cilvēki %1$d cilvēki - Gaida %s… Verificēts %s Verificē %s @@ -1159,7 +1136,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Jūs neizmantojat nevienu identitāšu serveri Rezerves kopiju nevarēja atšifrēt ar šo atkopšanās atslēgu: lūdzu, pārbaudiet, vai ievadījāt pareizo atkopšanās atslēgu. Kalkulē atkopšanās atslēgu… - Frāzveida parole pārāk vāja + Paroles vārdkopa ir pārāk vāja Nosūta doto ziņu ar sniegu Nosūta doto ziņu ar konfeti Atbastīta tikai šifrētās istabās @@ -1247,8 +1224,8 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Gandrīz galā! Vai %s redzams tas pats vairogs\? Kamēr šis lietotājs nav padarījis šo sesiju uzticamu, ziņas uz un no tās ir marķētas ar brīdinājumiem. Alternatīvi, jūs varat manuāli verificēt šos sesiju. %1$s (%2$s) pierakstījās, izmantojot jaunu sesiju: - Lūdzu, ievadiet frāzveida paroli - Frāzveida paroles nesakrīt + Lūgums ievadīt paroles vārdkopu + Paroles vārdkopa nesakrīt Piekļuve istabai Pārvaldiet epasta adreses un tālruņu numurus, kas saistīti ar jūsu Matrix kontu Epasti un tālruņa numuri @@ -1290,7 +1267,6 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Jauna šifrēto ziņu atslēga Izmantot Atslēgu Dublēšanu Nekad nezaudējiet šifrētās ziņas - Dzēst savu dublēto šifrēšanas atslēgu no servera\? Jūs vairs nevarēsiet izmantot atkopšanas atsļēgu, lai lasītu šifrēto ziņu vēsturi. Izdzēst dublējumu Pārbauda dublējuma statusu @@ -1303,10 +1279,10 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Izmantojiet savu Atgūšanas Atslēgu, lai atbloķētu savu šifrēto ziņu vēsturi izmantojiet savu atgūšanas atslēgu Lietotāju ielūgšana, izmešana un aizliegšana tika neaizskarta. - Konfigurēt Skaļās Notifikācijas + Uzstādīt skaļos paziņojumus Izslēgt ierobežojumus Pārbaudīt fona ierobežojumus - Notifikācija tika nospiesta! + Paziņojumam tika piesists! Neizdevās reģistrēt FCM žetonu mājasserverī: \n%1$s FCM žetons veiksmīgi reģistrēts mājasserverī. @@ -1322,19 +1298,17 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. \nPievienojiet dažas tagad\? Neizdevās izveidot reālā laika savienojumu. \nLūdzu, jautājiet sava servera administratoram, lai konfigurētu PAGRIEZIENA serveri, lai zvani strādātu uzticami. - Izmantojiet Integrācijas Pārvaldnieku, lai pārvaldītu botus, tiltus, logrīkus un uzlīmes. \nIntegrācijas Pārvaldnieks saņem konfigurācijas datus un var modificēt logrīkus, sūtīt istabu uzaicinājumus jūsu vārdā. - - Jūs nesaņemsiet jaunas notifikācijas, kad aplikācija ir fonā. + Par ienākošajiem ziņojumiem netiks paziņots, kad lietotne darbojas fonā. Bez fona sinhronizācijas Optimizēts reālajam laikam Fona Sinhronizācijas Režīms Izvēlaites LED krāsu, vibrāciju, skaņu… - Konfigurēt Klusās Notifikācijas - Konfigurēt Zvanu Notifikācijas + Uzstādīt klusos paziņojumus + Uzstādīt zvanu paziņojumus Pakalpojums startēsies, kad ierīce būs restartēta. - Notifikāciju Displejs + Paziņojumu attēlošana Google Play Servisu APK ir pieejams un atjaunināts. Filtrēt aizliegtos lietotājus Lūdzu palaidiet ${app_name} citā ierīcē, kas var atšifrēt ziņu, lai tā varētu nosūtīt šīs sesijas atslēgas. @@ -1380,21 +1354,20 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Lūdzu uztaisiet kopiju Stop Aizvietot - Dublējums jau pastāv jūsu mājasserverī - Atgūšanas atslēgas tika saglabātas. - - Saglabāt kā Failu + Rezerves kopija jau pastāv jūsu mājasserverī + Atkopšanas atslēgas tika saglabātas. + Saglabāt datnē Saglabāt Atgūšanas Atslēgu Es uztaisīju kopiju Saglabājiet savu atgūšanas atslēgu kaut kur ļoti drošā vietā, piemēram, paroles pārvaldniekā (vai seifā) Jūsu atkopšanas atslēga ir drošības tīkls - to var izmantot, lai atjaunotu piekļuvi jūsu šifrētajām ziņām, ja esat aizmirsis savu paroles frāzi. \nSaglabājiet savu atgūšanas atslēgu kaut kur ļoti drošā vietā, piemēram, paroles pārvaldniekā (vai seifā) - Jūsu atslēgas tiek dublētas. - Izdevās ! - (Advancēti) Iestatīt ar Atgūšanas Atslēgu - Vai, aizsargājiet jūsu dublējumu ar Atgūšanas Atslēgu, saglabājot to drošā vietā. - Taisa Dublējumu - Iestatīt Frāzi + Tiek veidota atslēgu rezerves kopija. + Izdevās! + (Papildu) Uzstādīt ar atkopšanas atslēgu + Vai aizsargā rezerves kopiju ar atkopšanas atslēgu, saglabājot to drošā vietā. + Veido rezerves kopiju + Uzstādīt paroles vārdkopu Mēs saglabāsim šifrētu jūsu atslēgu kopiju savā serverī. Aizsargājiet savu dublējumu ar frāzi, lai tā būtu droši aizsargāta. \n \nLai nodrošinātu maksimālu drošību, tam jāatšķiras no jūsu konta paroles. @@ -1454,7 +1427,6 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Publicēt jaunu adresi manuāli Šī ir galvenā adrese ${app_name} apkopo anonīmu analītiku, lai ļautu mums uzlabot aplikāciju. - Šis aizvietos jūsu esošo Atslēgu vai Frāzi. Izveidot jaunu Drošības Atslēgu vai iestatīt jaunu Drošības Frāzi jūsu esošajam dublējumam. Iestatīt uz šīs ierīces @@ -1509,7 +1481,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Pārbaudes diagnostika Paziņojumu pārbaude Paziņojumu svarīgums - Advancēti Notifikāciju Iestatījumi + Paziņojumu papildu iestatījumi Jūs apturējāt zvanu %s apturēja zvanu Apturēt @@ -1657,8 +1629,6 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Kopīgojiet atkopšanas atslēgu ar… Lūdzu, %s, lai turpinātu izmantot šo pakalpojumu. Lūdzu, %s, lai palielinātu šo limitu. - - Palaidiet sistēmas kameru, nevis pielāgotās kameras ekrānu. Lasīt DRM aizsargātu multividi Izmantojot to, var kopīgot datus ar %s: @@ -1700,7 +1670,6 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. %d izvēlēti Modificēt logrīku - Trūkst atļauju Lai veiktu šo darbību, lūdzu, piešķiriet kamerai atļauju sistēmas iestatījumos. Lai veiktu šo darbību, trūkst dažu atļauju. Lūdzu, sistēmas Iestatījumos piešķiriet atļaujas. @@ -1778,7 +1747,6 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Savienoties %1$s Pieskarieties, lai atgrieztos Aktīvs zvans (%1$s) · - Aktīvs zvans (%1$s) Tika pieļauta kļūda, meklējot tālruņa numuru Zvanu taustiņi @@ -1833,7 +1801,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Neatļaut ekrānšāviņus lietotnē Izmantot failu Ievadiet savu %s, lai turpinātu - Notifikāciju iestatījumi + Paziņojumu uzstādīšana Neizdevās importēt atslēgas %s, lai ļautu cilvēkiem uzzināt, par ko ir šī istaba. Nokopējiet to uz personālās mākoņa krātuves @@ -1945,7 +1913,6 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. \n• Jūsu servera administrators ir anulējis jūsu piekļuvi drošības apsvērumu dēļ. Neizskatās pēc derīgas e-pasta adreses Nosūta doto ziņojumu kā pārsteigumu - %1$s pie %2$s Šajā istabā nav multivides Multivide @@ -1961,7 +1928,6 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Atveriet izvēlni izveidot telpu Atveriet navigācijas atvilktni Izskatās, ka serveris pārāk ilgi neatbild, to var izraisīt slikts savienojums vai servera kļūda. Lūdzu, pēc brīža mēģiniet vēlreiz. - Identitātes serveris nesniedz nekādu politiku Slēpt identitātes servera politiku Parādīt identitātes servera politiku @@ -2000,7 +1966,6 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Šifrētās tiešās ziņas Tiešās ziņas Mans lietotājvārds - Neizdevās iegūt jaunāko atjaunošanas atslēgu versiju (%s). %d jaunu atslēgu tika pievienotas šai sesijai. @@ -2011,7 +1976,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Iegūst rezerves kopiju… Atjaunošanas atslēgas ģenerēšana, izmantojot paroli, šis process var aizņemt vairākas sekundes. Izskatās, ka jums jau ir izveidots atslēgas dublējums no citas sesijas. Vai vēlaties to aizstāt ar izveidoto\? - Lūdzu, dzēsiet parolesfrāzi, ja vēlaties, lai ${app_name} ģenerētu atkopšanas atslēgu. + Lūgums izdzēst paroles vārdkopu, ja ir vēlams, lai ${app_name} izveidotu atkopšanas atslēgu. Nav atrasts derīgs Google Play Services APK. Paziņojumi var nedarboties pareizi. Marķēšana ir izslēgta. Marķēšana ir ieslēgta. @@ -2063,9 +2028,6 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. izmetot lietotāju, noņems viņu no šīs Telpas. \n \nLai novērstu viņu atkārtotu pievienošanos, jums tā vietā vajadzētu viņiem aizliegt pievienoties. - - - Beidz zvanu… Nav atbildes Lietotājs, kuram zvanījāt, ir aizņemts. @@ -2091,4 +2053,18 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Lai sūtītu balss ziņojumus, piešķiriet Mikrofona atļauju. Telpas Uztver paziņojumus - + Atvērt aptauju + Sniegt atgriezenisko saiti + Piesist augšējā labajā stūrī, lai redzētu iespēju sniegt atgriezenisko saiti. + Tu beidzi balss pārraidi. + %1$s izbeidza balss pārraidi. + Izmantot ierīci, kurā veikta pieteikšanās, lai nolasītu zemāk esošo kvadrātkodu: + Nolasīt zemāk esošo kvadrātkodu ar ierīci, kurā ir notikusi atteikšanās. + Pieteikties ar kvadrātkodu + Jāizmanto ierīces kamera, lai nolasītu citā ierīcē attēlotu kvadrātkodu: + Nolasīt kvadrātkodu + 3 + 2 + 1 + Izmēģināt + \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-pl/strings.xml b/library/ui-strings/src/main/res/values-pl/strings.xml index 2bdd6e806d..0c87b20780 100644 --- a/library/ui-strings/src/main/res/values-pl/strings.xml +++ b/library/ui-strings/src/main/res/values-pl/strings.xml @@ -2827,4 +2827,5 @@ Witaj w ${app_name}, \n%s. Wszechstronna, bezpieczna aplikacja do czatowania dla zespołów, przyjaciół i organizacji. Utwórz czat lub dołącz do istniejącego pokoju, aby rozpocząć. + Twój token dostępu zapewnia pełny dostęp do Twojego konta. Nie udostępniaj go nikomu. \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-sk/strings.xml b/library/ui-strings/src/main/res/values-sk/strings.xml index 82deefb371..d825518e0f 100644 --- a/library/ui-strings/src/main/res/values-sk/strings.xml +++ b/library/ui-strings/src/main/res/values-sk/strings.xml @@ -2980,4 +2980,13 @@ Nemožno spustiť hlasovú správu Chyba pripojenia - nahrávanie pozastavené Použiť formát riadkového kódu + Prepnutie bloku kódu + Prepínanie citácie + Zrušiť odsadenie + Odsadenie + Zobraziť anketu na časovej osi + Toto hlasové vysielanie sa nedá dešifrovať. + Údaje o vašom účte sú spravované samostatne na %1$s. + Účet + Pri aktualizácii vašich predvolieb oznámení došlo k chybe. Skúste prosím znova. \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-sq/strings.xml b/library/ui-strings/src/main/res/values-sq/strings.xml index 447a2b52d7..50f7543f67 100644 --- a/library/ui-strings/src/main/res/values-sq/strings.xml +++ b/library/ui-strings/src/main/res/values-sq/strings.xml @@ -2906,4 +2906,13 @@ Gabim lidhjeje - Incizimi u ndal S’mund të nisni një mesazh zanor teksa jeni aktualisht duke incizuar një transmetim të drejtpërdrejtë. Ju lutemi, përfundoni transmetimin tuaj të drejtpërdrejtë, që të mund të nisni incizimin e një mesazhi zanor S’niset dot mesazh zanor + Shfaq/fshih bllok kodi + Shfaq/fshih citim + Hiqe zhvendosjen e kryeradhës + Zhvendos kryeradhën + Shihni pyetësor në rrjedhë kohore + S’arrihet të shfeshtëzohet ky transmetim zanor. + Hollësitë e llogarisë tuaj administrohen më vete, te %1$s. + Llogari + Ndodhi një gabim, kur u përditësuan parapëlqimet tuaja për njoftime. Ju lutemi, riprovoni. \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-sv/strings.xml b/library/ui-strings/src/main/res/values-sv/strings.xml index caf9913299..1c9688e99c 100644 --- a/library/ui-strings/src/main/res/values-sv/strings.xml +++ b/library/ui-strings/src/main/res/values-sv/strings.xml @@ -326,7 +326,7 @@ \nLägg till några nu\? Tyvärr har ingen extern applikation hittats som kan fullfölja denna handling. Logga in - Logga in med externt konto + Logga in med samlad inloggning Skicka in Fel användarnamn och/eller lösenord Det här ser inte ut som en giltig e-postadress @@ -1820,7 +1820,7 @@ Skickar det givna meddelandet med snöfall Skickar de givna meddelandet med konfetti Rensa historik - externt konto + samlad inloggning Logga in med %s Skapa konto med %s Fortsätt med %s @@ -2919,4 +2919,13 @@ Du kan inte påbörja ett röstmeddelande eftersom du för närvarande spelar in en röstsändning. Vänligen avsluta din röstsändning för att börja spela in ett röstmeddelande Kan inte starta röstsändning Startade en röstsändning + Använd inline-kodformat + Växla kodblock + Växla citat + Minska indrag + Indrag + Visa omröstning i tidslinjen + Kunder inte avkryptera den här röstsändningen. + Dina kontodetaljer hanteras separat på %1$s. + Konto \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-uk/strings.xml b/library/ui-strings/src/main/res/values-uk/strings.xml index 26d38bb324..9df054e1bd 100644 --- a/library/ui-strings/src/main/res/values-uk/strings.xml +++ b/library/ui-strings/src/main/res/values-uk/strings.xml @@ -1147,7 +1147,7 @@ Ви вийшли Вилучити… Наліпка - Використовувати ботів, мости, віджети та пакунки наліпок + Використовувати боти, мости, віджети та пакунки наліпок Зв\'язок із сервером втрачено Виправлень не знайдено Історія виправлень @@ -3040,4 +3040,13 @@ Не вдалося розпочати запис голосового повідомлення Помилка з\'єднання - Запис призупинено Застосовувати вбудований формат коду + Перемкнути блок коду + Перемкнути цитування + Без відступу + Відступ + Переглянути опитування у стрічці + Неможливо розшифрувати цю голосову трансляцію. + Керування подробицями вашого облікового запису відбувається окремо на %1$s. + Обліковий запис + Сталася помилка під час оновлення налаштувань сповіщень. Повторіть спробу. \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-zh-rCN/strings.xml b/library/ui-strings/src/main/res/values-zh-rCN/strings.xml index f3b5854afb..52d5e5f17a 100644 --- a/library/ui-strings/src/main/res/values-zh-rCN/strings.xml +++ b/library/ui-strings/src/main/res/values-zh-rCN/strings.xml @@ -9,7 +9,7 @@ %1$s 移除了 %2$s %1$s 解封了 %2$s %1$s 封禁了 %2$s - %1$s 更换了他们的头像 + %1$s 更换了其头像 %1$s 将他们的显示名称设置为 %2$s %1$s 将其显示名称从 %2$s 更改为 %3$s %1$s 移除了他们的显示名称(%2$s) @@ -178,7 +178,7 @@ 拒绝 挂断 引用 - 共享 + 分享 语音通话 视频通话 全部标记为已读 @@ -797,10 +797,10 @@ 房间 回应 同意 - 添加反应 - 查看反应 + 添加回应 + 查看回应 反应 - 由用户删除的事件 + 事件被用户删除 创建新房间 修改 请稍候…… @@ -825,7 +825,7 @@ 帮助和关于 (已编辑) - 撤消 + 撤销 断开连接 拒绝 这不是有效的 Matrix 服务器地址 @@ -942,7 +942,7 @@ 消息已移除 显示已移除消息 对已移除消息显示占位符 - 房间管理员主持的事件 + 事件被房间管理员删除 格式错误事件,无法显示 无网络。请检查你的网络连接。 更改网络 @@ -1265,7 +1265,7 @@ 上传 离开房间 - 正在离开房间… + 正在离开房间…… 管理员 协管员 自定义 @@ -1597,7 +1597,7 @@ %d 秒 轮询 - 用%s反应 + 用%s回应 验证结果 是否删除类型 %1$s 的账户数据? \n @@ -1702,7 +1702,7 @@ 从低优先级移除 添加到低优先级 - %2$d 的 %1$d + %1$d / %2$d 旋转和裁剪 添加图像自 授予许可 @@ -1758,7 +1758,7 @@ 权限 查看和更新更改房间各个部分所需的角色。 房间权限 - 此房间不公开。你没有邀请将无法重新加入。 + 此房间不公开。没有邀请,你将无法重新加入。 你保持通话 %s 保持通话 保持 @@ -1883,7 +1883,7 @@ 此别名当前无法被访问。 \n请稍后再试,或询问房间管理员你身份有权访问。 - 无论以何种方式加入 + 依然加入 加入空间 创建空间 暂且略过 @@ -2820,4 +2820,6 @@ 你无法启动语音消息因为你正在录制实时广播。请终止实时广播以开始录制语音消息 无法启动语音消息 结束了投票。 + 你的访问令牌提供对你账户的完全访问权限。勿与任何人分享它。 + 访问令牌 \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-zh-rTW/strings.xml b/library/ui-strings/src/main/res/values-zh-rTW/strings.xml index b3845e550d..ff0e7dbd40 100644 --- a/library/ui-strings/src/main/res/values-zh-rTW/strings.xml +++ b/library/ui-strings/src/main/res/values-zh-rTW/strings.xml @@ -1,159 +1,159 @@ %s 的邀請 - %1$s 邀請了 %2$s - %1$s 邀請您 + %1$s 已邀請 %2$s + %1$s 已邀請您 %1$s 已加入聊天室 %1$s 已離開聊天室 - %1$s 拒絕邀請 - %1$s 踢出 %2$s - %1$s 解除禁止 %2$s - %1$s 禁止 %2$s - %1$s 收回了對 %2$s 的邀請 - %1$s 變更了他們的大頭貼 - %1$s 設定了他們的顯示名稱為 %2$s - %1$s 變更了他們的顯示名稱從 %2$s 到 %3$s - %1$s 移除了他們的顯示名稱 (%2$s) - %1$s 變更主題為:%2$s - %1$s 變更房間名稱為:%2$s - %s 撥出了視訊通話。 - %s 撥出了語音通話。 - %s 回覆了通話。 - %s 結束通話。 - %1$s 讓房間未來可讓 %2$s 看到歷史紀錄 - 所有的房間成員,從他們被邀請的時間開始。 - 所有的房間成員,從他們加入的時間開始。 - 所有的房間成員。 + %1$s 已拒絕邀請 + %1$s 已移除 %2$s + %1$s 取消封鎖 %2$s + %1$s 已封鎖 %2$s + %1$s 已撤回 %2$s 的邀請 + %1$s 已變更他們的大頭照 + %1$s 已將他們的顯示名稱設定為 %2$s + %1$s 已將他們的顯示名稱從 %2$s 變更為 %3$s + %1$s 已移除了自己的顯示名稱(之前是 %2$s) + %1$s 已變更主題為:%2$s + %1$s 已變更聊天室名稱為:%2$s + %s 已開始視訊通話。 + %s 已開始語音通話。 + %s 已接聽通話。 + %s 已結束通話。 + %1$s 讓 %2$s 也能看到聊天室未來的歷史紀錄 + 所有聊天室的成員,從他們被邀請的時間點。 + 所有聊天室的成員,從他們加入的時間點。 + 聊天室的所有成員。 任何人。 - (大頭貼也變更了) - %1$s 移除了房間名稱 - %1$s 移除了房間主題 - %1$s 傳送加入房間的邀請給 %2$s - %1$s 接受 %2$s 的邀請 + (大頭照也已變更) + %1$s 已移除聊天室的名稱 + %1$s 已移除聊天室的主題 + %1$s 已傳送邀請給 %2$s 以加入聊天室 + %1$s 已接受 %2$s 的邀請 ** 無法解密:%s ** - 傳送者的裝置並未在此訊息傳送他們的金鑰。 + 傳送者的裝置並未將此訊息的金鑰傳給我們。 無法傳送訊息 Matrix 錯誤 - 電子郵件 + 電子郵件地址 電話號碼 聊天室邀請 - %1$s 和 %2$s - 空聊天室 - 初始化同步: -\n正在匯入帳號…… - 初始化同步: -\n正在匯入 crypto - 初始化同步: + %1$s 與 %2$s + 空的聊天室 + 初始同步: +\n正在匯入帳號… + 初始同步: +\n正在匯入加密資料 + 初始同步: \n正在匯入聊天室 - 初始化同步: + 初始同步: \n正在載入您的對話 -\n若您加入了很多聊天室,這可能會花一點時間 - 初始化同步: -\n正在匯入已邀請的聊天室 - 初始化同步: -\n正在匯入已離開的聊天室 - 初始化同步: +\n如果您加入了很多聊天室,可能要花一點時間 + 初始同步: +\n正在匯入已邀請的聊天室資料 + 初始同步: +\n正在匯入已離開的聊天室資料 + 初始同步: \n正在匯入帳號資料 %s 已升級此聊天室。 - 正在傳送訊息…… - %1$s 撤銷了 %2$s 加入聊天室的邀請 + 正在傳送訊息… + %1$s 已撤銷 %2$s 加入聊天室的邀請 %1$s 的邀請。理由:%2$s - %1$s 邀請了 %2$s。理由:%3$s - %1$s 邀請了您。理由:%2$s + %1$s 已邀請 %2$s。理由:%3$s + %1$s 已邀請您。理由:%2$s %1$s 已加入聊天室。理由:%2$s %1$s 已離開聊天室。理由:%2$s - %1$s 已回絕邀請。理由:%2$s - %1$s 踢走了 %2$s。理由:%3$s - %1$s 取消封鎖了 %2$s。理由:%3$s - %1$s 封鎖了 %2$s。理由:%3$s - %1$s 接受 %2$s 的邀請。理由:%3$s - %1$s 撤回了對 %2$s 的邀請。理由:%3$s + %1$s 已拒絕邀請。理由:%2$s + %1$s 已移除 %2$s。理由:%3$s + %1$s 已取消封鎖 %2$s。理由:%3$s + %1$s 已封鎖 %2$s。理由:%3$s + %1$s 已接受 %2$s 的邀請。理由:%3$s + %1$s 已撤回 %2$s 的邀請。理由:%3$s - %1$s 新增了 %2$s 為此聊天室的地址。 + %1$s 已新增 %2$s 為此聊天室的位址。 - %1$s 移除了此聊天室的 %2$s 地址。 + %1$s 已移除 %2$s 為此聊天室的位址。 - %1$s 為此聊天室新增 %2$s 並移除 %3$s 地址。 - %1$s 為此聊天室設定了 %2$s 為主地址。 - %1$s 為此聊天室移除了主要地址。 + %1$s 為此聊天室新增 %2$s 並移除 %3$s 位址。 + %1$s 將此聊天室的主要位址設定為 %2$s。 + %1$s 已移除聊天室的主要位址。 %1$s 已允許訪客加入聊天室。 %1$s 已禁止訪客加入聊天室。 - %1$s 已開啟端到端加密。 - %1$s 已開啟端到端加密(無法識別的演算法 %2$s)。 - %1$s 建立了聊天室 + %1$s 已開啟端對端加密。 + %1$s 已開啟端對端加密(無法識別的演算法 %2$s)。 + %1$s 已建立聊天室 您的邀請 - 您建立了聊天室 - 您邀請了 %1$s - 您加入了聊天室 - 您離開的聊天室 - 您回絕了邀請 - 您踢除了 %1$s - 您取消封鎖了 %1$s - 您封鎖了 %1$s - 您撤銷了 %1$s 的邀請 - 您變更了您的大頭貼 - 您將您的顯示名稱設定為 %1$s - 您將您的顯示名稱從 %1$s 變更為 %2$s - 您移除了您的顯示名稱(其曾為 %1$s) - 您將主題變更為:%1$s - %1$s 變更了聊天室大頭貼 - 您變更了聊天室大頭貼 - 您將聊天室名稱變更為:%1$s - 您發起了視訊通話。 - 您發起了音訊通話。 - %s 傳送了資料以建立通話。 - 您傳送了資料以建立通話。 - 您接了通話。 - 您結束了通話。 - 您已將未來的聊天室歷史設定為對 %1$s 可見 - 您升級了此聊天室。 - 您移除了聊天室名稱 - 您移除了聊天室主題 - %1$s 移除了聊天室大頭貼 - 您移除了聊天室大頭貼 - 您傳送了邀請給 %1$s 以加入聊天室 - 您已撤銷對 %1$s 加入聊天室的邀請 - 您接受了 %1$s 的邀請 - %1$s 新增了 %2$s 小工具 - 您新增了 %1$s 小工具 - %1$s 移除了 %2$s 小工具 - 您移除了 %1$s 小工具 - %1$s 修改了 %2$s 小工具 - 您修改了 %1$s 小工具 + 您已建立聊天室 + 您已邀請 %1$s + 您已加入聊天室 + 您已離開聊天室 + 您已拒絕邀請 + 您已移除 %1$s + 您已取消封鎖 %1$s + 您已封鎖 %1$s + 您已撤回 %1$s 的邀請 + 您已變更您的大頭照 + 您已將顯示名稱設定為 %1$s + 您已將您的顯示名稱從 %1$s 變更為 %2$s + 您已移除了您的顯示名稱(之前為 %1$s) + 您已變更主題為:%1$s + %1$s 已變更了聊天室大頭照 + 您已變更聊天室大頭照 + 您已變更聊天室名稱為:%1$s + 您已開始視訊通話。 + 您已開始語音通話。 + %s 傳送資料以設定通話。 + 您傳送資料以設定通話。 + 您已接聽通話。 + 您已結束通話。 + 您讓 %1$s 也能看到聊天室未來的歷史紀錄 + 您已升級此聊天室。 + 您已移除聊天室的名稱 + 您已移除聊天室的主題 + %1$s 已移除聊天室的大頭照 + 您已移除聊天室的大頭照 + 您已傳送邀請給 %1$s 以加入聊天室 + 您已撤銷 %1$s 加入聊天室的邀請 + 您已接受對 %1$s 的邀請 + %1$s 已新增 %2$s 小工具 + 您已新增 %1$s 小工具 + %1$s 已移除 %2$s 小工具 + 您已移除 %1$s 小工具 + %1$s 已修改 %2$s 小工具 + 您已修改 %1$s 小工具 管理員 - 板主 + 版主 預設 - 自訂 (%1$d) + 自訂(%1$d) 自訂 - 您變更了 %1$s 的權力等級。 - %1$s 變更了 %2$s 的權力等級。 - %1$s 從 %2$s 到 %3$s + 您已變更 %1$s 的權限等級。 + %1$s 已變更 %2$s 的權限等級。 + %1$s 的權限等級從 %2$s 調整為 %3$s 您的邀請。理由:%1$s - 您邀請了 %1$s。理由:%2$s - 您加入了聊天室。理由:%1$s + 您已邀請 %1$s。理由:%2$s + 您已加入聊天室。理由:%1$s 您離開了聊天室。理由:%1$s - 您回絕了邀請。理由:%1$s - 您踢除了 %1$s。理由:%2$s - 您取消封鎖了 %1$s。理由:%2$s - 您封鎖了 %1$s。理由:%2$s - 您接受了 %1$s 的邀請。理由:%2$s - 您撤回了 %1$s 的邀請。理由:%2$s + 您已拒絕邀請。理由:%1$s + 您已移除 %1$s。理由:%2$s + 您已取消封鎖 %1$s。理由:%2$s + 您已封鎖 %1$s。理由:%2$s + 您已接受 %1$s 的邀請。理由:%2$s + 您已撤回 %1$s 的邀請。理由:%2$s - 您為此聊天室新增了 %1$s 作為地址。 + 您已新增 %1$s 為此聊天室的位址。 - 您為此聊天室移除了 %1$s 作為地址。 + 您已移除 %1$s 為此聊天室的位址。 - 您為此聊天室新增了 %1$s 並移除了 %2$s 作為地址。 - 您將此聊天室的主要地址設定為 %1$s。 - 您將此聊天室的主要地址移除。 + 您為此聊天室新增 %1$s 並移除 %2$s 位址。 + 您將此聊天室的主要位址設定為 %1$s。 + 您已移除聊天室的主要位址。 您已允許訪客加入聊天室。 - 您已阻止訪客加入聊天室。 - 您開啟了端到端加密。 - 您開啟了端到端加密(無法識別的演算法 %1$s)。 - 您已避免訪客加入此聊天室。 - %1$s 已避免訪客加入此聊天室。 + 您已禁止訪客加入聊天室。 + 您已開啟端對端加密。 + 您已開啟端對端加密(無法識別的演算法 %1$s)。 + 您已禁止訪客加入聊天室。 + %1$s 已禁止訪客加入聊天室。 您已允許訪客加入這裡。 %1$s 已允許訪客加入這裡。 您已離開。理由:%1$s @@ -162,62 +162,62 @@ %1$s 已加入。理由:%2$s 您已撤銷對 %1$s 的邀請 %1$s 已撤銷對 %2$s 的邀請 - 您已邀請了 %1$s - %1$s 邀請了 %2$s - 您已在此升級。 - %s 已在此升級。 - 您讓未來的訊息對 %1$s 可見 - %1$s 已讓未來的訊息對 %2$s 可見 + 您已邀請 %1$s + %1$s 已邀請 %2$s + 您已升級這裡。 + %s 已升級這裡。 + 您讓 %1$s 也能看到聊天室未來的歷史紀錄 + %1$s 讓 %2$s 也能看到聊天室未來的歷史紀錄 您已離開聊天室 %1$s 已離開聊天室 您已加入 %1$s 已加入 - 您已建立此討論 - %1$s 已建立此討論 + 您已建立討論 + %1$s 已建立討論 空的聊天室(曾為 %s) - %1$s, %2$s, %3$s 與 %4$d 個其他 + %1$s、 %2$s、 %3$s 與另 %4$d 位成員 - %1$s, %2$s, %3$s 與 %4$s - %1$s, %2$s 與 %3$s - 🎉 禁止所有伺服器參與!無法再使用此聊天室。 + %1$s、%2$s、 %3$s 與 %4$s + %1$s、%2$s 與 %3$s + 🎉 已封鎖所有伺服器參與!無法再使用此聊天室。 無變更。 - • 禁止符合 IP 文字的伺服器。 - • 允許符合 IP 文字的伺服器。 - • 符合 %s 的伺服器已從允許清單中移除。 - • 允許符合 %s 的伺服器。 - • 符合 %s 的伺服器已從禁止清單中移除。 - • 現在禁止符合 %s 的伺服器。 - 您為此聊天室變更了伺服器 ACL。 - %s 為此聊天室變更了伺服器 ACL。 - • 禁止符合 IP 文字的伺服器。 - • 允許符合 IP 文字的伺服器。 + • 現在起將封鎖符合 IP 位置的伺服器。 + • 現在起允許符合 IP 位置的伺服器。 + • 已從允許清單中移除符合 %s 的伺服器。 + • 現在起將允許符合 %s 的伺服器。 + • 已將符合 %s 的伺服器從封鎖清單中移除。 + • 現在起將封鎖符合 %s 的伺服器。 + 您變更了此聊天室的伺服器存取控制清單。 + %s 變更了此聊天室的伺服器存取控制清單。 + • 已封鎖符合 IP 地址的伺服器。 + • 已允許符合 IP 地址的伺服器。 • 已允許符合 %s 的伺服器。 - • 已禁止符合 %s 的伺服器。 - 您為此聊天室設定了伺服器 ACL。 - %s 為此聊天室設定了伺服器 ACL。 - 您變更了此聊天室的地址。 - %1$s 變更了此聊天室的地址。 - 您為此聊天室變更了主要及備用地址。 - %1$s 為此聊天室變更了主要及備用地址。 - 您為此聊天室變更了備用地址。 - %1$s 變更了此聊天室的備用地址。 + • 已封鎖符合 %s 的伺服器。 + 您為此聊天室設定伺服器存取控制清單。 + %s 為此聊天室設定伺服器存取控制清單。 + 您已變更此聊天室的位址。 + %1$s 已變更此聊天室的位址。 + 您已變更此聊天室的主要及備用位址。 + %1$s 已變更此聊天室的主要及備用位址。 + 您已變更此聊天室的備用位址。 + %1$s 已變更此聊天室的備用位址。 - 您為此聊天室移除了備用地址 %1$s。 + 您為此聊天室移除備用位址 %1$s。 - %1$s 已為此聊天室移除備用地址 %2$s。 + %1$s 已為此聊天室移除備用位址 %2$s。 - 您為此聊天室新增了備用地址 %1$s。 + 您已為此聊天室新增備用位址 %1$s。 - %1$s 已為此聊天室新增了備用地址 %2$s。 + %1$s 已為此聊天室新增備用位址 %2$s。 淺色主題 深色主題 黑色主題 - 聆聽事件 + 正在監聽事件 接受 拒絕 掛斷 @@ -228,15 +228,15 @@ 離開 儲存 載入中… - 發送 + 傳送 分享 - 查看源代碼 - 通知音效 + 查看原始碼 + 吵雜通知 靜音通知 引用 - 稍後 + 稍後再說 永久連結 - 檢視解密的來源 + 檢視解密的原始碼 刪除 重新命名 回報內容 @@ -252,7 +252,7 @@ 已複製到剪貼簿 確認 警告 - 最愛 + 我的最愛 聯絡人 聊天室 過濾聊天室名稱 @@ -262,41 +262,41 @@ 僅 Matrix 聯絡人 沒有結果 聊天室 - 傳送記錄 + 傳送記錄檔 傳送當機紀錄 傳送螢幕截圖 - 回報臭蟲 + 回報錯誤 請描述此錯誤。做了什麼?預期發生什麼?實際發生什麼? - 在這裡描述你的問題 - 為了診斷問題,客戶端的紀錄將會同錯誤回報一起送出。這個包含了紀錄與螢幕截圖的錯誤回報將不會公開可見。如果您只想送出上方的文字,請取消勾選: - 您似乎沮喪的搖晃著手機。您要開啟錯誤回報畫面嗎? + 請在這裡描述您的問題 + 為了診斷問題,將會隨錯誤回報一起送出客戶端紀錄。這份錯誤回報(包含紀錄檔與螢幕截圖)將不會公開。如果您只想送出上方的文字,請取消勾選: + 您似乎沮喪的搖晃著手機。要開啟錯誤回報畫面嗎? 這個應用程式上次當掉了。您想要開啟錯誤回報畫面嗎? - 憤怒搖晃來回報臭蟲 - 此錯誤回報已成功送出 - 此錯誤回報無法送出(%s) + 透過大力搖晃回報錯誤 + 已成功送出錯誤回報 + 無法送出此錯誤回報(%s) 進度(%s%%) 加入聊天室 使用者名稱 登出 - 家伺服器網址 + 家伺服器 URL 搜尋 開始語音通話 開始視訊通話 - 您確定想要與開始語音通話? - 您確定想要與開始視訊通話? + 您確定要開始語音通話嗎? + 您確定要開始視訊通話嗎? 傳送檔案 拍照或錄影 拍照 錄影 登入 - 傳送 - 不正確的使用者名稱和/或密碼 - 這看起不像是有效的電子郵件地址 - 此電子郵件位址已經被定義。 + 送出 + 使用者名稱和/或密碼不正確 + 這看起來不是有效的電子郵件地址 + 此電子郵件地址已經被定義。 忘記密碼? - 這個家伺服器想要確定您不是機器人 + 這個家伺服器想要確認您不是機器人 電子郵件地址驗證失敗: 請確保您已點擊郵件中的連結 - 請輸入有效的網址 + 請輸入有效的 URL 異常的 JSON 沒有包含有效的 JSON 傳送太多請求 @@ -305,17 +305,17 @@ 通話 - 通話正在連線…… + 接通中… 通話結束 - 視訊來電 - 語音來電 - 通話進行中…… - 遠端未能接聽。 + 視訊通話來電 + 語音通話來電 + 通話中… + 對方未接聽電話。 資訊 - ${app_name} 需要權限來存取麥克風,來撥打語音通話。 - ${app_name} 需要權限來存取相機及麥克風來撥打視訊通話。 + ${app_name} 需要麥克風存取權限來撥打語音通話。 + ${app_name} 需要相機及麥克風存取權限來撥打視訊通話。 \n -\n為了可以正常使用通話功能,請在下個彈跳視窗中允許存取。 +\n請在下個彈跳視窗中允許存取,才能正常撥打。 繼續 @@ -325,22 +325,22 @@ 成員 跳到未讀 - %d 個成員 + %d 位成員 離開聊天室 您確定想要離開聊天室? 私人訊息 邀請 封鎖 - 封禁 + 解除封鎖 忽略 取消忽略 提及 封鎖使用者將會把他們從此聊天室中移除,並避免他們再次加入。 %s 正在打字… %1$s 和 %2$s 正在打字… - 由於您提升此使用者擁有的權限等級與你自己相同,因此您將無法還原此變更。 -\n你確定? + 由於您將此使用者的權限等級提升到與您相同,因此將無法還原此變更。 +\n確定嗎? %1$s 及 %2$s 和其他人正在打字… 您沒有權限在此聊天室發言。 @@ -351,31 +351,31 @@ 登出 忽略 指紋(%s): - 無法驗證遠端伺服器的身份。 + 無法驗證遠端伺服器的身分。 - %d 會員變動 + %d 個成員狀態變動 - 這可能表示有人正惡意的攔截你的流量,或你的手機不信任由遠端伺服器提供的憑證。 - 如果伺服器管理員說這是正常的,請確保底下的指紋與他們所提供的指紋符合。 - 你的手機曾經信任的憑證已經變動。這非常不尋常。建議你不要接受此新的憑證。 - 發送貼圖 + 這可能表示有人正惡意地攔截您的流量,或是您的手機並不信任由遠端伺服器提供的憑證。 + 如果伺服器管理員認為這是正常的,請確保底下的指紋與他們所提供的指紋相符。 + 憑證已從您手機原先信任的憑證更改為另一張憑證。這種情形非常不尋常,建議您「不要」接受這張新憑證。 + 傳送貼圖 下載 - 發送語音訊息 - 發送貼圖 + 傳送語音訊息 + 傳送貼圖 您目前沒有任何貼圖。 \n \n要現在新增一些貼圖嗎? 對不起,沒有應用程式可以完成此操作。 從其他工作階段重新請求加密金鑰。 - 請在另一個可以解密訊息的裝置上啟動 ${app_name},以便它將金鑰發送到此工作階段。 - 憑證已從以前受信任的更改為不受信任的憑證。伺服器可能已續訂其憑證。請與伺服器管理員聯繫以尋找所需的指紋。 - 僅當伺服器管理員發佈的指紋與上面的指紋匹配時才接受此憑證。 + 請在另一個可以解密訊息的裝置上啟動 ${app_name},以便它將金鑰傳送到此工作階段。 + 受信任憑證已被變更為不受信任的憑證。此伺服器可能已更新憑證。請與伺服器管理員聯繫,取得所需的指紋驗證。 + 僅當伺服器管理員發佈的指紋與上面的指紋相符時才接受此憑證。 搜尋 過濾聊天室成員 沒有結果 所有訊息 新增到主畫面 - 個人檔案圖片 + 大頭照 顯示名稱 新增電子郵件地址 新增電話號碼 @@ -385,24 +385,24 @@ 啟用這個帳號的通知 啟用此工作階段的通知 包含我顯示名稱的訊息 - 包含我用戶名稱的訊息 + 包含我的使用者名稱的訊息 來自私訊的訊息 來自群組的訊息 當我被邀請進聊天室時 - 通話請求 - 來自機器人的訊息 - 在裝置啓動時自動啓動 - 後臺同步 + 通話邀請 + 聊天機器人送出的訊息 + 裝置啓動時自動啓動 + 背景同步 同步請求超時 - 每次同步間的延遲 + 每次同步的時間間隔 版本 olm 版本 - 條款與條件 + 條款與細則 第三方通知 版權 隱私權政策 - 清空暫存檔 - 清除媒體暫存檔 + 清除快取 + 清除媒體快取 保留媒體檔案 使用者設定 通知 @@ -418,14 +418,14 @@ 釘選含有錯過的通知的聊天室 釘選含有未讀訊息的聊天室 內嵌 URL 預覽 - 對所有訊息顯示時間戳 - 用12小時制顯示時間戳 + 對所有訊息顯示時間戳記 + 用 12 小時制顯示時間戳記 提及使用者時震動 停用帳號 停用我的帳號 - 傳送分析資料 + 分析資料 傳送分析資料 - ${app_name} 會收集匿名分析以讓我們可以改進此應用程式。 + ${app_name} 會收集匿名分析資訊,讓我們可改進此應用程式。 ID 公開名稱 更新公開名稱 @@ -434,18 +434,18 @@ 授權 登入爲 家伺服器 - 身份認證伺服器 + 身分認證伺服器 使用者介面 語言 選擇語言 - 請檢查您的電子郵件並點選其中包含的連結。只要這個完成了,就點選選繼續。 + 請收信並點擊信中的連結。完成後,再點擊「繼續」。 此電子郵件地址已經被使用。 - 這個電話號碼已被使用。 + 此電話號碼已被使用。 變更密碼 目前的密碼 新密碼 - 更新密碼失敗 - 您的密碼已經更新 + 無法更新密碼 + 您的密碼已更新 顯示所有來自 %s 的訊息? 選擇國家 3 天 @@ -453,19 +453,19 @@ 1 個月 永遠 主題 - 房間歷史可讀性 + 聊天室歷史紀錄的可讀性 誰能檢視歷史訊息? 任何人 - 僅成員(自選取此選項開始) - 僅成員(自他們被邀請開始) - 僅成員(自他們加入開始) - 被封鎖的用戶 + 僅限成員(自選取此選項開始) + 僅限成員(自他們被邀請開始) + 僅限成員(自他們加入開始) + 被封鎖的使用者 進階 此聊天室的內部 ID 為 實驗室 - 這些是可能以非預期的方式壞掉的實驗性功能。請小心服用。 - 設定為主要地址 - 取消設定為主要地址 + 這些是可能以非預期的方式故障的實驗性功能。請小心服用。 + 設定為主要位址 + 取消設定為主要位址 主題 解密錯誤 公開名稱 @@ -475,31 +475,31 @@ 匯出聊天室的加密金鑰 匯出金鑰到本機檔案 匯出 - 輸入通關密語 - 確認通關密語 - 匯入聊天室端到端加密密鑰 + 輸入安全密語 + 確認安全密語 + 匯入聊天室端到端加密金鑰 匯入聊天室金鑰 從本機檔案匯入金鑰 匯入 - 僅加密驗證過的工作階段 - 從不自此工作階段傳送加密的訊息到未驗證的工作階段。 + 僅對驗證過的工作階段進行加密 + 絕不從此工作階段傳送加密訊息到未驗證的工作階段。 未驗證 已驗證 驗證 - 透過將以下內容與您的其他工作階段中的使用者設定來確認: - 如果不符合的話,您的通訊安全可能正受到威脅。 + 透過將下列內容與您其他工作階段中的「使用者設定」所顯示的內容來確認: + 如果不相符的話,您的通訊安全可能正受到威脅。 選擇一個聊天室目錄 伺服器名稱 在 %s 伺服器上的所有聊天室 所有本地 %s 聊天室 - %d 條未讀的已通知訊息 + %d 則未讀的通知訊息 %d 個聊天室 - %1$s 條在 %2$s 中 - 文字大小選擇 + %1$s 則 %2$s 的通知 + 字體大小 微小 一般 @@ -509,15 +509,15 @@ 巨大 您確定要從聊天室刪除小工具嗎? - %d 個作用中的小工具 + %d 個使用中的小工具 - 無法建立小部件。 - 發送請求失敗。 + 無法建立小工具。 + 無法傳送請求。 權限等級必需為正整數。 您不在此聊天室。 您沒有在這個聊天室做執行此操作的權限。 - 請求中缺失聊天室地址。 - 在請求中遺失使用者 ID。 + 請求中缺少 room_id。 + 請求中缺少 user_id。 聊天室 %s 不可見。 缺少必需的參數。 新增 Matrix 應用程式 @@ -528,48 +528,48 @@ 指令出錯 無法辨識的指令:%s 關閉 - 通知並震動 - 已加密的訊息 + 吵雜 + 加密訊息 建立 首頁 聊天室 已邀請 - 您已被 %2$s 從 %1$s 踢出 + 您已被 %2$s 從 %1$s 移除 您已被 %2$s 從 %1$s 封鎖 理由:%1$s - 用戶資料圖片 - 如要繼續使用此 %1$s 主伺服器,您必須同意該合約條款。 + 大頭照 + 如要繼續使用此 %1$s 家伺服器,您必須同意該條款與細則。 現在檢視 停用帳號 - 這將使您的帳戶永久不可用。您將無法登錄, 並且沒有人能夠重新註冊相同的使用者 ID。這將導致您的帳戶離開已參與的所有房間,它將從您的身份伺服器上刪除您的帳戶詳細資訊。此操作是不可逆轉的。 + 這將使您的帳號永遠無法使用。您將無法再登入,且沒有人能夠再次註冊相同的使用者 ID。這將導致您的帳號離開參與的所有聊天室,並將從身分伺服器上移除此帳號的詳細資訊。此操作無法還原。 \n -\n停用您的帳戶不會預設導致我們刪除您發送的訊息。如果您想讓我們忘記您的訊息, 請在下面的方框中打勾。 +\n單純停用帳號不會讓我們移除您傳送過的訊息。如果您希望同步刪除您的訊息,請勾選下面的選取盒。 \n -\nMatrix 中的訊息可見度類似于電子郵件。我們忘記您的訊息意味著您發送的訊息將不會與任何新的或未註冊的使用者分享,但是已註冊並已取得這些訊息的用戶仍然可以訪問他們的副本。 - 請在我的帳號停用時忘記我傳送過的所有訊息(警告:這將會造成未來的使用者無法看見完整的對話紀錄) +\nMatrix 中的訊息可見度類似電子郵件。刪除您的訊息只是代表您發送的訊息不會再顯示給任何新的或未註冊的使用者,但是已註冊並曾經取得訊息的使用者,仍然可以存取他們收到的訊息。 + 請在停用我的帳號時刪除我傳送過的所有訊息(警告:未來的使用者將無法看見完整對話紀錄) 停用帳號 請輸入您的密碼。 如果可以,請使用英文撰寫描述。 在傳送前預覽媒體 顯示動作 - 阻擋指定 id 的使用者 - 取消阻擋指定 id 的使用者 + 封鎖特定 ID 的使用者 + 取消封鎖特定 ID 的使用者 定義使用者的權限等級 - 取消指定 id 的使用者的管理權 - 邀請指定 id 的使用者到目前的聊天室 - 加入指定地址的聊天室 + 取消指定 ID 的使用者的管理權 + 邀請指定 ID 的使用者到目前的聊天室 + 加入指定位址的聊天室 離開聊天室 設定聊天室主題 - 踢除指定 id 的使用者 + 移除指定 ID 的使用者 變更您的顯示暱稱 開啟/關閉 markdown 為了修復 Matrix 應用程式管理 - 這個聊天室已被取代,且不再活躍。 + 這個聊天室已被取代,且不再使用。 對話在此繼續 - 這個聊天示是其他對話的延續 - 點選這裡以檢視更舊的訊息 + 這個聊天室是其他對話的延續 + 點擊這裡以檢視更舊的訊息 - %d 已選取 + 已選擇 %d 個 系統警告 聯絡您的服務管理員 @@ -581,25 +581,25 @@ 請 %s 以繼續使用此服務。 錯誤 抱歉,遇到錯誤 - 請建立密語以加密您匯出的金鑰。您將需要輸入這些密語以匯入金鑰。 - 建立密語 - 密語必須符合 + 請建立安全密語以加密您匯出的金鑰。您將需要輸入這些安全密語以匯入金鑰。 + 建立安全密語 + 密語不相符 展開 - 摺疊 + 收折 從聊天移除 當您的家伺服器支援此功能時在聊天中預覽連結。 - 傳送輸入通知 - 讓其他使用者知道您正在輸入。 + 傳送「輸入中」的通知 + 讓其他使用者知道您正在打字。 Markdown 格式 - 在您傳送之前使用 Markdown 格式化訊息。這讓您可以使用進階的格式化,如使用星號來顯示斜體文字。 + 在您傳送之前使用 Markdown 格式化訊息。此功能讓您可以對訊息使用進階的格式,例如用星號來顯示斜體文字。 顯示讀取回條 - 點選讀取回條以顯示詳細資料。 - 顯示加入與離開的活動 + 點擊讀取回條以顯示詳細資料。 + 顯示人員加入與離開的事件 邀請、移除與封鎖不受影響。 - 顯示帳號活動 - 包含大頭貼與顯示名稱變動。 + 顯示帳號事件 + 包含大頭照與顯示名稱變動。 密碼 - 啟動系統相機而非自訂的相機畫面。 + 使用系統相機而非自訂的相機畫面。 指令「%s」需要更多參數,或是有一些參數不正確。 Markdown 已啟用。 Markdown 已停用。 @@ -614,42 +614,42 @@ 疑難排解通知 疑難排解診斷 執行測試 - 正在執行……(%2$d 中的 %1$d) + 正在執行…(共 %2$d 個,第 %1$d 個) 基本診斷正常。如果您仍然沒有收到通知,請遞交錯誤回報以協助我們調查原因。 - 一個或更多的測試失敗,嘗試建議的修復。 + 一個或更多的測試失敗,可以嘗試建議的修正方式。 一個或更多的測試失敗,請遞交錯誤回報以協助我們調查原因。 - 系統設定。 - 通知已在系統設定中啟用。 - 通知已在系統設定中停用。 + 系統設定 + 已在系統設定中開啟通知。 + 系統設定中停用了通知。 \n請檢查系統設定。 開啟設定 - 帳號設定。 - 通知已為您的帳號啟用。 - 通知已為您的帳號停用。 + 帳號設定 + 您的帳號已開啟通知。 + 您的帳號已關閉通知。 \n請檢查帳號設定。 啟用 - 工作階段設定。 - 通知已為此工作階段啟用。 + 工作階段設定 + 此工作階段已啟用通知。 此工作階段未啟用通知。 \n請檢查 ${app_name} 設定。 啟用 - Play 服務檢查 - Google Play 服務 APK 可用且已為最新。 - ${app_name} 使用 Google Play 服務來傳遞推送訊息,但它似乎並未正確設定: + Play Services 檢查 + Google Play Services APK 可用且已為最新版。 + ${app_name} 使用 Google Play Services 來傳遞推送訊息,但它似乎並未正確設定: \n%1$s - 修復 Play 服務 - Firebase 權杖 + 修復 Play Services + Firebase Token 成功擷取 FCM token: \n%1$s - 擷取 FCM token 失敗: + 無法擷取 FCM token: \n%1$s - 註冊 token + 註冊權杖 FCM token 成功註冊至家伺服器。 - FCM token 註冊至家伺服器失敗: + 無法將 FCM token 註冊至家伺服器: \n%1$s 開機時啟動 服務將會在裝置重新啟動時自行啟動。 - 服務不會在裝置重心啟動時自行啟動,您將不能在 ${app_name} 開啟前收到通知。 + 服務不會在裝置重開時自行啟動,沒有再次開啟 ${app_name} 前不會收到通知。 啟用開機時啟動 檢查背景限制 背景限制已為 ${app_name} 停用。本測試應該使用行動數據執行(不是 WiFi)。 @@ -660,71 +660,71 @@ 停用限制 電池最佳化 ${app_name} 不會被電池最佳化影響。 - 如果使用者不為裝置充電,並讓其靜置一段時間,且將螢幕關閉,裝置將會進入 Doze 模式。這可能會導致應用程式無法存取網路,並延遲它們的工作、同步與標準警報。 + 如果裝置未充電,並且關閉螢幕後靜置一段時間,將進入瞌睡模式。此模式可能會導致應用程式無法使用網路,導致運作、同步與標準警報延後觸發。 忽略最佳化 - 找不到有效的 Google Play 服務 APK。通知可能無法正常運作。 - 視訊通話進行中…… + 找不到有效的 Google Play Services APK。通知可能無法正常運作。 + 視訊通話進行中… 金鑰備份 使用金鑰備份 略過 完成 進階通知設定 - 活動通知重要程度 - 自訂設定。 - 注意,某些訊息類型會被設定為靜音(將會生成沒有音效的通知)。 + 事件通知的重要性 + 自訂設定 + 注意:某些類型的訊息通知不會有音效。 有一些通知在您的自訂設定中被停用了。 [%1$s] \n這個錯誤並非 ${app_name} 所能控制,而是與 Google 有關,這個錯誤代表裝置註冊了太多使用 FCM 的應用程式。這個錯誤只會發生在有超大量的應用程式的裝置上,所以不應該影響一般的使用者。 [%1$s] -\n這個錯誤並非 ${app_name} 所能控制。可能由多種原因所導致。也可能會在稍後重試時就可以運作,您也可以檢查 Google Play 服務在系統設定中有沒有被限制使用資料,或是您裝置的時鐘是否正確,或是也可能會在自訂的 ROM 上發生。 +\n這個錯誤並非 ${app_name} 所能控制。可能由多種原因所導致。可能稍後再試一次就可以運作,您也可以在系統設定檢查 Google Play Service 有沒有被限制使用網路連線、您裝置的時間是否正確,或也可能會在自訂的 ROM 上發生。 [%1$s] \n這個錯誤並非 ${app_name} 所能控制。手機上沒有 Google 帳號。請開啟帳號管理員並新增一個 Google 帳號。 新增帳號 - 設定吵鬧的通知 + 設定吵雜通知 設定通話通知 設定安靜的通知 - 選擇 LED 顏色、震動、音效…… + 選擇 LED 顏色、震動、音效… 加密金鑰管理 管理金鑰備份 安靜 - 請輸入通關密語 - 通關密語太弱了 - 如果您想要讓 ${app_name} 生成復原金鑰的話,請刪除通關密語。 - 永不遺失已加密的訊息 - 在加密聊天室裡的訊息是使用端到端加密。只有您和接收者有金鑰可以閱讀這些訊息。 + 請輸入安全密語 + 安全密語太弱了 + 如果您想要讓 ${app_name} 產生復原金鑰的話,請刪除安全密語。 + 絕不失去加密訊息 + 加密聊天室裡的訊息使用端對端加密保護。只有您和接收者有金鑰可以閱讀這些訊息。 \n -\n安全地備份您的金鑰以避免失去它們。 - 設定通關密語 +\n請安全地備份您的金鑰,避免失去訊息內容。 + 設定安全密語 完成 儲存復原金鑰 儲存為檔案 - 請複製 - 分享復原金鑰與…… - 正在使用通關密語生成復原金鑰,這個過程可能需要數秒鐘。 + 請複製金鑰 + 使用…分享復原金鑰 + 正在使用安全密語產生復原金鑰,這個過程可能需要數秒鐘。 復原金鑰 未預期的錯誤 您確定嗎? - 如果您登出或遺失您的裝置的話,您可能會失去對您的訊息的存取權。 - 正在擷取…… - 使用您的復原通關密語以解鎖您的加密訊息歷史 + 如果您登出或遺失此裝置的話,可能會失去訊息的存取權。 + 正在取回備份版本… + 使用您的復原安全密語以解鎖加密訊息紀錄 使用您的復原金鑰 - 不知道您的復原通關密語,您可以 %s。 - 使用您的復原金鑰以解鎖您的加密訊息歷史 + 如果不知道復原安全密語,可以%s。 + 使用您的復原金鑰以解鎖加密訊息紀錄 輸入復原金鑰 - 遺失您的復原金鑰?您可以在設定中設定一個新的。 - 備份可能無法使用此通關密語解密:請驗證您是否輸入正確的復原通關密語。 + 遺失您的復原金鑰?可以到設定中打一把新的金鑰。 + 無法使用此安全密語解密備份:請確認您是否輸入正確的復原安全密語。 正在復原備份: 解鎖歷史紀錄 請輸入復原金鑰 - 備份可能無法使用此復原金鑰解密:請驗證您是否輸入正確的復原金鑰。 + 無法使用此復原金鑰解密備份:請確認您是否輸入正確的復原金鑰。 備份已復原 %s! - 使用 %d 金鑰復原備份。 + 還原了包含 %d 把金鑰的備份。 - %d 新的金鑰已加入到此工作階段。 + 已將 %d 把新金鑰加入到此工作階段。 - 取得最新的復原金鑰版本 (%s0。 + 無法取得最新的復原金鑰版本(%s)。 自備份復原 刪除備份 金鑰備份已為此工作階段正確設定。 @@ -736,55 +736,55 @@ 備份有從未驗證的 %s 工作階段而來的有效簽章 備份有從已驗證的 %s 工作階段而來的無效簽章 備份有從未驗證的 %s 工作階段而來的無效簽章 - 請刪除備份…… + 正在刪除備份… 刪除備份 - 要從伺服器刪除您已備份的加密金鑰?您將無法使用您的復原金鑰來讀取已加密的訊息歷史。 - 如果您現在登出的話,您將會失去您的加密訊息 - 金鑰備份進行中。如果您現在登出,您將會失去您的加密訊息。 - 安全金鑰備份應該在您所有的工作階段中啟用以避免失去對您的加密訊息的存取權。 + 真的要從伺服器刪除您備份的加密金鑰?您將無法再使用復原金鑰來讀取加密訊息記錄。 + 若您現在登出,將會失去加密訊息 + 正在備份金鑰。若您現在登出,將無法再存取加密訊息。 + 您應該在所有的工作階段中都開啟安全金鑰備份,以避免無法再存取加密訊息。 我不想要我的加密訊息 - 正在備份金鑰…… + 正在備份金鑰… 您確定嗎? 備份 - 除非您在登出前備份您的金鑰,否則您將會失去對您的加密訊息的存取權。 + 除非您在登出前先備份好加密金鑰,否則將會失去所有加密訊息。 您想要登出嗎? - 加密訊息復原 + 還原加密訊息 請輸入使用者名稱。 開始使用金鑰備份 (進階) 手動匯出金鑰 - 以通關密語保護您的備份。 - 我們將會在您的家伺服器上儲存一份加密過的您的金鑰副本。使用通關密語保護您的備份以確保其安全。 + 以安全密語保護您的備份。 + 我們將會在您的家伺服器儲存一份您的金鑰的加密副本。請使用安全密語保護您的備份以確保其安全。 \n -\n要有最大的安全性,這個應該要與您的帳號密碼不一樣。 +\n為了確保有最大的安全性,這個應該要與您的帳號密碼不一樣。 正在建立備份 或是使用復原金鑰來保障您的備份安全,將其儲存在安全的地方。 (進階)使用復原金鑰設定 成功! - 您的金鑰正在備份。 - 您的復原金鑰是安全網,如果您忘記您的通關密語,您可以使用它來復原您對您的加密訊息的存取權。 -\n把你的復原金鑰放在非常安全的地方,如密碼管理員(或保險箱) + 正在備份您的金鑰。 + 您的復原金鑰是一個安全網,如果您忘記您的安全密語,您可以使用它來復原您對加密訊息的存取權限。 +\n請把您的復原金鑰放在非常安全的地方,如密碼管理員(或保險箱) 確保您的復原金鑰放在非常安全的地方,如密碼管理員(或保險箱) - 我已經複製完了 + 我已經備份完了 分享 - 永遠不失去加密訊息 + 絕不失去加密訊息 使用金鑰備份 新安全訊息金鑰 管理金鑰備份 - 正在備份金鑰…… + 正在備份金鑰,可能會需要幾分鐘… 所有金鑰都已備份 - 正在備份 %d 金鑰…… + 正在備份 %d 把金鑰… 版本 演算法 簽章 - 要在此工作階段上使用金鑰備份,現在就使用您的通關密語或復原金鑰復原。 - 正在計算復原金鑰…… - 正在下載金鑰…… - 正在匯出金鑰…… + 要在此工作階段上使用金鑰備份,現在就使用您的安全密語或復原金鑰復原。 + 正在計算復原金鑰… + 正在下載金鑰… + 正在匯出金鑰… 忽略 - 使用單一登入系統登入 + 使用單一登入機制登入 使用 Enter 傳送訊息 軟體鍵盤的 Enter 按鈕將會傳送訊息而非換行 密碼無效 @@ -801,32 +801,32 @@ %d 個通知 - 新活動 + 新事件 聊天室 新訊息 新邀請 - ** 傳送失敗 - 請開啟聊天室 - 抱歉,使用 Jitsi 建立會議通話在舊裝置上並不支援(Android 系統版本小於 6.0 的裝置) + ** 無法傳送 - 請開啟聊天室 + 很抱歉,不支援在舊版裝置(Android 系統版本小於 6.0)使用 Jitsi 進行會議通話 未知的 IP 有新工作階段正在要求加密金鑰。 \n工作階段名稱:%1$s -\n上次檢視:%2$s +\n上次上線:%2$s \n如果您沒有登入其他工作階段,請忽略本請求。 - 有未驗證的工作階段正在要求加密金鑰。 -\n工作階段名稱:%1$s -\n上次檢視:%2$s + 有未驗證的工作階段正在要求加密金鑰。 +\n工作階段名稱:%1$s +\n上次上線:%2$s \n如果您沒有登入其他工作階段,請忽略本請求。 分享 金鑰分享請求 忽略 已驗證! - 知道了 + 了解 驗證請求 %s 想要驗證您的工作階段 未知錯誤 您的家伺服器上已有備份 - 看起來您已經從其他工作階段設定金鑰備份了。您想要用您正在建立的這個來取代它嗎? + 看起來您已經從其他工作階段設定金鑰備份了。想要用您正在建立的這個來取代嗎? 取代 停止 正在檢查備份狀態 @@ -838,25 +838,25 @@ 您都看完了! 您已經沒有未讀的訊息了 對話 - 您的直接訊息對話將會在此顯示。點擊右下角的 + 開始一些直接訊息。 + 將會在此顯示您的私人訊息對話。點擊右下角的 + 可以建立私訊。 聊天室 - 您的聊天室將會在此顯示。點擊右下角的 + 來尋找既有的或開始您自己的。 + 您的聊天室將會在此顯示。點擊右下角的 + 來尋找既有的或開始您自己的聊天室。 反應 同意 新增反應 檢視反應 反應 - 由使用者刪除的活動 - 由聊天室管理員審核的活動 - 活動格式錯誤,無法顯示 + 使用者刪除的事件 + 聊天室管理員刪除的事件 + 事件格式錯誤,無法顯示 建立新聊天室 沒有網路。請檢查您的網路連線。 變更 變更網路 - 請稍候…… + 請稍候… 無法預覽此聊天室 聊天室 - 直接訊息 + 私人訊息 建立 名稱 公開 @@ -865,50 +865,50 @@ 取得金鑰備份資料時發生錯誤 從檔案「%1$s」匯入 e2e 金鑰。 Matrix SDK 版本 - 其他第三方提醒 + 其他第三方程式庫授權資訊 您已在檢視此聊天室了! 一般 偏好設定 - 安全與隱私 + 安全性與隱私權 推送規則 - 未定義通送規則 + 未定義推送規則 沒有已註冊的推送閘道 - App ID: + 應用程式 ID: 推送金鑰: 應用程式顯示名稱: 工作階段顯示名稱: Url: 格式: - 音訊與視訊 - 說明與關於 + 語音與視訊 + 說明與協助 註冊權杖 - 建議 + 提出建議 請在下面編寫您的建議。 在此描述您的建議 感謝,建議已成功傳送 - 建議傳送失敗 (%s) - 在時間軸中顯示隱藏的活動 - 直接訊息 - 正在等待…… - 正在加密縮圖…… - 正在傳送縮圖 (%1$s / %2$s) - 正在加密檔案…… - 正在傳送檔案 (%1$s / %2$s) + 無法傳送建議(%s) + 在時間軸中顯示隱藏的事件 + 私人訊息 + 正在等待… + 正在加密縮圖… + 正在傳送縮圖(%1$s / %2$s) + 正在加密檔案… + 正在傳送檔案(%1$s / %2$s) 檔案 %1$s 已下載! (已編輯) 訊息編輯 找不到編輯 - 過濾對話…… + 過濾對話… 找不到您要尋找的東西? - 建立新聊天室 - 傳送新直接訊息 + 建立一間新聊天室 + 傳送新的私人訊息 檢視聊天室目錄 - 名稱或 ID (#example:matrix.org) + 名稱或 ID(#example:matrix.org) 啟用滑動以在時間軸上回覆 - 連結已複製到剪貼簿 + 已將連結複製到剪貼簿 整合管理員 - 正在建立聊天室…… - 檢視編輯歷史 + 正在建立聊天室… + 檢視編輯紀錄 婉拒 要繼續,您必須接受此服務的條款。 服務條款 @@ -919,8 +919,8 @@ 斷線 無法在此 URL 找到家伺服器,請檢查 背景同步模式 - 為電池最佳化 - ${app_name} 將會在背景同步以節省裝置的有限資源(電池)。 + 電力最佳化 + ${app_name} 將會在背景同步以節省裝置的電力資源。 \n取決於您裝置的資源狀態,作業系統可能會延遲同步。 為即時作業最佳化 ${app_name} 將會精準地定期在背景同步(可設定)。 @@ -929,38 +929,38 @@ 當應用程式在背景時,您將不會收到訊息通知。 探索 管理您的探索設定。 - 您未使用任何身份識別伺服器 - 看起來您正在嘗試連線到其它家伺服器。您想要登出嗎? - 身份識別伺服器 - 取消連線到身份識別伺服器 - 設定身份識別伺服器 - 變更身份識別伺服器 + 您未使用任何身分伺服器 + 看來您正在嘗試連線到其它家伺服器。您想要登出嗎? + 身分伺服器 + 取消連線到身分伺服器 + 設定身分伺服器 + 變更身分伺服器 您正在使用 %1$s 來探索與被您認識的既有聯絡人探索。 - 您目前並未使用身份識別伺服器。要探索與被您認識的聯絡人探索,請在下方設定一個。 + 您目前並未使用身分伺服器。要探索與被您認識的聯絡人探索,請在下方設定一個。 可探索的電子郵件地址 - 在您新增電子郵件地址後,探索選項將會出現。 - 在您新增電話號碼後,探索選項將會出現。 - 與您的身份識別伺服器斷線代表您無法被其他使用者探索,且您將無法透過電子郵件或電話邀請其他人。 + 新增電子郵件地址後,就會出現探索選項。 + 您新增電話號碼後,就會出現探索選項。 + 與您的身分伺服器斷線代表您無法被其他使用者探索,且您將無法透過電子郵件或電話邀請其他人。 可探索的電話號碼 - 我們已傳送電子郵件到 %s,請檢查您的電子郵件並在確認連結上點選 - 輸入身份識別伺服器 URL - 無法連線到身份識別伺服器 - 請輸入身份識別伺服器 URL - 身份識別伺服器無服務條款 - 您選擇的身份識別伺服器沒有任何服務條款。僅在您信任服務擁有者時才繼續 - 文字訊息已傳送給 %s。請輸入其中包含的驗證碼。 - 您目前正在身份識別伺服器 %1$s 上分享電子郵件地址或電話號碼。您將必須重新連線到 %2$s 以停止分享它們。 - 同意身份識別伺服器 (%s) 的服務條款以允許您被透過電子郵件地址或電話號碼探索。 + 我們已傳送電子郵件到 %s,請收信並點擊確認連結 + 輸入身分伺服器 URL + 無法連線到身分伺服器 + 請輸入身分伺服器 URL + 身分伺服器無服務條款 + 您選擇的身分伺服器沒有任何服務條款。僅在您信任服務擁有者時才繼續 + 已傳送簡訊給 %s。請輸入簡訊中的驗證碼。 + 您目前正在身分伺服器 %1$s 上分享電子郵件地址或電話號碼。您將必須重新連線到 %2$s 以停止分享它們。 + 同意身分伺服器 (%s) 的服務條款以允許您被透過電子郵件地址或電話號碼探索。 啟用詳細紀錄。 詳細紀錄可以協助開發者在您傳送憤怒搖晃時取得更多紀錄。即使啟用這個設定,應用程式依然不會紀錄訊息內容或任何個人資料。 - 請在您接受您家伺服器的條款與條件前繼續重試。 - 看起來伺服器回應時間似乎太久了,這可能是不良的網路連線或伺服器錯誤所造成。請稍後再試。 + 請在您接受您家伺服器的條款與細則前繼續重試。 + 看起來伺服器回應時間似乎太久了,可能您的網路連線不穩或伺服器發生錯誤所造成。請稍後再試。 傳送附件 開啟導航選單 開啟建立聊天室選單 - 關閉建立聊天室選單…… - 建立新的直接對話 - 建立新的聊天室 + 關閉建立聊天室選單… + 建立新的私人對話 + 建立一間新聊天室 關閉金鑰備份橫幅 跳到底部 %1$s、%2$s 與 %3$s 已閱讀 @@ -976,8 +976,8 @@ 貼圖 無法處理分享資料 垃圾訊息 - 不合適 - 自訂回報…… + 不合適的訊息 + 自訂回報… 回報此內容 回報此內容的理由 回報 @@ -995,21 +995,21 @@ \n \n如果您不想要看到從此使用者而來的更多內容,您可以忽略他們以隱藏他們的訊息。 整合 - 使用整合管理員以管理機器人、橋接、小工具與貼紙包。 -\n整合管理員可以代表您接收設定資料,調整小工具、傳送聊天室邀請並設定權力等級。 + 使用整合管理員以管理聊天機器人、橋接、小工具與貼圖包。 +\n整合管理員可以代表您接收設定資料,調整小工具、傳送聊天室邀請並設定權限等級。 允許整合 小工具 載入小工具 - 此小工具新增由: + 此小工具新增者: 使用它可能會設定 cookies 並與 %s 分享資料: 使用它可能會與 %s 分享資料: - 載入小工具失敗。 + 無法載入小工具。 \n%s 重新載入小工具 在瀏覽器中開啟 撤銷我的存取權限 您的顯示名稱 - 您的大頭貼 URL + 您的大頭照 URL 您的使用者 ID 您的佈景主題 小工具 ID @@ -1020,7 +1020,7 @@ 使用相機 使用麥克風 讀取 DRM 保護的媒體 - 這不是有效的 Matrix 伺服器位置 + 這不是有效的 Matrix 伺服器位址 忽略使用者 所有訊息(吵雜) 所有訊息 @@ -1035,17 +1035,17 @@ 您未忽略任何使用者 長按聊天室以檢視更多選項 %1$s 將聊天室設為公開給所有知道連結的人。 - %1$s 將聊天室設為僅邀請可進入。 + %1$s 將聊天室設為邀請才可進入。 未讀訊息 這是您的通訊。您才是所有者。 - 直接或在群組中與夥伴們聊天 + 與夥伴們直接,或在群組中聊天 透過加密讓對話保持隱密 擴展並自訂您的體驗 開始 選取伺服器 就像電子郵件,帳號也有一個家,不過您還是可以跟任何人交談 在最大的公開伺服器上免費加入數百萬人之中 - 組織另有專業主機可用 + 組織級需求,另提供專業代管服務 了解更多 其他 自訂與進階設定 @@ -1057,57 +1057,57 @@ 註冊 登入 以 SSO 繼續 - Element Matrix Services 位置 - 位置 - 組織有專業主機 - 輸入 Modular Element 或您想要使用的伺服器位置 - 載入頁面時發生錯誤:%1$s (%2$d) + Element Matrix Services 位址 + 位址 + 組織用的收費代管服務 + 輸入 Modular Element 或您想要使用的伺服器位址 + 載入頁面時發生錯誤:%1$s(%2$d) 應用程式無法登入此家伺服器。家伺服器支援以下登入類型:%1$s。 \n -\n您想要使用網路客戶端登入嗎? +\n您想要使用網頁客戶端登入嗎? 抱歉,此伺服器不接受新帳號。 應用程式無法在此家伺服器上建立帳號。 \n -\n您想要使用網路客戶端註冊嗎? +\n您想要使用網頁客戶端註冊嗎? 此電子郵件地址未關聯到任何帳號。 在 %1$s 上重設密碼 - 驗證郵件已傳送到您的收件匣以確認您要設定新密碼。 + 將傳送確認信到您的信箱,以確定您要設定新密碼。 下一步 電子郵件 新密碼 警告! - 變更您的密碼將會重設在您所有工作階段中任何的端到端加密金鑰,讓已加密的聊天歷史無法讀取。請在重設您的密碼前從其他工作階段設定金鑰備份或匯出您的聊天室金鑰。 + 變更您的密碼將會重設您所有工作階段中任何的端對端加密金鑰,讓加密聊天紀錄無法讀取。請在重設您的密碼前從其他工作階段設定金鑰備份或匯出聊天室金鑰。 繼續 - 此電子郵件地址未被連結到任何帳號 + 此電子郵件位址未連結到任何帳號 檢查您的收件匣 驗證電子郵件已傳送至 %1$s。 - 輕點連結以確認您的新密碼。在您使用了其中包含的連結後,請點擊下方。 - 我已經驗證了我的電子郵件地址 + 點擊連結即可確認您的新密碼。完成驗證後,請點擊下方。 + 我已經驗證了電子郵件地址 成功! 您的密碼已被重設。 - 您已登出所有工作階段,且不會再收到推播通知。要重新啟用通知,請在裝置上再次登入。 + 您已登出所有工作階段,且不會再收到推送通知。要重新啟用通知,請在裝置上再次登入。 返回登入 警告 - 您的密碼未變更。 + 您的密碼尚未變更。 \n -\n停止密碼變更流程? +\n要停止密碼變更流程嗎? 設定電子郵件地址 設定電子郵件地址以復原您的帳號。之後您也可以選擇性地讓您認識的人透過此地址找到您。 電子郵件 - 電子郵件(選擇性) + 電子郵件(選填) 下一個 設定電話號碼 設定電話號碼以讓您認識的人找到您。 請使用國際格式。 電話號碼 - 電話號碼(選擇性) + 電話號碼(選填) 下一個 確認電話號碼 - 我們剛傳送了驗證碼給 %1$s。在下方輸入以驗證是您。 + 我們剛傳送了驗證碼給 %1$s。請在下方輸入以驗證您的號碼。 輸入驗證碼 再次傳送 下一個 - 國際電話號碼必須以加號開頭 + 國際電話號碼必須以「+」開頭 電話號碼似乎無效。請檢查 註冊至 %1$s 使用者名稱或電子郵件 @@ -1119,7 +1119,7 @@ 選取 matrix.org 選取 Element Matrix Services 選取自訂的家伺服器 - 請執行 captcha 挑戰 + 請執行 captcha 驗證 接受條款以繼續 請檢查您的電子郵件 我們剛傳送電子郵件給 %1$s。 @@ -1127,7 +1127,7 @@ 輸入的驗證碼不正確。請檢查。 未更新的家伺服器 - 傳送了太多請求。您可以在 %1$d 秒後重試…… + 傳送了太多請求。您可以在 %1$d 秒後重試… 檢視由 您已登出 @@ -1141,7 +1141,7 @@ 再次登入 您已登出 登入 - 您的家伺服器 (%1$s) 管理員已將您的帳號 %2$s (%3$s) 登出。 + 您的家伺服器(%1$s) 管理員已將您的帳號 %2$s(%3$s) 登出。 登入以復原僅儲存在此裝置上的加密金鑰。您在任何裝置上都需要它們來讀取您所有的安全訊息。 登入 密碼 @@ -1158,7 +1158,7 @@ \n請先清除您的資料,然後再以其他帳號登入。 您的 matrix.to 連結格式錯誤 描述太短了 - 初始化同步…… + 初始化同步… 進階設定 開發者模式 開發者模式會啟用隱藏的功能,但可能會造成應用程式較不穩定。僅供開發者使用! @@ -1169,12 +1169,12 @@ 設定 目前的工作階段 其他工作階段 - 僅顯示第一個結果,輸入更多字母…… + 僅顯示第一個結果,輸入更多字母… 快速失敗 在發生非預期的錯誤時,${app_name} 可能更常當機 將 ¯\\_(ツ)_/¯ 附加到純文字訊息中 啟用加密 - 加密一旦啟用就無法停用。 + 一旦啟用加密就無法停用。 您的電子郵件網域無法在此伺服器上註冊 未受信任的登入 它們相符 @@ -1190,7 +1190,7 @@ 圖片。 音訊 檔案 - 正在等待…… + 正在等待… %s 已取消 您已取消 %s 已接受 @@ -1201,16 +1201,16 @@ 掃描其他使用者裝置的條碼以安全地相互驗證 掃描他們的條碼 無法掃描 - 如果您無法面對面進行,請用比較顏文字代替 - 透過比較顏文字驗證 + 如果您無法面對面進行,請用比較表情符號代替 + 透過比較表情符號驗證 驗證 %s 已驗證 %s - 正在等待驗證 %s…… - 此聊天室中的訊息未端到端加密。 - 此聊天室中的訊息有端到端加密。 + 正在等待驗證 %s… + 此聊天室中的訊息未端對端加密。 + 此聊天室的訊息有端對端加密。 \n -\n您的訊息已被鎖保護,只有您與收件者有獨一無二的金鑰可以將其解鎖。 - 安全性 +\n您的訊息已被鎖保護,只有您與收件者有獨特的金鑰可以解鎖。 + 安全 取得更多資訊 更多 聊天室設定 @@ -1220,18 +1220,18 @@ 上傳 離開聊天室 - 正在離開聊天室…… + 正在離開聊天室… 管理員 - 板主 + 主持人 自訂 邀請 使用者 %1$s 中的管理員 - %1$s 中的板主 - %2$s 中的自訂 (%1$d) + %1$s 中的主持人 + 自訂 %2$s 中的(%1$d) 跳至讀取回條 ${app_name} 無法處理類型為「%1$s」的事件 - 在彩現 id「%1$s」事件的內容時,${app_name} 遇到問題 + 在渲染 ID「%1$s」事件的內容時,${app_name} 遇到問題 取消忽略 此工作階段無法與您其他的工作階段分享此驗證。 \n驗證將會儲存在本機並在未來版本的應用程式中共享。 @@ -1239,16 +1239,16 @@ 將指定的表情符號以彩虹的方式上色後傳送 時間軸 訊息編輯器 - 啟用端到端加密…… - 啟用加密? - 一旦啟用對聊天室的加密就無法停用。傳送到已加密聊天室的訊息無法被伺服器看見,僅有聊天室的參與者可見。啟用加密可能會讓許多機器人與橋接無法運作。 + 啟用端對端加密… + 要開啟加密嗎? + 啟用後,就無法再停用對聊天室的加密。伺服器無法解讀傳送到加密聊天室的訊息內容,而僅有聊天室的參與者可見。開啟加密可能會讓許多聊天機器人與橋接無法運作。 啟用加密 要確定安全,請透過一次性的代碼驗證 %s。 要確定安全,請面對面進行或使用其他方式來通訊。 - 比較獨一無二的顏文字,並確保它們以相同的順序出現。 + 比較獨特的表情符號,並確保它們以相同的順序出現。 以其中一方的畫面上顯示的代碼與其他使用者的畫面比較。 - 與此使用者的訊息是端到端加密的,無法被第三方讀取。 - 您的新工作階段已被驗證。其已存取您已加密的訊息,其他使用者也會看到其已受信任。 + 與此使用者的訊息是端對端加密的,無法被第三方讀取。 + 您新的工作階段現在已被驗證。新階段可存取您的加密訊息,其他使用者也會看到其已受信任。 交叉簽署 交叉簽署已啟用 \n私鑰在裝置上。 @@ -1264,25 +1264,25 @@ 登出此工作階段 無可用的密碼學資訊 因為您已驗證此工作階段,所以其在安全通訊上是可受信任的: - 驗證此工作階段以將其標記為受信任並讓其可以存取已加密的訊息。如果您並未登入此工作階段,您的帳號可能已被盜用: + 驗證此工作階段以將其標記為受信任並讓其可以存取加密訊息。如果您並未登入此工作階段,您的帳號可能遭到盜用: %d 活躍的工作階段 驗證此裝置 - 使用既有的工作階段來驗證這個,讓它可以存取已加密的訊息。 + 使用已存在的使用階段來驗證這個階段,讓此階段可以存取加密訊息。 驗證 已驗證 警告 - 取得工作階段失敗 + 無法取得工作階段 工作階段 受信任 未受信任 - 因為 %1$s (%2$s) 已驗證此工作階段,所以其在安全通訊上是可受信任的: - %1$s (%2$s) 使用新的工作階段登入: - 直到此使用者信任此工作階段為止,傳送到該工作階段與從它傳送的訊息都會以警告標記。或者您可以手動進行驗證。 + 因為 %1$s (%2$s)已驗證此工作階段,所以其在安全通訊上是可受信任的: + %1$s(%2$s)使用新的工作階段登入: + 直到此使用者信任此工作階段為止,傳送到該工作階段與從它傳送的訊息都會標記為警告。或者您可以手動進行驗證。 初始化交叉簽署 重設金鑰 - QR code + QR Code 就快完成了!%s 有顯示打勾嗎? @@ -1290,32 +1290,32 @@ 使用者名稱 開發者工具 帳號資料 - 使用復原通關密語或金鑰 + 使用復原安全密語或金鑰 如果您無法存取既有的工作階段的話 - 在儲存空間中找不到秘密 - 移除…… + 在儲存空間找不到秘密 + 移除… 您想要傳送此附件到 %1$s 嗎? 使用原始大小傳送圖片 確認移除 - 您確定您想要移除(刪除)此活動嗎?注意,如果您刪除聊天室名稱或主題的變更,變更會被撤銷。 + 您確定要刪除此事件嗎?注意,如果您刪除聊天室名稱或主題的變更事件,該變更將被取消。 包含理由 修改原因 - 被使用者刪除的活動,理由:%1$s - 由聊天室管理員管理的活動,理由:%1$s + 使用者刪除事件,理由:%1$s + 聊天室管理員管理了事件,理由:%1$s 金鑰已為最新! ${app_name} Android 金鑰請求 - 解鎖已加密的訊息歷史 + 解鎖加密訊息紀錄 重新整理 新登入。是您嗎? - 使用此工作階段來驗證新的,讓它可以存取已加密的訊息。 + 使用此使用階段來驗證新的使用階段,讓新階段可以存取加密訊息。 這不是我 您的帳號可能已被盜用 - 如果您取消,您可能無法在此裝置上讀取已加密的訊息,而其他使用者也不會信任它 - 如果您取消,您可能無法在您新的裝置上讀取已加密的訊息,而其他使用者也不會信任它 - 如果您現在取消,您將無法驗證 %1$s (%2$s)。在他們的使用者檔案中重新開始。 + 如果您取消,可能無法在此裝置上讀取加密訊息,而其他使用者也不會信任此階段 + 如果您取消,可能無法在您的新裝置讀取加密訊息,而其他使用者也不會信任此裝置 + 如果您現在取消,您將無法驗證 %1$s(%2$s)。在他們的使用者檔案中重新開始。 下列其中一項可能已被盜用: \n \n- 您的密碼 @@ -1326,98 +1326,98 @@ \n我們建議您立刻在設定中變更您的密碼與復原金鑰。 已取消驗證。您可以再次開始驗證。 驗證已取消 - 復原通關密語 + 復原安全密語 訊息金鑰 輸入您的 %s 以繼續。 - 不要重用您的帳號密碼。 + 不要使用您的帳號密碼。 這可能需要數秒,請稍候。 設定復原。 已完成! 把它放在安全的地方 完成 正在發佈已建立的識別金鑰 - 正在從通關密語生成安全金鑰 + 從安全密語產生安全金鑰 正在定義 SSSS 預設金鑰 - 正在同步主控金鑰 + 正在同步主金鑰 正在同步使用者金鑰 正在同步自行簽署金鑰 設定金鑰備份 您的 %2$s 與 %1$s 設定好了。 \n -\n請保護它們的安全!如果您遺失所有作用中的工作階段,您將會需要它們來解鎖已加密的訊息並保護資訊。 +\n請把它們保管好!如果您失去所有使用中的工作階段,將會需要使用它們來解鎖加密訊息與安全資訊。 列印並將其存放在安全的地方 將其儲存在 USB 隨身碟或備份磁碟上 將其複製到您的私人雲端儲存空間 加密已啟用 - 在此聊天室中的訊息已端到端加密。取得更多資訊並在使用者的個人檔案中驗證他們。 + 在此聊天室中的訊息已端對端加密。取得更多資訊並在使用者的個人檔案中驗證他們。 加密未啟用 用於此聊天室的加密未受支援 %s 建立並設定聊天室。 就快完成了!其他裝置有顯示打勾嗎? - 就快完成了!正在等待確認…… - 正在等待 %s…… - 匯入金鑰失敗 + 就快完成了!正在等待確認… + 正在等待 %s… + 無法匯入金鑰 通知設定 訊息包含 @room - 在一對一的聊天中已加密的訊息 - 群組聊天中的已加密訊息 + 一對一聊天中的加密訊息 + 群組聊天中的加密訊息 當聊天室升級時 疑難排解 - 傳送純文字訊息,不將它們解譯為 markdown + 傳送純文字訊息,不將它們轉譯為 markdown 不正確的使用者名稱及/或密碼。輸入的密碼以空格開頭或結尾,請檢查。 - 訊息…… + 訊息… 提供加密升級 - 驗證您自己與其他人以保證聊天安全 + 驗證您自己與其他人以確保聊天安全 輸入您的 %s 以繼續 使用檔案 這不是有效的復原金鑰 請輸入復原金鑰 正在檢查備份金鑰 - 正在檢查備份金鑰 (%s) + 正在檢查備份金鑰 (%s) 正在取得曲線金鑰 - 正在從通關密語生成 SSSS 金鑰 - 正在從通關密語生成 SSSS 金鑰 (%s) - 正在從復原金鑰生成 SSSS 金鑰 - 正在 SSSS 中儲存金鑰備份秘密 - 輸入您的金鑰備份通關密語以繼續。 + 從安全密語產生 SSSS 金鑰 + 從安全密語產生 SSSS 金鑰(%s) + 從復原金鑰產生 SSSS 金鑰 + 正在 SSSS 中儲存金鑰備份的秘密 + 輸入您的金鑰備份安全密語以繼續。 使用您的金鑰備份復原金鑰 - 不知道您的金鑰備份通關密語,您可以 %s。 + 不知道您的金鑰備份安全密語,您可以 %s。 金鑰備份復原金鑰 避免對應用程式進行螢幕截圖 啟用此設定會新增 FLAG_SECURE 到所有活動。重新啟動應用程式以讓變動生效。 - 設定新的帳號密碼…… - 在您的其他裝置上使用最新的 ${app_name}、${app_name} Web、${app_name} 桌面版、${app_name} iOS、${app_name} for Android 或其他有交叉簽章功能的 Matrix 客戶端 - ${app_name} Web + 設定新的帳號密碼… + 在您的其他裝置上使用最新的 ${app_name}、${app_name} 網頁版、${app_name} 桌面版、${app_name} iOS、${app_name} for Android 或其他有交叉簽署功能的 Matrix 客戶端 + ${app_name} 網頁版 \n${app_name} 桌面版 ${app_name} iOS \n${app_name} Android - 或其他有交叉簽章功能的 Matrix 客戶端 + 或其他有交叉簽署功能的 Matrix 客戶端 在您的其他裝置上使用最新的 ${app_name}: 強制丟棄目前在加密聊天室中的外發群組工作階段 僅在加密聊天室中支援 使用您的 %1$s 或使用您的 %2$s 以繼續。 使用復原金鑰 選取您的復原金鑰,或是透過打字或從您的剪貼簿貼上來手動輸入 - 存取安全儲存空間失敗 + 無法存取安全儲存空間 透過文字手動驗證 驗證登入 - 透過顏文字來進行互動驗證 - 從您的其他工作階段驗證此登入以確認您的身份並授予存取加密訊息的權限。 + 透過表情符號來進行互動驗證 + 從您的其他工作階段驗證此登入以確認您的身分,並授予存取加密訊息的權限。 請選擇使用者名稱。 請選擇密碼。 - 仔細檢查此連結 + 請再次檢查此連結 連結 %1$s 正在將您帶往其他網站:%2$s。 \n -\n您確定您想要繼續嗎? +\n您確定要繼續嗎? 我們無法建立您的直接訊息。請檢查您想要邀請的使用者,然後再試一次。 未加密 - 由未驗證的裝置加密 + 已由未驗證的裝置加密 驗證正在存取您帳號的新登入:%1$s %1$s:%2$s %1$s:%2$s %3$s 新增成員 邀請 - 正在邀請使用者…… + 正在邀請使用者… 邀請使用者 邀請已傳送給 %1$s 邀請已傳送給 %1$s 與 %2$s @@ -1428,44 +1428,44 @@ 訊息已移除 顯示已移除的訊息 為已移除的訊息顯示佔位符 - 我們已傳送電子郵件到 %s,請先檢查您的電子郵件並點擊確認連結 - 驗證代碼不正確。 + 我們已傳送電子郵件到 %s,請先收信並點擊確認連結 + 驗證碼不正確。 媒體 此聊天室中沒有媒體 檔案 - %1$s 在 %2$s + 由 %1$s 於 %2$s 上傳 此聊天室中沒有檔案 使用我的 Matrix 識別符登入 登入 如果您在家伺服器上建立了帳號,使用您的 Matrix ID(例如 @user:domain.com)與下方的密碼。 - 使用者識別符 + Matrix ID 這不是有效的使用者識別符。預期中的格式:\'@user:homeserver.org\' 找不到有效的家伺服器。請檢查您的識別符 飛航模式開啟 目前語言 其他可用的語言 - 正在載入可用的語言…… + 正在載入可用的語言… 開啟 %s 的條款 - 從身份識別伺服器 %s 斷線? - 此身份識別伺服器太舊了。${app_name} 僅支援 API V2。 + 從身分伺服器 %s 斷線? + 此身分伺服器太舊了。${app_name} 僅支援 API V2。 此動作是不可能的。家伺服器太舊了。 - 請先設定身份識別伺服器。 - 請先在設定中同意身份識別伺服器的條款。 + 請先設定身分伺服器。 + 請先在設定中同意身分伺服器的條款。 為了保護您的隱私,${app_name} 僅支援傳送雜湊過的使用者電子郵件地址與電話號碼。 - 關聯失敗。 + 已無法關聯。 目前沒有此識別符的關聯。 - 您的家伺服器 (%1$s) 建議將 %2$s 用於您的身份識別伺服器 + 您的家伺服器(%1$s) 建議將 %2$s 用於您的身分伺服器 使用 %1$s - 或者您可以輸入任何身份識別伺服器 URL - 輸入身份識別伺服器 URL + 或者您可以輸入任何身分伺服器 URL + 輸入身分伺服器 URL 遞交 播放 - 忽略 + 關閉 複製 成功 通知 ${app_name} 呼叫失敗 - 建立即時連線失敗。 + 無法建立即時連線。 \n請要求您家伺服器的管理員設定 TURN 伺服器以讓通話的運作更可靠。 選取音效裝置 電話 @@ -1477,25 +1477,25 @@ 關閉 HD 開啟 HD - SSL 錯誤:對方的身份未驗證。 + SSL 錯誤:無法驗證對方的身分。 SSL 錯誤。 取消邀請 將您自己降級? - 您將無法復原此變更,因為您要把自己降級,如果您是聊天室中最後一個有此權限的使用者,那將無法重新取得權限。 + 因為您要把自己降級,將無法復原此變更。如果您是聊天室中最後一個有此權限的使用者,那將無法重新取得權限。 降級 忽略使用者 - 忽略此使用者將會從您分享的聊天室移除他們的訊息。 + 忽略此使用者後,將會從你們共同的聊天室中隱藏他的訊息。 \n -\n您隨時都可以在一般設定中撤銷此動作。 +\n您隨時都可以在一般設定中還原此動作。 取消忽略使用者 取消忽略此使用者將再次顯示從他們而來的所有訊息。 取消邀請 - 您確定您想要取消對此使用者的邀請嗎? - 踢除使用者 - 踢除的理由 - 踢除使用者將會將他們從此聊天室中移除。 + 您確定要取消對此使用者的邀請嗎? + 移除使用者 + 移除的理由 + 使用者將會從此聊天室中被移除。 \n -\n要避免他們再次加入,您應改為封鎖他們。 +\n為了防止他們再加入,您應該封鎖他們。 封鎖使用者 封鎖的理由 取消封鎖使用者 @@ -1504,9 +1504,9 @@ 設定安全備份 重設安全備份 在此裝置上設定 - 透過備份加密金鑰到您的伺服器上以防止遺失對加密訊息與資料的存取權。 - 為您現有的備份生成新的安全金鑰或設定新的安全密語。 - 這將會取代您目前的金鑰或密語。 + 透過備份加密金鑰到您的伺服器上,以防止失去存取已加密訊息與資料。 + 為您現有的備份產生新的安全金鑰或設定新的安全密語。 + 將會取代您目前的金鑰或密語。 整合被停用 在設定中啟用「允許整合」以執行此動作。 @@ -1514,27 +1514,27 @@ 金鑰已成功匯出 檢視 - 作用中的小工具 + 使用中的小工具 復原金鑰已儲存。 安全備份 - 防止遺失對加密訊息與資料的存取權 + 小心不要失去加密訊息與資料的存取權 設定安全備份 - 在主畫面上新增專用的未讀通知分頁。 - 新增到最愛 - 從最愛移除 + 新增專用的未讀通知分頁到主畫面。 + 新增到我的最愛 + 從我的最愛中移除 您未做出變更 您讓聊天室對所有知道連結的人公開。 - 您讓聊天室變為僅邀請可加入。 - 輸入您想要使用的伺服器地址 + 您讓聊天室變為邀請才可加入。 + 輸入您想要使用的伺服器位址 如果您不知道您的密碼,請返回並重設。 貼圖 管理動作 在 %1$s 中的預設 - 您的伺服器管理員已在私人聊天室與直接訊息中預設停用端到端加密。 + 您的伺服器管理員已在私人聊天室與私人訊息中預設停用端對端加密。 輸入只有您知道的安全密語,用於保護您伺服器上的安全祕密。 - 如果您現在取消,您可能會失去對加密訊息與資料的存取權,如果您失去對您登入的存取權的話。 + 如果您現在取消,當您忘記登入資訊的話,可能會失去存取加密訊息與資料。 \n -\n您也可以在設定中設定安全備份與管理您的金鑰。 +\n您也可以在「設定」中設定安全備份並管理您的金鑰。 您建立並設定了聊天室。 此帳號已被停用。 無法儲存媒體檔案 @@ -1546,12 +1546,12 @@ 停止相機 開啟相機 安全備份 - 透過備份加密金鑰到您的伺服器上以防止遺失對加密訊息與資料的存取權。 + 確保您能存取加密訊息與資料,請在您的伺服器上備份您的金鑰。 設定 使用安全金鑰 - 生成安全金鑰並儲存在安全的地方,如密碼管理員或保險櫃。 + 產生安全金鑰並儲存在安全的地方,如密碼管理員或保險櫃。 使用安全密語 - 輸入只有您知道的密語,並生成金鑰備份。 + 輸入只有您知道的密語,並產生金鑰備份。 儲存您的安全金鑰 將您的安全金鑰儲存在安全的地方,如密碼管理員或保險櫃。 設定安全密語 @@ -1563,28 +1563,28 @@ 您成功變更了聊天室設定 您無法存取此訊息 正在等待此訊息,可能需要花一點時間 - 由於端到端加密,您可能要等待某人的訊息抵達,因為加密金鑰未正確地傳送給您。 + 由於端對端加密,您可能要等待某人的訊息抵達,因為加密金鑰未正確地傳送給您。 您無法存取此訊息,因為您被傳送者封鎖 您無法存取此訊息,因為您的工作階段未被傳送者所信任 您無法存取此訊息,因為傳送者刻意未傳送金鑰 - 正在等待加密歷史 + 正在等待加密紀錄 Riot 現在是 Element 了! 我們很高興地我們已變更名稱!您的應用程式是最新的,而您也登入了您的帳號。 - 知道了 + 了解 取得更多資訊 儲存復原金鑰於 - 正在擷取您的聯絡人…… + 正在擷取您的聯絡人… 您的通訊錄為空 通訊錄 撤銷邀請 撤銷對 %1$s 的邀請? 被 %1$s 封鎖 - 取消封鎖使用者失敗 + 無法取消封鎖使用者 推送通知已停用 審閱您的設定以啟用推送通知 選擇 PIN 碼以確保安全 確認 PIN 碼 - 驗證 PIN 碼失敗,請點擊新的。 + 無法驗證 PIN 碼,請點擊新的。 輸入您的 PIN 碼 忘記 PIN 碼? 重設 PIN 碼 @@ -1592,26 +1592,26 @@ 要重設您的 PIN 碼,您將需要重新登入並建立新的。 啟用 PIN 碼 如果您想要重設您的 PIN 碼,點擊忘記 PIN 碼以登出並重設。 - 避免意外的通話 + 避免不小心撥出通話 開始通話前要求確認 您沒有在此聊天室中開始會議通話的權限 開始視訊會議 - 開始音訊會議 - 會議使用 Jitsi 安全與權限策略。目前在聊天室中的所有人在您的會議開始時都會看到加入的邀請。 + 開始語音會議 + 會議使用 Jitsi 安全與權限策略。目前在聊天室中的所有人,在會議開始時都會看到加入邀請。 您無法與自己通話 您無法與自己進行通話,請等候參與者接受邀請 - 新增小工具失敗 - 移除小工具失敗 + 無法新增小工具 + 無法移除小工具 - %1$d/%2$d 金鑰匯入成功。 + 成功匯入 %1$d 把金鑰,共 %2$d 把。 管理整合 - 無作用中的小工具 + 非使用中的小工具 已建立聊天室,但因為以下理由而未傳送某些邀請: \n \n%s - %1$s, %2$s 與 %3$d 個其他人已讀取 + %1$s、%2$s 與另 %3$d 個人已讀取 錯誤的代碼,還可以嘗試 %d 次 @@ -1628,9 +1628,9 @@ 電子郵件與電話號碼 管理連結到您 Matrix 帳號的電子郵件地址與電話號碼 代碼 - 請使用國際格式(電話號碼必須以 \'+\' 開頭) - 透過確認此登入來驗證您的身份,以及授予存取加密訊息的權限。 - 無法開啟禁止您進入的聊天室。 + 請使用國際格式(以「+」開頭的電話號碼) + 驗證此登入來確認您的身分,以及授予存取加密訊息的權限。 + 無法開啟封鎖您的聊天室。 找不到此聊天室。請確定它存在。 %d 秒 @@ -1657,24 +1657,24 @@ 顯示您現在可以驗證的 %d 個裝置 - 您將會重新啟動,沒有歷史紀錄,已信任的裝置或已信任的使用者 + 重新啟動後,將清空所有聊天紀錄、訊息、已信任的裝置或已信任的使用者 如果您重設了所有東西 僅在您沒有其他裝置可以驗證此裝置時才使用這個。 - 重設所有東西 + 恢復所有設定 忘記或遺失所有復原選項?重設所有東西 您已加入。 %s 已加入。 - 此聊天中的訊息有端到端加密。 + 此聊天中的訊息有端對端加密。 離開 設定 - 這裡的訊息有端到端加密。 + 這裡的訊息有端對端加密。 \n -\n您的訊息已使用鎖來保護,只有您與收件者才有獨特的金鑰來解鎖他們。 - 這裡的訊息沒有端到端加密。 - 此家伺服器正在執行較舊的版本。請要求您的家伺服器管理原升級。您可以繼續,但某些功能可能無法正常運作。 - 您讓此變為僅邀請。 - %1$s 讓此變為僅邀請。 - 在已加密的聊天室中顯示完整歷史紀錄 +\n您的訊息已被鎖保護,只有您與收件者有獨特的金鑰可以解鎖。 + 這裡的訊息沒有端對端加密。 + 此家伺服器執行的是舊版伺服器。請要求您的家伺服器管理員升級。您可以繼續,但某些功能可能無法正常運作。 + 您讓此變為邀請制。 + %1$s 讓此變為邀請制。 + 在加密聊天室顯示完整歷史紀錄 %1$s 與 %2$s %1$s 在 %2$s 與 %3$s @@ -1684,10 +1684,10 @@ 請點擊通知。如果您沒有看到通知,請檢查系統設定。 通知顯示 您正在檢視通知!點擊我! - 接收推播失敗。解決方法可能是重新安裝應用程式。 - 應用程式正在接收 PUSH - 應用程式正在等待 PUSH - 測試推播 + 接收推送失敗。解決方法可能是重新安裝應用程式。 + 應用程式正在接收推送 + 應用程式正在等待推送 + 測試推送 過濾被封鎖的使用者 您沒有開始通話的權限 您沒有開始會議通話的權限 @@ -1702,24 +1702,24 @@ 新增圖片從 聊天室設定 主題 - 聊天室主題(選擇性) + 聊天室主題(選填) 聊天室名稱 匯出審核 - 直接訊息 - 傳送金鑰共享請求歷史 + 私人訊息 + 傳送金鑰共享請求紀錄 沒有更多結果 - 未掃描 QR code! - 無效的 QR code(無效的 URI)! + 未掃描 QR Code! + 無效的 QR Code(無效的 URI)! 無法對您自己直接訊息! 透過文字分享 在 Matrix 上搜尋聯絡人 設定大頭照 使用者尚未提供其同意。 - 與夥伴們分享此條碼,這樣他們就可以掃描它來加入您並開始聊天。 - 我的條碼 - 分享我的條碼 - 掃描 QR code - 不是有效的 Matrix QR code + 與夥伴們分享此邀請碼,這樣他們就可以掃描它來加入您並開始聊天。 + 我的邀請碼 + 分享我的邀請碼 + 掃描 QR Code + 不是有效的 Matrix QR Code 🔐️ 在 ${app_name} 上加入我 嗨,和我在 ${app_name} 上聊天吧:%s 邀請朋友 @@ -1727,58 +1727,58 @@ 新增夥伴 新增主題 %s 讓人們知道這個聊天室是做什麼用的。 - 這是您與 %s 直接訊息歷史紀錄的開頭。 + 這是您與 %s 私人訊息紀錄的開頭。 這是此對話的開頭。 這是 %s 的開頭。 您沒有在此聊天室中啟用加密的權限。 - 正在建立聊天室…… - 不允許部份字元 - 請提供聊天室地址 - 此地址已被使用 - 如果聊天室僅用於與您的家伺服氣上的內部團隊協作的話,可以啟用此功能。但無法在稍後變更。 + 正在建立聊天室… + 有些字元未被允許 + 請提供聊天室位址 + 此位址已被使用 + 如果聊天室僅用於與您家伺服器上內部團隊協作的話,可以啟用此功能。但無法在稍後變更。 封鎖任何不是 %s 一部分的人加入此聊天室 隱藏進階 顯示進階 - %2$d 中的 %1$d + 第 %1$d 個,共 %2$d 個 給予同意 撤銷我的同意 - 您已同意傳送電子郵件地址與電話號碼到此身份提供者以從您的聯絡人中探索其他使用者。 + 您已同意傳送電子郵件地址與電話號碼到此身分提供者,以從您的聯絡人中探索其他使用者。 傳送電子郵件或電話號碼 建議 已知的使用者 - QR code - 透過 QR code 新增 + QR Code + 透過 QR Code 新增 允許存取您聯絡人的權限。 - 要掃描 QR code,您必須允許存取攝影機。 + 要掃描 QR Code,您必須允許存取相機。 開始聊天 變更您目前的 PIN 變更 PIN 無法預覽此聊天室。您想要加入嗎? 目前無法存取此聊天室。 \n請稍後再試,或是詢問聊天室管理員來檢查您是否可以存取。 - 無法擷取目前的聊天室目錄可見度 (%1$s)。 + 無法擷取目前的聊天室目錄可見度(%1$s)。 將此聊天室在 %1$s 的聊天室目錄中公開發佈? - 取消發佈此地址 - 發佈此地址 - 新增本地地址 - 此聊天室沒有本地地址 - 設定此聊天室的地址,這樣使用者就可以透過您的家伺服器 (%1$s) 尋找此聊天室 - 本地地址 - 新發佈的地址(例如:#alias:server) - 尚無其他已發佈的地址。 - 尚無其他已發佈的地址,在下面新增一個。 - 刪除地址「%1$s」? - 取消發佈地址「%1$s」? + 取消發佈此位址 + 發佈此位址 + 新增本地位址 + 此聊天室沒有本地位址 + 設定此聊天室的位址,這樣使用者就可以透過您的家伺服器(%1$s)搜尋此聊天室 + 本地位址 + 新發佈的位址(例如:#alias:server) + 尚無其他已發佈的位址。 + 尚無其他已發佈的位址,在下面新增一個。 + 刪除位址「%1$s」? + 取消發佈位址「%1$s」? 發佈 - 手動發佈新地址 - 其他已發佈地址: - 這是主要地址 - 任何伺服器上的任何人都可以用已發佈的地址加入您的聊天室。要發佈地址,必須先將其設為本地地址才行。 - 已發佈的地址 - 檢視並管理此聊天室的地址,以及其在聊天室目錄中的可見性。 - 聊天室地址 + 手動發佈新位址 + 其他已發佈位址: + 這是主要位址 + 任何伺服器上的任何人,都可以用已發佈的位址加入您的聊天室。要發佈位址,必須先將其設為本地位址才行。 + 已發佈的位址 + 檢視並管理此聊天室的位址,以及其在聊天室目錄中的可見性。 + 聊天室位址 聊天室存取權 - 對可讀取歷史紀錄的人的變更將僅試用於此聊天室中的未來訊息。現有歷史紀錄的可見性將保持不便。 + 對可閱讀歷史訊息的人員的變更,將僅適用於此聊天室的新訊息。現有訊息的可見性將保持不變。 取消發佈 新增 傳送下雪特效 ❄️ @@ -1789,48 +1789,48 @@ 單一登入 使用 %s 登入 使用 %s 註冊 - 繼續 %s + 使用 %s 繼續 使用 /confetti 指令或傳送包含 ❄️ 或 🎉 的訊息 顯示聊天效果 變更主題 升級聊天室 - 傳送 m.room.server_acl 活動 + 傳送 m.room.server_acl 事件 變更權限 變更聊天室名稱 變更歷史紀錄可見性 啟用聊天室加密 - 變更聊天室的主要位置 + 變更聊天室的主要位址 變更聊天室大頭照 修改小工具 通知每個人 移除其他人傳送的訊息 封鎖使用者 - 踢除使用者 + 移除使用者 變更設定 邀請使用者 傳送訊息 預設角色 - 您無權更新聊天室各部份所需的角色 + 您無權限更新聊天室各部份所需的角色 選取聊天室各部份所需的角色 權限 檢視並更新聊天室各部份所需的角色。 聊天室權限 - 此聊天室不公開。沒有邀請,您將無法重新加入。 - 系統預設值 + 此聊天室不公開。需要再次受邀才能重新加入。 + 系統預設 在訊息編輯器上新增按鈕以開啟表情符號鍵盤 顯示表情符號鍵盤 - 驗證失敗 + 無法驗證 ${app_name} 需要您輸入您的憑證來執行此動作。 需要重新驗證 - 未能設定交叉簽章 - 未授權,缺少有效的身份驗證憑證 + 未能設定交叉簽署 + 未授權,缺少有效的身分驗證憑證 使用者 - 轉移電話時發生錯誤 - 轉移 + 轉接電話時發生錯誤 + 轉接 連線 先諮詢 - 通話中 (%1$s) + 通話中(%1$s) 查詢電話號碼時發生錯誤 撥號鍵盤 回撥 @@ -1839,21 +1839,21 @@ 您保留了通話 %s 保留了通話 保留 - 恢復 - 活動內容 - 狀態活動已傳送! - 活動已傳送! - 活動格式錯誤 + 繼續 + 事件內容 + 已傳送狀態事件! + 已傳送事件! + 格式錯誤的事件 訊息內容遺失 無內容 - 活動內容 + 事件內容 狀態金鑰 類型 - 傳送自訂狀態活動 + 傳送自訂狀態事件 編輯內容 - 狀態活動 - 傳送狀態活動 - 傳送自訂活動 + 狀態事件 + 傳送狀態事件 + 傳送自訂事件 探索聊天室狀態 開發工具 檢視讀取回條 @@ -1886,29 +1886,29 @@ 伺服器版本 伺服器名稱 聊天室設定 - 離開目前的會議並切換其他的? + 離開目前的會議並切換到另一場會議? 聊天室版本 新值 切換 初始同步: -\n正在下載資料…… +\n正在下載資料… 初始同步: -\n正在等待伺服器回應…… +\n正在等待伺服器回應… 您確定您想要刪除此聊天室中所有未傳送的訊息嗎? 刪除未傳送的訊息 - 訊息傳送失敗 + 無法傳送訊息 您想要取消傳送訊息嗎? - 刪除所有失敗的訊息 - 失敗 + 刪除所有錯誤的訊息 + 錯誤 已傳送 正在傳送 - 顯示聊天室目錄中的所有聊天室,包含有明確內容的聊天室。 - 顯示帶有明確內容的聊天室 + 顯示聊天室目錄中的所有聊天室時,也顯示含有兒童不宜內容的聊天室。 + 顯示帶有兒童不宜內容的聊天室 聊天室目錄 訊息已傳送 您被邀請了 - 空間是將聊天室與人們分組的新方式。 - 新增既有的聊天室與空間 + 聊天空間是將聊天室與人們分組的新方式。 + 新增既有的聊天室與聊天空間 離開 新增聊天室 探索聊天室 @@ -1918,10 +1918,10 @@ 目前無法存取此別名。 \n請稍後再試,或要求聊天室管理員檢查您是否有權存取。 無論如何都要加入 - 加入空間 - 建立空間 + 加入聊天空間 + 建立聊天空間 現在略過 - 加入我的空間 %1$s %2$s + 加入我的聊天空間 %1$s %2$s 他們不會是 %s 的一部分 剛到此聊天室 他們將可以探索 %s @@ -1930,9 +1930,9 @@ 透過電子郵件邀請 此刻只有您。%s 與其他人一起會更好。 邀請夥伴 - 邀請夥伴進入您的空間 + 邀請夥伴進入您的聊天空間 描述 - 正在建立空間…… + 正在建立聊天空間… 隨機 一般 讓我們為每個主題建立一個聊天室。您也可以稍後再新增,包含既有的。 @@ -1942,40 +1942,40 @@ 為它命名以繼續。 新增一些詳細資訊以協助人們識別。您隨時可以變更這些資訊。 加入一些詳細資訊以協助其脫穎而出。您隨時都可以變更這些資料。 - 建立空間 - 僅邀請,最適合您自己或團隊 + 建立聊天空間 + 邀請制,最適合您自己或團隊 私人 對任何人開放,最適合社群 公開 - 供您與您的隊友使用的私人空間 - 我與隊友 - 用來整理您聊天室的私人空間 + 您與您的團隊成員的私人聊天空間 + 我與團隊成員 + 整理您聊天室的私人聊天空間 只有我 確定合適的人有權存取 %s。 您與誰一起工作? - 要加入既有的空間,您需要邀請。 + 要加入既有的聊天空間,您需要取得邀請。 您可以稍後再更改 - 您想要建立哪種類型的空間? - 您的私人空間 - 您的公開空間 - 新增空間 - 離開有指定 id 的聊天室(如果是 null 的話則為目前聊天室) - 使用指定的 id 加入空間 - 建立空間 + 您想要建立哪種類型的聊天空間? + 您的私人聊天空間 + 您公開的聊天空間 + 新增聊天空間 + 離開有指定 ID 的聊天室(如果是 null 的話則為目前聊天室) + 使用特定的 ID 加入聊天空間 + 建立聊天空間 未檢查 搜尋名稱 - 擁有此聊天室空間中的任何人都可以尋找並加入。僅此聊天室的管理員可以將其新增到空間中。 - 僅空間成員 + 包含此聊天室的聊天空間中的任何人都可以找到並加入此聊天室。但只有聊天室管理員可以將其新增到聊天空間中。 + 僅限限聊天空間成員 任何人都可以尋找聊天室並加入 公開 僅被邀請的夥伴可以尋找並加入 私人 - 未知的存取設定 (%s) - 任何人都可以要求加入聊天室,成員可以接受或回絕 + 存取設定未知(%s) + 任何人都可以要求加入聊天室,成員可以接受或拒絕 允許訪客加入 - 空間 + 聊天空間 建議的聊天室 - 管理聊天室與空間 + 管理聊天室與聊天空間 標記為不建議 標記為建議 建議 @@ -1989,34 +1989,34 @@ 檔案太大了,無法上傳。 正在壓縮影片 %d%% - 正在壓縮圖片…… + 正在壓縮圖片… 預設使用,不再詢問 總是詢問 - 有些聊天室可能是隱藏的,因為其為私人聊天室,您需要邀請。 - 有些聊天室可能是隱藏的,因為其為私人聊天室,您需要邀請。 + 有些聊天室可能是隱藏的,因為其為私人聊天室,您需要被邀請。 + 有些聊天室可能是隱藏的,因為其為私人聊天室,您需要被邀請。 \n您沒有權限新增聊天室。 - 此空間沒有聊天室 + 此聊天空間沒有聊天室 請聯絡您的家伺服器管理員以取得進一步的資訊 - 看來您的家伺服器尚未支援空間 + 看來您的家伺服器尚未支援聊天空間 想要做點實驗嗎? -\n您可以將既有的空間新增至其他空間中。 - 您是此空間唯一的管理員。若離開將意味著沒有人可以控制它。 +\n您可以將既有的聊天空間新增至其他聊天空間中。 + 您是此聊天空間唯一的管理員。若離開將意味著沒有人可以控制它。 除非被重新邀請,否則您將無法重新加入。 您是這裡唯一的人。如果您離開,包含您在內的所有人都將無法加入。 邀請至 %s 給予回饋 - 回饋傳送失敗 (%s) + 無法傳送回饋(%s) 謝謝,您的回饋已成功傳送 如果您有任何後續的問題,歡迎聯絡我 - 您正在使用空間的測試版本。您的回饋有助於改善未來的版本。我們將會紀錄您的平台與使用者名稱以協助我們使用您的回饋。 + 您正在使用聊天空間的 Beta 測試版本。您的回饋有助於改善未來的版本。我們將會紀錄您的平台與使用者名稱以協助我們使用您的回饋。 回饋 - 空間回饋 + 聊天空間回饋 抱歉,嘗試加入會議時發生錯誤 未命名聊天室 - 私人空間 - 公開空間 + 私人聊天空間 + 公開聊天空間 未知的人 - 轉移至 %1$s + 轉接至 %1$s 與 %1$s 進行諮詢 此伺服器已在清單中 找不到此伺服器或其聊天室清單 @@ -2024,17 +2024,17 @@ 加入新的伺服器 您的伺服器 抱歉,試圖加入時發生錯誤:%s - 空間地址 - 檢視與管理此空間的地址。 - 空間地址 + 聊天空間位址 + 檢視與管理此聊天空間的位址。 + 聊天空間位址 升級到建議的聊天室版本 此聊天室正在執行聊天室版本 %s,此家伺服器已被標記為不穩定。 您需要升級聊天室的權限 - 自動更新空間上層 + 自動更新母聊天空間 自動邀請使用者 您將要把此聊天室從 %1$s 升級到 %2$s。 - 升級聊天是是一項進階動作,通常建議在聊天室因臭蟲、缺少功能或安全漏洞而不穩定時使用。 -\n這通常只會影響聊天是在伺服器上的處理方式。 + 升級聊天室是一項進階動作,通常建議在聊天室因錯誤、缺少功能或安全漏洞而不穩定時使用。 +\n這通常只會影響在伺服器上處理聊天室的方式。 升級私人聊天室 升級公開聊天室 升級 @@ -2050,19 +2050,19 @@ 使用您的其他裝置掃描條碼或切換並使用此裝置掃描 家伺服器 API URL 缺少權限 - 要執行此動作,請從系統設定中授予「相機」權限。 - 缺少執行此動作的部份權限,請從系統設定中授予權限。 + 要執行此動作,請至系統設定授予「相機」權限。 + 缺少執行此動作的部份權限,請至系統設定授予權限。 - %d 通未接聽通話 + %d 通未接視訊通話 - %d 通未接聽通話 + %d 通未接來電 請注意,升級會讓聊天室變為新的版本。目前的所有訊息都將保留在此被封存的聊天室中。 - 上層空間中的任何人都可以找到並加入此聊天室,不需要手動邀請所有人。您隨時都可以在聊天室設定中變更此設定。 + 母聊天空間中的任何人都可以找到並加入此聊天室,不需要手動邀請所有人。您隨時都可以在聊天室設定中變更此設定。 任何在 %s 中的人都可以找到並加入此聊天室,不需要手動邀請所有人。您隨時都可以在聊天室設定中變更此設定。 - 語音訊息 (%1$s) - 語音訊息作用中時無法回覆或編輯 + 語音訊息(%1$s) + 語音訊息活躍時無法回覆或編輯 無法錄製語音訊息 無法播放此語音訊息 點擊您的錄音以停止或收聽 @@ -2076,41 +2076,41 @@ 錄製語音訊息 必須升級 語音 - 您可能不知道的其他空間或聊天室 - 您所知的包含此聊天是的空間 + 您可能不知道的其他聊天空間或聊天室 + 您所知包含此聊天室的聊天空間 決定誰可以找到並加入此聊天室。 - 點擊以編輯空間 - 選取空間 - 決定哪些空間可以存取此聊天室。若選取了某個空間,其成員就可以找到並加入此聊天室。 - 可以存取的空間 - 允許空間成員尋找並存取。 - 空間 %s 的成員可以找到、預覽並加入。 - 私人(僅邀請) + 點擊以編輯聊天空間 + 選取聊天空間 + 決定哪些聊天空間可以存取此聊天室。若選取了某個聊天空間,其成員就可以找到並加入此聊天室。 + 可以存取的聊天空間 + 允許聊天空間成員找到並存取此空間。 + 聊天空間 %s 的成員可以找到、預覽並加入。 + 私人(邀請制) 要傳送語音訊息,請授予「麥克風」權限。 聊天室升級 - 機器人訊息 + 來自聊天機器人的訊息 聊天室邀請 - 已加密的群組訊息 + 加密的群組訊息 群組訊息 - 已加密的直接訊息 - 直接訊息 + 加密的私人訊息 + 私人訊息 我的使用者名稱 我的顯示名稱 通知我 其他 - 提及與關鍵字 + 僅有被提及與出現關鍵字時 預設通知 設定中的 %s 可直接在 ${app_name} 中接收邀請。 將此電子郵件地址與您的帳號連結 - 此空間的邀請已傳送給與您的帳號無關的 %s - 此聊天室的邀請已傳送給與您的帳號無關的 %s + 此聊天空間的邀請已傳送給與您的帳號無關聯的 %s + 此聊天室的邀請已傳送給與您的帳號無關聯的 %s 您所在的所有聊天室都會顯示在 Home 中。 顯示 Home 中的所有聊天室 滑動結束通話 %1$s 點擊返回 - 作用中的通話 (%1$s)。 + 進行中的通話(%1$s)。 - %1$d 作用中的通話。 + %1$d 活躍的通話。 無回應 未接聽的視訊通話 @@ -2119,77 +2119,77 @@ 視訊通話已回絕 視訊通話已結束 • %1$s 語音通話已結束 • %1$s - 作用中的視訊通話 - 作用中的語音通話 - 來電視訊通話 - 來電語音通話 + 進行中的視訊通話 + 進行中的語音通話 + 視訊通話來電中 + 語音通話來電中 您回絕了此通話 帳號設定 您可以管理 %1$s 中的通知。 - 請注意,行動裝置上的加密聊天室並不提供提及與關鍵字通知。 + 請注意,行動裝置上的加密聊天室不提供被提及與關鍵字通知。 通知我 - 您不會在行動裝置上收到在加密聊天室中的提及與關鍵字通知。 + 您將不會在手機上收到加密聊天室中被提及或關鍵字的通知。 關鍵字 \@room 關鍵字不能包含「%s」 - 關鍵字不能以 \'.\' 開頭 - 新增新的關鍵字 + 關鍵字不能以「.」開頭 + 新增關鍵字 您的關鍵字 - 僅提及與關鍵字 - 結束通話…… - 無回應 - 您要通話的使用者忙碌中。 - 使用者忙碌 - 使用 %s 音訊通話 - 使用 %s 視訊通話 - 通話響鈴中…… - 空間 - 新增空間至您管理的任何空間。 - 新增既有的空間 + 僅有被提及與出現關鍵字時 + 結束通話… + 未接聽 + 您要通話的使用者忙線中。 + 使用者忙線中 + 與 %s 進行語音通話 + 與 %s 進行視訊通話 + 通話響鈴中… + 聊天空間 + 新增聊天空間至您管理的任何聊天空間。 + 新增既有的聊天空間 新增既有的聊天室 您確定您想要離開 %s? - 探索 (%s) + 探索 (%s) 結束設定 - 透過電子郵件邀請、尋找聯絡人以及更多…… + 透過電子郵件邀請、尋找聯絡人以及更多… 完成探索設定。 - 您目前並未使用身份認證伺服器。為了邀請隊友並被他們探索,請在下方設定一個。 + 您目前並未使用身分認證伺服器。為了邀請團隊成員並被他們探索,請在下方設定一個。 透過使用者名稱或電子郵件邀請 確保合適的人可以存取 %s 公司。您可以稍後再邀請。 - 誰是您的隊友? - 新增至指定的空間 - 正在建立空間…… + 誰是您的團隊成員? + 新增至特定的聊天空間 + 正在建立聊天空間… 顯示一些有用的資訊以協助應用程式除錯 在畫面上顯示除錯資訊 看起來不是有效的電子郵件地址 開啟探索設定 以名稱、ID 或電子郵件搜尋 - 建立新空間 - 任何人都可以找到空間並加入 - 空間存取 + 建立新聊天空間 + 任何人都可以找到此聊天空間並加入 + 存取聊天空間 誰可以存取? 啟用 %s 的電子郵件通知 - 要收到通知用的電子郵件,請將電子郵件地址關聯至您的 Matrix 帳號 + 要收到通知用的電子郵件,請將電子郵件地址連結至您的 Matrix 帳號 電子郵件通知 - 升級空間 - 變更空間名稱 - 啟用空間加密 - 變更空間的主要位置 - 變更空間大頭照 - 您無權更新變更此空間各部份所需角色的權限 - 選取變更此空間各部份所需的角色 - 檢視並更新變更空間各部份所需的角色。 - 空間權限 - 取消封鎖使用者將讓他們可以再次加入空間。 - 封鎖使用者會將他們自此空間移除並防止他們再次加入。 - 踢除使用者將會將他們從此空間中移除。 + 升級聊天空間 + 變更聊天空間名稱 + 啟用聊天空間加密 + 變更聊天空間的主要位址 + 變更聊天空間大頭照 + 您無權限更新變更此聊天空間各部份所需角色的權限 + 選取變更此聊天空間各部份所需的角色 + 檢視並更新變更聊天空間各部份所需的角色。 + 聊天空間權限 + 取消封鎖使用者將讓他們可以再次加入聊天空間。 + 封鎖使用者將會把他們從此聊天空間中移除,並避免他們再次加入。 + 使用者將會從此聊天空間中被移除。 \n \n為了防止他們再加入,您應該封鎖他們。 停止錄製 將 ( ͡° ͜ʖ ͡°) 附加至純文字訊息 - 身份伺服器未提供政策 - 隱藏身份伺服器政策 - 顯示身份伺服器政策 + 身分伺服器未提供政策 + 隱藏身分伺服器政策 + 顯示身分伺服器政策 顯示關於使用者的資訊 僅在目前的聊天室中變更您的大頭照 變更目前聊天室的大頭照 @@ -2202,7 +2202,7 @@ 線上 選擇家伺服器 無法存取 URL %s 的家伺服器。請檢查您的連結或手動選擇家伺服器。 - 監聽通知 + 正在監聽通知 需要至少 %1$s 選項 @@ -2216,11 +2216,11 @@ 建立投票 投票 向 %s 傳送電子郵件地址與電話號碼 - 您的通訊錄是私人的。要從您的通訊錄中探索使用者,我們需要您的權限來傳送聯絡人資訊到您的身份識別伺服器。 + 您的通訊錄是私人的。要從您的通訊錄中探索使用者,我們需要您的權限來傳送聯絡人資訊到您的身分伺服器。 已登出工作階段! 已離開聊天室! 您同意傳送此資訊嗎? - 要探索現有聯絡人,您必須傳送聯絡人資訊(電子郵件地址與電話號碼)到您的身份識別伺服器。我們會在傳送前對您的資料進行雜湊處理以保護隱私。 + 要探索現有聯絡人,您必須傳送聯絡人資訊(電子郵件地址與電話號碼)到您的身分伺服器。我們會在傳送前對您的資料進行雜湊處理以保護隱私。 現在不要 您確定要移除此投票?移除後將無法復原。 移除投票 @@ -2244,48 +2244,48 @@ 系統設定 版本 - 取得關於使用 ${app_name} 的協助 + 取得關於使用 ${app_name} 的說明 說明與支援 說明 - 法律 + 法律資訊 此伺服器並未提供任何政策。 第三方函式庫 - 您的身份識別伺服器政策 + 您的身分伺服器政策 您的家伺服器政策 ${app_name} 政策 您隨時可以在設定中關閉此功能 我們不會與第三方分享資訊 我們不會記錄或分析任何帳號資料 這裡 - 透過分享匿名使用資料協助我們找出問題並改善 ${app_name}。為了了解人們如何使用多裝置,我們將會產生隨機識別字串,在您的裝置間共享。 + 透過分享匿名使用資料協助我們找出問題並改善 ${app_name}。為了解人們如何使用多台裝置,我們將會產生隨機識別字串,在您的裝置間共享。 \n -\n您可以閱讀我們的條款 %s。 +\n您可以在 %s 閱讀我們的服務條款。 協助改善 ${app_name} 啟用 重新啟動應用程式以讓變更生效。 啟用 LaTeX 數學 - 您無法加入此聊天室 + 您未被允許加入此聊天室 建立投票 開啟通訊錄 傳送貼圖 上傳檔案 傳送圖片與影片 - 開啟攝影機 + 開啟相機 發生無法解密錯誤時,您的系統將會自動傳送紀錄檔 自動回報解密錯誤。 覆寫顯示名稱色彩 - 我已有一個帳號 + 我已有帳號 安全傳送訊息。 您已掌控了您的資料。 擁有您的對話。 分享位置 - 開啟以 + 開啟時用 ${app_name} 無法存取您的位置。請稍後再試。 ${app_name} 無法存取您的位置 位置 分享位置 結果僅在您結束投票後顯示 - 封闭式投票 + 已結束投票 投票者在投票後可以立刻看到投票結果 開放式投票 投票類型 @@ -2298,14 +2298,14 @@ 分享了他們的位置 建立帳號 為您的團隊傳送訊息。 - 端到端加密,不需要電話號碼。沒有廣告或資料挖礦。 + 端對端加密且不需電話號碼。沒有廣告或資料探勘。 選擇保留對話的位置,讓您擁有控制權與獨立性。透過 Matrix 連結。 安全且獨立的通訊,為您提供與在家中進行面對面對話相同的隱私等級。 位置 加密設定錯誤,因此您無法傳送訊息。點擊以開啟設定。 加密設定錯誤,因此您無法傳送訊息。請聯絡管理員將加密還原至有效的狀態。 顯示訊息泡泡 - 載入地圖失敗 + 無法載入地圖 地圖 注意:應用程式將會重新啟動 啟用討論串訊息 @@ -2321,20 +2321,20 @@ 您已經在檢視此討論串了! 在聊天室中檢視 在討論串中回覆 - 可識別指令「%s」,但在討論串中不支援。 + 可識別指令「%s」,但討論串中不支援此指令。 來自討論串 秘訣:長按訊息並使用「%s」。 - 討論串可以協助您的對話不離題且易於追蹤。 - 使用討論串來讓討論保持有條不紊 + 「討論串」功能可以協助您的對話不離題且易於追蹤。 + 使用「討論串」功能,讓討論保持有條不紊 顯示您參與的所有討論串 我的討論串 - 從目前的聊天室顯示所有討論串 + 顯示目前聊天室的所有討論串 所有討論串 過濾 討論串 討論串 過濾聊天室中的討論串 - 複製連結至討論串 + 複製討論串連結 在聊天室中檢視 檢視討論串 聊天室通知 @@ -2345,7 +2345,7 @@ 顯示較少 - %d 伺服器 ACL 變更 + 改變了伺服器 %d 的存取控制清單 %1$s 與 %2$s %1$s、%2$s 與其他人 @@ -2359,12 +2359,12 @@ 分享我目前的位置 縮放至目前位置 地圖上選定位置的圖釘 - 我們愈來愈接近將討論串釋出為公開測試版。 + 「討論串」功能的公開 Beta 測試版即將推出。 \n -\n在我們為此做準備時,我們需要做出一些變動:先前建立的討論串將會顯示為一般回覆。 +\n正當我們為此做準備時,需要做一些變動:先前建立的討論串將會顯示為一般回覆。 \n -\n這會是一次性的過渡,因為討論串現在是 Matrix 規範的一部分了。 - 討論串接近測試版了 🎉 +\n這只會發生一次,因為「討論串」功能現在已經進入 Matrix 規格了。 + 「討論串」快要有 Beta 測試版了 🎉 正在進行位置分享 ${app_name} 即時位置 家伺服器不接受僅有數字的使用者名稱。 @@ -2381,15 +2381,15 @@ 您的帳號 %s 已建立 恭喜! 帶我回家 - 個人化檔案 + 個人化簡介檔案 停用 - 正在載入即時位置…… + 正在載入即時位置… 8小時 1小時 15分鐘 分享您的即時位置 (%1$s) - %1$s (%2$s) + %1$s(%2$s) 無法播放 %1$s 暫停 %1$s 播放 %1$s @@ -2397,36 +2397,36 @@ %1$s, %2$s, %3$s 分享了他們的即時位置 ${app_name} 也非常適合工作場所。其受到世界上最安全的組織信任。 - 測試版 - 討論串是一項正在進行中的工作,包含了新的、令人興奮的即將推出的功能,例如改進的通知。我們想要聽到您的回饋! - 討論串測試版回饋 + Beta 測試版 + 「討論串」是一項正在進行中的工作,包含了即將推出令人興奮的新功能(例如改善後的通知)。我們想要聽到您的回饋! + 「討論串」功能 Beta 測試回饋 給予回饋 - 測試版 + BETA 測試版 若啟用,即使在使用應用程式時,您也會對其他使用者顯示為離線狀態。 離線模式 - 在場 - 您的家伺服器目前不支援討論串,所以此功能可能不可靠。部份已進入討論串的訊息可能無法可靠地使用。%s您仍想啟用討論串嗎? - 討論串測試版 - 討論串有助於讓您的對話不離題且易於追蹤。%s啟用討論串將會重新整理應用程式。對於特定帳號,可能需要更長的時間。 - 討論串測試版 + 出席 + 您的家伺服器目前不支援討論串,所以此功能可能並不可靠。部份已進入討論串的訊息,可能無法可靠地使用。%s 您仍想啟用討論串功能嗎? + 「討論串」Beta 測試版 + 「討論串」功能有助於讓您的對話不離題且易於追蹤。%s 啟用討論串將會重新整理應用程式。對於特定帳號可能需要較多的時間。 + 討論串 Beta 測試版 取得更多資訊 試試看 正在分享畫面 ${app_name} 分享畫面中 停止分享畫面 分享畫面 - - 部份使用者已被取消忽略 - ${app_name} 需要執行清除快取以保持最新狀態,原因如下: + - 有些使用者已被取消忽略 + ${app_name} 需要清除快取以保持最新狀態,原因如下: \n%s \n -\n注意,此動作將會重新啟動應用程式,並可能需要一些時間。 +\n注意:此動作會重新啟動應用程式,可能需要一些時間。 初始同步請求 顯示所有訊息的最新個人檔案資訊(大頭照與顯示名稱)。 顯示最新的使用者資訊 忙碌 - 備份具有來自該使用者的有效簽名。 + 備份具有來自該使用者的有效簽章。 %1$s 前已更新 - 暫時的實作:位置會保留在聊天室歷史紀錄中 + 暫時實作:保留位置資訊在聊天紀錄中 啟用即時位置分享 剩餘 %1$s 即時分享至 %1$s @@ -2436,14 +2436,14 @@ 找不到結果 不離開 離開全部 - 此空間內的東西 + 此聊天空間內的東西 動畫圖片一出現就在時間軸中播放 自動播放動畫圖片 - 分鐘 + 小時 啟用位置分享 - 請注意:這是使用暫時實作的實驗室功能。這代表了您將無法刪除您的位置歷史紀錄,即使您停止與此聊天室分享您的即時位置,進階使用者也還是能看到您的位置歷史紀錄。 + 請注意:這是使用暫時實作的實驗性功能。這代表您將無法刪除您的位置紀錄,即使您停止與此聊天室分享您的即時位置,進階使用者也還是能看到您的位置紀錄。 即時位置分享 目前閘道:%s 閘道 @@ -2463,28 +2463,28 @@ Google 服務 選擇如何接收通知 無法啟用生物特徵驗證。 - 生物特徵驗證方式被停用,因為最新新增了新的生物特徵驗證方式。您可以在「設定」中再次啟用它。 + 生物特徵驗證方式被停用,因為最新增了生物特徵驗證方式。您可以在「設定」中再次啟用它。 重設通知方式 個人檔案標籤: - 註冊端點權杖至家伺服器失敗: + 無法將端點權杖註冊至家伺服器: \n%1$s 端點成功註冊至家伺服器。 端點註冊 下一步 結果將在投票結束時可見 - 在分享歷史的加密聊天室中邀請時,加密歷史會是可見的。 + 邀請成員進入分享聊天紀錄的加密聊天室時,加密紀錄會是可見的。 MSC3061:為過去的訊息分享聊天室金鑰 傳送您的第一則訊息以邀請 %s 來聊天 - 此聊天中的訊息將會是端到端加密。 - 出發 + 此聊天中的訊息將會是端對端加密。 + 繼續前往 已移除 %d 則訊息 分享位置 您必須擁有正確的權限才能在此聊天室中分享即時位置。 - 您無權分享即時位置 - 無法開啟此連結:社群已被空間取代 - 使用者名稱 / 電子郵件 / 電話 + 您沒有權限分享即時位置 + 無法開啟此連結:社群已被聊天空間取代 + 使用者名稱/電子郵件/電話 您是人類嗎? 按照寄送到 %s 的說明操作 密碼重設 @@ -2498,12 +2498,12 @@ 確認您的電話號碼 登出所有裝置 重設密碼 - 確認其為 8 個字元或更多。 + 確認長度需有 8 個字元以上。 選擇新密碼 新密碼 - 檢查您的電子郵件。 - %s 將會傳送給您驗證連結 - 確認代碼 + 請到您的電子郵件信箱收信。 + %s 將會傳送驗證連結給您 + 驗證碼 電話號碼 %s 必須驗證您的帳號 輸入您的電話號碼 @@ -2513,36 +2513,36 @@ 請仔細閱讀 %s 的條款與政策 伺服器政策 保持聯絡 - Element Matrix Services (EMS) 是一個強大且可靠的主機託管服務,可實現快速、安全且即時的通訊。可在 element.io/ems 上了解如何使用 + Element Matrix Services(EMS)是一套強大且可靠的代管服務,可進行快速、安全且即時的通訊。可到 element.io/ems 了解如何使用 想要架設自己的伺服器? 伺服器 URL - 您的伺服器位置是什麼?這就像您所有資料的家 + 您的伺服器位址是什麼?這就像您所有資料的家 選取您的伺服器 歡迎回來! 編輯 您的對話將在哪裡進行 - 必須是 8 個字元或更多 - 其他人可以探索您 %s + 必須是 8 個以上字元長 + 其他人可以透過 %s 找到您 建立您的帳號 使用系統預設值 手動選擇 自動設定 選擇字型大小 - 自動批准 Element Call 小工具並授予相機/麥克風存取權限 + 自動批准 Element Call 小工具並授予相機/麥克風存取權限 啟用 Element Call 權限捷徑 即時位置 - 這個 QR code 的格式似乎不正確。請嘗試使用其他方法進行驗證。 - 您將無法存取已加密的訊息歷史紀錄。重設您的安全訊息備份與驗證金鑰以重新開始。 + 這個 QR Code 格式似乎不正確。請使用其他方法進行驗證。 + 您將無法存取加密訊息紀錄。重設您的安全訊息備份與驗證金鑰以重新開始。 無法驗證此裝置 - 您的伺服器地址是? - 您的對話所在位置 - 正在更新您的資料…… + 您的伺服器位址是? + 您的對話要在哪裡進行 + 正在更新您的資料… - %1$s 與 %2$d 個其他人 + %1$s 與其他 %2$d 位人員 %1$s 與 %2$s - 電子郵件未驗證,請檢查您的收件匣 + 電子郵件未驗證,請到您的信箱收信驗證 無法載入地圖 \n此家伺服器可能未設定好顯示地圖。 開啟設定 @@ -2550,10 +2550,10 @@ 為了取得最佳安全性,請驗證您的工作階段並登出任何您無法識別或不再使用的工作階段。 其他工作階段 工作階段 - 開啟空間清單 - 建立新的對話或聊天室 + 開啟聊天空間清單 + 建立一則新的對話或一間聊天室 聯絡人 - 最愛 + 我的最愛 未讀 全部 A - Z @@ -2567,7 +2567,7 @@ 開始聊天 未驗證 · 最後活動 %1$s 已驗證 · 最後活動 %1$s - 檢視全部 (%1$d) + 檢視全部(%1$d) 檢視詳細資訊 驗證工作階段 未驗證的工作階段 @@ -2582,8 +2582,8 @@ 試試看 輕點右上角來檢視回饋選項。 給予回饋 - 存取您的空間(右下角)比以往任何時候都更快且更輕鬆。 - 存取空間 + 存取您的聊天空間(右下角)比以往任何時候都更快且更輕鬆。 + 存取聊天空間 為了簡化您的 ${app_name},分頁現在是選擇性的。使用右上角的選單管理它們。 歡迎使用新的檢視! 當您有一些未讀的訊息時,這裡會顯示您的未讀訊息。 @@ -2591,7 +2591,7 @@ 適用於團隊、朋友與組織的多合一安全聊天應用程式。建立聊天室,或加入一個既有的聊天室。 歡迎使用 ${app_name}, \n%s. - 空間是一種為聊天室與人們分組的新方式。使用右下角的按鈕新增既有的聊天室或建立新的。 + 聊天空間是一種為聊天室與人們分組的新方式。使用右下角的按鈕新增既有的聊天室或建立新的。 %s \n看起來有點空。 @@ -2603,16 +2603,16 @@ 按照這些建議提高您的帳號安全性。 安全建議 - 不活躍 %1$d+ 天 (%2$s) + 不活躍 %1$d+ 天(%2$s) - 這是您的新請求與邀請的所在。 + 這是您新請求與邀請的所在。 沒有新東西。 - 空間是一種對聊天室與人們分組的新方式。建立空間以開始。 - 尚無空間。 - 折疊 %s 個子空間 - 展開 %s 個子空間 - 變更空間 - IP 位置 + 聊天空間是一種分類聊天室與聯絡人的新方式。開始建立聊天空間吧。 + 尚無聊天空間。 + 收折 %s 個子聊天空間 + 展開 %s 個子聊天空間 + 變更聊天空間 + IP 位址 最後活動 工作階段名稱 應用程式、裝置與活動資訊。 @@ -2622,13 +2622,13 @@ 找不到未驗證的工作階段。 找不到已驗證的工作階段。 - 閒置%1$d天或更久 + 閒置 %1$d 天或更久 - 考慮登出您不再使用的舊工作階段(%1$d天或更久)。 + 考慮登出您不再使用(%1$d天或更久)的舊工作階段。 不活躍 - 驗證您的工作階段以強化安全通訊或從您無法識別或不再使用的工作階段登出。 + 驗證您的工作階段以強化安全通訊,或從您無法識別或不再使用的工作階段登出。 未驗證 為取得最佳安全性,請從任何您無法識別或不再使用的工作階段登出。 已驗證 @@ -2636,7 +2636,7 @@ 不活躍 尚未準備好安全通訊 未驗證 - 準備好安全通訊 + 已準備好安全通訊 已驗證 所有工作階段 過濾 @@ -2648,11 +2648,11 @@ 驗證您目前的工作階段以強化安全通訊。 此工作階段已準備好安全通訊。 您目前的工作階段已準備好安全通訊。 - 僅在第一則訊息上建立直接訊息 - 啟用延期直接訊息 + 僅在第一則訊息上建立私人訊息 + 啟用延期的私人訊息 包含選擇性分頁的簡潔 Element 啟用新佈局 - 您加入的直接消息與聊天室中的其他使用者可以檢視您的工作階段的完整清單。 + 您加入的直接消息與聊天室中的其他使用者,可以檢視您的工作階段的完整清單。 \n \n這讓他們確信他們真的在與您交談,但這也意味著他們可以看到您在此處輸入的工作階段名稱。 正在重新命名工作階段 @@ -2663,7 +2663,7 @@ 未驗證的工作階段 不活躍的工作階段是您有一段時間未使用的工作階段,但它們會繼續接收加密金鑰。 \n -\n移除不活躍的工作階段可以改善安全性與效能,並讓您可以更容易地識別新的工作階段是否可疑。 +\n刪除不活躍的工作階段可以改善安全性與效能,並讓您可以更容易地識別新的工作階段是否可疑。 不活躍的工作階段 請注意,與您交流的人也可以看到工作階段名稱。 自訂工作階段名稱可以協助您更輕鬆地識別您的裝置。 @@ -2673,15 +2673,15 @@ 未驗證 · 您目前的工作階段 開始語音廣播 此裝置無法保證此加密訊息的真實性。 - 要求鍵盤不要根據您在對話中輸入的內容更新任何個人化資料(如輸入歷史紀錄與字典等)。請注意,某些鍵盤可能不會遵守此設定。 + 要求鍵盤不要根據您在對話中輸入的內容更新任何個人化資料(如輸入紀錄與字典等)。請注意,某些鍵盤可能不會遵守此設定。 無痕式鍵盤 將 (╯°□°)╯︵ ┻━┻ 放到純文字訊息之前 語音廣播 開啟開發者工具畫面 - 🔒 您已在「安全」設定中為所有聊天室啟用加密驗證工作階段。 + 🔒 您已在「安全性設定」中為所有聊天室啟用僅傳送加密訊息至已驗證的工作階段。 ⚠ 此聊天室中有未驗證的裝置,它們將無法解密您傳送的訊息。 - 切莫向此聊天室中未經驗證的工作階段傳送加密訊息。 - 知道了 + 絕不在此聊天室傳送加密訊息到未驗證的工作階段。 + 了解 套用底線格式 套用刪除線格式 套用義式斜體格式 @@ -2697,8 +2697,8 @@ 版本 名稱 應用程式 - 接收關於此工作階段的推播通知。 - 推播通知 + 接收關於此工作階段的推送通知。 + 推送通知 驗證您目前的工作階段以顯示此工作階段的驗證狀態。 未知的驗證狀態 已啟用: @@ -2707,66 +2707,66 @@ 授予權限 ${app_name} 需要權限以顯示通知。 \n請授予權限。 - ${app_name} 需要權限才能顯示通知。通知可以顯示您的訊息、您的邀請等等。 + ${app_name} 需要權限才能顯示通知。將在通知中顯示您收到的訊息、邀請等等。 \n -\n請在下一個彈出式視窗允許存取以檢視通知。 - 試用格式化文字編輯器(純文字模式即將推出) - 啟用格式化文字編輯器 - 請確保您知道此驗證碼的來源。透過連結裝置,您將為某人提供對您帳號的完整存取權限。 +\n請在下個彈跳視窗中允許存取,才能檢視通知。 + 試用富文字編輯器(純文字模式即將推出) + 啟用富文字編輯器 + 請確認您知道此驗證碼的來源。連結裝置後,您將為某人提供對您帳號的完整存取權限。 確認 - 再試一次 + 再次嘗試 不相符? - 登入 + 正在將你登入 連線至裝置 - 掃描 QR code - 正在使用行動裝置登入? - 在此裝置顯示 QR code - 選取「掃描 QR code」 + 掃描 QR Code + 要登入行動裝置嗎? + 在此裝置顯示 QR Code + 選取「掃描 QR Code」 從登入畫面開始 - 選取「使用 QR code 登入」 + 選取「使用 QR Code 登入」 從登入畫面開始 - 選取「顯示 QR code」 - 到「設定」→「安全與隱私」 - 在您的其他裝置上開啟應用程式 + 選取「顯示 QR Code」 + 到「設定」→「安全性與隱私權」 + 在您的另一台裝置上開啟應用程式 請求在另一台裝置上被拒絕。 - 連結未在規定時間內完成。 + 連結未在要求的時間內完成。 不支援與其裝置連結。 連線不成功 請檢查您已登入的裝置,應該會顯示以下驗證碼。請確認以下驗證碼與該裝置相符: 已建立安全連線 - 使用您已登出的裝置掃描以下 QR code。 - 使用您已登入的裝置來掃描下方的 QR code: - 使用 QR code 登入 - 使用此裝置的相機掃描您其他裝置上顯示的 QR code: - 掃描 QR code + 使用您已登出的裝置掃描以下 QR Code。 + 使用您已登入的裝置來掃描下方的 QR Code: + 使用 QR Code 登入 + 使用此裝置的相機掃描您其他裝置上顯示的 QR Code: + 掃描 QR Code 3 2 1 - 您可以使用此裝置透過 QR code 登入移動裝置或網路裝置。有兩種方法可以作到: - 使用 QR code 登入 - 掃描 QR code - 家伺服器不支援使用 QR code 登入。 - 登入已在其他裝置上取消。 - 該 QR code 無效。 - 其他裝置必須登入。 - 其他裝置已登入。 - 設定安全訊息傳遞時遇到安全問題。以下其中一項可能已被駭入:您的家伺服器、您的網際網路連線、您的裝置; - 請求失敗。 + 您可以使用此裝置透過 QR Code 登入行動裝置或網路裝置。有兩種方法可以作到: + 使用 QR Code 登入 + 掃描 QR Code + 家伺服器不支援使用 QR Code 登入。 + 已在另一台裝置上取消登入。 + 該 QR Code 無效。 + 另一台裝置必須登入。 + 另一台裝置已登入。 + 設定安全訊息傳遞時遇到安全問題。以下其中一項可能已被駭入:您的家伺服器;您的網際網路連線;您的裝置; + 無法請求。 可以在聊天室時間軸中錄製並傳送語音廣播。 啟用語音廣播 - 正在緩衝…… + 正在緩衝… 暫停語音廣播 播放或繼續語音廣播 - 停止語音廣播錄製 - 暫停語音廣播錄製 - 繼續語音廣播錄製 + 停止錄製語音廣播 + 暫停錄製語音廣播 + 繼續錄製語音廣播 直播 選取工作階段 聯絡人 相機 位置 投票 - 音訊廣播 + 語音廣播 附件 貼圖 照片媒體庫 @@ -2783,9 +2783,9 @@ 無法開始新的語音廣播 快轉30秒 快退30秒 - 已驗證的工作階段是您輸入通關密語或透過另一個已驗證工作階段確認您的身份後使用此帳號的任何地方。 + 已驗證的工作階段是您輸入安全密語或透過另一個已驗證工作階段確認您的身分後,使用此帳號的任何地方。 \n -\n這代表了您擁有解鎖加密訊息並向其他使用者確認您信任此工作階段所需的所有金鑰。 +\n這代表您擁有解鎖加密訊息並向其他使用者確認您信任此工作階段所需的所有金鑰。 登出 %1$d 個工作階段 @@ -2799,27 +2799,27 @@ 已傳送音訊檔。 已傳送檔案。 回覆給 - 隱藏 IP 位置 - 顯示 IP 位置 + 隱藏 IP 位址 + 顯示 IP 位址 引用 回覆給 %s 正在編輯 - 在系統分享選單中顯示最近聊天 + 在系統分享選單中顯示最近使用過的聊天室 啟用直接分享 - 檢查以確保您的帳號安全 + 請確認您的帳號安全 您有未驗證的工作階段 - 此工作階段不支援加密,因此無法驗證。 + 無法對此對話進行加密,因此也無法驗證。 \n -\n使用此工作階段時,您將無法參與啟用了加密的聊天室。 +\n使用此工作階段時無法進入加密聊天室中。 \n -\n為了取得最佳的安全性與隱私,建議使用支援加密的 Matrix 客戶端。 - 登出其他所有工作階段 - 此工作階段不支援加密,因此無法驗證。 - 取得最新版本(注意:您可能會無法登入) - Nightly 版本 - 即時廣播 - 您結束了語音廣播。 - %1$s 結束了語音廣播。 +\n為了安全與隱私,建議使用支援加密的 Matrix 客戶端。 + 登出所有其他的工作階段 + 此工作階段無法被加密,因此無法被驗證。 + 取得最新版本的建構(注意:您可能會無法登入) + 每夜建構 + 直播 + 您已結束語音直播。 + %1$s 已結束語音直播。 您真的想要停止您的即時廣播嗎?這將會結束廣播,完整的錄音會在聊天室中提供。 停止即時廣播? 是的,停止 @@ -2836,7 +2836,7 @@ 過去的投票 此聊天室沒有正在進行的投票 進行中的投票 - 投票歷史紀錄 + 投票紀錄 已結束投票 投票 已結束投票。 @@ -2860,4 +2860,13 @@ 您無法開始語音訊息,因為您目前正在錄製直播。請結束您的直播以開始錄製語音訊息 無法開始語音訊息 套用內嵌程式碼格式 + 切換程式碼區塊 + 切換引用 + 取消縮排 + 縮排 + 在時間軸中檢視投票 + 無法解密此語音廣播。 + 您的帳號詳細資訊已單獨於 %1$s 中管理。 + 帳號 + 更新您的通知偏好設定時發生錯誤。請再試一次。 \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 15ee44bb36..3d99c43883 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -863,6 +863,8 @@ Keywords cannot start with \'.\' Keywords cannot contain \'%s\' + An error occurred when updating your notification preferences. Please try again. + Troubleshoot Notifications Troubleshooting diagnostics Run Tests @@ -1064,6 +1066,9 @@ Discovery Manage your discovery settings. + Account + Your account details are managed separately at %1$s. + Analytics Send analytics data @@ -1813,6 +1818,7 @@ Add by QR code QR code "Creating room…" + You can only invite one email at a time Known Users Suggestions @@ -1821,7 +1827,7 @@ Terms of Service Be discoverable by others - Use Bots, bridges, widgets and sticker packs + Use bots, bridges, widgets and sticker packs Identity server Disconnect identity server @@ -2564,6 +2570,8 @@ Messages in this room are end-to-end encrypted. Learn more & verify users in their profile. Messages in this chat are end-to-end encrypted. Messages in this chat will be end-to-end encrypted. + Waiting for users to join ${app_name} + Once invited users have joined ${app_name}, you will be able to chat and the room will be end-to-end encrypted Encryption not enabled Encryption is misconfigured The encryption used by this room is not supported @@ -3131,6 +3139,7 @@ You are already recording a voice broadcast. Please end your current voice broadcast to start a new one. Unable to play this voice broadcast. Connection error - Recording paused + Unable to decrypt this voice broadcast. %1$s left Stop live broadcasting? @@ -3218,6 +3227,7 @@ Displaying polls Load more polls Error fetching polls. + View poll in timeline Share location @@ -3513,7 +3523,11 @@ Set link Toggle numbered list Toggle bullet list + Indent + Unindent + Toggle quote Apply inline code format + Toggle code block Toggle full screen mode Text diff --git a/library/ui-styles/src/main/res/values/styles_edit_text.xml b/library/ui-styles/src/main/res/values/styles_edit_text.xml index 6b282a7674..abb180ad87 100644 --- a/library/ui-styles/src/main/res/values/styles_edit_text.xml +++ b/library/ui-styles/src/main/res/values/styles_edit_text.xml @@ -15,7 +15,6 @@ @android:color/transparent textCapSentences|textMultiLine 10 - 40dp 10dp 10dp 12dp diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index f378b3acc2..0f2ae3567d 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -63,7 +63,7 @@ android { // that the app's state is completely cleared between tests. testInstrumentationRunnerArguments clearPackageData: 'true' - buildConfigField "String", "SDK_VERSION", "\"1.5.24\"" + buildConfigField "String", "SDK_VERSION", "\"1.5.30\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\"" @@ -209,7 +209,7 @@ dependencies { implementation 'com.facebook.stetho:stetho-okhttp3:1.6.0' // Video compression - implementation 'com.otaliastudios:transcoder:0.10.4' + implementation 'com.otaliastudios:transcoder:0.10.5' // Exif data handling implementation libs.apache.commonsImaging diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/rendezvous/RendezvousTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/rendezvous/RendezvousTest.kt new file mode 100644 index 0000000000..5b5aad4c51 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/rendezvous/RendezvousTest.kt @@ -0,0 +1,110 @@ +/* + * Copyright 2023 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.rendezvous + +import org.amshove.kluent.invoking +import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldBeInstanceOf +import org.amshove.kluent.shouldThrow +import org.amshove.kluent.with +import org.junit.Test +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.rendezvous.channels.ECDHRendezvousChannel +import org.matrix.android.sdk.api.rendezvous.model.RendezvousError +import org.matrix.android.sdk.common.CommonTestHelper + +class RendezvousTest : InstrumentedTest { + + @Test + fun shouldSuccessfullyBuildChannels() = CommonTestHelper.runCryptoTest(context()) { _, _ -> + val cases = listOf( + // v1: + "{\"rendezvous\":{\"algorithm\":\"org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256\"," + + "\"key\":\"aeSGwYTV1IUhikUyCapzC6p2xG5NpJ4Lwj2UgUMlcTk\",\"transport\":" + + "{\"type\":\"org.matrix.msc3886.http.v1\",\"uri\":\"https://rendezvous.lab.element.dev/bcab62cd-3e34-48b4-bc39-90895da8f6fe\"}}," + + "\"intent\":\"login.reciprocate\"}", + // v2: + "{\"rendezvous\":{\"algorithm\":\"org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256\"," + + "\"key\":\"aeSGwYTV1IUhikUyCapzC6p2xG5NpJ4Lwj2UgUMlcTk\",\"transport\":" + + "{\"type\":\"org.matrix.msc3886.http.v1\",\"uri\":\"https://rendezvous.lab.element.dev/bcab62cd-3e34-48b4-bc39-90895da8f6fe\"}}," + + "\"intent\":\"login.reciprocate\"}", + ) + + cases.forEach { input -> + Rendezvous.buildChannelFromCode(input).channel shouldBeInstanceOf ECDHRendezvousChannel::class + } + } + + @Test + fun shouldFailToBuildChannelAsUnsupportedAlgorithm() { + invoking { + Rendezvous.buildChannelFromCode( + "{\"rendezvous\":{\"algorithm\":\"bad algo\"," + + "\"key\":\"aeSGwYTV1IUhikUyCapzC6p2xG5NpJ4Lwj2UgUMlcTk\",\"transport\":" + + "{\"type\":\"org.matrix.msc3886.http.v1\",\"uri\":\"https://rendezvous.lab.element.dev/bcab62cd-3e34-48b4-bc39-90895da8f6fe\"}}," + + "\"intent\":\"login.reciprocate\"}" + ) + } shouldThrow RendezvousError::class with { + this.reason shouldBeEqualTo RendezvousFailureReason.UnsupportedAlgorithm + } + } + + @Test + fun shouldFailToBuildChannelAsUnsupportedTransport() { + invoking { + Rendezvous.buildChannelFromCode( + "{\"rendezvous\":{\"algorithm\":\"org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256\"," + + "\"key\":\"aeSGwYTV1IUhikUyCapzC6p2xG5NpJ4Lwj2UgUMlcTk\",\"transport\":" + + "{\"type\":\"bad transport\",\"uri\":\"https://rendezvous.lab.element.dev/bcab62cd-3e34-48b4-bc39-90895da8f6fe\"}}," + + "\"intent\":\"login.reciprocate\"}" + ) + } shouldThrow RendezvousError::class with { + this.reason shouldBeEqualTo RendezvousFailureReason.UnsupportedTransport + } + } + + @Test + fun shouldFailToBuildChannelWithInvalidIntent() { + invoking { + Rendezvous.buildChannelFromCode( + "{\"rendezvous\":{\"algorithm\":\"org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256\"," + + "\"key\":\"aeSGwYTV1IUhikUyCapzC6p2xG5NpJ4Lwj2UgUMlcTk\",\"transport\":" + + "{\"type\":\"org.matrix.msc3886.http.v1\",\"uri\":\"https://rendezvous.lab.element.dev/bcab62cd-3e34-48b4-bc39-90895da8f6fe\"}}," + + "\"intent\":\"foo\"}" + ) + } shouldThrow RendezvousError::class with { + this.reason shouldBeEqualTo RendezvousFailureReason.InvalidCode + } + } + + @Test + fun shouldFailToBuildChannelAsInvalidCode() { + val cases = listOf( + "{}", + "rubbish", + "" + ) + + cases.forEach { input -> + invoking { + Rendezvous.buildChannelFromCode(input) + } shouldThrow RendezvousError::class with { + this.reason shouldBeEqualTo RendezvousFailureReason.InvalidCode + } + } + } +} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt index 79acfa25dc..c98d8e5278 100644 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt +++ b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt @@ -94,7 +94,10 @@ internal class EventDecryptor @Inject constructor( * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. */ suspend fun decryptEventAndSaveResult(event: Event, timeline: String) { - tryOrNull(message = "Unable to decrypt the event") { + // event is not encrypted or already decrypted + if (event.getClearType() != EventType.ENCRYPTED) return + + tryOrNull(message = "decryptEventAndSaveResult | Unable to decrypt the event") { decryptEvent(event, timeline) } ?.let { result -> diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt index e490311b91..c6fab7762f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt @@ -44,7 +44,7 @@ interface AuthenticationService { /** * Get a SSO url. */ - fun getSsoUrl(redirectUrl: String, deviceId: String?, providerId: String?): String? + fun getSsoUrl(redirectUrl: String, deviceId: String?, providerId: String?, action: SSOAction): String? /** * Get the sign in or sign up fallback URL. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/SSOAction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/SSOAction.kt new file mode 100644 index 0000000000..db2dd870d5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/SSOAction.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.auth + +/** + * See https://github.com/matrix-org/matrix-spec-proposals/pull/3824 + */ +enum class SSOAction { + LOGIN, + REGISTER; +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/DelegatedAuthConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/DelegatedAuthConfig.kt new file mode 100644 index 0000000000..b57472ab7c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/DelegatedAuthConfig.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2023 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.auth.data + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * https://github.com/matrix-org/matrix-spec-proposals/pull/2965 + *
+ * {
+ *     "issuer": "https://id.server.org",
+ *     "account": "https://id.server.org/my-account",
+ * }
+ * 
+ * . + */ + +@JsonClass(generateAdapter = true) +data class DelegatedAuthConfig( + @Json(name = "issuer") + val issuer: String, + + @Json(name = "account") + val accountManagementUrl: String, +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt index 5de83033e1..5d737b716b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt @@ -22,6 +22,7 @@ data class LoginFlowResult( val isLoginAndRegistrationSupported: Boolean, val homeServerUrl: String, val isOutdatedHomeserver: Boolean, + val hasOidcCompatibilityFlow: Boolean, val isLogoutDevicesSupported: Boolean, val isLoginWithQrSupported: Boolean, ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/WellKnown.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/WellKnown.kt index 10c7d51392..95488bd682 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/WellKnown.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/WellKnown.kt @@ -54,5 +54,11 @@ data class WellKnown( val identityServer: WellKnownBaseConfig? = null, @Json(name = "m.integrations") - val integrations: JsonDict? = null + val integrations: JsonDict? = null, + + /** + * For delegation of auth via OIDC as per [MSC2965](https://github.com/matrix-org/matrix-spec-proposals/pull/2965). + */ + @Json(name = "org.matrix.msc2965.authentication") + val unstableDelegatedAuthConfig: DelegatedAuthConfig? = null, ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt index 851b75e44d..91f3c2a506 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt @@ -26,8 +26,11 @@ import org.matrix.android.sdk.api.rendezvous.model.Outcome import org.matrix.android.sdk.api.rendezvous.model.Payload import org.matrix.android.sdk.api.rendezvous.model.PayloadType import org.matrix.android.sdk.api.rendezvous.model.Protocol +import org.matrix.android.sdk.api.rendezvous.model.RendezvousCode import org.matrix.android.sdk.api.rendezvous.model.RendezvousError import org.matrix.android.sdk.api.rendezvous.model.RendezvousIntent +import org.matrix.android.sdk.api.rendezvous.model.RendezvousTransportType +import org.matrix.android.sdk.api.rendezvous.model.SecureRendezvousChannelAlgorithm import org.matrix.android.sdk.api.rendezvous.transports.SimpleHttpRendezvousTransport import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel @@ -50,18 +53,37 @@ class Rendezvous( @Throws(RendezvousError::class) fun buildChannelFromCode(code: String): Rendezvous { - val parsed = try { - // we rely on moshi validating the code and throwing exception if invalid JSON or doesn't + // we first check that the code is valid JSON and has right high-level structure + val genericParsed = try { + // we rely on moshi validating the code and throwing exception if invalid JSON or algorithm doesn't match + MatrixJsonParser.getMoshi().adapter(RendezvousCode::class.java).fromJson(code) + } catch (a: Throwable) { + throw RendezvousError("Malformed code", RendezvousFailureReason.InvalidCode) + } ?: throw RendezvousError("Code is null", RendezvousFailureReason.InvalidCode) + + // then we check that algorithm is supported + if (!SecureRendezvousChannelAlgorithm.values().map { it.value }.contains(genericParsed.rendezvous.algorithm)) { + throw RendezvousError("Unsupported algorithm", RendezvousFailureReason.UnsupportedAlgorithm) + } + + // and, that the transport is supported + if (!RendezvousTransportType.values().map { it.value }.contains(genericParsed.rendezvous.transport.type)) { + throw RendezvousError("Unsupported transport", RendezvousFailureReason.UnsupportedTransport) + } + + // now that we know the overall structure looks sensible, we rely on moshi validating the code and + // throwing exception if other parts are invalid + val supportedParsed = try { MatrixJsonParser.getMoshi().adapter(ECDHRendezvousCode::class.java).fromJson(code) } catch (a: Throwable) { - throw RendezvousError("Invalid code", RendezvousFailureReason.InvalidCode) - } ?: throw RendezvousError("Invalid code", RendezvousFailureReason.InvalidCode) + throw RendezvousError("Malformed ECDH rendezvous code", RendezvousFailureReason.InvalidCode) + } ?: throw RendezvousError("ECDH rendezvous code is null", RendezvousFailureReason.InvalidCode) - val transport = SimpleHttpRendezvousTransport(parsed.rendezvous.transport.uri) + val transport = SimpleHttpRendezvousTransport(supportedParsed.rendezvous.transport.uri) return Rendezvous( - ECDHRendezvousChannel(transport, parsed.rendezvous.key), - parsed.intent + ECDHRendezvousChannel(transport, supportedParsed.rendezvous.algorithm, supportedParsed.rendezvous.key), + supportedParsed.intent ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/channels/ECDHRendezvousChannel.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/channels/ECDHRendezvousChannel.kt index 4612e5463b..bcde4a2a7f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/channels/ECDHRendezvousChannel.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/channels/ECDHRendezvousChannel.kt @@ -41,7 +41,11 @@ import javax.crypto.spec.SecretKeySpec * Implements X25519 ECDH key agreement and AES-256-GCM encryption channel as per MSC3903: * https://github.com/matrix-org/matrix-spec-proposals/pull/3903 */ -class ECDHRendezvousChannel(override var transport: RendezvousTransport, theirPublicKeyBase64: String?) : RendezvousChannel { +class ECDHRendezvousChannel( + override var transport: RendezvousTransport, + private val algorithm: SecureRendezvousChannelAlgorithm, + theirPublicKeyBase64: String?, +) : RendezvousChannel { companion object { private const val ALGORITHM_SPEC = "AES/GCM/NoPadding" private const val KEY_SPEC = "AES" @@ -53,7 +57,7 @@ class ECDHRendezvousChannel(override var transport: RendezvousTransport, theirPu val algorithm: SecureRendezvousChannelAlgorithm? = null, val key: String? = null, val ciphertext: String? = null, - val iv: String? = null + val iv: String? = null, ) private val olmSASMutex = Mutex() @@ -65,10 +69,22 @@ class ECDHRendezvousChannel(override var transport: RendezvousTransport, theirPu init { theirPublicKeyBase64?.let { - theirPublicKey = Base64.decode(it, Base64.NO_WRAP) + theirPublicKey = decodeBase64(it) } olmSAS = OlmSAS() - ourPublicKey = Base64.decode(olmSAS!!.publicKey, Base64.NO_WRAP) + ourPublicKey = decodeBase64(olmSAS!!.publicKey) + } + + fun encodeBase64(input: ByteArray?): String? { + if (algorithm == SecureRendezvousChannelAlgorithm.ECDH_V2) { + return Base64.encodeToString(input, Base64.NO_WRAP or Base64.NO_PADDING) + } + return Base64.encodeToString(input, Base64.NO_WRAP) + } + + fun decodeBase64(input: String?): ByteArray { + // for decoding we aren't concerned about padding + return Base64.decode(input, Base64.NO_WRAP) } @Throws(RendezvousError::class) @@ -86,25 +102,25 @@ class ECDHRendezvousChannel(override var transport: RendezvousTransport, theirPu RendezvousFailureReason.UnsupportedAlgorithm, ) } - theirPublicKey = Base64.decode(res.key, Base64.NO_WRAP) + theirPublicKey = decodeBase64(res.key) } else { // send our public key unencrypted Timber.tag(TAG).i("Sending public key") send( ECDHPayload( - algorithm = SecureRendezvousChannelAlgorithm.ECDH_V1, - key = Base64.encodeToString(ourPublicKey, Base64.NO_WRAP) + algorithm = algorithm, + key = encodeBase64(ourPublicKey) ) ) } olmSASMutex.withLock { - sas.setTheirPublicKey(Base64.encodeToString(theirPublicKey, Base64.NO_WRAP)) - sas.setTheirPublicKey(Base64.encodeToString(theirPublicKey, Base64.NO_WRAP)) + sas.setTheirPublicKey(encodeBase64(theirPublicKey)) + sas.setTheirPublicKey(encodeBase64(theirPublicKey)) - val initiatorKey = Base64.encodeToString(if (isInitiator) ourPublicKey else theirPublicKey, Base64.NO_WRAP) - val recipientKey = Base64.encodeToString(if (isInitiator) theirPublicKey else ourPublicKey, Base64.NO_WRAP) - val aesInfo = "${SecureRendezvousChannelAlgorithm.ECDH_V1.value}|$initiatorKey|$recipientKey" + val initiatorKey = encodeBase64(if (isInitiator) ourPublicKey else theirPublicKey) + val recipientKey = encodeBase64(if (isInitiator) theirPublicKey else ourPublicKey) + val aesInfo = "${algorithm.value}|$initiatorKey|$recipientKey" aesKey = sas.generateShortCode(aesInfo, 32) @@ -162,20 +178,20 @@ class ECDHRendezvousChannel(override var transport: RendezvousTransport, theirPu cipherText.addAll(encryptCipher.doFinal().toList()) return ECDHPayload( - ciphertext = Base64.encodeToString(cipherText.toByteArray(), Base64.NO_WRAP), - iv = Base64.encodeToString(iv, Base64.NO_WRAP) + ciphertext = encodeBase64(cipherText.toByteArray()), + iv = encodeBase64(iv) ) } private fun decrypt(payload: ECDHPayload): ByteArray { - val iv = Base64.decode(payload.iv, Base64.NO_WRAP) + val iv = decodeBase64(payload.iv) val encryptCipher = Cipher.getInstance(ALGORITHM_SPEC) val secretKeySpec = SecretKeySpec(aesKey, KEY_SPEC) val ivParameterSpec = IvParameterSpec(iv) encryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec) val plainText = LinkedList() - plainText.addAll(encryptCipher.update(Base64.decode(payload.ciphertext, Base64.NO_WRAP)).toList()) + plainText.addAll(encryptCipher.update(decodeBase64(payload.ciphertext)).toList()) plainText.addAll(encryptCipher.doFinal().toList()) return plainText.toByteArray() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/Rendezvous.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/Rendezvous.kt new file mode 100644 index 0000000000..f424f8cab0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/Rendezvous.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2023 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.rendezvous.model + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +open class Rendezvous( + val transport: RendezvousTransportDetails, + val algorithm: String, +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/RendezvousCode.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/RendezvousCode.kt new file mode 100644 index 0000000000..ffa8bf6661 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/RendezvousCode.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2023 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.rendezvous.model + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +open class RendezvousCode( + open val intent: RendezvousIntent, + open val rendezvous: Rendezvous +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/RendezvousTransportDetails.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/RendezvousTransportDetails.kt index 1bde43ab7e..34d96ac64a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/RendezvousTransportDetails.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/RendezvousTransportDetails.kt @@ -20,5 +20,5 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) open class RendezvousTransportDetails( - val type: RendezvousTransportType + val type: String ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/SecureRendezvousChannelAlgorithm.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/SecureRendezvousChannelAlgorithm.kt index 75f0024fda..123e41a5d7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/SecureRendezvousChannelAlgorithm.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/SecureRendezvousChannelAlgorithm.kt @@ -22,5 +22,7 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = false) enum class SecureRendezvousChannelAlgorithm(val value: String) { @Json(name = "org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256") - ECDH_V1("org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256") + ECDH_V1("org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256"), + @Json(name = "org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256") + ECDH_V2("org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256") } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/SimpleHttpRendezvousTransportDetails.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/SimpleHttpRendezvousTransportDetails.kt index 049aa8b756..d2342bb9d5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/SimpleHttpRendezvousTransportDetails.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/SimpleHttpRendezvousTransportDetails.kt @@ -21,4 +21,4 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class SimpleHttpRendezvousTransportDetails( val uri: String -) : RendezvousTransportDetails(type = RendezvousTransportType.MSC3886_SIMPLE_HTTP_V1) +) : RendezvousTransportDetails(type = RendezvousTransportType.MSC3886_SIMPLE_HTTP_V1.name) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/CryptoDeviceInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/CryptoDeviceInfo.kt index 418b1e6ce3..a1c5866fff 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/CryptoDeviceInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/CryptoDeviceInfo.kt @@ -34,6 +34,9 @@ data class CryptoDeviceInfo( val isVerified: Boolean get() = trustLevel?.isVerified() == true + val isCrossSigningVerified: Boolean + get() = trustLevel?.isCrossSigningVerified() == true + val isUnknown: Boolean get() = trustLevel == null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/EventService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/EventService.kt index 7f275bf952..11ef3f0d2f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/EventService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/EventService.kt @@ -28,4 +28,12 @@ interface EventService { roomId: String, eventId: String ): Event + + /** + * Get an Event from cache. Return null if not found. + */ + fun getEventFromCache( + roomId: String, + eventId: String + ): Event? } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt index d5539e9ae8..bad8b3766d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt @@ -493,7 +493,7 @@ fun Event.getPollContent(): MessagePollContent? { } fun Event.supportsNotification() = - this.getClearType() in EventType.MESSAGE + EventType.POLL_START.values + EventType.STATE_ROOM_BEACON_INFO.values + this.getClearType() in EventType.MESSAGE + EventType.POLL_START.values + EventType.POLL_END.values + EventType.STATE_ROOM_BEACON_INFO.values fun Event.isContentReportable() = this.getClearType() in EventType.MESSAGE + EventType.STATE_ROOM_BEACON_INFO.values diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt index 96e52469c3..4968df775a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt @@ -80,6 +80,11 @@ data class HomeServerCapabilities( * True if the home server supports event redaction with relations. */ var canRedactEventWithRelations: Boolean = false, + + /** + * External account management url for use with MSC3824 delegated OIDC, provided in Wellknown. + */ + val externalAccountManagementUrl: String? = null, ) { enum class RoomCapabilitySupport { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/EventMatchCondition.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/EventMatchCondition.kt index 15d5cd3153..c3ddbf14b3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/EventMatchCondition.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/EventMatchCondition.kt @@ -39,8 +39,17 @@ class EventMatchCondition( override fun technicalDescription() = "'$key' matches '$pattern'" fun isSatisfied(event: Event): Boolean { - // TODO encrypted events? - val rawJson = MoshiProvider.providesMoshi().adapter(Event::class.java).toJsonValue(event) as? Map<*, *> + val rawJson: Map<*, *> = (MoshiProvider.providesMoshi().adapter(Event::class.java).toJsonValue(event) as? Map<*, *>) + ?.let { rawJson -> + val decryptedRawJson = event.mxDecryptionResult?.payload + if (decryptedRawJson != null) { + rawJson + .toMutableMap() + .apply { putAll(decryptedRawJson) } + } else { + rawJson + } + } ?: return false val value = extractField(rawJson, key) ?: return false diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/RuleIds.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/RuleIds.kt index 4f35fb79c3..34581b613a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/RuleIds.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/RuleIds.kt @@ -47,6 +47,16 @@ object RuleIds { const val RULE_ID_ALL_OTHER_MESSAGES_ROOMS = ".m.rule.message" const val RULE_ID_ENCRYPTED = ".m.rule.encrypted" + const val RULE_ID_POLL_START_ONE_TO_ONE = ".m.rule.poll_start_one_to_one" + const val RULE_ID_POLL_START_ONE_TO_ONE_UNSTABLE = ".org.matrix.msc3930.rule.poll_start_one_to_one" + const val RULE_ID_POLL_END_ONE_TO_ONE = ".m.rule.poll_end_one_to_one" + const val RULE_ID_POLL_END_ONE_TO_ONE_UNSTABLE = ".org.matrix.msc3930.rule.poll_end_one_to_one" + + const val RULE_ID_POLL_START = ".m.rule.poll_start" + const val RULE_ID_POLL_START_UNSTABLE = ".org.matrix.msc3930.rule.poll_start" + const val RULE_ID_POLL_END = ".m.rule.poll_end" + const val RULE_ID_POLL_END_UNSTABLE = ".org.matrix.msc3930.rule.poll_end" + // Not documented const val RULE_ID_FALLBACK = ".m.rule.fallback" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/rest/RuleSet.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/rest/RuleSet.kt index 9498ed002c..9287a7828d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/rest/RuleSet.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/rest/RuleSet.kt @@ -47,21 +47,14 @@ data class RuleSet( * @param ruleId a RULE_ID_XX value * @return the matched bing rule or null it doesn't exist. */ - fun findDefaultRule(ruleId: String?): PushRuleAndKind? { - var result: PushRuleAndKind? = null - // sanity check - if (null != ruleId) { - if (RuleIds.RULE_ID_CONTAIN_USER_NAME == ruleId) { - result = findRule(content, ruleId)?.let { PushRuleAndKind(it, RuleSetKey.CONTENT) } - } else { - // assume that the ruleId is unique. - result = findRule(override, ruleId)?.let { PushRuleAndKind(it, RuleSetKey.OVERRIDE) } - if (null == result) { - result = findRule(underride, ruleId)?.let { PushRuleAndKind(it, RuleSetKey.UNDERRIDE) } - } - } + fun findDefaultRule(ruleId: String): PushRuleAndKind? { + return if (RuleIds.RULE_ID_CONTAIN_USER_NAME == ruleId) { + findRule(content, ruleId)?.let { PushRuleAndKind(it, RuleSetKey.CONTENT) } + } else { + // assume that the ruleId is unique. + findRule(override, ruleId)?.let { PushRuleAndKind(it, RuleSetKey.OVERRIDE) } + ?: findRule(underride, ruleId)?.let { PushRuleAndKind(it, RuleSetKey.UNDERRIDE) } } - return result } /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageEndPollContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageEndPollContent.kt index 6e31320b13..9c894ebe28 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageEndPollContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageEndPollContent.kt @@ -33,5 +33,9 @@ data class MessageEndPollContent( override val msgType: String = MessageType.MSGTYPE_POLL_END, @Json(name = "body") override val body: String = "", @Json(name = "m.new_content") override val newContent: Content? = null, - @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null -) : MessageContent + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "org.matrix.msc1767.text") val unstableText: String? = null, + @Json(name = "m.text") val text: String? = null, +) : MessageContent { + fun getBestText() = text ?: unstableText +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt index 3aa480094c..a49c20ccbb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt @@ -28,6 +28,7 @@ import org.matrix.android.sdk.api.session.events.model.isLiveLocation import org.matrix.android.sdk.api.session.events.model.isPoll import org.matrix.android.sdk.api.session.events.model.isReply import org.matrix.android.sdk.api.session.events.model.isSticker +import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary import org.matrix.android.sdk.api.session.room.model.ReadReceipt @@ -36,6 +37,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocati import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithFormattedBody import org.matrix.android.sdk.api.session.room.model.message.MessageEndPollContent +import org.matrix.android.sdk.api.session.room.model.message.MessageFormat import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent @@ -157,7 +159,39 @@ fun TimelineEvent.getLastMessageContent(): MessageContent? { } fun TimelineEvent.getLastEditNewContent(): Content? { - return annotations?.editSummary?.latestEdit?.getClearContent()?.toModel()?.newContent + val lastContent = annotations?.editSummary?.latestEdit?.getClearContent()?.toModel()?.newContent + return if (isReply()) { + val previousFormattedBody = root.getClearContent().toModel()?.formattedBody + if (previousFormattedBody?.isNotEmpty() == true) { + val lastMessageContent = lastContent.toModel() + lastMessageContent?.let { ensureCorrectFormattedBodyInTextReply(it, previousFormattedBody) }?.toContent() ?: lastContent + } else { + lastContent + } + } else { + lastContent + } +} + +private const val MX_REPLY_END_TAG = "" + +/** + * Not every client sends a formatted body in the last edited event since this is not required in the + * [Matrix specification](https://spec.matrix.org/v1.4/client-server-api/#applying-mnew_content). + * We must ensure there is one so that it is still considered as a reply when rendering the message. + */ +private fun ensureCorrectFormattedBodyInTextReply(messageTextContent: MessageTextContent, previousFormattedBody: String): MessageTextContent { + return when { + messageTextContent.formattedBody.isNullOrEmpty() && previousFormattedBody.contains(MX_REPLY_END_TAG) -> { + // take previous formatted body with the new body content + val newFormattedBody = previousFormattedBody.replaceAfterLast(MX_REPLY_END_TAG, messageTextContent.body) + messageTextContent.copy( + formattedBody = newFormattedBody, + format = MessageFormat.FORMAT_MATRIX_HTML, + ) + } + else -> messageTextContent + } } private fun TimelineEvent.getLastPollEditNewContent(): Content? { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt index d9c2afcb40..d1dd0238ba 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt @@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.MatrixPatterns.getServerName import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.LoginType +import org.matrix.android.sdk.api.auth.SSOAction import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.data.LoginFlowResult @@ -88,7 +89,7 @@ internal class DefaultAuthenticationService @Inject constructor( return getLoginFlow(homeServerConnectionConfig) } - override fun getSsoUrl(redirectUrl: String, deviceId: String?, providerId: String?): String? { + override fun getSsoUrl(redirectUrl: String, deviceId: String?, providerId: String?, action: SSOAction): String? { val homeServerUrlBase = getHomeServerUrlBase() ?: return null return buildString { @@ -103,6 +104,9 @@ internal class DefaultAuthenticationService @Inject constructor( // But https://github.com/matrix-org/synapse/issues/5755 appendParamToUrl("device_id", it) } + + // unstable MSC3824 action param + appendParamToUrl("org.matrix.msc3824.action", action.toString()) } } @@ -292,12 +296,18 @@ internal class DefaultAuthenticationService @Inject constructor( val loginFlowResponse = executeRequest(null) { authAPI.getLoginFlows() } + + // If an m.login.sso flow is present that is flagged as being for MSC3824 OIDC compatibility then we only return that flow + val oidcCompatibilityFlow = loginFlowResponse.flows.orEmpty().firstOrNull { it.type == "m.login.sso" && it.delegatedOidcCompatibilty == true } + val flows = if (oidcCompatibilityFlow != null) listOf(oidcCompatibilityFlow) else loginFlowResponse.flows + return LoginFlowResult( - supportedLoginTypes = loginFlowResponse.flows.orEmpty().mapNotNull { it.type }, - ssoIdentityProviders = loginFlowResponse.flows.orEmpty().firstOrNull { it.type == LoginFlowTypes.SSO }?.ssoIdentityProvider, + supportedLoginTypes = flows.orEmpty().mapNotNull { it.type }, + ssoIdentityProviders = flows.orEmpty().firstOrNull { it.type == LoginFlowTypes.SSO }?.ssoIdentityProvider, isLoginAndRegistrationSupported = versions.isLoginAndRegistrationSupportedBySdk(), homeServerUrl = homeServerUrl, isOutdatedHomeserver = !versions.isSupportedBySdk(), + hasOidcCompatibilityFlow = oidcCompatibilityFlow != null, isLogoutDevicesSupported = versions.doesServerSupportLogoutDevices(), isLoginWithQrSupported = versions.doesServerSupportQrCodeLogin(), ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt index df10e110d1..971407388c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt @@ -43,6 +43,13 @@ internal data class LoginFlow( * See MSC #2858 */ @Json(name = "identity_providers") - val ssoIdentityProvider: List? = null + val ssoIdentityProvider: List? = null, + /** + * Whether this login flow is preferred for OIDC-aware clients. + * + * See [MSC3824](https://github.com/matrix-org/matrix-spec-proposals/pull/3824) + */ + @Json(name = "org.matrix.msc3824.delegated_oidc_compatibility") + val delegatedOidcCompatibilty: Boolean? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index c5ececcddb..bca189134b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -68,6 +68,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo048 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo049 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo050 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo051 +import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo052 import org.matrix.android.sdk.internal.util.Normalizer import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import javax.inject.Inject @@ -76,7 +77,7 @@ internal class RealmSessionStoreMigration @Inject constructor( private val normalizer: Normalizer ) : MatrixRealmMigration( dbName = "Session", - schemaVersion = 51L, + schemaVersion = 52L, ) { /** * Forces all RealmSessionStoreMigration instances to be equal. @@ -137,5 +138,6 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion < 49) MigrateSessionTo049(realm).perform() if (oldVersion < 50) MigrateSessionTo050(realm).perform() if (oldVersion < 51) MigrateSessionTo051(realm).perform() + if (oldVersion < 52) MigrateSessionTo052(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt index 7999a2ea14..a100741452 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt @@ -297,7 +297,7 @@ internal fun updateThreadNotifications(roomId: String, realm: Realm, currentUser val readReceipt = findMyReadReceipt(realm, roomId, currentUserId, threadId = rootThreadEventId) ?: return val readReceiptChunk = ChunkEntity - .findIncludingEvent(realm, readReceipt) ?: return + .findIncludingEvent(realm, roomId, readReceipt) ?: return val readReceiptChunkThreadEvents = readReceiptChunk .timelineEvents diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt index 83f3e87d05..1c7a0591a1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt @@ -48,6 +48,7 @@ internal object HomeServerCapabilitiesMapper { canUseThreadReadReceiptsAndNotifications = entity.canUseThreadReadReceiptsAndNotifications, canRemotelyTogglePushNotificationsOfDevices = entity.canRemotelyTogglePushNotificationsOfDevices, canRedactEventWithRelations = entity.canRedactEventWithRelations, + externalAccountManagementUrl = entity.externalAccountManagementUrl, ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo051.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo051.kt index f5729a1f5e..3ca9846024 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo051.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo051.kt @@ -17,18 +17,14 @@ package org.matrix.android.sdk.internal.database.migration import io.realm.DynamicRealm -import org.matrix.android.sdk.internal.database.model.EventEntityFields +import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields +import org.matrix.android.sdk.internal.extensions.forceRefreshOfHomeServerCapabilities import org.matrix.android.sdk.internal.util.database.RealmMigrator -/** - * As we compute message e2ee verification state at decryption time, it might get outdated. - * Adding a new field to mark a decryption state as dirty - */ internal class MigrateSessionTo051(realm: DynamicRealm) : RealmMigrator(realm, 51) { - override fun doMigrate(realm: DynamicRealm) { - realm.schema.get("EventEntity") - ?.addField(EventEntityFields.IS_VERIFICATION_STATE_DIRTY, Boolean::class.java) - ?.setNullable(EventEntityFields.IS_VERIFICATION_STATE_DIRTY, true) + realm.schema.get("HomeServerCapabilitiesEntity") + ?.addField(HomeServerCapabilitiesEntityFields.EXTERNAL_ACCOUNT_MANAGEMENT_URL, String::class.java) + ?.forceRefreshOfHomeServerCapabilities() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo052.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo052.kt new file mode 100644 index 0000000000..1638c2334d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo052.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.migration + +import io.realm.DynamicRealm +import org.matrix.android.sdk.internal.database.model.EventEntityFields +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +internal class MigrateSessionTo052(realm: DynamicRealm) : RealmMigrator(realm, 52) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.get("EventEntity") + ?.addField(EventEntityFields.IS_VERIFICATION_STATE_DIRTY, Boolean::class.java) + ?.setNullable(EventEntityFields.IS_VERIFICATION_STATE_DIRTY, true) + } +} + diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt index 9acdcde7e5..35a5c654de 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt @@ -35,6 +35,7 @@ internal open class HomeServerCapabilitiesEntity( var canUseThreadReadReceiptsAndNotifications: Boolean = false, var canRemotelyTogglePushNotificationsOfDevices: Boolean = false, var canRedactEventWithRelations: Boolean = false, + var externalAccountManagementUrl: String? = null, ) : RealmObject() { companion object diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt index 1e5d96b496..08c8bcf86e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt @@ -72,15 +72,16 @@ internal fun ChunkEntity.Companion.findEventInThreadChunk(realm: Realm, roomId: .findFirst() } -internal fun ChunkEntity.Companion.findAllIncludingEvents(realm: Realm, eventIds: List): RealmResults { +internal fun ChunkEntity.Companion.findAllIncludingEvents(realm: Realm, roomId: String, eventIds: List): RealmResults { return realm.where() + .equalTo(ChunkEntityFields.ROOM.ROOM_ID, roomId) .`in`(ChunkEntityFields.TIMELINE_EVENTS.EVENT_ID, eventIds.toTypedArray()) .isNull(ChunkEntityFields.ROOT_THREAD_EVENT_ID) .findAll() } -internal fun ChunkEntity.Companion.findIncludingEvent(realm: Realm, eventId: String): ChunkEntity? { - return findAllIncludingEvents(realm, listOf(eventId)).firstOrNull() +internal fun ChunkEntity.Companion.findIncludingEvent(realm: Realm, roomId: String, eventId: String): ChunkEntity? { + return findAllIncludingEvents(realm, roomId, listOf(eventId)).firstOrNull() } internal fun ChunkEntity.Companion.create( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt index 4805c36f8c..75232f01f1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt @@ -47,6 +47,12 @@ internal fun EventEntity.Companion.where(realm: Realm, eventId: String): RealmQu .equalTo(EventEntityFields.EVENT_ID, eventId) } +internal fun EventEntity.Companion.where(realm: Realm, roomId: String, eventId: String): RealmQuery { + return realm.where() + .equalTo(EventEntityFields.ROOM_ID, roomId) + .equalTo(EventEntityFields.EVENT_ID, eventId) +} + internal fun EventEntity.Companion.whereRoomId(realm: Realm, roomId: String): RealmQuery { return realm.where() .equalTo(EventEntityFields.ROOM_ID, roomId) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt index ebfe23105e..0cc4abcb3d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt @@ -76,11 +76,11 @@ private fun hasReadMissingEvent(realm: Realm, userId: String, eventId: String, threadId: String? = ReadService.THREAD_ID_MAIN): Boolean { - return realm.doesEventExistInChunkHistory(eventId) && realm.hasReadReceiptInLatestChunk(latestChunkEntity, roomId, userId, threadId) + return realm.doesEventExistInChunkHistory(roomId, eventId) && realm.hasReadReceiptInLatestChunk(latestChunkEntity, roomId, userId, threadId) } -private fun Realm.doesEventExistInChunkHistory(eventId: String): Boolean { - return ChunkEntity.findIncludingEvent(this, eventId) != null +private fun Realm.doesEventExistInChunkHistory(roomId: String, eventId: String): Boolean { + return ChunkEntity.findIncludingEvent(this, roomId, eventId) != null } private fun Realm.hasReadReceiptInLatestChunk(latestChunkEntity: ChunkEntity, roomId: String, userId: String, threadId: String?): Boolean { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt index ad28286a84..d8cdd162f1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt @@ -41,7 +41,7 @@ internal class WorkManagerProvider @Inject constructor( private val coroutineDispatchers: MatrixCoroutineDispatchers, private val sessionScope: CoroutineScope ) { - private val tag = MATRIX_SDK_TAG_PREFIX + sessionId + val tag = MATRIX_SDK_TAG_PREFIX + sessionId val workManager = WorkManager.getInstance(context) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/events/DefaultEventService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/events/DefaultEventService.kt index 51d305f441..4ba5c3b946 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/events/DefaultEventService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/events/DefaultEventService.kt @@ -18,13 +18,18 @@ package org.matrix.android.sdk.internal.session.events import org.matrix.android.sdk.api.session.events.EventService import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.database.RealmSessionProvider +import org.matrix.android.sdk.internal.database.mapper.asDomain +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.session.call.CallEventProcessor import org.matrix.android.sdk.internal.session.room.timeline.GetEventTask import javax.inject.Inject internal class DefaultEventService @Inject constructor( private val getEventTask: GetEventTask, - private val callEventProcessor: CallEventProcessor + private val callEventProcessor: CallEventProcessor, + private val realmSessionProvider: RealmSessionProvider, ) : EventService { override suspend fun getEvent(roomId: String, eventId: String): Event { @@ -36,4 +41,16 @@ internal class DefaultEventService @Inject constructor( return event } + + override fun getEventFromCache(roomId: String, eventId: String): Event? { + return realmSessionProvider.withRealm { realm -> + EventEntity.where( + realm = realm, + roomId = roomId, + eventId = eventId + ) + .findFirst() + ?.asDomain() + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt index 5a6107821d..ec12695ecd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt @@ -167,6 +167,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( Timber.v("Extracted integration config : $config") realm.insertOrUpdate(config) } + homeServerCapabilitiesEntity.externalAccountManagementUrl = getWellknownResult.wellKnown.unstableDelegatedAuthConfig?.accountManagementUrl } homeServerCapabilitiesEntity.lastUpdatedTimestamp = Date().time } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushrules/ProcessEventForPushTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushrules/ProcessEventForPushTask.kt index 9fe93d8262..d000d709a9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushrules/ProcessEventForPushTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushrules/ProcessEventForPushTask.kt @@ -21,6 +21,7 @@ import org.matrix.android.sdk.api.session.events.model.isInvitation import org.matrix.android.sdk.api.session.pushrules.PushEvents import org.matrix.android.sdk.api.session.pushrules.rest.PushRule import org.matrix.android.sdk.api.session.sync.model.RoomsSyncResponse +import org.matrix.android.sdk.internal.crypto.EventDecryptor import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.task.Task import timber.log.Timber @@ -36,27 +37,29 @@ internal interface ProcessEventForPushTask : Task value.timeline?.events?.mapNotNull { - it.takeIf { !it.isInvitation() }?.copy(roomId = key) + it.takeIf { !it.isInvitation() }?.copyAll(roomId = key) } } .flatten() val inviteEvents = params.syncResponse.invite .mapNotNull { (key, value) -> - value.inviteState?.events?.map { it.copy(roomId = key) } + value.inviteState?.events?.map { it.copyAll(roomId = key) } } .flatten() val allEvents = (newJoinEvents + inviteEvents).filter { event -> when (event.type) { in EventType.POLL_START.values, + in EventType.POLL_END.values, in EventType.STATE_ROOM_BEACON_INFO.values, EventType.MESSAGE, EventType.REDACTION, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt index 2ff43d6812..ca224cd543 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt @@ -84,7 +84,6 @@ internal class DefaultPollAggregationProcessor @Inject constructor( val roomId = event.roomId ?: return false val senderId = event.senderId ?: return false val targetEventId = event.getRelationContent()?.eventId ?: return false - val targetPollContent = getPollContent(session, roomId, targetEventId) ?: return false val annotationsSummaryEntity = getAnnotationsSummaryEntity(realm, roomId, targetEventId) val aggregatedPollSummaryEntity = getAggregatedPollSummaryEntity(realm, annotationsSummaryEntity) @@ -108,7 +107,8 @@ internal class DefaultPollAggregationProcessor @Inject constructor( } val vote = content.getBestResponse()?.answers?.first() ?: return false - if (!targetPollContent.getBestPollCreationInfo()?.answers?.map { it.id }?.contains(vote).orFalse()) { + val targetPollContent = getPollContent(session, roomId, targetEventId) + if (targetPollContent != null && !targetPollContent.getBestPollCreationInfo()?.answers?.map { it.id }?.contains(vote).orFalse()) { return false } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt index c1dca5c7ae..5bef61cae1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt @@ -190,10 +190,8 @@ internal class CreateRoomBodyBuilder @Inject constructor( private suspend fun canEnableEncryption(params: CreateRoomParams): Boolean { return params.enableEncryptionIfInvitedUsersSupportIt && // Parity with web, enable if users have encryption ready devices - // for now remove checks on cross signing and 3pid invites + // for now remove checks on cross signing // && crossSigningService.isCrossSigningVerified() - params.invite3pids.isEmpty() && - params.invitedUserIds.isNotEmpty() && params.invitedUserIds.let { userIds -> val keys = cryptoService.downloadKeysIfNeeded(userIds, forceDownload = false) userIds.all { userId -> diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt index 4645bb05ab..7497ecf21b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt @@ -115,7 +115,12 @@ internal class RoomDisplayNameResolver @Inject constructor( val leftMembersNames = roomMembers.queryLeftRoomMembersEvent() .findAll() .map { displayNameResolver.getBestName(it.toMatrixItem()) } - roomDisplayNameFallbackProvider.getNameForEmptyRoom(roomSummary?.isDirect.orFalse(), leftMembersNames) + val directUserId = roomSummary?.directUserId + if (!directUserId.isNullOrBlank() && leftMembersNames.isEmpty()) { + directUserId + } else { + roomDisplayNameFallbackProvider.getNameForEmptyRoom(roomSummary?.isDirect.orFalse(), leftMembersNames) + } } 1 -> { roomDisplayNameFallbackProvider.getNameFor1member( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index b5114ec1dd..c2bdec3596 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -242,7 +242,8 @@ internal class LocalEchoEventFactory @Inject constructor( relatesTo = RelationDefaultContent( type = RelationType.REFERENCE, eventId = eventId - ) + ), + unstableText = "Ended poll", ) val localId = LocalEcho.createLocalEchoId() return Event( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/FetchTokenAndPaginateTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/FetchTokenAndPaginateTask.kt index 9d8d8ecbf1..b73dd8160b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/FetchTokenAndPaginateTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/FetchTokenAndPaginateTask.kt @@ -59,7 +59,7 @@ internal class DefaultFetchTokenAndPaginateTask @Inject constructor( ?: throw IllegalStateException("No token found") monarchy.awaitTransaction { realm -> - val chunkToUpdate = ChunkEntity.findIncludingEvent(realm, params.lastKnownEventId) + val chunkToUpdate = ChunkEntity.findIncludingEvent(realm, params.roomId, params.lastKnownEventId) if (params.direction == PaginationDirection.FORWARDS) { chunkToUpdate?.nextToken = fromToken } else { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt index 6654eeadfc..2143ac1d21 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt @@ -278,7 +278,7 @@ internal class LoadTimelineStrategy constructor( .findAll() } is Mode.Permalink -> { - ChunkEntity.findAllIncludingEvents(realm, listOf(mode.originEventId)) + ChunkEntity.findAllIncludingEvents(realm, roomId, listOf(mode.originEventId)) } is Mode.Thread -> { recreateThreadChunkEntity(realm, mode.rootThreadEventId) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/session/pushrules/PushRulesConditionTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/session/pushrules/PushRulesConditionTest.kt index 3ddf940241..50d084755b 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/session/pushrules/PushRulesConditionTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/session/pushrules/PushRulesConditionTest.kt @@ -23,7 +23,11 @@ import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test import org.matrix.android.sdk.MatrixTest +import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.members.MembershipService @@ -38,15 +42,40 @@ class PushRulesConditionTest : MatrixTest { * Test EventMatchCondition * ========================================================================================== */ + private fun createFakeEncryptedEvent() = Event( + type = EventType.ENCRYPTED, + eventId = "mx0", + roomId = "!fakeRoom", + content = EncryptedEventContent( + algorithm = MXCRYPTO_ALGORITHM_MEGOLM, + ciphertext = "AwgBEpACQEKOkd4Gp0+gSXG4M+btcrnPgsF23xs/lUmS2I4YjmqF...", + sessionId = "TO2G4u2HlnhtbIJk", + senderKey = "5e3EIqg3JfooZnLQ2qHIcBarbassQ4qXblai0", + deviceId = "FAKEE" + ).toContent() + ) + private fun createSimpleTextEvent(text: String): Event { return Event( - type = "m.room.message", + type = EventType.MESSAGE, eventId = "mx0", content = MessageTextContent("m.text", text).toContent(), - originServerTs = 0 + originServerTs = 0, ) } + private fun createSimpleTextEventEncrypted(text: String): Event { + return createFakeEncryptedEvent().apply { + mxDecryptionResult = OlmDecryptionResult( + payload = mapOf( + "type" to EventType.MESSAGE, + "content" to MessageTextContent("m.text", text).toContent(), + ), + senderKey = "the_real_sender_key", + ) + } + } + @Test fun test_eventmatch_type_condition() { val condition = EventMatchCondition("type", "m.room.message") @@ -70,6 +99,26 @@ class PushRulesConditionTest : MatrixTest { assertFalse(condition.isSatisfied(simpleRoomMemberEvent)) } + @Test + fun test_decrypted_eventmatch_type_condition() { + val condition = EventMatchCondition("type", "m.room.message") + + val simpleDecryptedTextEvent = createSimpleTextEventEncrypted("Yo wtf?") + + val encryptedDummyEvent = createFakeEncryptedEvent().apply { + mxDecryptionResult = OlmDecryptionResult( + payload = mapOf( + "type" to EventType.DUMMY, + ) + ) + } + val encryptedEvent = createFakeEncryptedEvent() + + assert(condition.isSatisfied(simpleDecryptedTextEvent)) + assertFalse(condition.isSatisfied(encryptedDummyEvent)) + assertFalse(condition.isSatisfied(encryptedEvent)) + } + @Test fun test_eventmatch_path_condition() { val condition = EventMatchCondition("content.msgtype", "m.text") @@ -125,6 +174,22 @@ class PushRulesConditionTest : MatrixTest { assert(condition.isSatisfied(createSimpleTextEvent("BEN"))) } + @Test + fun test_encrypted_eventmatch_words_only_condition() { + val condition = EventMatchCondition("content.body", "ben") + + assertFalse(condition.isSatisfied(createSimpleTextEventEncrypted("benoit"))) + assertFalse(condition.isSatisfied(createSimpleTextEventEncrypted("Hello benoit"))) + assertFalse(condition.isSatisfied(createSimpleTextEventEncrypted("superben"))) + + assert(condition.isSatisfied(createSimpleTextEventEncrypted("ben"))) + assert(condition.isSatisfied(createSimpleTextEventEncrypted("hello ben"))) + assert(condition.isSatisfied(createSimpleTextEventEncrypted("ben is there"))) + assert(condition.isSatisfied(createSimpleTextEventEncrypted("hello ben!"))) + assert(condition.isSatisfied(createSimpleTextEventEncrypted("hello Ben!"))) + assert(condition.isSatisfied(createSimpleTextEventEncrypted("BEN"))) + } + @Test fun test_eventmatch_at_room_condition() { val condition = EventMatchCondition("content.body", "@room") @@ -140,6 +205,21 @@ class PushRulesConditionTest : MatrixTest { assert(condition.isSatisfied(createSimpleTextEvent("Don't ping @room!"))) } + @Test + fun test_encrypted_eventmatch_at_room_condition() { + val condition = EventMatchCondition("content.body", "@room") + + assertFalse(condition.isSatisfied(createSimpleTextEventEncrypted("@roomba"))) + assertFalse(condition.isSatisfied(createSimpleTextEventEncrypted("room benoit"))) + assertFalse(condition.isSatisfied(createSimpleTextEventEncrypted("abc@roomba"))) + + assert(condition.isSatisfied(createSimpleTextEventEncrypted("@room"))) + assert(condition.isSatisfied(createSimpleTextEventEncrypted("@room, ben"))) + assert(condition.isSatisfied(createSimpleTextEventEncrypted("@ROOM"))) + assert(condition.isSatisfied(createSimpleTextEventEncrypted("Use:@room"))) + assert(condition.isSatisfied(createSimpleTextEventEncrypted("Don't ping @room!"))) + } + @Test fun test_notice_condition() { val conditionEqual = EventMatchCondition("content.msgtype", "m.notice") @@ -155,6 +235,17 @@ class PushRulesConditionTest : MatrixTest { } } + @Test + fun test_eventmatch_encrypted_type_condition() { + val condition = EventMatchCondition("type", "m.room.encrypted") + + val simpleDecryptedTextEvent = createSimpleTextEventEncrypted("Yo wtf?") + val encryptedEvent = createFakeEncryptedEvent() + + assertFalse(condition.isSatisfied(simpleDecryptedTextEvent)) + assert(condition.isSatisfied(encryptedEvent)) + } + /* ========================================================================================== * Test RoomMemberCountCondition * ========================================================================================== */ diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt index 766e51a8e5..248c4b322d 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt @@ -147,6 +147,19 @@ class DefaultPollAggregationProcessorTest { pollAggregationProcessor.handlePollResponseEvent(session, realm.instance, AN_INVALID_POLL_RESPONSE_EVENT).shouldBeFalse() } + @Test + fun `given a poll response event and no existing poll start event, when processing, then is processed and returns true`() { + // Given + mockRoom(roomId = A_ROOM_ID, eventId = AN_EVENT_ID, hasExistingTimelineEvent = false) + every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity() + + // When + val result = pollAggregationProcessor.handlePollResponseEvent(session, realm.instance, A_POLL_RESPONSE_EVENT) + + // Then + result.shouldBeTrue() + } + @Test fun `given a poll end event, when processing, then is processed and return true`() = runTest { // Given @@ -234,11 +247,12 @@ class DefaultPollAggregationProcessorTest { private fun mockRoom( roomId: String, - eventId: String + eventId: String, + hasExistingTimelineEvent: Boolean = true, ) { val room = mockk() every { session.getRoom(roomId) } returns room - every { room.getTimelineEvent(eventId) } returns A_TIMELINE_EVENT + every { room.getTimelineEvent(eventId) } returns if (hasExistingTimelineEvent) A_TIMELINE_EVENT else null } private fun mockRedactionPowerLevels(userId: String, isAbleToRedact: Boolean): PowerLevelsHelper { diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManagerProvider.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManagerProvider.kt index 51ff24c01d..437fda65e4 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManagerProvider.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManagerProvider.kt @@ -26,5 +26,6 @@ internal class FakeWorkManagerProvider( val instance = mockk().also { every { it.workManager } returns fakeWorkManager.instance + every { it.tag } returns "Tag" } } diff --git a/vector-app/build.gradle b/vector-app/build.gradle index 5578986463..6108dc958d 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -37,7 +37,7 @@ ext.versionMinor = 5 // Note: even values are reserved for regular release, odd values for hotfix release. // When creating a hotfix, you should decrease the value, since the current value // is the value for the next regular release. -ext.versionPatch = 24 +ext.versionPatch = 30 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' @@ -449,7 +449,7 @@ dependencies { androidTestImplementation libs.mockk.mockkAndroid androidTestUtil libs.androidx.orchestrator androidTestImplementation libs.androidx.fragmentTesting - androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.8.0" + androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.8.10" debugImplementation libs.androidx.fragmentTestingManifest debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10' } diff --git a/vector-app/src/androidTest/java/im/vector/app/EspressoExt.kt b/vector-app/src/androidTest/java/im/vector/app/EspressoExt.kt index c9155378f5..96337901a1 100644 --- a/vector-app/src/androidTest/java/im/vector/app/EspressoExt.kt +++ b/vector-app/src/androidTest/java/im/vector/app/EspressoExt.kt @@ -151,7 +151,7 @@ fun initialSyncIdlingResource(session: Session): IdlingResource { this.callback = callback } - override fun onChanged(t: SyncState?) { + override fun onChanged(value: SyncState) { val isIdle = session.syncService().hasAlreadySynced() if (isIdle) { callback?.onTransitionToIdle() @@ -242,10 +242,10 @@ fun allSecretsKnownIdling(session: Session): IdlingResource { this.callback = callback } - override fun onChanged(t: Optional?) { - println("*** [$name] allSecretsKnownIdling ${t?.getOrNull()}") - privateKeysInfo = t?.getOrNull() - if (t?.getOrNull()?.allKnown() == true) { + override fun onChanged(value: Optional) { + println("*** [$name] allSecretsKnownIdling ${value.getOrNull()}") + privateKeysInfo = value.getOrNull() + if (value.getOrNull()?.allKnown() == true) { session.cryptoService().crossSigningService().getLiveCrossSigningPrivateKeys().removeObserver(this) callback?.onTransitionToIdle() } diff --git a/vector-app/src/androidTest/java/im/vector/app/VerificationTestBase.kt b/vector-app/src/androidTest/java/im/vector/app/VerificationTestBase.kt index a5f0a7ab53..48549a93dd 100644 --- a/vector-app/src/androidTest/java/im/vector/app/VerificationTestBase.kt +++ b/vector-app/src/androidTest/java/im/vector/app/VerificationTestBase.kt @@ -141,7 +141,7 @@ abstract class VerificationTestBase { session.syncService().getSyncStateLive() } val syncObserver = object : Observer { - override fun onChanged(t: SyncState?) { + override fun onChanged(value: SyncState) { if (session.syncService().hasAlreadySynced()) { lock.countDown() syncLiveData.removeObserver(this) diff --git a/vector/build.gradle b/vector/build.gradle index d5223a1f6a..578dda489a 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -202,7 +202,7 @@ dependencies { implementation 'com.github.hyuwah:DraggableView:1.0.0' // Custom Tab - implementation 'androidx.browser:browser:1.4.0' + implementation 'androidx.browser:browser:1.5.0' // Passphrase strength helper implementation 'com.nulab-inc:zxcvbn:1.7.0' @@ -236,7 +236,7 @@ dependencies { kapt libs.dagger.hiltCompiler // Analytics - implementation('com.posthog.android:posthog:2.0.1') { + implementation('com.posthog.android:posthog:2.0.3') { exclude group: 'com.android.support', module: 'support-annotations' } implementation libs.sentry.sentryAndroid @@ -311,7 +311,7 @@ dependencies { // Fix issue with Jitsi. Inspired from https://github.com/android/android-test/issues/861#issuecomment-872067868 // Error was lots of `Duplicate class org.checkerframework.common.reflection.qual.MethodVal found in modules jetified-checker-3.1 (org.checkerframework:checker:3.1.1) and jetified-checker-qual-3.12.0 (org.checkerframework:checker-qual:3.12.0) //noinspection GradleDependency Cannot use latest 3.15.0 since it required min API 26. - implementation "org.checkerframework:checker:3.30.0" + implementation "org.checkerframework:checker:3.32.0" androidTestImplementation libs.androidx.testCore androidTestImplementation libs.androidx.testRunner @@ -335,5 +335,5 @@ dependencies { androidTestUtil libs.androidx.orchestrator debugImplementation libs.androidx.fragmentTestingManifest androidTestImplementation libs.androidx.fragmentTesting - androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.8.0" + androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.8.10" } diff --git a/vector/src/androidTest/java/im/vector/app/core/utils/TestSpan.kt b/vector/src/androidTest/java/im/vector/app/core/utils/TestSpan.kt index 1d0d6548e1..ebbe565642 100644 --- a/vector/src/androidTest/java/im/vector/app/core/utils/TestSpan.kt +++ b/vector/src/androidTest/java/im/vector/app/core/utils/TestSpan.kt @@ -20,6 +20,7 @@ import android.graphics.Canvas import android.graphics.Paint import android.text.Layout import android.text.Spanned +import android.text.style.StrikethroughSpan import androidx.core.text.getSpans import im.vector.app.features.html.HtmlCodeSpan import io.element.android.wysiwyg.spans.InlineCodeSpan @@ -28,6 +29,7 @@ import io.mockk.mockk import io.mockk.slot import io.mockk.verify import io.noties.markwon.core.spans.EmphasisSpan +import io.noties.markwon.core.spans.LinkSpan import io.noties.markwon.core.spans.OrderedListItemSpan import io.noties.markwon.core.spans.StrongEmphasisSpan import me.gujun.android.span.style.CustomTypefaceSpan @@ -59,6 +61,8 @@ private fun Any.readTags(): SpanTags { StrongEmphasisSpan::class -> "bold" EmphasisSpan::class, CustomTypefaceSpan::class -> "italic" InlineCodeSpan::class -> "inline code" + StrikethroughSpan::class -> "strikethrough" + LinkSpan::class -> "link" else -> if (this::class.qualifiedName!!.startsWith("android.widget")) { null } else { diff --git a/vector/src/androidTest/java/im/vector/app/features/html/EventHtmlRendererTest.kt b/vector/src/androidTest/java/im/vector/app/features/html/EventHtmlRendererTest.kt index 7f3293e7d1..94d60538d9 100644 --- a/vector/src/androidTest/java/im/vector/app/features/html/EventHtmlRendererTest.kt +++ b/vector/src/androidTest/java/im/vector/app/features/html/EventHtmlRendererTest.kt @@ -21,6 +21,7 @@ import androidx.core.text.toSpanned import androidx.test.platform.app.InstrumentationRegistry import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.toTestSpan import im.vector.app.features.settings.VectorPreferences import io.mockk.every @@ -40,9 +41,10 @@ class EventHtmlRendererTest { every { it.isRichTextEditorEnabled() } returns false } private val fakeSessionHolder = mockk() + private val fakeDimensionConverter = mockk() private val renderer = EventHtmlRenderer( - MatrixHtmlPluginConfigure(ColorProvider(context), context.resources, fakeVectorPreferences), + MatrixHtmlPluginConfigure(ColorProvider(context), context.resources, fakeVectorPreferences, fakeDimensionConverter), context, fakeVectorPreferences, fakeSessionHolder, @@ -71,6 +73,30 @@ class EventHtmlRendererTest { result shouldBeEqualTo "__italic__ **bold**" } + // https://github.com/noties/Markwon/issues/423 + @Test + fun doesNotIntroduceExtraNewLines() { + // Given initial render (required to trigger bug) + """Some italic""".renderAsTestSpan() + val results = arrayOf( + """Some italic""".renderAsTestSpan(), + """Some bold""".renderAsTestSpan(), + """Some code""".renderAsTestSpan(), + """Some link""".renderAsTestSpan(), + """Some strikethrough""".renderAsTestSpan(), + """Some span""".renderAsTestSpan(), + ) + + results shouldBeEqualTo arrayOf( + "Some [italic]italic[/italic]", + "Some [bold]bold[/bold]", + "Some [inline code]code[/inline code]", + "Some [link]link[/link]", + "Some \n[strikethrough]strikethrough[/strikethrough]", // FIXME + "Some \nspan", // FIXME + ) + } + @Test fun processesHtmlWithinCodeBlocks() { val result = """italic bold""".renderAsTestSpan() diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 9c8186b2d4..922d4ca292 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -327,6 +327,7 @@ + diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt index 9470f7d577..8751907f2a 100644 --- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt @@ -39,6 +39,7 @@ import im.vector.app.features.discovery.DiscoverySettingsViewModel import im.vector.app.features.discovery.change.SetIdentityServerViewModel import im.vector.app.features.home.HomeActivityViewModel import im.vector.app.features.home.HomeDetailViewModel +import im.vector.app.features.home.NewHomeDetailViewModel import im.vector.app.features.home.UnknownDeviceDetectorSharedViewModel import im.vector.app.features.home.UnreadMessagesSharedViewModel import im.vector.app.features.home.UserColorAccountDataViewModel @@ -55,7 +56,6 @@ import im.vector.app.features.home.room.list.RoomListViewModel import im.vector.app.features.home.room.list.home.HomeRoomListViewModel import im.vector.app.features.home.room.list.home.invites.InvitesViewModel import im.vector.app.features.home.room.list.home.release.ReleaseNotesViewModel -import im.vector.app.features.homeserver.HomeServerCapabilitiesViewModel import im.vector.app.features.invite.InviteUsersToRoomViewModel import im.vector.app.features.location.LocationSharingViewModel import im.vector.app.features.location.live.map.LiveLocationMapViewModel @@ -84,6 +84,7 @@ import im.vector.app.features.roomprofile.members.RoomMemberListViewModel import im.vector.app.features.roomprofile.notifications.RoomNotificationSettingsViewModel import im.vector.app.features.roomprofile.permissions.RoomPermissionsViewModel import im.vector.app.features.roomprofile.polls.RoomPollsViewModel +import im.vector.app.features.roomprofile.polls.detail.ui.RoomPollDetailViewModel import im.vector.app.features.roomprofile.settings.RoomSettingsViewModel import im.vector.app.features.roomprofile.settings.joinrule.advanced.RoomJoinRuleChooseRestrictedViewModel import im.vector.app.features.roomprofile.uploads.RoomUploadsViewModel @@ -106,7 +107,8 @@ import im.vector.app.features.settings.ignored.IgnoredUsersViewModel import im.vector.app.features.settings.labs.VectorSettingsLabsViewModel import im.vector.app.features.settings.legals.LegalsViewModel import im.vector.app.features.settings.locale.LocalePickerViewModel -import im.vector.app.features.settings.notifications.VectorSettingsNotificationPreferenceViewModel +import im.vector.app.features.settings.notifications.VectorSettingsNotificationViewModel +import im.vector.app.features.settings.notifications.VectorSettingsPushRuleNotificationViewModel import im.vector.app.features.settings.push.PushGatewaysViewModel import im.vector.app.features.settings.threepids.ThreePidsSettingsViewModel import im.vector.app.features.share.IncomingShareViewModel @@ -496,11 +498,6 @@ interface MavericksViewModelModule { @MavericksViewModelKey(StartAppViewModel::class) fun startAppViewModelFactory(factory: StartAppViewModel.Factory): MavericksAssistedViewModelFactory<*, *> - @Binds - @IntoMap - @MavericksViewModelKey(HomeServerCapabilitiesViewModel::class) - fun homeServerCapabilitiesViewModelFactory(factory: HomeServerCapabilitiesViewModel.Factory): MavericksAssistedViewModelFactory<*, *> - @Binds @IntoMap @MavericksViewModelKey(InviteUsersToRoomViewModel::class) @@ -693,9 +690,16 @@ interface MavericksViewModelModule { @Binds @IntoMap - @MavericksViewModelKey(VectorSettingsNotificationPreferenceViewModel::class) + @MavericksViewModelKey(VectorSettingsNotificationViewModel::class) fun vectorSettingsNotificationPreferenceViewModelFactory( - factory: VectorSettingsNotificationPreferenceViewModel.Factory + factory: VectorSettingsNotificationViewModel.Factory + ): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(VectorSettingsPushRuleNotificationViewModel::class) + fun vectorSettingsPushRuleNotificationPreferenceViewModelFactory( + factory: VectorSettingsPushRuleNotificationViewModel.Factory ): MavericksAssistedViewModelFactory<*, *> @Binds @@ -707,4 +711,14 @@ interface MavericksViewModelModule { @IntoMap @MavericksViewModelKey(RoomPollsViewModel::class) fun roomPollsViewModelFactory(factory: RoomPollsViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(RoomPollDetailViewModel::class) + fun roomPollDetailViewModelFactory(factory: RoomPollDetailViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(NewHomeDetailViewModel::class) + fun newHomeDetailViewModelFactory(factory: NewHomeDetailViewModel.Factory): MavericksAssistedViewModelFactory<*, *> } diff --git a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt index 0966227917..84f866d1f3 100644 --- a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt +++ b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt @@ -160,7 +160,9 @@ class DefaultErrorFormatter @Inject constructor( RecordingError.BlockedBySomeoneElse -> stringProvider.getString(R.string.error_voice_broadcast_blocked_by_someone_else_message) RecordingError.NoPermission -> stringProvider.getString(R.string.error_voice_broadcast_permission_denied_message) RecordingError.UserAlreadyBroadcasting -> stringProvider.getString(R.string.error_voice_broadcast_already_in_progress_message) - is VoiceBroadcastFailure.ListeningError -> stringProvider.getString(R.string.error_voice_broadcast_unable_to_play) + is VoiceBroadcastFailure.ListeningError.UnableToPlay, + is VoiceBroadcastFailure.ListeningError.PrepareMediaPlayerError -> stringProvider.getString(R.string.error_voice_broadcast_unable_to_play) + is VoiceBroadcastFailure.ListeningError.UnableToDecrypt -> stringProvider.getString(R.string.error_voice_broadcast_unable_to_decrypt) } } diff --git a/vector/src/main/java/im/vector/app/core/event/GetTimelineEventUseCase.kt b/vector/src/main/java/im/vector/app/core/event/GetTimelineEventUseCase.kt new file mode 100644 index 0000000000..4265aac53e --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/event/GetTimelineEventUseCase.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.event + +import androidx.lifecycle.asFlow +import im.vector.app.core.di.ActiveSessionHolder +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import org.matrix.android.sdk.api.session.getRoom +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.flow.unwrap +import javax.inject.Inject + +class GetTimelineEventUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, +) { + + fun execute(roomId: String, eventId: String): Flow { + return activeSessionHolder.getActiveSession().getRoom(roomId) + ?.timelineService() + ?.getTimelineEventLive(eventId) + ?.asFlow() + ?.unwrap() + ?: emptyFlow() + } +} diff --git a/vector/src/main/java/im/vector/app/core/extensions/TextInputLayout.kt b/vector/src/main/java/im/vector/app/core/extensions/TextInputLayout.kt index 909c343a45..bb6f6a2cbb 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/TextInputLayout.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/TextInputLayout.kt @@ -21,13 +21,16 @@ import android.text.Editable import android.view.View import android.view.inputmethod.EditorInfo import androidx.autofill.HintConstants +import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.google.android.material.textfield.TextInputLayout import im.vector.app.core.platform.SimpleTextWatcher import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import reactivecircus.flowbinding.android.widget.textChanges fun TextInputLayout.editText() = this.editText!! @@ -85,7 +88,7 @@ fun TextInputLayout.setOnImeDoneListener(action: () -> Unit) { fun TextInputLayout.setOnFocusLostListener(lifecycleOwner: LifecycleOwner, action: () -> Unit) { editText().setOnFocusChangeListener { _, hasFocus -> when (hasFocus) { - false -> lifecycleOwner.lifecycleScope.launchWhenResumed { action() } + false -> lifecycleOwner.lifecycleScope.launch { lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) { action() } } else -> { // do nothing } diff --git a/vector/src/main/java/im/vector/app/core/notification/PushRulesUpdater.kt b/vector/src/main/java/im/vector/app/core/notification/PushRulesUpdater.kt new file mode 100644 index 0000000000..4925941f92 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/notification/PushRulesUpdater.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.notification + +import im.vector.app.features.session.coroutineScope +import im.vector.app.features.settings.notifications.usecase.UpdatePushRulesIfNeededUseCase +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes +import org.matrix.android.sdk.flow.flow +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Listen changes in Account Data to update the push rules if needed. + */ +@Singleton +class PushRulesUpdater @Inject constructor( + private val updatePushRulesIfNeededUseCase: UpdatePushRulesIfNeededUseCase, +) { + + private var job: Job? = null + + fun onSessionStarted(session: Session) { + updatePushRulesOnChange(session) + } + + private fun updatePushRulesOnChange(session: Session) { + job?.cancel() + job = session.coroutineScope.launch { + session.flow() + .liveUserAccountData(UserAccountDataTypes.TYPE_PUSH_RULES) + .onEach { updatePushRulesIfNeededUseCase.execute(session) } + .collect() + } + } +} diff --git a/vector/src/main/java/im/vector/app/core/preference/VectorCheckboxPreference.kt b/vector/src/main/java/im/vector/app/core/preference/VectorCheckboxPreference.kt index 13b65e11b4..355bab8da8 100644 --- a/vector/src/main/java/im/vector/app/core/preference/VectorCheckboxPreference.kt +++ b/vector/src/main/java/im/vector/app/core/preference/VectorCheckboxPreference.kt @@ -32,6 +32,8 @@ class VectorCheckboxPreference : CheckBoxPreference { constructor(context: Context) : super(context) + var summaryTextColor: Int? = null + init { // Set to false to remove the space when there is no icon isIconSpaceReserved = true @@ -42,4 +44,9 @@ class VectorCheckboxPreference : CheckBoxPreference { (holder.findViewById(android.R.id.title) as? TextView)?.isSingleLine = false super.onBindViewHolder(holder) } + + override fun syncSummaryView(holder: PreferenceViewHolder) { + super.syncSummaryView(holder) + summaryTextColor?.let { (holder.findViewById(android.R.id.summary) as? TextView)?.setTextColor(it) } + } } diff --git a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt index c6a2635e6c..b9573e9292 100644 --- a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt @@ -20,6 +20,7 @@ import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext import im.vector.app.core.extensions.startSyncing import im.vector.app.core.notification.NotificationsSettingUpdater +import im.vector.app.core.notification.PushRulesUpdater import im.vector.app.core.session.clientinfo.UpdateMatrixClientInfoUseCase import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.session.coroutineScope @@ -37,6 +38,7 @@ class ConfigureAndStartSessionUseCase @Inject constructor( private val vectorPreferences: VectorPreferences, private val notificationsSettingUpdater: NotificationsSettingUpdater, private val updateNotificationSettingsAccountDataUseCase: UpdateNotificationSettingsAccountDataUseCase, + private val pushRulesUpdater: PushRulesUpdater, ) { fun execute(session: Session, startSyncing: Boolean = true) { @@ -50,6 +52,7 @@ class ConfigureAndStartSessionUseCase @Inject constructor( updateMatrixClientInfoIfNeeded(session) createNotificationSettingsAccountDataIfNeeded(session) notificationsSettingUpdater.onSessionStarted(session) + pushRulesUpdater.onSessionStarted(session) } private fun updateMatrixClientInfoIfNeeded(session: Session) { diff --git a/vector/src/main/java/im/vector/app/core/utils/FileUtils.kt b/vector/src/main/java/im/vector/app/core/utils/FileUtils.kt index c735b8b33d..63f141ef52 100644 --- a/vector/src/main/java/im/vector/app/core/utils/FileUtils.kt +++ b/vector/src/main/java/im/vector/app/core/utils/FileUtils.kt @@ -17,6 +17,7 @@ package im.vector.app.core.utils import android.content.Context +import androidx.annotation.WorkerThread import timber.log.Timber import java.io.File import java.util.Locale @@ -125,6 +126,7 @@ fun getFileExtension(fileUri: String): String? { * Size * ========================================================================================== */ +@WorkerThread fun getSizeOfFiles(root: File): Long { return root.walkTopDown() .onEnter { diff --git a/vector/src/main/java/im/vector/app/core/utils/LiveEvent.kt b/vector/src/main/java/im/vector/app/core/utils/LiveEvent.kt index 061790cb7a..e462715edb 100644 --- a/vector/src/main/java/im/vector/app/core/utils/LiveEvent.kt +++ b/vector/src/main/java/im/vector/app/core/utils/LiveEvent.kt @@ -48,9 +48,7 @@ open class LiveEvent(private val content: T) { * [onEventUnhandledContent] is *only* called if the [LiveEvent]'s contents has not been handled. */ class EventObserver(private val onEventUnhandledContent: (T) -> Unit) : Observer> { - override fun onChanged(event: LiveEvent?) { - event?.getContentIfNotHandled()?.let { value -> - onEventUnhandledContent(value) - } + override fun onChanged(value: LiveEvent) { + value.getContentIfNotHandled()?.let { onEventUnhandledContent(it) } } } diff --git a/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt b/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt index b38a4d934b..90a4a3213c 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt @@ -137,7 +137,7 @@ class DecryptionFailureTracker @Inject constructor( // for now we ignore events already reported even if displayed again? .filter { alreadyReported.contains(it).not() } .forEach { failedEventId -> - analyticsTracker.capture(Error(aggregation.key.first, Error.Domain.E2EE, aggregation.key.second)) + analyticsTracker.capture(Error(context = aggregation.key.first, domain = Error.Domain.E2EE, name = aggregation.key.second)) alreadyReported.add(failedEventId) } } diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/Error.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/Error.kt index 5c082f530e..386c090848 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/Error.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/Error.kt @@ -29,6 +29,10 @@ data class Error( * Context - client defined, can be used for debugging. */ val context: String? = null, + /** + * Which crypto module is the client currently using. + */ + val cryptoModule: CryptoModule? = null, val domain: Domain, val name: Name, ) : VectorAnalyticsEvent { @@ -52,11 +56,25 @@ data class Error( VoipUserMediaFailed, } + enum class CryptoModule { + + /** + * Native / legacy crypto module specific to each platform. + */ + Native, + + /** + * Shared / cross-platform crypto module written in Rust. + */ + Rust, + } + override fun getName() = "Error" override fun getProperties(): Map? { return mutableMapOf().apply { context?.let { put("context", it) } + cryptoModule?.let { put("cryptoModule", it.name) } put("domain", domain.name) put("name", name.name) }.takeIf { it.isNotEmpty() } diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt index acaf24dca7..fbddf815c6 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt @@ -93,6 +93,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() { title = getString(R.string.fab_menu_create_chat), menuResId = R.menu.vector_create_direct_room, submitMenuItemId = R.id.action_create_direct_room, + single3pidSelection = true, ) ) } diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt index 3f67708a28..dce1e63766 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt @@ -124,7 +124,7 @@ class CreateDirectRoomViewModel @AssistedInject constructor( } val result = runCatchingToAsync { - if (vectorPreferences.isDeferredDmEnabled()) { + if (vectorPreferences.isDeferredDmEnabled() && roomParams.invite3pids.isEmpty()) { session.roomService().createLocalRoom(roomParams) } else { analyticsTracker.capture(CreatedRoom(isDM = roomParams.isDirect.orFalse())) diff --git a/vector/src/main/java/im/vector/app/features/home/GetSpacesNotificationBadgeStateUseCase.kt b/vector/src/main/java/im/vector/app/features/home/GetSpacesNotificationBadgeStateUseCase.kt new file mode 100644 index 0000000000..62d1501dab --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/GetSpacesNotificationBadgeStateUseCase.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home + +import im.vector.app.features.home.room.list.UnreadCounterBadgeView +import im.vector.app.features.spaces.GetSpacesUseCase +import im.vector.app.features.spaces.notification.GetNotificationCountForSpacesUseCase +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.query.SpaceFilter +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.spaceSummaryQueryParams +import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount +import javax.inject.Inject + +class GetSpacesNotificationBadgeStateUseCase @Inject constructor( + private val getNotificationCountForSpacesUseCase: GetNotificationCountForSpacesUseCase, + private val getSpacesUseCase: GetSpacesUseCase, +) { + + fun execute(): Flow { + val params = spaceSummaryQueryParams { + memberships = listOf(Membership.INVITE) + displayName = QueryStringValue.IsNotEmpty + } + return combine( + getNotificationCountForSpacesUseCase.execute(SpaceFilter.NoFilter), + getSpacesUseCase.execute(params), + ) { spacesNotificationCount, spaceInvites -> + computeSpacesNotificationCounterBadgeState(spacesNotificationCount, spaceInvites) + } + } + + private fun computeSpacesNotificationCounterBadgeState( + spacesNotificationCount: RoomAggregateNotificationCount, + spaceInvites: List, + ): UnreadCounterBadgeView.State { + val hasPendingSpaceInvites = spaceInvites.isNotEmpty() + return if (hasPendingSpaceInvites && spacesNotificationCount.notificationCount == 0) { + UnreadCounterBadgeView.State.Text( + text = "!", + highlighted = true, + ) + } else { + UnreadCounterBadgeView.State.Count( + count = spacesNotificationCount.notificationCount, + highlighted = spacesNotificationCount.isHighlight || hasPendingSpaceInvites, + ) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index a0d28ffe44..4b42903d32 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -30,7 +30,9 @@ import androidx.core.view.isVisible import androidx.drawerlayout.widget.DrawerLayout import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.viewModel import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -401,8 +403,8 @@ class HomeActivity : private fun handleStartRecoverySetup() { // To avoid IllegalStateException in case the transaction was executed after onSaveInstanceState - lifecycleScope.launchWhenResumed { - navigator.open4SSetup(this@HomeActivity, SetupMode.NORMAL) + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { navigator.open4SSetup(this@HomeActivity, SetupMode.NORMAL) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt index 3189c2b99e..ef855ff15b 100644 --- a/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt @@ -47,6 +47,7 @@ import im.vector.app.features.call.SharedKnownCallsViewModel import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.dialpad.PstnDialActivity import im.vector.app.features.call.webrtc.WebRtcCallManager +import im.vector.app.features.home.room.list.UnreadCounterBadgeView import im.vector.app.features.home.room.list.actions.RoomListSharedAction import im.vector.app.features.home.room.list.actions.RoomListSharedActionViewModel import im.vector.app.features.home.room.list.home.HomeRoomListFragment @@ -82,6 +83,7 @@ class NewHomeDetailFragment : @Inject lateinit var buildMeta: BuildMeta private val viewModel: HomeDetailViewModel by fragmentViewModel() + private val newHomeDetailViewModel: NewHomeDetailViewModel by fragmentViewModel() private val unknownDeviceDetectorSharedViewModel: UnknownDeviceDetectorSharedViewModel by activityViewModel() private val serverBackupStatusViewModel: ServerBackupStatusViewModel by activityViewModel() @@ -180,6 +182,10 @@ class NewHomeDetailFragment : currentCallsViewPresenter.updateCall(callManager.getCurrentCall(), callManager.getCalls()) invalidateOptionsMenu() } + + newHomeDetailViewModel.onEach { viewState -> + refreshUnreadCounterBadge(viewState.spacesNotificationCounterBadgeState) + } } private fun setupObservers() { @@ -379,6 +385,10 @@ class NewHomeDetailFragment : } } + private fun refreshUnreadCounterBadge(badgeState: UnreadCounterBadgeView.State) { + views.spacesUnreadCounterBadge.render(badgeState) + } + override fun onTapToReturnToCall() { callManager.getCurrentCall()?.let { call -> VectorCallActivity.newIntent( diff --git a/vector/src/main/java/im/vector/app/features/home/NewHomeDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/NewHomeDetailViewModel.kt new file mode 100644 index 0000000000..67b4645944 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/NewHomeDetailViewModel.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home + +import com.airbnb.mvrx.MavericksViewModelFactory +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.platform.EmptyAction +import im.vector.app.core.platform.EmptyViewEvents +import im.vector.app.core.platform.VectorViewModel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +class NewHomeDetailViewModel @AssistedInject constructor( + @Assisted initialState: NewHomeDetailViewState, + private val getSpacesNotificationBadgeStateUseCase: GetSpacesNotificationBadgeStateUseCase, +) : VectorViewModel(initialState) { + + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: NewHomeDetailViewState): NewHomeDetailViewModel + } + + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + + init { + observeSpacesNotificationBadgeState() + } + + private fun observeSpacesNotificationBadgeState() { + getSpacesNotificationBadgeStateUseCase.execute() + .onEach { badgeState -> setState { copy(spacesNotificationCounterBadgeState = badgeState) } } + .launchIn(viewModelScope) + } + + override fun handle(action: EmptyAction) { + // do nothing + } +} diff --git a/vector/src/main/java/im/vector/app/features/homeserver/HomeServerCapabilitiesViewState.kt b/vector/src/main/java/im/vector/app/features/home/NewHomeDetailViewState.kt similarity index 65% rename from vector/src/main/java/im/vector/app/features/homeserver/HomeServerCapabilitiesViewState.kt rename to vector/src/main/java/im/vector/app/features/home/NewHomeDetailViewState.kt index d7ced5e632..7e368fb2d1 100644 --- a/vector/src/main/java/im/vector/app/features/homeserver/HomeServerCapabilitiesViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/NewHomeDetailViewState.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 New Vector Ltd + * Copyright (c) 2023 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,12 +14,11 @@ * limitations under the License. */ -package im.vector.app.features.homeserver +package im.vector.app.features.home import com.airbnb.mvrx.MavericksState -import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities +import im.vector.app.features.home.room.list.UnreadCounterBadgeView -data class HomeServerCapabilitiesViewState( - val capabilities: HomeServerCapabilities = HomeServerCapabilities(), - val isE2EByDefault: Boolean = true +data class NewHomeDetailViewState( + val spacesNotificationCounterBadgeState: UnreadCounterBadgeView.State = UnreadCounterBadgeView.State.Count(count = 0, highlighted = false), ) : MavericksState diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index e2619d0f39..fd60fb1ce9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -47,12 +47,15 @@ import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.core.view.updatePadding import androidx.fragment.app.setFragmentResultListener +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.transition.TransitionManager import com.airbnb.epoxy.EpoxyModel +import com.airbnb.epoxy.EpoxyVisibilityTracker import com.airbnb.epoxy.OnModelBuildFinishedListener import com.airbnb.epoxy.addGlidePreloader import com.airbnb.epoxy.glidePreloader @@ -76,7 +79,6 @@ import im.vector.app.core.extensions.filterDirectionOverrides import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.setTextOrHide -import im.vector.app.core.extensions.trackItemsVisibilityChange import im.vector.app.core.glide.GlideApp import im.vector.app.core.glide.GlideRequests import im.vector.app.core.intent.getFilenameFromUri @@ -265,6 +267,7 @@ class TimelineFragment : private val timelineViewModel: TimelineViewModel by fragmentViewModel() private val messageComposerViewModel: MessageComposerViewModel by fragmentViewModel() private val debouncer = Debouncer(createUIHandler()) + private val itemVisibilityTracker = EpoxyVisibilityTracker() private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback private lateinit var scrollOnHighlightedEventCallback: ScrollOnHighlightedEventCallback @@ -972,11 +975,11 @@ class TimelineFragment : override fun onResume() { super.onResume() + itemVisibilityTracker.attach(views.timelineRecyclerView) notificationDrawerManager.setCurrentRoom(timelineArgs.roomId) notificationDrawerManager.setCurrentThread(timelineArgs.threadTimelineArgs?.rootThreadEventId) roomDetailPendingActionStore.data?.let { handlePendingAction(it) } roomDetailPendingActionStore.data = null - views.timelineRecyclerView.adapter = timelineEventController.adapter } private fun handlePendingAction(roomDetailPendingAction: RoomDetailPendingAction) { @@ -993,9 +996,9 @@ class TimelineFragment : override fun onPause() { super.onPause() + itemVisibilityTracker.detach(views.timelineRecyclerView) notificationDrawerManager.setCurrentRoom(null) notificationDrawerManager.setCurrentThread(null) - views.timelineRecyclerView.adapter = null } private val emojiActivityResultLauncher = registerStartForActivityResult { activityResult -> @@ -1038,7 +1041,6 @@ class TimelineFragment : timelineEventController.callback = this timelineEventController.timeline = timelineViewModel.timeline - views.timelineRecyclerView.trackItemsVisibilityChange() layoutManager = object : LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, true) { override fun onLayoutCompleted(state: RecyclerView.State) { super.onLayoutCompleted(state) @@ -1061,6 +1063,7 @@ class TimelineFragment : it.dispatchTo(scrollOnHighlightedEventCallback) } timelineEventController.addModelBuildListener(modelBuildListener) + views.timelineRecyclerView.adapter = timelineEventController.adapter if (vectorPreferences.swipeToReplyIsEnabled()) { val quickReplyHandler = object : RoomMessageTouchHelperCallback.QuickReplayHandler { @@ -1108,29 +1111,31 @@ class TimelineFragment : private fun updateJumpToReadMarkerViewVisibility() { if (isThreadTimeLine()) return - viewLifecycleOwner.lifecycleScope.launchWhenResumed { - val state = timelineViewModel.awaitState() - val showJumpToUnreadBanner = when (state.unreadState) { - UnreadState.Unknown, - UnreadState.HasNoUnread -> false - is UnreadState.ReadMarkerNotLoaded -> true - is UnreadState.HasUnread -> { - if (state.canShowJumpToReadMarker) { - val lastVisibleItem = layoutManager.findLastCompletelyVisibleItemPosition() - val positionOfReadMarker = withContext(Dispatchers.Default) { - timelineEventController.getPositionOfReadMarker() - } - if (positionOfReadMarker == null) { - false + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) { + val state = timelineViewModel.awaitState() + val showJumpToUnreadBanner = when (state.unreadState) { + UnreadState.Unknown, + UnreadState.HasNoUnread -> false + is UnreadState.ReadMarkerNotLoaded -> true + is UnreadState.HasUnread -> { + if (state.canShowJumpToReadMarker) { + val lastVisibleItem = layoutManager.findLastCompletelyVisibleItemPosition() + val positionOfReadMarker = withContext(Dispatchers.Default) { + timelineEventController.getPositionOfReadMarker() + } + if (positionOfReadMarker == null) { + false + } else { + positionOfReadMarker > lastVisibleItem + } } else { - positionOfReadMarker > lastVisibleItem + false } - } else { - false } } + views.jumpToReadMarkerView.isVisible = showJumpToUnreadBanner } - views.jumpToReadMarkerView.isVisible = showJumpToUnreadBanner } } @@ -1175,6 +1180,10 @@ class TimelineFragment : views.hideComposerViews() views.notificationAreaView.render(NotificationAreaView.State.Tombstone(mainState.tombstoneEvent)) } + + if (summary.isDirect && summary.isEncrypted && summary.joinedMembersCount == 1 && summary.invitedMembersCount == 0) { + views.hideComposerViews() + } } else if (summary?.membership == Membership.INVITE && inviter != null) { views.hideComposerViews() lazyLoadedViews.inviteView(true)?.apply { @@ -1615,14 +1624,16 @@ class TimelineFragment : } override fun onRoomCreateLinkClicked(url: String) { - viewLifecycleOwner.lifecycleScope.launchWhenResumed { - permalinkHandler - .launch(requireActivity(), url, object : NavigationInterceptor { - override fun navToRoom(roomId: String?, eventId: String?, deepLink: Uri?, rootThreadEventId: String?): Boolean { - requireActivity().finish() - return false - } - }) + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) { + permalinkHandler + .launch(requireActivity(), url, object : NavigationInterceptor { + override fun navToRoom(roomId: String?, eventId: String?, deepLink: Uri?, rootThreadEventId: String?): Boolean { + requireActivity().finish() + return false + } + }) + } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index f4872dbea6..1098225042 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -54,6 +54,7 @@ import im.vector.app.features.crypto.verification.SupportedVerificationMethodsPr import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction import im.vector.app.features.home.room.detail.error.RoomNotFound import im.vector.app.features.home.room.detail.location.RedactLiveLocationShareEventUseCase +import im.vector.app.features.home.room.detail.poll.VoteToPollUseCase import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler import im.vector.app.features.home.room.detail.timeline.factory.TimelineFactory import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever @@ -91,7 +92,6 @@ import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.LocalEcho import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage @@ -155,6 +155,7 @@ class TimelineViewModel @AssistedInject constructor( timelineFactory: TimelineFactory, private val spaceStateHandler: SpaceStateHandler, private val voiceBroadcastHelper: VoiceBroadcastHelper, + private val voteToPollUseCase: VoteToPollUseCase, ) : VectorViewModel(initialState), Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener, LocationSharingServiceConnection.Callback { @@ -1243,15 +1244,11 @@ class TimelineViewModel @AssistedInject constructor( private fun handleVoteToPoll(action: RoomDetailAction.VoteToPoll) { if (room == null) return - // Do not allow to vote unsent local echo of the poll event - if (LocalEcho.isLocalEchoId(action.eventId)) return - // Do not allow to vote the same option twice - room.getTimelineEvent(action.eventId)?.let { pollTimelineEvent -> - val currentVote = pollTimelineEvent.annotations?.pollResponseSummary?.aggregatedContent?.myVote - if (currentVote != action.optionKey) { - room.sendService().voteToPoll(action.eventId, action.optionKey) - } - } + voteToPollUseCase.execute( + roomId = room.roomId, + pollEventId = action.eventId, + optionId = action.optionKey, + ) } private fun handleEndPoll(eventId: String) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/ComposerEditText.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/ComposerEditText.kt index 9e88882866..f0014f4ecd 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/ComposerEditText.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/ComposerEditText.kt @@ -17,7 +17,6 @@ package im.vector.app.features.home.room.detail.composer -import android.content.ClipData import android.content.Context import android.net.Uri import android.os.Build @@ -27,12 +26,12 @@ import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputConnection import androidx.annotation.RequiresApi import androidx.appcompat.widget.AppCompatEditText -import androidx.core.view.OnReceiveContentListener import androidx.core.view.ViewCompat import androidx.core.view.inputmethod.EditorInfoCompat import androidx.core.view.inputmethod.InputConnectionCompat import im.vector.app.core.extensions.ooi import im.vector.app.core.platform.SimpleTextWatcher +import im.vector.app.features.home.room.detail.composer.images.UriContentListener import im.vector.app.features.html.PillImageSpan import timber.log.Timber @@ -56,27 +55,11 @@ class ComposerEditText @JvmOverloads constructor( EditorInfoCompat.setContentMimeTypes(editorInfo, mimeTypes) ic = InputConnectionCompat.createWrapper(this, ic, editorInfo) - val onReceiveContentListener = OnReceiveContentListener { _, payload -> - val split = payload.partition { item -> item.uri != null } - val uriContent = split.first - val remaining = split.second - - if (uriContent != null) { - val clip: ClipData = uriContent.clip - for (i in 0 until clip.itemCount) { - val uri = clip.getItemAt(i).uri - // ... app-specific logic to handle the URI ... - callback?.onRichContentSelected(uri) - } - } - // Return anything that we didn't handle ourselves. This preserves the default platform - // behavior for text and anything else for which we are not implementing custom handling. - // Return anything that we didn't handle ourselves. This preserves the default platform - // behavior for text and anything else for which we are not implementing custom handling. - remaining - } - - ViewCompat.setOnReceiveContentListener(this, mimeTypes, onReceiveContentListener) + ViewCompat.setOnReceiveContentListener( + this, + mimeTypes, + UriContentListener { callback?.onRichContentSelected(it) } + ) return ic } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt index 2c0d77045e..a821458939 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt @@ -36,6 +36,7 @@ import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.constraintlayout.widget.ConstraintSet import androidx.core.text.toSpannable +import androidx.core.view.ViewCompat import androidx.core.view.isGone import androidx.core.view.isInvisible import androidx.core.view.isVisible @@ -47,6 +48,7 @@ import im.vector.app.core.extensions.showKeyboard import im.vector.app.core.utils.DimensionConverter import im.vector.app.databinding.ComposerRichTextLayoutBinding import im.vector.app.databinding.ViewRichTextMenuButtonBinding +import im.vector.app.features.home.room.detail.composer.images.UriContentListener import io.element.android.wysiwyg.EditorEditText import io.element.android.wysiwyg.inputhandlers.models.InlineFormat import io.element.android.wysiwyg.inputhandlers.models.LinkAction @@ -188,6 +190,16 @@ internal class RichTextComposerLayout @JvmOverloads constructor( views.plainTextComposerEditText.addTextChangedListener( TextChangeListener({ callback?.onTextChanged(it) }, { updateTextFieldBorder(isFullScreen) }) ) + ViewCompat.setOnReceiveContentListener( + views.richTextComposerEditText, + arrayOf("image/*"), + UriContentListener { callback?.onRichContentSelected(it) } + ) + ViewCompat.setOnReceiveContentListener( + views.plainTextComposerEditText, + arrayOf("image/*"), + UriContentListener { callback?.onRichContentSelected(it) } + ) disallowParentInterceptTouchEvent(views.richTextComposerEditText) disallowParentInterceptTouchEvent(views.plainTextComposerEditText) @@ -232,6 +244,27 @@ internal class RichTextComposerLayout @JvmOverloads constructor( addRichTextMenuItem(R.drawable.ic_composer_strikethrough, R.string.rich_text_editor_format_strikethrough, ComposerAction.STRIKE_THROUGH) { views.richTextComposerEditText.toggleInlineFormat(InlineFormat.StrikeThrough) } + addRichTextMenuItem(R.drawable.ic_composer_bullet_list, R.string.rich_text_editor_bullet_list, ComposerAction.UNORDERED_LIST) { + views.richTextComposerEditText.toggleList(ordered = false) + } + addRichTextMenuItem(R.drawable.ic_composer_numbered_list, R.string.rich_text_editor_numbered_list, ComposerAction.ORDERED_LIST) { + views.richTextComposerEditText.toggleList(ordered = true) + } + addRichTextMenuItem(R.drawable.ic_composer_indent, R.string.rich_text_editor_indent, ComposerAction.INDENT) { + views.richTextComposerEditText.indent() + } + addRichTextMenuItem(R.drawable.ic_composer_unindent, R.string.rich_text_editor_unindent, ComposerAction.UNINDENT) { + views.richTextComposerEditText.unindent() + } + addRichTextMenuItem(R.drawable.ic_composer_quote, R.string.rich_text_editor_quote, ComposerAction.QUOTE) { + views.richTextComposerEditText.toggleQuote() + } + addRichTextMenuItem(R.drawable.ic_composer_inline_code, R.string.rich_text_editor_inline_code, ComposerAction.INLINE_CODE) { + views.richTextComposerEditText.toggleInlineFormat(InlineFormat.InlineCode) + } + addRichTextMenuItem(R.drawable.ic_composer_code_block, R.string.rich_text_editor_code_block, ComposerAction.CODE_BLOCK) { + views.richTextComposerEditText.toggleCodeBlock() + } addRichTextMenuItem(R.drawable.ic_composer_link, R.string.rich_text_editor_link, ComposerAction.LINK) { views.richTextComposerEditText.getLinkAction()?.let { when (it) { @@ -240,15 +273,6 @@ internal class RichTextComposerLayout @JvmOverloads constructor( } } } - addRichTextMenuItem(R.drawable.ic_composer_bullet_list, R.string.rich_text_editor_bullet_list, ComposerAction.UNORDERED_LIST) { - views.richTextComposerEditText.toggleList(ordered = false) - } - addRichTextMenuItem(R.drawable.ic_composer_numbered_list, R.string.rich_text_editor_numbered_list, ComposerAction.ORDERED_LIST) { - views.richTextComposerEditText.toggleList(ordered = true) - } - addRichTextMenuItem(R.drawable.ic_composer_inline_code, R.string.rich_text_editor_inline_code, ComposerAction.INLINE_CODE) { - views.richTextComposerEditText.toggleInlineFormat(InlineFormat.InlineCode) - } } fun setLink(link: String?) = @@ -294,7 +318,7 @@ internal class RichTextComposerLayout @JvmOverloads constructor( private fun updateEditTextVisibility() { views.richTextComposerEditText.isVisible = isTextFormattingEnabled - views.richTextMenu.isVisible = isTextFormattingEnabled + views.richTextMenuScrollView.isVisible = isTextFormattingEnabled views.plainTextComposerEditText.isVisible = !isTextFormattingEnabled // The layouts for formatted text mode and plain text mode are different, so we need to update the constraints @@ -331,11 +355,11 @@ internal class RichTextComposerLayout @JvmOverloads constructor( * Updates the non-active input with the contents of the active input. */ private fun syncEditTexts() = - if (isTextFormattingEnabled) { - views.plainTextComposerEditText.setText(views.richTextComposerEditText.getMarkdown()) - } else { - views.richTextComposerEditText.setMarkdown(views.plainTextComposerEditText.text.toString()) - } + if (isTextFormattingEnabled) { + views.plainTextComposerEditText.setText(views.richTextComposerEditText.getMarkdown()) + } else { + views.richTextComposerEditText.setMarkdown(views.plainTextComposerEditText.text.toString()) + } private fun addRichTextMenuItem(@DrawableRes iconId: Int, @StringRes description: Int, action: ComposerAction, onClick: () -> Unit) { val inflater = LayoutInflater.from(context) @@ -355,6 +379,13 @@ internal class RichTextComposerLayout @JvmOverloads constructor( val stateForAction = menuState[action] button.isEnabled = stateForAction != ActionState.DISABLED button.isSelected = stateForAction == ActionState.REVERSED + + if (action == ComposerAction.INDENT || action == ComposerAction.UNINDENT) { + val indentationButtonIsVisible = + menuState[ComposerAction.ORDERED_LIST] == ActionState.REVERSED || + menuState[ComposerAction.UNORDERED_LIST] == ActionState.REVERSED + button.isVisible = indentationButtonIsVisible + } } fun estimateCollapsedHeight(): Int { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/images/UriContentListener.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/images/UriContentListener.kt new file mode 100644 index 0000000000..da0152fd9e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/images/UriContentListener.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail.composer.images + +import android.content.ClipData +import android.net.Uri +import android.view.View +import androidx.core.view.ContentInfoCompat +import androidx.core.view.OnReceiveContentListener + +class UriContentListener( + private val onContent: (uri: Uri) -> Unit +) : OnReceiveContentListener { + override fun onReceiveContent(view: View, payload: ContentInfoCompat): ContentInfoCompat? { + val split = payload.partition { item -> item.uri != null } + val uriContent = split.first + val remaining = split.second + + if (uriContent != null) { + val clip: ClipData = uriContent.clip + for (i in 0 until clip.itemCount) { + val uri = clip.getItemAt(i).uri + // ... app-specific logic to handle the URI ... + onContent(uri) + } + } + // Return anything that we didn't handle ourselves. This preserves the default platform + // behavior for text and anything else for which we are not implementing custom handling. + return remaining + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/poll/VoteToPollUseCase.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/poll/VoteToPollUseCase.kt new file mode 100644 index 0000000000..62f8006988 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/poll/VoteToPollUseCase.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail.poll + +import im.vector.app.core.di.ActiveSessionHolder +import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.api.session.getRoom +import org.matrix.android.sdk.api.session.room.getTimelineEvent +import timber.log.Timber +import javax.inject.Inject + +class VoteToPollUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, +) { + + fun execute(roomId: String, pollEventId: String, optionId: String) { + // Do not allow to vote unsent local echo of the poll event + if (LocalEcho.isLocalEchoId(pollEventId)) return + + runCatching { + val room = activeSessionHolder.getActiveSession().getRoom(roomId) + room?.getTimelineEvent(pollEventId)?.let { pollTimelineEvent -> + val currentVote = pollTimelineEvent + .annotations + ?.pollResponseSummary + ?.aggregatedContent + ?.myVote + if (currentVote != optionId) { + room.sendService().voteToPoll( + pollEventId = pollEventId, + answerId = optionId + ) + } + } + }.onFailure { Timber.w("Failed to vote in poll with id $pollEventId in room with id $roomId") } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt index 69b4f6e039..9d757a5f2e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt @@ -56,22 +56,37 @@ class EncryptionItemFactory @Inject constructor( val description: String val shield: StatusTileTimelineItem.ShieldUIState if (isSafeAlgorithm) { - val isDirect = session.getRoomSummary(event.root.roomId.orEmpty())?.isDirect.orFalse() - title = stringProvider.getString(R.string.encryption_enabled) - description = stringProvider.getString( + val roomSummary = session.getRoomSummary(event.root.roomId.orEmpty()) + val isDirect = roomSummary?.isDirect.orFalse() + val (resTitle, resDescription, resShield) = when { + isDirect -> { + val isWaitingUser = roomSummary?.isEncrypted.orFalse() && roomSummary?.joinedMembersCount == 1 && roomSummary.invitedMembersCount == 0 when { - isDirect && RoomLocalEcho.isLocalEchoId(event.root.roomId.orEmpty()) -> { - R.string.direct_room_encryption_enabled_tile_description_future - } - isDirect -> { - R.string.direct_room_encryption_enabled_tile_description - } - else -> { - R.string.encryption_enabled_tile_description - } + RoomLocalEcho.isLocalEchoId(event.root.roomId.orEmpty()) -> Triple( + R.string.encryption_enabled, + R.string.direct_room_encryption_enabled_tile_description_future, + StatusTileTimelineItem.ShieldUIState.BLACK + ) + isWaitingUser -> Triple( + R.string.direct_room_encryption_enabled_waiting_users, + R.string.direct_room_encryption_enabled_waiting_users_tile_description, + StatusTileTimelineItem.ShieldUIState.WAITING + ) + else -> Triple( + R.string.encryption_enabled, + R.string.direct_room_encryption_enabled_tile_description, + StatusTileTimelineItem.ShieldUIState.BLACK + ) } - ) - shield = StatusTileTimelineItem.ShieldUIState.BLACK + } + else -> { + Triple(R.string.encryption_enabled, R.string.encryption_enabled_tile_description, StatusTileTimelineItem.ShieldUIState.BLACK) + } + } + + title = stringProvider.getString(resTitle) + description = stringProvider.getString(resDescription) + shield = resShield } else { title = stringProvider.getString(R.string.encryption_misconfigured) description = stringProvider.getString(R.string.encryption_unknown_algorithm_tile_description) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 67983fc351..09b91cc4f1 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -114,7 +114,6 @@ import org.matrix.android.sdk.api.session.room.model.relation.ReplyToContent import org.matrix.android.sdk.api.session.room.timeline.getRelationContent import org.matrix.android.sdk.api.settings.LightweightSettingsStorage import org.matrix.android.sdk.api.util.MimeTypes -import timber.log.Timber import javax.inject.Inject class MessageItemFactory @Inject constructor( @@ -160,8 +159,8 @@ class MessageItemFactory @Inject constructor( textRendererFactory.create(roomId) } - private val useRichTextEditorStyle: Boolean get() = - vectorPreferences.isRichTextEditorEnabled() + private val useRichTextEditorStyle: Boolean + get() = vectorPreferences.isRichTextEditorEnabled() fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? { val event = params.event @@ -255,12 +254,16 @@ class MessageItemFactory @Inject constructor( attributes: AbsMessageItem.Attributes, isEnded: Boolean, ): PollItem { - val pollViewState = pollItemViewStateFactory.create(pollContent, informationData) + val pollViewState = pollItemViewStateFactory.create( + pollContent = pollContent, + pollResponseData = informationData.pollResponseAggregatedSummary, + isSent = informationData.sendState.isSent(), + ) return PollItem_() .attributes(attributes) .eventId(informationData.eventId) - .pollQuestion(createPollQuestion(informationData, pollViewState.question, callback)) + .pollTitle(createPollQuestion(informationData, pollViewState.question, callback)) .canVote(pollViewState.canVote) .votesStatus(pollViewState.votesStatus) .optionViewStates(pollViewState.optionViewStates.orEmpty()) @@ -277,21 +280,37 @@ class MessageItemFactory @Inject constructor( highlight: Boolean, callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes, - ): PollItem? { - pollStartEventId ?: return null.also { - Timber.e("### buildEndedPollItem. Cannot render poll end event because poll start event id is null") + ): PollItem { + val pollStartEvent = if (pollStartEventId?.isNotEmpty() == true) { + session.roomService().getRoom(roomId)?.getTimelineEvent(pollStartEventId) + } else { + null } - val pollStartEvent = session.roomService().getRoom(roomId)?.getTimelineEvent(pollStartEventId) - val pollContent = pollStartEvent?.root?.getClearContent()?.toModel() ?: return null + val pollContent = pollStartEvent?.root?.getClearContent()?.toModel() - return buildPollItem( - pollContent, - informationData, - highlight, - callback, - attributes, - isEnded = true - ) + return if (pollContent == null) { + val title = stringProvider.getString(R.string.message_reply_to_ended_poll_preview).toEpoxyCharSequence() + PollItem_() + .attributes(attributes) + .eventId(informationData.eventId) + .pollTitle(title) + .optionViewStates(emptyList()) + .edited(informationData.hasBeenEdited) + .ended(true) + .hasContent(false) + .highlighted(highlight) + .leftGuideline(avatarSizeProvider.leftGuideline) + .callback(callback) + } else { + buildPollItem( + pollContent, + informationData, + highlight, + callback, + attributes, + isEnded = true, + ) + } } private fun createPollQuestion( @@ -483,7 +502,6 @@ class MessageItemFactory @Inject constructor( highlight, callback, attributes, - useRichTextEditorStyle = vectorPreferences.isRichTextEditorEnabled(), ) } @@ -590,7 +608,7 @@ class MessageItemFactory @Inject constructor( val replyToContent = messageContent.relatesTo?.inReplyTo buildFormattedTextItem(matrixFormattedBody, informationData, highlight, callback, attributes, replyToContent) } else { - buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes, useRichTextEditorStyle) + buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes) } } @@ -614,7 +632,6 @@ class MessageItemFactory @Inject constructor( highlight, callback, attributes, - useRichTextEditorStyle, ) } @@ -625,7 +642,6 @@ class MessageItemFactory @Inject constructor( highlight: Boolean, callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes, - useRichTextEditorStyle: Boolean, ): MessageTextItem? { val renderedBody = textRenderer.render(body) val bindingOptions = spanUtils.getBindingOptions(renderedBody) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt index 3c1a1cfd85..b630a514e4 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt @@ -18,9 +18,8 @@ package im.vector.app.features.home.room.detail.timeline.factory import im.vector.app.R import im.vector.app.core.resources.StringProvider -import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData import im.vector.app.features.home.room.detail.timeline.item.PollResponseData -import im.vector.app.features.poll.PollViewState +import im.vector.app.features.poll.PollItemViewState import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.PollCreationInfo @@ -33,27 +32,25 @@ class PollItemViewStateFactory @Inject constructor( fun create( pollContent: MessagePollContent, - informationData: MessageInformationData, - ): PollViewState { + pollResponseData: PollResponseData?, + isSent: Boolean, + ): PollItemViewState { val pollCreationInfo = pollContent.getBestPollCreationInfo() - val question = pollCreationInfo?.question?.getBestQuestion().orEmpty() - - val pollResponseSummary = informationData.pollResponseAggregatedSummary - val totalVotes = pollResponseSummary?.totalVotes ?: 0 + val totalVotes = pollResponseData?.totalVotes ?: 0 return when { - !informationData.sendState.isSent() -> { + !isSent -> { createSendingPollViewState(question, pollCreationInfo) } - informationData.pollResponseAggregatedSummary?.isClosed.orFalse() -> { - createEndedPollViewState(question, pollCreationInfo, pollResponseSummary, totalVotes) + pollResponseData?.isClosed.orFalse() -> { + createEndedPollViewState(question, pollCreationInfo, pollResponseData, totalVotes) } pollContent.getBestPollCreationInfo()?.isUndisclosed().orFalse() -> { - createUndisclosedPollViewState(question, pollCreationInfo, pollResponseSummary) + createUndisclosedPollViewState(question, pollCreationInfo, pollResponseData) } - informationData.pollResponseAggregatedSummary?.myVote?.isNotEmpty().orFalse() -> { - createVotedPollViewState(question, pollCreationInfo, pollResponseSummary, totalVotes) + pollResponseData?.myVote?.isNotEmpty().orFalse() -> { + createVotedPollViewState(question, pollCreationInfo, pollResponseData, totalVotes) } else -> { createReadyPollViewState(question, pollCreationInfo, totalVotes) @@ -61,8 +58,8 @@ class PollItemViewStateFactory @Inject constructor( } } - private fun createSendingPollViewState(question: String, pollCreationInfo: PollCreationInfo?): PollViewState { - return PollViewState( + private fun createSendingPollViewState(question: String, pollCreationInfo: PollCreationInfo?): PollItemViewState { + return PollItemViewState( question = question, votesStatus = stringProvider.getString(R.string.poll_no_votes_cast), canVote = false, @@ -73,51 +70,51 @@ class PollItemViewStateFactory @Inject constructor( private fun createEndedPollViewState( question: String, pollCreationInfo: PollCreationInfo?, - pollResponseSummary: PollResponseData?, + pollResponseData: PollResponseData?, totalVotes: Int, - ): PollViewState { - val totalVotesText = if (pollResponseSummary?.hasEncryptedRelatedEvents.orFalse()) { + ): PollItemViewState { + val totalVotesText = if (pollResponseData?.hasEncryptedRelatedEvents.orFalse()) { stringProvider.getString(R.string.unable_to_decrypt_some_events_in_poll) } else { stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, totalVotes, totalVotes) } - return PollViewState( + return PollItemViewState( question = question, votesStatus = totalVotesText, canVote = false, - optionViewStates = pollOptionViewStateFactory.createPollEndedOptions(pollCreationInfo, pollResponseSummary), + optionViewStates = pollOptionViewStateFactory.createPollEndedOptions(pollCreationInfo, pollResponseData), ) } private fun createUndisclosedPollViewState( question: String, pollCreationInfo: PollCreationInfo?, - pollResponseSummary: PollResponseData? - ): PollViewState { - return PollViewState( + pollResponseData: PollResponseData? + ): PollItemViewState { + return PollItemViewState( question = question, votesStatus = stringProvider.getString(R.string.poll_undisclosed_not_ended), canVote = true, - optionViewStates = pollOptionViewStateFactory.createPollUndisclosedOptions(pollCreationInfo, pollResponseSummary), + optionViewStates = pollOptionViewStateFactory.createPollUndisclosedOptions(pollCreationInfo, pollResponseData), ) } private fun createVotedPollViewState( question: String, pollCreationInfo: PollCreationInfo?, - pollResponseSummary: PollResponseData?, + pollResponseData: PollResponseData?, totalVotes: Int - ): PollViewState { - val totalVotesText = if (pollResponseSummary?.hasEncryptedRelatedEvents.orFalse()) { + ): PollItemViewState { + val totalVotesText = if (pollResponseData?.hasEncryptedRelatedEvents.orFalse()) { stringProvider.getString(R.string.unable_to_decrypt_some_events_in_poll) } else { stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, totalVotes, totalVotes) } - return PollViewState( + return PollItemViewState( question = question, votesStatus = totalVotesText, canVote = true, - optionViewStates = pollOptionViewStateFactory.createPollVotedOptions(pollCreationInfo, pollResponseSummary), + optionViewStates = pollOptionViewStateFactory.createPollVotedOptions(pollCreationInfo, pollResponseData), ) } @@ -125,13 +122,13 @@ class PollItemViewStateFactory @Inject constructor( question: String, pollCreationInfo: PollCreationInfo?, totalVotes: Int - ): PollViewState { + ): PollItemViewState { val totalVotesText = if (totalVotes == 0) { stringProvider.getString(R.string.poll_no_votes_cast) } else { stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_not_voted, totalVotes, totalVotes) } - return PollViewState( + return PollItemViewState( question = question, votesStatus = totalVotesText, canVote = true, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index 61b2385d1d..84b71ceedf 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -22,8 +22,12 @@ import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.features.analytics.DecryptionFailureTracker import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants +import im.vector.app.features.voicebroadcast.model.isVoiceBroadcast +import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.timeline.getRelationContent import timber.log.Timber import javax.inject.Inject @@ -39,6 +43,7 @@ class TimelineItemFactory @Inject constructor( private val callItemFactory: CallItemFactory, private val decryptionFailureTracker: DecryptionFailureTracker, private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper, + private val session: Session, ) { /** @@ -130,11 +135,16 @@ class TimelineItemFactory @Inject constructor( EventType.CALL_ANSWER -> callItemFactory.create(params) // Crypto EventType.ENCRYPTED -> { - if (event.root.isRedacted()) { + val relationContent = event.getRelationContent() + when { // Redacted event, let the MessageItemFactory handle it - messageItemFactory.create(params) - } else { - encryptedItemFactory.create(params) + event.root.isRedacted() -> messageItemFactory.create(params) + relationContent?.type == RelationType.REFERENCE -> { + // Hide the decryption error for VoiceBroadcast chunks + val relatedEvent = relationContent.eventId?.let { session.eventService().getEventFromCache(event.roomId, it) } + if (relatedEvent?.isVoiceBroadcast() != true) encryptedItemFactory.create(params) else null + } + else -> encryptedItemFactory.create(params) } } EventType.KEY_VERIFICATION_CANCEL, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt index 3439fb1f57..7d05463b28 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt @@ -75,6 +75,7 @@ class VoiceBroadcastItemFactory @Inject constructor( voiceBroadcast = voiceBroadcast, voiceBroadcastState = voiceBroadcastContent.voiceBroadcastState, duration = voiceBroadcastEventsGroup.getDuration(), + hasUnableToDecryptEvent = voiceBroadcastEventsGroup.hasUnableToDecryptEvent(), recorderName = params.event.senderInfo.disambiguatedDisplayName, recorder = voiceBroadcastRecorder, player = voiceBroadcastPlayer, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt index a4bfa9e155..a3e3f502b6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt @@ -25,6 +25,8 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.getRelationContent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent @@ -61,6 +63,7 @@ class TimelineEventsGroups { private fun TimelineEvent.getGroupIdOrNull(): String? { val type = root.getClearType() val content = root.getClearContent() + val relationContent = root.getRelationContent() return when { EventType.isCallEvent(type) -> (content?.get("call_id") as? String) type == VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO -> root.asVoiceBroadcastEvent()?.reference?.eventId @@ -69,6 +72,9 @@ class TimelineEventsGroups { // Group voice messages with a reference to an eventId root.asMessageAudioEvent()?.getVoiceBroadcastEventId() } + type == EventType.ENCRYPTED && relationContent?.type == RelationType.REFERENCE -> { + relationContent.eventId + } else -> { null } @@ -153,4 +159,8 @@ class VoiceBroadcastEventsGroup(private val group: TimelineEventsGroup) { fun getDuration(): Int { return group.events.mapNotNull { it.root.asMessageAudioEvent()?.duration }.sum() } + + fun hasUnableToDecryptEvent(): Boolean { + return group.events.any { it.root.getClearType() == EventType.ENCRYPTED } + } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt index 7cde978e42..21d1abbdf2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt @@ -45,6 +45,7 @@ abstract class AbsMessageVoiceBroadcastItem { - if (attributes.isLocalRoom) { - resources.getString(R.string.direct_room_encryption_enabled_tile_description_future) - } else { - resources.getString(R.string.direct_room_encryption_enabled_tile_description) + val isWaitingUser = roomSummary?.isEncrypted.orFalse() && roomSummary?.joinedMembersCount == 1 && roomSummary?.invitedMembersCount == 0 + when { + attributes.isLocalRoom -> Triple( + R.string.encryption_enabled, + R.string.direct_room_encryption_enabled_tile_description_future, + R.drawable.ic_shield_black + ) + isWaitingUser -> Triple( + R.string.direct_room_encryption_enabled_waiting_users, + R.string.direct_room_encryption_enabled_waiting_users_tile_description, + R.drawable.ic_room_profile_member_list + ) + else -> Triple( + R.string.encryption_enabled, + R.string.direct_room_encryption_enabled_tile_description, + R.drawable.ic_shield_black + ) } } else -> { - resources.getString(R.string.encryption_enabled_tile_description) + Triple(R.string.encryption_enabled, R.string.encryption_enabled_tile_description, R.drawable.ic_shield_black) } } - holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_enabled) + holder.e2eTitleTextView.text = holder.expandView.resources.getString(title) holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds( - ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_black), + ContextCompat.getDrawable(holder.view.context, drawable), null, null, null ) - holder.e2eTitleDescriptionView.text = description + holder.e2eTitleDescriptionView.text = holder.expandView.resources.getString(description) holder.e2eTitleDescriptionView.textAlignment = View.TEXT_ALIGNMENT_CENTER } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index 0aa2aaad3b..b9d70b51cb 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -29,6 +29,7 @@ import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker.Listener.State +import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.views.VoiceBroadcastBufferingView @@ -136,12 +137,19 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem private fun renderPlaybackError(holder: Holder, playbackState: State) { with(holder) { - if (playbackState is State.Error) { - controlsGroup.isVisible = false - errorView.setTextOrHide(errorFormatter.toHumanReadable(playbackState.failure)) - } else { - errorView.isVisible = false - controlsGroup.isVisible = true + when { + playbackState is State.Error -> { + controlsGroup.isVisible = false + errorView.setTextOrHide(errorFormatter.toHumanReadable(playbackState.failure)) + } + playbackState is State.Idle && hasUnableToDecryptEvent -> { + controlsGroup.isVisible = false + errorView.setTextOrHide(errorFormatter.toHumanReadable(VoiceBroadcastFailure.ListeningError.UnableToDecrypt)) + } + else -> { + errorView.isVisible = false + controlsGroup.isVisible = true + } } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt index 6fe19e9762..220e422365 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt @@ -16,6 +16,7 @@ package im.vector.app.features.home.room.detail.timeline.item +import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView import androidx.core.view.children @@ -23,6 +24,7 @@ import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R +import im.vector.app.core.extensions.setTextOrHide import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence @@ -31,7 +33,7 @@ import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence abstract class PollItem : AbsMessageItem() { @EpoxyAttribute - var pollQuestion: EpoxyCharSequence? = null + var pollTitle: EpoxyCharSequence? = null @EpoxyAttribute var callback: TimelineEventController.Callback? = null @@ -54,6 +56,9 @@ abstract class PollItem : AbsMessageItem() { @EpoxyAttribute var ended: Boolean = false + @EpoxyAttribute + var hasContent: Boolean = true + override fun getViewStubId() = STUB_ID override fun bind(holder: Holder) { @@ -61,8 +66,8 @@ abstract class PollItem : AbsMessageItem() { renderSendState(holder.view, holder.questionTextView) - holder.questionTextView.text = pollQuestion?.charSequence - holder.votesStatusTextView.text = votesStatus + holder.questionTextView.text = pollTitle?.charSequence + holder.votesStatusTextView.setTextOrHide(votesStatus) while (holder.optionsContainer.childCount < optionViewStates.size) { holder.optionsContainer.addView(PollOptionView(holder.view.context)) @@ -80,7 +85,8 @@ abstract class PollItem : AbsMessageItem() { } } - holder.endedPollTextView.isVisible = ended + holder.endedPollTextView.isVisible = ended && hasContent + holder.pollIcon.isVisible = ended && hasContent.not() } private fun onPollItemClick(optionViewState: PollOptionViewState) { @@ -96,6 +102,7 @@ abstract class PollItem : AbsMessageItem() { val optionsContainer by bind(R.id.optionsContainer) val votesStatusTextView by bind(R.id.optionsVotesStatusTextView) val endedPollTextView by bind(R.id.endedPollTextView) + val pollIcon by bind(R.id.timelinePollIcon) } companion object { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/StatusTileTimelineItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/StatusTileTimelineItem.kt index 1e5bb0521d..5a0c024ec8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/StatusTileTimelineItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/StatusTileTimelineItem.kt @@ -57,6 +57,7 @@ abstract class StatusTileTimelineItem : AbsBaseMessageItem R.drawable.ic_shield_trusted ShieldUIState.BLACK -> R.drawable.ic_shield_black ShieldUIState.RED -> R.drawable.ic_shield_warning + ShieldUIState.WAITING -> R.drawable.ic_room_profile_member_list ShieldUIState.ERROR -> R.drawable.ic_warning_badge } @@ -101,6 +102,7 @@ abstract class StatusTileTimelineItem : AbsBaseMessageItem(initialState) { - - @AssistedFactory - interface Factory : MavericksAssistedViewModelFactory { - override fun create(initialState: HomeServerCapabilitiesViewState): HomeServerCapabilitiesViewModel - } - - companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() { - - override fun initialState(viewModelContext: ViewModelContext): HomeServerCapabilitiesViewState { - val session = EntryPoints.get(viewModelContext.app(), SingletonEntryPoint::class.java).activeSessionHolder().getSafeActiveSession() - return HomeServerCapabilitiesViewState( - capabilities = session?.homeServerCapabilitiesService()?.getHomeServerCapabilities() ?: HomeServerCapabilities() - ) - } - } - - init { - - initAdminE2eByDefault() - } - - private fun initAdminE2eByDefault() { - viewModelScope.launch(Dispatchers.IO) { - val adminE2EByDefault = tryOrNull { - rawService.getElementWellknown(session.sessionParams) - ?.isE2EByDefault() - ?: true - } ?: true - - setState { - copy( - isE2EByDefault = adminE2EByDefault - ) - } - } - } - - override fun handle(action: EmptyAction) {} -} diff --git a/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt b/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt index bc9ba0b85a..cb3f12d867 100644 --- a/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt @@ -31,6 +31,9 @@ import android.graphics.Typeface import android.graphics.drawable.Drawable import android.text.Spannable import android.text.SpannableStringBuilder +import android.text.style.StrikethroughSpan +import android.text.style.URLSpan +import android.text.style.UnderlineSpan import android.widget.TextView import androidx.core.text.toSpannable import com.bumptech.glide.Glide @@ -46,6 +49,8 @@ import io.noties.markwon.Markwon import io.noties.markwon.MarkwonPlugin import io.noties.markwon.MarkwonSpansFactory import io.noties.markwon.PrecomputedFutureTextSetterCompat +import io.noties.markwon.core.spans.EmphasisSpan +import io.noties.markwon.core.spans.StrongEmphasisSpan import io.noties.markwon.ext.latex.JLatexMathPlugin import io.noties.markwon.ext.latex.JLatexMathTheme import io.noties.markwon.html.HtmlPlugin @@ -154,14 +159,24 @@ class EventHtmlRenderer @Inject constructor( /** * Workaround for https://github.com/noties/Markwon/issues/423 */ - private val removeLeadingNewlineForInlineCode = object : AbstractMarkwonPlugin() { + private val removeLeadingNewlineForInlineElement = object : AbstractMarkwonPlugin() { override fun afterSetText(textView: TextView) { super.afterSetText(textView) val text = SpannableStringBuilder(textView.text.toSpannable()) - val inlineCodeSpans = text.getSpans(0, textView.length(), InlineCodeSpan::class.java).toList() - val legacyInlineCodeSpans = text.getSpans(0, textView.length(), HtmlCodeSpan::class.java).filter { !it.isBlock } - val spans = inlineCodeSpans + legacyInlineCodeSpans + val length = textView.length() + val spans = arrayOf( + InlineCodeSpan::class.java, + EmphasisSpan::class.java, + CustomTypefaceSpan::class.java, + StrongEmphasisSpan::class.java, + UnderlineSpan::class.java, + URLSpan::class.java, + StrikethroughSpan::class.java + ).map { text.getSpans(0, length, it) } + .toTypedArray() + .plus(text.getSpans(0, length, HtmlCodeSpan::class.java).filter { !it.isBlock }.toTypedArray()) + .flatten() if (spans.isEmpty()) return @@ -179,11 +194,11 @@ class EventHtmlRenderer @Inject constructor( private val markwon = Markwon.builder(context) .usePlugin(HtmlRootTagPlugin()) .usePlugin(HtmlPlugin.create(htmlConfigure)) - .usePlugin(removeLeadingNewlineForInlineCode) + .usePlugin(removeLeadingNewlineForInlineElement) .usePlugin(glidePlugin) .apply { if (vectorPreferences.latexMathsIsEnabled()) { - // If latex maths is enabled in app preferences, refomat it so Markwon recognises it + // If latex maths is enabled in app preferences, reformat it so Markwon recognises it // It needs to be in this specific format: https://noties.io/Markwon/docs/v4/ext-latex latexPlugins.forEach(::usePlugin) } @@ -240,6 +255,7 @@ class MatrixHtmlPluginConfigure @Inject constructor( private val colorProvider: ColorProvider, private val resources: Resources, private val vectorPreferences: VectorPreferences, + private val dimensionConverter: DimensionConverter, ) : HtmlPlugin.HtmlConfigure { override fun configureHtml(plugin: HtmlPlugin) { @@ -248,7 +264,7 @@ class MatrixHtmlPluginConfigure @Inject constructor( .addHandler(FontTagHandler()) .addHandler(ParagraphHandler(DimensionConverter(resources))) .addHandler(MxReplyTagHandler()) - .addHandler(CodePostProcessorTagHandler(vectorPreferences)) + .addHandler(CodePostProcessorTagHandler(vectorPreferences, dimensionConverter)) .addHandler(CodePreTagHandler()) .addHandler(CodeTagHandler()) .addHandler(SpanHandler(colorProvider)) diff --git a/vector/src/main/java/im/vector/app/features/html/HtmlCodeHandlers.kt b/vector/src/main/java/im/vector/app/features/html/HtmlCodeHandlers.kt index 295b74c7a9..3175996ba1 100644 --- a/vector/src/main/java/im/vector/app/features/html/HtmlCodeHandlers.kt +++ b/vector/src/main/java/im/vector/app/features/html/HtmlCodeHandlers.kt @@ -16,7 +16,9 @@ package im.vector.app.features.html +import im.vector.app.core.utils.DimensionConverter import im.vector.app.features.settings.VectorPreferences +import io.element.android.wysiwyg.spans.CodeBlockSpan import io.element.android.wysiwyg.spans.InlineCodeSpan import io.noties.markwon.MarkwonVisitor import io.noties.markwon.SpannableBuilder @@ -68,6 +70,7 @@ internal class CodePreTagHandler : TagHandler() { internal class CodePostProcessorTagHandler( private val vectorPreferences: VectorPreferences, + private val dimensionConverter: DimensionConverter, ) : TagHandler() { override fun supportedTags() = listOf(HtmlRootTagPlugin.ROOT_TAG_NAME) @@ -90,6 +93,7 @@ internal class CodePostProcessorTagHandler( val intermediateCodeSpan = code.what as IntermediateCodeSpan val theme = visitor.configuration().theme() val span = intermediateCodeSpan.toFinalCodeSpan(theme) + SpannableBuilder.setSpans( visitor.builder(), span, code.start, code.end ) @@ -98,9 +102,15 @@ internal class CodePostProcessorTagHandler( private fun IntermediateCodeSpan.toFinalCodeSpan( markwonTheme: MarkwonTheme - ): Any = if (vectorPreferences.isRichTextEditorEnabled() && !isBlock) { - InlineCodeSpan() + ): Any = if (vectorPreferences.isRichTextEditorEnabled()) { + toRichTextEditorSpan() } else { HtmlCodeSpan(markwonTheme, isBlock) } + + private fun IntermediateCodeSpan.toRichTextEditorSpan() = if (isBlock) { + CodeBlockSpan(dimensionConverter.dpToPx(10), dimensionConverter.dpToPx(4)) + } else { + InlineCodeSpan() + } } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationDialog.kt b/vector/src/main/java/im/vector/app/features/location/LocationDialog.kt new file mode 100644 index 0000000000..81ce75e57d --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/LocationDialog.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.location + +import androidx.fragment.app.Fragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import im.vector.app.R + +fun Fragment.showUserLocationNotAvailableErrorDialog(onConfirmListener: () -> Unit) { + MaterialAlertDialogBuilder(requireActivity()) + .setTitle(R.string.location_not_available_dialog_title) + .setMessage(R.string.location_not_available_dialog_content) + .setPositiveButton(R.string.ok) { _, _ -> + onConfirmListener() + } + .setCancelable(false) + .show() +} diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt index 779818b3d6..dea2e08a1f 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt @@ -26,7 +26,9 @@ import androidx.core.content.ContextCompat import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.setFragmentResultListener +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -47,6 +49,7 @@ import im.vector.app.features.location.live.duration.ChooseLiveDurationBottomShe import im.vector.app.features.location.live.tracking.LocationSharingAndroidService import im.vector.app.features.location.option.LocationSharingOption import im.vector.app.features.settings.VectorPreferences +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.util.MatrixItem import java.lang.ref.WeakReference import javax.inject.Inject @@ -97,11 +100,13 @@ class LocationSharingFragment : }.also { views.mapView.addOnDidFailLoadingMapListener(it) } views.mapView.onCreate(savedInstanceState) - lifecycleScope.launchWhenCreated { - views.mapView.initialize( - url = urlMapProvider.getMapUrl(), - locationTargetChangeListener = this@LocationSharingFragment - ) + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) { + views.mapView.initialize( + url = urlMapProvider.getMapUrl(), + locationTargetChangeListener = this@LocationSharingFragment + ) + } } initLocateButton() @@ -176,14 +181,7 @@ class LocationSharingFragment : } private fun handleLocationNotAvailableError() { - MaterialAlertDialogBuilder(requireActivity()) - .setTitle(R.string.location_not_available_dialog_title) - .setMessage(R.string.location_not_available_dialog_content) - .setPositiveButton(R.string.ok) { _, _ -> - locationSharingNavigator.quit() - } - .setCancelable(false) - .show() + showUserLocationNotAvailableErrorDialog { locationSharingNavigator.quit() } } private fun handleLiveLocationSharingNotEnoughPermission() { diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt index c7a2349afa..e11bfbf16e 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt @@ -47,7 +47,7 @@ data class LocationSharingViewState( fun LocationSharingViewState.toMapState() = MapState( zoomOnlyOnce = true, - userLocationData = lastKnownUserLocation, + pinLocationData = lastKnownUserLocation, pinId = DEFAULT_PIN_ID, pinDrawable = null, // show the map pin only when target location and user location are not equal diff --git a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt index c617277f3f..f78b5e4311 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt @@ -66,6 +66,8 @@ class LocationTracker @Inject constructor( @VisibleForTesting var hasLocationFromGPSProvider = false + private var isStarted = false + private var isStarting = false private var firstLocationHandled = false private val _locations = MutableSharedFlow(replay = 1) @@ -90,43 +92,48 @@ class LocationTracker @Inject constructor( @RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION]) fun start() { - Timber.d("start()") + if (!isStarting && !isStarted) { + isStarting = true + Timber.d("start()") - if (locationManager == null) { - Timber.v("LocationManager is not available") - onNoLocationProviderAvailable() - return - } + if (locationManager == null) { + Timber.v("LocationManager is not available") + onNoLocationProviderAvailable() + return + } - val providers = locationManager.allProviders + val providers = locationManager.allProviders - if (providers.isEmpty()) { - Timber.v("There is no location provider available") - onNoLocationProviderAvailable() - } else { - // Take GPS first - providers.sortedByDescending(::getProviderPriority) - .mapNotNull { provider -> - Timber.d("track location using $provider") + if (providers.isEmpty()) { + Timber.v("There is no location provider available") + onNoLocationProviderAvailable() + } else { + // Take GPS first + providers.sortedByDescending(::getProviderPriority) + .mapNotNull { provider -> + Timber.d("track location using $provider") - locationManager.requestLocationUpdates( - provider, - minDurationToUpdateLocationMillis, - MIN_DISTANCE_TO_UPDATE_LOCATION_METERS, - this - ) + locationManager.requestLocationUpdates( + provider, + minDurationToUpdateLocationMillis, + MIN_DISTANCE_TO_UPDATE_LOCATION_METERS, + this + ) - locationManager.getLastKnownLocation(provider) - } - .maxByOrNull { location -> location.time } - ?.let { latestKnownLocation -> - if (buildMeta.lowPrivacyLoggingEnabled) { - Timber.d("lastKnownLocation: $latestKnownLocation") - } else { - Timber.d("lastKnownLocation: ${latestKnownLocation.provider}") + locationManager.getLastKnownLocation(provider) } - notifyLocation(latestKnownLocation) - } + .maxByOrNull { location -> location.time } + ?.let { latestKnownLocation -> + if (buildMeta.lowPrivacyLoggingEnabled) { + Timber.d("lastKnownLocation: $latestKnownLocation") + } else { + Timber.d("lastKnownLocation: ${latestKnownLocation.provider}") + } + notifyLocation(latestKnownLocation) + } + } + isStarted = true + isStarting = false } } @@ -148,6 +155,8 @@ class LocationTracker @Inject constructor( callbacks.clear() hasLocationFromGPSProvider = false hasLocationFromFusedProvider = false + isStarting = false + isStarted = false } /** diff --git a/vector/src/main/java/im/vector/app/features/location/MapBoxMapExt.kt b/vector/src/main/java/im/vector/app/features/location/MapBoxMapExt.kt index 8e917c665a..5a6e69b103 100644 --- a/vector/src/main/java/im/vector/app/features/location/MapBoxMapExt.kt +++ b/vector/src/main/java/im/vector/app/features/location/MapBoxMapExt.kt @@ -17,6 +17,7 @@ package im.vector.app.features.location import com.mapbox.mapboxsdk.camera.CameraPosition +import com.mapbox.mapboxsdk.camera.CameraUpdateFactory import com.mapbox.mapboxsdk.constants.MapboxConstants import com.mapbox.mapboxsdk.geometry.LatLng import com.mapbox.mapboxsdk.geometry.LatLngBounds @@ -28,12 +29,12 @@ fun MapboxMap?.zoomToLocation(locationData: LocationData, preserveCurrentZoomLev } else { INITIAL_MAP_ZOOM_IN_PREVIEW } - this?.easeCamera { - CameraPosition.Builder() - .target(LatLng(locationData.latitude, locationData.longitude)) - .zoom(zoomLevel) - .build() - } + val expectedCameraPosition = CameraPosition.Builder() + .target(LatLng(locationData.latitude, locationData.longitude)) + .zoom(zoomLevel) + .build() + val cameraUpdate = CameraUpdateFactory.newCameraPosition(expectedCameraPosition) + this?.easeCamera(cameraUpdate) } fun MapboxMap?.zoomToBounds(latLngBounds: LatLngBounds) { diff --git a/vector/src/main/java/im/vector/app/features/location/MapState.kt b/vector/src/main/java/im/vector/app/features/location/MapState.kt index c4325291a8..2224317b02 100644 --- a/vector/src/main/java/im/vector/app/features/location/MapState.kt +++ b/vector/src/main/java/im/vector/app/features/location/MapState.kt @@ -21,9 +21,10 @@ import androidx.annotation.Px data class MapState( val zoomOnlyOnce: Boolean, - val userLocationData: LocationData? = null, + val pinLocationData: LocationData? = null, val pinId: String, val pinDrawable: Drawable? = null, val showPin: Boolean = true, - @Px val logoMarginBottom: Int = 0 + val userLocationData: LocationData? = null, + @Px val logoMarginBottom: Int = 0, ) diff --git a/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt b/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt index b001621bf4..d7e3463a5c 100644 --- a/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt +++ b/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt @@ -18,6 +18,7 @@ package im.vector.app.features.location import android.content.Context import android.content.res.TypedArray +import android.graphics.drawable.Drawable import android.util.AttributeSet import android.view.Gravity import android.widget.ImageView @@ -38,6 +39,8 @@ import im.vector.app.R import im.vector.app.core.utils.DimensionConverter import timber.log.Timber +private const val USER_PIN_ID = "user-pin-id" + class MapTilerMapView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, @@ -101,9 +104,11 @@ class MapTilerMapView @JvmOverloads constructor( private fun initMapStyle(map: MapboxMap, url: String) { map.setStyle(url) { style -> + val symbolManager = SymbolManager(this, map, style) + symbolManager.iconAllowOverlap = true mapRefs = MapRefs( map, - SymbolManager(this, map, style), + symbolManager, style ) pendingState?.let { render(it) } @@ -166,29 +171,43 @@ class MapTilerMapView @JvmOverloads constructor( } val pinDrawable = state.pinDrawable ?: userLocationDrawable - pinDrawable?.let { drawable -> - if (!safeMapRefs.style.isFullyLoaded || - safeMapRefs.style.getImage(state.pinId) == null) { - safeMapRefs.style.addImage(state.pinId, drawable.toBitmap()) - } - } + addImageToMapStyle(pinDrawable, state.pinId, safeMapRefs) - state.userLocationData?.let { locationData -> + safeMapRefs.symbolManager.deleteAll() + state.pinLocationData?.let { locationData -> if (!initZoomDone || !state.zoomOnlyOnce) { zoomToLocation(locationData) initZoomDone = true } - safeMapRefs.symbolManager.deleteAll() if (pinDrawable != null && state.showPin) { - safeMapRefs.symbolManager.create( - SymbolOptions() - .withLatLng(LatLng(locationData.latitude, locationData.longitude)) - .withIconImage(state.pinId) - .withIconAnchor(Property.ICON_ANCHOR_BOTTOM) - ) + createSymbol(locationData, state.pinId, safeMapRefs) } } + + state.userLocationData?.let { locationData -> + addImageToMapStyle(userLocationDrawable, USER_PIN_ID, safeMapRefs) + if (userLocationDrawable != null) { + createSymbol(locationData, USER_PIN_ID, safeMapRefs) + } + } + } + + private fun addImageToMapStyle(image: Drawable?, imageId: String, mapRefs: MapRefs) { + image?.let { drawable -> + if (!mapRefs.style.isFullyLoaded || mapRefs.style.getImage(imageId) == null) { + mapRefs.style.addImage(imageId, drawable.toBitmap()) + } + } + } + + private fun createSymbol(locationData: LocationData, imageId: String, mapRefs: MapRefs) { + mapRefs.symbolManager.create( + SymbolOptions() + .withLatLng(LatLng(locationData.latitude, locationData.longitude)) + .withIconImage(imageId) + .withIconAnchor(Property.ICON_ANCHOR_BOTTOM) + ) } fun zoomToLocation(locationData: LocationData) { diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapAction.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapAction.kt index 295d6b5d41..4bb86c8f53 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapAction.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapAction.kt @@ -23,4 +23,5 @@ sealed class LiveLocationMapAction : VectorViewModelAction { data class RemoveMapSymbol(val key: String) : LiveLocationMapAction() object StopSharing : LiveLocationMapAction() object ShowMapLoadingError : LiveLocationMapAction() + object ZoomToUserLocation : LiveLocationMapAction() } diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapViewEvents.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapViewEvents.kt index 2c4f34dce0..89a300a2e2 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapViewEvents.kt @@ -17,7 +17,10 @@ package im.vector.app.features.location.live.map import im.vector.app.core.platform.VectorViewEvents +import im.vector.app.features.location.LocationData sealed interface LiveLocationMapViewEvents : VectorViewEvents { - data class Error(val error: Throwable) : LiveLocationMapViewEvents + data class LiveLocationError(val error: Throwable) : LiveLocationMapViewEvents + data class ZoomToUserLocation(val userLocation: LocationData) : LiveLocationMapViewEvents + object UserLocationNotAvailableError : LiveLocationMapViewEvents } diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapViewFragment.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapViewFragment.kt index 942021dd64..3c02d5d87d 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapViewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapViewFragment.kt @@ -24,6 +24,8 @@ import android.view.ViewGroup import androidx.core.graphics.drawable.toBitmap import androidx.core.view.isGone import androidx.core.view.isVisible +import androidx.core.view.marginBottom +import androidx.core.view.marginTop import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState @@ -46,11 +48,17 @@ import im.vector.app.core.extensions.addChildFragment import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.resources.DrawableProvider import im.vector.app.core.utils.DimensionConverter +import im.vector.app.core.utils.PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING +import im.vector.app.core.utils.checkPermissions +import im.vector.app.core.utils.onPermissionDeniedDialog import im.vector.app.core.utils.openLocation +import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.databinding.FragmentLiveLocationMapViewBinding import im.vector.app.features.location.LocationData import im.vector.app.features.location.UrlMapProvider +import im.vector.app.features.location.showUserLocationNotAvailableErrorDialog import im.vector.app.features.location.zoomToBounds import im.vector.app.features.location.zoomToLocation import kotlinx.coroutines.launch @@ -58,6 +66,8 @@ import timber.log.Timber import java.lang.ref.WeakReference import javax.inject.Inject +private const val USER_LOCATION_PIN_ID = "user-location-pin-id" + /** * Screen showing a map with all the current users sharing their live location in a room. */ @@ -68,6 +78,7 @@ class LiveLocationMapViewFragment : @Inject lateinit var urlMapProvider: UrlMapProvider @Inject lateinit var bottomSheetController: LiveLocationBottomSheetController @Inject lateinit var dimensionConverter: DimensionConverter + @Inject lateinit var drawableProvider: DrawableProvider private val viewModel: LiveLocationMapViewModel by fragmentViewModel() @@ -75,7 +86,7 @@ class LiveLocationMapViewFragment : private var mapView: MapView? = null private var symbolManager: SymbolManager? = null private var mapStyle: Style? = null - private val pendingLiveLocations = mutableListOf() + private val userLocationDrawable by lazy { drawableProvider.getDrawable(R.drawable.ic_location_user) } private var isMapFirstUpdate = true private var onSymbolClickListener: OnSymbolClickListener? = null private var mapLoadingErrorListener: MapView.OnDidFailLoadingMapListener? = null @@ -88,6 +99,7 @@ class LiveLocationMapViewFragment : super.onViewCreated(view, savedInstanceState) observeViewEvents() setupMap() + initLocateButton() views.liveLocationBottomSheetRecyclerView.configureWith(bottomSheetController, hasFixedSize = false, disableItemAnimation = true) @@ -105,11 +117,23 @@ class LiveLocationMapViewFragment : private fun observeViewEvents() { viewModel.observeViewEvents { viewEvent -> when (viewEvent) { - is LiveLocationMapViewEvents.Error -> displayErrorDialog(viewEvent.error) + is LiveLocationMapViewEvents.LiveLocationError -> displayErrorDialog(viewEvent.error) + is LiveLocationMapViewEvents.ZoomToUserLocation -> handleZoomToUserLocationEvent(viewEvent) + LiveLocationMapViewEvents.UserLocationNotAvailableError -> handleUserLocationNotAvailableError() } } } + private fun handleZoomToUserLocationEvent(event: LiveLocationMapViewEvents.ZoomToUserLocation) { + mapboxMap?.get().zoomToLocation(event.userLocation) + } + + private fun handleUserLocationNotAvailableError() { + showUserLocationNotAvailableErrorDialog { + // do nothing + } + } + override fun onDestroyView() { onSymbolClickListener?.let { symbolManager?.removeClickListener(it) } symbolManager?.onDestroy() @@ -139,14 +163,33 @@ class LiveLocationMapViewFragment : true }.also { addClickListener(it) } } - pendingLiveLocations - .takeUnless { it.isEmpty() } - ?.let { updateMap(it) } + // force refresh of the map using the last viewState + invalidate() } } } } + private fun initLocateButton() { + views.liveLocationMapLocateButton.setOnClickListener { + if (checkPermissions(PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING, requireActivity(), foregroundLocationResultLauncher)) { + zoomToUserLocation() + } + } + } + + private fun zoomToUserLocation() { + viewModel.handle(LiveLocationMapAction.ZoomToUserLocation) + } + + private val foregroundLocationResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> + if (allGranted) { + zoomToUserLocation() + } else if (deniedPermanently) { + activity?.onPermissionDeniedDialog(R.string.denied_permission_generic) + } + } + private fun listenMapLoadingError(mapView: MapView) { mapLoadingErrorListener = MapView.OnDidFailLoadingMapListener { viewModel.handle(LiveLocationMapAction.ShowMapLoadingError) @@ -189,9 +232,15 @@ class LiveLocationMapViewFragment : views.mapPreviewLoadingError.isVisible = true } else { views.mapPreviewLoadingError.isGone = true - updateMap(viewState.userLocations) + updateMap(userLiveLocations = viewState.userLocations, userLocation = viewState.lastKnownUserLocation) + } + if (viewState.isLoadingUserLocation) { + showLoadingDialog() + } else { + dismissLoadingDialog() } updateUserListBottomSheet(viewState.userLocations) + updateLocateButton(showLocateButton = viewState.showLocateUserButton) } private fun updateUserListBottomSheet(userLocations: List) { @@ -236,7 +285,24 @@ class LiveLocationMapViewFragment : } } - private fun updateMap(userLiveLocations: List) { + private fun updateLocateButton(showLocateButton: Boolean) { + views.liveLocationMapLocateButton.isVisible = showLocateButton + adjustCompassButton() + } + + private fun adjustCompassButton() { + val locateButton = views.liveLocationMapLocateButton + locateButton.post { + val marginTop = locateButton.height + locateButton.marginTop + locateButton.marginBottom + val marginRight = locateButton.context.resources.getDimensionPixelOffset(R.dimen.location_sharing_compass_button_margin_horizontal) + mapboxMap?.get()?.uiSettings?.setCompassMargins(0, marginTop, marginRight, 0) + } + } + + private fun updateMap( + userLiveLocations: List, + userLocation: LocationData?, + ) { symbolManager?.let { sManager -> val latLngBoundsBuilder = LatLngBounds.Builder() userLiveLocations.forEach { userLocation -> @@ -249,28 +315,60 @@ class LiveLocationMapViewFragment : removeOutdatedSymbols(userLiveLocations, sManager) updateMapZoomWhenNeeded(userLiveLocations, latLngBoundsBuilder) - } ?: postponeUpdateOfMap(userLiveLocations) - } - - private fun createOrUpdateSymbol(userLocation: UserLiveLocationViewState, symbolManager: SymbolManager) = withState(viewModel) { state -> - val symbolId = state.mapSymbolIds[userLocation.matrixItem.id] - - if (symbolId == null || symbolManager.annotations.get(symbolId) == null) { - createSymbol(userLocation, symbolManager) - } else { - updateSymbol(symbolId, userLocation, symbolManager) + if (userLocation == null) { + removeUserSymbol(sManager) + } else { + createOrUpdateUserSymbol(userLocation, sManager) + } } } - private fun createSymbol(userLocation: UserLiveLocationViewState, symbolManager: SymbolManager) { - addUserPinToMapStyle(userLocation.matrixItem.id, userLocation.pinDrawable) - val symbolOptions = buildSymbolOptions(userLocation) - val symbol = symbolManager.create(symbolOptions) - viewModel.handle(LiveLocationMapAction.AddMapSymbol(userLocation.matrixItem.id, symbol.id)) + private fun createOrUpdateSymbol(userLocation: UserLiveLocationViewState, symbolManager: SymbolManager) { + val pinId = userLocation.matrixItem.id + val pinDrawable = userLocation.pinDrawable + createOrUpdateSymbol(pinId, pinDrawable, userLocation.locationData, symbolManager) } - private fun updateSymbol(symbolId: Long, userLocation: UserLiveLocationViewState, symbolManager: SymbolManager) { - val newLocation = LatLng(userLocation.locationData.latitude, userLocation.locationData.longitude) + private fun createOrUpdateUserSymbol(locationData: LocationData, symbolManager: SymbolManager) { + userLocationDrawable?.let { pinDrawable -> createOrUpdateSymbol(USER_LOCATION_PIN_ID, pinDrawable, locationData, symbolManager) } + } + + private fun removeUserSymbol(symbolManager: SymbolManager) = withState(viewModel) { state -> + val pinId = USER_LOCATION_PIN_ID + state.mapSymbolIds[pinId]?.let { symbolId -> + removeSymbol(pinId, symbolId, symbolManager) + } + } + + private fun createOrUpdateSymbol( + pinId: String, + pinDrawable: Drawable, + locationData: LocationData, + symbolManager: SymbolManager + ) = withState(viewModel) { state -> + val symbolId = state.mapSymbolIds[pinId] + + if (symbolId == null || symbolManager.annotations.get(symbolId) == null) { + createSymbol(pinId, pinDrawable, locationData, symbolManager) + } else { + updateSymbol(symbolId, locationData, symbolManager) + } + } + + private fun createSymbol( + pinId: String, + pinDrawable: Drawable, + locationData: LocationData, + symbolManager: SymbolManager + ) { + addPinToMapStyle(pinId, pinDrawable) + val symbolOptions = buildSymbolOptions(locationData, pinId) + val symbol = symbolManager.create(symbolOptions) + viewModel.handle(LiveLocationMapAction.AddMapSymbol(pinId, symbol.id)) + } + + private fun updateSymbol(symbolId: Long, locationData: LocationData, symbolManager: SymbolManager) { + val newLocation = LatLng(locationData.latitude, locationData.longitude) val symbol = symbolManager.annotations.get(symbolId) symbol?.let { it.latLng = newLocation @@ -279,17 +377,11 @@ class LiveLocationMapViewFragment : } private fun removeOutdatedSymbols(userLiveLocations: List, symbolManager: SymbolManager) = withState(viewModel) { state -> - val userIdsToRemove = state.mapSymbolIds.keys.subtract(userLiveLocations.map { it.matrixItem.id }.toSet()) - userIdsToRemove.forEach { userId -> - removeUserPinFromMapStyle(userId) - viewModel.handle(LiveLocationMapAction.RemoveMapSymbol(userId)) - - state.mapSymbolIds[userId]?.let { symbolId -> - Timber.d("trying to delete symbol with id: $symbolId") - symbolManager.annotations.get(symbolId)?.let { - symbolManager.delete(it) - } - } + val pinIdsToKeep = userLiveLocations.map { it.matrixItem.id } + USER_LOCATION_PIN_ID + val pinIdsToRemove = state.mapSymbolIds.keys.subtract(pinIdsToKeep.toSet()) + pinIdsToRemove.forEach { pinId -> + val symbolId = state.mapSymbolIds[pinId] + removeSymbol(pinId, symbolId, symbolManager) } } @@ -304,27 +396,35 @@ class LiveLocationMapViewFragment : } } - private fun postponeUpdateOfMap(userLiveLocations: List) { - pendingLiveLocations.clear() - pendingLiveLocations.addAll(userLiveLocations) - } - - private fun addUserPinToMapStyle(userId: String, userPinDrawable: Drawable) { + private fun addPinToMapStyle(pinId: String, pinDrawable: Drawable) { mapStyle?.let { style -> - if (style.getImage(userId) == null) { - style.addImage(userId, userPinDrawable.toBitmap()) + if (style.getImage(pinId) == null) { + style.addImage(pinId, pinDrawable.toBitmap()) } } } - private fun removeUserPinFromMapStyle(userId: String) { - mapStyle?.removeImage(userId) + private fun removeSymbol(pinId: String, symbolId: Long?, symbolManager: SymbolManager) { + removeUserPinFromMapStyle(pinId) + + symbolId?.let { id -> + Timber.d("trying to delete symbol with id: $id") + symbolManager.annotations.get(id)?.let { + symbolManager.delete(it) + } + } + + viewModel.handle(LiveLocationMapAction.RemoveMapSymbol(pinId)) } - private fun buildSymbolOptions(userLiveLocation: UserLiveLocationViewState) = + private fun removeUserPinFromMapStyle(pinId: String) { + mapStyle?.removeImage(pinId) + } + + private fun buildSymbolOptions(locationData: LocationData, pinId: String) = SymbolOptions() - .withLatLng(LatLng(userLiveLocation.locationData.latitude, userLiveLocation.locationData.longitude)) - .withIconImage(userLiveLocation.matrixItem.id) + .withLatLng(LatLng(locationData.latitude, locationData.longitude)) + .withIconImage(pinId) .withIconAnchor(Property.ICON_ANCHOR_BOTTOM) private fun handleBottomSheetUserSelected(userId: String) = withState(viewModel) { state -> diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapViewModel.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapViewModel.kt index 33c584ff85..15e41470e0 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapViewModel.kt @@ -23,19 +23,27 @@ import dagger.assisted.AssistedInject import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel +import im.vector.app.features.location.LocationData +import im.vector.app.features.location.LocationTracker import im.vector.app.features.location.live.StopLiveLocationShareUseCase import im.vector.app.features.location.live.tracking.LocationSharingServiceConnection import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult class LiveLocationMapViewModel @AssistedInject constructor( @Assisted private val initialState: LiveLocationMapViewState, + private val session: Session, getListOfUserLiveLocationUseCase: GetListOfUserLiveLocationUseCase, private val locationSharingServiceConnection: LocationSharingServiceConnection, private val stopLiveLocationShareUseCase: StopLiveLocationShareUseCase, -) : VectorViewModel(initialState), LocationSharingServiceConnection.Callback { + private val locationTracker: LocationTracker, +) : + VectorViewModel(initialState), + LocationSharingServiceConnection.Callback, + LocationTracker.Callback { @AssistedFactory interface Factory : MavericksAssistedViewModelFactory { @@ -46,12 +54,37 @@ class LiveLocationMapViewModel @AssistedInject constructor( init { getListOfUserLiveLocationUseCase.execute(initialState.roomId) - .onEach { setState { copy(userLocations = it) } } + .onEach { setState { copy(userLocations = it, showLocateUserButton = it.none { it.matrixItem.id == session.myUserId }) } } .launchIn(viewModelScope) locationSharingServiceConnection.bind(this) + initLocationTracking() + } + + private fun initLocationTracking() { + locationTracker.addCallback(this) + locationTracker.locations + .onEach(::onLocationUpdate) + .launchIn(viewModelScope) + } + + private fun onLocationUpdate(locationData: LocationData) = withState { state -> + val zoomToUserLocation = state.isLoadingUserLocation + val showLocateButton = state.showLocateUserButton + + setState { + copy( + lastKnownUserLocation = if (showLocateButton) locationData else null, + isLoadingUserLocation = false, + ) + } + + if (zoomToUserLocation) { + _viewEvents.post(LiveLocationMapViewEvents.ZoomToUserLocation(locationData)) + } } override fun onCleared() { + locationTracker.removeCallback(this) locationSharingServiceConnection.unbind(this) super.onCleared() } @@ -62,6 +95,7 @@ class LiveLocationMapViewModel @AssistedInject constructor( is LiveLocationMapAction.RemoveMapSymbol -> handleRemoveMapSymbol(action) LiveLocationMapAction.StopSharing -> handleStopSharing() LiveLocationMapAction.ShowMapLoadingError -> handleShowMapLoadingError() + LiveLocationMapAction.ZoomToUserLocation -> handleZoomToUserLocation() } } @@ -83,7 +117,7 @@ class LiveLocationMapViewModel @AssistedInject constructor( viewModelScope.launch { val result = stopLiveLocationShareUseCase.execute(initialState.roomId) if (result is UpdateLiveLocationShareResult.Failure) { - _viewEvents.post(LiveLocationMapViewEvents.Error(result.error)) + _viewEvents.post(LiveLocationMapViewEvents.LiveLocationError(result.error)) } } } @@ -92,6 +126,18 @@ class LiveLocationMapViewModel @AssistedInject constructor( setState { copy(loadingMapHasFailed = true) } } + private fun handleZoomToUserLocation() = withState { state -> + if (!state.isLoadingUserLocation) { + setState { + copy(isLoadingUserLocation = true) + } + viewModelScope.launch(session.coroutineDispatchers.main) { + locationTracker.start() + locationTracker.requestLastKnownLocation() + } + } + } + override fun onLocationServiceRunning(roomIds: Set) { // NOOP } @@ -101,6 +147,10 @@ class LiveLocationMapViewModel @AssistedInject constructor( } override fun onLocationServiceError(error: Throwable) { - _viewEvents.post(LiveLocationMapViewEvents.Error(error)) + _viewEvents.post(LiveLocationMapViewEvents.LiveLocationError(error)) + } + + override fun onNoLocationProviderAvailable() { + _viewEvents.post(LiveLocationMapViewEvents.UserLocationNotAvailableError) } } diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapViewState.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapViewState.kt index ddd1cd2369..74b0023a08 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapViewState.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapViewState.kt @@ -29,6 +29,9 @@ data class LiveLocationMapViewState( */ val mapSymbolIds: Map = emptyMap(), val loadingMapHasFailed: Boolean = false, + val showLocateUserButton: Boolean = false, + val isLoadingUserLocation: Boolean = false, + val lastKnownUserLocation: LocationData? = null, ) : MavericksState { constructor(liveLocationMapViewArgs: LiveLocationMapViewArgs) : this( roomId = liveLocationMapViewArgs.roomId diff --git a/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewAction.kt b/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewAction.kt index 38f6952f67..094c2206fa 100644 --- a/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewAction.kt +++ b/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewAction.kt @@ -20,4 +20,5 @@ import im.vector.app.core.platform.VectorViewModelAction sealed class LocationPreviewAction : VectorViewModelAction { object ShowMapLoadingError : LocationPreviewAction() + object ZoomToUserLocation : LocationPreviewAction() } diff --git a/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewFragment.kt b/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewFragment.kt index 082cee02f0..aa4be84985 100644 --- a/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewFragment.kt @@ -22,7 +22,9 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState @@ -31,18 +33,23 @@ import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorMenuProvider +import im.vector.app.core.utils.PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING +import im.vector.app.core.utils.checkPermissions +import im.vector.app.core.utils.onPermissionDeniedDialog import im.vector.app.core.utils.openLocation +import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.databinding.FragmentLocationPreviewBinding -import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider import im.vector.app.features.location.DEFAULT_PIN_ID import im.vector.app.features.location.LocationSharingArgs import im.vector.app.features.location.MapState import im.vector.app.features.location.UrlMapProvider +import im.vector.app.features.location.showUserLocationNotAvailableErrorDialog +import kotlinx.coroutines.launch import java.lang.ref.WeakReference import javax.inject.Inject -/* - * TODO Move locationPinProvider to a ViewModel +/** + * Screen displaying the expanded map of a static location share. */ @AndroidEntryPoint class LocationPreviewFragment : @@ -50,7 +57,6 @@ class LocationPreviewFragment : VectorMenuProvider { @Inject lateinit var urlMapProvider: UrlMapProvider - @Inject lateinit var locationPinProvider: LocationPinProvider private val args: LocationSharingArgs by args() @@ -74,10 +80,33 @@ class LocationPreviewFragment : }.also { views.mapView.addOnDidFailLoadingMapListener(it) } views.mapView.onCreate(savedInstanceState) - lifecycleScope.launchWhenCreated { - views.mapView.initialize(urlMapProvider.getMapUrl()) - loadPinDrawable() + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) { + views.mapView.initialize(urlMapProvider.getMapUrl()) + } } + + observeViewEvents() + initLocateButton() + } + + private fun observeViewEvents() { + viewModel.observeViewEvents { + when (it) { + LocationPreviewViewEvents.UserLocationNotAvailableError -> handleUserLocationNotAvailableError() + is LocationPreviewViewEvents.ZoomToUserLocation -> handleZoomToUserLocationEvent(it) + } + } + } + + private fun handleUserLocationNotAvailableError() { + showUserLocationNotAvailableErrorDialog { + // do nothing + } + } + + private fun handleZoomToUserLocationEvent(event: LocationPreviewViewEvents.ZoomToUserLocation) { + views.mapView.zoomToLocation(event.userLocation) } override fun onDestroyView() { @@ -124,6 +153,24 @@ class LocationPreviewFragment : override fun invalidate() = withState(viewModel) { state -> views.mapPreviewLoadingError.isVisible = state.loadingMapHasFailed + if (state.isLoadingUserLocation) { + showLoadingDialog() + } else { + dismissLoadingDialog() + } + updateMap(state) + } + + private fun updateMap(viewState: LocationPreviewViewState) { + views.mapView.render( + MapState( + zoomOnlyOnce = true, + pinLocationData = viewState.pinLocationData, + pinId = viewState.pinUserId ?: DEFAULT_PIN_ID, + pinDrawable = viewState.pinDrawable, + userLocationData = viewState.lastKnownUserLocation, + ) + ) } override fun getMenuRes() = R.menu.menu_location_preview @@ -143,21 +190,23 @@ class LocationPreviewFragment : openLocation(requireActivity(), location.latitude, location.longitude) } - private fun loadPinDrawable() { - val location = args.initialLocationData ?: return - val userId = args.locationOwnerId - - locationPinProvider.create(userId) { pinDrawable -> - lifecycleScope.launchWhenResumed { - views.mapView.render( - MapState( - zoomOnlyOnce = true, - userLocationData = location, - pinId = args.locationOwnerId ?: DEFAULT_PIN_ID, - pinDrawable = pinDrawable - ) - ) + private fun initLocateButton() { + views.mapView.locateButton.setOnClickListener { + if (checkPermissions(PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING, requireActivity(), foregroundLocationResultLauncher)) { + zoomToUserLocation() } } } + + private fun zoomToUserLocation() { + viewModel.handle(LocationPreviewAction.ZoomToUserLocation) + } + + private val foregroundLocationResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> + if (allGranted) { + zoomToUserLocation() + } else if (deniedPermanently) { + activity?.onPermissionDeniedDialog(R.string.denied_permission_generic) + } + } } diff --git a/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewViewEvents.kt b/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewViewEvents.kt new file mode 100644 index 0000000000..605c240d06 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewViewEvents.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.location.preview + +import im.vector.app.core.platform.VectorViewEvents +import im.vector.app.features.location.LocationData + +sealed class LocationPreviewViewEvents : VectorViewEvents { + data class ZoomToUserLocation(val userLocation: LocationData) : LocationPreviewViewEvents() + object UserLocationNotAvailableError : LocationPreviewViewEvents() +} diff --git a/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewViewModel.kt b/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewViewModel.kt index f0698249ce..a1544ac2af 100644 --- a/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewViewModel.kt @@ -22,12 +22,21 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory -import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel +import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider +import im.vector.app.features.location.LocationData +import im.vector.app.features.location.LocationTracker +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.session.Session class LocationPreviewViewModel @AssistedInject constructor( @Assisted private val initialState: LocationPreviewViewState, -) : VectorViewModel(initialState) { + private val session: Session, + private val locationPinProvider: LocationPinProvider, + private val locationTracker: LocationTracker, +) : VectorViewModel(initialState), LocationTracker.Callback { @AssistedFactory interface Factory : MavericksAssistedViewModelFactory { @@ -36,13 +45,68 @@ class LocationPreviewViewModel @AssistedInject constructor( companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + init { + initPin(initialState.pinUserId) + initLocationTracking() + } + + private fun initPin(userId: String?) { + locationPinProvider.create(userId) { pinDrawable -> + setState { copy(pinDrawable = pinDrawable) } + } + } + + private fun initLocationTracking() { + locationTracker.addCallback(this) + locationTracker.locations + .onEach(::onLocationUpdate) + .launchIn(viewModelScope) + } + + override fun onCleared() { + super.onCleared() + locationTracker.removeCallback(this) + } + override fun handle(action: LocationPreviewAction) { when (action) { LocationPreviewAction.ShowMapLoadingError -> handleShowMapLoadingError() + LocationPreviewAction.ZoomToUserLocation -> handleZoomToUserLocationAction() } } private fun handleShowMapLoadingError() { setState { copy(loadingMapHasFailed = true) } } + + private fun handleZoomToUserLocationAction() = withState { state -> + if (!state.isLoadingUserLocation) { + setState { + copy(isLoadingUserLocation = true) + } + viewModelScope.launch(session.coroutineDispatchers.main) { + locationTracker.start() + locationTracker.requestLastKnownLocation() + } + } + } + + override fun onNoLocationProviderAvailable() { + _viewEvents.post(LocationPreviewViewEvents.UserLocationNotAvailableError) + } + + private fun onLocationUpdate(locationData: LocationData) = withState { state -> + val zoomToUserLocation = state.isLoadingUserLocation + + setState { + copy( + lastKnownUserLocation = locationData, + isLoadingUserLocation = false, + ) + } + + if (zoomToUserLocation) { + _viewEvents.post(LocationPreviewViewEvents.ZoomToUserLocation(locationData)) + } + } } diff --git a/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewViewState.kt b/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewViewState.kt index 96e8316323..23f8d4d7dc 100644 --- a/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewViewState.kt +++ b/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewViewState.kt @@ -16,8 +16,22 @@ package im.vector.app.features.location.preview +import android.graphics.drawable.Drawable import com.airbnb.mvrx.MavericksState +import im.vector.app.features.location.LocationData +import im.vector.app.features.location.LocationSharingArgs data class LocationPreviewViewState( - val loadingMapHasFailed: Boolean = false -) : MavericksState + val pinLocationData: LocationData? = null, + val pinUserId: String? = null, + val pinDrawable: Drawable? = null, + val loadingMapHasFailed: Boolean = false, + val isLoadingUserLocation: Boolean = false, + val lastKnownUserLocation: LocationData? = null, +) : MavericksState { + + constructor(args: LocationSharingArgs) : this( + pinLocationData = args.initialLocationData, + pinUserId = args.locationOwnerId, + ) +} diff --git a/vector/src/main/java/im/vector/app/features/login/AbstractSSOLoginFragment.kt b/vector/src/main/java/im/vector/app/features/login/AbstractSSOLoginFragment.kt index ddab65d981..77bcaed3fb 100644 --- a/vector/src/main/java/im/vector/app/features/login/AbstractSSOLoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/AbstractSSOLoginFragment.kt @@ -24,6 +24,7 @@ import androidx.browser.customtabs.CustomTabsSession import androidx.viewbinding.ViewBinding import com.airbnb.mvrx.withState import im.vector.app.core.utils.openUrlInChromeCustomTab +import org.matrix.android.sdk.api.auth.SSOAction abstract class AbstractSSOLoginFragment : AbstractLoginFragment() { @@ -90,7 +91,8 @@ abstract class AbstractSSOLoginFragment : AbstractLoginFragmen loginViewModel.getSsoUrl( redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, deviceId = state.deviceId, - providerId = null + providerId = null, + action = if (state.signMode == SignMode.SignUp) SSOAction.REGISTER else SSOAction.LOGIN ) ?.let { prefetchUrl(it) } } diff --git a/vector/src/main/java/im/vector/app/features/login/HomeServerConnectionConfigFactory.kt b/vector/src/main/java/im/vector/app/features/login/HomeServerConnectionConfigFactory.kt index 253c514e5a..c8cd6df4ca 100644 --- a/vector/src/main/java/im/vector/app/features/login/HomeServerConnectionConfigFactory.kt +++ b/vector/src/main/java/im/vector/app/features/login/HomeServerConnectionConfigFactory.kt @@ -23,7 +23,7 @@ import javax.inject.Inject class HomeServerConnectionConfigFactory @Inject constructor() { - fun create(url: String?, fingerprint: Fingerprint? = null): HomeServerConnectionConfig? { + fun create(url: String?, fingerprints: List? = null): HomeServerConnectionConfig? { if (url == null) { return null } @@ -31,13 +31,7 @@ class HomeServerConnectionConfigFactory @Inject constructor() { return try { HomeServerConnectionConfig.Builder() .withHomeServerUri(url) - .run { - if (fingerprint == null) { - this - } else { - withAllowedFingerPrints(listOf(fingerprint)) - } - } + .withAllowedFingerPrints(fingerprints) .build() } catch (t: Throwable) { Timber.e(t) diff --git a/vector/src/main/java/im/vector/app/features/login/LoginAction.kt b/vector/src/main/java/im/vector/app/features/login/LoginAction.kt index 5947fa0cb5..984c3694e8 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginAction.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginAction.kt @@ -69,7 +69,8 @@ sealed class LoginAction : VectorViewModelAction { data class SetupSsoForSessionRecovery( val homeServerUrl: String, val deviceId: String, - val ssoIdentityProviders: List? + val ssoIdentityProviders: List?, + val hasOidcCompatibilityFlow: Boolean ) : LoginAction() data class PostViewEvent(val viewEvent: LoginViewEvents) : LoginAction() diff --git a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt index 4e4df5d1aa..9dfae7ff5f 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt @@ -46,6 +46,7 @@ import im.vector.app.features.login.terms.LoginTermsFragmentArgument import im.vector.app.features.onboarding.AuthenticationDescription import im.vector.app.features.pin.UnlockedActivity import im.vector.lib.core.utils.compat.getParcelableExtraCompat +import org.matrix.android.sdk.api.auth.SSOAction import org.matrix.android.sdk.api.auth.registration.FlowResult import org.matrix.android.sdk.api.auth.registration.Stage import org.matrix.android.sdk.api.auth.toLocalizedLoginTerms @@ -300,6 +301,7 @@ open class LoginActivity : VectorBaseActivity(), UnlockedA redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, deviceId = state.deviceId, providerId = null, + action = SSOAction.LOGIN )?.let { ssoUrl -> openUrlInChromeCustomTab(this, null, ssoUrl) } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt index d61044d101..01d15db3d1 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt @@ -38,6 +38,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import org.matrix.android.sdk.api.auth.SSOAction import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.failure.isInvalidPassword @@ -200,11 +201,12 @@ class LoginFragment : if (state.loginMode is LoginMode.SsoAndPassword) { views.loginSocialLoginContainer.isVisible = true - views.loginSocialLoginButtons.render(state.loginMode.ssoState, ssoMode(state)) { provider -> + views.loginSocialLoginButtons.render(state.loginMode, ssoMode(state)) { provider -> loginViewModel.getSsoUrl( redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, deviceId = state.deviceId, - providerId = provider?.id + providerId = provider?.id, + action = if (state.signMode == SignMode.SignUp) SSOAction.REGISTER else SSOAction.LOGIN ) ?.let { openInCustomTab(it) } } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginMode.kt b/vector/src/main/java/im/vector/app/features/login/LoginMode.kt index 944b159441..384108e6a8 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginMode.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginMode.kt @@ -23,8 +23,8 @@ sealed class LoginMode : Parcelable { // Parcelable because persist state @Parcelize object Unknown : LoginMode() @Parcelize object Password : LoginMode() - @Parcelize data class Sso(val ssoState: SsoState) : LoginMode() - @Parcelize data class SsoAndPassword(val ssoState: SsoState) : LoginMode() + @Parcelize data class Sso(val ssoState: SsoState, val hasOidcCompatibilityFlow: Boolean) : LoginMode() + @Parcelize data class SsoAndPassword(val ssoState: SsoState, val hasOidcCompatibilityFlow: Boolean) : LoginMode() @Parcelize object Unsupported : LoginMode() } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginSignUpSignInSelectionFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginSignUpSignInSelectionFragment.kt index dbcf674847..5ed806622f 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginSignUpSignInSelectionFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginSignUpSignInSelectionFragment.kt @@ -27,6 +27,7 @@ import im.vector.app.R import im.vector.app.core.extensions.toReducedUrl import im.vector.app.databinding.FragmentLoginSignupSigninSelectionBinding import im.vector.app.features.login.SocialLoginButtonsView.Mode +import org.matrix.android.sdk.api.auth.SSOAction /** * In this screen, the user is asked to sign up or to sign in to the homeserver. @@ -75,11 +76,12 @@ class LoginSignUpSignInSelectionFragment : when (state.loginMode) { is LoginMode.SsoAndPassword -> { views.loginSignupSigninSignInSocialLoginContainer.isVisible = true - views.loginSignupSigninSocialLoginButtons.render(state.loginMode.ssoState(), Mode.MODE_CONTINUE) { provider -> + views.loginSignupSigninSocialLoginButtons.render(state.loginMode, Mode.MODE_CONTINUE) { provider -> loginViewModel.getSsoUrl( redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, deviceId = state.deviceId, - providerId = provider?.id + providerId = provider?.id, + action = if (state.signMode == SignMode.SignUp) SSOAction.REGISTER else SSOAction.LOGIN ) ?.let { openInCustomTab(it) } } @@ -111,7 +113,8 @@ class LoginSignUpSignInSelectionFragment : loginViewModel.getSsoUrl( redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, deviceId = state.deviceId, - providerId = null + providerId = null, + action = if (state.signMode == SignMode.SignUp) SSOAction.REGISTER else SSOAction.LOGIN ) ?.let { openInCustomTab(it) } } else { diff --git a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt index 8d520628f0..4da022d4bb 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt @@ -39,6 +39,7 @@ import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixPatterns.getServerName import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.HomeServerHistoryService +import org.matrix.android.sdk.api.auth.SSOAction import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.auth.login.LoginWizard @@ -224,7 +225,7 @@ class LoginViewModel @AssistedInject constructor( setState { copy( signMode = SignMode.SignIn, - loginMode = LoginMode.Sso(action.ssoIdentityProviders.toSsoState()), + loginMode = LoginMode.Sso(action.ssoIdentityProviders.toSsoState(), action.hasOidcCompatibilityFlow), homeServerUrlFromUser = action.homeServerUrl, homeServerUrl = action.homeServerUrl, deviceId = action.deviceId @@ -817,8 +818,11 @@ class LoginViewModel @AssistedInject constructor( val loginMode = when { // SSO login is taken first data.supportedLoginTypes.contains(LoginFlowTypes.SSO) && - data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders.toSsoState()) - data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders.toSsoState()) + data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword( + data.ssoIdentityProviders.toSsoState(), + data.hasOidcCompatibilityFlow + ) + data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders.toSsoState(), data.hasOidcCompatibilityFlow) data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password else -> LoginMode.Unsupported } @@ -845,8 +849,8 @@ class LoginViewModel @AssistedInject constructor( return loginConfig?.homeServerUrl } - fun getSsoUrl(redirectUrl: String, deviceId: String?, providerId: String?): String? { - return authenticationService.getSsoUrl(redirectUrl, deviceId, providerId) + fun getSsoUrl(redirectUrl: String, deviceId: String?, providerId: String?, action: SSOAction): String? { + return authenticationService.getSsoUrl(redirectUrl, deviceId, providerId, action) } fun getFallbackUrl(forSignIn: Boolean, deviceId: String?): String? { diff --git a/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt b/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt index 816050420e..4ac98d6f2d 100644 --- a/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt +++ b/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt @@ -56,6 +56,14 @@ class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs: } } + var hasOidcCompatibilityFlow: Boolean = false + set(value) { + if (value != hasOidcCompatibilityFlow) { + field = value + update() + } + } + var listener: InteractionListener? = null private fun update() { @@ -70,7 +78,8 @@ class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs: transformationMethod = null textAlignment = View.TEXT_ALIGNMENT_CENTER }.let { - it.text = getButtonTitle(context.getString(R.string.login_social_sso)) + it.text = if (hasOidcCompatibilityFlow) context.getString(R.string.login_continue) + else getButtonTitle(context.getString(R.string.login_social_sso)) it.textAlignment = View.TEXT_ALIGNMENT_CENTER it.setOnClickListener { listener?.onProviderSelected(null) @@ -160,11 +169,14 @@ class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs: } } -fun SocialLoginButtonsView.render(state: SsoState, mode: SocialLoginButtonsView.Mode, listener: (SsoIdentityProvider?) -> Unit) { +fun SocialLoginButtonsView.render(loginMode: LoginMode, mode: SocialLoginButtonsView.Mode, listener: (SsoIdentityProvider?) -> Unit) { this.mode = mode + val state = loginMode.ssoState() this.ssoIdentityProviders = when (state) { SsoState.Fallback -> null is SsoState.IdentityProviders -> state.providers.sorted() } + this.hasOidcCompatibilityFlow = (loginMode is LoginMode.Sso && loginMode.hasOidcCompatibilityFlow) || + (loginMode is LoginMode.SsoAndPassword && loginMode.hasOidcCompatibilityFlow) this.listener = SocialLoginButtonsView.InteractionListener { listener(it) } } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt index f7c6498450..fa7c3c3f58 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt @@ -66,9 +66,6 @@ class NotifiableEventResolver @Inject constructor( private val buildMeta: BuildMeta, ) { - private val nonEncryptedNotifiableEventTypes: List = - listOf(EventType.MESSAGE) + EventType.POLL_START.values + EventType.STATE_ROOM_BEACON_INFO.values - suspend fun resolveEvent(event: Event, session: Session, isNoisy: Boolean): NotifiableEvent? { val roomID = event.roomId ?: return null val eventId = event.eventId ?: return null @@ -76,9 +73,8 @@ class NotifiableEventResolver @Inject constructor( return resolveStateRoomEvent(event, session, canBeReplaced = false, isNoisy = isNoisy) } val timelineEvent = session.getRoom(roomID)?.getTimelineEvent(eventId) ?: return null - return when (event.getClearType()) { - in nonEncryptedNotifiableEventTypes, - EventType.ENCRYPTED -> { + return when { + event.supportsNotification() || event.type == EventType.ENCRYPTED -> { resolveMessageEvent(timelineEvent, session, canBeReplaced = false, isNoisy = isNoisy) } else -> { @@ -163,8 +159,8 @@ class NotifiableEventResolver @Inject constructor( } else { event.attemptToDecryptIfNeeded(session) // only convert encrypted messages to NotifiableMessageEvents - when (event.root.getClearType()) { - in nonEncryptedNotifiableEventTypes -> { + when { + event.root.supportsNotification() -> { val body = displayableEventFormatter.format(event, isDm = room.roomSummary()?.isDirect.orFalse(), appendAuthor = false).toString() val roomName = room.roomSummary()?.displayName ?: "" val senderDisplayName = event.senderInfo.disambiguatedDisplayName diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt index 04487c6198..fc6878ffd2 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt @@ -55,6 +55,7 @@ import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.MatrixPatterns.getServerName import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.HomeServerHistoryService +import org.matrix.android.sdk.api.auth.SSOAction import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider import org.matrix.android.sdk.api.auth.login.LoginWizard @@ -159,6 +160,8 @@ class OnboardingViewModel @AssistedInject constructor( private var emailVerificationPollingJob: Job? by cancelCurrentOnSet() private var currentJob: Job? by cancelCurrentOnSet() + private val trustedFingerprints = mutableListOf() + override fun handle(action: OnboardingAction) { when (action) { is OnboardingAction.SplashAction -> handleSplashAction(action) @@ -292,13 +295,14 @@ class OnboardingViewModel @AssistedInject constructor( private fun handleUserAcceptCertificate(action: OnboardingAction.UserAcceptCertificate) { // It happens when we get the login flow, or during direct authentication. // So alter the homeserver config and retrieve again the login flow + trustedFingerprints.add(action.fingerprint) when (action.retryAction) { - is OnboardingAction.HomeServerChange -> handleHomeserverChange(action.retryAction, fingerprint = action.fingerprint) + is OnboardingAction.HomeServerChange -> handleHomeserverChange(action.retryAction, fingerprints = trustedFingerprints) is AuthenticateAction.LoginDirect -> handleDirectLogin( action.retryAction, // Will be replaced by the task - homeServerConnectionConfigFactory.create("https://dummy.org", action.fingerprint) + homeServerConnectionConfigFactory.create("https://dummy.org", trustedFingerprints) ) else -> Unit } @@ -695,10 +699,10 @@ class OnboardingViewModel @AssistedInject constructor( private fun handleHomeserverChange( action: OnboardingAction.HomeServerChange, serverTypeOverride: ServerType? = null, - fingerprint: Fingerprint? = null, + fingerprints: List? = null, postAction: suspend () -> Unit = {}, ) { - val homeServerConnectionConfig = homeServerConnectionConfigFactory.create(action.homeServerUrl, fingerprint) + val homeServerConnectionConfig = homeServerConnectionConfigFactory.create(action.homeServerUrl, fingerprints) if (homeServerConnectionConfig == null) { // This is invalid _viewEvents.post(OnboardingViewEvents.Failure(Throwable("Unable to create a HomeServerConnectionConfig"))) @@ -841,12 +845,12 @@ class OnboardingViewModel @AssistedInject constructor( fun getDefaultHomeserverUrl() = defaultHomeserverUrl - fun fetchSsoUrl(redirectUrl: String, deviceId: String?, provider: SsoIdentityProvider?): String? { + fun fetchSsoUrl(redirectUrl: String, deviceId: String?, provider: SsoIdentityProvider?, action: SSOAction): String? { setState { val authDescription = AuthenticationDescription.Register(provider.toAuthenticationType()) copy(selectedAuthenticationState = SelectedAuthenticationState(authDescription)) } - return authenticationService.getSsoUrl(redirectUrl, deviceId, provider?.id) + return authenticationService.getSsoUrl(redirectUrl, deviceId, provider?.id, action) } fun getFallbackUrl(forSignIn: Boolean, deviceId: String?): String? { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt index ea0d940952..58b28ac4e4 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt @@ -75,6 +75,7 @@ data class SelectedHomeserverState( val upstreamUrl: String? = null, val preferredLoginMode: LoginMode = LoginMode.Unknown, val supportedLoginTypes: List = emptyList(), + val hasOidcCompatibilityFlow: Boolean = false, val isLogoutDevicesSupported: Boolean = false, val isLoginWithQrSupported: Boolean = false, ) : Parcelable diff --git a/vector/src/main/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCase.kt b/vector/src/main/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCase.kt index 9b8f0a1cc4..14a3a9bfd0 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCase.kt @@ -47,13 +47,17 @@ class StartAuthenticationFlowUseCase @Inject constructor( upstreamUrl = authFlow.homeServerUrl, preferredLoginMode = preferredLoginMode, supportedLoginTypes = authFlow.supportedLoginTypes, + hasOidcCompatibilityFlow = authFlow.hasOidcCompatibilityFlow, isLogoutDevicesSupported = authFlow.isLogoutDevicesSupported, - isLoginWithQrSupported = authFlow.isLoginWithQrSupported, + isLoginWithQrSupported = authFlow.isLoginWithQrSupported ) private fun LoginFlowResult.findPreferredLoginMode() = when { - supportedLoginTypes.containsAllItems(LoginFlowTypes.SSO, LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(ssoIdentityProviders.toSsoState()) - supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(ssoIdentityProviders.toSsoState()) + supportedLoginTypes.containsAllItems(LoginFlowTypes.SSO, LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword( + ssoIdentityProviders.toSsoState(), + hasOidcCompatibilityFlow + ) + supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(ssoIdentityProviders.toSsoState(), hasOidcCompatibilityFlow) supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password else -> LoginMode.Unsupported } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/AbstractSSOFtueAuthFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/AbstractSSOFtueAuthFragment.kt index b1352db0cc..211c630320 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/AbstractSSOFtueAuthFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/AbstractSSOFtueAuthFragment.kt @@ -27,6 +27,8 @@ import im.vector.app.core.utils.openUrlInChromeCustomTab import im.vector.app.features.login.SSORedirectRouterActivity import im.vector.app.features.login.hasSso import im.vector.app.features.login.ssoState +import im.vector.app.features.onboarding.OnboardingFlow +import org.matrix.android.sdk.api.auth.SSOAction abstract class AbstractSSOFtueAuthFragment : AbstractFtueAuthFragment() { @@ -93,7 +95,8 @@ abstract class AbstractSSOFtueAuthFragment : AbstractFtueAuthF viewModel.fetchSsoUrl( redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, deviceId = state.deviceId, - provider = null + provider = null, + action = if (state.onboardingFlow == OnboardingFlow.SignUp) SSOAction.REGISTER else SSOAction.LOGIN ) ?.let { prefetchUrl(it) } } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt index aad54877c9..69090172ea 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt @@ -23,6 +23,7 @@ import android.view.View import android.view.ViewGroup import androidx.autofill.HintConstants import androidx.core.view.isVisible +import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R @@ -40,7 +41,6 @@ import im.vector.app.features.VectorFeatures import im.vector.app.features.login.LoginMode import im.vector.app.features.login.SSORedirectRouterActivity import im.vector.app.features.login.SocialLoginButtonsView -import im.vector.app.features.login.SsoState import im.vector.app.features.login.qr.QrCodeLoginArgs import im.vector.app.features.login.qr.QrCodeLoginType import im.vector.app.features.login.render @@ -49,6 +49,7 @@ import im.vector.app.features.onboarding.OnboardingViewEvents import im.vector.app.features.onboarding.OnboardingViewState import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn +import org.matrix.android.sdk.api.auth.SSOAction import reactivecircus.flowbinding.android.widget.textChanges import javax.inject.Inject @@ -103,7 +104,7 @@ class FtueAuthCombinedLoginFragment : combine(views.loginInput.editText().textChanges(), views.loginPasswordInput.editText().textChanges()) { account, password -> views.loginSubmit.isEnabled = account.isNotEmpty() && password.isNotEmpty() - }.launchIn(viewLifecycleOwner.lifecycleScope) + }.flowWithLifecycle(lifecycle).launchIn(viewLifecycleOwner.lifecycleScope) } private fun submit() { @@ -152,11 +153,11 @@ class FtueAuthCombinedLoginFragment : when (state.selectedHomeserver.preferredLoginMode) { is LoginMode.SsoAndPassword -> { showUsernamePassword() - renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoState) + renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode) } is LoginMode.Sso -> { hideUsernamePassword() - renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoState) + renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode) } else -> { showUsernamePassword() @@ -165,14 +166,15 @@ class FtueAuthCombinedLoginFragment : } } - private fun renderSsoProviders(deviceId: String?, ssoState: SsoState) { + private fun renderSsoProviders(deviceId: String?, loginMode: LoginMode) { views.ssoGroup.isVisible = true views.ssoButtonsHeader.isVisible = isUsernameAndPasswordVisible() - views.ssoButtons.render(ssoState, SocialLoginButtonsView.Mode.MODE_CONTINUE) { id -> + views.ssoButtons.render(loginMode, SocialLoginButtonsView.Mode.MODE_CONTINUE) { id -> viewModel.fetchSsoUrl( redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, deviceId = deviceId, - provider = id + provider = id, + action = SSOAction.LOGIN )?.let { openInCustomTab(it) } } } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt index 66668f5303..83a9a9c00b 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt @@ -45,7 +45,6 @@ import im.vector.app.databinding.FragmentFtueCombinedRegisterBinding import im.vector.app.features.login.LoginMode import im.vector.app.features.login.SSORedirectRouterActivity import im.vector.app.features.login.SocialLoginButtonsView -import im.vector.app.features.login.SsoState import im.vector.app.features.login.render import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingAction.AuthenticateAction @@ -53,6 +52,7 @@ import im.vector.app.features.onboarding.OnboardingViewEvents import im.vector.app.features.onboarding.OnboardingViewState import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn +import org.matrix.android.sdk.api.auth.SSOAction import org.matrix.android.sdk.api.failure.isHomeserverUnavailable import org.matrix.android.sdk.api.failure.isInvalidPassword import org.matrix.android.sdk.api.failure.isInvalidUsername @@ -207,18 +207,19 @@ class FtueAuthCombinedRegisterFragment : } when (state.selectedHomeserver.preferredLoginMode) { - is LoginMode.SsoAndPassword -> renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoState) + is LoginMode.SsoAndPassword -> renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode) else -> hideSsoProviders() } } - private fun renderSsoProviders(deviceId: String?, ssoState: SsoState) { + private fun renderSsoProviders(deviceId: String?, loginMode: LoginMode) { views.ssoGroup.isVisible = true - views.ssoButtons.render(ssoState, SocialLoginButtonsView.Mode.MODE_CONTINUE) { provider -> + views.ssoButtons.render(loginMode, SocialLoginButtonsView.Mode.MODE_CONTINUE) { provider -> viewModel.fetchSsoUrl( redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, deviceId = deviceId, - provider = provider + provider = provider, + action = SSOAction.REGISTER )?.let { openInCustomTab(it) } } } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLoginFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLoginFragment.kt index 3fd8df6bb9..8cf8dffaf3 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLoginFragment.kt @@ -47,6 +47,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import org.matrix.android.sdk.api.auth.SSOAction import org.matrix.android.sdk.api.failure.isInvalidPassword import org.matrix.android.sdk.api.failure.isInvalidUsername import org.matrix.android.sdk.api.failure.isLoginEmailUnknown @@ -215,11 +216,12 @@ class FtueAuthLoginFragment : if (state.selectedHomeserver.preferredLoginMode is LoginMode.SsoAndPassword) { views.loginSocialLoginContainer.isVisible = true - views.loginSocialLoginButtons.render(state.selectedHomeserver.preferredLoginMode.ssoState, ssoMode(state)) { provider -> + views.loginSocialLoginButtons.render(state.selectedHomeserver.preferredLoginMode, ssoMode(state)) { provider -> viewModel.fetchSsoUrl( redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, deviceId = state.deviceId, - provider = provider + provider = provider, + action = if (state.signMode == SignMode.SignUp) SSOAction.REGISTER else SSOAction.LOGIN ) ?.let { openInCustomTab(it) } } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSignUpSignInSelectionFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSignUpSignInSelectionFragment.kt index b2f2eeb167..cd387f5f6b 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSignUpSignInSelectionFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSignUpSignInSelectionFragment.kt @@ -34,7 +34,9 @@ import im.vector.app.features.login.SignMode import im.vector.app.features.login.SocialLoginButtonsView.Mode import im.vector.app.features.login.render import im.vector.app.features.onboarding.OnboardingAction +import im.vector.app.features.onboarding.OnboardingFlow import im.vector.app.features.onboarding.OnboardingViewState +import org.matrix.android.sdk.api.auth.SSOAction /** * In this screen, the user is asked to sign up or to sign in to the homeserver. @@ -81,11 +83,12 @@ class FtueAuthSignUpSignInSelectionFragment : when (state.selectedHomeserver.preferredLoginMode) { is LoginMode.SsoAndPassword -> { views.loginSignupSigninSignInSocialLoginContainer.isVisible = true - views.loginSignupSigninSocialLoginButtons.render(state.selectedHomeserver.preferredLoginMode.ssoState, Mode.MODE_CONTINUE) { provider -> + views.loginSignupSigninSocialLoginButtons.render(state.selectedHomeserver.preferredLoginMode, Mode.MODE_CONTINUE) { provider -> viewModel.fetchSsoUrl( redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, deviceId = state.deviceId, - provider = provider + provider = provider, + action = if (state.signMode == SignMode.SignUp) SSOAction.REGISTER else SSOAction.LOGIN ) ?.let { openInCustomTab(it) } } @@ -110,7 +113,8 @@ class FtueAuthSignUpSignInSelectionFragment : when (state.selectedHomeserver.preferredLoginMode) { is LoginMode.Sso -> { // change to only one button that is sign in with sso - views.loginSignupSigninSubmit.text = getString(R.string.login_signin_sso) + views.loginSignupSigninSubmit.text = + if (state.selectedHomeserver.hasOidcCompatibilityFlow) getString(R.string.login_continue) else getString(R.string.login_signin_sso) views.loginSignupSigninSignIn.isVisible = false } else -> { @@ -125,7 +129,8 @@ class FtueAuthSignUpSignInSelectionFragment : viewModel.fetchSsoUrl( redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, deviceId = state.deviceId, - provider = null + provider = null, + action = if (state.onboardingFlow == OnboardingFlow.SignUp) SSOAction.REGISTER else SSOAction.LOGIN ) ?.let { openInCustomTab(it) } } else { @@ -144,5 +149,7 @@ class FtueAuthSignUpSignInSelectionFragment : override fun updateWithState(state: OnboardingViewState) { render(state) setupButtons(state) + // if talking to OIDC enabled homeserver in compatibility mode then immediately start SSO + if (state.selectedHomeserver.hasOidcCompatibilityFlow) submit() } } diff --git a/vector/src/main/java/im/vector/app/features/poll/PollViewState.kt b/vector/src/main/java/im/vector/app/features/poll/PollItemViewState.kt similarity index 96% rename from vector/src/main/java/im/vector/app/features/poll/PollViewState.kt rename to vector/src/main/java/im/vector/app/features/poll/PollItemViewState.kt index ecbee7438a..e5b4f71f1d 100644 --- a/vector/src/main/java/im/vector/app/features/poll/PollViewState.kt +++ b/vector/src/main/java/im/vector/app/features/poll/PollItemViewState.kt @@ -18,7 +18,7 @@ package im.vector.app.features.poll import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState -data class PollViewState( +data class PollItemViewState( val question: String, val votesStatus: String, val canVote: Boolean, diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt index bdf2978cca..9585e6aaa1 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt @@ -107,16 +107,10 @@ class RoomMemberProfileController @Inject constructor( // Cross signing is enabled for this user if (state.userMXCrossSigningInfo.isTrusted()) { // User is trusted - val icon = if (state.allDevicesAreTrusted) { - R.drawable.ic_shield_trusted + val (icon, titleRes) = if (state.allDevicesAreCrossSignedTrusted) { + Pair(R.drawable.ic_shield_trusted, R.string.verification_profile_verified) } else { - R.drawable.ic_shield_warning - } - - val titleRes = if (state.allDevicesAreTrusted) { - R.string.verification_profile_verified - } else { - R.string.verification_profile_warning + Pair(R.drawable.ic_shield_warning, R.string.verification_profile_warning) } buildProfileAction( diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheetViewModel.kt index 5a3f5eeeaf..f8ec3f9c0c 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheetViewModel.kt @@ -45,6 +45,7 @@ data class DeviceListViewState( val allowDeviceAction: Boolean, val userItem: MatrixItem? = null, val memberCrossSigningKey: MXCrossSigningInfo? = null, + val myDeviceId: String = "", val cryptoDevices: Async> = Loading(), val selectedDevice: CryptoDeviceInfo? = null ) : MavericksState @@ -70,6 +71,7 @@ class DeviceListBottomSheetViewModel @AssistedInject constructor( userId = userId, allowDeviceAction = args.allowDeviceAction, userItem = session.getUserOrDefault(userId).toMatrixItem(), + myDeviceId = session.sessionParams.deviceId ?: "", ) } } diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListEpoxyController.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListEpoxyController.kt index 995de365eb..764268e6a5 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListEpoxyController.kt @@ -32,9 +32,12 @@ import im.vector.app.core.ui.list.genericItem import im.vector.app.core.ui.list.genericWithValueItem import im.vector.app.core.utils.DimensionConverter import im.vector.app.features.settings.VectorPreferences +import im.vector.app.features.settings.devices.TrustUtils import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import me.gujun.android.span.span +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import javax.inject.Inject class DeviceListEpoxyController @Inject constructor( @@ -68,10 +71,20 @@ class DeviceListEpoxyController @Inject constructor( it.isVerified } + val trustMSK = data.memberCrossSigningKey?.isTrusted().orFalse() + val legacyMode = data.memberCrossSigningKey == null + // Build top header - val allGreen = deviceList.fold(true, { prev, device -> - prev && device.isVerified - }) + val allGreen = deviceList.fold(true) { prev, device -> + val trustLevel = TrustUtils.shieldForTrust( + data.myDeviceId == device.deviceId, + trustMSK, + legacyMode, + device.trustLevel + ) + + prev && trustLevel == RoomEncryptionTrustLevel.Trusted + } genericItem { id("title") @@ -105,8 +118,21 @@ class DeviceListEpoxyController @Inject constructor( // Build list of device with status deviceList.forEach { device -> genericWithValueItem { + val trustLevel = TrustUtils.shieldForTrust( + data.myDeviceId == device.deviceId, + trustMSK, + legacyMode, + device.trustLevel + ) + val shield = when (trustLevel) { + RoomEncryptionTrustLevel.Default -> R.drawable.ic_shield_unknown + RoomEncryptionTrustLevel.Warning -> R.drawable.ic_shield_warning + RoomEncryptionTrustLevel.Trusted -> R.drawable.ic_shield_trusted + RoomEncryptionTrustLevel.E2EWithUnsupportedAlgorithm -> R.drawable.ic_warning_badge + } + id(device.deviceId) - titleIconResourceId(if (device.isVerified) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning) + titleIconResourceId(shield) apply { val title = if (host.vectorPreferences.developerMode()) { val seq = span { @@ -126,12 +152,12 @@ class DeviceListEpoxyController @Inject constructor( } value( host.stringProvider.getString( - if (device.isVerified) R.string.trusted else R.string.not_trusted + if (trustLevel == RoomEncryptionTrustLevel.Trusted) R.string.trusted else R.string.not_trusted ) ) valueColorInt( host.colorProvider.getColorFromAttribute( - if (device.isVerified) R.attr.colorPrimary else R.attr.colorError + if (trustLevel == RoomEncryptionTrustLevel.Trusted) R.attr.colorPrimary else R.attr.colorError ) ) itemClickAction { diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt index 569a7f980c..20c388dd95 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt @@ -27,9 +27,12 @@ import im.vector.app.core.ui.list.genericWithValueItem import im.vector.app.core.utils.DimensionConverter import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationActionItem import im.vector.app.features.settings.VectorPreferences +import im.vector.app.features.settings.devices.TrustUtils import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import me.gujun.android.span.span +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import javax.inject.Inject class DeviceTrustInfoEpoxyController @Inject constructor( @@ -49,11 +52,26 @@ class DeviceTrustInfoEpoxyController @Inject constructor( override fun buildModels(data: DeviceListViewState?) { val host = this data?.selectedDevice?.let { cryptoDeviceInfo -> - val isVerified = cryptoDeviceInfo.trustLevel?.isVerified() == true + val trustMSK = data.memberCrossSigningKey?.isTrusted().orFalse() + val legacyMode = data.memberCrossSigningKey == null + val isMyDevice = data.myDeviceId == cryptoDeviceInfo.deviceId + val trustLevel = TrustUtils.shieldForTrust( + isMyDevice, + trustMSK, + legacyMode, + cryptoDeviceInfo.trustLevel + ) + val isVerified = trustLevel == RoomEncryptionTrustLevel.Trusted + val shield = when (trustLevel) { + RoomEncryptionTrustLevel.Default -> R.drawable.ic_shield_unknown + RoomEncryptionTrustLevel.Warning -> R.drawable.ic_shield_warning + RoomEncryptionTrustLevel.Trusted -> R.drawable.ic_shield_trusted + RoomEncryptionTrustLevel.E2EWithUnsupportedAlgorithm -> R.drawable.ic_warning_badge + } genericItem { id("title") style(ItemStyle.BIG_TEXT) - titleIconResourceId(if (isVerified) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning) + titleIconResourceId(shield) title( host.stringProvider .getString(if (isVerified) R.string.verification_profile_verified else R.string.verification_profile_warning) @@ -90,7 +108,7 @@ class DeviceTrustInfoEpoxyController @Inject constructor( genericWithValueItem { id(cryptoDeviceInfo.deviceId) - titleIconResourceId(if (isVerified) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning) + titleIconResourceId(shield) title( span { +(cryptoDeviceInfo.displayName() ?: "") @@ -103,7 +121,7 @@ class DeviceTrustInfoEpoxyController @Inject constructor( ) } - if (!isVerified) { + if (!isVerified && !isMyDevice) { genericFooterItem { id("warn") centered(false) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt index 30bd6c7ed3..1fbfaba2bb 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt @@ -18,7 +18,6 @@ package im.vector.app.features.roomprofile import com.airbnb.epoxy.TypedEpoxyController -import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.epoxy.expandableTextItem import im.vector.app.core.epoxy.profiles.buildProfileAction @@ -265,15 +264,14 @@ class RoomProfileController @Inject constructor( action = { callback?.onBannedMemberListClicked() } ) } - if (BuildConfig.DEBUG) { - // WIP, will be in release when related screens will be finished - buildProfileAction( - id = "poll_history", - title = stringProvider.getString(R.string.room_profile_section_more_polls), - icon = R.drawable.ic_attachment_poll, - action = { callback?.onPollHistoryClicked() } - ) - } + + buildProfileAction( + id = "poll_history", + title = stringProvider.getString(R.string.room_profile_section_more_polls), + icon = R.drawable.ic_attachment_poll, + action = { callback?.onPollHistoryClicked() } + ) + buildProfileAction( id = "uploads", title = stringProvider.getString(R.string.room_profile_section_more_uploads), diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt index 91f57d33e9..9436bafc03 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt @@ -64,7 +64,7 @@ import javax.inject.Inject @Parcelize data class RoomProfileArgs( - val roomId: String + val roomId: String, ) : Parcelable @AndroidEntryPoint diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/domain/GetEndedPollEventIdUseCase.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/domain/GetEndedPollEventIdUseCase.kt new file mode 100644 index 0000000000..aa1ba1b274 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/domain/GetEndedPollEventIdUseCase.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.polls.detail.domain + +import im.vector.app.core.di.ActiveSessionHolder +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.isPollEnd +import timber.log.Timber +import javax.inject.Inject + +class GetEndedPollEventIdUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, +) { + + fun execute(roomId: String, startPollEventId: String): String? { + val result = runCatching { + activeSessionHolder.getActiveSession().roomService().getRoom(roomId) + ?.timelineService() + ?.getTimelineEventsRelatedTo(RelationType.REFERENCE, startPollEventId) + ?.find { it.root.isPollEnd() } + ?.eventId + }.onFailure { Timber.w("failed to retrieve the ended poll event id for eventId:$startPollEventId") } + return result.getOrNull() + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetail.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetail.kt new file mode 100644 index 0000000000..7857a30eeb --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetail.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.polls.detail.ui + +import im.vector.app.features.poll.PollItemViewState + +data class RoomPollDetail( + val creationTimestamp: Long, + val isEnded: Boolean, + val endedPollEventId: String?, + val pollItemViewState: PollItemViewState, +) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailAction.kt new file mode 100644 index 0000000000..dbf8436399 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailAction.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.polls.detail.ui + +import im.vector.app.core.platform.VectorViewModelAction + +sealed interface RoomPollDetailAction : VectorViewModelAction { + data class Vote(val pollEventId: String, val optionId: String) : RoomPollDetailAction +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailActivity.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailActivity.kt new file mode 100644 index 0000000000..cf29d5618a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailActivity.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.polls.detail.ui + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import com.airbnb.mvrx.Mavericks +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.core.extensions.addFragment +import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.databinding.ActivitySimpleBinding +import im.vector.lib.core.utils.compat.getParcelableExtraCompat + +/** + * Display the details of a given poll. + */ +@AndroidEntryPoint +class RoomPollDetailActivity : VectorBaseActivity() { + + override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (isFirstCreation()) { + addFragment( + container = views.simpleFragmentContainer, + fragmentClass = RoomPollDetailFragment::class.java, + params = intent.getParcelableExtraCompat(Mavericks.KEY_ARG) + ) + } + } + + companion object { + fun newIntent(context: Context, pollId: String, roomId: String, isEnded: Boolean): Intent { + return Intent(context, RoomPollDetailActivity::class.java).apply { + val args = RoomPollDetailArgs( + pollId = pollId, + roomId = roomId, + isEnded = isEnded, + ) + putExtra(Mavericks.KEY_ARG, args) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailController.kt new file mode 100644 index 0000000000..7a246f812b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailController.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.polls.detail.ui + +import com.airbnb.epoxy.TypedEpoxyController +import im.vector.app.core.date.DateFormatKind +import im.vector.app.core.date.VectorDateFormatter +import java.util.UUID +import javax.inject.Inject + +class RoomPollDetailController @Inject constructor( + val dateFormatter: VectorDateFormatter, +) : TypedEpoxyController() { + + interface Callback { + fun vote(pollEventId: String, optionId: String) + fun goToTimelineEvent(eventId: String) + } + + var callback: Callback? = null + + override fun buildModels(viewState: RoomPollDetailViewState?) { + val pollDetail = viewState?.pollDetail ?: return + val pollItemViewState = pollDetail.pollItemViewState + val host = this + + roomPollDetailItem { + id(viewState.pollId) + eventId(viewState.pollId) + formattedDate(host.dateFormatter.format(pollDetail.creationTimestamp, DateFormatKind.TIMELINE_DAY_DIVIDER)) + question(pollItemViewState.question) + canVote(pollItemViewState.canVote) + votesStatus(pollItemViewState.votesStatus) + optionViewStates(pollItemViewState.optionViewStates.orEmpty()) + callback(host.callback) + } + + buildGoToTimelineItem(targetEventId = pollDetail.endedPollEventId ?: viewState.pollId) + } + + private fun buildGoToTimelineItem(targetEventId: String) { + val host = this + roomPollGoToTimelineItem { + id(UUID.randomUUID().toString()) + clickListener { + host.callback?.goToTimelineEvent(targetEventId) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailFragment.kt new file mode 100644 index 0000000000..9c118bb897 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailFragment.kt @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.polls.detail.ui + +import android.os.Bundle +import android.os.Parcelable +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.airbnb.mvrx.args +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.R +import im.vector.app.core.extensions.cleanup +import im.vector.app.core.extensions.configureWith +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.databinding.FragmentRoomPollDetailBinding +import kotlinx.parcelize.Parcelize +import javax.inject.Inject + +@Parcelize +data class RoomPollDetailArgs( + val pollId: String, + val roomId: String, + val isEnded: Boolean, +) : Parcelable + +@AndroidEntryPoint +class RoomPollDetailFragment : + VectorBaseFragment(), + RoomPollDetailController.Callback { + + @Inject lateinit var viewNavigator: RoomPollDetailNavigator + @Inject lateinit var roomPollDetailController: RoomPollDetailController + + private val viewModel: RoomPollDetailViewModel by fragmentViewModel() + private val roomPollDetailArgs: RoomPollDetailArgs by args() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRoomPollDetailBinding { + return FragmentRoomPollDetailBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupToolbar(isEnded = roomPollDetailArgs.isEnded) + setupDetailView() + } + + override fun onDestroyView() { + roomPollDetailController.callback = null + views.pollDetailRecyclerView.cleanup() + super.onDestroyView() + } + + private fun setupDetailView() { + roomPollDetailController.callback = this + views.pollDetailRecyclerView.configureWith( + roomPollDetailController, + hasFixedSize = true, + ) + } + + private fun setupToolbar(isEnded: Boolean) { + val title = when (isEnded) { + true -> getString(R.string.room_polls_ended) + false -> getString(R.string.room_polls_active) + } + + setupToolbar(views.roomPollDetailToolbar) + .setTitle(title) + .allowBack(useCross = true) + } + + override fun invalidate() = withState(viewModel) { state -> + roomPollDetailController.setData(state) + } + + override fun vote(pollEventId: String, optionId: String) { + viewModel.handle(RoomPollDetailAction.Vote(pollEventId = pollEventId, optionId = optionId)) + } + + override fun goToTimelineEvent(eventId: String) = withState(viewModel) { state -> + viewNavigator.goToTimelineEvent( + context = requireContext(), + roomId = state.roomId, + eventId = eventId, + ) + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailItem.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailItem.kt new file mode 100644 index 0000000000..b3f905e661 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailItem.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.polls.detail.ui + +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.view.isVisible +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.features.home.room.detail.timeline.item.PollOptionView +import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState + +@EpoxyModelClass +abstract class RoomPollDetailItem : VectorEpoxyModel(R.layout.item_poll_detail) { + + @EpoxyAttribute + lateinit var formattedDate: String + + @EpoxyAttribute + var question: String? = null + + @EpoxyAttribute + var callback: RoomPollDetailController.Callback? = null + + @EpoxyAttribute + var eventId: String? = null + + @EpoxyAttribute + var canVote: Boolean = false + + @EpoxyAttribute + var votesStatus: String? = null + + @EpoxyAttribute + lateinit var optionViewStates: List + + @EpoxyAttribute + var ended: Boolean = false + + override fun bind(holder: Holder) { + super.bind(holder) + holder.date.text = formattedDate + holder.questionTextView.text = question + holder.votesStatusTextView.text = votesStatus + holder.optionsContainer.removeAllViews() + holder.optionsContainer.isVisible = optionViewStates.isNotEmpty() + for (option in optionViewStates) { + val optionView = PollOptionView(holder.view.context) + holder.optionsContainer.addView(optionView) + optionView.render(option) + optionView.setOnClickListener { onOptionClicked(option) } + } + + holder.endedPollTextView.isVisible = false + } + + private fun onOptionClicked(optionViewState: PollOptionViewState) { + val relatedEventId = eventId + + if (canVote && relatedEventId != null) { + callback?.vote(pollEventId = relatedEventId, optionId = optionViewState.optionId) + } + } + + class Holder : VectorEpoxyHolder() { + val date by bind(R.id.pollDetailDate) + val questionTextView by bind(R.id.questionTextView) + val optionsContainer by bind(R.id.optionsContainer) + val votesStatusTextView by bind(R.id.optionsVotesStatusTextView) + val endedPollTextView by bind(R.id.endedPollTextView) + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailMapper.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailMapper.kt new file mode 100644 index 0000000000..8f14118d43 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailMapper.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.polls.detail.ui + +import im.vector.app.core.extensions.getVectorLastMessageContent +import im.vector.app.features.home.room.detail.timeline.factory.PollItemViewStateFactory +import im.vector.app.features.home.room.detail.timeline.helper.PollResponseDataFactory +import im.vector.app.features.home.room.detail.timeline.item.PollResponseData +import im.vector.app.features.roomprofile.polls.detail.domain.GetEndedPollEventIdUseCase +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import timber.log.Timber +import javax.inject.Inject + +class RoomPollDetailMapper @Inject constructor( + private val pollResponseDataFactory: PollResponseDataFactory, + private val pollItemViewStateFactory: PollItemViewStateFactory, + private val getEndedPollEventIdUseCase: GetEndedPollEventIdUseCase, +) { + + fun map(timelineEvent: TimelineEvent): RoomPollDetail? { + val eventId = timelineEvent.root.eventId.orEmpty() + val result = runCatching { + val content = timelineEvent.getVectorLastMessageContent() + val pollResponseData = pollResponseDataFactory.create(timelineEvent) + val creationTimestamp = timelineEvent.root.originServerTs ?: 0 + return if (eventId.isNotEmpty() && creationTimestamp > 0 && content is MessagePollContent) { + val isPollEnded = pollResponseData?.isClosed.orFalse() + val endedPollEventId = getEndedPollEventId( + isPollEnded, + startPollEventId = eventId, + roomId = timelineEvent.roomId, + ) + convertToRoomPollDetail( + creationTimestamp = creationTimestamp, + content = content, + pollResponseData = pollResponseData, + isPollEnded = isPollEnded, + endedPollEventId = endedPollEventId, + ) + } else { + Timber.w("missing mandatory info about poll event with id=$eventId") + null + } + } + + if (result.isFailure) { + Timber.w("failed to map event with id $eventId") + } + return result.getOrNull() + } + + private fun convertToRoomPollDetail( + creationTimestamp: Long, + content: MessagePollContent, + pollResponseData: PollResponseData?, + isPollEnded: Boolean, + endedPollEventId: String?, + ): RoomPollDetail { + // we assume the poll has been sent + val pollItemViewState = pollItemViewStateFactory.create( + pollContent = content, + pollResponseData = pollResponseData, + isSent = true, + ) + return RoomPollDetail( + creationTimestamp = creationTimestamp, + isEnded = isPollEnded, + pollItemViewState = pollItemViewState, + endedPollEventId = endedPollEventId, + ) + } + + private fun getEndedPollEventId( + isPollEnded: Boolean, + startPollEventId: String, + roomId: String, + ): String? { + return if (isPollEnded) { + getEndedPollEventIdUseCase.execute(startPollEventId = startPollEventId, roomId = roomId) + } else { + null + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailNavigator.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailNavigator.kt new file mode 100644 index 0000000000..a19bb87d9e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailNavigator.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.polls.detail.ui + +import android.content.Context +import im.vector.app.features.navigation.Navigator +import javax.inject.Inject + +class RoomPollDetailNavigator @Inject constructor( + private val navigator: Navigator, +) { + + fun goToTimelineEvent(context: Context, roomId: String, eventId: String) { + navigator.openRoom( + context = context, + roomId = roomId, + eventId = eventId, + buildTask = true, + ) + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailViewModel.kt new file mode 100644 index 0000000000..487595d20b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailViewModel.kt @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.polls.detail.ui + +import com.airbnb.mvrx.MavericksViewModelFactory +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.event.GetTimelineEventUseCase +import im.vector.app.core.platform.EmptyViewEvents +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.features.home.room.detail.poll.VoteToPollUseCase +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach + +class RoomPollDetailViewModel @AssistedInject constructor( + @Assisted initialState: RoomPollDetailViewState, + private val getTimelineEventUseCase: GetTimelineEventUseCase, + private val roomPollDetailMapper: RoomPollDetailMapper, + private val voteToPollUseCase: VoteToPollUseCase, +) : VectorViewModel(initialState) { + + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: RoomPollDetailViewState): RoomPollDetailViewModel + } + + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + + init { + observePollDetails( + pollId = initialState.pollId, + roomId = initialState.roomId, + ) + } + + private fun observePollDetails(pollId: String, roomId: String) { + getTimelineEventUseCase.execute(roomId = roomId, eventId = pollId) + .map { roomPollDetailMapper.map(it) } + .onEach { setState { copy(pollDetail = it) } } + .launchIn(viewModelScope) + } + + override fun handle(action: RoomPollDetailAction) { + when (action) { + is RoomPollDetailAction.Vote -> handleVote(action) + } + } + + private fun handleVote(vote: RoomPollDetailAction.Vote) = withState { state -> + voteToPollUseCase.execute( + roomId = state.roomId, + pollEventId = vote.pollEventId, + optionId = vote.optionId, + ) + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailViewState.kt new file mode 100644 index 0000000000..a2906dc88f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailViewState.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.polls.detail.ui + +import com.airbnb.mvrx.MavericksState + +data class RoomPollDetailViewState( + val pollId: String, + val roomId: String, + val pollDetail: RoomPollDetail? = null, +) : MavericksState { + + constructor(roomPollDetailArgs: RoomPollDetailArgs) : this( + pollId = roomPollDetailArgs.pollId, + roomId = roomPollDetailArgs.roomId, + ) +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollGoToTimelineItem.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollGoToTimelineItem.kt new file mode 100644 index 0000000000..59a5539a4f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollGoToTimelineItem.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.polls.detail.ui + +import android.widget.Button +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.ClickListener +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.epoxy.onClick + +@EpoxyModelClass +abstract class RoomPollGoToTimelineItem : VectorEpoxyModel(R.layout.item_poll_go_to_timeline) { + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + var clickListener: ClickListener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + holder.goToTimelineButton.onClick(clickListener) + } + + class Holder : VectorEpoxyHolder() { + val goToTimelineButton by bind