Browse Source

Merge branch 'release/0.2.3' into main

pull/1441/head v0.2.3
Benoit Marty 12 months ago
parent
commit
43ff5a9116
  1. 35
      .github/ISSUE_TEMPLATE/story.yml
  2. 4
      .github/workflows/build.yml
  3. 2
      .github/workflows/danger.yml
  4. 2
      .github/workflows/nightlyReports.yml
  5. 4
      .github/workflows/quality.yml
  6. 2
      .github/workflows/recordScreenshots.yml
  7. 2
      .github/workflows/release.yml
  8. 2
      .github/workflows/sonar.yml
  9. 2
      .github/workflows/tests.yml
  10. 13
      CHANGES.md
  11. 20
      appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
  12. 2
      appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt
  13. 2
      fastlane/metadata/android/en-US/changelogs/40002030.txt
  14. 2
      features/analytics/impl/src/main/res/values-zh-rTW/translations.xml
  15. 1
      features/call/src/main/AndroidManifest.xml
  16. 36
      features/call/src/main/kotlin/io/element/android/features/call/CallIntentDataParser.kt
  17. 6
      features/call/src/main/res/values-cs/translations.xml
  18. 5
      features/call/src/main/res/values-zh-rTW/translations.xml
  19. 76
      features/call/src/test/kotlin/io/element/android/features/call/CallIntentDataParserTests.kt
  20. 2
      features/ftue/impl/src/main/res/values-cs/translations.xml
  21. 4
      features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt
  22. 7
      features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListPresenter.kt
  23. 4
      features/login/impl/src/main/res/values-ru/translations.xml
  24. 20
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt
  25. 2
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt
  26. 54
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaActions.kt
  27. 3
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt
  28. 18
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt
  29. 7
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt
  30. 4
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt
  31. 12
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt
  32. 13
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt
  33. 36
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt
  34. 8
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt
  35. 8
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt
  36. 33
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollView.kt
  37. 19
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt
  38. 10
      features/messages/impl/src/main/res/drawable/ic_apk_install.xml
  39. 1
      features/messages/impl/src/main/res/values-cs/translations.xml
  40. 6
      features/messages/impl/src/main/res/values-zh-rTW/translations.xml
  41. 1
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt
  42. 26
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt
  43. 2
      features/onboarding/impl/src/main/res/values-ru/translations.xml
  44. 10
      features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerViewProvider.kt
  45. 124
      features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt
  46. 2
      features/poll/impl/src/main/res/values-cs/translations.xml
  47. 12
      features/poll/impl/src/main/res/values-zh-rTW/translations.xml
  48. 9
      features/preferences/impl/src/main/res/values-cs/translations.xml
  49. 8
      features/preferences/impl/src/main/res/values-zh-rTW/translations.xml
  50. 18
      features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenter.kt
  51. 1
      features/rageshake/impl/src/main/res/values-zh-rTW/translations.xml
  52. 3
      features/roomdetails/impl/src/main/res/values-zh-rTW/translations.xml
  53. 2
      features/roomlist/impl/src/main/res/values-zh-rTW/translations.xml
  54. 14
      features/verifysession/impl/src/main/res/values-zh-rTW/translations.xml
  55. 6
      gradle/libs.versions.toml
  56. 8
      libraries/core/src/main/kotlin/io/element/android/libraries/core/mimetype/MimeTypes.kt
  57. 15
      libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/swipe/SwipeableActionsState.kt
  58. 4
      libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Slider.kt
  59. 1
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt
  60. 5
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt
  61. 3
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt
  62. 1
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt
  63. 3
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomContentForwarder.kt
  64. 33
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt
  65. 6
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt
  66. 4
      libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedUsersList.kt
  67. 7
      libraries/permissions/api/src/main/res/values-cs/translations.xml
  68. 7
      libraries/permissions/api/src/main/res/values-de/translations.xml
  69. 8
      libraries/permissions/api/src/main/res/values-fr/translations.xml
  70. 7
      libraries/permissions/api/src/main/res/values-ru/translations.xml
  71. 57
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt
  72. 6
      libraries/textcomposer/impl/src/main/res/values-cs/translations.xml
  73. 1
      libraries/textcomposer/impl/src/main/res/values-zh-rTW/translations.xml
  74. 17
      libraries/ui-strings/src/main/res/values-cs/translations.xml
  75. 3
      libraries/ui-strings/src/main/res/values-de/translations.xml
  76. 6
      libraries/ui-strings/src/main/res/values-ru/translations.xml
  77. 31
      libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml
  78. 3
      libraries/ui-strings/src/main/res/values/localazy.xml
  79. 2
      plugins/src/main/kotlin/Versions.kt
  80. 45
      services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/DefaultAppNavigationStateService.kt
  81. 5
      services/appnavstate/impl/src/test/kotlin/io/element/android/services/appnavstate/impl/DefaultNavigationStateServiceTest.kt
  82. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.media.viewer_null_MediaViewerView_0_null_6,NEXUS_5,1.0,en].png
  83. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.media.viewer_null_MediaViewerView_0_null_7,NEXUS_5,1.0,en].png
  84. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-D-32_32_null_0,NEXUS_5,1.0,en].png
  85. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-D-32_32_null_1,NEXUS_5,1.0,en].png
  86. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-N-32_33_null_0,NEXUS_5,1.0,en].png
  87. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-N-32_33_null_1,NEXUS_5,1.0,en].png
  88. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemRedactedView-D-33_33_null,NEXUS_5,1.0,en].png
  89. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemRedactedView-N-33_34_null,NEXUS_5,1.0,en].png
  90. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemStateView-D-34_34_null,NEXUS_5,1.0,en].png
  91. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemStateView-N-34_35_null,NEXUS_5,1.0,en].png
  92. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-35_35_null_0,NEXUS_5,1.0,en].png
  93. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-35_35_null_1,NEXUS_5,1.0,en].png
  94. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-35_35_null_2,NEXUS_5,1.0,en].png
  95. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-35_35_null_3,NEXUS_5,1.0,en].png
  96. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-35_35_null_4,NEXUS_5,1.0,en].png
  97. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-35_35_null_5,NEXUS_5,1.0,en].png
  98. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-35_36_null_0,NEXUS_5,1.0,en].png
  99. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-35_36_null_1,NEXUS_5,1.0,en].png
  100. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-35_36_null_2,NEXUS_5,1.0,en].png
  101. Some files were not shown because too many files have changed in this diff Show More

35
.github/ISSUE_TEMPLATE/story.yml

@ -1,35 +0,0 @@
name: User story issue
description: Second-level planning issue template. A story should take about a week or a sprint to finish.
title: "[Story] <title>"
labels: [T-Story]
body:
- type: textarea
attributes:
label: Story
description: A story should take roughly a week or a sprint to finish. Each story is usually made up of a number of tasks that take half to a full day.
value: |
As a user…
I want to…
so that I can…
## Scope
<!--These should be a list of technical tasks which take ½-1 day to complete-->
```[tasklist]
### Tasklist
- [ ] Task 1
```
- [ ] QA signoff on completion
- [ ] Design signoff on completion
- [ ] Product signoff on completion
## Stretch goals
None at this time
<!--or add a tasklist-->
## Out of scope
-
validations:
required: false

4
.github/workflows/build.yml

@ -38,7 +38,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '17'
- name: Configure gradle - name: Configure gradle
uses: gradle/gradle-build-action@v2.8.0 uses: gradle/gradle-build-action@v2.8.1
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Assemble debug APK - name: Assemble debug APK
@ -55,7 +55,7 @@ jobs:
name: elementx-debug name: elementx-debug
path: | path: |
app/build/outputs/apk/debug/*.apk app/build/outputs/apk/debug/*.apk
- uses: rnkdsh/action-upload-diawi@v1.5.1 - uses: rnkdsh/action-upload-diawi@v1.5.2
id: diawi id: diawi
# Do not fail the whole build if Diawi upload fails # Do not fail the whole build if Diawi upload fails
continue-on-error: true continue-on-error: true

2
.github/workflows/danger.yml

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

2
.github/workflows/nightlyReports.yml

@ -62,7 +62,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '17'
- name: Configure gradle - name: Configure gradle
uses: gradle/gradle-build-action@v2.8.0 uses: gradle/gradle-build-action@v2.8.1
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Dependency analysis - name: Dependency analysis

4
.github/workflows/quality.yml

@ -40,7 +40,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '17'
- name: Configure gradle - name: Configure gradle
uses: gradle/gradle-build-action@v2.8.0 uses: gradle/gradle-build-action@v2.8.1
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run code quality check suite - name: Run code quality check suite
@ -60,7 +60,7 @@ jobs:
yarn add danger-plugin-lint-report --dev yarn add danger-plugin-lint-report --dev
- name: Danger lint - name: Danger lint
if: always() if: always()
uses: danger/danger-js@11.2.8 uses: danger/danger-js@11.3.0
with: with:
args: "--dangerfile ./tools/danger/dangerfile-lint.js" args: "--dangerfile ./tools/danger/dangerfile-lint.js"
env: env:

2
.github/workflows/recordScreenshots.yml

@ -24,7 +24,7 @@ jobs:
java-version: '17' java-version: '17'
# Add gradle cache, this should speed up the process # Add gradle cache, this should speed up the process
- name: Configure gradle - name: Configure gradle
uses: gradle/gradle-build-action@v2.8.0 uses: gradle/gradle-build-action@v2.8.1
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Record screenshots - name: Record screenshots

2
.github/workflows/release.yml

@ -25,7 +25,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '17'
- name: Configure gradle - name: Configure gradle
uses: gradle/gradle-build-action@v2.8.0 uses: gradle/gradle-build-action@v2.8.1
- name: Create app bundle - name: Create app bundle
env: env:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }} ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}

2
.github/workflows/sonar.yml

@ -32,7 +32,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '17'
- name: Configure gradle - name: Configure gradle
uses: gradle/gradle-build-action@v2.8.0 uses: gradle/gradle-build-action@v2.8.1
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: 🔊 Publish results to Sonar - name: 🔊 Publish results to Sonar

2
.github/workflows/tests.yml

@ -44,7 +44,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '17'
- name: Configure gradle - name: Configure gradle
uses: gradle/gradle-build-action@v2.8.0 uses: gradle/gradle-build-action@v2.8.1
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}

13
CHANGES.md

@ -1,3 +1,16 @@
Changes in Element X v0.2.3 (2023-09-27)
========================================
Features ✨
----------
- Handle installation of Apks from the media viewer. ([#1432](https://github.com/vector-im/element-x-android/pull/1432))
- Integrate SDK 0.1.58 ([#1437](https://github.com/vector-im/element-x-android/pull/1437))
Other changes
-------------
- Element call: add custom parameters to Element Call urls. ([#1434](https://github.com/vector-im/element-x-android/issues/1434))
Changes in Element X v0.2.2 (2023-09-21) Changes in Element X v0.2.2 (2023-09-21)
======================================== ========================================

20
appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt

@ -54,6 +54,7 @@ import io.element.android.features.verifysession.api.VerifySessionEntryPoint
import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.waitForChildAttached
import io.element.android.libraries.deeplink.DeeplinkData import io.element.android.libraries.deeplink.DeeplinkData
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
@ -68,6 +69,7 @@ import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import timber.log.Timber import timber.log.Timber
@ -304,6 +306,15 @@ class LoggedInFlowNode @AssistedInject constructor(
} }
} }
internal suspend fun attachInviteList(deeplinkData: DeeplinkData.InviteList) = withContext(lifecycleScope.coroutineContext) {
notificationDrawerManager.clearMembershipNotificationForSession(deeplinkData.sessionId)
backstack.singleTop(NavTarget.RoomList)
backstack.push(NavTarget.InviteList)
waitForChildAttached<Node, NavTarget> { navTarget ->
navTarget is NavTarget.InviteList
}
}
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
Box(modifier = modifier) { Box(modifier = modifier) {
@ -321,13 +332,4 @@ class LoggedInFlowNode @AssistedInject constructor(
} }
} }
} }
internal suspend fun attachRoom(deeplinkData: DeeplinkData.Room) {
backstack.push(NavTarget.Room(deeplinkData.roomId))
}
internal suspend fun attachInviteList(deeplinkData: DeeplinkData.InviteList) {
notificationDrawerManager.clearMembershipNotificationForSession(deeplinkData.sessionId)
backstack.push(NavTarget.InviteList)
}
} }

2
appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt

@ -234,7 +234,7 @@ class RootFlowNode @AssistedInject constructor(
.apply { .apply {
when (deeplinkData) { when (deeplinkData) {
is DeeplinkData.Root -> attachRoot() is DeeplinkData.Root -> attachRoot()
is DeeplinkData.Room -> attachRoom(deeplinkData) is DeeplinkData.Room -> attachRoom(deeplinkData.roomId)
is DeeplinkData.InviteList -> attachInviteList(deeplinkData) is DeeplinkData.InviteList -> attachInviteList(deeplinkData)
} }
} }

2
fastlane/metadata/android/en-US/changelogs/40002030.txt

@ -0,0 +1,2 @@
Main changes in this version: bugfixes.
Full changelog: https://github.com/vector-im/element-x-android/releases

2
features/analytics/impl/src/main/res/values-zh-rTW/translations.xml

@ -2,7 +2,7 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_data_usage">"我們不會紀錄或剖繪您的個人資料"</string> <string name="screen_analytics_prompt_data_usage">"我們不會紀錄或剖繪您的個人資料"</string>
<string name="screen_analytics_prompt_help_us_improve">"分享匿名的使用數據以協助我們釐清問題"</string> <string name="screen_analytics_prompt_help_us_improve">"分享匿名的使用數據以協助我們釐清問題"</string>
<string name="screen_analytics_prompt_read_terms">"您可以到 %1$s 閱讀我們的條款。"</string> <string name="screen_analytics_prompt_read_terms">"您可以到%1$s閱讀我們的條款。"</string>
<string name="screen_analytics_prompt_read_terms_content_link">"這裡"</string> <string name="screen_analytics_prompt_read_terms_content_link">"這裡"</string>
<string name="screen_analytics_prompt_settings">"您可以在任何時候關閉它"</string> <string name="screen_analytics_prompt_settings">"您可以在任何時候關閉它"</string>
<string name="screen_analytics_prompt_third_party_sharing">"我們不會和第三方分享您的資料"</string> <string name="screen_analytics_prompt_third_party_sharing">"我們不會和第三方分享您的資料"</string>

1
features/call/src/main/AndroidManifest.xml

@ -39,7 +39,6 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" /> <data android:scheme="https" />
<data android:host="call.element.io" /> <data android:host="call.element.io" />

36
features/call/src/main/kotlin/io/element/android/features/call/CallIntentDataParser.kt

@ -21,13 +21,13 @@ import javax.inject.Inject
class CallIntentDataParser @Inject constructor() { class CallIntentDataParser @Inject constructor() {
private val validHttpSchemes = sequenceOf("http", "https") private val validHttpSchemes = sequenceOf("https")
fun parse(data: String?): String? { fun parse(data: String?): String? {
val parsedUrl = data?.let { Uri.parse(data) } ?: return null val parsedUrl = data?.let { Uri.parse(data) } ?: return null
val scheme = parsedUrl.scheme val scheme = parsedUrl.scheme
return when { return when {
scheme in validHttpSchemes && parsedUrl.host == "call.element.io" -> data scheme in validHttpSchemes && parsedUrl.host == "call.element.io" -> parsedUrl
scheme == "element" && parsedUrl.host == "call" -> { scheme == "element" && parsedUrl.host == "call" -> {
// We use this custom scheme to load arbitrary URLs for other instances of Element Call, // We use this custom scheme to load arbitrary URLs for other instances of Element Call,
// so we can only verify it's an HTTP/HTTPs URL with a non-empty host // so we can only verify it's an HTTP/HTTPs URL with a non-empty host
@ -40,14 +40,36 @@ class CallIntentDataParser @Inject constructor() {
} }
// This should never be possible, but we still need to take into account the possibility // This should never be possible, but we still need to take into account the possibility
else -> null else -> null
} }?.withCustomParameters()
} }
private fun Uri.getUrlParameter(): String? { private fun Uri.getUrlParameter(): Uri? {
return getQueryParameter("url") return getQueryParameter("url")
?.takeIf { ?.let { urlParameter ->
val internalUri = Uri.parse(it) Uri.parse(urlParameter).takeIf { uri ->
internalUri.scheme in validHttpSchemes && !internalUri.host.isNullOrBlank() uri.scheme in validHttpSchemes && !uri.host.isNullOrBlank()
}
} }
} }
} }
/**
* Ensure the uri has the following parameters and value:
* - appPrompt=false
* - confineToRoom=true
* to ensure that the rendering will bo correct on the embedded Webview.
*/
private fun Uri.withCustomParameters(): String {
val builder = buildUpon()
builder.clearQuery()
queryParameterNames.forEach {
if (it == APP_PROMPT_PARAMETER || it == CONFINE_TO_ROOM_PARAMETER) return@forEach
builder.appendQueryParameter(it, getQueryParameter(it))
}
builder.appendQueryParameter(APP_PROMPT_PARAMETER, "false")
builder.appendQueryParameter(CONFINE_TO_ROOM_PARAMETER, "true")
return builder.build().toString()
}
private const val APP_PROMPT_PARAMETER = "appPrompt"
private const val CONFINE_TO_ROOM_PARAMETER = "confineToRoom"

6
features/call/src/main/res/values-cs/translations.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="call_foreground_service_channel_title_android">"Probíhající hovor"</string>
<string name="call_foreground_service_message_android">"Klepněte pro návrat k hovoru"</string>
<string name="call_foreground_service_title_android">"☎ Probíhá hovor"</string>
</resources>

5
features/call/src/main/res/values-zh-rTW/translations.xml

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="call_foreground_service_message_android">"點擊以返回到通話頁面"</string>
<string name="call_foreground_service_title_android">"☎ 通話中"</string>
</resources>

76
features/call/src/test/kotlin/io/element/android/features/call/CallIntentDataParserTests.kt

@ -52,15 +52,19 @@ class CallIntentDataParserTests {
} }
@Test @Test
fun `Element Call urls will be returned as is`() { fun `Element Call http urls returns null`() {
val httpBaseUrl = "http://call.element.io" val httpBaseUrl = "http://call.element.io"
val httpCallUrl = "http://call.element.io/some-actual-call?with=parameters" val httpCallUrl = "http://call.element.io/some-actual-call?with=parameters"
assertThat(callIntentDataParser.parse(httpBaseUrl)).isNull()
assertThat(callIntentDataParser.parse(httpCallUrl)).isNull()
}
@Test
fun `Element Call urls will be returned as is`() {
val httpsBaseUrl = "https://call.element.io" val httpsBaseUrl = "https://call.element.io"
val httpsCallUrl = "https://call.element.io/some-actual-call?with=parameters" val httpsCallUrl = VALID_CALL_URL_WITH_PARAM
assertThat(callIntentDataParser.parse(httpBaseUrl)).isEqualTo(httpBaseUrl) assertThat(callIntentDataParser.parse(httpsBaseUrl)).isEqualTo("$httpsBaseUrl?$EXTRA_PARAMS")
assertThat(callIntentDataParser.parse(httpCallUrl)).isEqualTo(httpCallUrl) assertThat(callIntentDataParser.parse(httpsCallUrl)).isEqualTo("$httpsCallUrl&$EXTRA_PARAMS")
assertThat(callIntentDataParser.parse(httpsBaseUrl)).isEqualTo(httpsBaseUrl)
assertThat(callIntentDataParser.parse(httpsCallUrl)).isEqualTo(httpsCallUrl)
} }
@Test @Test
@ -76,19 +80,35 @@ class CallIntentDataParserTests {
} }
@Test @Test
fun `element scheme with call host and url param gets url extracted`() { fun `element scheme with call host and url with http will returns null`() {
val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters" val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters"
val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8")
val url = "element://call?url=$encodedUrl" val url = "element://call?url=$encodedUrl"
assertThat(callIntentDataParser.parse(url)).isEqualTo(embeddedUrl) assertThat(callIntentDataParser.parse(url)).isNull()
} }
@Test @Test
fun `element scheme 2 with url param gets url extracted`() { fun `element scheme with call host and url param gets url extracted`() {
val embeddedUrl = VALID_CALL_URL_WITH_PARAM
val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8")
val url = "element://call?url=$encodedUrl"
assertThat(callIntentDataParser.parse(url)).isEqualTo("$VALID_CALL_URL_WITH_PARAM&$EXTRA_PARAMS")
}
@Test
fun `element scheme 2 with url param with http returns null`() {
val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters" val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters"
val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8")
val url = "io.element.call:/?url=$encodedUrl" val url = "io.element.call:/?url=$encodedUrl"
assertThat(callIntentDataParser.parse(url)).isEqualTo(embeddedUrl) assertThat(callIntentDataParser.parse(url)).isNull()
}
@Test
fun `element scheme 2 with url param gets url extracted`() {
val embeddedUrl = VALID_CALL_URL_WITH_PARAM
val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8")
val url = "io.element.call:/?url=$encodedUrl"
assertThat(callIntentDataParser.parse(url)).isEqualTo("$VALID_CALL_URL_WITH_PARAM&$EXTRA_PARAMS")
} }
@Test @Test
@ -101,7 +121,7 @@ class CallIntentDataParserTests {
@Test @Test
fun `element scheme 2 with no url returns null`() { fun `element scheme 2 with no url returns null`() {
val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters" val embeddedUrl = VALID_CALL_URL_WITH_PARAM
val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8")
val url = "io.element.call:/?no_url=$encodedUrl" val url = "io.element.call:/?no_url=$encodedUrl"
assertThat(callIntentDataParser.parse(url)).isNull() assertThat(callIntentDataParser.parse(url)).isNull()
@ -109,7 +129,7 @@ class CallIntentDataParserTests {
@Test @Test
fun `element scheme with no call host returns null`() { fun `element scheme with no call host returns null`() {
val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters" val embeddedUrl = VALID_CALL_URL_WITH_PARAM
val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8")
val url = "element://no-call?url=$encodedUrl" val url = "element://no-call?url=$encodedUrl"
assertThat(callIntentDataParser.parse(url)).isNull() assertThat(callIntentDataParser.parse(url)).isNull()
@ -129,9 +149,39 @@ class CallIntentDataParserTests {
@Test @Test
fun `element invalid scheme returns null`() { fun `element invalid scheme returns null`() {
val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters" val embeddedUrl = VALID_CALL_URL_WITH_PARAM
val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8")
val url = "bad.scheme:/?url=$encodedUrl" val url = "bad.scheme:/?url=$encodedUrl"
assertThat(callIntentDataParser.parse(url)).isNull() assertThat(callIntentDataParser.parse(url)).isNull()
} }
@Test
fun `element scheme 2 with url extra param appPrompt gets url extracted`() {
val embeddedUrl = "${VALID_CALL_URL_WITH_PARAM}&appPrompt=true"
val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8")
val url = "io.element.call:/?url=$encodedUrl"
assertThat(callIntentDataParser.parse(url)).isEqualTo("$VALID_CALL_URL_WITH_PARAM&$EXTRA_PARAMS")
}
@Test
fun `element scheme 2 with url extra param confineToRoom gets url extracted`() {
val embeddedUrl = "${VALID_CALL_URL_WITH_PARAM}&confineToRoom=false"
val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8")
val url = "io.element.call:/?url=$encodedUrl"
assertThat(callIntentDataParser.parse(url)).isEqualTo("$VALID_CALL_URL_WITH_PARAM&$EXTRA_PARAMS")
}
@Test
fun `element scheme 2 with url fragment gets url extracted`() {
val embeddedUrl = "${VALID_CALL_URL_WITH_PARAM}#fragment"
val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8")
val url = "io.element.call:/?url=$encodedUrl"
assertThat(callIntentDataParser.parse(url)).isEqualTo("$VALID_CALL_URL_WITH_PARAM&$EXTRA_PARAMS#fragment")
}
companion object {
const val VALID_CALL_URL_WITH_PARAM = "https://call.element.io/some-actual-call?with=parameters"
const val EXTRA_PARAMS = "appPrompt=false&confineToRoom=true"
}
} }

2
features/ftue/impl/src/main/res/values-cs/translations.xml

@ -2,6 +2,8 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_migration_message">"Jedná se o jednorázový proces, prosíme o strpení."</string> <string name="screen_migration_message">"Jedná se o jednorázový proces, prosíme o strpení."</string>
<string name="screen_migration_title">"Nastavení vašeho účtu"</string> <string name="screen_migration_title">"Nastavení vašeho účtu"</string>
<string name="screen_notification_optin_subtitle">"Nastavení můžete později změnit."</string>
<string name="screen_notification_optin_title">"Povolte oznámení a nezmeškejte žádnou zprávu"</string>
<string name="screen_welcome_bullet_1">"Hovory, hlasování, vyhledávání a další budou přidány koncem tohoto roku."</string> <string name="screen_welcome_bullet_1">"Hovory, hlasování, vyhledávání a další budou přidány koncem tohoto roku."</string>
<string name="screen_welcome_bullet_2">"Historie zpráv šifrovaných místností nebude v této aktualizaci k dispozici."</string> <string name="screen_welcome_bullet_2">"Historie zpráv šifrovaných místností nebude v této aktualizaci k dispozici."</string>
<string name="screen_welcome_bullet_3">"Rádi bychom se od vás dozvěděli, co si o tom myslíte, dejte nám vědět prostřednictvím stránky s nastavením."</string> <string name="screen_welcome_bullet_3">"Rádi bychom se od vás dozvěděli, co si o tom myslíte, dejte nám vědět prostřednictvím stránky s nastavením."</string>

4
features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt

@ -21,7 +21,7 @@ import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -64,7 +64,7 @@ fun StaticMapView(
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
val context = LocalContext.current val context = LocalContext.current
var retryHash by remember { mutableStateOf(0) } var retryHash by remember { mutableIntStateOf(0) }
val builder = remember { StaticMapUrlBuilder(context) } val builder = remember { StaticMapUrlBuilder(context) }
val painter = rememberAsyncImagePainter( val painter = rememberAsyncImagePainter(
model = if (constraints.isZero) { model = if (constraints.isZero) {

7
features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListPresenter.kt

@ -18,6 +18,7 @@ package io.element.android.features.login.impl.screens.waitlistscreen
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
@ -58,14 +59,14 @@ class WaitListPresenter @AssistedInject constructor(
mutableStateOf(Async.Uninitialized) mutableStateOf(Async.Uninitialized)
} }
val attemptNumber: MutableState<Int> = remember { mutableStateOf(0) } val attemptNumber = remember { mutableIntStateOf(0) }
fun handleEvents(event: WaitListEvents) { fun handleEvents(event: WaitListEvents) {
when (event) { when (event) {
WaitListEvents.AttemptLogin -> { WaitListEvents.AttemptLogin -> {
// Do not attempt to login on first resume of the View. // Do not attempt to login on first resume of the View.
attemptNumber.value++ attemptNumber.intValue++
if (attemptNumber.value > 1) { if (attemptNumber.intValue > 1) {
coroutineScope.loginAttempt(formState, loginAction) coroutineScope.loginAttempt(formState, loginAction)
} }
} }

4
features/login/impl/src/main/res/values-ru/translations.xml

@ -14,9 +14,9 @@
<string name="screen_change_account_provider_subtitle">"Используйте другого поставщика учетных записей, например, собственный частный сервер или рабочую учетную запись."</string> <string name="screen_change_account_provider_subtitle">"Используйте другого поставщика учетных записей, например, собственный частный сервер или рабочую учетную запись."</string>
<string name="screen_change_account_provider_title">"Сменить поставщика учетной записи"</string> <string name="screen_change_account_provider_title">"Сменить поставщика учетной записи"</string>
<string name="screen_change_server_error_invalid_homeserver">"Нам не удалось связаться с этим домашним сервером. Убедитесь, что вы правильно ввели URL-адрес домашнего сервера. Если URL-адрес указан правильно, обратитесь к администратору домашнего сервера за дополнительной помощью."</string> <string name="screen_change_server_error_invalid_homeserver">"Нам не удалось связаться с этим домашним сервером. Убедитесь, что вы правильно ввели URL-адрес домашнего сервера. Если URL-адрес указан правильно, обратитесь к администратору домашнего сервера за дополнительной помощью."</string>
<string name="screen_change_server_error_no_sliding_sync_message">"В настоящее время этот сервер не поддерживает скользящую синхронизацию."</string> <string name="screen_change_server_error_no_sliding_sync_message">"К сожалению данный сервер не поддерживает sliding sync."</string>
<string name="screen_change_server_form_header">"URL-адрес домашнего сервера"</string> <string name="screen_change_server_form_header">"URL-адрес домашнего сервера"</string>
<string name="screen_change_server_form_notice">"Вы можете подключиться только к существующему серверу, поддерживающему скользящую синхронизацию. Администратору домашнего сервера потребуется настроить его. %1$s"</string> <string name="screen_change_server_form_notice">"Вы можете подключиться только к существующему серверу, поддерживающему sliding sync. Администратору домашнего сервера потребуется настроить его. %1$s"</string>
<string name="screen_change_server_subtitle">"Какой адрес у вашего сервера?"</string> <string name="screen_change_server_subtitle">"Какой адрес у вашего сервера?"</string>
<string name="screen_login_error_deactivated_account">"Данная учетная запись была деактивирована."</string> <string name="screen_login_error_deactivated_account">"Данная учетная запись была деактивирована."</string>
<string name="screen_login_error_invalid_credentials">"Неверное имя пользователя и/или пароль"</string> <string name="screen_login_error_invalid_credentials">"Неверное имя пользователя и/или пароль"</string>

20
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt

@ -30,7 +30,6 @@ import androidx.compose.runtime.setValue
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.PollEnd
import io.element.android.features.messages.impl.actionlist.ActionListEvents import io.element.android.features.messages.impl.actionlist.ActionListEvents
import io.element.android.features.messages.impl.actionlist.ActionListPresenter import io.element.android.features.messages.impl.actionlist.ActionListPresenter
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
@ -39,6 +38,7 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.TimelineEvents
import io.element.android.features.messages.impl.timeline.TimelinePresenter import io.element.android.features.messages.impl.timeline.TimelinePresenter
import io.element.android.features.messages.impl.timeline.TimelineState
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryPresenter import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryPresenter
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuPresenter import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuPresenter
@ -76,7 +76,6 @@ import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
import io.element.android.libraries.matrix.ui.room.canRedactAsState import io.element.android.libraries.matrix.ui.room.canRedactAsState
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
import io.element.android.libraries.textcomposer.MessageComposerMode import io.element.android.libraries.textcomposer.MessageComposerMode
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -95,7 +94,6 @@ class MessagesPresenter @AssistedInject constructor(
private val messageSummaryFormatter: MessageSummaryFormatter, private val messageSummaryFormatter: MessageSummaryFormatter,
private val dispatchers: CoroutineDispatchers, private val dispatchers: CoroutineDispatchers,
private val clipboardHelper: ClipboardHelper, private val clipboardHelper: ClipboardHelper,
private val analyticsService: AnalyticsService,
private val preferencesStore: PreferencesStore, private val preferencesStore: PreferencesStore,
@Assisted private val navigator: MessagesNavigator, @Assisted private val navigator: MessagesNavigator,
) : Presenter<MessagesState> { ) : Presenter<MessagesState> {
@ -155,6 +153,7 @@ class MessagesPresenter @AssistedInject constructor(
targetEvent = event.event, targetEvent = event.event,
composerState = composerState, composerState = composerState,
enableTextFormatting = enableTextFormatting, enableTextFormatting = enableTextFormatting,
timelineState = timelineState,
) )
} }
is MessagesEvents.ToggleReaction -> { is MessagesEvents.ToggleReaction -> {
@ -206,6 +205,7 @@ class MessagesPresenter @AssistedInject constructor(
targetEvent: TimelineItem.Event, targetEvent: TimelineItem.Event,
composerState: MessageComposerState, composerState: MessageComposerState,
enableTextFormatting: Boolean, enableTextFormatting: Boolean,
timelineState: TimelineState,
) = launch { ) = launch {
when (action) { when (action) {
TimelineItemAction.Copy -> handleCopyContents(targetEvent) TimelineItemAction.Copy -> handleCopyContents(targetEvent)
@ -216,7 +216,7 @@ class MessagesPresenter @AssistedInject constructor(
TimelineItemAction.ViewSource -> handleShowDebugInfoAction(targetEvent) TimelineItemAction.ViewSource -> handleShowDebugInfoAction(targetEvent)
TimelineItemAction.Forward -> handleForwardAction(targetEvent) TimelineItemAction.Forward -> handleForwardAction(targetEvent)
TimelineItemAction.ReportContent -> handleReportAction(targetEvent) TimelineItemAction.ReportContent -> handleReportAction(targetEvent)
TimelineItemAction.EndPoll -> handleEndPollAction(targetEvent) TimelineItemAction.EndPoll -> handleEndPollAction(targetEvent, timelineState)
} }
} }
@ -266,7 +266,7 @@ class MessagesPresenter @AssistedInject constructor(
targetEvent: TimelineItem.Event, targetEvent: TimelineItem.Event,
composerState: MessageComposerState, composerState: MessageComposerState,
enableTextFormatting: Boolean, enableTextFormatting: Boolean,
) { ) {
val composerMode = MessageComposerMode.Edit( val composerMode = MessageComposerMode.Edit(
targetEvent.eventId, targetEvent.eventId,
(targetEvent.content as? TimelineItemTextBasedContent)?.let { (targetEvent.content as? TimelineItemTextBasedContent)?.let {
@ -344,11 +344,11 @@ class MessagesPresenter @AssistedInject constructor(
navigator.onReportContentClicked(event.eventId, event.senderId) navigator.onReportContentClicked(event.eventId, event.senderId)
} }
private suspend fun handleEndPollAction(event: TimelineItem.Event) { private fun handleEndPollAction(
event.eventId?.let { event: TimelineItem.Event,
room.endPoll(it, "The poll with event id: $it has ended.") timelineState: TimelineState,
analyticsService.capture(PollEnd()) ) {
} event.eventId?.let { timelineState.eventSink(TimelineEvents.PollEndClicked(it)) }
} }
private suspend fun handleCopyContents(event: TimelineItem.Event) { private suspend fun handleCopyContents(event: TimelineItem.Event) {

2
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt

@ -108,7 +108,7 @@ class ActionListPresenter @Inject constructor(
buildList { buildList {
val isMineOrCanRedact = timelineItem.isMine || userCanRedact val isMineOrCanRedact = timelineItem.isMine || userCanRedact
// TODO Poll: Reply to poll. Ensure to update `fun TimelineItemEventContent.canBeReplied()` // TODO Polls: Reply to poll. Ensure to update `fun TimelineItemEventContent.canBeReplied()`
// when touching this // when touching this
// if (timelineItem.isRemote) { // if (timelineItem.isRemote) {
// // Can only reply or forward messages already uploaded to the server // // Can only reply or forward messages already uploaded to the server

54
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaActions.kt

@ -16,6 +16,7 @@
package io.element.android.features.messages.impl.media.local package io.element.android.features.messages.impl.media.local
import android.app.Activity
import android.content.ContentResolver import android.content.ContentResolver
import android.content.ContentValues import android.content.ContentValues
import android.content.Context import android.content.Context
@ -24,17 +25,25 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.MediaStore import android.provider.MediaStore
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.net.toFile import androidx.core.net.toFile
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.androidutils.system.startInstallFromSourceIntent
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.ApplicationContext
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
@ -50,10 +59,27 @@ class AndroidLocalMediaActions @Inject constructor(
) : LocalMediaActions { ) : LocalMediaActions {
private var activityContext: Context? = null private var activityContext: Context? = null
private var apkInstallLauncher: ManagedActivityResultLauncher<Intent, ActivityResult>? = null
private var pendingMedia: LocalMedia? = null
@Composable @Composable
override fun Configure() { override fun Configure() {
val context = LocalContext.current val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
apkInstallLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult(),
) { activityResult ->
if (activityResult.resultCode == Activity.RESULT_OK) {
pendingMedia?.let {
coroutineScope.launch {
openFile(it)
}
}
} else {
// User cancelled
}
pendingMedia = null
}
return DisposableEffect(Unit) { return DisposableEffect(Unit) {
activityContext = context activityContext = context
onDispose { onDispose {
@ -99,11 +125,20 @@ class AndroidLocalMediaActions @Inject constructor(
override suspend fun open(localMedia: LocalMedia): Result<Unit> = withContext(coroutineDispatchers.io) { override suspend fun open(localMedia: LocalMedia): Result<Unit> = withContext(coroutineDispatchers.io) {
require(localMedia.uri.scheme == ContentResolver.SCHEME_FILE) require(localMedia.uri.scheme == ContentResolver.SCHEME_FILE)
runCatching { runCatching {
val openMediaIntent = Intent(Intent.ACTION_VIEW) when (localMedia.info.mimeType) {
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) MimeTypes.Apk -> {
.setDataAndType(localMedia.toShareableUri(), localMedia.info.mimeType) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
withContext(coroutineDispatchers.main) { if (activityContext?.packageManager?.canRequestPackageInstalls() == false) {
activityContext!!.startActivity(openMediaIntent) pendingMedia = localMedia
activityContext?.startInstallFromSourceIntent(apkInstallLauncher!!).let { }
} else {
openFile(localMedia)
}
} else {
openFile(localMedia)
}
}
else -> openFile(localMedia)
} }
}.onSuccess { }.onSuccess {
Timber.v("Open media succeed") Timber.v("Open media succeed")
@ -112,6 +147,15 @@ class AndroidLocalMediaActions @Inject constructor(
} }
} }
private suspend fun openFile(localMedia: LocalMedia) {
val openMediaIntent = Intent(Intent.ACTION_VIEW)
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.setDataAndType(localMedia.toShareableUri(), localMedia.info.mimeType)
withContext(coroutineDispatchers.main) {
activityContext?.startActivity(openMediaIntent)
}
}
private fun LocalMedia.toShareableUri(): Uri { private fun LocalMedia.toShareableUri(): Uri {
val mediaAsFile = this.toFile() val mediaAsFile = this.toFile()
val authority = "${buildMeta.applicationId}.fileprovider" val authority = "${buildMeta.applicationId}.fileprovider"

3
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt

@ -21,6 +21,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
@ -59,7 +60,7 @@ class MediaViewerPresenter @AssistedInject constructor(
@Composable @Composable
override fun present(): MediaViewerState { override fun present(): MediaViewerState {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
var loadMediaTrigger by remember { mutableStateOf(0) } var loadMediaTrigger by remember { mutableIntStateOf(0) }
val mediaFile: MutableState<MediaFile?> = remember { val mediaFile: MutableState<MediaFile?> = remember {
mutableStateOf(null) mutableStateOf(null)
} }

18
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt

@ -47,11 +47,13 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import io.element.android.features.messages.impl.R
import io.element.android.features.messages.impl.media.local.LocalMedia import io.element.android.features.messages.impl.media.local.LocalMedia
import io.element.android.features.messages.impl.media.local.LocalMediaView import io.element.android.features.messages.impl.media.local.LocalMediaView
import io.element.android.features.messages.impl.media.local.MediaInfo import io.element.android.features.messages.impl.media.local.MediaInfo
import io.element.android.features.messages.impl.media.local.rememberLocalMediaViewState import io.element.android.features.messages.impl.media.local.rememberLocalMediaViewState
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewDark
@ -92,6 +94,7 @@ fun MediaViewerView(
topBar = { topBar = {
MediaViewerTopBar( MediaViewerTopBar(
actionsEnabled = state.downloadedMedia is Async.Success, actionsEnabled = state.downloadedMedia is Async.Success,
mimeType = state.mediaInfo.mimeType,
onBackPressed = onBackPressed, onBackPressed = onBackPressed,
eventSink = state.eventSink eventSink = state.eventSink
) )
@ -162,6 +165,7 @@ private fun rememberShowProgress(downloadedMedia: Async<LocalMedia>): Boolean {
@Composable @Composable
private fun MediaViewerTopBar( private fun MediaViewerTopBar(
actionsEnabled: Boolean, actionsEnabled: Boolean,
mimeType: String,
onBackPressed: () -> Unit, onBackPressed: () -> Unit,
eventSink: (MediaViewerEvents) -> Unit, eventSink: (MediaViewerEvents) -> Unit,
) { ) {
@ -175,10 +179,16 @@ private fun MediaViewerTopBar(
eventSink(MediaViewerEvents.OpenWith) eventSink(MediaViewerEvents.OpenWith)
}, },
) { ) {
Icon( when (mimeType) {
imageVector = Icons.Default.OpenInNew, MimeTypes.Apk -> Icon(
contentDescription = stringResource(id = CommonStrings.action_open_with) resourceId = R.drawable.ic_apk_install,
) contentDescription = stringResource(id = CommonStrings.common_install_apk_android)
)
else -> Icon(
imageVector = Icons.Default.OpenInNew,
contentDescription = stringResource(id = CommonStrings.action_open_with)
)
}
} }
IconButton( IconButton(
enabled = actionsEnabled, enabled = actionsEnabled,

7
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt

@ -154,12 +154,10 @@ class MessageComposerPresenter @Inject constructor(
fun handleEvents(event: MessageComposerEvents) { fun handleEvents(event: MessageComposerEvents) {
when (event) { when (event) {
MessageComposerEvents.ToggleFullScreenState -> isFullScreen.value = !isFullScreen.value MessageComposerEvents.ToggleFullScreenState -> isFullScreen.value = !isFullScreen.value
MessageComposerEvents.CloseSpecialMode -> { MessageComposerEvents.CloseSpecialMode -> {
richTextEditorState.setHtml("") richTextEditorState.setHtml("")
messageComposerContext.composerMode = MessageComposerMode.Normal("") messageComposerContext.composerMode = MessageComposerMode.Normal("")
} }
is MessageComposerEvents.SendMessage -> appCoroutineScope.sendMessage( is MessageComposerEvents.SendMessage -> appCoroutineScope.sendMessage(
message = event.message, message = event.message,
updateComposerMode = { messageComposerContext.composerMode = it }, updateComposerMode = { messageComposerContext.composerMode = it },
@ -167,6 +165,11 @@ class MessageComposerPresenter @Inject constructor(
) )
is MessageComposerEvents.SetMode -> { is MessageComposerEvents.SetMode -> {
messageComposerContext.composerMode = event.composerMode messageComposerContext.composerMode = event.composerMode
if (event.composerMode is MessageComposerMode.Reply) {
appCoroutineScope.launch {
room.enterReplyMode(event.composerMode.eventId)
}
}
} }
MessageComposerEvents.AddAttachment -> localCoroutineScope.launch { MessageComposerEvents.AddAttachment -> localCoroutineScope.launch {
showAttachmentSourcePicker = true showAttachmentSourcePicker = true

4
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt

@ -26,4 +26,8 @@ sealed interface TimelineEvents {
val pollStartId: EventId, val pollStartId: EventId,
val answerId: String val answerId: String
) : TimelineEvents ) : TimelineEvents
data class PollEndClicked(
val pollStartId: EventId,
) : TimelineEvents
} }

12
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt

@ -26,6 +26,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import im.vector.app.features.analytics.plan.PollEnd
import im.vector.app.features.analytics.plan.PollVote import im.vector.app.features.analytics.plan.PollVote
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItem
@ -98,11 +99,18 @@ class TimelinePresenter @Inject constructor(
) )
analyticsService.capture(PollVote()) analyticsService.capture(PollVote())
} }
is TimelineEvents.PollEndClicked -> appScope.launch {
room.endPoll(
pollStartId = event.pollStartId,
text = "The poll with event id: ${event.pollStartId} has ended."
)
analyticsService.capture(PollEnd())
}
} }
} }
LaunchedEffect(timelineItems.size) { LaunchedEffect(timelineItems.size) {
computeHasNewItems(timelineItems, prevMostRecentItemId, hasNewItems) computeHasNewItems(timelineItems, prevMostRecentItemId, hasNewItems)
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@ -123,7 +131,7 @@ class TimelinePresenter @Inject constructor(
paginationState = paginationState, paginationState = paginationState,
timelineItems = timelineItems, timelineItems = timelineItems,
hasNewItems = hasNewItems.value, hasNewItems = hasNewItems.value,
eventSink = { handleEvents(it) } eventSink = ::handleEvents
) )
} }

13
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt

@ -103,10 +103,6 @@ fun TimelineView(
// TODO implement this logic once we have support to 'jump to event X' in sliding sync // TODO implement this logic once we have support to 'jump to event X' in sliding sync
} }
fun onPollAnswerSelected(pollStartId: EventId, answerId: String) {
state.eventSink(TimelineEvents.PollAnswerSelected(pollStartId, answerId))
}
// Animate alpha when timeline is first displayed, to avoid flashes or glitching when viewing rooms // Animate alpha when timeline is first displayed, to avoid flashes or glitching when viewing rooms
val alpha by alphaAnimation(label = "alpha for timeline") val alpha by alphaAnimation(label = "alpha for timeline")
@ -134,7 +130,7 @@ fun TimelineView(
onReactionLongClick = onReactionLongClicked, onReactionLongClick = onReactionLongClicked,
onMoreReactionsClick = onMoreReactionsClicked, onMoreReactionsClick = onMoreReactionsClicked,
onTimestampClicked = onTimestampClicked, onTimestampClicked = onTimestampClicked,
onPollAnswerSelected = ::onPollAnswerSelected, eventSink = state.eventSink,
onSwipeToReply = onSwipeToReply, onSwipeToReply = onSwipeToReply,
) )
} }
@ -172,7 +168,7 @@ fun TimelineItemRow(
onMoreReactionsClick: (TimelineItem.Event) -> Unit, onMoreReactionsClick: (TimelineItem.Event) -> Unit,
onTimestampClicked: (TimelineItem.Event) -> Unit, onTimestampClicked: (TimelineItem.Event) -> Unit,
onSwipeToReply: (TimelineItem.Event) -> Unit, onSwipeToReply: (TimelineItem.Event) -> Unit,
onPollAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit, eventSink: (TimelineEvents) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
when (timelineItem) { when (timelineItem) {
@ -189,6 +185,7 @@ fun TimelineItemRow(
isHighlighted = highlightedItem == timelineItem.identifier(), isHighlighted = highlightedItem == timelineItem.identifier(),
onClick = { onClick(timelineItem) }, onClick = { onClick(timelineItem) },
onLongClick = { onLongClick(timelineItem) }, onLongClick = { onLongClick(timelineItem) },
eventSink = eventSink,
modifier = modifier, modifier = modifier,
) )
} else { } else {
@ -205,7 +202,7 @@ fun TimelineItemRow(
onMoreReactionsClick = onMoreReactionsClick, onMoreReactionsClick = onMoreReactionsClick,
onTimestampClicked = onTimestampClicked, onTimestampClicked = onTimestampClicked,
onSwipeToReply = { onSwipeToReply(timelineItem) }, onSwipeToReply = { onSwipeToReply(timelineItem) },
onPollAnswerSelected = onPollAnswerSelected, eventSink = eventSink,
modifier = modifier, modifier = modifier,
) )
} }
@ -243,7 +240,7 @@ fun TimelineItemRow(
onReactionClick = onReactionClick, onReactionClick = onReactionClick,
onReactionLongClick = onReactionLongClick, onReactionLongClick = onReactionLongClick,
onMoreReactionsClick = onMoreReactionsClick, onMoreReactionsClick = onMoreReactionsClick,
onPollAnswerSelected = onPollAnswerSelected, eventSink = eventSink,
onSwipeToReply = {}, onSwipeToReply = {},
) )
} }

36
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt

@ -61,6 +61,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import androidx.constraintlayout.compose.ConstrainScope import androidx.constraintlayout.compose.ConstrainScope
import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.ConstraintLayout
import io.element.android.features.messages.impl.timeline.TimelineEvents
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView
@ -80,9 +81,9 @@ import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
import io.element.android.libraries.designsystem.components.EqualWidthColumn import io.element.android.libraries.designsystem.components.EqualWidthColumn
import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.swipe.SwipeableActionsState import io.element.android.libraries.designsystem.swipe.SwipeableActionsState
import io.element.android.libraries.designsystem.swipe.rememberSwipeableActionsState import io.element.android.libraries.designsystem.swipe.rememberSwipeableActionsState
import io.element.android.libraries.designsystem.text.toPx import io.element.android.libraries.designsystem.text.toPx
@ -123,7 +124,7 @@ fun TimelineItemEventRow(
onReactionLongClick: (emoji: String, eventId: TimelineItem.Event) -> Unit, onReactionLongClick: (emoji: String, eventId: TimelineItem.Event) -> Unit,
onMoreReactionsClick: (eventId: TimelineItem.Event) -> Unit, onMoreReactionsClick: (eventId: TimelineItem.Event) -> Unit,
onSwipeToReply: () -> Unit, onSwipeToReply: () -> Unit,
onPollAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit, eventSink: (TimelineEvents) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
@ -146,7 +147,7 @@ fun TimelineItemEventRow(
} }
if (canReply) { if (canReply) {
val state: SwipeableActionsState = rememberSwipeableActionsState() val state: SwipeableActionsState = rememberSwipeableActionsState()
val offset = state.offset.value val offset = state.offset.floatValue
val swipeThresholdPx = 40.dp.toPx() val swipeThresholdPx = 40.dp.toPx()
val thresholdCrossed = abs(offset) > swipeThresholdPx val thresholdCrossed = abs(offset) > swipeThresholdPx
SwipeSensitivity(3f) { SwipeSensitivity(3f) {
@ -181,7 +182,7 @@ fun TimelineItemEventRow(
onReactionClicked = { emoji -> onReactionClick(emoji, event) }, onReactionClicked = { emoji -> onReactionClick(emoji, event) },
onReactionLongClicked = { emoji -> onReactionLongClick(emoji, event) }, onReactionLongClicked = { emoji -> onReactionLongClick(emoji, event) },
onMoreReactionsClicked = { onMoreReactionsClick(event) }, onMoreReactionsClicked = { onMoreReactionsClick(event) },
onPollAnswerSelected = onPollAnswerSelected, eventSink = eventSink,
) )
} }
} }
@ -198,7 +199,7 @@ fun TimelineItemEventRow(
onReactionClicked = { emoji -> onReactionClick(emoji, event) }, onReactionClicked = { emoji -> onReactionClick(emoji, event) },
onReactionLongClicked = { emoji -> onReactionLongClick(emoji, event) }, onReactionLongClicked = { emoji -> onReactionLongClick(emoji, event) },
onMoreReactionsClicked = { onMoreReactionsClick(event) }, onMoreReactionsClicked = { onMoreReactionsClick(event) },
onPollAnswerSelected = onPollAnswerSelected, eventSink = eventSink,
) )
} }
} }
@ -240,7 +241,7 @@ private fun TimelineItemEventRowContent(
onReactionClicked: (emoji: String) -> Unit, onReactionClicked: (emoji: String) -> Unit,
onReactionLongClicked: (emoji: String) -> Unit, onReactionLongClicked: (emoji: String) -> Unit,
onMoreReactionsClicked: (event: TimelineItem.Event) -> Unit, onMoreReactionsClicked: (event: TimelineItem.Event) -> Unit,
onPollAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit, eventSink: (TimelineEvents) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
fun ConstrainScope.linkStartOrEnd(event: TimelineItem.Event) = if (event.isMine) { fun ConstrainScope.linkStartOrEnd(event: TimelineItem.Event) = if (event.isMine) {
@ -299,7 +300,7 @@ private fun TimelineItemEventRowContent(
onTimestampClicked = { onTimestampClicked = {
onTimestampClicked(event) onTimestampClicked(event)
}, },
onPollAnswerSelected = onPollAnswerSelected, eventSink = eventSink,
) )
} }
@ -371,7 +372,7 @@ private fun MessageEventBubbleContent(
onMessageLongClick: () -> Unit, onMessageLongClick: () -> Unit,
inReplyToClick: () -> Unit, inReplyToClick: () -> Unit,
onTimestampClicked: () -> Unit, onTimestampClicked: () -> Unit,
onPollAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit, eventSink: (TimelineEvents) -> Unit,
@SuppressLint("ModifierParameter") bubbleModifier: Modifier = Modifier, // need to rename this modifier to distinguish it from the following ones @SuppressLint("ModifierParameter") bubbleModifier: Modifier = Modifier, // need to rename this modifier to distinguish it from the following ones
) { ) {
@ -385,11 +386,12 @@ private fun MessageEventBubbleContent(
) { ) {
TimelineItemEventContentView( TimelineItemEventContentView(
content = event.content, content = event.content,
isMine = event.isMine,
interactionSource = interactionSource, interactionSource = interactionSource,
onClick = onMessageClick, onClick = onMessageClick,
onLongClick = onMessageLongClick, onLongClick = onMessageLongClick,
extraPadding = event.toExtraPadding(), extraPadding = event.toExtraPadding(),
onPollAnswerSelected = onPollAnswerSelected, eventSink = eventSink,
modifier = modifier, modifier = modifier,
) )
} }
@ -652,7 +654,7 @@ internal fun TimelineItemEventRowPreview() = ElementPreview {
onMoreReactionsClick = {}, onMoreReactionsClick = {},
onTimestampClicked = {}, onTimestampClicked = {},
onSwipeToReply = {}, onSwipeToReply = {},
onPollAnswerSelected = { _, _ -> }, eventSink = {},
) )
TimelineItemEventRow( TimelineItemEventRow(
event = aTimelineItemEvent( event = aTimelineItemEvent(
@ -673,7 +675,7 @@ internal fun TimelineItemEventRowPreview() = ElementPreview {
onMoreReactionsClick = {}, onMoreReactionsClick = {},
onTimestampClicked = {}, onTimestampClicked = {},
onSwipeToReply = {}, onSwipeToReply = {},
onPollAnswerSelected = { _, _ -> }, eventSink = {},
) )
} }
} }
@ -712,7 +714,7 @@ internal fun TimelineItemEventRowWithReplyPreview() = ElementPreview {
onMoreReactionsClick = {}, onMoreReactionsClick = {},
onTimestampClicked = {}, onTimestampClicked = {},
onSwipeToReply = {}, onSwipeToReply = {},
onPollAnswerSelected = { _, _ -> }, eventSink = {},
) )
TimelineItemEventRow( TimelineItemEventRow(
event = aTimelineItemEvent( event = aTimelineItemEvent(
@ -735,7 +737,7 @@ internal fun TimelineItemEventRowWithReplyPreview() = ElementPreview {
onMoreReactionsClick = {}, onMoreReactionsClick = {},
onTimestampClicked = {}, onTimestampClicked = {},
onSwipeToReply = {}, onSwipeToReply = {},
onPollAnswerSelected = { _, _ -> }, eventSink = {},
) )
} }
} }
@ -786,7 +788,7 @@ internal fun TimelineItemEventRowTimestampPreview(
onMoreReactionsClick = {}, onMoreReactionsClick = {},
onTimestampClicked = {}, onTimestampClicked = {},
onSwipeToReply = {}, onSwipeToReply = {},
onPollAnswerSelected = { _, _ -> }, eventSink = {},
) )
} }
} }
@ -818,7 +820,7 @@ internal fun TimelineItemEventRowWithManyReactionsPreview() = ElementPreview {
onMoreReactionsClick = {}, onMoreReactionsClick = {},
onSwipeToReply = {}, onSwipeToReply = {},
onTimestampClicked = {}, onTimestampClicked = {},
onPollAnswerSelected = { _, _ -> }, eventSink = {},
) )
} }
} }
@ -843,7 +845,7 @@ internal fun TimelineItemEventRowLongSenderNamePreview() = ElementPreviewLight {
onMoreReactionsClick = {}, onMoreReactionsClick = {},
onSwipeToReply = {}, onSwipeToReply = {},
onTimestampClicked = {}, onTimestampClicked = {},
onPollAnswerSelected = { _, _ -> }, eventSink = {},
) )
} }
@ -864,6 +866,6 @@ internal fun TimelineItemEventTimestampBelowPreview() = ElementPreviewLight {
onMoreReactionsClick = {}, onMoreReactionsClick = {},
onSwipeToReply = {}, onSwipeToReply = {},
onTimestampClicked = {}, onTimestampClicked = {},
onPollAnswerSelected = { _, _ -> }, eventSink = {},
) )
} }

8
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt

@ -28,6 +28,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import io.element.android.features.messages.impl.timeline.TimelineEvents
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView
import io.element.android.features.messages.impl.timeline.components.event.noExtraPadding import io.element.android.features.messages.impl.timeline.components.event.noExtraPadding
@ -35,8 +36,8 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
import io.element.android.features.messages.impl.timeline.util.defaultTimelineContentPadding import io.element.android.features.messages.impl.timeline.util.defaultTimelineContentPadding
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@Composable @Composable
fun TimelineItemStateEventRow( fun TimelineItemStateEventRow(
@ -44,6 +45,7 @@ fun TimelineItemStateEventRow(
isHighlighted: Boolean, isHighlighted: Boolean,
onClick: () -> Unit, onClick: () -> Unit,
onLongClick: () -> Unit, onLongClick: () -> Unit,
eventSink: (TimelineEvents) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
@ -65,11 +67,12 @@ fun TimelineItemStateEventRow(
) { ) {
TimelineItemEventContentView( TimelineItemEventContentView(
content = event.content, content = event.content,
isMine = event.isMine,
interactionSource = interactionSource, interactionSource = interactionSource,
onClick = onClick, onClick = onClick,
onLongClick = onLongClick, onLongClick = onLongClick,
extraPadding = noExtraPadding, extraPadding = noExtraPadding,
onPollAnswerSelected = { _, _ -> error("Polls are not supported in state events") }, eventSink = eventSink,
modifier = Modifier.defaultTimelineContentPadding() modifier = Modifier.defaultTimelineContentPadding()
) )
} }
@ -88,5 +91,6 @@ internal fun TimelineItemStateEventRowPreview() = ElementPreview {
isHighlighted = false, isHighlighted = false,
onClick = {}, onClick = {},
onLongClick = {}, onLongClick = {},
eventSink = {}
) )
} }

8
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt

@ -19,6 +19,7 @@ package io.element.android.features.messages.impl.timeline.components.event
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import io.element.android.features.messages.impl.timeline.TimelineEvents
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
@ -31,16 +32,16 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
import io.element.android.libraries.matrix.api.core.EventId
@Composable @Composable
fun TimelineItemEventContentView( fun TimelineItemEventContentView(
content: TimelineItemEventContent, content: TimelineItemEventContent,
isMine: Boolean,
interactionSource: MutableInteractionSource, interactionSource: MutableInteractionSource,
extraPadding: ExtraPadding, extraPadding: ExtraPadding,
onClick: () -> Unit, onClick: () -> Unit,
onLongClick: () -> Unit, onLongClick: () -> Unit,
onPollAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit, eventSink: (TimelineEvents) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
when (content) { when (content) {
@ -95,7 +96,8 @@ fun TimelineItemEventContentView(
) )
is TimelineItemPollContent -> TimelineItemPollView( is TimelineItemPollContent -> TimelineItemPollView(
content = content, content = content,
onAnswerSelected = onPollAnswerSelected, isMine = isMine,
eventSink = eventSink,
modifier = modifier, modifier = modifier,
) )
} }

33
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollView.kt

@ -19,27 +19,40 @@ package io.element.android.features.messages.impl.timeline.components.event
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.messages.impl.timeline.TimelineEvents
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContentProvider import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContentProvider
import io.element.android.features.poll.api.PollContentView import io.element.android.features.poll.api.PollContentView
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@Composable @Composable
fun TimelineItemPollView( fun TimelineItemPollView(
content: TimelineItemPollContent, content: TimelineItemPollContent,
onAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit, isMine: Boolean,
eventSink: (TimelineEvents) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
fun onAnswerSelected(pollStartId: EventId, answerId: String) {
eventSink(TimelineEvents.PollAnswerSelected(pollStartId, answerId))
}
fun onPollEnd(pollStartId: EventId) {
eventSink(TimelineEvents.PollEndClicked(pollStartId))
}
PollContentView( PollContentView(
eventId = content.eventId, eventId = content.eventId,
question = content.question, question = content.question,
answerItems = content.answerItems.toImmutableList(), answerItems = content.answerItems.toImmutableList(),
pollKind = content.pollKind, pollKind = content.pollKind,
isPollEnded = content.isEnded, isPollEnded = content.isEnded,
onAnswerSelected = onAnswerSelected, isMine = isMine,
onAnswerSelected = ::onAnswerSelected,
onPollEdit = {}, // TODO Polls: Wire up this callback once poll edit screen is done.
onPollEnd = ::onPollEnd,
modifier = modifier, modifier = modifier,
) )
} }
@ -50,6 +63,18 @@ internal fun TimelineItemPollViewPreview(@PreviewParameter(TimelineItemPollConte
ElementPreview { ElementPreview {
TimelineItemPollView( TimelineItemPollView(
content = content, content = content,
onAnswerSelected = { _, _ -> }, isMine = false,
eventSink = {},
)
}
@PreviewsDayNight
@Composable
internal fun TimelineItemPollCreatorViewPreview(@PreviewParameter(TimelineItemPollContentProvider::class) content: TimelineItemPollContent) =
ElementPreview {
TimelineItemPollView(
content = content,
isMine = true,
eventSink = {},
) )
} }

19
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt

@ -109,14 +109,17 @@ class TimelineItemContentMessageFactory @Inject constructor(
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0), formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
fileExtension = fileExtensionExtractor.extractFromName(messageType.body) fileExtension = fileExtensionExtractor.extractFromName(messageType.body)
) )
is FileMessageType -> TimelineItemFileContent( is FileMessageType -> {
body = messageType.body, val fileExtension = fileExtensionExtractor.extractFromName(messageType.body)
thumbnailSource = messageType.info?.thumbnailSource, TimelineItemFileContent(
fileSource = messageType.source, body = messageType.body,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, thumbnailSource = messageType.info?.thumbnailSource,
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0), fileSource = messageType.source,
fileExtension = fileExtensionExtractor.extractFromName(messageType.body) mimeType = messageType.info?.mimetype ?: MimeTypes.fromFileExtension(fileExtension),
) formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
fileExtension = fileExtension
)
}
is NoticeMessageType -> TimelineItemNoticeContent( is NoticeMessageType -> TimelineItemNoticeContent(
body = messageType.body, body = messageType.body,
htmlDocument = messageType.formatted?.toHtmlDocument(), htmlDocument = messageType.formatted?.toHtmlDocument(),

10
features/messages/impl/src/main/res/drawable/ic_apk_install.xml

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M160,880Q127,880 103.5,856.5Q80,833 80,800L80,160Q80,127 103.5,103.5Q127,80 160,80L480,80L720,320L720,490L640,490L640,360L440,360L440,160L160,160Q160,160 160,160Q160,160 160,160L160,800Q160,800 160,800Q160,800 160,800L600,800L600,880L160,880ZM160,800L160,490L160,490L160,360L160,160L160,160Q160,160 160,160Q160,160 160,160L160,800Q160,800 160,800Q160,800 160,800L160,800ZM200,760Q204,711 230,670Q256,629 298,605L260,537Q260,536 264,522Q269,520 273.5,520Q278,520 280,525L319,595Q339,587 359,582.5Q379,578 400,578Q421,578 441,582.5Q461,587 481,595L520,525Q520,525 535,521Q540,523 541,528Q542,533 540,537L502,605Q544,629 570,670Q596,711 600,760L200,760ZM310,700Q318,700 324,694Q330,688 330,680Q330,672 324,666Q318,660 310,660Q302,660 296,666Q290,672 290,680Q290,688 296,694Q302,700 310,700ZM490,700Q498,700 504,694Q510,688 510,680Q510,672 504,666Q498,660 490,660Q482,660 476,666Q470,672 470,680Q470,688 476,694Q482,700 490,700ZM800,880L640,720L696,663L760,726L760,560L840,560L840,726L904,663L960,720L800,880Z"/>
</vector>

1
features/messages/impl/src/main/res/values-cs/translations.xml

@ -12,6 +12,7 @@
<string name="screen_room_attachment_source_gallery">"Knihovna fotografií a videí"</string> <string name="screen_room_attachment_source_gallery">"Knihovna fotografií a videí"</string>
<string name="screen_room_attachment_source_location">"Poloha"</string> <string name="screen_room_attachment_source_location">"Poloha"</string>
<string name="screen_room_attachment_source_poll">"Hlasování"</string> <string name="screen_room_attachment_source_poll">"Hlasování"</string>
<string name="screen_room_attachment_text_formatting">"Formátování textu"</string>
<string name="screen_room_encrypted_history_banner">"Historie zpráv je momentálně v této místnosti nedostupná"</string> <string name="screen_room_encrypted_history_banner">"Historie zpráv je momentálně v této místnosti nedostupná"</string>
<string name="screen_room_error_failed_retrieving_user_details">"Nepodařilo se načíst údaje o uživateli"</string> <string name="screen_room_error_failed_retrieving_user_details">"Nepodařilo se načíst údaje o uživateli"</string>
<string name="screen_room_invite_again_alert_message">"Chtěli byste je pozvat zpět?"</string> <string name="screen_room_invite_again_alert_message">"Chtěli byste je pozvat zpět?"</string>

6
features/messages/impl/src/main/res/values-zh-rTW/translations.xml

@ -9,6 +9,7 @@
<string name="screen_room_attachment_source_files">"附件"</string> <string name="screen_room_attachment_source_files">"附件"</string>
<string name="screen_room_attachment_source_location">"位置"</string> <string name="screen_room_attachment_source_location">"位置"</string>
<string name="screen_room_attachment_source_poll">"投票"</string> <string name="screen_room_attachment_source_poll">"投票"</string>
<string name="screen_room_attachment_text_formatting">"格式化文字"</string>
<string name="screen_room_invite_again_alert_title">"此聊天室只有您一個人"</string> <string name="screen_room_invite_again_alert_title">"此聊天室只有您一個人"</string>
<string name="screen_room_message_copied">"訊息已複製"</string> <string name="screen_room_message_copied">"訊息已複製"</string>
<string name="screen_room_no_permission_to_post">"您沒有權限在此聊天室傳送訊息"</string> <string name="screen_room_no_permission_to_post">"您沒有權限在此聊天室傳送訊息"</string>
@ -17,8 +18,11 @@
<string name="screen_room_notification_settings_error_restoring_default">"無法重設為預設模式,請再試一次。"</string> <string name="screen_room_notification_settings_error_restoring_default">"無法重設為預設模式,請再試一次。"</string>
<string name="screen_room_notification_settings_error_setting_mode">"無法設定模式,請再試一次。"</string> <string name="screen_room_notification_settings_error_setting_mode">"無法設定模式,請再試一次。"</string>
<string name="screen_room_notification_settings_mode_all_messages">"所有訊息"</string> <string name="screen_room_notification_settings_mode_all_messages">"所有訊息"</string>
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"只限提及與關鍵字"</string> <string name="screen_room_notification_settings_mode_mentions_and_keywords">"僅限提及與關鍵字"</string>
<string name="screen_room_reactions_show_less">"較少"</string>
<string name="screen_room_reactions_show_more">"更多"</string>
<string name="screen_room_retry_send_menu_send_again_action">"重傳"</string> <string name="screen_room_retry_send_menu_send_again_action">"重傳"</string>
<string name="screen_room_retry_send_menu_title">"無法傳送您的訊息"</string> <string name="screen_room_retry_send_menu_title">"無法傳送您的訊息"</string>
<string name="screen_room_timeline_less_reactions">"較少"</string>
<string name="screen_room_retry_send_menu_remove_action">"移除"</string> <string name="screen_room_retry_send_menu_remove_action">"移除"</string>
</resources> </resources>

1
features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt

@ -644,7 +644,6 @@ class MessagesPresenterTest {
messageSummaryFormatter = FakeMessageSummaryFormatter(), messageSummaryFormatter = FakeMessageSummaryFormatter(),
navigator = navigator, navigator = navigator,
clipboardHelper = clipboardHelper, clipboardHelper = clipboardHelper,
analyticsService = analyticsService,
preferencesStore = preferencesStore, preferencesStore = preferencesStore,
dispatchers = coroutineDispatchers, dispatchers = coroutineDispatchers,
) )

26
features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt

@ -20,7 +20,9 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.PollEnd
import im.vector.app.features.analytics.plan.PollVote import im.vector.app.features.analytics.plan.PollVote
import io.element.android.features.messages.fixtures.aMessageEvent
import io.element.android.features.messages.fixtures.aTimelineItemsFactory import io.element.android.features.messages.fixtures.aTimelineItemsFactory
import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.TimelineEvents
import io.element.android.features.messages.impl.timeline.TimelinePresenter import io.element.android.features.messages.impl.timeline.TimelinePresenter
@ -42,6 +44,7 @@ import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.awaitWithLatch import io.element.android.tests.testutils.awaitWithLatch
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import io.element.android.tests.testutils.waitForPredicate
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -280,6 +283,29 @@ class TimelinePresenterTest {
assertThat(analyticsService.capturedEvents.last()).isEqualTo(PollVote()) assertThat(analyticsService.capturedEvents.last()).isEqualTo(PollVote())
} }
@Test
fun `present - PollEndClicked event calls into rust room api and analytics`() = runTest {
val room = FakeMatrixRoom()
val analyticsService = FakeAnalyticsService()
val presenter = createTimelinePresenter(
room = room,
analyticsService = analyticsService,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(TimelineEvents.PollEndClicked(aMessageEvent().eventId!!))
waitForPredicate { room.endPollInvocations.size == 1 }
cancelAndIgnoreRemainingEvents()
assertThat(room.endPollInvocations.size).isEqualTo(1)
assertThat(room.endPollInvocations.first().pollStartId).isEqualTo(AN_EVENT_ID)
assertThat(room.endPollInvocations.first().text).isEqualTo("The poll with event id: \$anEventId has ended.")
assertThat(analyticsService.capturedEvents.size).isEqualTo(1)
assertThat(analyticsService.capturedEvents.last()).isEqualTo(PollEnd())
}
}
private fun TestScope.createTimelinePresenter( private fun TestScope.createTimelinePresenter(
timeline: MatrixTimeline = FakeMatrixTimeline(), timeline: MatrixTimeline = FakeMatrixTimeline(),
timelineItemsFactory: TimelineItemsFactory = aTimelineItemsFactory() timelineItemsFactory: TimelineItemsFactory = aTimelineItemsFactory()

2
features/onboarding/impl/src/main/res/values-ru/translations.xml

@ -6,5 +6,5 @@
<string name="screen_onboarding_subtitle">"Безопасное общение и совместная работа"</string> <string name="screen_onboarding_subtitle">"Безопасное общение и совместная работа"</string>
<string name="screen_onboarding_welcome_message">"Добро пожаловать в самый быстрый Element. Преимущество в скорости и простоте."</string> <string name="screen_onboarding_welcome_message">"Добро пожаловать в самый быстрый Element. Преимущество в скорости и простоте."</string>
<string name="screen_onboarding_welcome_subtitle">"Добро пожаловать в %1$s. Supercharged — это скорость и простота."</string> <string name="screen_onboarding_welcome_subtitle">"Добро пожаловать в %1$s. Supercharged — это скорость и простота."</string>
<string name="screen_onboarding_welcome_title">"Будь в своей стихии"</string> <string name="screen_onboarding_welcome_title">"Будь c element"</string>
</resources> </resources>

10
features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerViewProvider.kt

@ -19,13 +19,17 @@ package io.element.android.features.poll.api
import io.element.android.libraries.matrix.api.poll.PollAnswer import io.element.android.libraries.matrix.api.poll.PollAnswer
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
fun aPollAnswerItemList(isEnded: Boolean = false, isDisclosed: Boolean = true) = persistentListOf( fun aPollAnswerItemList(
hasVotes: Boolean = true,
isEnded: Boolean = false,
isDisclosed: Boolean = true,
) = persistentListOf(
aPollAnswerItem( aPollAnswerItem(
answer = PollAnswer("option_1", "Italian \uD83C\uDDEE\uD83C\uDDF9"), answer = PollAnswer("option_1", "Italian \uD83C\uDDEE\uD83C\uDDF9"),
isDisclosed = isDisclosed, isDisclosed = isDisclosed,
isEnabled = !isEnded, isEnabled = !isEnded,
isWinner = isEnded, isWinner = isEnded,
votesCount = 5, votesCount = if (hasVotes) 5 else 0,
percentage = 0.5f percentage = 0.5f
), ),
aPollAnswerItem( aPollAnswerItem(
@ -42,7 +46,7 @@ fun aPollAnswerItemList(isEnded: Boolean = false, isDisclosed: Boolean = true) =
isEnabled = !isEnded, isEnabled = !isEnded,
isWinner = false, isWinner = false,
isSelected = true, isSelected = true,
votesCount = 1, votesCount = if (hasVotes) 1 else 0,
percentage = 0.1f percentage = 0.1f
), ),
aPollAnswerItem(isDisclosed = isDisclosed, isEnabled = !isEnded), aPollAnswerItem(isDisclosed = isDisclosed, isEnabled = !isEnded),

124
features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt

@ -26,14 +26,19 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.designsystem.utils.CommonDrawables
@ -51,13 +56,37 @@ fun PollContentView(
answerItems: ImmutableList<PollAnswerItem>, answerItems: ImmutableList<PollAnswerItem>,
pollKind: PollKind, pollKind: PollKind,
isPollEnded: Boolean, isPollEnded: Boolean,
isMine: Boolean,
onAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit, onAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit,
onPollEdit: (pollStartId: EventId) -> Unit,
onPollEnd: (pollStartId: EventId) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val votesCount = remember(answerItems) { answerItems.sumOf { it.votesCount } }
fun onAnswerSelected(pollAnswer: PollAnswer) { fun onAnswerSelected(pollAnswer: PollAnswer) {
eventId?.let { onAnswerSelected(it, pollAnswer.id) } eventId?.let { onAnswerSelected(it, pollAnswer.id) }
} }
fun onPollEdit() {
eventId?.let { onPollEdit(it) }
}
fun onPollEnd() {
eventId?.let { onPollEnd(it) }
}
var showConfirmation: Boolean by remember { mutableStateOf(false) }
if (showConfirmation) ConfirmationDialog(
content = stringResource(id = CommonStrings.common_poll_end_confirmation),
onSubmitClicked = {
onPollEnd()
showConfirmation = false
},
onDismiss = { showConfirmation = false },
)
Column( Column(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp),
@ -67,11 +96,20 @@ fun PollContentView(
PollAnswers(answerItems = answerItems, onAnswerSelected = ::onAnswerSelected) PollAnswers(answerItems = answerItems, onAnswerSelected = ::onAnswerSelected)
if (isPollEnded || pollKind == PollKind.Disclosed) { if (isPollEnded || pollKind == PollKind.Disclosed) {
val votesCount = remember(answerItems) { answerItems.sumOf { it.votesCount } }
DisclosedPollBottomNotice(votesCount = votesCount) DisclosedPollBottomNotice(votesCount = votesCount)
} else { } else {
UndisclosedPollBottomNotice() UndisclosedPollBottomNotice()
} }
if (isMine) {
CreatorView(
votesCount = 1, // TODO Polls: set to `votesCount` when edit poll screen is implemented.
isPollEnded = isPollEnded,
onPollEdit = ::onPollEdit,
onPollEnd = { showConfirmation = true },
modifier = Modifier.fillMaxWidth(),
)
}
} }
} }
@ -157,6 +195,31 @@ private fun ColumnScope.UndisclosedPollBottomNotice(
) )
} }
@Composable
private fun CreatorView(
@Suppress("SameParameterValue") votesCount: Int, // TODO Polls: remove @Suppress when edit poll screen is implemented.
isPollEnded: Boolean,
onPollEdit: () -> Unit,
onPollEnd: () -> Unit,
modifier: Modifier = Modifier
) {
if (!isPollEnded) {
if (votesCount == 0) {
Button(
text = stringResource(id = CommonStrings.action_edit_poll),
onClick = onPollEdit,
modifier = modifier,
)
} else {
Button(
text = stringResource(id = CommonStrings.action_end_poll),
onClick = onPollEnd,
modifier = modifier,
)
}
}
}
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun PollContentUndisclosedPreview() = ElementPreview { internal fun PollContentUndisclosedPreview() = ElementPreview {
@ -166,7 +229,10 @@ internal fun PollContentUndisclosedPreview() = ElementPreview {
answerItems = aPollAnswerItemList(isDisclosed = false), answerItems = aPollAnswerItemList(isDisclosed = false),
pollKind = PollKind.Undisclosed, pollKind = PollKind.Undisclosed,
isPollEnded = false, isPollEnded = false,
isMine = false,
onAnswerSelected = { _, _ -> }, onAnswerSelected = { _, _ -> },
onPollEdit = {},
onPollEnd = {},
) )
} }
@ -179,7 +245,10 @@ internal fun PollContentDisclosedPreview() = ElementPreview {
answerItems = aPollAnswerItemList(), answerItems = aPollAnswerItemList(),
pollKind = PollKind.Disclosed, pollKind = PollKind.Disclosed,
isPollEnded = false, isPollEnded = false,
isMine = false,
onAnswerSelected = { _, _ -> }, onAnswerSelected = { _, _ -> },
onPollEdit = {},
onPollEnd = {},
) )
} }
@ -192,6 +261,57 @@ internal fun PollContentEndedPreview() = ElementPreview {
answerItems = aPollAnswerItemList(isEnded = true), answerItems = aPollAnswerItemList(isEnded = true),
pollKind = PollKind.Disclosed, pollKind = PollKind.Disclosed,
isPollEnded = true, isPollEnded = true,
isMine = false,
onAnswerSelected = { _, _ -> },
onPollEdit = {},
onPollEnd = {},
)
}
@PreviewsDayNight
@Composable
internal fun PollContentCreatorNoVotesPreview() = ElementPreview {
PollContentView(
eventId = EventId("\$anEventId"),
question = "What type of food should we have at the party?",
answerItems = aPollAnswerItemList(hasVotes = false, isEnded = false),
pollKind = PollKind.Disclosed,
isPollEnded = false,
isMine = true,
onAnswerSelected = { _, _ -> },
onPollEdit = {},
onPollEnd = {},
)
}
@PreviewsDayNight
@Composable
internal fun PollContentCreatorPreview() = ElementPreview {
PollContentView(
eventId = EventId("\$anEventId"),
question = "What type of food should we have at the party?",
answerItems = aPollAnswerItemList(isEnded = false),
pollKind = PollKind.Disclosed,
isPollEnded = false,
isMine = true,
onAnswerSelected = { _, _ -> },
onPollEdit = {},
onPollEnd = {},
)
}
@PreviewsDayNight
@Composable
internal fun PollContentCreatorEndedPreview() = ElementPreview {
PollContentView(
eventId = EventId("\$anEventId"),
question = "What type of food should we have at the party?",
answerItems = aPollAnswerItemList(isEnded = true),
pollKind = PollKind.Disclosed,
isPollEnded = true,
isMine = true,
onAnswerSelected = { _, _ -> }, onAnswerSelected = { _, _ -> },
onPollEdit = {},
onPollEnd = {},
) )
} }

2
features/poll/impl/src/main/res/values-cs/translations.xml

@ -4,6 +4,8 @@
<string name="screen_create_poll_anonymous_desc">"Zobrazit výsledky až po skončení hlasování"</string> <string name="screen_create_poll_anonymous_desc">"Zobrazit výsledky až po skončení hlasování"</string>
<string name="screen_create_poll_anonymous_headline">"Anonymní hlasování"</string> <string name="screen_create_poll_anonymous_headline">"Anonymní hlasování"</string>
<string name="screen_create_poll_answer_hint">"Volba %1$d"</string> <string name="screen_create_poll_answer_hint">"Volba %1$d"</string>
<string name="screen_create_poll_discard_confirmation">"Opravdu chcete zrušit toto hlasování?"</string>
<string name="screen_create_poll_discard_confirmation_title">"Zrušit hlasování"</string>
<string name="screen_create_poll_question_desc">"Otázka nebo téma"</string> <string name="screen_create_poll_question_desc">"Otázka nebo téma"</string>
<string name="screen_create_poll_question_hint">"Čeho se hlasování týká?"</string> <string name="screen_create_poll_question_hint">"Čeho se hlasování týká?"</string>
<string name="screen_create_poll_title">"Vytvořit hlasování"</string> <string name="screen_create_poll_title">"Vytvořit hlasování"</string>

12
features/poll/impl/src/main/res/values-zh-rTW/translations.xml

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"新增選項"</string>
<string name="screen_create_poll_anonymous_desc">"只在投票結束後顯示結果"</string>
<string name="screen_create_poll_anonymous_headline">"隱藏票數"</string>
<string name="screen_create_poll_answer_hint">"選項 %1$d"</string>
<string name="screen_create_poll_discard_confirmation">"您確定要捨棄這項投票嗎?"</string>
<string name="screen_create_poll_discard_confirmation_title">"捨棄投票"</string>
<string name="screen_create_poll_question_desc">"問題或主題"</string>
<string name="screen_create_poll_question_hint">"投什麼?"</string>
<string name="screen_create_poll_title">"建立投票"</string>
</resources>

9
features/preferences/impl/src/main/res/values-cs/translations.xml

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_edit_profile_display_name">"Zobrazované jméno"</string>
<string name="screen_edit_profile_display_name_placeholder">"Vaše zobrazované jméno"</string>
<string name="screen_edit_profile_error">"Došlo k neznámé chybě a informace nelze změnit."</string>
<string name="screen_edit_profile_error_title">"Nelze aktualizovat profil"</string>
<string name="screen_edit_profile_title">"Upravit profil"</string>
<string name="screen_edit_profile_updating_details">"Aktualizace profilu…"</string>
</resources>

8
features/preferences/impl/src/main/res/values-zh-rTW/translations.xml

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_edit_profile_display_name">"顯示名稱"</string>
<string name="screen_edit_profile_display_name_placeholder">"您的顯示名稱"</string>
<string name="screen_edit_profile_error_title">"無法更新個人檔案"</string>
<string name="screen_edit_profile_title">"編輯個人檔案"</string>
<string name="screen_edit_profile_updating_details">"正在更新個人檔案…"</string>
</resources>

18
features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenter.kt

@ -17,9 +17,11 @@
package io.element.android.features.rageshake.impl.bugreport package io.element.android.features.rageshake.impl.bugreport
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableFloatState
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
@ -43,27 +45,27 @@ class BugReportPresenter @Inject constructor(
) : Presenter<BugReportState> { ) : Presenter<BugReportState> {
private class BugReporterUploadListener( private class BugReporterUploadListener(
private val sendingProgress: MutableState<Float>, private val sendingProgress: MutableFloatState,
private val sendingAction: MutableState<Async<Unit>> private val sendingAction: MutableState<Async<Unit>>
) : BugReporterListener { ) : BugReporterListener {
override fun onUploadCancelled() { override fun onUploadCancelled() {
sendingProgress.value = 0f sendingProgress.floatValue = 0f
sendingAction.value = Async.Uninitialized sendingAction.value = Async.Uninitialized
} }
override fun onUploadFailed(reason: String?) { override fun onUploadFailed(reason: String?) {
sendingProgress.value = 0f sendingProgress.floatValue = 0f
sendingAction.value = Async.Failure(Exception(reason)) sendingAction.value = Async.Failure(Exception(reason))
} }
override fun onProgress(progress: Int) { override fun onProgress(progress: Int) {
sendingProgress.value = progress.toFloat() / 100 sendingProgress.floatValue = progress.toFloat() / 100
sendingAction.value = Async.Loading() sendingAction.value = Async.Loading()
} }
override fun onUploadSucceed(reportUrl: String?) { override fun onUploadSucceed(reportUrl: String?) {
sendingProgress.value = 0f sendingProgress.floatValue = 0f
sendingAction.value = Async.Success(Unit) sendingAction.value = Async.Success(Unit)
} }
} }
@ -80,7 +82,7 @@ class BugReportPresenter @Inject constructor(
.collectAsState(initial = "") .collectAsState(initial = "")
val sendingProgress = remember { val sendingProgress = remember {
mutableStateOf(0f) mutableFloatStateOf(0f)
} }
val sendingAction: MutableState<Async<Unit>> = remember { val sendingAction: MutableState<Async<Unit>> = remember {
mutableStateOf(Async.Uninitialized) mutableStateOf(Async.Uninitialized)
@ -107,7 +109,7 @@ class BugReportPresenter @Inject constructor(
copy(sendScreenshot = event.sendScreenshot) copy(sendScreenshot = event.sendScreenshot)
} }
BugReportEvents.ClearError -> { BugReportEvents.ClearError -> {
sendingProgress.value = 0f sendingProgress.floatValue = 0f
sendingAction.value = Async.Uninitialized sendingAction.value = Async.Uninitialized
} }
} }
@ -115,7 +117,7 @@ class BugReportPresenter @Inject constructor(
return BugReportState( return BugReportState(
hasCrashLogs = crashInfo.isNotEmpty(), hasCrashLogs = crashInfo.isNotEmpty(),
sendingProgress = sendingProgress.value, sendingProgress = sendingProgress.floatValue,
sending = sendingAction.value, sending = sendingAction.value,
formState = formState.value, formState = formState.value,
screenshotUri = screenshotUri.value, screenshotUri = screenshotUri.value,

1
features/rageshake/impl/src/main/res/values-zh-rTW/translations.xml

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_bug_report_attach_screenshot">"附上螢幕截圖"</string> <string name="screen_bug_report_attach_screenshot">"附上螢幕截圖"</string>
<string name="screen_bug_report_contact_me">"如果有其他問題,你可以聯絡我。"</string>
<string name="screen_bug_report_contact_me_title">"聯絡我"</string> <string name="screen_bug_report_contact_me_title">"聯絡我"</string>
<string name="screen_bug_report_edit_screenshot">"編輯螢幕截圖"</string> <string name="screen_bug_report_edit_screenshot">"編輯螢幕截圖"</string>
<string name="screen_bug_report_include_screenshot">"傳送螢幕截圖"</string> <string name="screen_bug_report_include_screenshot">"傳送螢幕截圖"</string>

3
features/roomdetails/impl/src/main/res/values-zh-rTW/translations.xml

@ -23,12 +23,13 @@
<string name="screen_room_notification_settings_error_restoring_default">"無法重設為預設模式,請再試一次。"</string> <string name="screen_room_notification_settings_error_restoring_default">"無法重設為預設模式,請再試一次。"</string>
<string name="screen_room_notification_settings_error_setting_mode">"無法設定模式,請再試一次。"</string> <string name="screen_room_notification_settings_error_setting_mode">"無法設定模式,請再試一次。"</string>
<string name="screen_room_notification_settings_mode_all_messages">"所有訊息"</string> <string name="screen_room_notification_settings_mode_all_messages">"所有訊息"</string>
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"限提及與關鍵字"</string> <string name="screen_room_notification_settings_mode_mentions_and_keywords">"限提及與關鍵字"</string>
<string name="screen_dm_details_block_alert_action">"封鎖"</string> <string name="screen_dm_details_block_alert_action">"封鎖"</string>
<string name="screen_dm_details_block_user">"封鎖使用者"</string> <string name="screen_dm_details_block_user">"封鎖使用者"</string>
<string name="screen_dm_details_unblock_alert_action">"解除封鎖"</string> <string name="screen_dm_details_unblock_alert_action">"解除封鎖"</string>
<string name="screen_dm_details_unblock_user">"解除封鎖使用者"</string> <string name="screen_dm_details_unblock_user">"解除封鎖使用者"</string>
<string name="screen_room_details_leave_room_title">"離開聊天室"</string> <string name="screen_room_details_leave_room_title">"離開聊天室"</string>
<string name="screen_room_details_people_title">"夥伴"</string> <string name="screen_room_details_people_title">"夥伴"</string>
<string name="screen_room_details_security_title">"安全性"</string>
<string name="screen_room_details_topic_title">"主題"</string> <string name="screen_room_details_topic_title">"主題"</string>
</resources> </resources>

2
features/roomlist/impl/src/main/res/values-zh-rTW/translations.xml

@ -2,4 +2,6 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_roomlist_a11y_create_message">"建立新的對話或聊天室"</string> <string name="screen_roomlist_a11y_create_message">"建立新的對話或聊天室"</string>
<string name="screen_roomlist_main_space_title">"所有聊天室"</string> <string name="screen_roomlist_main_space_title">"所有聊天室"</string>
<string name="session_verification_banner_message">"您似乎正在使用新的裝置。請使用另一個裝置進行驗證,以存取您的加密訊息。"</string>
<string name="session_verification_banner_title">"驗證這是您本人"</string>
</resources> </resources>

14
features/verifysession/impl/src/main/res/values-zh-rTW/translations.xml

@ -1,9 +1,19 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_session_verification_cancelled_subtitle">"似乎出了一點問題。有可能是因為等候逾時,或是請求被拒絕。"</string>
<string name="screen_session_verification_compare_emojis_subtitle">"確認顯示在其他工作階段上的表情符號是否和下方的相同。"</string>
<string name="screen_session_verification_compare_emojis_title">"比對表情符號"</string>
<string name="screen_session_verification_complete_subtitle">"新的工作階段已完成驗證。它能夠存取您的加密訊息,而其他使用者會將它視為可信任的。"</string>
<string name="screen_session_verification_open_existing_session_subtitle">"為了存取被加密的歷史訊息,請證明這是您本人。"</string>
<string name="screen_session_verification_open_existing_session_title">"開啟一個現存的工作階段"</string>
<string name="screen_session_verification_positive_button_canceled">"重新嘗試驗證"</string>
<string name="screen_session_verification_positive_button_initial">"我準備好了"</string> <string name="screen_session_verification_positive_button_initial">"我準備好了"</string>
<string name="screen_session_verification_positive_button_verifying_ongoing">"等待比對"</string> <string name="screen_session_verification_positive_button_verifying_ongoing">"等待比對"</string>
<string name="screen_session_verification_they_dont_match">"不相符"</string> <string name="screen_session_verification_request_accepted_subtitle">"表情符號是唯一的,請相互比對,確認它們的排列順序是否相同。"</string>
<string name="screen_session_verification_they_match">"相符"</string> <string name="screen_session_verification_they_dont_match">"不一樣"</string>
<string name="screen_session_verification_they_match">"一樣"</string>
<string name="screen_session_verification_waiting_to_accept_subtitle">"準備開始驗證,請到您的其他工作階段接受請求。"</string>
<string name="screen_session_verification_waiting_to_accept_title">"等待接受請求"</string>
<string name="screen_session_verification_cancelled_title">"驗證已取消"</string> <string name="screen_session_verification_cancelled_title">"驗證已取消"</string>
<string name="screen_session_verification_positive_button_ready">"開始"</string> <string name="screen_session_verification_positive_button_ready">"開始"</string>
</resources> </resources>

6
gradle/libs.versions.toml

@ -45,7 +45,7 @@ dependencycheck = "8.4.0"
dependencyanalysis = "1.22.0" dependencyanalysis = "1.22.0"
stem = "2.3.0" stem = "2.3.0"
sqldelight = "1.5.5" sqldelight = "1.5.5"
telephoto = "0.6.1" telephoto = "0.6.2"
wysiwyg = "2.12.0" wysiwyg = "2.12.0"
# DI # DI
@ -128,7 +128,7 @@ test_junit = "junit:junit:4.13.2"
test_runner = "androidx.test:runner:1.5.2" test_runner = "androidx.test:runner:1.5.2"
test_uiautomator = "androidx.test.uiautomator:uiautomator:2.2.0" test_uiautomator = "androidx.test.uiautomator:uiautomator:2.2.0"
test_junitext = "androidx.test.ext:junit:1.1.5" test_junitext = "androidx.test.ext:junit:1.1.5"
test_mockk = "io.mockk:mockk:1.13.7" test_mockk = "io.mockk:mockk:1.13.8"
test_barista = "com.adevinta.android:barista:4.3.0" test_barista = "com.adevinta.android:barista:4.3.0"
test_hamcrest = "org.hamcrest:hamcrest:2.2" test_hamcrest = "org.hamcrest:hamcrest:2.2"
test_orchestrator = "androidx.test:orchestrator:1.4.2" test_orchestrator = "androidx.test:orchestrator:1.4.2"
@ -150,7 +150,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" } molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" }
timber = "com.jakewharton.timber:timber:5.0.1" timber = "com.jakewharton.timber:timber:5.0.1"
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.57" matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.58"
matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" } matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" } matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" }
sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" } sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" }

8
libraries/core/src/main/kotlin/io/element/android/libraries/core/mimetype/MimeTypes.kt

@ -51,4 +51,12 @@ object MimeTypes {
fun String?.isMimeTypeFile() = this?.startsWith("file/").orFalse() fun String?.isMimeTypeFile() = this?.startsWith("file/").orFalse()
fun String?.isMimeTypeText() = this?.startsWith("text/").orFalse() fun String?.isMimeTypeText() = this?.startsWith("text/").orFalse()
fun String?.isMimeTypeAny() = this?.startsWith("*/").orFalse() fun String?.isMimeTypeAny() = this?.startsWith("*/").orFalse()
fun fromFileExtension(fileExtension: String): String {
return when (fileExtension.lowercase()) {
"apk" -> Apk
"pdf" -> Pdf
else -> OctetStream
}
}
} }

15
libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/swipe/SwipeableActionsState.kt

@ -21,9 +21,10 @@ import androidx.compose.animation.core.tween
import androidx.compose.foundation.MutatePriority import androidx.compose.foundation.MutatePriority
import androidx.compose.foundation.gestures.DraggableState import androidx.compose.foundation.gestures.DraggableState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.FloatState
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@ -41,8 +42,8 @@ class SwipeableActionsState {
/** /**
* The current position (in pixels) of the content. * The current position (in pixels) of the content.
*/ */
val offset: State<Float> get() = offsetState val offset: FloatState get() = offsetState
private var offsetState = mutableStateOf(0f) private var offsetState = mutableFloatStateOf(0f)
/** /**
* Whether the content is currently animating to reset its offset after it was swiped. * Whether the content is currently animating to reset its offset after it was swiped.
@ -51,21 +52,21 @@ class SwipeableActionsState {
private set private set
val draggableState = DraggableState { delta -> val draggableState = DraggableState { delta ->
val targetOffset = offsetState.value + delta val targetOffset = offsetState.floatValue + delta
val isAllowed = isResettingOnRelease || targetOffset > 0f val isAllowed = isResettingOnRelease || targetOffset > 0f
offsetState.value += if (isAllowed) delta else 0f offsetState.floatValue += if (isAllowed) delta else 0f
} }
suspend fun resetOffset() { suspend fun resetOffset() {
draggableState.drag(MutatePriority.PreventUserInput) { draggableState.drag(MutatePriority.PreventUserInput) {
isResettingOnRelease = true isResettingOnRelease = true
try { try {
Animatable(offsetState.value).animateTo( Animatable(offsetState.floatValue).animateTo(
targetValue = 0f, targetValue = 0f,
animationSpec = tween(durationMillis = 300), animationSpec = tween(durationMillis = 300),
) { ) {
dragBy(value - offsetState.value) dragBy(value - offsetState.floatValue)
} }
} finally { } finally {
isResettingOnRelease = false isResettingOnRelease = false

4
libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Slider.kt

@ -22,7 +22,7 @@ import androidx.compose.material3.SliderColors
import androidx.compose.material3.SliderDefaults import androidx.compose.material3.SliderDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -62,7 +62,7 @@ internal fun SlidersPreview() = ElementThemedPreview { ContentToPreview() }
@Composable @Composable
private fun ContentToPreview() { private fun ContentToPreview() {
var value by remember { mutableStateOf(0.33f) } var value by remember { mutableFloatStateOf(0.33f) }
Column { Column {
Slider(onValueChange = { value = it }, value = value, enabled = true) Slider(onValueChange = { value = it }, value = value, enabled = true)
Slider(steps = 10, onValueChange = { value = it }, value = value, enabled = true) Slider(steps = 10, onValueChange = { value = it }, value = value, enabled = true)

1
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt

@ -61,6 +61,7 @@ sealed interface NotificationContent {
) : MessageLike ) : MessageLike
data object RoomRedaction : MessageLike data object RoomRedaction : MessageLike
data object Sticker : MessageLike data object Sticker : MessageLike
data class Poll(val question: String) : MessageLike
} }
sealed interface StateEvent : NotificationContent { sealed interface StateEvent : NotificationContent {

5
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt

@ -89,6 +89,8 @@ interface MatrixRoom : Closeable {
suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String?): Result<Unit> suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String?): Result<Unit>
suspend fun enterReplyMode(eventId: EventId): Result<Unit>
suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?): Result<Unit> suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?): Result<Unit>
suspend fun redactEvent(eventId: EventId, reason: String? = null): Result<Unit> suspend fun redactEvent(eventId: EventId, reason: String? = null): Result<Unit>
@ -184,7 +186,4 @@ interface MatrixRoom : Closeable {
suspend fun endPoll(pollStartId: EventId, text: String): Result<Unit> suspend fun endPoll(pollStartId: EventId, text: String): Result<Unit>
override fun close() = destroy() override fun close() = destroy()
} }

3
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt

@ -17,6 +17,7 @@
package io.element.android.libraries.matrix.impl.media package io.element.android.libraries.matrix.impl.media
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.media.MediaFile import io.element.android.libraries.matrix.api.media.MediaFile
import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.MediaSource
@ -77,7 +78,7 @@ class RustMediaLoader(
val mediaFile = innerClient.getMediaFile( val mediaFile = innerClient.getMediaFile(
mediaSource = mediaSource, mediaSource = mediaSource,
body = body, body = body,
mimeType = mimeType ?: "application/octet-stream", mimeType = mimeType ?: MimeTypes.OctetStream,
tempDir = cacheDirectory.path, tempDir = cacheDirectory.path,
) )
RustMediaFile(mediaFile) RustMediaFile(mediaFile)

1
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt

@ -94,6 +94,7 @@ private fun MessageLikeEventContent.toContent(senderId: UserId): NotificationCon
} }
MessageLikeEventContent.RoomRedaction -> NotificationContent.MessageLike.RoomRedaction MessageLikeEventContent.RoomRedaction -> NotificationContent.MessageLike.RoomRedaction
MessageLikeEventContent.Sticker -> NotificationContent.MessageLike.Sticker MessageLikeEventContent.Sticker -> NotificationContent.MessageLike.Sticker
is MessageLikeEventContent.Poll -> NotificationContent.MessageLike.Poll(question)
} }
} }
} }

3
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomContentForwarder.kt

@ -27,7 +27,6 @@ import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomListService import org.matrix.rustcomponents.sdk.RoomListService
import org.matrix.rustcomponents.sdk.TimelineDiff import org.matrix.rustcomponents.sdk.TimelineDiff
import org.matrix.rustcomponents.sdk.TimelineListener import org.matrix.rustcomponents.sdk.TimelineListener
import org.matrix.rustcomponents.sdk.genTransactionId
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
/** /**
@ -61,7 +60,7 @@ class RoomContentForwarder(
// Sending a message requires a registered timeline listener // Sending a message requires a registered timeline listener
targetRoom.addTimelineListener(NoOpTimelineListener) targetRoom.addTimelineListener(NoOpTimelineListener)
withTimeout(timeoutMs.milliseconds) { withTimeout(timeoutMs.milliseconds) {
targetRoom.send(content, genTransactionId()) targetRoom.send(content)
} }
} }
// After sending, we remove the timeline // After sending, we remove the timeline

33
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt

@ -60,6 +60,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.EventTimelineItem
import org.matrix.rustcomponents.sdk.RequiredState import org.matrix.rustcomponents.sdk.RequiredState
import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomListItem import org.matrix.rustcomponents.sdk.RoomListItem
@ -67,7 +68,6 @@ import org.matrix.rustcomponents.sdk.RoomMember
import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation
import org.matrix.rustcomponents.sdk.RoomSubscription import org.matrix.rustcomponents.sdk.RoomSubscription
import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle
import org.matrix.rustcomponents.sdk.genTransactionId
import org.matrix.rustcomponents.sdk.messageEventContentFromHtml import org.matrix.rustcomponents.sdk.messageEventContentFromHtml
import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown
import timber.log.Timber import timber.log.Timber
@ -139,6 +139,7 @@ class RustMatrixRoom(
roomCoroutineScope.cancel() roomCoroutineScope.cancel()
innerRoom.destroy() innerRoom.destroy()
roomListItem.destroy() roomListItem.destroy()
inReplyToEventTimelineItem?.destroy()
} }
override val name: String? override val name: String?
@ -241,10 +242,9 @@ class RustMatrixRoom(
} }
override suspend fun sendMessage(body: String, htmlBody: String?): Result<Unit> = withContext(roomDispatcher) { override suspend fun sendMessage(body: String, htmlBody: String?): Result<Unit> = withContext(roomDispatcher) {
val transactionId = genTransactionId()
messageEventContentFromParts(body, htmlBody).use { content -> messageEventContentFromParts(body, htmlBody).use { content ->
runCatching { runCatching {
innerRoom.send(content, transactionId) innerRoom.send(content)
} }
} }
} }
@ -253,26 +253,39 @@ class RustMatrixRoom(
withContext(roomDispatcher) { withContext(roomDispatcher) {
if (originalEventId != null) { if (originalEventId != null) {
runCatching { runCatching {
innerRoom.edit(messageEventContentFromParts(body, htmlBody), originalEventId.value, transactionId?.value) innerRoom.edit(messageEventContentFromParts(body, htmlBody), originalEventId.value)
} }
} else { } else {
runCatching { runCatching {
transactionId?.let { cancelSend(it) } transactionId?.let { cancelSend(it) }
innerRoom.send(messageEventContentFromParts(body, htmlBody), genTransactionId()) innerRoom.send(messageEventContentFromParts(body, htmlBody))
} }
} }
} }
private var inReplyToEventTimelineItem: EventTimelineItem? = null
override suspend fun enterReplyMode(eventId: EventId): Result<Unit> = withContext(roomDispatcher) {
runCatching {
inReplyToEventTimelineItem?.destroy()
inReplyToEventTimelineItem = null
inReplyToEventTimelineItem = innerRoom.getEventTimelineItemByEventId(eventId.value)
}
}
override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?): Result<Unit> = withContext(roomDispatcher) { override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?): Result<Unit> = withContext(roomDispatcher) {
runCatching { runCatching {
innerRoom.sendReply(messageEventContentFromParts(body, htmlBody), eventId.value, genTransactionId()) val inReplyTo = inReplyToEventTimelineItem ?: innerRoom.getEventTimelineItemByEventId(eventId.value)
inReplyTo.use { eventTimelineItem ->
innerRoom.sendReply(messageEventContentFromParts(body, htmlBody), eventTimelineItem)
}
inReplyToEventTimelineItem = null
} }
} }
override suspend fun redactEvent(eventId: EventId, reason: String?) = withContext(roomDispatcher) { override suspend fun redactEvent(eventId: EventId, reason: String?) = withContext(roomDispatcher) {
val transactionId = genTransactionId()
runCatching { runCatching {
innerRoom.redact(eventId.value, reason, transactionId) innerRoom.redact(eventId.value, reason)
} }
} }
@ -416,7 +429,6 @@ class RustMatrixRoom(
description = description, description = description,
zoomLevel = zoomLevel?.toUByte(), zoomLevel = zoomLevel?.toUByte(),
assetType = assetType?.toInner(), assetType = assetType?.toInner(),
txnId = genTransactionId(),
) )
} }
} }
@ -433,7 +445,6 @@ class RustMatrixRoom(
answers = answers, answers = answers,
maxSelections = maxSelections.toUByte(), maxSelections = maxSelections.toUByte(),
pollKind = pollKind.toInner(), pollKind = pollKind.toInner(),
txnId = genTransactionId(),
) )
} }
} }
@ -446,7 +457,6 @@ class RustMatrixRoom(
innerRoom.sendPollResponse( innerRoom.sendPollResponse(
pollStartId = pollStartId.value, pollStartId = pollStartId.value,
answers = answers, answers = answers,
txnId = genTransactionId(),
) )
} }
} }
@ -459,7 +469,6 @@ class RustMatrixRoom(
innerRoom.endPoll( innerRoom.endPoll(
pollStartId = pollStartId.value, pollStartId = pollStartId.value,
text = text, text = text,
txnId = genTransactionId(),
) )
} }
} }

6
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt

@ -31,8 +31,8 @@ import io.element.android.libraries.matrix.api.notificationsettings.Notification
import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.MatrixRoomNotificationSettingsState import io.element.android.libraries.matrix.api.room.MatrixRoomNotificationSettingsState
import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
@ -208,6 +208,10 @@ class FakeMatrixRoom(
var replyMessageParameter: Pair<String, String?>? = null var replyMessageParameter: Pair<String, String?>? = null
private set private set
override suspend fun enterReplyMode(eventId: EventId): Result<Unit> {
return Result.success(Unit)
}
override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?): Result<Unit> { override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?): Result<Unit> {
replyMessageParameter = body to htmlBody replyMessageParameter = body to htmlBody
return Result.success(Unit) return Result.success(Unit)

4
libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedUsersList.kt

@ -29,7 +29,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@ -55,7 +55,7 @@ fun SelectedUsersList(
) { ) {
val lazyListState = rememberLazyListState() val lazyListState = rememberLazyListState()
if (autoScroll) { if (autoScroll) {
var currentSize by rememberSaveable { mutableStateOf(selectedUsers.size) } var currentSize by rememberSaveable { mutableIntStateOf(selectedUsers.size) }
LaunchedEffect(selectedUsers.size) { LaunchedEffect(selectedUsers.size) {
val isItemAdded = selectedUsers.size > currentSize val isItemAdded = selectedUsers.size > currentSize
if (isItemAdded) { if (isItemAdded) {

7
libraries/permissions/api/src/main/res/values-cs/translations.xml

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"Aby mohla aplikace používat fotoaparát, udělte prosím oprávnění v nastavení systému."</string>
<string name="dialog_permission_generic">"Udělte prosím oprávnění v nastavení systému."</string>
<string name="dialog_permission_microphone">"Aby aplikace mohla používat mikrofon, udělte prosím oprávnění v nastavení systému."</string>
<string name="dialog_permission_notification">"Aby aplikace mohla zobrazovat upozornění, udělte prosím oprávnění v nastavení systému."</string>
</resources>

7
libraries/permissions/api/src/main/res/values-de/translations.xml

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"Damit die Anwendung die Kamera verwenden kann, erteilen Sie bitte die Erlaubnis in den Systemeinstellungen."</string>
<string name="dialog_permission_generic">"Bitte erteilen Sie die Erlaubnis in den Systemeinstellungen."</string>
<string name="dialog_permission_microphone">"Damit die Anwendung das Mikrofon verwenden kann, erteilen Sie bitte die Erlaubnis in den Systemeinstellungen."</string>
<string name="dialog_permission_notification">"Damit die Anwendung Benachrichtigungen anzeigen kann, erteilen Sie bitte die Erlaubnis in den Systemeinstellungen."</string>
</resources>

8
libraries/permissions/api/src/main/res/values-fr/translations.xml

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"Pour permettre à l\'application d\'utiliser l\'appareil photo, veuillez accorder l\'autorisation dans les paramètres du système."</string> <string name="dialog_permission_camera">"Pour permettre à l’application d’utiliser l’appareil photo, veuillez accorder l’autorisation dans les paramètres du système."</string>
<string name="dialog_permission_generic">"Veuillez accorder l\'autorisation dans les paramètres du système."</string> <string name="dialog_permission_generic">"Veuillez accorder lautorisation dans les paramètres du système."</string>
<string name="dialog_permission_microphone">"Pour permettre à l\'application d\'utiliser le microphone, veuillez accorder l\'autorisation dans les paramètres du système."</string> <string name="dialog_permission_microphone">"Pour permettre à l\'application d’utiliser le microphone, veuillez accorder l’autorisation dans les paramètres du système."</string>
<string name="dialog_permission_notification">"Pour permettre à l\'application d\'afficher les notifications, veuillez accorder l\'autorisation dans les paramètres du système."</string> <string name="dialog_permission_notification">"Pour permettre à l’application d’afficher les notifications, veuillez accorder l’autorisation dans les paramètres du système."</string>
</resources> </resources>

7
libraries/permissions/api/src/main/res/values-ru/translations.xml

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"Чтобы приложение могло использовать камеру, предоставьте разрешение в системных настройках."</string>
<string name="dialog_permission_generic">"Пожалуйста, предоставьте разрешение в системных настройках."</string>
<string name="dialog_permission_microphone">"Чтобы приложение могло использовать микрофон, предоставьте разрешение в системных настройках."</string>
<string name="dialog_permission_notification">"Чтобы приложение отображало уведомления, предоставьте разрешение в системных настройках."</string>
</resources>

57
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt

@ -114,10 +114,63 @@ class NotifiableEventResolver @Inject constructor(
title = null, // TODO check if title is needed anymore title = null, // TODO check if title is needed anymore
) )
} else { } else {
fallbackNotifiableEvent(userId, roomId, eventId) Timber.tag(loggerTag.value).d("Ignoring notification state event for membership ${content.membershipState}")
null
} }
} }
else -> fallbackNotifiableEvent(userId, roomId, eventId) NotificationContent.MessageLike.CallAnswer,
NotificationContent.MessageLike.CallCandidates,
NotificationContent.MessageLike.CallHangup,
NotificationContent.MessageLike.CallInvite -> null.also {
Timber.tag(loggerTag.value).d("Ignoring notification for call ${content.javaClass.simpleName}")
}
NotificationContent.MessageLike.KeyVerificationAccept,
NotificationContent.MessageLike.KeyVerificationCancel,
NotificationContent.MessageLike.KeyVerificationDone,
NotificationContent.MessageLike.KeyVerificationKey,
NotificationContent.MessageLike.KeyVerificationMac,
NotificationContent.MessageLike.KeyVerificationReady,
NotificationContent.MessageLike.KeyVerificationStart -> null.also {
Timber.tag(loggerTag.value).d("Ignoring notification for verification ${content.javaClass.simpleName}")
}
is NotificationContent.MessageLike.Poll -> null.also {
// TODO Polls: handle notification rendering
Timber.tag(loggerTag.value).d("Ignoring notification for poll")
}
is NotificationContent.MessageLike.ReactionContent -> null.also {
Timber.tag(loggerTag.value).d("Ignoring notification for reaction")
}
NotificationContent.MessageLike.RoomEncrypted -> fallbackNotifiableEvent(userId, roomId, eventId).also {
Timber.tag(loggerTag.value).w("Notification with encrypted content -> fallback")
}
NotificationContent.MessageLike.RoomRedaction -> null.also {
Timber.tag(loggerTag.value).d("Ignoring notification for redaction")
}
NotificationContent.MessageLike.Sticker -> null.also {
Timber.tag(loggerTag.value).d("Ignoring notification for sticker")
}
NotificationContent.StateEvent.PolicyRuleRoom,
NotificationContent.StateEvent.PolicyRuleServer,
NotificationContent.StateEvent.PolicyRuleUser,
NotificationContent.StateEvent.RoomAliases,
NotificationContent.StateEvent.RoomAvatar,
NotificationContent.StateEvent.RoomCanonicalAlias,
NotificationContent.StateEvent.RoomCreate,
NotificationContent.StateEvent.RoomEncryption,
NotificationContent.StateEvent.RoomGuestAccess,
NotificationContent.StateEvent.RoomHistoryVisibility,
NotificationContent.StateEvent.RoomJoinRules,
NotificationContent.StateEvent.RoomName,
NotificationContent.StateEvent.RoomPinnedEvents,
NotificationContent.StateEvent.RoomPowerLevels,
NotificationContent.StateEvent.RoomServerAcl,
NotificationContent.StateEvent.RoomThirdPartyInvite,
NotificationContent.StateEvent.RoomTombstone,
NotificationContent.StateEvent.RoomTopic,
NotificationContent.StateEvent.SpaceChild,
NotificationContent.StateEvent.SpaceParent -> null.also {
Timber.tag(loggerTag.value).d("Ignoring notification for state event ${content.javaClass.simpleName}")
}
} }
} }

6
libraries/textcomposer/impl/src/main/res/values-cs/translations.xml

@ -1,8 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="rich_text_editor_bullet_list">"Přepnout seznam s odrážkami"</string> <string name="rich_text_editor_bullet_list">"Přepnout seznam s odrážkami"</string>
<string name="rich_text_editor_close_formatting_options">"Zavřít možnosti formátování"</string>
<string name="rich_text_editor_code_block">"Přepnout blok kódu"</string> <string name="rich_text_editor_code_block">"Přepnout blok kódu"</string>
<string name="rich_text_editor_composer_placeholder">"Zpráva…"</string> <string name="rich_text_editor_composer_placeholder">"Zpráva…"</string>
<string name="rich_text_editor_create_link">"Vytvořit odkaz"</string>
<string name="rich_text_editor_edit_link">"Upravit odkaz"</string>
<string name="rich_text_editor_format_bold">"Použít tučný text"</string> <string name="rich_text_editor_format_bold">"Použít tučný text"</string>
<string name="rich_text_editor_format_italic">"Použít kurzívu"</string> <string name="rich_text_editor_format_italic">"Použít kurzívu"</string>
<string name="rich_text_editor_format_strikethrough">"Použít přeškrtnutí"</string> <string name="rich_text_editor_format_strikethrough">"Použít přeškrtnutí"</string>
@ -12,7 +15,10 @@
<string name="rich_text_editor_inline_code">"Použít formát inline kódu"</string> <string name="rich_text_editor_inline_code">"Použít formát inline kódu"</string>
<string name="rich_text_editor_link">"Nastavit odkaz"</string> <string name="rich_text_editor_link">"Nastavit odkaz"</string>
<string name="rich_text_editor_numbered_list">"Přepnout číslovaný seznam"</string> <string name="rich_text_editor_numbered_list">"Přepnout číslovaný seznam"</string>
<string name="rich_text_editor_open_compose_options">"Otevřít možnosti psaní"</string>
<string name="rich_text_editor_quote">"Přepnout citaci"</string> <string name="rich_text_editor_quote">"Přepnout citaci"</string>
<string name="rich_text_editor_remove_link">"Odstranit odkaz"</string>
<string name="rich_text_editor_unindent">"Zrušit odsazení"</string> <string name="rich_text_editor_unindent">"Zrušit odsazení"</string>
<string name="rich_text_editor_url_placeholder">"Odkaz"</string>
<string name="rich_text_editor_a11y_add_attachment">"Přidat přílohu"</string> <string name="rich_text_editor_a11y_add_attachment">"Přidat přílohu"</string>
</resources> </resources>

1
libraries/textcomposer/impl/src/main/res/values-zh-rTW/translations.xml

@ -14,6 +14,7 @@
<string name="rich_text_editor_link">"設定連結"</string> <string name="rich_text_editor_link">"設定連結"</string>
<string name="rich_text_editor_numbered_list">"切換數字編號"</string> <string name="rich_text_editor_numbered_list">"切換數字編號"</string>
<string name="rich_text_editor_quote">"切換引用"</string> <string name="rich_text_editor_quote">"切換引用"</string>
<string name="rich_text_editor_remove_link">"移除連結"</string>
<string name="rich_text_editor_unindent">"減少縮排"</string> <string name="rich_text_editor_unindent">"減少縮排"</string>
<string name="rich_text_editor_url_placeholder">"連結"</string> <string name="rich_text_editor_url_placeholder">"連結"</string>
<string name="rich_text_editor_a11y_add_attachment">"新增附件"</string> <string name="rich_text_editor_a11y_add_attachment">"新增附件"</string>

17
libraries/ui-strings/src/main/res/values-cs/translations.xml

@ -1,10 +1,15 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="a11y_hide_password">"Skrýt heslo"</string> <string name="a11y_hide_password">"Skrýt heslo"</string>
<string name="a11y_notifications_mentions_only">"Pouze zmínky"</string>
<string name="a11y_notifications_muted">"Ztišeno"</string>
<string name="a11y_poll">"Hlasování"</string>
<string name="a11y_poll_end">"Hlasování ukončeno"</string>
<string name="a11y_send_files">"Odeslat soubory"</string> <string name="a11y_send_files">"Odeslat soubory"</string>
<string name="a11y_show_password">"Zobrazit heslo"</string> <string name="a11y_show_password">"Zobrazit heslo"</string>
<string name="a11y_user_menu">"Uživatelské menu"</string> <string name="a11y_user_menu">"Uživatelské menu"</string>
<string name="action_accept">"Přijmout"</string> <string name="action_accept">"Přijmout"</string>
<string name="action_add_to_timeline">"Přidat na časovou osu"</string>
<string name="action_back">"Zpět"</string> <string name="action_back">"Zpět"</string>
<string name="action_cancel">"Zrušit"</string> <string name="action_cancel">"Zrušit"</string>
<string name="action_choose_photo">"Vybrat fotku"</string> <string name="action_choose_photo">"Vybrat fotku"</string>
@ -34,16 +39,20 @@
<string name="action_learn_more">"Zjistit více"</string> <string name="action_learn_more">"Zjistit více"</string>
<string name="action_leave">"Odejít"</string> <string name="action_leave">"Odejít"</string>
<string name="action_leave_room">"Opustit místnost"</string> <string name="action_leave_room">"Opustit místnost"</string>
<string name="action_manage_account">"Spravovat účet"</string>
<string name="action_manage_devices">"Spravovat zařízení"</string>
<string name="action_next">"Další"</string> <string name="action_next">"Další"</string>
<string name="action_no">"Ne"</string> <string name="action_no">"Ne"</string>
<string name="action_not_now">"Teď ne"</string> <string name="action_not_now">"Teď ne"</string>
<string name="action_ok">"OK"</string> <string name="action_ok">"OK"</string>
<string name="action_open_settings">"Otevřít nastavení"</string>
<string name="action_open_with">"Otevřít v aplikaci"</string> <string name="action_open_with">"Otevřít v aplikaci"</string>
<string name="action_quick_reply">"Rychlá odpověď"</string> <string name="action_quick_reply">"Rychlá odpověď"</string>
<string name="action_quote">"Citovat"</string> <string name="action_quote">"Citovat"</string>
<string name="action_react">"Reagovat"</string> <string name="action_react">"Reagovat"</string>
<string name="action_remove">"Odstranit"</string> <string name="action_remove">"Odstranit"</string>
<string name="action_reply">"Odpovědět"</string> <string name="action_reply">"Odpovědět"</string>
<string name="action_reply_in_thread">"Odpovědět ve vlákně"</string>
<string name="action_report_bug">"Nahlásit chybu"</string> <string name="action_report_bug">"Nahlásit chybu"</string>
<string name="action_report_content">"Nahlásit obsah"</string> <string name="action_report_content">"Nahlásit obsah"</string>
<string name="action_retry">"Zkusit znovu"</string> <string name="action_retry">"Zkusit znovu"</string>
@ -64,6 +73,7 @@
<string name="action_yes">"Ano"</string> <string name="action_yes">"Ano"</string>
<string name="common_about">"O aplikaci"</string> <string name="common_about">"O aplikaci"</string>
<string name="common_acceptable_use_policy">"Zásady používání"</string> <string name="common_acceptable_use_policy">"Zásady používání"</string>
<string name="common_advanced_settings">"Pokročilá nastavení"</string>
<string name="common_analytics">"Analytika"</string> <string name="common_analytics">"Analytika"</string>
<string name="common_audio">"Zvuk"</string> <string name="common_audio">"Zvuk"</string>
<string name="common_bubbles">"Bubliny"</string> <string name="common_bubbles">"Bubliny"</string>
@ -82,6 +92,7 @@
<string name="common_forward_message">"Přeposlat zprávu"</string> <string name="common_forward_message">"Přeposlat zprávu"</string>
<string name="common_gif">"GIF"</string> <string name="common_gif">"GIF"</string>
<string name="common_image">"Obrázek"</string> <string name="common_image">"Obrázek"</string>
<string name="common_in_reply_to">"V odpovědi na %1$s"</string>
<string name="common_invite_unknown_profile">"Tento Matrix identifikátor nelze najít, takže pozvánka nemusí být přijata."</string> <string name="common_invite_unknown_profile">"Tento Matrix identifikátor nelze najít, takže pozvánka nemusí být přijata."</string>
<string name="common_leaving_room">"Opuštění místnosti"</string> <string name="common_leaving_room">"Opuštění místnosti"</string>
<string name="common_link_copied_to_clipboard">"Odkaz zkopírován do schránky"</string> <string name="common_link_copied_to_clipboard">"Odkaz zkopírován do schránky"</string>
@ -96,14 +107,17 @@
<string name="common_password">"Heslo"</string> <string name="common_password">"Heslo"</string>
<string name="common_people">"Lidé"</string> <string name="common_people">"Lidé"</string>
<string name="common_permalink">"Trvalý odkaz"</string> <string name="common_permalink">"Trvalý odkaz"</string>
<string name="common_permission">"Oprávnění"</string>
<string name="common_poll_total_votes">"Celkový počet hlasů: %1$s"</string> <string name="common_poll_total_votes">"Celkový počet hlasů: %1$s"</string>
<string name="common_poll_undisclosed_text">"Výsledky se zobrazí po skončení hlasování"</string> <string name="common_poll_undisclosed_text">"Výsledky se zobrazí po skončení hlasování"</string>
<string name="common_privacy_policy">"Zásady ochrany osobních údajů"</string> <string name="common_privacy_policy">"Zásady ochrany osobních údajů"</string>
<string name="common_reaction">"Reakce"</string>
<string name="common_reactions">"Reakce"</string> <string name="common_reactions">"Reakce"</string>
<string name="common_refreshing">"Obnovování…"</string> <string name="common_refreshing">"Obnovování…"</string>
<string name="common_replying_to">"Odpověď na %1$s"</string> <string name="common_replying_to">"Odpověď na %1$s"</string>
<string name="common_report_a_bug">"Nahlásit chybu"</string> <string name="common_report_a_bug">"Nahlásit chybu"</string>
<string name="common_report_submitted">"Zpráva odeslána"</string> <string name="common_report_submitted">"Zpráva odeslána"</string>
<string name="common_rich_text_editor">"Editor formátovaného textu"</string>
<string name="common_room_name">"Název místnosti"</string> <string name="common_room_name">"Název místnosti"</string>
<string name="common_room_name_placeholder">"např. název vašeho projektu"</string> <string name="common_room_name_placeholder">"např. název vašeho projektu"</string>
<string name="common_search_for_someone">"Hledat někoho"</string> <string name="common_search_for_someone">"Hledat někoho"</string>
@ -120,7 +134,9 @@
<string name="common_success">"Úspěch"</string> <string name="common_success">"Úspěch"</string>
<string name="common_suggestions">"Návrhy"</string> <string name="common_suggestions">"Návrhy"</string>
<string name="common_syncing">"Synchronizace"</string> <string name="common_syncing">"Synchronizace"</string>
<string name="common_text">"Text"</string>
<string name="common_third_party_notices">"Oznámení třetích stran"</string> <string name="common_third_party_notices">"Oznámení třetích stran"</string>
<string name="common_thread">"Vlákno"</string>
<string name="common_topic">"Téma"</string> <string name="common_topic">"Téma"</string>
<string name="common_topic_placeholder">"O čem je tato místnost?"</string> <string name="common_topic_placeholder">"O čem je tato místnost?"</string>
<string name="common_unable_to_decrypt">"Nelze dešifrovat"</string> <string name="common_unable_to_decrypt">"Nelze dešifrovat"</string>
@ -133,6 +149,7 @@
<string name="common_verification_complete">"Ověření dokončeno"</string> <string name="common_verification_complete">"Ověření dokončeno"</string>
<string name="common_video">"Video"</string> <string name="common_video">"Video"</string>
<string name="common_waiting">"Čekání…"</string> <string name="common_waiting">"Čekání…"</string>
<string name="common_poll_summary">"Hlasování: %1$s"</string>
<string name="dialog_title_confirmation">"Potvrzení"</string> <string name="dialog_title_confirmation">"Potvrzení"</string>
<string name="dialog_title_warning">"Upozornění"</string> <string name="dialog_title_warning">"Upozornění"</string>
<string name="emoji_picker_category_activity">"Aktivity"</string> <string name="emoji_picker_category_activity">"Aktivity"</string>

3
libraries/ui-strings/src/main/res/values-de/translations.xml

@ -45,6 +45,7 @@
<string name="action_no">"Nein"</string> <string name="action_no">"Nein"</string>
<string name="action_not_now">"Nicht jetzt"</string> <string name="action_not_now">"Nicht jetzt"</string>
<string name="action_ok">"OK"</string> <string name="action_ok">"OK"</string>
<string name="action_open_settings">"Einstellungen öffnen"</string>
<string name="action_open_with">"Öffnen mit"</string> <string name="action_open_with">"Öffnen mit"</string>
<string name="action_quick_reply">"Schnelle Antwort"</string> <string name="action_quick_reply">"Schnelle Antwort"</string>
<string name="action_quote">"Zitat"</string> <string name="action_quote">"Zitat"</string>
@ -106,6 +107,7 @@
<string name="common_password">"Passwort"</string> <string name="common_password">"Passwort"</string>
<string name="common_people">"Personen"</string> <string name="common_people">"Personen"</string>
<string name="common_permalink">"Permalink"</string> <string name="common_permalink">"Permalink"</string>
<string name="common_permission">"Erlaubnis"</string>
<string name="common_poll_total_votes">"Stimmen insgesamt: %1$s"</string> <string name="common_poll_total_votes">"Stimmen insgesamt: %1$s"</string>
<string name="common_poll_undisclosed_text">"Die Ergebnisse werden nach Ende der Umfrage angezeigt"</string> <string name="common_poll_undisclosed_text">"Die Ergebnisse werden nach Ende der Umfrage angezeigt"</string>
<string name="common_privacy_policy">"Datenschutz­erklärung"</string> <string name="common_privacy_policy">"Datenschutz­erklärung"</string>
@ -147,6 +149,7 @@
<string name="common_verification_complete">"Verifizierung abgeschlossen"</string> <string name="common_verification_complete">"Verifizierung abgeschlossen"</string>
<string name="common_video">"Video"</string> <string name="common_video">"Video"</string>
<string name="common_waiting">"Warten…"</string> <string name="common_waiting">"Warten…"</string>
<string name="common_poll_summary">"Umfrage: %1$s"</string>
<string name="dialog_title_confirmation">"Bestätigung"</string> <string name="dialog_title_confirmation">"Bestätigung"</string>
<string name="dialog_title_warning">"Warnung"</string> <string name="dialog_title_warning">"Warnung"</string>
<string name="emoji_picker_category_activity">"Aktivitäten"</string> <string name="emoji_picker_category_activity">"Aktivitäten"</string>

6
libraries/ui-strings/src/main/res/values-ru/translations.xml

@ -9,6 +9,7 @@
<string name="a11y_show_password">"Показать пароль"</string> <string name="a11y_show_password">"Показать пароль"</string>
<string name="a11y_user_menu">"Меню пользователя"</string> <string name="a11y_user_menu">"Меню пользователя"</string>
<string name="action_accept">"Разрешить"</string> <string name="action_accept">"Разрешить"</string>
<string name="action_add_to_timeline">"Добавить в хронологию"</string>
<string name="action_back">"Назад"</string> <string name="action_back">"Назад"</string>
<string name="action_cancel">"Отмена"</string> <string name="action_cancel">"Отмена"</string>
<string name="action_choose_photo">"Выбрать фото"</string> <string name="action_choose_photo">"Выбрать фото"</string>
@ -44,6 +45,7 @@
<string name="action_no">"Нет"</string> <string name="action_no">"Нет"</string>
<string name="action_not_now">"Не сейчас"</string> <string name="action_not_now">"Не сейчас"</string>
<string name="action_ok">"Ок"</string> <string name="action_ok">"Ок"</string>
<string name="action_open_settings">"Открыть настройки"</string>
<string name="action_open_with">"Открыть с помощью"</string> <string name="action_open_with">"Открыть с помощью"</string>
<string name="action_quick_reply">"Быстрый ответ"</string> <string name="action_quick_reply">"Быстрый ответ"</string>
<string name="action_quote">"Цитата"</string> <string name="action_quote">"Цитата"</string>
@ -105,6 +107,7 @@
<string name="common_password">"Пароль"</string> <string name="common_password">"Пароль"</string>
<string name="common_people">"Пользователи"</string> <string name="common_people">"Пользователи"</string>
<string name="common_permalink">"Постоянная ссылка"</string> <string name="common_permalink">"Постоянная ссылка"</string>
<string name="common_permission">"Разрешение"</string>
<string name="common_poll_total_votes">"Всего голосов: %1$s"</string> <string name="common_poll_total_votes">"Всего голосов: %1$s"</string>
<string name="common_poll_undisclosed_text">"Результаты будут показаны после завершения опроса"</string> <string name="common_poll_undisclosed_text">"Результаты будут показаны после завершения опроса"</string>
<string name="common_privacy_policy">"Политика конфиденциальности"</string> <string name="common_privacy_policy">"Политика конфиденциальности"</string>
@ -146,6 +149,7 @@
<string name="common_verification_complete">"Проверка завершена"</string> <string name="common_verification_complete">"Проверка завершена"</string>
<string name="common_video">"Видео"</string> <string name="common_video">"Видео"</string>
<string name="common_waiting">"Ожидание…"</string> <string name="common_waiting">"Ожидание…"</string>
<string name="common_poll_summary">"Опрос: %1$s"</string>
<string name="dialog_title_confirmation">"Подтверждение"</string> <string name="dialog_title_confirmation">"Подтверждение"</string>
<string name="dialog_title_warning">"Предупреждение"</string> <string name="dialog_title_warning">"Предупреждение"</string>
<string name="emoji_picker_category_activity">"Деятельность"</string> <string name="emoji_picker_category_activity">"Деятельность"</string>
@ -223,7 +227,7 @@
<string name="screen_share_location_title">"Поделиться местоположением"</string> <string name="screen_share_location_title">"Поделиться местоположением"</string>
<string name="screen_share_my_location_action">"Поделиться моим местоположением"</string> <string name="screen_share_my_location_action">"Поделиться моим местоположением"</string>
<string name="screen_share_open_apple_maps">"Открыть в Apple Maps"</string> <string name="screen_share_open_apple_maps">"Открыть в Apple Maps"</string>
<string name="screen_share_open_google_maps">"Открыть в Google Картах"</string> <string name="screen_share_open_google_maps">"Открыть в Google Maps"</string>
<string name="screen_share_open_osm_maps">"Открыть в OpenStreetMap"</string> <string name="screen_share_open_osm_maps">"Открыть в OpenStreetMap"</string>
<string name="screen_share_this_location_action">"Поделиться этим местоположением"</string> <string name="screen_share_this_location_action">"Поделиться этим местоположением"</string>
<string name="screen_view_location_title">"Местоположение"</string> <string name="screen_view_location_title">"Местоположение"</string>

31
libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml

@ -1,6 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="a11y_hide_password">"隱藏密碼"</string> <string name="a11y_hide_password">"隱藏密碼"</string>
<string name="a11y_notifications_mentions_only">"僅限提及"</string>
<string name="a11y_notifications_muted">"已關閉通知"</string>
<string name="a11y_poll">"投票"</string>
<string name="a11y_poll_end">"投票已結束"</string>
<string name="a11y_send_files">"傳送檔案"</string> <string name="a11y_send_files">"傳送檔案"</string>
<string name="a11y_show_password">"顯示密碼"</string> <string name="a11y_show_password">"顯示密碼"</string>
<string name="a11y_user_menu">"使用者選單"</string> <string name="a11y_user_menu">"使用者選單"</string>
@ -21,6 +25,7 @@
<string name="action_done">"完成"</string> <string name="action_done">"完成"</string>
<string name="action_edit">"編輯"</string> <string name="action_edit">"編輯"</string>
<string name="action_enable">"啟用"</string> <string name="action_enable">"啟用"</string>
<string name="action_end_poll">"結束投票"</string>
<string name="action_forgot_password">"忘記密碼?"</string> <string name="action_forgot_password">"忘記密碼?"</string>
<string name="action_forward">"轉寄"</string> <string name="action_forward">"轉寄"</string>
<string name="action_invite">"邀請"</string> <string name="action_invite">"邀請"</string>
@ -33,16 +38,19 @@
<string name="action_leave_room">"離開聊天室"</string> <string name="action_leave_room">"離開聊天室"</string>
<string name="action_manage_account">"管理帳號"</string> <string name="action_manage_account">"管理帳號"</string>
<string name="action_manage_devices">"管理裝置"</string> <string name="action_manage_devices">"管理裝置"</string>
<string name="action_next">"下一"</string> <string name="action_next">"下一"</string>
<string name="action_no">"否"</string> <string name="action_no">"否"</string>
<string name="action_not_now">"以後再說"</string> <string name="action_not_now">"以後再說"</string>
<string name="action_ok">"OK"</string> <string name="action_ok">"OK"</string>
<string name="action_open_settings">"開啟設定"</string>
<string name="action_open_with">"用其他方式開啟"</string> <string name="action_open_with">"用其他方式開啟"</string>
<string name="action_quick_reply">"快速回覆"</string> <string name="action_quick_reply">"快速回覆"</string>
<string name="action_quote">"引用"</string> <string name="action_quote">"引用"</string>
<string name="action_react">"回應"</string> <string name="action_react">"回應"</string>
<string name="action_remove">"移除"</string> <string name="action_remove">"移除"</string>
<string name="action_reply">"回覆"</string> <string name="action_reply">"回覆"</string>
<string name="action_reply_in_thread">"在討論串中回覆"</string>
<string name="action_report_bug">"回報程式錯誤"</string>
<string name="action_report_content">"檢舉內容"</string> <string name="action_report_content">"檢舉內容"</string>
<string name="action_retry">"再試一次"</string> <string name="action_retry">"再試一次"</string>
<string name="action_retry_decryption">"再次嘗試解密"</string> <string name="action_retry_decryption">"再次嘗試解密"</string>
@ -52,7 +60,7 @@
<string name="action_send_message">"傳送訊息"</string> <string name="action_send_message">"傳送訊息"</string>
<string name="action_share">"分享"</string> <string name="action_share">"分享"</string>
<string name="action_share_link">"分享連結"</string> <string name="action_share_link">"分享連結"</string>
<string name="action_skip">"過"</string> <string name="action_skip">"過"</string>
<string name="action_start">"開始"</string> <string name="action_start">"開始"</string>
<string name="action_start_chat">"開始聊天"</string> <string name="action_start_chat">"開始聊天"</string>
<string name="action_start_verification">"開始驗證"</string> <string name="action_start_verification">"開始驗證"</string>
@ -61,6 +69,8 @@
<string name="action_view_source">"檢視原始碼"</string> <string name="action_view_source">"檢視原始碼"</string>
<string name="action_yes">"是"</string> <string name="action_yes">"是"</string>
<string name="common_about">"關於"</string> <string name="common_about">"關於"</string>
<string name="common_acceptable_use_policy">"可接受使用政策"</string>
<string name="common_advanced_settings">"進階設定"</string>
<string name="common_analytics">"分析"</string> <string name="common_analytics">"分析"</string>
<string name="common_audio">"音訊"</string> <string name="common_audio">"音訊"</string>
<string name="common_copyright">"著作權"</string> <string name="common_copyright">"著作權"</string>
@ -70,6 +80,7 @@
<string name="common_developer_options">"開發者選項"</string> <string name="common_developer_options">"開發者選項"</string>
<string name="common_edited_suffix">"(已編輯)"</string> <string name="common_edited_suffix">"(已編輯)"</string>
<string name="common_editing">"編輯中"</string> <string name="common_editing">"編輯中"</string>
<string name="common_emote">"* %1$s %2$s"</string>
<string name="common_encryption_enabled">"已啟用加密"</string> <string name="common_encryption_enabled">"已啟用加密"</string>
<string name="common_error">"錯誤"</string> <string name="common_error">"錯誤"</string>
<string name="common_file">"檔案"</string> <string name="common_file">"檔案"</string>
@ -77,6 +88,7 @@
<string name="common_forward_message">"訊息轉寄"</string> <string name="common_forward_message">"訊息轉寄"</string>
<string name="common_gif">"GIF"</string> <string name="common_gif">"GIF"</string>
<string name="common_image">"圖片"</string> <string name="common_image">"圖片"</string>
<string name="common_in_reply_to">"回覆 %1$s"</string>
<string name="common_invite_unknown_profile">"找不到此 Matrix ID,因此可能沒有人會收到邀請。"</string> <string name="common_invite_unknown_profile">"找不到此 Matrix ID,因此可能沒有人會收到邀請。"</string>
<string name="common_leaving_room">"正在離開聊天室"</string> <string name="common_leaving_room">"正在離開聊天室"</string>
<string name="common_link_copied_to_clipboard">"連結已複製到剪貼簿"</string> <string name="common_link_copied_to_clipboard">"連結已複製到剪貼簿"</string>
@ -91,14 +103,20 @@
<string name="common_password">"密碼"</string> <string name="common_password">"密碼"</string>
<string name="common_people">"夥伴"</string> <string name="common_people">"夥伴"</string>
<string name="common_permalink">"永久連結"</string> <string name="common_permalink">"永久連結"</string>
<string name="common_permission">"權限"</string>
<string name="common_poll_total_votes">"總票數:%1$s"</string>
<string name="common_poll_undisclosed_text">"結果將在投票結束後公佈"</string> <string name="common_poll_undisclosed_text">"結果將在投票結束後公佈"</string>
<string name="common_privacy_policy">"隱私權政策"</string> <string name="common_privacy_policy">"隱私權政策"</string>
<string name="common_reaction">"回應"</string>
<string name="common_reactions">"回應"</string> <string name="common_reactions">"回應"</string>
<string name="common_refreshing">"重新整理…"</string> <string name="common_refreshing">"重新整理…"</string>
<string name="common_replying_to">"正在回覆%1$s"</string> <string name="common_replying_to">"正在回覆%1$s"</string>
<string name="common_report_a_bug">"回報程式錯誤"</string>
<string name="common_rich_text_editor">"格式化文字編輯器"</string>
<string name="common_room_name">"聊天室名稱"</string> <string name="common_room_name">"聊天室名稱"</string>
<string name="common_room_name_placeholder">"範例:您的計畫名稱"</string> <string name="common_room_name_placeholder">"範例:您的計畫名稱"</string>
<string name="common_search_results">"搜尋結果"</string> <string name="common_search_results">"搜尋結果"</string>
<string name="common_security">"安全性"</string>
<string name="common_select_your_server">"選擇您的伺服器"</string> <string name="common_select_your_server">"選擇您的伺服器"</string>
<string name="common_sending">"傳送中…"</string> <string name="common_sending">"傳送中…"</string>
<string name="common_server_url">"伺服器 URL"</string> <string name="common_server_url">"伺服器 URL"</string>
@ -107,6 +125,8 @@
<string name="common_success">"成功"</string> <string name="common_success">"成功"</string>
<string name="common_suggestions">"建議"</string> <string name="common_suggestions">"建議"</string>
<string name="common_syncing">"同步中"</string> <string name="common_syncing">"同步中"</string>
<string name="common_text">"文字"</string>
<string name="common_thread">"討論串"</string>
<string name="common_topic">"主題"</string> <string name="common_topic">"主題"</string>
<string name="common_unable_to_decrypt">"無法解密"</string> <string name="common_unable_to_decrypt">"無法解密"</string>
<string name="common_unable_to_invite_message">"無法發送邀請給一或多個使用者。"</string> <string name="common_unable_to_invite_message">"無法發送邀請給一或多個使用者。"</string>
@ -117,6 +137,7 @@
<string name="common_verification_complete">"驗證完成"</string> <string name="common_verification_complete">"驗證完成"</string>
<string name="common_video">"影片"</string> <string name="common_video">"影片"</string>
<string name="common_waiting">"等待中…"</string> <string name="common_waiting">"等待中…"</string>
<string name="common_poll_summary">"投票:%1$s"</string>
<string name="dialog_title_confirmation">"確認"</string> <string name="dialog_title_confirmation">"確認"</string>
<string name="dialog_title_warning">"警告"</string> <string name="dialog_title_warning">"警告"</string>
<string name="emoji_picker_category_activity">"活動"</string> <string name="emoji_picker_category_activity">"活動"</string>
@ -148,6 +169,7 @@
<string name="screen_media_upload_preview_error_failed_sending">"無法上傳媒體檔案,請稍後再試。"</string> <string name="screen_media_upload_preview_error_failed_sending">"無法上傳媒體檔案,請稍後再試。"</string>
<string name="screen_notification_settings_additional_settings_section_title">"其他設定"</string> <string name="screen_notification_settings_additional_settings_section_title">"其他設定"</string>
<string name="screen_notification_settings_direct_chats">"私訊"</string> <string name="screen_notification_settings_direct_chats">"私訊"</string>
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"僅限提及與關鍵字"</string>
<string name="screen_notification_settings_enable_notifications">"在這個裝置上開啟通知"</string> <string name="screen_notification_settings_enable_notifications">"在這個裝置上開啟通知"</string>
<string name="screen_notification_settings_group_chats">"群組聊天"</string> <string name="screen_notification_settings_group_chats">"群組聊天"</string>
<string name="screen_notification_settings_mentions_section_title">"提及"</string> <string name="screen_notification_settings_mentions_section_title">"提及"</string>
@ -155,6 +177,7 @@
<string name="screen_notification_settings_system_notifications_action_required_content_link">"系統設定"</string> <string name="screen_notification_settings_system_notifications_action_required_content_link">"系統設定"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"已關閉系統通知"</string> <string name="screen_notification_settings_system_notifications_turned_off">"已關閉系統通知"</string>
<string name="screen_notification_settings_title">"通知"</string> <string name="screen_notification_settings_title">"通知"</string>
<string name="screen_settings_oidc_account">"帳號與裝置"</string>
<string name="screen_share_location_title">"分享位置"</string> <string name="screen_share_location_title">"分享位置"</string>
<string name="screen_share_my_location_action">"分享我的位置"</string> <string name="screen_share_my_location_action">"分享我的位置"</string>
<string name="screen_share_open_apple_maps">"在 Apple Maps 中開啟"</string> <string name="screen_share_open_apple_maps">"在 Apple Maps 中開啟"</string>
@ -168,7 +191,7 @@
<string name="dialog_title_error">"錯誤"</string> <string name="dialog_title_error">"錯誤"</string>
<string name="dialog_title_success">"成功"</string> <string name="dialog_title_success">"成功"</string>
<string name="screen_analytics_settings_help_us_improve">"分享匿名的使用數據以協助我們釐清問題"</string> <string name="screen_analytics_settings_help_us_improve">"分享匿名的使用數據以協助我們釐清問題"</string>
<string name="screen_analytics_settings_read_terms">"您可以到 %1$s 閱讀我們的條款。"</string> <string name="screen_analytics_settings_read_terms">"您可以到%1$s閱讀我們的條款。"</string>
<string name="screen_analytics_settings_read_terms_content_link">"這裡"</string> <string name="screen_analytics_settings_read_terms_content_link">"這裡"</string>
<string name="screen_report_content_block_user">"封鎖使用者"</string> <string name="screen_report_content_block_user">"封鎖使用者"</string>
</resources> </resources>

3
libraries/ui-strings/src/main/res/values/localazy.xml

@ -71,6 +71,7 @@
<string name="action_take_photo">"Take photo"</string> <string name="action_take_photo">"Take photo"</string>
<string name="action_view_source">"View Source"</string> <string name="action_view_source">"View Source"</string>
<string name="action_yes">"Yes"</string> <string name="action_yes">"Yes"</string>
<string name="action_edit_poll">"Edit poll"</string>
<string name="common_about">"About"</string> <string name="common_about">"About"</string>
<string name="common_acceptable_use_policy">"Acceptable use policy"</string> <string name="common_acceptable_use_policy">"Acceptable use policy"</string>
<string name="common_advanced_settings">"Advanced settings"</string> <string name="common_advanced_settings">"Advanced settings"</string>
@ -93,6 +94,7 @@
<string name="common_gif">"GIF"</string> <string name="common_gif">"GIF"</string>
<string name="common_image">"Image"</string> <string name="common_image">"Image"</string>
<string name="common_in_reply_to">"In reply to %1$s"</string> <string name="common_in_reply_to">"In reply to %1$s"</string>
<string name="common_install_apk_android">"Install APK"</string>
<string name="common_invite_unknown_profile">"This Matrix ID can\'t be found, so the invite might not be received."</string> <string name="common_invite_unknown_profile">"This Matrix ID can\'t be found, so the invite might not be received."</string>
<string name="common_leaving_room">"Leaving room"</string> <string name="common_leaving_room">"Leaving room"</string>
<string name="common_link_copied_to_clipboard">"Link copied to clipboard"</string> <string name="common_link_copied_to_clipboard">"Link copied to clipboard"</string>
@ -149,6 +151,7 @@
<string name="common_verification_complete">"Verification complete"</string> <string name="common_verification_complete">"Verification complete"</string>
<string name="common_video">"Video"</string> <string name="common_video">"Video"</string>
<string name="common_waiting">"Waiting…"</string> <string name="common_waiting">"Waiting…"</string>
<string name="common_poll_end_confirmation">"Are you sure you want to end this poll?"</string>
<string name="common_poll_summary">"Poll: %1$s"</string> <string name="common_poll_summary">"Poll: %1$s"</string>
<string name="dialog_title_confirmation">"Confirmation"</string> <string name="dialog_title_confirmation">"Confirmation"</string>
<string name="dialog_title_warning">"Warning"</string> <string name="dialog_title_warning">"Warning"</string>

2
plugins/src/main/kotlin/Versions.kt

@ -56,7 +56,7 @@ private const val versionMinor = 2
// Note: even values are reserved for regular release, odd values for hotfix release. // 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 // When creating a hotfix, you should decrease the value, since the current value
// is the value for the next regular release. // is the value for the next regular release.
private const val versionPatch = 2 private const val versionPatch = 3
object Versions { object Versions {
val versionCode = 4_000_000 + versionMajor * 1_00_00 + versionMinor * 1_00 + versionPatch val versionCode = 4_000_000 + versionMajor * 1_00_00 + versionMinor * 1_00 + versionPatch

45
services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/DefaultAppNavigationStateService.kt

@ -25,9 +25,9 @@ import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.SpaceId import io.element.android.libraries.matrix.api.core.SpaceId
import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.services.appnavstate.api.AppForegroundStateService import io.element.android.services.appnavstate.api.AppForegroundStateService
import io.element.android.services.appnavstate.api.NavigationState
import io.element.android.services.appnavstate.api.AppNavigationStateService
import io.element.android.services.appnavstate.api.AppNavigationState import io.element.android.services.appnavstate.api.AppNavigationState
import io.element.android.services.appnavstate.api.AppNavigationStateService
import io.element.android.services.appnavstate.api.NavigationState
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -50,16 +50,15 @@ class DefaultAppNavigationStateService @Inject constructor(
private val state = MutableStateFlow( private val state = MutableStateFlow(
AppNavigationState( AppNavigationState(
navigationState = NavigationState.Root, navigationState = NavigationState.Root,
isInForeground = true, isInForeground = true,
) )
) )
override val appNavigationState: StateFlow<AppNavigationState> = state override val appNavigationState: StateFlow<AppNavigationState> = state
init { init {
coroutineScope.launch { coroutineScope.launch {
appForegroundStateService.start() appForegroundStateService.start()
appForegroundStateService.isInForeground.collect { isInForeground -> appForegroundStateService.isInForeground.collect { isInForeground ->
state.getAndUpdate { it.copy(isInForeground = isInForeground) } state.getAndUpdate { it.copy(isInForeground = isInForeground) }
} }
@ -83,7 +82,7 @@ class DefaultAppNavigationStateService @Inject constructor(
val currentValue = state.value.navigationState val currentValue = state.value.navigationState
Timber.tag(loggerTag.value).d("Navigating to space $spaceId. Current state: $currentValue") Timber.tag(loggerTag.value).d("Navigating to space $spaceId. Current state: $currentValue")
val newValue: NavigationState.Space = when (currentValue) { val newValue: NavigationState.Space = when (currentValue) {
NavigationState.Root -> error("onNavigateToSession() must be called first") NavigationState.Root -> return logError("onNavigateToSession()")
is NavigationState.Session -> NavigationState.Space(owner, spaceId, currentValue) is NavigationState.Session -> NavigationState.Space(owner, spaceId, currentValue)
is NavigationState.Space -> NavigationState.Space(owner, spaceId, currentValue.parentSession) is NavigationState.Space -> NavigationState.Space(owner, spaceId, currentValue.parentSession)
is NavigationState.Room -> NavigationState.Space(owner, spaceId, currentValue.parentSpace.parentSession) is NavigationState.Room -> NavigationState.Space(owner, spaceId, currentValue.parentSpace.parentSession)
@ -96,8 +95,8 @@ class DefaultAppNavigationStateService @Inject constructor(
val currentValue = state.value.navigationState val currentValue = state.value.navigationState
Timber.tag(loggerTag.value).d("Navigating to room $roomId. Current state: $currentValue") Timber.tag(loggerTag.value).d("Navigating to room $roomId. Current state: $currentValue")
val newValue: NavigationState.Room = when (currentValue) { val newValue: NavigationState.Room = when (currentValue) {
NavigationState.Root -> error("onNavigateToSession() must be called first") NavigationState.Root -> return logError("onNavigateToSession()")
is NavigationState.Session -> error("onNavigateToSpace() must be called first") is NavigationState.Session -> return logError("onNavigateToSpace()")
is NavigationState.Space -> NavigationState.Room(owner, roomId, currentValue) is NavigationState.Space -> NavigationState.Room(owner, roomId, currentValue)
is NavigationState.Room -> NavigationState.Room(owner, roomId, currentValue.parentSpace) is NavigationState.Room -> NavigationState.Room(owner, roomId, currentValue.parentSpace)
is NavigationState.Thread -> NavigationState.Room(owner, roomId, currentValue.parentRoom.parentSpace) is NavigationState.Thread -> NavigationState.Room(owner, roomId, currentValue.parentRoom.parentSpace)
@ -109,9 +108,9 @@ class DefaultAppNavigationStateService @Inject constructor(
val currentValue = state.value.navigationState val currentValue = state.value.navigationState
Timber.tag(loggerTag.value).d("Navigating to thread $threadId. Current state: $currentValue") Timber.tag(loggerTag.value).d("Navigating to thread $threadId. Current state: $currentValue")
val newValue: NavigationState.Thread = when (currentValue) { val newValue: NavigationState.Thread = when (currentValue) {
NavigationState.Root -> error("onNavigateToSession() must be called first") NavigationState.Root -> return logError("onNavigateToSession()")
is NavigationState.Session -> error("onNavigateToSpace() must be called first") is NavigationState.Session -> return logError("onNavigateToSpace()")
is NavigationState.Space -> error("onNavigateToRoom() must be called first") is NavigationState.Space -> return logError("onNavigateToRoom()")
is NavigationState.Room -> NavigationState.Thread(owner, threadId, currentValue) is NavigationState.Room -> NavigationState.Thread(owner, threadId, currentValue)
is NavigationState.Thread -> NavigationState.Thread(owner, threadId, currentValue.parentRoom) is NavigationState.Thread -> NavigationState.Thread(owner, threadId, currentValue.parentRoom)
} }
@ -123,10 +122,10 @@ class DefaultAppNavigationStateService @Inject constructor(
Timber.tag(loggerTag.value).d("Leaving thread. Current state: $currentValue") Timber.tag(loggerTag.value).d("Leaving thread. Current state: $currentValue")
if (!currentValue.assertOwner(owner)) return if (!currentValue.assertOwner(owner)) return
val newValue: NavigationState.Room = when (currentValue) { val newValue: NavigationState.Room = when (currentValue) {
NavigationState.Root -> error("onNavigateToSession() must be called first") NavigationState.Root -> return logError("onNavigateToSession()")
is NavigationState.Session -> error("onNavigateToSpace() must be called first") is NavigationState.Session -> return logError("onNavigateToSpace()")
is NavigationState.Space -> error("onNavigateToRoom() must be called first") is NavigationState.Space -> return logError("onNavigateToRoom()")
is NavigationState.Room -> error("onNavigateToThread() must be called first") is NavigationState.Room -> return logError("onNavigateToThread()")
is NavigationState.Thread -> currentValue.parentRoom is NavigationState.Thread -> currentValue.parentRoom
} }
state.getAndUpdate { it.copy(navigationState = newValue) } state.getAndUpdate { it.copy(navigationState = newValue) }
@ -137,9 +136,9 @@ class DefaultAppNavigationStateService @Inject constructor(
Timber.tag(loggerTag.value).d("Leaving room. Current state: $currentValue") Timber.tag(loggerTag.value).d("Leaving room. Current state: $currentValue")
if (!currentValue.assertOwner(owner)) return if (!currentValue.assertOwner(owner)) return
val newValue: NavigationState.Space = when (currentValue) { val newValue: NavigationState.Space = when (currentValue) {
NavigationState.Root -> error("onNavigateToSession() must be called first") NavigationState.Root -> return logError("onNavigateToSession()")
is NavigationState.Session -> error("onNavigateToSpace() must be called first") is NavigationState.Session -> return logError("onNavigateToSpace()")
is NavigationState.Space -> error("onNavigateToRoom() must be called first") is NavigationState.Space -> return logError("onNavigateToRoom()")
is NavigationState.Room -> currentValue.parentSpace is NavigationState.Room -> currentValue.parentSpace
is NavigationState.Thread -> currentValue.parentRoom.parentSpace is NavigationState.Thread -> currentValue.parentRoom.parentSpace
} }
@ -151,8 +150,8 @@ class DefaultAppNavigationStateService @Inject constructor(
Timber.tag(loggerTag.value).d("Leaving space. Current state: $currentValue") Timber.tag(loggerTag.value).d("Leaving space. Current state: $currentValue")
if (!currentValue.assertOwner(owner)) return if (!currentValue.assertOwner(owner)) return
val newValue: NavigationState.Session = when (currentValue) { val newValue: NavigationState.Session = when (currentValue) {
NavigationState.Root -> error("onNavigateToSession() must be called first") NavigationState.Root -> return logError("onNavigateToSession()")
is NavigationState.Session -> error("onNavigateToSpace() must be called first") is NavigationState.Session -> return logError("onNavigateToSpace()")
is NavigationState.Space -> currentValue.parentSession is NavigationState.Space -> currentValue.parentSession
is NavigationState.Room -> currentValue.parentSpace.parentSession is NavigationState.Room -> currentValue.parentSpace.parentSession
is NavigationState.Thread -> currentValue.parentRoom.parentSpace.parentSession is NavigationState.Thread -> currentValue.parentRoom.parentSpace.parentSession
@ -167,6 +166,10 @@ class DefaultAppNavigationStateService @Inject constructor(
state.getAndUpdate { it.copy(navigationState = NavigationState.Root) } state.getAndUpdate { it.copy(navigationState = NavigationState.Root) }
} }
private fun logError(logPrefix: String) {
Timber.tag(loggerTag.value).w("$logPrefix must be call first.")
}
private fun NavigationState.assertOwner(owner: String): Boolean { private fun NavigationState.assertOwner(owner: String): Boolean {
if (this.owner != owner) { if (this.owner != owner) {
Timber.tag(loggerTag.value).d("Can't leave current state as the owner is not the same (current = ${this.owner}, new = $owner)") Timber.tag(loggerTag.value).d("Can't leave current state as the owner is not the same (current = ${this.owner}, new = $owner)")

5
services/appnavstate/impl/src/test/kotlin/io/element/android/services/appnavstate/impl/DefaultNavigationStateServiceTest.kt

@ -29,7 +29,6 @@ import io.element.android.services.appnavstate.test.A_THREAD_OWNER
import io.element.android.tests.testutils.runCancellableScopeTest import io.element.android.tests.testutils.runCancellableScopeTest
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import org.junit.Assert.assertThrows
import org.junit.Test import org.junit.Test
class DefaultNavigationStateServiceTest { class DefaultNavigationStateServiceTest {
@ -63,8 +62,8 @@ class DefaultNavigationStateServiceTest {
@Test @Test
fun testFailure() = runCancellableScopeTest { scope -> fun testFailure() = runCancellableScopeTest { scope ->
val service = createStateService(scope) val service = createStateService(scope)
service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID)
assertThrows(IllegalStateException::class.java) { service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID) } assertThat(service.appNavigationState.value.navigationState).isEqualTo(NavigationState.Root)
} }
private fun createStateService( private fun createStateService(

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.media.viewer_null_MediaViewerView_0_null_6,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.media.viewer_null_MediaViewerView_0_null_7,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-D-32_32_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-D-32_32_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-N-32_33_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-N-32_33_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemRedactedView-D-32_32_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemRedactedView-D-33_33_null,NEXUS_5,1.0,en].png

0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemRedactedView-N-32_33_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemRedactedView-N-33_34_null,NEXUS_5,1.0,en].png

0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemStateView-D-33_33_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemStateView-D-34_34_null,NEXUS_5,1.0,en].png

0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemStateView-N-33_34_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemStateView-N-34_35_null,NEXUS_5,1.0,en].png

0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-34_34_null_0,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-35_35_null_0,NEXUS_5,1.0,en].png

0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-34_34_null_1,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-35_35_null_1,NEXUS_5,1.0,en].png

0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-34_34_null_2,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-35_35_null_2,NEXUS_5,1.0,en].png

0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-34_34_null_3,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-35_35_null_3,NEXUS_5,1.0,en].png

0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-34_34_null_4,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-35_35_null_4,NEXUS_5,1.0,en].png

0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-34_34_null_5,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-35_35_null_5,NEXUS_5,1.0,en].png

0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-34_35_null_0,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-35_36_null_0,NEXUS_5,1.0,en].png

0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-34_35_null_1,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-35_36_null_1,NEXUS_5,1.0,en].png

0
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-34_35_null_2,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-35_36_null_2,NEXUS_5,1.0,en].png

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

Loading…
Cancel
Save