Merge branch 'release/0.2.3' into main
This commit is contained in:
commit
43ff5a9116
179 changed files with 810 additions and 243 deletions
35
.github/ISSUE_TEMPLATE/story.yml
vendored
35
.github/ISSUE_TEMPLATE/story.yml
vendored
|
|
@ -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
vendored
4
.github/workflows/build.yml
vendored
|
|
@ -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:
|
|||
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
vendored
2
.github/workflows/danger.yml
vendored
|
|
@ -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
vendored
2
.github/workflows/nightlyReports.yml
vendored
|
|
@ -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
vendored
4
.github/workflows/quality.yml
vendored
|
|
@ -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:
|
|||
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
vendored
2
.github/workflows/recordScreenshots.yml
vendored
|
|
@ -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
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -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
vendored
2
.github/workflows/sonar.yml
vendored
|
|
@ -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
vendored
2
.github/workflows/tests.yml
vendored
|
|
@ -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
13
CHANGES.md
|
|
@ -1,3 +1,16 @@
|
|||
Changes in Element X v0.2.3 (2023-09-27)
|
||||
========================================
|
||||
|
||||
Features ✨
|
||||
----------
|
||||
- Handle installation of Apks from the media viewer. ([#1432](https://github.com/vector-im/element-x-android/pull/1432))
|
||||
- Integrate SDK 0.1.58 ([#1437](https://github.com/vector-im/element-x-android/pull/1437))
|
||||
|
||||
Other changes
|
||||
-------------
|
||||
- Element call: add custom parameters to Element Call urls. ([#1434](https://github.com/vector-im/element-x-android/issues/1434))
|
||||
|
||||
|
||||
Changes in Element X v0.2.2 (2023-09-21)
|
||||
========================================
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|||
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(
|
|||
}
|
||||
}
|
||||
|
||||
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(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
Normal file
2
fastlane/metadata/android/en-US/changelogs/40002030.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
Main changes in this version: bugfixes.
|
||||
Full changelog: https://github.com/vector-im/element-x-android/releases
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
}
|
||||
// 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
Normal file
6
features/call/src/main/res/values-cs/translations.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="call_foreground_service_channel_title_android">"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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 {
|
|||
}
|
||||
|
||||
@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 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)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `element scheme 2 with url param gets url extracted`() {
|
||||
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:/?url=$encodedUrl"
|
||||
assertThat(callIntentDataParser.parse(url)).isEqualTo(embeddedUrl)
|
||||
assertThat(callIntentDataParser.parse(url)).isEqualTo("$VALID_CALL_URL_WITH_PARAM&$EXTRA_PARAMS")
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -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 {
|
|||
|
||||
@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 {
|
|||
|
||||
@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,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>
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
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) {
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
|||
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
|
|||
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(
|
|||
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(
|
|||
targetEvent = event.event,
|
||||
composerState = composerState,
|
||||
enableTextFormatting = enableTextFormatting,
|
||||
timelineState = timelineState,
|
||||
)
|
||||
}
|
||||
is MessagesEvents.ToggleReaction -> {
|
||||
|
|
@ -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(
|
|||
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(
|
|||
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(
|
|||
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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
|||
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(
|
|||
) : 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(
|
|||
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(
|
|||
}
|
||||
}
|
||||
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
@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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
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 {
|
|||
@Composable
|
||||
private fun MediaViewerTopBar(
|
||||
actionsEnabled: Boolean,
|
||||
mimeType: String,
|
||||
onBackPressed: () -> Unit,
|
||||
eventSink: (MediaViewerEvents) -> Unit,
|
||||
) {
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -26,4 +26,8 @@ sealed interface TimelineEvents {
|
|||
val pollStartId: EventId,
|
||||
val answerId: String
|
||||
) : TimelineEvents
|
||||
|
||||
data class PollEndClicked(
|
||||
val pollStartId: EventId,
|
||||
) : TimelineEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
)
|
||||
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(
|
|||
paginationState = paginationState,
|
||||
timelineItems = timelineItems,
|
||||
hasNewItems = hasNewItems.value,
|
||||
eventSink = { handleEvents(it) }
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
onReactionLongClick = onReactionLongClicked,
|
||||
onMoreReactionsClick = onMoreReactionsClicked,
|
||||
onTimestampClicked = onTimestampClicked,
|
||||
onPollAnswerSelected = ::onPollAnswerSelected,
|
||||
eventSink = state.eventSink,
|
||||
onSwipeToReply = onSwipeToReply,
|
||||
)
|
||||
}
|
||||
|
|
@ -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(
|
|||
isHighlighted = highlightedItem == timelineItem.identifier(),
|
||||
onClick = { onClick(timelineItem) },
|
||||
onLongClick = { onLongClick(timelineItem) },
|
||||
eventSink = eventSink,
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
|
|
@ -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(
|
|||
onReactionClick = onReactionClick,
|
||||
onReactionLongClick = onReactionLongClick,
|
||||
onMoreReactionsClick = onMoreReactionsClick,
|
||||
onPollAnswerSelected = onPollAnswerSelected,
|
||||
eventSink = eventSink,
|
||||
onSwipeToReply = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|||
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(
|
|||
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(
|
|||
}
|
||||
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(
|
|||
onReactionClicked = { emoji -> onReactionClick(emoji, event) },
|
||||
onReactionLongClicked = { emoji -> onReactionLongClick(emoji, event) },
|
||||
onMoreReactionsClicked = { onMoreReactionsClick(event) },
|
||||
onPollAnswerSelected = onPollAnswerSelected,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
|||
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(
|
|||
onTimestampClicked = {
|
||||
onTimestampClicked(event)
|
||||
},
|
||||
onPollAnswerSelected = onPollAnswerSelected,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -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(
|
|||
) {
|
||||
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 {
|
|||
onMoreReactionsClick = {},
|
||||
onTimestampClicked = {},
|
||||
onSwipeToReply = {},
|
||||
onPollAnswerSelected = { _, _ -> },
|
||||
eventSink = {},
|
||||
)
|
||||
TimelineItemEventRow(
|
||||
event = aTimelineItemEvent(
|
||||
|
|
@ -673,7 +675,7 @@ internal fun TimelineItemEventRowPreview() = ElementPreview {
|
|||
onMoreReactionsClick = {},
|
||||
onTimestampClicked = {},
|
||||
onSwipeToReply = {},
|
||||
onPollAnswerSelected = { _, _ -> },
|
||||
eventSink = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -712,7 +714,7 @@ internal fun TimelineItemEventRowWithReplyPreview() = ElementPreview {
|
|||
onMoreReactionsClick = {},
|
||||
onTimestampClicked = {},
|
||||
onSwipeToReply = {},
|
||||
onPollAnswerSelected = { _, _ -> },
|
||||
eventSink = {},
|
||||
)
|
||||
TimelineItemEventRow(
|
||||
event = aTimelineItemEvent(
|
||||
|
|
@ -735,7 +737,7 @@ internal fun TimelineItemEventRowWithReplyPreview() = ElementPreview {
|
|||
onMoreReactionsClick = {},
|
||||
onTimestampClicked = {},
|
||||
onSwipeToReply = {},
|
||||
onPollAnswerSelected = { _, _ -> },
|
||||
eventSink = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -786,7 +788,7 @@ internal fun TimelineItemEventRowTimestampPreview(
|
|||
onMoreReactionsClick = {},
|
||||
onTimestampClicked = {},
|
||||
onSwipeToReply = {},
|
||||
onPollAnswerSelected = { _, _ -> },
|
||||
eventSink = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -818,7 +820,7 @@ internal fun TimelineItemEventRowWithManyReactionsPreview() = ElementPreview {
|
|||
onMoreReactionsClick = {},
|
||||
onSwipeToReply = {},
|
||||
onTimestampClicked = {},
|
||||
onPollAnswerSelected = { _, _ -> },
|
||||
eventSink = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -843,7 +845,7 @@ internal fun TimelineItemEventRowLongSenderNamePreview() = ElementPreviewLight {
|
|||
onMoreReactionsClick = {},
|
||||
onSwipeToReply = {},
|
||||
onTimestampClicked = {},
|
||||
onPollAnswerSelected = { _, _ -> },
|
||||
eventSink = {},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -864,6 +866,6 @@ internal fun TimelineItemEventTimestampBelowPreview() = ElementPreviewLight {
|
|||
onMoreReactionsClick = {},
|
||||
onSwipeToReply = {},
|
||||
onTimestampClicked = {},
|
||||
onPollAnswerSelected = { _, _ -> },
|
||||
eventSink = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|||
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(
|
|||
isHighlighted: Boolean,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
eventSink: (TimelineEvents) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
|
@ -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 {
|
|||
isHighlighted = false,
|
||||
onClick = {},
|
||||
onLongClick = {},
|
||||
eventSink = {}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|||
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(
|
|||
)
|
||||
is TimelineItemPollContent -> TimelineItemPollView(
|
||||
content = content,
|
||||
onAnswerSelected = onPollAnswerSelected,
|
||||
isMine = isMine,
|
||||
eventSink = eventSink,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|||
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 = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
<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>
|
||||
|
|
|
|||
|
|
@ -644,7 +644,6 @@ class MessagesPresenterTest {
|
|||
messageSummaryFormatter = FakeMessageSummaryFormatter(),
|
||||
navigator = navigator,
|
||||
clipboardHelper = clipboardHelper,
|
||||
analyticsService = analyticsService,
|
||||
preferencesStore = preferencesStore,
|
||||
dispatchers = coroutineDispatchers,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
|||
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 {
|
|||
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()
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) =
|
|||
isEnabled = !isEnded,
|
||||
isWinner = false,
|
||||
isSelected = true,
|
||||
votesCount = 1,
|
||||
votesCount = if (hasVotes) 1 else 0,
|
||||
percentage = 0.1f
|
||||
),
|
||||
aPollAnswerItem(isDisclosed = isDisclosed, isEnabled = !isEnded),
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
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(
|
|||
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(
|
|||
)
|
||||
}
|
||||
|
||||
@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 {
|
|||
answerItems = aPollAnswerItemList(isDisclosed = false),
|
||||
pollKind = PollKind.Undisclosed,
|
||||
isPollEnded = false,
|
||||
isMine = false,
|
||||
onAnswerSelected = { _, _ -> },
|
||||
onPollEdit = {},
|
||||
onPollEnd = {},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -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 {
|
|||
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 = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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(
|
|||
) : 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(
|
|||
.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(
|
|||
copy(sendScreenshot = event.sendScreenshot)
|
||||
}
|
||||
BugReportEvents.ClearError -> {
|
||||
sendingProgress.value = 0f
|
||||
sendingProgress.floatValue = 0f
|
||||
sendingAction.value = Async.Uninitialized
|
||||
}
|
||||
}
|
||||
|
|
@ -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,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>
|
||||
|
|
|
|||
|
|
@ -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,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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
|||
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" }
|
|||
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" }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
/**
|
||||
* 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 {
|
|||
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
|
||||
|
|
|
|||
|
|
@ -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() }
|
|||
|
||||
@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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
suspend fun endPoll(pollStartId: EventId, text: String): Result<Unit>
|
||||
|
||||
override fun close() = destroy()
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
val mediaFile = innerClient.getMediaFile(
|
||||
mediaSource = mediaSource,
|
||||
body = body,
|
||||
mimeType = mimeType ?: "application/octet-stream",
|
||||
mimeType = mimeType ?: MimeTypes.OctetStream,
|
||||
tempDir = cacheDirectory.path,
|
||||
)
|
||||
RustMediaFile(mediaFile)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
// 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
|
||||
|
|
|
|||
|
|
@ -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
|
|||
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(
|
|||
roomCoroutineScope.cancel()
|
||||
innerRoom.destroy()
|
||||
roomListItem.destroy()
|
||||
inReplyToEventTimelineItem?.destroy()
|
||||
}
|
||||
|
||||
override val name: String?
|
||||
|
|
@ -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(
|
|||
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(
|
|||
description = description,
|
||||
zoomLevel = zoomLevel?.toUByte(),
|
||||
assetType = assetType?.toInner(),
|
||||
txnId = genTransactionId(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -433,7 +445,6 @@ class RustMatrixRoom(
|
|||
answers = answers,
|
||||
maxSelections = maxSelections.toUByte(),
|
||||
pollKind = pollKind.toInner(),
|
||||
txnId = genTransactionId(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -446,7 +457,6 @@ class RustMatrixRoom(
|
|||
innerRoom.sendPollResponse(
|
||||
pollStartId = pollStartId.value,
|
||||
answers = answers,
|
||||
txnId = genTransactionId(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -459,7 +469,6 @@ class RustMatrixRoom(
|
|||
innerRoom.endPoll(
|
||||
pollStartId = pollStartId.value,
|
||||
text = text,
|
||||
txnId = genTransactionId(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
) {
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 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>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
<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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
<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 @@
|
|||
<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 @@
|
|||
<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 @@
|
|||
<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 @@
|
|||
<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 @@
|
|||
<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>
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
<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">"Datenschutzerklärung"</string>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
<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 @@
|
|||
<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 @@
|
|||
<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 @@
|
|||
<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>
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
<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 @@
|
|||
<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 @@
|
|||
<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 @@
|
|||
<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 @@
|
|||
<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 @@
|
|||
<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 @@
|
|||
<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 @@
|
|||
<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 @@
|
|||
<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 @@
|
|||
<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 @@
|
|||
<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 @@
|
|||
<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>
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
<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 @@
|
|||
<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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
|
||||
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(
|
|||
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(
|
|||
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(
|
|||
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(
|
|||
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(
|
|||
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(
|
|||
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(
|
|||
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)")
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
@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(
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:15ec8227e84c01b8e9acd5271e379d989c8e4ed72b57c87d252a992a0d3f1a1a
|
||||
size 15702
|
||||
oid sha256:bd2a831abc63de6366f2a4fbac653d551e29fbbbb698f5dc3ae51de04dbf6138
|
||||
size 15941
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:08c11b272ebe9b4d8f9b76841b60a519d08a9e5f37970c66b78bd36319bcd53f
|
||||
size 15782
|
||||
oid sha256:6b7dea2e1df15375ae07db4e6cf7b2a14a26c0ff1f2cbb11efdaea263d954550
|
||||
size 16067
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:09440b685e219b4a5f5dc78d2694707d7eeb1905d6fdea6395e3dcc941482ac5
|
||||
size 51846
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ae8d6b561d358c7ebccae2da4281f3ea1b4d433f52de3c3b2008c1208c0c8bd7
|
||||
size 54037
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a0cd0936ace9bac190f21680bf339cc23839e5f5d241522cb8f328b2db0887f6
|
||||
size 48293
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cc96f4b704396e0931a08a968139ee9d586cd1e14358b546361d11bc91e7bab5
|
||||
size 50373
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue