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

2
.github/workflows/danger.yml

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

2
.github/workflows/nightlyReports.yml

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

4
.github/workflows/quality.yml

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

2
.github/workflows/recordScreenshots.yml

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

2
.github/workflows/release.yml

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

2
.github/workflows/sonar.yml

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

2
.github/workflows/tests.yml

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

13
CHANGES.md

@ -1,3 +1,16 @@ @@ -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)
========================================

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

@ -54,6 +54,7 @@ import io.element.android.features.verifysession.api.VerifySessionEntryPoint @@ -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.animation.rememberDefaultTransitionHandler
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.designsystem.utils.SnackbarDispatcher
import io.element.android.libraries.di.SessionScope
@ -68,6 +69,7 @@ import kotlinx.coroutines.FlowPreview @@ -68,6 +69,7 @@ import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import timber.log.Timber
@ -304,6 +306,15 @@ class LoggedInFlowNode @AssistedInject constructor( @@ -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
override fun View(modifier: Modifier) {
Box(modifier = modifier) {
@ -321,13 +332,4 @@ class LoggedInFlowNode @AssistedInject constructor( @@ -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( @@ -234,7 +234,7 @@ class RootFlowNode @AssistedInject constructor(
.apply {
when (deeplinkData) {
is DeeplinkData.Root -> attachRoot()
is DeeplinkData.Room -> attachRoom(deeplinkData)
is DeeplinkData.Room -> attachRoom(deeplinkData.roomId)
is DeeplinkData.InviteList -> attachInviteList(deeplinkData)
}
}

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

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

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

@ -39,7 +39,6 @@ @@ -39,7 +39,6 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<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 @@ -21,13 +21,13 @@ import javax.inject.Inject
class CallIntentDataParser @Inject constructor() {
private val validHttpSchemes = sequenceOf("http", "https")
private val validHttpSchemes = sequenceOf("https")
fun parse(data: String?): String? {
val parsedUrl = data?.let { Uri.parse(data) } ?: return null
val scheme = parsedUrl.scheme
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" -> {
// 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
@ -40,14 +40,36 @@ class CallIntentDataParser @Inject constructor() { @@ -40,14 +40,36 @@ class CallIntentDataParser @Inject constructor() {
}
// This should never be possible, but we still need to take into account the possibility
else -> null
}
}?.withCustomParameters()
}
private fun Uri.getUrlParameter(): String? {
private fun Uri.getUrlParameter(): Uri? {
return getQueryParameter("url")
?.takeIf {
val internalUri = Uri.parse(it)
internalUri.scheme in validHttpSchemes && !internalUri.host.isNullOrBlank()
?.let { urlParameter ->
Uri.parse(urlParameter).takeIf { uri ->
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 @@ @@ -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 @@ @@ -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 { @@ -52,15 +52,19 @@ class CallIntentDataParserTests {
}
@Test
fun `Element Call urls will be returned as is`() {
fun `Element Call http urls returns null`() {
val httpBaseUrl = "http://call.element.io"
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 httpsCallUrl = "https://call.element.io/some-actual-call?with=parameters"
assertThat(callIntentDataParser.parse(httpBaseUrl)).isEqualTo(httpBaseUrl)
assertThat(callIntentDataParser.parse(httpCallUrl)).isEqualTo(httpCallUrl)
assertThat(callIntentDataParser.parse(httpsBaseUrl)).isEqualTo(httpsBaseUrl)
assertThat(callIntentDataParser.parse(httpsCallUrl)).isEqualTo(httpsCallUrl)
val httpsCallUrl = VALID_CALL_URL_WITH_PARAM
assertThat(callIntentDataParser.parse(httpsBaseUrl)).isEqualTo("$httpsBaseUrl?$EXTRA_PARAMS")
assertThat(callIntentDataParser.parse(httpsCallUrl)).isEqualTo("$httpsCallUrl&$EXTRA_PARAMS")
}
@Test
@ -76,19 +80,35 @@ class CallIntentDataParserTests { @@ -76,19 +80,35 @@ class CallIntentDataParserTests {
}
@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 encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8")
val url = "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`() {
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 encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8")
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
@ -101,7 +121,7 @@ class CallIntentDataParserTests { @@ -101,7 +121,7 @@ class CallIntentDataParserTests {
@Test
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 url = "io.element.call:/?no_url=$encodedUrl"
assertThat(callIntentDataParser.parse(url)).isNull()
@ -109,7 +129,7 @@ class CallIntentDataParserTests { @@ -109,7 +129,7 @@ class CallIntentDataParserTests {
@Test
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 url = "element://no-call?url=$encodedUrl"
assertThat(callIntentDataParser.parse(url)).isNull()
@ -129,9 +149,39 @@ class CallIntentDataParserTests { @@ -129,9 +149,39 @@ class CallIntentDataParserTests {
@Test
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 url = "bad.scheme:/?url=$encodedUrl"
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 @@ @@ -2,6 +2,8 @@
<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_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_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>

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 @@ -21,7 +21,7 @@ import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
@ -64,7 +64,7 @@ fun StaticMapView( @@ -64,7 +64,7 @@ fun StaticMapView(
contentAlignment = Alignment.Center
) {
val context = LocalContext.current
var retryHash by remember { mutableStateOf(0) }
var retryHash by remember { mutableIntStateOf(0) }
val builder = remember { StaticMapUrlBuilder(context) }
val painter = rememberAsyncImagePainter(
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 @@ -18,6 +18,7 @@ package io.element.android.features.login.impl.screens.waitlistscreen
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@ -58,14 +59,14 @@ class WaitListPresenter @AssistedInject constructor( @@ -58,14 +59,14 @@ class WaitListPresenter @AssistedInject constructor(
mutableStateOf(Async.Uninitialized)
}
val attemptNumber: MutableState<Int> = remember { mutableStateOf(0) }
val attemptNumber = remember { mutableIntStateOf(0) }
fun handleEvents(event: WaitListEvents) {
when (event) {
WaitListEvents.AttemptLogin -> {
// Do not attempt to login on first resume of the View.
attemptNumber.value++
if (attemptNumber.value > 1) {
attemptNumber.intValue++
if (attemptNumber.intValue > 1) {
coroutineScope.loginAttempt(formState, loginAction)
}
}

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

@ -14,9 +14,9 @@ @@ -14,9 +14,9 @@
<string name="screen_change_account_provider_subtitle">"Используйте другого поставщика учетных записей, например, собственный частный сервер или рабочую учетную запись."</string>
<string name="screen_change_account_provider_title">"Сменить поставщика учетной записи"</string>
<string name="screen_change_server_error_invalid_homeserver">"Нам не удалось связаться с этим домашним сервером. Убедитесь, что вы правильно ввели URL-адрес домашнего сервера. Если URL-адрес указан правильно, обратитесь к администратору домашнего сервера за дополнительной помощью."</string>
<string name="screen_change_server_error_no_sliding_sync_message">"В настоящее время этот сервер не поддерживает скользящую синхронизацию."</string>
<string name="screen_change_server_error_no_sliding_sync_message">"К сожалению данный сервер не поддерживает sliding sync."</string>
<string name="screen_change_server_form_header">"URL-адрес домашнего сервера"</string>
<string name="screen_change_server_form_notice">"Вы можете подключиться только к существующему серверу, поддерживающему скользящую синхронизацию. Администратору домашнего сервера потребуется настроить его. %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_login_error_deactivated_account">"Данная учетная запись была деактивирована."</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 @@ -30,7 +30,6 @@ import androidx.compose.runtime.setValue
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
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.ActionListPresenter
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
@ -39,6 +38,7 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer @@ -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.timeline.TimelineEvents
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.reactionsummary.ReactionSummaryPresenter
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuPresenter
@ -76,7 +76,6 @@ import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType @@ -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.canSendMessageAsState
import io.element.android.libraries.textcomposer.MessageComposerMode
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -95,7 +94,6 @@ class MessagesPresenter @AssistedInject constructor( @@ -95,7 +94,6 @@ class MessagesPresenter @AssistedInject constructor(
private val messageSummaryFormatter: MessageSummaryFormatter,
private val dispatchers: CoroutineDispatchers,
private val clipboardHelper: ClipboardHelper,
private val analyticsService: AnalyticsService,
private val preferencesStore: PreferencesStore,
@Assisted private val navigator: MessagesNavigator,
) : Presenter<MessagesState> {
@ -155,6 +153,7 @@ class MessagesPresenter @AssistedInject constructor( @@ -155,6 +153,7 @@ class MessagesPresenter @AssistedInject constructor(
targetEvent = event.event,
composerState = composerState,
enableTextFormatting = enableTextFormatting,
timelineState = timelineState,
)
}
is MessagesEvents.ToggleReaction -> {
@ -206,6 +205,7 @@ class MessagesPresenter @AssistedInject constructor( @@ -206,6 +205,7 @@ class MessagesPresenter @AssistedInject constructor(
targetEvent: TimelineItem.Event,
composerState: MessageComposerState,
enableTextFormatting: Boolean,
timelineState: TimelineState,
) = launch {
when (action) {
TimelineItemAction.Copy -> handleCopyContents(targetEvent)
@ -216,7 +216,7 @@ class MessagesPresenter @AssistedInject constructor( @@ -216,7 +216,7 @@ class MessagesPresenter @AssistedInject constructor(
TimelineItemAction.ViewSource -> handleShowDebugInfoAction(targetEvent)
TimelineItemAction.Forward -> handleForwardAction(targetEvent)
TimelineItemAction.ReportContent -> handleReportAction(targetEvent)
TimelineItemAction.EndPoll -> handleEndPollAction(targetEvent)
TimelineItemAction.EndPoll -> handleEndPollAction(targetEvent, timelineState)
}
}
@ -266,7 +266,7 @@ class MessagesPresenter @AssistedInject constructor( @@ -266,7 +266,7 @@ class MessagesPresenter @AssistedInject constructor(
targetEvent: TimelineItem.Event,
composerState: MessageComposerState,
enableTextFormatting: Boolean,
) {
) {
val composerMode = MessageComposerMode.Edit(
targetEvent.eventId,
(targetEvent.content as? TimelineItemTextBasedContent)?.let {
@ -344,11 +344,11 @@ class MessagesPresenter @AssistedInject constructor( @@ -344,11 +344,11 @@ class MessagesPresenter @AssistedInject constructor(
navigator.onReportContentClicked(event.eventId, event.senderId)
}
private suspend fun handleEndPollAction(event: TimelineItem.Event) {
event.eventId?.let {
room.endPoll(it, "The poll with event id: $it has ended.")
analyticsService.capture(PollEnd())
}
private fun handleEndPollAction(
event: TimelineItem.Event,
timelineState: TimelineState,
) {
event.eventId?.let { timelineState.eventSink(TimelineEvents.PollEndClicked(it)) }
}
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( @@ -108,7 +108,7 @@ class ActionListPresenter @Inject constructor(
buildList {
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
// if (timelineItem.isRemote) {
// // 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 @@ @@ -16,6 +16,7 @@
package io.element.android.features.messages.impl.media.local
import android.app.Activity
import android.content.ContentResolver
import android.content.ContentValues
import android.content.Context
@ -24,17 +25,25 @@ import android.net.Uri @@ -24,17 +25,25 @@ import android.net.Uri
import android.os.Build
import android.os.Environment
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.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.FileProvider
import androidx.core.net.toFile
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.meta.BuildMeta
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File
@ -50,10 +59,27 @@ class AndroidLocalMediaActions @Inject constructor( @@ -50,10 +59,27 @@ class AndroidLocalMediaActions @Inject constructor(
) : LocalMediaActions {
private var activityContext: Context? = null
private var apkInstallLauncher: ManagedActivityResultLauncher<Intent, ActivityResult>? = null
private var pendingMedia: LocalMedia? = null
@Composable
override fun Configure() {
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) {
activityContext = context
onDispose {
@ -99,11 +125,20 @@ class AndroidLocalMediaActions @Inject constructor( @@ -99,11 +125,20 @@ class AndroidLocalMediaActions @Inject constructor(
override suspend fun open(localMedia: LocalMedia): Result<Unit> = withContext(coroutineDispatchers.io) {
require(localMedia.uri.scheme == ContentResolver.SCHEME_FILE)
runCatching {
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)
when (localMedia.info.mimeType) {
MimeTypes.Apk -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (activityContext?.packageManager?.canRequestPackageInstalls() == false) {
pendingMedia = localMedia
activityContext?.startInstallFromSourceIntent(apkInstallLauncher!!).let { }
} else {
openFile(localMedia)
}
} else {
openFile(localMedia)
}
}
else -> openFile(localMedia)
}
}.onSuccess {
Timber.v("Open media succeed")
@ -112,6 +147,15 @@ class AndroidLocalMediaActions @Inject constructor( @@ -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 {
val mediaAsFile = this.toFile()
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 @@ -21,6 +21,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@ -59,7 +60,7 @@ class MediaViewerPresenter @AssistedInject constructor( @@ -59,7 +60,7 @@ class MediaViewerPresenter @AssistedInject constructor(
@Composable
override fun present(): MediaViewerState {
val coroutineScope = rememberCoroutineScope()
var loadMediaTrigger by remember { mutableStateOf(0) }
var loadMediaTrigger by remember { mutableIntStateOf(0) }
val mediaFile: MutableState<MediaFile?> = remember {
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 @@ -47,11 +47,13 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
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.LocalMediaView
import io.element.android.features.messages.impl.media.local.MediaInfo
import io.element.android.features.messages.impl.media.local.rememberLocalMediaViewState
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.dialogs.RetryDialog
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
@ -92,6 +94,7 @@ fun MediaViewerView( @@ -92,6 +94,7 @@ fun MediaViewerView(
topBar = {
MediaViewerTopBar(
actionsEnabled = state.downloadedMedia is Async.Success,
mimeType = state.mediaInfo.mimeType,
onBackPressed = onBackPressed,
eventSink = state.eventSink
)
@ -162,6 +165,7 @@ private fun rememberShowProgress(downloadedMedia: Async<LocalMedia>): Boolean { @@ -162,6 +165,7 @@ private fun rememberShowProgress(downloadedMedia: Async<LocalMedia>): Boolean {
@Composable
private fun MediaViewerTopBar(
actionsEnabled: Boolean,
mimeType: String,
onBackPressed: () -> Unit,
eventSink: (MediaViewerEvents) -> Unit,
) {
@ -175,10 +179,16 @@ private fun MediaViewerTopBar( @@ -175,10 +179,16 @@ private fun MediaViewerTopBar(
eventSink(MediaViewerEvents.OpenWith)
},
) {
Icon(
imageVector = Icons.Default.OpenInNew,
contentDescription = stringResource(id = CommonStrings.action_open_with)
)
when (mimeType) {
MimeTypes.Apk -> Icon(
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(
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( @@ -154,12 +154,10 @@ class MessageComposerPresenter @Inject constructor(
fun handleEvents(event: MessageComposerEvents) {
when (event) {
MessageComposerEvents.ToggleFullScreenState -> isFullScreen.value = !isFullScreen.value
MessageComposerEvents.CloseSpecialMode -> {
richTextEditorState.setHtml("")
messageComposerContext.composerMode = MessageComposerMode.Normal("")
}
is MessageComposerEvents.SendMessage -> appCoroutineScope.sendMessage(
message = event.message,
updateComposerMode = { messageComposerContext.composerMode = it },
@ -167,6 +165,11 @@ class MessageComposerPresenter @Inject constructor( @@ -167,6 +165,11 @@ class MessageComposerPresenter @Inject constructor(
)
is MessageComposerEvents.SetMode -> {
messageComposerContext.composerMode = event.composerMode
if (event.composerMode is MessageComposerMode.Reply) {
appCoroutineScope.launch {
room.enterReplyMode(event.composerMode.eventId)
}
}
}
MessageComposerEvents.AddAttachment -> localCoroutineScope.launch {
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 { @@ -26,4 +26,8 @@ sealed interface TimelineEvents {
val pollStartId: EventId,
val answerId: String
) : 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 @@ -26,6 +26,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import im.vector.app.features.analytics.plan.PollEnd
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.model.TimelineItem
@ -98,11 +99,18 @@ class TimelinePresenter @Inject constructor( @@ -98,11 +99,18 @@ class TimelinePresenter @Inject constructor(
)
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) {
computeHasNewItems(timelineItems, prevMostRecentItemId, hasNewItems)
computeHasNewItems(timelineItems, prevMostRecentItemId, hasNewItems)
}
LaunchedEffect(Unit) {
@ -123,7 +131,7 @@ class TimelinePresenter @Inject constructor( @@ -123,7 +131,7 @@ class TimelinePresenter @Inject constructor(
paginationState = paginationState,
timelineItems = timelineItems,
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( @@ -103,10 +103,6 @@ fun TimelineView(
// 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
val alpha by alphaAnimation(label = "alpha for timeline")
@ -134,7 +130,7 @@ fun TimelineView( @@ -134,7 +130,7 @@ fun TimelineView(
onReactionLongClick = onReactionLongClicked,
onMoreReactionsClick = onMoreReactionsClicked,
onTimestampClicked = onTimestampClicked,
onPollAnswerSelected = ::onPollAnswerSelected,
eventSink = state.eventSink,
onSwipeToReply = onSwipeToReply,
)
}
@ -172,7 +168,7 @@ fun TimelineItemRow( @@ -172,7 +168,7 @@ fun TimelineItemRow(
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
onTimestampClicked: (TimelineItem.Event) -> Unit,
onSwipeToReply: (TimelineItem.Event) -> Unit,
onPollAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit,
eventSink: (TimelineEvents) -> Unit,
modifier: Modifier = Modifier
) {
when (timelineItem) {
@ -189,6 +185,7 @@ fun TimelineItemRow( @@ -189,6 +185,7 @@ fun TimelineItemRow(
isHighlighted = highlightedItem == timelineItem.identifier(),
onClick = { onClick(timelineItem) },
onLongClick = { onLongClick(timelineItem) },
eventSink = eventSink,
modifier = modifier,
)
} else {
@ -205,7 +202,7 @@ fun TimelineItemRow( @@ -205,7 +202,7 @@ fun TimelineItemRow(
onMoreReactionsClick = onMoreReactionsClick,
onTimestampClicked = onTimestampClicked,
onSwipeToReply = { onSwipeToReply(timelineItem) },
onPollAnswerSelected = onPollAnswerSelected,
eventSink = eventSink,
modifier = modifier,
)
}
@ -243,7 +240,7 @@ fun TimelineItemRow( @@ -243,7 +240,7 @@ fun TimelineItemRow(
onReactionClick = onReactionClick,
onReactionLongClick = onReactionLongClick,
onMoreReactionsClick = onMoreReactionsClick,
onPollAnswerSelected = onPollAnswerSelected,
eventSink = eventSink,
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 @@ -61,6 +61,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.constraintlayout.compose.ConstrainScope
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.aTimelineItemReactions
import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView
@ -80,9 +81,9 @@ import io.element.android.libraries.designsystem.colors.AvatarColorsProvider @@ -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.avatar.Avatar
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.ElementPreviewLight
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.swipe.SwipeableActionsState
import io.element.android.libraries.designsystem.swipe.rememberSwipeableActionsState
import io.element.android.libraries.designsystem.text.toPx
@ -123,7 +124,7 @@ fun TimelineItemEventRow( @@ -123,7 +124,7 @@ fun TimelineItemEventRow(
onReactionLongClick: (emoji: String, eventId: TimelineItem.Event) -> Unit,
onMoreReactionsClick: (eventId: TimelineItem.Event) -> Unit,
onSwipeToReply: () -> Unit,
onPollAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit,
eventSink: (TimelineEvents) -> Unit,
modifier: Modifier = Modifier
) {
val coroutineScope = rememberCoroutineScope()
@ -146,7 +147,7 @@ fun TimelineItemEventRow( @@ -146,7 +147,7 @@ fun TimelineItemEventRow(
}
if (canReply) {
val state: SwipeableActionsState = rememberSwipeableActionsState()
val offset = state.offset.value
val offset = state.offset.floatValue
val swipeThresholdPx = 40.dp.toPx()
val thresholdCrossed = abs(offset) > swipeThresholdPx
SwipeSensitivity(3f) {
@ -181,7 +182,7 @@ fun TimelineItemEventRow( @@ -181,7 +182,7 @@ fun TimelineItemEventRow(
onReactionClicked = { emoji -> onReactionClick(emoji, event) },
onReactionLongClicked = { emoji -> onReactionLongClick(emoji, event) },
onMoreReactionsClicked = { onMoreReactionsClick(event) },
onPollAnswerSelected = onPollAnswerSelected,
eventSink = eventSink,
)
}
}
@ -198,7 +199,7 @@ fun TimelineItemEventRow( @@ -198,7 +199,7 @@ fun TimelineItemEventRow(
onReactionClicked = { emoji -> onReactionClick(emoji, event) },
onReactionLongClicked = { emoji -> onReactionLongClick(emoji, event) },
onMoreReactionsClicked = { onMoreReactionsClick(event) },
onPollAnswerSelected = onPollAnswerSelected,
eventSink = eventSink,
)
}
}
@ -240,7 +241,7 @@ private fun TimelineItemEventRowContent( @@ -240,7 +241,7 @@ private fun TimelineItemEventRowContent(
onReactionClicked: (emoji: String) -> Unit,
onReactionLongClicked: (emoji: String) -> Unit,
onMoreReactionsClicked: (event: TimelineItem.Event) -> Unit,
onPollAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit,
eventSink: (TimelineEvents) -> Unit,
modifier: Modifier = Modifier,
) {
fun ConstrainScope.linkStartOrEnd(event: TimelineItem.Event) = if (event.isMine) {
@ -299,7 +300,7 @@ private fun TimelineItemEventRowContent( @@ -299,7 +300,7 @@ private fun TimelineItemEventRowContent(
onTimestampClicked = {
onTimestampClicked(event)
},
onPollAnswerSelected = onPollAnswerSelected,
eventSink = eventSink,
)
}
@ -371,7 +372,7 @@ private fun MessageEventBubbleContent( @@ -371,7 +372,7 @@ private fun MessageEventBubbleContent(
onMessageLongClick: () -> Unit,
inReplyToClick: () -> 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
) {
@ -385,11 +386,12 @@ private fun MessageEventBubbleContent( @@ -385,11 +386,12 @@ private fun MessageEventBubbleContent(
) {
TimelineItemEventContentView(
content = event.content,
isMine = event.isMine,
interactionSource = interactionSource,
onClick = onMessageClick,
onLongClick = onMessageLongClick,
extraPadding = event.toExtraPadding(),
onPollAnswerSelected = onPollAnswerSelected,
eventSink = eventSink,
modifier = modifier,
)
}
@ -652,7 +654,7 @@ internal fun TimelineItemEventRowPreview() = ElementPreview { @@ -652,7 +654,7 @@ internal fun TimelineItemEventRowPreview() = ElementPreview {
onMoreReactionsClick = {},
onTimestampClicked = {},
onSwipeToReply = {},
onPollAnswerSelected = { _, _ -> },
eventSink = {},
)
TimelineItemEventRow(
event = aTimelineItemEvent(
@ -673,7 +675,7 @@ internal fun TimelineItemEventRowPreview() = ElementPreview { @@ -673,7 +675,7 @@ internal fun TimelineItemEventRowPreview() = ElementPreview {
onMoreReactionsClick = {},
onTimestampClicked = {},
onSwipeToReply = {},
onPollAnswerSelected = { _, _ -> },
eventSink = {},
)
}
}
@ -712,7 +714,7 @@ internal fun TimelineItemEventRowWithReplyPreview() = ElementPreview { @@ -712,7 +714,7 @@ internal fun TimelineItemEventRowWithReplyPreview() = ElementPreview {
onMoreReactionsClick = {},
onTimestampClicked = {},
onSwipeToReply = {},
onPollAnswerSelected = { _, _ -> },
eventSink = {},
)
TimelineItemEventRow(
event = aTimelineItemEvent(
@ -735,7 +737,7 @@ internal fun TimelineItemEventRowWithReplyPreview() = ElementPreview { @@ -735,7 +737,7 @@ internal fun TimelineItemEventRowWithReplyPreview() = ElementPreview {
onMoreReactionsClick = {},
onTimestampClicked = {},
onSwipeToReply = {},
onPollAnswerSelected = { _, _ -> },
eventSink = {},
)
}
}
@ -786,7 +788,7 @@ internal fun TimelineItemEventRowTimestampPreview( @@ -786,7 +788,7 @@ internal fun TimelineItemEventRowTimestampPreview(
onMoreReactionsClick = {},
onTimestampClicked = {},
onSwipeToReply = {},
onPollAnswerSelected = { _, _ -> },
eventSink = {},
)
}
}
@ -818,7 +820,7 @@ internal fun TimelineItemEventRowWithManyReactionsPreview() = ElementPreview { @@ -818,7 +820,7 @@ internal fun TimelineItemEventRowWithManyReactionsPreview() = ElementPreview {
onMoreReactionsClick = {},
onSwipeToReply = {},
onTimestampClicked = {},
onPollAnswerSelected = { _, _ -> },
eventSink = {},
)
}
}
@ -843,7 +845,7 @@ internal fun TimelineItemEventRowLongSenderNamePreview() = ElementPreviewLight { @@ -843,7 +845,7 @@ internal fun TimelineItemEventRowLongSenderNamePreview() = ElementPreviewLight {
onMoreReactionsClick = {},
onSwipeToReply = {},
onTimestampClicked = {},
onPollAnswerSelected = { _, _ -> },
eventSink = {},
)
}
@ -864,6 +866,6 @@ internal fun TimelineItemEventTimestampBelowPreview() = ElementPreviewLight { @@ -864,6 +866,6 @@ internal fun TimelineItemEventTimestampBelowPreview() = ElementPreviewLight {
onMoreReactionsClick = {},
onSwipeToReply = {},
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 @@ -28,6 +28,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
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.components.event.TimelineItemEventContentView
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 @@ -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.event.aTimelineItemStateEventContent
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.PreviewsDayNight
@Composable
fun TimelineItemStateEventRow(
@ -44,6 +45,7 @@ fun TimelineItemStateEventRow( @@ -44,6 +45,7 @@ fun TimelineItemStateEventRow(
isHighlighted: Boolean,
onClick: () -> Unit,
onLongClick: () -> Unit,
eventSink: (TimelineEvents) -> Unit,
modifier: Modifier = Modifier
) {
val interactionSource = remember { MutableInteractionSource() }
@ -65,11 +67,12 @@ fun TimelineItemStateEventRow( @@ -65,11 +67,12 @@ fun TimelineItemStateEventRow(
) {
TimelineItemEventContentView(
content = event.content,
isMine = event.isMine,
interactionSource = interactionSource,
onClick = onClick,
onLongClick = onLongClick,
extraPadding = noExtraPadding,
onPollAnswerSelected = { _, _ -> error("Polls are not supported in state events") },
eventSink = eventSink,
modifier = Modifier.defaultTimelineContentPadding()
)
}
@ -88,5 +91,6 @@ internal fun TimelineItemStateEventRowPreview() = ElementPreview { @@ -88,5 +91,6 @@ internal fun TimelineItemStateEventRowPreview() = ElementPreview {
isHighlighted = false,
onClick = {},
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 @@ -19,6 +19,7 @@ package io.element.android.features.messages.impl.timeline.components.event
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.runtime.Composable
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.TimelineItemEncryptedContent
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 @@ -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.TimelineItemUnknownContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
import io.element.android.libraries.matrix.api.core.EventId
@Composable
fun TimelineItemEventContentView(
content: TimelineItemEventContent,
isMine: Boolean,
interactionSource: MutableInteractionSource,
extraPadding: ExtraPadding,
onClick: () -> Unit,
onLongClick: () -> Unit,
onPollAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit,
eventSink: (TimelineEvents) -> Unit,
modifier: Modifier = Modifier
) {
when (content) {
@ -95,7 +96,8 @@ fun TimelineItemEventContentView( @@ -95,7 +96,8 @@ fun TimelineItemEventContentView(
)
is TimelineItemPollContent -> TimelineItemPollView(
content = content,
onAnswerSelected = onPollAnswerSelected,
isMine = isMine,
eventSink = eventSink,
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 @@ -19,27 +19,40 @@ package io.element.android.features.messages.impl.timeline.components.event
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
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.TimelineItemPollContentProvider
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.PreviewsDayNight
import io.element.android.libraries.matrix.api.core.EventId
import kotlinx.collections.immutable.toImmutableList
@Composable
fun TimelineItemPollView(
content: TimelineItemPollContent,
onAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit,
isMine: Boolean,
eventSink: (TimelineEvents) -> Unit,
modifier: Modifier = Modifier,
) {
fun onAnswerSelected(pollStartId: EventId, answerId: String) {
eventSink(TimelineEvents.PollAnswerSelected(pollStartId, answerId))
}
fun onPollEnd(pollStartId: EventId) {
eventSink(TimelineEvents.PollEndClicked(pollStartId))
}
PollContentView(
eventId = content.eventId,
question = content.question,
answerItems = content.answerItems.toImmutableList(),
pollKind = content.pollKind,
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,
)
}
@ -50,6 +63,18 @@ internal fun TimelineItemPollViewPreview(@PreviewParameter(TimelineItemPollConte @@ -50,6 +63,18 @@ internal fun TimelineItemPollViewPreview(@PreviewParameter(TimelineItemPollConte
ElementPreview {
TimelineItemPollView(
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( @@ -109,14 +109,17 @@ class TimelineItemContentMessageFactory @Inject constructor(
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
fileExtension = fileExtensionExtractor.extractFromName(messageType.body)
)
is FileMessageType -> TimelineItemFileContent(
body = messageType.body,
thumbnailSource = messageType.info?.thumbnailSource,
fileSource = messageType.source,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
fileExtension = fileExtensionExtractor.extractFromName(messageType.body)
)
is FileMessageType -> {
val fileExtension = fileExtensionExtractor.extractFromName(messageType.body)
TimelineItemFileContent(
body = messageType.body,
thumbnailSource = messageType.info?.thumbnailSource,
fileSource = messageType.source,
mimeType = messageType.info?.mimetype ?: MimeTypes.fromFileExtension(fileExtension),
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
fileExtension = fileExtension
)
}
is NoticeMessageType -> TimelineItemNoticeContent(
body = messageType.body,
htmlDocument = messageType.formatted?.toHtmlDocument(),

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

@ -0,0 +1,10 @@ @@ -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 @@ @@ -12,6 +12,7 @@
<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_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_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>

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

@ -9,6 +9,7 @@ @@ -9,6 +9,7 @@
<string name="screen_room_attachment_source_files">"附件"</string>
<string name="screen_room_attachment_source_location">"位置"</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_message_copied">"訊息已複製"</string>
<string name="screen_room_no_permission_to_post">"您沒有權限在此聊天室傳送訊息"</string>
@ -17,8 +18,11 @@ @@ -17,8 +18,11 @@
<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_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_title">"無法傳送您的訊息"</string>
<string name="screen_room_timeline_less_reactions">"較少"</string>
<string name="screen_room_retry_send_menu_remove_action">"移除"</string>
</resources>

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

@ -644,7 +644,6 @@ class MessagesPresenterTest { @@ -644,7 +644,6 @@ class MessagesPresenterTest {
messageSummaryFormatter = FakeMessageSummaryFormatter(),
navigator = navigator,
clipboardHelper = clipboardHelper,
analyticsService = analyticsService,
preferencesStore = preferencesStore,
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 @@ -20,7 +20,9 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.PollEnd
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.impl.timeline.TimelineEvents
import io.element.android.features.messages.impl.timeline.TimelinePresenter
@ -42,6 +44,7 @@ import io.element.android.services.analytics.test.FakeAnalyticsService @@ -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.awaitWithLatch
import io.element.android.tests.testutils.testCoroutineDispatchers
import io.element.android.tests.testutils.waitForPredicate
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
@ -280,6 +283,29 @@ class TimelinePresenterTest { @@ -280,6 +283,29 @@ class TimelinePresenterTest {
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(
timeline: MatrixTimeline = FakeMatrixTimeline(),
timelineItemsFactory: TimelineItemsFactory = aTimelineItemsFactory()

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

@ -6,5 +6,5 @@ @@ -6,5 +6,5 @@
<string name="screen_onboarding_subtitle">"Безопасное общение и совместная работа"</string>
<string name="screen_onboarding_welcome_message">"Добро пожаловать в самый быстрый Element. Преимущество в скорости и простоте."</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>

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 @@ -19,13 +19,17 @@ package io.element.android.features.poll.api
import io.element.android.libraries.matrix.api.poll.PollAnswer
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(
answer = PollAnswer("option_1", "Italian \uD83C\uDDEE\uD83C\uDDF9"),
isDisclosed = isDisclosed,
isEnabled = !isEnded,
isWinner = isEnded,
votesCount = 5,
votesCount = if (hasVotes) 5 else 0,
percentage = 0.5f
),
aPollAnswerItem(
@ -42,7 +46,7 @@ fun aPollAnswerItemList(isEnded: Boolean = false, isDisclosed: Boolean = true) = @@ -42,7 +46,7 @@ fun aPollAnswerItemList(isEnded: Boolean = false, isDisclosed: Boolean = true) =
isEnabled = !isEnded,
isWinner = false,
isSelected = true,
votesCount = 1,
votesCount = if (hasVotes) 1 else 0,
percentage = 0.1f
),
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 @@ -26,14 +26,19 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
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.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.Text
import io.element.android.libraries.designsystem.utils.CommonDrawables
@ -51,13 +56,37 @@ fun PollContentView( @@ -51,13 +56,37 @@ fun PollContentView(
answerItems: ImmutableList<PollAnswerItem>,
pollKind: PollKind,
isPollEnded: Boolean,
isMine: Boolean,
onAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit,
onPollEdit: (pollStartId: EventId) -> Unit,
onPollEnd: (pollStartId: EventId) -> Unit,
modifier: Modifier = Modifier,
) {
val votesCount = remember(answerItems) { answerItems.sumOf { it.votesCount } }
fun onAnswerSelected(pollAnswer: PollAnswer) {
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(
modifier = modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(16.dp),
@ -67,11 +96,20 @@ fun PollContentView( @@ -67,11 +96,20 @@ fun PollContentView(
PollAnswers(answerItems = answerItems, onAnswerSelected = ::onAnswerSelected)
if (isPollEnded || pollKind == PollKind.Disclosed) {
val votesCount = remember(answerItems) { answerItems.sumOf { it.votesCount } }
DisclosedPollBottomNotice(votesCount = votesCount)
} else {
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( @@ -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
@Composable
internal fun PollContentUndisclosedPreview() = ElementPreview {
@ -166,7 +229,10 @@ internal fun PollContentUndisclosedPreview() = ElementPreview { @@ -166,7 +229,10 @@ internal fun PollContentUndisclosedPreview() = ElementPreview {
answerItems = aPollAnswerItemList(isDisclosed = false),
pollKind = PollKind.Undisclosed,
isPollEnded = false,
isMine = false,
onAnswerSelected = { _, _ -> },
onPollEdit = {},
onPollEnd = {},
)
}
@ -179,7 +245,10 @@ internal fun PollContentDisclosedPreview() = ElementPreview { @@ -179,7 +245,10 @@ internal fun PollContentDisclosedPreview() = ElementPreview {
answerItems = aPollAnswerItemList(),
pollKind = PollKind.Disclosed,
isPollEnded = false,
isMine = false,
onAnswerSelected = { _, _ -> },
onPollEdit = {},
onPollEnd = {},
)
}
@ -192,6 +261,57 @@ internal fun PollContentEndedPreview() = ElementPreview { @@ -192,6 +261,57 @@ internal fun PollContentEndedPreview() = ElementPreview {
answerItems = aPollAnswerItemList(isEnded = true),
pollKind = PollKind.Disclosed,
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 = { _, _ -> },
onPollEdit = {},
onPollEnd = {},
)
}

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

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

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

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<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_edit_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 @@ @@ -23,12 +23,13 @@
<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_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_user">"封鎖使用者"</string>
<string name="screen_dm_details_unblock_alert_action">"解除封鎖"</string>
<string name="screen_dm_details_unblock_user">"解除封鎖使用者"</string>
<string name="screen_room_details_leave_room_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>
</resources>

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

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

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

@ -1,9 +1,19 @@ @@ -1,9 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<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_verifying_ongoing">"等待比對"</string>
<string name="screen_session_verification_they_dont_match">"不相符"</string>
<string name="screen_session_verification_they_match">"相符"</string>
<string name="screen_session_verification_request_accepted_subtitle">"表情符號是唯一的,請相互比對,確認它們的排列順序是否相同。"</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_positive_button_ready">"開始"</string>
</resources>

6
gradle/libs.versions.toml

@ -45,7 +45,7 @@ dependencycheck = "8.4.0" @@ -45,7 +45,7 @@ dependencycheck = "8.4.0"
dependencyanalysis = "1.22.0"
stem = "2.3.0"
sqldelight = "1.5.5"
telephoto = "0.6.1"
telephoto = "0.6.2"
wysiwyg = "2.12.0"
# DI
@ -128,7 +128,7 @@ test_junit = "junit:junit:4.13.2" @@ -128,7 +128,7 @@ test_junit = "junit:junit:4.13.2"
test_runner = "androidx.test:runner:1.5.2"
test_uiautomator = "androidx.test.uiautomator:uiautomator:2.2.0"
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_hamcrest = "org.hamcrest:hamcrest:2.2"
test_orchestrator = "androidx.test:orchestrator:1.4.2"
@ -150,7 +150,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } @@ -150,7 +150,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" }
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_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" }
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 { @@ -51,4 +51,12 @@ object MimeTypes {
fun String?.isMimeTypeFile() = this?.startsWith("file/").orFalse()
fun String?.isMimeTypeText() = this?.startsWith("text/").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 @@ -21,9 +21,10 @@ import androidx.compose.animation.core.tween
import androidx.compose.foundation.MutatePriority
import androidx.compose.foundation.gestures.DraggableState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.FloatState
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@ -41,8 +42,8 @@ class SwipeableActionsState { @@ -41,8 +42,8 @@ class SwipeableActionsState {
/**
* The current position (in pixels) of the content.
*/
val offset: State<Float> get() = offsetState
private var offsetState = mutableStateOf(0f)
val offset: FloatState get() = offsetState
private var offsetState = mutableFloatStateOf(0f)
/**
* Whether the content is currently animating to reset its offset after it was swiped.
@ -51,21 +52,21 @@ class SwipeableActionsState { @@ -51,21 +52,21 @@ class SwipeableActionsState {
private set
val draggableState = DraggableState { delta ->
val targetOffset = offsetState.value + delta
val targetOffset = offsetState.floatValue + delta
val isAllowed = isResettingOnRelease || targetOffset > 0f
offsetState.value += if (isAllowed) delta else 0f
offsetState.floatValue += if (isAllowed) delta else 0f
}
suspend fun resetOffset() {
draggableState.drag(MutatePriority.PreventUserInput) {
isResettingOnRelease = true
try {
Animatable(offsetState.value).animateTo(
Animatable(offsetState.floatValue).animateTo(
targetValue = 0f,
animationSpec = tween(durationMillis = 300),
) {
dragBy(value - offsetState.value)
dragBy(value - offsetState.floatValue)
}
} finally {
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 @@ -22,7 +22,7 @@ import androidx.compose.material3.SliderColors
import androidx.compose.material3.SliderDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
@ -62,7 +62,7 @@ internal fun SlidersPreview() = ElementThemedPreview { ContentToPreview() } @@ -62,7 +62,7 @@ internal fun SlidersPreview() = ElementThemedPreview { ContentToPreview() }
@Composable
private fun ContentToPreview() {
var value by remember { mutableStateOf(0.33f) }
var value by remember { mutableFloatStateOf(0.33f) }
Column {
Slider(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 { @@ -61,6 +61,7 @@ sealed interface NotificationContent {
) : MessageLike
data object RoomRedaction : MessageLike
data object Sticker : MessageLike
data class Poll(val question: String) : MessageLike
}
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 { @@ -89,6 +89,8 @@ interface MatrixRoom : Closeable {
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 redactEvent(eventId: EventId, reason: String? = null): Result<Unit>
@ -184,7 +186,4 @@ interface MatrixRoom : Closeable { @@ -184,7 +186,4 @@ interface MatrixRoom : Closeable {
suspend fun endPoll(pollStartId: EventId, text: String): Result<Unit>
override fun close() = destroy()
}

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

@ -17,6 +17,7 @@ @@ -17,6 +17,7 @@
package io.element.android.libraries.matrix.impl.media
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.MediaFile
import io.element.android.libraries.matrix.api.media.MediaSource
@ -77,7 +78,7 @@ class RustMediaLoader( @@ -77,7 +78,7 @@ class RustMediaLoader(
val mediaFile = innerClient.getMediaFile(
mediaSource = mediaSource,
body = body,
mimeType = mimeType ?: "application/octet-stream",
mimeType = mimeType ?: MimeTypes.OctetStream,
tempDir = cacheDirectory.path,
)
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 @@ -94,6 +94,7 @@ private fun MessageLikeEventContent.toContent(senderId: UserId): NotificationCon
}
MessageLikeEventContent.RoomRedaction -> NotificationContent.MessageLike.RoomRedaction
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 @@ -27,7 +27,6 @@ import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomListService
import org.matrix.rustcomponents.sdk.TimelineDiff
import org.matrix.rustcomponents.sdk.TimelineListener
import org.matrix.rustcomponents.sdk.genTransactionId
import kotlin.time.Duration.Companion.milliseconds
/**
@ -61,7 +60,7 @@ class RoomContentForwarder( @@ -61,7 +60,7 @@ class RoomContentForwarder(
// Sending a message requires a registered timeline listener
targetRoom.addTimelineListener(NoOpTimelineListener)
withTimeout(timeoutMs.milliseconds) {
targetRoom.send(content, genTransactionId())
targetRoom.send(content)
}
}
// 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 @@ -60,6 +60,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.EventTimelineItem
import org.matrix.rustcomponents.sdk.RequiredState
import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomListItem
@ -67,7 +68,6 @@ import org.matrix.rustcomponents.sdk.RoomMember @@ -67,7 +68,6 @@ import org.matrix.rustcomponents.sdk.RoomMember
import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation
import org.matrix.rustcomponents.sdk.RoomSubscription
import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle
import org.matrix.rustcomponents.sdk.genTransactionId
import org.matrix.rustcomponents.sdk.messageEventContentFromHtml
import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown
import timber.log.Timber
@ -139,6 +139,7 @@ class RustMatrixRoom( @@ -139,6 +139,7 @@ class RustMatrixRoom(
roomCoroutineScope.cancel()
innerRoom.destroy()
roomListItem.destroy()
inReplyToEventTimelineItem?.destroy()
}
override val name: String?
@ -241,10 +242,9 @@ class RustMatrixRoom( @@ -241,10 +242,9 @@ class RustMatrixRoom(
}
override suspend fun sendMessage(body: String, htmlBody: String?): Result<Unit> = withContext(roomDispatcher) {
val transactionId = genTransactionId()
messageEventContentFromParts(body, htmlBody).use { content ->
runCatching {
innerRoom.send(content, transactionId)
innerRoom.send(content)
}
}
}
@ -253,26 +253,39 @@ class RustMatrixRoom( @@ -253,26 +253,39 @@ class RustMatrixRoom(
withContext(roomDispatcher) {
if (originalEventId != null) {
runCatching {
innerRoom.edit(messageEventContentFromParts(body, htmlBody), originalEventId.value, transactionId?.value)
innerRoom.edit(messageEventContentFromParts(body, htmlBody), originalEventId.value)
}
} else {
runCatching {
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) {
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) {
val transactionId = genTransactionId()
runCatching {
innerRoom.redact(eventId.value, reason, transactionId)
innerRoom.redact(eventId.value, reason)
}
}
@ -416,7 +429,6 @@ class RustMatrixRoom( @@ -416,7 +429,6 @@ class RustMatrixRoom(
description = description,
zoomLevel = zoomLevel?.toUByte(),
assetType = assetType?.toInner(),
txnId = genTransactionId(),
)
}
}
@ -433,7 +445,6 @@ class RustMatrixRoom( @@ -433,7 +445,6 @@ class RustMatrixRoom(
answers = answers,
maxSelections = maxSelections.toUByte(),
pollKind = pollKind.toInner(),
txnId = genTransactionId(),
)
}
}
@ -446,7 +457,6 @@ class RustMatrixRoom( @@ -446,7 +457,6 @@ class RustMatrixRoom(
innerRoom.sendPollResponse(
pollStartId = pollStartId.value,
answers = answers,
txnId = genTransactionId(),
)
}
}
@ -459,7 +469,6 @@ class RustMatrixRoom( @@ -459,7 +469,6 @@ class RustMatrixRoom(
innerRoom.endPoll(
pollStartId = pollStartId.value,
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 @@ -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.room.MatrixRoom
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.MessageEventType
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.timeline.MatrixTimeline
@ -208,6 +208,10 @@ class FakeMatrixRoom( @@ -208,6 +208,10 @@ class FakeMatrixRoom(
var replyMessageParameter: Pair<String, String?>? = null
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> {
replyMessageParameter = body to htmlBody
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 @@ -29,7 +29,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
@ -55,7 +55,7 @@ fun SelectedUsersList( @@ -55,7 +55,7 @@ fun SelectedUsersList(
) {
val lazyListState = rememberLazyListState()
if (autoScroll) {
var currentSize by rememberSaveable { mutableStateOf(selectedUsers.size) }
var currentSize by rememberSaveable { mutableIntStateOf(selectedUsers.size) }
LaunchedEffect(selectedUsers.size) {
val isItemAdded = selectedUsers.size > currentSize
if (isItemAdded) {

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

@ -0,0 +1,7 @@ @@ -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 @@ @@ -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 @@ @@ -1,7 +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">"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_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_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 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_notification">"Pour permettre à l’application d’afficher les notifications, veuillez accorder l’autorisation dans les paramètres du système."</string>
</resources>

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

@ -0,0 +1,7 @@ @@ -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( @@ -114,10 +114,63 @@ class NotifiableEventResolver @Inject constructor(
title = null, // TODO check if title is needed anymore
)
} 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 @@ @@ -1,8 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<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_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_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_italic">"Použít kurzívu"</string>
<string name="rich_text_editor_format_strikethrough">"Použít přeškrtnutí"</string>
@ -12,7 +15,10 @@ @@ -12,7 +15,10 @@
<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_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_remove_link">"Odstranit odkaz"</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>
</resources>

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

@ -14,6 +14,7 @@ @@ -14,6 +14,7 @@
<string name="rich_text_editor_link">"設定連結"</string>
<string name="rich_text_editor_numbered_list">"切換數字編號"</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_url_placeholder">"連結"</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 @@ @@ -1,10 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<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_show_password">"Zobrazit heslo"</string>
<string name="a11y_user_menu">"Uživatelské menu"</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_cancel">"Zrušit"</string>
<string name="action_choose_photo">"Vybrat fotku"</string>
@ -34,16 +39,20 @@ @@ -34,16 +39,20 @@
<string name="action_learn_more">"Zjistit více"</string>
<string name="action_leave">"Odejít"</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_no">"Ne"</string>
<string name="action_not_now">"Teď ne"</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_quick_reply">"Rychlá odpověď"</string>
<string name="action_quote">"Citovat"</string>
<string name="action_react">"Reagovat"</string>
<string name="action_remove">"Odstranit"</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_content">"Nahlásit obsah"</string>
<string name="action_retry">"Zkusit znovu"</string>
@ -64,6 +73,7 @@ @@ -64,6 +73,7 @@
<string name="action_yes">"Ano"</string>
<string name="common_about">"O aplikaci"</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_audio">"Zvuk"</string>
<string name="common_bubbles">"Bubliny"</string>
@ -82,6 +92,7 @@ @@ -82,6 +92,7 @@
<string name="common_forward_message">"Přeposlat zprávu"</string>
<string name="common_gif">"GIF"</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_leaving_room">"Opuštění místnosti"</string>
<string name="common_link_copied_to_clipboard">"Odkaz zkopírován do schránky"</string>
@ -96,14 +107,17 @@ @@ -96,14 +107,17 @@
<string name="common_password">"Heslo"</string>
<string name="common_people">"Lidé"</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_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_reaction">"Reakce"</string>
<string name="common_reactions">"Reakce"</string>
<string name="common_refreshing">"Obnovování…"</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_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_placeholder">"např. název vašeho projektu"</string>
<string name="common_search_for_someone">"Hledat někoho"</string>
@ -120,7 +134,9 @@ @@ -120,7 +134,9 @@
<string name="common_success">"Úspěch"</string>
<string name="common_suggestions">"Návrhy"</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_thread">"Vlákno"</string>
<string name="common_topic">"Téma"</string>
<string name="common_topic_placeholder">"O čem je tato místnost?"</string>
<string name="common_unable_to_decrypt">"Nelze dešifrovat"</string>
@ -133,6 +149,7 @@ @@ -133,6 +149,7 @@
<string name="common_verification_complete">"Ověření dokončeno"</string>
<string name="common_video">"Video"</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_warning">"Upozornění"</string>
<string name="emoji_picker_category_activity">"Aktivity"</string>

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

@ -45,6 +45,7 @@ @@ -45,6 +45,7 @@
<string name="action_no">"Nein"</string>
<string name="action_not_now">"Nicht jetzt"</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_quick_reply">"Schnelle Antwort"</string>
<string name="action_quote">"Zitat"</string>
@ -106,6 +107,7 @@ @@ -106,6 +107,7 @@
<string name="common_password">"Passwort"</string>
<string name="common_people">"Personen"</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_undisclosed_text">"Die Ergebnisse werden nach Ende der Umfrage angezeigt"</string>
<string name="common_privacy_policy">"Datenschutz­erklärung"</string>
@ -147,6 +149,7 @@ @@ -147,6 +149,7 @@
<string name="common_verification_complete">"Verifizierung abgeschlossen"</string>
<string name="common_video">"Video"</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_warning">"Warnung"</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 @@ @@ -9,6 +9,7 @@
<string name="a11y_show_password">"Показать пароль"</string>
<string name="a11y_user_menu">"Меню пользователя"</string>
<string name="action_accept">"Разрешить"</string>
<string name="action_add_to_timeline">"Добавить в хронологию"</string>
<string name="action_back">"Назад"</string>
<string name="action_cancel">"Отмена"</string>
<string name="action_choose_photo">"Выбрать фото"</string>
@ -44,6 +45,7 @@ @@ -44,6 +45,7 @@
<string name="action_no">"Нет"</string>
<string name="action_not_now">"Не сейчас"</string>
<string name="action_ok">"Ок"</string>
<string name="action_open_settings">"Открыть настройки"</string>
<string name="action_open_with">"Открыть с помощью"</string>
<string name="action_quick_reply">"Быстрый ответ"</string>
<string name="action_quote">"Цитата"</string>
@ -105,6 +107,7 @@ @@ -105,6 +107,7 @@
<string name="common_password">"Пароль"</string>
<string name="common_people">"Пользователи"</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_privacy_policy">"Политика конфиденциальности"</string>
@ -146,6 +149,7 @@ @@ -146,6 +149,7 @@
<string name="common_verification_complete">"Проверка завершена"</string>
<string name="common_video">"Видео"</string>
<string name="common_waiting">"Ожидание…"</string>
<string name="common_poll_summary">"Опрос: %1$s"</string>
<string name="dialog_title_confirmation">"Подтверждение"</string>
<string name="dialog_title_warning">"Предупреждение"</string>
<string name="emoji_picker_category_activity">"Деятельность"</string>
@ -223,7 +227,7 @@ @@ -223,7 +227,7 @@
<string name="screen_share_location_title">"Поделиться местоположением"</string>
<string name="screen_share_my_location_action">"Поделиться моим местоположением"</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_this_location_action">"Поделиться этим местоположением"</string>
<string name="screen_view_location_title">"Местоположение"</string>

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

@ -1,6 +1,10 @@ @@ -1,6 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<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_show_password">"顯示密碼"</string>
<string name="a11y_user_menu">"使用者選單"</string>
@ -21,6 +25,7 @@ @@ -21,6 +25,7 @@
<string name="action_done">"完成"</string>
<string name="action_edit">"編輯"</string>
<string name="action_enable">"啟用"</string>
<string name="action_end_poll">"結束投票"</string>
<string name="action_forgot_password">"忘記密碼?"</string>
<string name="action_forward">"轉寄"</string>
<string name="action_invite">"邀請"</string>
@ -33,16 +38,19 @@ @@ -33,16 +38,19 @@
<string name="action_leave_room">"離開聊天室"</string>
<string name="action_manage_account">"管理帳號"</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_not_now">"以後再說"</string>
<string name="action_ok">"OK"</string>
<string name="action_open_settings">"開啟設定"</string>
<string name="action_open_with">"用其他方式開啟"</string>
<string name="action_quick_reply">"快速回覆"</string>
<string name="action_quote">"引用"</string>
<string name="action_react">"回應"</string>
<string name="action_remove">"移除"</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_retry">"再試一次"</string>
<string name="action_retry_decryption">"再次嘗試解密"</string>
@ -52,7 +60,7 @@ @@ -52,7 +60,7 @@
<string name="action_send_message">"傳送訊息"</string>
<string name="action_share">"分享"</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_chat">"開始聊天"</string>
<string name="action_start_verification">"開始驗證"</string>
@ -61,6 +69,8 @@ @@ -61,6 +69,8 @@
<string name="action_view_source">"檢視原始碼"</string>
<string name="action_yes">"是"</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_audio">"音訊"</string>
<string name="common_copyright">"著作權"</string>
@ -70,6 +80,7 @@ @@ -70,6 +80,7 @@
<string name="common_developer_options">"開發者選項"</string>
<string name="common_edited_suffix">"(已編輯)"</string>
<string name="common_editing">"編輯中"</string>
<string name="common_emote">"* %1$s %2$s"</string>
<string name="common_encryption_enabled">"已啟用加密"</string>
<string name="common_error">"錯誤"</string>
<string name="common_file">"檔案"</string>
@ -77,6 +88,7 @@ @@ -77,6 +88,7 @@
<string name="common_forward_message">"訊息轉寄"</string>
<string name="common_gif">"GIF"</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_leaving_room">"正在離開聊天室"</string>
<string name="common_link_copied_to_clipboard">"連結已複製到剪貼簿"</string>
@ -91,14 +103,20 @@ @@ -91,14 +103,20 @@
<string name="common_password">"密碼"</string>
<string name="common_people">"夥伴"</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_privacy_policy">"隱私權政策"</string>
<string name="common_reaction">"回應"</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_report_a_bug">"回報程式錯誤"</string>
<string name="common_rich_text_editor">"格式化文字編輯器"</string>
<string name="common_room_name">"聊天室名稱"</string>
<string name="common_room_name_placeholder">"範例:您的計畫名稱"</string>
<string name="common_search_results">"搜尋結果"</string>
<string name="common_security">"安全性"</string>
<string name="common_select_your_server">"選擇您的伺服器"</string>
<string name="common_sending">"傳送中…"</string>
<string name="common_server_url">"伺服器 URL"</string>
@ -107,6 +125,8 @@ @@ -107,6 +125,8 @@
<string name="common_success">"成功"</string>
<string name="common_suggestions">"建議"</string>
<string name="common_syncing">"同步中"</string>
<string name="common_text">"文字"</string>
<string name="common_thread">"討論串"</string>
<string name="common_topic">"主題"</string>
<string name="common_unable_to_decrypt">"無法解密"</string>
<string name="common_unable_to_invite_message">"無法發送邀請給一或多個使用者。"</string>
@ -117,6 +137,7 @@ @@ -117,6 +137,7 @@
<string name="common_verification_complete">"驗證完成"</string>
<string name="common_video">"影片"</string>
<string name="common_waiting">"等待中…"</string>
<string name="common_poll_summary">"投票:%1$s"</string>
<string name="dialog_title_confirmation">"確認"</string>
<string name="dialog_title_warning">"警告"</string>
<string name="emoji_picker_category_activity">"活動"</string>
@ -148,6 +169,7 @@ @@ -148,6 +169,7 @@
<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_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_group_chats">"群組聊天"</string>
<string name="screen_notification_settings_mentions_section_title">"提及"</string>
@ -155,6 +177,7 @@ @@ -155,6 +177,7 @@
<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_title">"通知"</string>
<string name="screen_settings_oidc_account">"帳號與裝置"</string>
<string name="screen_share_location_title">"分享位置"</string>
<string name="screen_share_my_location_action">"分享我的位置"</string>
<string name="screen_share_open_apple_maps">"在 Apple Maps 中開啟"</string>
@ -168,7 +191,7 @@ @@ -168,7 +191,7 @@
<string name="dialog_title_error">"錯誤"</string>
<string name="dialog_title_success">"成功"</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_report_content_block_user">"封鎖使用者"</string>
</resources>

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

@ -71,6 +71,7 @@ @@ -71,6 +71,7 @@
<string name="action_take_photo">"Take photo"</string>
<string name="action_view_source">"View Source"</string>
<string name="action_yes">"Yes"</string>
<string name="action_edit_poll">"Edit poll"</string>
<string name="common_about">"About"</string>
<string name="common_acceptable_use_policy">"Acceptable use policy"</string>
<string name="common_advanced_settings">"Advanced settings"</string>
@ -93,6 +94,7 @@ @@ -93,6 +94,7 @@
<string name="common_gif">"GIF"</string>
<string name="common_image">"Image"</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_leaving_room">"Leaving room"</string>
<string name="common_link_copied_to_clipboard">"Link copied to clipboard"</string>
@ -149,6 +151,7 @@ @@ -149,6 +151,7 @@
<string name="common_verification_complete">"Verification complete"</string>
<string name="common_video">"Video"</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="dialog_title_confirmation">"Confirmation"</string>
<string name="dialog_title_warning">"Warning"</string>

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

@ -56,7 +56,7 @@ private const val versionMinor = 2 @@ -56,7 +56,7 @@ private const val versionMinor = 2
// Note: even values are reserved for regular release, odd values for hotfix release.
// When creating a hotfix, you should decrease the value, since the current value
// is the value for the next regular release.
private const val versionPatch = 2
private const val versionPatch = 3
object Versions {
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 @@ -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.ThreadId
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.AppNavigationStateService
import io.element.android.services.appnavstate.api.NavigationState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -50,16 +50,15 @@ class DefaultAppNavigationStateService @Inject constructor( @@ -50,16 +50,15 @@ class DefaultAppNavigationStateService @Inject constructor(
private val state = MutableStateFlow(
AppNavigationState(
navigationState = NavigationState.Root,
isInForeground = true,
)
navigationState = NavigationState.Root,
isInForeground = true,
)
)
override val appNavigationState: StateFlow<AppNavigationState> = state
init {
coroutineScope.launch {
appForegroundStateService.start()
appForegroundStateService.isInForeground.collect { isInForeground ->
state.getAndUpdate { it.copy(isInForeground = isInForeground) }
}
@ -83,7 +82,7 @@ class DefaultAppNavigationStateService @Inject constructor( @@ -83,7 +82,7 @@ class DefaultAppNavigationStateService @Inject constructor(
val currentValue = state.value.navigationState
Timber.tag(loggerTag.value).d("Navigating to space $spaceId. Current state: $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.Space -> NavigationState.Space(owner, spaceId, currentValue.parentSession)
is NavigationState.Room -> NavigationState.Space(owner, spaceId, currentValue.parentSpace.parentSession)
@ -96,8 +95,8 @@ class DefaultAppNavigationStateService @Inject constructor( @@ -96,8 +95,8 @@ class DefaultAppNavigationStateService @Inject constructor(
val currentValue = state.value.navigationState
Timber.tag(loggerTag.value).d("Navigating to room $roomId. Current state: $currentValue")
val newValue: NavigationState.Room = when (currentValue) {
NavigationState.Root -> error("onNavigateToSession() must be called first")
is NavigationState.Session -> error("onNavigateToSpace() must be called first")
NavigationState.Root -> return logError("onNavigateToSession()")
is NavigationState.Session -> return logError("onNavigateToSpace()")
is NavigationState.Space -> NavigationState.Room(owner, roomId, currentValue)
is NavigationState.Room -> NavigationState.Room(owner, roomId, currentValue.parentSpace)
is NavigationState.Thread -> NavigationState.Room(owner, roomId, currentValue.parentRoom.parentSpace)
@ -109,9 +108,9 @@ class DefaultAppNavigationStateService @Inject constructor( @@ -109,9 +108,9 @@ class DefaultAppNavigationStateService @Inject constructor(
val currentValue = state.value.navigationState
Timber.tag(loggerTag.value).d("Navigating to thread $threadId. Current state: $currentValue")
val newValue: NavigationState.Thread = when (currentValue) {
NavigationState.Root -> error("onNavigateToSession() must be called first")
is NavigationState.Session -> error("onNavigateToSpace() must be called first")
is NavigationState.Space -> error("onNavigateToRoom() must be called first")
NavigationState.Root -> return logError("onNavigateToSession()")
is NavigationState.Session -> return logError("onNavigateToSpace()")
is NavigationState.Space -> return logError("onNavigateToRoom()")
is NavigationState.Room -> NavigationState.Thread(owner, threadId, currentValue)
is NavigationState.Thread -> NavigationState.Thread(owner, threadId, currentValue.parentRoom)
}
@ -123,10 +122,10 @@ class DefaultAppNavigationStateService @Inject constructor( @@ -123,10 +122,10 @@ class DefaultAppNavigationStateService @Inject constructor(
Timber.tag(loggerTag.value).d("Leaving thread. Current state: $currentValue")
if (!currentValue.assertOwner(owner)) return
val newValue: NavigationState.Room = when (currentValue) {
NavigationState.Root -> error("onNavigateToSession() must be called first")
is NavigationState.Session -> error("onNavigateToSpace() must be called first")
is NavigationState.Space -> error("onNavigateToRoom() must be called first")
is NavigationState.Room -> error("onNavigateToThread() must be called first")
NavigationState.Root -> return logError("onNavigateToSession()")
is NavigationState.Session -> return logError("onNavigateToSpace()")
is NavigationState.Space -> return logError("onNavigateToRoom()")
is NavigationState.Room -> return logError("onNavigateToThread()")
is NavigationState.Thread -> currentValue.parentRoom
}
state.getAndUpdate { it.copy(navigationState = newValue) }
@ -137,9 +136,9 @@ class DefaultAppNavigationStateService @Inject constructor( @@ -137,9 +136,9 @@ class DefaultAppNavigationStateService @Inject constructor(
Timber.tag(loggerTag.value).d("Leaving room. Current state: $currentValue")
if (!currentValue.assertOwner(owner)) return
val newValue: NavigationState.Space = when (currentValue) {
NavigationState.Root -> error("onNavigateToSession() must be called first")
is NavigationState.Session -> error("onNavigateToSpace() must be called first")
is NavigationState.Space -> error("onNavigateToRoom() must be called first")
NavigationState.Root -> return logError("onNavigateToSession()")
is NavigationState.Session -> return logError("onNavigateToSpace()")
is NavigationState.Space -> return logError("onNavigateToRoom()")
is NavigationState.Room -> currentValue.parentSpace
is NavigationState.Thread -> currentValue.parentRoom.parentSpace
}
@ -151,8 +150,8 @@ class DefaultAppNavigationStateService @Inject constructor( @@ -151,8 +150,8 @@ class DefaultAppNavigationStateService @Inject constructor(
Timber.tag(loggerTag.value).d("Leaving space. Current state: $currentValue")
if (!currentValue.assertOwner(owner)) return
val newValue: NavigationState.Session = when (currentValue) {
NavigationState.Root -> error("onNavigateToSession() must be called first")
is NavigationState.Session -> error("onNavigateToSpace() must be called first")
NavigationState.Root -> return logError("onNavigateToSession()")
is NavigationState.Session -> return logError("onNavigateToSpace()")
is NavigationState.Space -> currentValue.parentSession
is NavigationState.Room -> currentValue.parentSpace.parentSession
is NavigationState.Thread -> currentValue.parentRoom.parentSpace.parentSession
@ -167,6 +166,10 @@ class DefaultAppNavigationStateService @Inject constructor( @@ -167,6 +166,10 @@ class DefaultAppNavigationStateService @Inject constructor(
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 {
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)")

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