diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 91dc6d830b..1ba71c1f61 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -20,6 +20,10 @@ jobs:
fail-fast: false
matrix:
target: [ Gplay, Fdroid ]
+ # Allow all jobs on develop. Just one per PR.
+ concurrency:
+ group: ${{ github.ref == 'refs/heads/develop' && format('integration-tests-develop-{0}-{1}', matrix.target, github.sha) || format('build-debug-{0}-{1}', matrix.target, github.ref) }}
+ cancel-in-progress: true
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v2
@@ -43,6 +47,7 @@ jobs:
name: Build unsigned GPlay APKs
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
+ # Only runs on main, no concurrency.
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v2
diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml
index 405a2b3065..ee4a87293f 100644
--- a/.github/workflows/gradle-wrapper-validation.yml
+++ b/.github/workflows/gradle-wrapper-validation.yml
@@ -5,6 +5,7 @@ jobs:
validation:
name: "Validation"
runs-on: ubuntu-latest
+ # No concurrency required, this is a prerequisite to other actions and should run every time.
steps:
- uses: actions/checkout@v2
- uses: gradle/wrapper-validation-action@v1
diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
index 4deb266824..192be1fe9e 100644
--- a/.github/workflows/nightly.yml
+++ b/.github/workflows/nightly.yml
@@ -20,8 +20,13 @@ jobs:
build-android-test-matrix-sdk:
name: Matrix SDK - Build Android Tests
runs-on: macos-latest
+ # No concurrency required, runs every time on a schedule.
steps:
- uses: actions/checkout@v2
+ - uses: actions/setup-java@v2
+ with:
+ distribution: 'adopt'
+ java-version: 11
- uses: actions/cache@v2
with:
path: |
@@ -37,8 +42,13 @@ jobs:
build-android-test-app:
name: App - Build Android Tests
runs-on: macos-latest
+ # No concurrency required, runs every time on a schedule.
steps:
- uses: actions/checkout@v2
+ - uses: actions/setup-java@v2
+ with:
+ distribution: 'adopt'
+ java-version: 11
- uses: actions/cache@v2
with:
path: |
@@ -50,7 +60,7 @@ jobs:
- name: Build Android Tests for vector
run: ./gradlew clean vector:assembleAndroidTest $CI_GRADLE_ARG_PROPERTIES --stacktrace
- # Run Android Tests
+ # Run Android Tests
integration-tests:
name: Matrix SDK - Running Integration Tests
runs-on: macos-latest
@@ -58,6 +68,7 @@ jobs:
fail-fast: false
matrix:
api-level: [ 28 ]
+ # No concurrency required, runs every time on a schedule.
steps:
- uses: actions/checkout@v2
- uses: gradle/wrapper-validation-action@v1
@@ -83,7 +94,7 @@ jobs:
curl https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh -o start.sh
chmod 777 start.sh
./start.sh --no-rate-limit
-# package: org.matrix.android.sdk.session
+ # package: org.matrix.android.sdk.session
- name: Run integration tests for Matrix SDK [org.matrix.android.sdk.session] API[${{ matrix.api-level }}]
uses: reactivecircus/android-emulator-runner@v2
with:
@@ -113,7 +124,7 @@ jobs:
if: always()
id: get-comment-body-account
run: python3 ./tools/ci/render_test_output.py account ./matrix-sdk-android/build/outputs/androidTest-results/connected/*.xml
-# package: org.matrix.android.sdk.internal
+ # package: org.matrix.android.sdk.internal
- name: Run integration tests for Matrix SDK [org.matrix.android.sdk.internal] API[${{ matrix.api-level }}]
if: always()
uses: reactivecircus/android-emulator-runner@v2
@@ -129,7 +140,7 @@ jobs:
if: always()
id: get-comment-body-internal
run: python3 ./tools/ci/render_test_output.py internal ./matrix-sdk-android/build/outputs/androidTest-results/connected/*.xml
-# package: org.matrix.android.sdk.ordering
+ # package: org.matrix.android.sdk.ordering
- name: Run integration tests for Matrix SDK [org.matrix.android.sdk.ordering] API[${{ matrix.api-level }}]
if: always()
uses: reactivecircus/android-emulator-runner@v2
@@ -145,7 +156,7 @@ jobs:
if: always()
id: get-comment-body-ordering
run: python3 ./tools/ci/render_test_output.py ordering ./matrix-sdk-android/build/outputs/androidTest-results/connected/*.xml
-# package: class PermalinkParserTest
+ # package: class PermalinkParserTest
- name: Run integration tests for Matrix SDK class [org.matrix.android.sdk.PermalinkParserTest] API[${{ matrix.api-level }}]
if: always()
uses: reactivecircus/android-emulator-runner@v2
@@ -161,7 +172,7 @@ jobs:
if: always()
id: get-comment-body-permalink
run: python3 ./tools/ci/render_test_output.py permalink ./matrix-sdk-android/build/outputs/androidTest-results/connected/*.xml
-# package: class PermalinkParserTest
+ # package: class PermalinkParserTest
- name: Find Comment
if: always() && github.event_name == 'pull_request'
uses: peter-evans/find-comment@v1
@@ -193,6 +204,7 @@ jobs:
fail-fast: false
matrix:
api-level: [ 28 ]
+ # No concurrency required, runs every time on a schedule.
steps:
- uses: actions/checkout@v2
with:
@@ -243,17 +255,19 @@ jobs:
emulator.log
failure_screenshots/
+# Notify the channel about scheduled runs, do not notify for manually triggered runs
notify:
runs-on: ubuntu-latest
needs:
- - integration-tests
- - ui-tests
- if: always()
+ - integration-tests
+ - ui-tests
+ if: always() && github.event_name != 'workflow_dispatch'
+ # No concurrency required, runs every time on a schedule.
steps:
- - uses: michaelkaye/matrix-hookshot-action@v0.2.0
- with:
- github_token: ${{ secrets.GITHUB_TOKEN }}
- matrix_access_token: ${{ secrets.ELEMENT_ANDROID_NOTIFICATION_ACCESS_TOKEN }}
- matrix_room_id: ${{ secrets.ELEMENT_ANDROID_INTERNAL_ROOM_ID }}
- text_template: "Nightly test run: {{#each job_statuses }}{{#with this }}{{#if completed }} {{name}} {{conclusion}} at {{completed_at}}, {{/if}}{{/with}}{{/each}}"
- html_template: "Nightly test run results: {{#each job_statuses }}{{#with this }}{{#if completed }}
{{name}} {{conclusion}} at {{completed_at}} [details]{{/if}}{{/with}}{{/each}}"
+ - uses: michaelkaye/matrix-hookshot-action@v0.2.0
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ matrix_access_token: ${{ secrets.ELEMENT_ANDROID_NOTIFICATION_ACCESS_TOKEN }}
+ matrix_room_id: ${{ secrets.ELEMENT_ANDROID_INTERNAL_ROOM_ID }}
+ text_template: "Nightly test run: {{#each job_statuses }}{{#with this }}{{#if completed }} {{name}} {{conclusion}} at {{completed_at}}, {{/if}}{{/with}}{{/each}}"
+ html_template: "Nightly test run results: {{#each job_statuses }}{{#with this }}{{#if completed }}
{{name}} {{conclusion}} at {{completed_at}} [details]{{/if}}{{/with}}{{/each}}"
\ No newline at end of file
diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml
index 69f17a3875..02827e7f17 100644
--- a/.github/workflows/quality.yml
+++ b/.github/workflows/quality.yml
@@ -18,6 +18,10 @@ jobs:
ktlint:
name: Kotlin Linter
runs-on: ubuntu-latest
+ # Allow all jobs on main and develop. Just one per PR.
+ concurrency:
+ group: ${{ github.ref == 'refs/heads/main' && format('ktlint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('ktlint-develop-{0}', github.sha) || format('ktlint-{0}', github.ref) }}
+ cancel-in-progress: true
steps:
- uses: actions/checkout@v2
- name: Run ktlint
@@ -87,6 +91,10 @@ jobs:
android-lint:
name: Android Linter
runs-on: ubuntu-latest
+ # Allow all jobs on main and develop. Just one per PR.
+ concurrency:
+ group: ${{ github.ref == 'refs/heads/main' && format('android-lint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('android-lint-develop-{0}', github.sha) || format('android-lint-{0}', github.ref) }}
+ cancel-in-progress: true
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v2
@@ -116,6 +124,10 @@ jobs:
fail-fast: false
matrix:
target: [ Gplay, Fdroid ]
+ # Allow all jobs on develop. Just one per PR.
+ concurrency:
+ group: ${{ github.ref == 'refs/heads/develop' && format('apk-lint-develop-{0}-{1}', matrix.target, github.sha) || format('apk-lint-{0}-{1}', matrix.target, github.ref) }}
+ cancel-in-progress: true
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v2
diff --git a/.github/workflows/sanity_test.yml b/.github/workflows/sanity_test.yml
deleted file mode 100644
index 83ad067446..0000000000
--- a/.github/workflows/sanity_test.yml
+++ /dev/null
@@ -1,84 +0,0 @@
-name: Sanity Test
-
-on:
- schedule:
- # At 20:00 every day UTC
- - cron: '0 20 * * *'
-
-# Enrich gradle.properties for CI/CD
-env:
- CI_GRADLE_ARG_PROPERTIES: >
- -Porg.gradle.jvmargs=-Xmx4g
- -Porg.gradle.parallel=false
-
-jobs:
- integration-tests:
- name: Sanity Tests (Synapse)
- runs-on: macos-latest
- strategy:
- fail-fast: false
- matrix:
- api-level: [ 28 ]
- steps:
- - uses: actions/checkout@v2
- with:
- ref: develop
- - name: Set up Python 3.8
- uses: actions/setup-python@v2
- with:
- python-version: 3.8
- - uses: actions/cache@v2
- with:
- path: |
- ~/.gradle/caches
- ~/.gradle/wrapper
- key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
- restore-keys: |
- ${{ runner.os }}-gradle-
- - name: Start synapse server
- run: |
- pip install matrix-synapse
- curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh \
- | sed s/127.0.0.1/0.0.0.0/g | sed 's/http:\/\/localhost/http:\/\/10.0.2.2/g' | bash -s -- --no-rate-limit
- - uses: actions/setup-java@v2
- with:
- distribution: 'adopt'
- java-version: '11'
- - name: Run sanity tests on API ${{ matrix.api-level }}
- uses: reactivecircus/android-emulator-runner@v2
- with:
- api-level: ${{ matrix.api-level }}
- arch: x86
- profile: Nexus 5X
- force-avd-creation: false
- emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
- emulator-build: 7425822 # workaround to emulator bug: https://github.com/ReactiveCircus/android-emulator-runner/issues/160
- script: |
- adb root
- adb logcat -c
- touch emulator.log
- chmod 777 emulator.log
- adb logcat >> emulator.log &
- ./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedGplayDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=im.vector.app.ui.UiAllScreensSanityTest || (adb pull storage/emulated/0/Pictures/failure_screenshots && exit 1 )
- - name: Upload Test Report Log
- uses: actions/upload-artifact@v2
- if: always()
- with:
- name: sanity-error-results
- path: |
- emulator.log
- failure_screenshots/
-
-
- notify:
- runs-on: ubuntu-latest
- needs: integration-tests
- if: always()
- steps:
- - uses: michaelkaye/matrix-hookshot-action@v0.2.0
- with:
- github_token: ${{ secrets.GITHUB_TOKEN }}
- matrix_access_token: ${{ secrets.ELEMENT_ANDROID_NOTIFICATION_ACCESS_TOKEN }}
- matrix_room_id: ${{ secrets.ELEMENT_ANDROID_INTERNAL_ROOM_ID }}
- text_template: "Sanity test run: {{#each job_statuses }}{{#with this }}{{#if completed }} {{name}} {{conclusion}} at {{completed_at}} {{html_url}}{{/if}}{{/with}}{{/each}}"
- html_template: "CI Sanity test run results: {{#each job_statuses }}{{#with this }}{{#if completed }} {{name}} {{conclusion}} at {{completed_at}} [details]{{/if}}{{/with}}{{/each}}"
diff --git a/.github/workflows/sync-from-external-sources.yml b/.github/workflows/sync-from-external-sources.yml
index a890082575..2323af0554 100644
--- a/.github/workflows/sync-from-external-sources.yml
+++ b/.github/workflows/sync-from-external-sources.yml
@@ -9,6 +9,7 @@ jobs:
runs-on: ubuntu-latest
# Skip in forks
if: github.repository == 'vector-im/element-android'
+ # No concurrency required, runs every time on a schedule.
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
@@ -35,6 +36,7 @@ jobs:
runs-on: ubuntu-latest
# Skip in forks
if: github.repository == 'vector-im/element-android'
+ # No concurrency required, runs every time on a schedule.
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
@@ -60,6 +62,7 @@ jobs:
runs-on: ubuntu-latest
# Skip in forks
if: github.repository == 'vector-im/element-android'
+ # No concurrency required, runs every time on a schedule.
steps:
- uses: actions/checkout@v2
- name: Run analytics import script
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 50195638de..d6e194916b 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -15,6 +15,10 @@ jobs:
unit-tests:
name: Run Unit Tests
runs-on: ubuntu-latest
+ # Allow all jobs on main and develop. Just one per PR.
+ concurrency:
+ group: ${{ github.ref == 'refs/heads/main' && format('unit-tests-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('unit-tests-develop-{0}', github.sha) || format('unit-tests-{0}', github.ref) }}
+ cancel-in-progress: true
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v2
diff --git a/README.md b/README.md
index d784841e2c..8306fd8593 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ It is a total rewrite of [Riot-Android](https://github.com/vector-im/riot-androi
[](https://play.google.com/store/apps/details?id=im.vector.app)
[](https://f-droid.org/app/im.vector.app)
-Nightly build: [![Buildkite](https://badge.buildkite.com/ad0065c1b70f557cd3b1d3d68f9c2154010f83c4d6f71706a9.svg?branch=develop)](https://buildkite.com/matrix-dot-org/element-android/builds?branch=develop) Nighly sanity test status: [![allScreensTest](https://github.com/vector-im/element-android/actions/workflows/sanity_test.yml/badge.svg)](https://github.com/vector-im/element-android/actions/workflows/sanity_test.yml)
+Nightly build: [![Buildkite](https://badge.buildkite.com/ad0065c1b70f557cd3b1d3d68f9c2154010f83c4d6f71706a9.svg?branch=develop)](https://buildkite.com/matrix-dot-org/element-android/builds?branch=develop) Nighly test status: [![allScreensTest](https://github.com/vector-im/element-android/actions/workflows/nightly.yml/badge.svg)](https://github.com/vector-im/element-android/actions/workflows/nightly.yml)
# New Android SDK
diff --git a/build.gradle b/build.gradle
index 6d5ef35547..9cae9e7e70 100644
--- a/build.gradle
+++ b/build.gradle
@@ -144,11 +144,6 @@ project(":library:diff-match-patch") {
}
}
-// Global configurations across all modules
-ext {
- isThreadingEnabled = true
-}
-
//project(":matrix-sdk-android") {
// sonarqube {
// properties {
diff --git a/changelog.d/4319.bugfix b/changelog.d/4319.bugfix
new file mode 100644
index 0000000000..da42c864c6
--- /dev/null
+++ b/changelog.d/4319.bugfix
@@ -0,0 +1 @@
+Open direct message screen when clicking on DM button in the space members list
diff --git a/changelog.d/5005.feature b/changelog.d/5005.feature
new file mode 100644
index 0000000000..ce3b2ad1f9
--- /dev/null
+++ b/changelog.d/5005.feature
@@ -0,0 +1 @@
+Add possibility to save media from Gallery + reorder choices in message context menu
diff --git a/changelog.d/5325.feature b/changelog.d/5325.feature
new file mode 100644
index 0000000000..23754c790d
--- /dev/null
+++ b/changelog.d/5325.feature
@@ -0,0 +1 @@
+Adds forceLoginFallback feature flag and usages to FTUE login and registration
\ No newline at end of file
diff --git a/changelog.d/5330.misc b/changelog.d/5330.misc
new file mode 100644
index 0000000000..6315ad536c
--- /dev/null
+++ b/changelog.d/5330.misc
@@ -0,0 +1 @@
+Continue improving realm usage.
\ No newline at end of file
diff --git a/changelog.d/5330.sdk b/changelog.d/5330.sdk
new file mode 100644
index 0000000000..3f6d46401c
--- /dev/null
+++ b/changelog.d/5330.sdk
@@ -0,0 +1 @@
+Change name of getTimeLineEvent and getTimeLineEventLive methods to getTimelineEvent and getTimelineEventLive.
\ No newline at end of file
diff --git a/changelog.d/5379.misc b/changelog.d/5379.misc
new file mode 100644
index 0000000000..d485636f10
--- /dev/null
+++ b/changelog.d/5379.misc
@@ -0,0 +1 @@
+Cleanup unused threads build configurations
\ No newline at end of file
diff --git a/library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt b/library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt
index 573138bf5c..21af114c26 100644
--- a/library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt
+++ b/library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt
@@ -45,6 +45,8 @@ import kotlin.math.abs
abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventListener {
+ protected val rootView: View
+ get() = views.rootContainer
protected val pager2: ViewPager2
get() = views.attachmentPager
protected val imageTransitionView: ImageView
@@ -298,10 +300,11 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi
private fun createSwipeToDismissHandler(): SwipeToDismissHandler =
SwipeToDismissHandler(
- swipeView = views.dismissContainer,
- shouldAnimateDismiss = { shouldAnimateDismiss() },
- onDismiss = { animateClose() },
- onSwipeViewMove = ::handleSwipeViewMove)
+ swipeView = views.dismissContainer,
+ shouldAnimateDismiss = { shouldAnimateDismiss() },
+ onDismiss = { animateClose() },
+ onSwipeViewMove = ::handleSwipeViewMove
+ )
private fun createSwipeDirectionDetector() =
SwipeDirectionDetector(this) { swipeDirection = it }
diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt
index 826f584f6a..c5d1d19fec 100644
--- a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt
+++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt
@@ -58,9 +58,9 @@ class FlowRoom(private val room: Room) {
}
fun liveTimelineEvent(eventId: String): Flow> {
- return room.getTimeLineEventLive(eventId).asFlow()
+ return room.getTimelineEventLive(eventId).asFlow()
.startWith(room.coroutineDispatchers.io) {
- room.getTimeLineEvent(eventId).toOptional()
+ room.getTimelineEvent(eventId).toOptional()
}
}
diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle
index 7407e1750e..3e301eebb9 100644
--- a/matrix-sdk-android/build.gradle
+++ b/matrix-sdk-android/build.gradle
@@ -38,8 +38,6 @@ android {
resValue "string", "git_sdk_revision_unix_date", "\"${gitRevisionUnixDate()}\""
resValue "string", "git_sdk_revision_date", "\"${gitRevisionDate()}\""
- // Indicates whether or not threading support is enabled
- buildConfigField "Boolean", "THREADING_ENABLED", "${isThreadingEnabled}"
defaultConfig {
consumerProguardFiles 'proguard-rules.pro'
}
@@ -169,7 +167,7 @@ dependencies {
implementation libs.apache.commonsImaging
// Phone number https://github.com/google/libphonenumber
- implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.43'
+ implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.44'
testImplementation libs.tests.junit
testImplementation 'org.robolectric:robolectric:4.7.3'
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt
index c95cc6b4ca..a7a81bacf5 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt
@@ -95,7 +95,7 @@ class PreShareKeysTest : InstrumentedTest {
assertEquals(megolmSessionId, sentEvent.root.content.toModel()?.sessionId, "Unexpected megolm session")
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
- bobSession.getRoom(e2eRoomID)?.getTimeLineEvent(sentEvent.eventId)?.root?.getClearType() == EventType.MESSAGE
+ bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEvent.eventId)?.root?.getClearType() == EventType.MESSAGE
}
}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt
index e0605db0b8..82aee454eb 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt
@@ -92,7 +92,7 @@ class KeyShareTests : InstrumentedTest {
val roomSecondSessionPOV = aliceSession2.getRoom(roomId)
- val receivedEvent = roomSecondSessionPOV?.getTimeLineEvent(sentEventId)
+ val receivedEvent = roomSecondSessionPOV?.getTimelineEvent(sentEventId)
assertNotNull(receivedEvent)
assert(receivedEvent!!.isEncrypted())
@@ -382,7 +382,7 @@ class KeyShareTests : InstrumentedTest {
commonTestHelper.sendTextMessage(roomAlicePov, "After", 1)
val roomRoomBobPov = aliceSession.getRoom(roomId)
- val beforeJoin = roomRoomBobPov!!.getTimeLineEvent(secondEventId)
+ val beforeJoin = roomRoomBobPov!!.getTimelineEvent(secondEventId)
var dRes = tryOrNull { bobSession.cryptoService().decryptEvent(beforeJoin!!.root, "") }
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt
index 586d96b007..9fda21763a 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt
@@ -80,11 +80,11 @@ class WithHeldTests : InstrumentedTest {
// await for bob unverified session to get the message
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
- bobUnverifiedSession.getRoom(roomId)?.getTimeLineEvent(timelineEvent.eventId) != null
+ bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(timelineEvent.eventId) != null
}
}
- val eventBobPOV = bobUnverifiedSession.getRoom(roomId)?.getTimeLineEvent(timelineEvent.eventId)!!
+ val eventBobPOV = bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(timelineEvent.eventId)!!
// =============================
// ASSERT
@@ -109,7 +109,7 @@ class WithHeldTests : InstrumentedTest {
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
- val ev = bobUnverifiedSession.getRoom(roomId)?.getTimeLineEvent(secondEvent.eventId)
+ val ev = bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(secondEvent.eventId)
// wait until it's decrypted
ev?.root?.getClearType() == EventType.MESSAGE
}
@@ -157,12 +157,12 @@ class WithHeldTests : InstrumentedTest {
// await for bob session to get the message
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
- bobSession.getRoom(testData.roomId)?.getTimeLineEvent(eventId) != null
+ bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId) != null
}
}
// Previous message should still be undecryptable (partially withheld session)
- val eventBobPOV = bobSession.getRoom(testData.roomId)?.getTimeLineEvent(eventId)
+ val eventBobPOV = bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId)
try {
// .. might need to wait a bit for stability?
bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "")
@@ -190,7 +190,7 @@ class WithHeldTests : InstrumentedTest {
// await for bob SecondSession session to get the message
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
- bobSecondSession.getRoom(testData.roomId)?.getTimeLineEvent(secondMessageId) != null
+ bobSecondSession.getRoom(testData.roomId)?.getTimelineEvent(secondMessageId) != null
}
}
@@ -231,7 +231,7 @@ class WithHeldTests : InstrumentedTest {
// await for bob SecondSession session to get the message
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
- val timeLineEvent = bobSecondSession.getRoom(testData.roomId)?.getTimeLineEvent(eventId)?.also {
+ val timeLineEvent = bobSecondSession.getRoom(testData.roomId)?.getTimelineEvent(eventId)?.also {
// try to decrypt and force key request
tryOrNull { bobSecondSession.cryptoService().decryptEvent(it.root, "") }
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt
index 3c021384e1..6152069644 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt
@@ -41,7 +41,7 @@ interface TimelineService {
* At the opposite of getTimeLineEventLive which will be updated when local echo event is synced, it will return null in this case.
* @param eventId the eventId to get the TimelineEvent
*/
- fun getTimeLineEvent(eventId: String): TimelineEvent?
+ fun getTimelineEvent(eventId: String): TimelineEvent?
/**
* Creates a LiveData of Optional TimelineEvent event with eventId.
@@ -49,7 +49,7 @@ interface TimelineService {
* In this case, makes sure to use the new synced eventId from the TimelineEvent class if you want to interact, as the local echo is removed from the SDK.
* @param eventId the eventId to listen for TimelineEvent
*/
- fun getTimeLineEventLive(eventId: String): LiveData>
+ fun getTimelineEventLive(eventId: String): LiveData>
/**
* Returns a snapshot list of TimelineEvent with EventType.MESSAGE and MessageType.MSGTYPE_IMAGE or MessageType.MSGTYPE_VIDEO.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt
index f3770e4afe..2be4510b6f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt
@@ -16,8 +16,11 @@
package org.matrix.android.sdk.internal.database.mapper
+import io.realm.Realm
+import io.realm.RealmList
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
import org.matrix.android.sdk.internal.database.RealmSessionProvider
+import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity
import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
import org.matrix.android.sdk.internal.database.query.where
@@ -32,14 +35,22 @@ internal class ReadReceiptsSummaryMapper @Inject constructor(
return emptyList()
}
val readReceipts = readReceiptsSummaryEntity.readReceipts
-
- return realmSessionProvider.withRealm { realm ->
- readReceipts
- .mapNotNull {
- val roomMember = RoomMemberSummaryEntity.where(realm, roomId = it.roomId, userId = it.userId).findFirst()
- ?: return@mapNotNull null
- ReadReceipt(roomMember.asDomain(), it.originServerTs.toLong())
- }
+ // Avoid opening a new realm if we already have one opened
+ return if (readReceiptsSummaryEntity.isManaged) {
+ map(readReceipts, readReceiptsSummaryEntity.realm)
+ } else {
+ realmSessionProvider.withRealm { realm ->
+ map(readReceipts, realm)
+ }
}
}
+
+ private fun map(readReceipts: RealmList, realm: Realm): List {
+ return readReceipts
+ .mapNotNull {
+ val roomMember = RoomMemberSummaryEntity.where(realm, roomId = it.roomId, userId = it.userId).findFirst()
+ ?: return@mapNotNull null
+ ReadReceipt(roomMember.asDomain(), it.originServerTs.toLong())
+ }
+ }
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
index acceaf6e24..1e0eb8b497 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
@@ -513,7 +513,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
private fun getPollEvent(roomId: String, eventId: String): TimelineEvent? {
val session = sessionManager.getSessionComponent(sessionId)?.session()
- return session?.getRoom(roomId)?.getTimeLineEvent(eventId) ?: return null.also {
+ return session?.getRoom(roomId)?.getTimelineEvent(eventId) ?: return null.also {
Timber.v("## POLL target poll event $eventId not found in room $roomId")
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt
index 848e14ff57..d5019aea7b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt
@@ -30,15 +30,14 @@ import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.NoOpCancellable
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional
-import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
-import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
+import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDataSource
import org.matrix.android.sdk.internal.util.fetchCopyMap
import timber.log.Timber
@@ -50,7 +49,7 @@ internal class DefaultRelationService @AssistedInject constructor(
private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
private val fetchEditHistoryTask: FetchEditHistoryTask,
private val fetchThreadTimelineTask: FetchThreadTimelineTask,
- private val timelineEventMapper: TimelineEventMapper,
+ private val timelineEventDataSource: TimelineEventDataSource,
@SessionDatabase private val monarchy: Monarchy
) : RelationService {
@@ -60,14 +59,8 @@ internal class DefaultRelationService @AssistedInject constructor(
}
override fun sendReaction(targetEventId: String, reaction: String): Cancelable {
- return if (monarchy
- .fetchCopyMap(
- { realm ->
- TimelineEventEntity.where(realm, roomId, targetEventId).findFirst()
- },
- { entity, _ ->
- timelineEventMapper.map(entity)
- })
+ val targetTimelineEvent = timelineEventDataSource.getTimelineEvent(roomId, targetEventId)
+ return if (targetTimelineEvent
?.annotations
?.reactionsSummary
.orEmpty()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt
index d7d61f0b47..8094fee504 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt
@@ -21,36 +21,23 @@ import com.zhuinden.monarchy.Monarchy
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
-import io.realm.Sort
-import io.realm.kotlin.where
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
-import org.matrix.android.sdk.api.session.events.model.isImageMessage
-import org.matrix.android.sdk.api.session.events.model.isVideoMessage
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.TimelineService
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import org.matrix.android.sdk.api.util.Optional
-import org.matrix.android.sdk.internal.database.RealmSessionProvider
import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
-import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
-import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
-import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase
-import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler
import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
-import org.matrix.android.sdk.internal.task.TaskExecutor
internal class DefaultTimelineService @AssistedInject constructor(
@Assisted private val roomId: String,
- @UserId private val userId: String,
@SessionDatabase private val monarchy: Monarchy,
- private val realmSessionProvider: RealmSessionProvider,
private val timelineInput: TimelineInput,
- private val taskExecutor: TaskExecutor,
private val contextOfEventTask: GetContextOfEventTask,
private val eventDecryptor: TimelineEventDecryptor,
private val paginationTask: PaginationTask,
@@ -60,7 +47,8 @@ internal class DefaultTimelineService @AssistedInject constructor(
private val threadsAwarenessHandler: ThreadsAwarenessHandler,
private val lightweightSettingsStorage: LightweightSettingsStorage,
private val readReceiptHandler: ReadReceiptHandler,
- private val coroutineDispatchers: MatrixCoroutineDispatchers
+ private val coroutineDispatchers: MatrixCoroutineDispatchers,
+ private val timelineEventDataSource: TimelineEventDataSource
) : TimelineService {
@AssistedFactory
@@ -88,27 +76,15 @@ internal class DefaultTimelineService @AssistedInject constructor(
)
}
- override fun getTimeLineEvent(eventId: String): TimelineEvent? {
- return realmSessionProvider.withRealm { realm ->
- TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId).findFirst()?.let {
- timelineEventMapper.map(it)
- }
- }
+ override fun getTimelineEvent(eventId: String): TimelineEvent? {
+ return timelineEventDataSource.getTimelineEvent(roomId, eventId)
}
- override fun getTimeLineEventLive(eventId: String): LiveData> {
- return LiveTimelineEvent(monarchy, taskExecutor.executorScope, timelineEventMapper, roomId, eventId)
+ override fun getTimelineEventLive(eventId: String): LiveData> {
+ return timelineEventDataSource.getTimelineEventLive(roomId, eventId)
}
override fun getAttachmentMessages(): List {
- // TODO pretty bad query.. maybe we should denormalize clear type in base?
- return realmSessionProvider.withRealm { realm ->
- realm.where()
- .equalTo(TimelineEventEntityFields.ROOM_ID, roomId)
- .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
- .findAll()
- ?.mapNotNull { timelineEventMapper.map(it).takeIf { it.root.isImageMessage() || it.root.isVideoMessage() } }
- .orEmpty()
- }
+ return timelineEventDataSource.getAttachmentMessages(roomId)
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt
index a98de1c595..b7a2cf2fce 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt
@@ -46,7 +46,7 @@ internal class RealmSendingEventsDataSource(
private val sendingTimelineEventsListener = RealmChangeListener> { events ->
uiEchoManager.onSentEventsInDatabase(events.map { it.eventId })
- frozenSendingTimelineEvents = sendingTimelineEvents?.freeze()
+ updateFrozenResults(events)
onEventsUpdated(false)
}
@@ -59,10 +59,17 @@ internal class RealmSendingEventsDataSource(
override fun stop() {
sendingTimelineEvents?.removeChangeListener(sendingTimelineEventsListener)
+ updateFrozenResults(null)
sendingTimelineEvents = null
roomEntity = null
}
+ private fun updateFrozenResults(sendingEvents: RealmList?) {
+ // Makes sure to close the previous frozen realm
+ frozenSendingTimelineEvents?.realm?.close()
+ frozenSendingTimelineEvents = sendingEvents?.freeze()
+ }
+
override fun buildSendingEvents(): List {
val builtSendingEvents = mutableListOf()
uiEchoManager.getInMemorySendingEvents()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDataSource.kt
new file mode 100644
index 0000000000..638866a46e
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDataSource.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.room.timeline
+
+import androidx.lifecycle.LiveData
+import com.zhuinden.monarchy.Monarchy
+import io.realm.Sort
+import io.realm.kotlin.where
+import org.matrix.android.sdk.api.session.events.model.isImageMessage
+import org.matrix.android.sdk.api.session.events.model.isVideoMessage
+import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+import org.matrix.android.sdk.api.util.Optional
+import org.matrix.android.sdk.internal.database.RealmSessionProvider
+import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
+import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
+import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
+import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.di.SessionDatabase
+import org.matrix.android.sdk.internal.task.TaskExecutor
+import javax.inject.Inject
+
+internal class TimelineEventDataSource @Inject constructor(private val realmSessionProvider: RealmSessionProvider,
+ private val timelineEventMapper: TimelineEventMapper,
+ private val taskExecutor: TaskExecutor,
+ @SessionDatabase private val monarchy: Monarchy) {
+
+ fun getTimelineEvent(roomId: String, eventId: String): TimelineEvent? {
+ return realmSessionProvider.withRealm { realm ->
+ TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId).findFirst()?.let {
+ timelineEventMapper.map(it)
+ }
+ }
+ }
+
+ fun getTimelineEventLive(roomId: String, eventId: String): LiveData> {
+ return LiveTimelineEvent(monarchy, taskExecutor.executorScope, timelineEventMapper, roomId, eventId)
+ }
+
+ fun getAttachmentMessages(roomId: String): List {
+ // TODO pretty bad query.. maybe we should denormalize clear type in base?
+ return realmSessionProvider.withRealm { realm ->
+ realm.where()
+ .equalTo(TimelineEventEntityFields.ROOM_ID, roomId)
+ .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
+ .findAll()
+ ?.mapNotNull { timelineEventMapper.map(it).takeIf { it.root.isImageMessage() || it.root.isVideoMessage() } }
+ .orEmpty()
+ }
+ }
+}
diff --git a/vector/build.gradle b/vector/build.gradle
index 597d86b457..676b84839f 100644
--- a/vector/build.gradle
+++ b/vector/build.gradle
@@ -146,9 +146,6 @@ android {
// This *must* only be set in trusted environments.
buildConfigField "Boolean", "handleCallAssertedIdentityEvents", "false"
- // Indicates whether or not threading support is enabled
- buildConfigField "Boolean", "THREADING_ENABLED", "${isThreadingEnabled}"
-
buildConfigField "Boolean", "enableLocationSharing", "true"
buildConfigField "String", "mapTilerKey", "\"fU3vlMsMn4Jb6dnEIFsx\""
@@ -367,7 +364,7 @@ dependencies {
implementation 'com.facebook.stetho:stetho:1.6.0'
// Phone number https://github.com/google/libphonenumber
- implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.43'
+ implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.44'
// FlowBinding
implementation libs.github.flowBinding
diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt
index 5cc4bd3bde..8702c8d966 100644
--- a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt
+++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt
@@ -53,7 +53,7 @@ class DebugFeaturesStateFactory @Inject constructor(
label = "FTUE Personalize profile",
key = DebugFeatureKeys.onboardingPersonalize,
factory = VectorFeatures::isOnboardingPersonalizeEnabled
- )
+ ),
))
}
diff --git a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsFragment.kt b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsFragment.kt
index 808c379354..b54d776901 100644
--- a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsFragment.kt
+++ b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsFragment.kt
@@ -43,9 +43,13 @@ class DebugPrivateSettingsFragment : VectorBaseFragment
viewModel.handle(DebugPrivateSettingsViewActions.SetDialPadVisibility(isChecked))
}
+ views.forceLoginFallback.setOnCheckedChangeListener { _, isChecked ->
+ viewModel.handle(DebugPrivateSettingsViewActions.SetForceLoginFallbackEnabled(isChecked))
+ }
}
override fun invalidate() = withState(viewModel) {
views.forceDialPadTabDisplay.isChecked = it.dialPadVisible
+ views.forceLoginFallback.isChecked = it.forceLoginFallback
}
}
diff --git a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewActions.kt b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewActions.kt
index ecbb241387..1c76cf6fb2 100644
--- a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewActions.kt
+++ b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewActions.kt
@@ -20,4 +20,5 @@ import im.vector.app.core.platform.VectorViewModelAction
sealed class DebugPrivateSettingsViewActions : VectorViewModelAction {
data class SetDialPadVisibility(val force: Boolean) : DebugPrivateSettingsViewActions()
+ data class SetForceLoginFallbackEnabled(val force: Boolean) : DebugPrivateSettingsViewActions()
}
diff --git a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewModel.kt b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewModel.kt
index 624c46556a..038b1e6cc7 100644
--- a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewModel.kt
+++ b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewModel.kt
@@ -45,15 +45,18 @@ class DebugPrivateSettingsViewModel @AssistedInject constructor(
private fun observeVectorDataStore() {
vectorDataStore.forceDialPadDisplayFlow.setOnEach {
- copy(
- dialPadVisible = it
- )
+ copy(dialPadVisible = it)
+ }
+
+ vectorDataStore.forceLoginFallbackFlow.setOnEach {
+ copy(forceLoginFallback = it)
}
}
override fun handle(action: DebugPrivateSettingsViewActions) {
when (action) {
- is DebugPrivateSettingsViewActions.SetDialPadVisibility -> handleSetDialPadVisibility(action)
+ is DebugPrivateSettingsViewActions.SetDialPadVisibility -> handleSetDialPadVisibility(action)
+ is DebugPrivateSettingsViewActions.SetForceLoginFallbackEnabled -> handleSetForceLoginFallbackEnabled(action)
}
}
@@ -62,4 +65,10 @@ class DebugPrivateSettingsViewModel @AssistedInject constructor(
vectorDataStore.setForceDialPadDisplay(action.force)
}
}
+
+ private fun handleSetForceLoginFallbackEnabled(action: DebugPrivateSettingsViewActions.SetForceLoginFallbackEnabled) {
+ viewModelScope.launch {
+ vectorDataStore.setForceLoginFallbackFlow(action.force)
+ }
+ }
}
diff --git a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewState.kt b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewState.kt
index 0ad4b185ec..7fca29af8c 100644
--- a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewState.kt
+++ b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewState.kt
@@ -19,5 +19,6 @@ package im.vector.app.features.debug.settings
import com.airbnb.mvrx.MavericksState
data class DebugPrivateSettingsViewState(
- val dialPadVisible: Boolean = false
+ val dialPadVisible: Boolean = false,
+ val forceLoginFallback: Boolean = false,
) : MavericksState
diff --git a/vector/src/debug/res/layout/fragment_debug_private_settings.xml b/vector/src/debug/res/layout/fragment_debug_private_settings.xml
index b4186e7bba..6760c68169 100644
--- a/vector/src/debug/res/layout/fragment_debug_private_settings.xml
+++ b/vector/src/debug/res/layout/fragment_debug_private_settings.xml
@@ -25,6 +25,12 @@
android:layout_height="wrap_content"
android:text="Force DialPad tab display" />
+
+
diff --git a/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt b/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt
index e323506e9f..93a82f24f9 100755
--- a/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt
+++ b/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt
@@ -213,7 +213,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
try {
val session = activeSessionHolder.getSafeActiveSession() ?: return false
val room = session.getRoom(roomId) ?: return false
- return room.getTimeLineEvent(eventId) != null
+ return room.getTimelineEvent(eventId) != null
} catch (e: Exception) {
Timber.tag(loggerTag.value).e(e, "## isEventAlreadyKnown() : failed to check if the event was already defined")
}
diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
index 2cd7136ffc..33afcf1dfb 100644
--- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
+++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
@@ -58,6 +58,7 @@ import im.vector.app.features.login.LoginViewModel
import im.vector.app.features.login2.LoginViewModel2
import im.vector.app.features.login2.created.AccountCreatedViewModel
import im.vector.app.features.matrixto.MatrixToBottomSheetViewModel
+import im.vector.app.features.media.VectorAttachmentViewerViewModel
import im.vector.app.features.onboarding.OnboardingViewModel
import im.vector.app.features.poll.create.CreatePollViewModel
import im.vector.app.features.qrcode.QrCodeScannerViewModel
@@ -594,4 +595,9 @@ interface MavericksViewModelModule {
@IntoMap
@MavericksViewModelKey(LocationSharingViewModel::class)
fun createLocationSharingViewModelFactory(factory: LocationSharingViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
+
+ @Binds
+ @IntoMap
+ @MavericksViewModelKey(VectorAttachmentViewerViewModel::class)
+ fun vectorAttachmentViewerViewModelFactory(factory: VectorAttachmentViewerViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt
index 14c8e598f8..d10b363519 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt
@@ -92,7 +92,6 @@ sealed class RoomDetailAction : VectorViewModelAction {
data class UpdateJoinJitsiCallStatus(val conferenceEvent: ConferenceEvent) : RoomDetailAction()
- data class OpenOrCreateDm(val userId: String) : RoomDetailAction()
data class JumpToReadReceipt(val userId: String) : RoomDetailAction()
object QuickActionInvitePeople : RoomDetailAction()
object QuickActionSetAvatar : RoomDetailAction()
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailPendingAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailPendingAction.kt
index fccab500c5..b42f551ba0 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailPendingAction.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailPendingAction.kt
@@ -17,7 +17,6 @@
package im.vector.app.features.home.room.detail
sealed class RoomDetailPendingAction {
- data class OpenOrCreateDm(val userId: String) : RoomDetailPendingAction()
data class JumpToReadReceipt(val userId: String) : RoomDetailPendingAction()
data class MentionUser(val userId: String) : RoomDetailPendingAction()
data class OpenRoom(val roomId: String, val closeCurrentRoom: Boolean = false) : RoomDetailPendingAction()
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
index 1a40018526..501b6c9078 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
@@ -1247,8 +1247,6 @@ class TimelineFragment @Inject constructor(
timelineViewModel.handle(RoomDetailAction.JumpToReadReceipt(roomDetailPendingAction.userId))
is RoomDetailPendingAction.MentionUser ->
insertUserDisplayNameInTextEditor(roomDetailPendingAction.userId)
- is RoomDetailPendingAction.OpenOrCreateDm ->
- timelineViewModel.handle(RoomDetailAction.OpenOrCreateDm(roomDetailPendingAction.userId))
is RoomDetailPendingAction.OpenRoom ->
handleOpenRoom(RoomDetailViewEvents.OpenRoom(roomDetailPendingAction.roomId, roomDetailPendingAction.closeCurrentRoom))
}.exhaustive
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
index 14f5df9055..3bdcbc6529 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
@@ -417,7 +417,6 @@ class TimelineViewModel @AssistedInject constructor(
is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId)
is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action)
is RoomDetailAction.CancelSend -> handleCancel(action)
- is RoomDetailAction.OpenOrCreateDm -> handleOpenOrCreateDm(action)
is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(action)
RoomDetailAction.QuickActionInvitePeople -> handleInvitePeople()
RoomDetailAction.QuickActionSetAvatar -> handleQuickSetAvatar()
@@ -497,20 +496,6 @@ class TimelineViewModel @AssistedInject constructor(
_viewEvents.post(RoomDetailViewEvents.OpenSetRoomAvatarDialog)
}
- private fun handleOpenOrCreateDm(action: RoomDetailAction.OpenOrCreateDm) {
- viewModelScope.launch {
- val roomId = try {
- directRoomHelper.ensureDMExists(action.userId)
- } catch (failure: Throwable) {
- _viewEvents.post(RoomDetailViewEvents.ActionFailure(action, failure))
- return@launch
- }
- if (roomId != initialState.roomId) {
- _viewEvents.post(RoomDetailViewEvents.OpenRoom(roomId = roomId))
- }
- }
- }
-
private fun handleJumpToReadReceipt(action: RoomDetailAction.JumpToReadReceipt) {
room.getUserReadReceipt(action.userId)
?.let { handleNavigateToEvent(RoomDetailAction.NavigateToEvent(it, true)) }
@@ -742,7 +727,7 @@ class TimelineViewModel @AssistedInject constructor(
}
private fun handleRedactEvent(action: RoomDetailAction.RedactAction) {
- val event = room.getTimeLineEvent(action.targetEventId) ?: return
+ val event = room.getTimelineEvent(action.targetEventId) ?: return
room.redactEvent(event.root, action.reason)
}
@@ -782,7 +767,7 @@ class TimelineViewModel @AssistedInject constructor(
}
// We need to update this with the related m.replace also (to move read receipt)
action.event.annotations?.editSummary?.sourceEvents?.forEach {
- room.getTimeLineEvent(it)?.let { event ->
+ room.getTimelineEvent(it)?.let { event ->
visibleEventsSource.post(RoomDetailAction.TimelineEventTurnsVisible(event))
}
}
@@ -810,7 +795,7 @@ class TimelineViewModel @AssistedInject constructor(
notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(initialState.roomId) }
viewModelScope.launch {
try {
- session.leaveRoom(room.roomId)
+ session.leaveRoom(room.roomId)
} catch (throwable: Throwable) {
_viewEvents.post(RoomDetailViewEvents.Failure(throwable, showInDialog = true))
}
@@ -886,7 +871,7 @@ class TimelineViewModel @AssistedInject constructor(
private fun handleResendEvent(action: RoomDetailAction.ResendMessage) {
val targetEventId = action.eventId
- room.getTimeLineEvent(targetEventId)?.let {
+ room.getTimelineEvent(targetEventId)?.let {
// State must be UNDELIVERED or Failed
if (!it.root.sendState.hasFailed()) {
Timber.e("Cannot resend message, it is not failed, Cancel first")
@@ -904,7 +889,7 @@ class TimelineViewModel @AssistedInject constructor(
private fun handleRemove(action: RoomDetailAction.RemoveFailedEcho) {
val targetEventId = action.eventId
- room.getTimeLineEvent(targetEventId)?.let {
+ room.getTimelineEvent(targetEventId)?.let {
// State must be UNDELIVERED or Failed
if (!it.root.sendState.hasFailed()) {
Timber.e("Cannot resend message, it is not failed, Cancel first")
@@ -920,7 +905,7 @@ class TimelineViewModel @AssistedInject constructor(
return
}
val targetEventId = action.eventId
- room.getTimeLineEvent(targetEventId)?.let {
+ room.getTimelineEvent(targetEventId)?.let {
// State must be in one of the sending states
if (!it.root.sendState.isSending()) {
Timber.e("Cannot cancel message, it is not sending")
@@ -1046,14 +1031,14 @@ class TimelineViewModel @AssistedInject constructor(
private fun handleReRequestKeys(action: RoomDetailAction.ReRequestKeys) {
// Check if this request is still active and handled by me
- room.getTimeLineEvent(action.eventId)?.let {
+ room.getTimelineEvent(action.eventId)?.let {
session.cryptoService().reRequestRoomKeyForEvent(it.root)
_viewEvents.post(RoomDetailViewEvents.ShowMessage(stringProvider.getString(R.string.e2e_re_request_encryption_key_dialog_content)))
}
}
private fun handleTapOnFailedToDecrypt(action: RoomDetailAction.TapOnFailedToDecrypt) {
- room.getTimeLineEvent(action.eventId)?.let {
+ room.getTimelineEvent(action.eventId)?.let {
val code = when (it.root.mCryptoError) {
MXCryptoError.ErrorType.KEYS_WITHHELD -> {
WithHeldCode.fromCode(it.root.mCryptoErrorReason)
@@ -1069,7 +1054,7 @@ class TimelineViewModel @AssistedInject constructor(
// Do not allow to vote unsent local echo of the poll event
if (LocalEcho.isLocalEchoId(action.eventId)) return
// Do not allow to vote the same option twice
- room.getTimeLineEvent(action.eventId)?.let { pollTimelineEvent ->
+ room.getTimelineEvent(action.eventId)?.let { pollTimelineEvent ->
val currentVote = pollTimelineEvent.annotations?.pollResponseSummary?.aggregatedContent?.myVote
if (currentVote != action.optionKey) {
room.voteToPoll(action.eventId, action.optionKey)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
index 6adf248af9..325e9b9330 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
@@ -143,7 +143,7 @@ class MessageComposerViewModel @AssistedInject constructor(
}
private fun handleEnterEditMode(action: MessageComposerAction.EnterEditMode) {
- room.getTimeLineEvent(action.eventId)?.let { timelineEvent ->
+ room.getTimelineEvent(action.eventId)?.let { timelineEvent ->
setState { copy(sendMode = SendMode.Edit(timelineEvent, timelineEvent.getTextEditableContent())) }
}
}
@@ -175,13 +175,13 @@ class MessageComposerViewModel @AssistedInject constructor(
}
private fun handleEnterQuoteMode(action: MessageComposerAction.EnterQuoteMode) {
- room.getTimeLineEvent(action.eventId)?.let { timelineEvent ->
+ room.getTimelineEvent(action.eventId)?.let { timelineEvent ->
setState { copy(sendMode = SendMode.Quote(timelineEvent, action.text)) }
}
}
private fun handleEnterReplyMode(action: MessageComposerAction.EnterReplyMode) {
- room.getTimeLineEvent(action.eventId)?.let { timelineEvent ->
+ room.getTimelineEvent(action.eventId)?.let { timelineEvent ->
setState { copy(sendMode = SendMode.Reply(timelineEvent, action.text)) }
}
}
@@ -479,7 +479,7 @@ class MessageComposerViewModel @AssistedInject constructor(
if (inReplyTo != null) {
// TODO check if same content?
- room.getTimeLineEvent(inReplyTo)?.let {
+ room.getTimelineEvent(inReplyTo)?.let {
room.editReply(state.sendMode.timelineEvent, it, action.text.toString())
}
} else {
@@ -555,17 +555,17 @@ class MessageComposerViewModel @AssistedInject constructor(
sendMode = when (currentDraft) {
is UserDraft.Regular -> SendMode.Regular(currentDraft.content, false)
is UserDraft.Quote -> {
- room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent ->
+ room.getTimelineEvent(currentDraft.linkedEventId)?.let { timelineEvent ->
SendMode.Quote(timelineEvent, currentDraft.content)
}
}
is UserDraft.Reply -> {
- room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent ->
+ room.getTimelineEvent(currentDraft.linkedEventId)?.let { timelineEvent ->
SendMode.Reply(timelineEvent, currentDraft.content)
}
}
is UserDraft.Edit -> {
- room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent ->
+ room.getTimelineEvent(currentDraft.linkedEventId)?.let { timelineEvent ->
SendMode.Edit(timelineEvent, currentDraft.content)
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
index 745cb0c731..5575d9b7f6 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
@@ -343,24 +343,6 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
add(EventSharedAction.Edit(eventId, timelineEvent.root.getClearType()))
}
- if (canRedact(timelineEvent, actionPermissions)) {
- if (timelineEvent.root.getClearType() == EventType.POLL_START) {
- add(EventSharedAction.Redact(
- eventId,
- askForReason = informationData.senderId != session.myUserId,
- dialogTitleRes = R.string.delete_poll_dialog_title,
- dialogDescriptionRes = R.string.delete_poll_dialog_content
- ))
- } else {
- add(EventSharedAction.Redact(
- eventId,
- askForReason = informationData.senderId != session.myUserId,
- dialogTitleRes = R.string.delete_event_dialog_title,
- dialogDescriptionRes = R.string.delete_event_dialog_content
- ))
- }
- }
-
if (canCopy(msgType)) {
// TODO copy images? html? see ClipBoard
add(EventSharedAction.Copy(messageContent!!.body))
@@ -382,12 +364,30 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
add(EventSharedAction.ViewEditHistory(informationData))
}
+ if (canSave(msgType) && messageContent is MessageWithAttachmentContent) {
+ add(EventSharedAction.Save(timelineEvent.eventId, messageContent))
+ }
+
if (canShare(msgType)) {
add(EventSharedAction.Share(timelineEvent.eventId, messageContent!!))
}
- if (canSave(msgType) && messageContent is MessageWithAttachmentContent) {
- add(EventSharedAction.Save(timelineEvent.eventId, messageContent))
+ if (canRedact(timelineEvent, actionPermissions)) {
+ if (timelineEvent.root.getClearType() == EventType.POLL_START) {
+ add(EventSharedAction.Redact(
+ eventId,
+ askForReason = informationData.senderId != session.myUserId,
+ dialogTitleRes = R.string.delete_poll_dialog_title,
+ dialogDescriptionRes = R.string.delete_poll_dialog_content
+ ))
+ } else {
+ add(EventSharedAction.Redact(
+ eventId,
+ askForReason = informationData.senderId != session.myUserId,
+ dialogTitleRes = R.string.delete_event_dialog_title,
+ dialogDescriptionRes = R.string.delete_event_dialog_content
+ ))
+ }
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VerificationItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VerificationItemFactory.kt
index e972ddcab5..bdc6906593 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VerificationItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VerificationItemFactory.kt
@@ -65,7 +65,7 @@ class VerificationItemFactory @Inject constructor(
?: return ignoredConclusion(params)
// If we cannot find the referenced request we do not display the done event
- val refEvent = session.getRoom(event.root.roomId ?: "")?.getTimeLineEvent(refEventId)
+ val refEvent = session.getRoom(event.root.roomId ?: "")?.getTimelineEvent(refEventId)
?: return ignoredConclusion(params)
// If it's not a request ignore this event
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt
index 686d767850..25d6f907b5 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt
@@ -86,7 +86,7 @@ class ViewReactionsViewModel @AssistedInject constructor(@Assisted
annotationsSummary.reactionsSummary
.flatMap { reactionsSummary ->
reactionsSummary.sourceEvents.map {
- val event = room.getTimeLineEvent(it)
+ val event = room.getTimelineEvent(it)
?: throw RuntimeException("Your eventId is not valid")
ReactionInfo(
event.root.eventId!!,
diff --git a/vector/src/main/java/im/vector/app/features/media/AttachmentInteractionListener.kt b/vector/src/main/java/im/vector/app/features/media/AttachmentInteractionListener.kt
new file mode 100644
index 0000000000..b0cb913596
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/media/AttachmentInteractionListener.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.media
+
+interface AttachmentInteractionListener {
+ fun onDismiss()
+ fun onShare()
+ fun onDownload()
+ fun onPlayPause(play: Boolean)
+ fun videoSeekTo(percent: Int)
+}
diff --git a/vector/src/main/java/im/vector/app/features/media/AttachmentOverlayView.kt b/vector/src/main/java/im/vector/app/features/media/AttachmentOverlayView.kt
index f79fb03898..58d10d2f2d 100644
--- a/vector/src/main/java/im/vector/app/features/media/AttachmentOverlayView.kt
+++ b/vector/src/main/java/im/vector/app/features/media/AttachmentOverlayView.kt
@@ -30,35 +30,33 @@ class AttachmentOverlayView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr), AttachmentEventListener {
- var onShareCallback: (() -> Unit)? = null
- var onBack: (() -> Unit)? = null
- var onPlayPause: ((play: Boolean) -> Unit)? = null
- var videoSeekTo: ((progress: Int) -> Unit)? = null
-
+ var interactionListener: AttachmentInteractionListener? = null
val views: MergeImageAttachmentOverlayBinding
- var isPlaying = false
-
- var suspendSeekBarUpdate = false
+ private var isPlaying = false
+ private var suspendSeekBarUpdate = false
init {
inflate(context, R.layout.merge_image_attachment_overlay, this)
views = MergeImageAttachmentOverlayBinding.bind(this)
setBackgroundColor(Color.TRANSPARENT)
views.overlayBackButton.setOnClickListener {
- onBack?.invoke()
+ interactionListener?.onDismiss()
}
views.overlayShareButton.setOnClickListener {
- onShareCallback?.invoke()
+ interactionListener?.onShare()
+ }
+ views.overlayDownloadButton.setOnClickListener {
+ interactionListener?.onDownload()
}
views.overlayPlayPauseButton.setOnClickListener {
- onPlayPause?.invoke(!isPlaying)
+ interactionListener?.onPlayPause(!isPlaying)
}
views.overlaySeekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
if (fromUser) {
- videoSeekTo?.invoke(progress)
+ interactionListener?.videoSeekTo(progress)
}
}
diff --git a/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt b/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt
index ca469bfbcb..4039ea112b 100644
--- a/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt
+++ b/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt
@@ -49,14 +49,7 @@ abstract class BaseAttachmentProvider(
private val stringProvider: StringProvider
) : AttachmentSourceProvider {
- interface InteractionListener {
- fun onDismissTapped()
- fun onShareTapped()
- fun onPlayPause(play: Boolean)
- fun videoSeekTo(percent: Int)
- }
-
- var interactionListener: InteractionListener? = null
+ var interactionListener: AttachmentInteractionListener? = null
private var overlayView: AttachmentOverlayView? = null
@@ -68,18 +61,7 @@ abstract class BaseAttachmentProvider(
if (position == -1) return null
if (overlayView == null) {
overlayView = AttachmentOverlayView(context)
- overlayView?.onBack = {
- interactionListener?.onDismissTapped()
- }
- overlayView?.onShareCallback = {
- interactionListener?.onShareTapped()
- }
- overlayView?.onPlayPause = { play ->
- interactionListener?.onPlayPause(play)
- }
- overlayView?.videoSeekTo = { percent ->
- interactionListener?.videoSeekTo(percent)
- }
+ overlayView?.interactionListener = interactionListener
}
val timelineEvent = getTimelineEventAtPosition(position)
diff --git a/vector/src/main/java/im/vector/app/features/media/DataAttachmentRoomProvider.kt b/vector/src/main/java/im/vector/app/features/media/DataAttachmentRoomProvider.kt
index 31162f309f..b9d98429a7 100644
--- a/vector/src/main/java/im/vector/app/features/media/DataAttachmentRoomProvider.kt
+++ b/vector/src/main/java/im/vector/app/features/media/DataAttachmentRoomProvider.kt
@@ -81,7 +81,7 @@ class DataAttachmentRoomProvider(
override fun getTimelineEventAtPosition(position: Int): TimelineEvent? {
val item = getItem(position)
- return room?.getTimeLineEvent(item.eventId)
+ return room?.getTimelineEvent(item.eventId)
}
override suspend fun getFileForSharing(position: Int): File? {
diff --git a/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerAction.kt b/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerAction.kt
new file mode 100644
index 0000000000..5af3cd193a
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerAction.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.media
+
+import im.vector.app.core.platform.VectorViewModelAction
+import java.io.File
+
+sealed class VectorAttachmentViewerAction : VectorViewModelAction {
+ data class DownloadMedia(val file: File) : VectorAttachmentViewerAction()
+}
diff --git a/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt b/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt
index 103511bad5..d8c2b83f9b 100644
--- a/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt
@@ -17,6 +17,7 @@ package im.vector.app.features.media
import android.content.Context
import android.content.Intent
+import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import android.view.View
@@ -30,16 +31,25 @@ import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.transition.Transition
+import com.airbnb.mvrx.viewModel
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
+import im.vector.app.core.extensions.singletonEntryPoint
import im.vector.app.core.intent.getMimeTypeFromUri
+import im.vector.app.core.platform.showOptimizedSnackbar
+import im.vector.app.core.utils.PERMISSIONS_FOR_WRITING_FILES
+import im.vector.app.core.utils.checkPermissions
+import im.vector.app.core.utils.onPermissionDeniedDialog
+import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.core.utils.shareMedia
import im.vector.app.features.themes.ActivityOtherThemes
import im.vector.app.features.themes.ThemeUtils
import im.vector.lib.attachmentviewer.AttachmentCommands
import im.vector.lib.attachmentviewer.AttachmentViewerActivity
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
@@ -47,7 +57,7 @@ import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
-class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmentProvider.InteractionListener {
+class VectorAttachmentViewerActivity : AttachmentViewerActivity(), AttachmentInteractionListener {
@Parcelize
data class Args(
@@ -58,15 +68,28 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen
@Inject
lateinit var sessionHolder: ActiveSessionHolder
+
@Inject
lateinit var dataSourceFactory: AttachmentProviderFactory
+
@Inject
lateinit var imageContentRenderer: ImageContentRenderer
+ private val viewModel: VectorAttachmentViewerViewModel by viewModel()
+ private val errorFormatter by lazy(LazyThreadSafetyMode.NONE) { singletonEntryPoint().errorFormatter() }
private var initialIndex = 0
private var isAnimatingOut = false
-
private var currentSourceProvider: BaseAttachmentProvider<*>? = null
+ private val downloadActionResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently ->
+ if (allGranted) {
+ viewModel.pendingAction?.let {
+ viewModel.handle(it)
+ }
+ } else if (deniedPermanently) {
+ onPermissionDeniedDialog(R.string.denied_permission_generic)
+ }
+ viewModel.pendingAction = null
+ }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -128,6 +151,8 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen
window.statusBarColor = ContextCompat.getColor(this, R.color.black_alpha)
window.navigationBarColor = ContextCompat.getColor(this, R.color.black_alpha)
+
+ observeViewEvents()
}
override fun onResume() {
@@ -140,12 +165,6 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen
Timber.i("onPause Activity ${javaClass.simpleName}")
}
- private fun getOtherThemes() = ActivityOtherThemes.VectorAttachmentsPreview
-
- override fun shouldAnimateDismiss(): Boolean {
- return currentPosition != initialIndex
- }
-
override fun onBackPressed() {
if (currentPosition == initialIndex) {
// show back the transition view
@@ -156,6 +175,10 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen
super.onBackPressed()
}
+ override fun shouldAnimateDismiss(): Boolean {
+ return currentPosition != initialIndex
+ }
+
override fun animateClose() {
if (currentPosition == initialIndex) {
// show back the transition view
@@ -166,9 +189,7 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen
ActivityCompat.finishAfterTransition(this)
}
- // ==========================================================================================
- // PRIVATE METHODS
- // ==========================================================================================
+ private fun getOtherThemes() = ActivityOtherThemes.VectorAttachmentsPreview
/**
* Try and add a [Transition.TransitionListener] to the entering shared element
@@ -218,10 +239,72 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen
})
}
+ private fun observeViewEvents() {
+ viewModel.viewEvents
+ .stream()
+ .onEach(::handleViewEvents)
+ .launchIn(lifecycleScope)
+ }
+
+ private fun handleViewEvents(event: VectorAttachmentViewerViewEvents) {
+ when (event) {
+ is VectorAttachmentViewerViewEvents.ErrorDownloadingMedia -> showSnackBarError(event.error)
+ }
+ }
+
+ private fun showSnackBarError(error: Throwable) {
+ rootView.showOptimizedSnackbar(errorFormatter.toHumanReadable(error))
+ }
+
+ private fun hasWritePermission() =
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ||
+ checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, downloadActionResultLauncher)
+
+ override fun onDismiss() {
+ animateClose()
+ }
+
+ override fun onPlayPause(play: Boolean) {
+ handle(if (play) AttachmentCommands.StartVideo else AttachmentCommands.PauseVideo)
+ }
+
+ override fun videoSeekTo(percent: Int) {
+ handle(AttachmentCommands.SeekTo(percent))
+ }
+
+ override fun onShare() {
+ lifecycleScope.launch(Dispatchers.IO) {
+ val file = currentSourceProvider?.getFileForSharing(currentPosition) ?: return@launch
+
+ withContext(Dispatchers.Main) {
+ shareMedia(
+ this@VectorAttachmentViewerActivity,
+ file,
+ getMimeTypeFromUri(this@VectorAttachmentViewerActivity, file.toUri())
+ )
+ }
+ }
+ }
+
+ override fun onDownload() {
+ lifecycleScope.launch(Dispatchers.IO) {
+ val hasWritePermission = withContext(Dispatchers.Main) {
+ hasWritePermission()
+ }
+
+ val file = currentSourceProvider?.getFileForSharing(currentPosition) ?: return@launch
+ if (hasWritePermission) {
+ viewModel.handle(VectorAttachmentViewerAction.DownloadMedia(file))
+ } else {
+ viewModel.pendingAction = VectorAttachmentViewerAction.DownloadMedia(file)
+ }
+ }
+ }
+
companion object {
- const val EXTRA_ARGS = "EXTRA_ARGS"
- const val EXTRA_IMAGE_DATA = "EXTRA_IMAGE_DATA"
- const val EXTRA_IN_MEMORY_DATA = "EXTRA_IN_MEMORY_DATA"
+ private const val EXTRA_ARGS = "EXTRA_ARGS"
+ private const val EXTRA_IMAGE_DATA = "EXTRA_IMAGE_DATA"
+ private const val EXTRA_IN_MEMORY_DATA = "EXTRA_IN_MEMORY_DATA"
fun newIntent(context: Context,
mediaData: AttachmentData,
@@ -236,30 +319,4 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen
}
}
}
-
- override fun onDismissTapped() {
- animateClose()
- }
-
- override fun onPlayPause(play: Boolean) {
- handle(if (play) AttachmentCommands.StartVideo else AttachmentCommands.PauseVideo)
- }
-
- override fun videoSeekTo(percent: Int) {
- handle(AttachmentCommands.SeekTo(percent))
- }
-
- override fun onShareTapped() {
- lifecycleScope.launch(Dispatchers.IO) {
- val file = currentSourceProvider?.getFileForSharing(currentPosition) ?: return@launch
-
- withContext(Dispatchers.Main) {
- shareMedia(
- this@VectorAttachmentViewerActivity,
- file,
- getMimeTypeFromUri(this@VectorAttachmentViewerActivity, file.toUri())
- )
- }
- }
- }
}
diff --git a/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerViewEvents.kt b/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerViewEvents.kt
new file mode 100644
index 0000000000..e46ee02155
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerViewEvents.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.media
+
+import im.vector.app.core.platform.VectorViewEvents
+
+sealed class VectorAttachmentViewerViewEvents : VectorViewEvents {
+ data class ErrorDownloadingMedia(val error: Throwable) : VectorAttachmentViewerViewEvents()
+}
diff --git a/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerViewModel.kt b/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerViewModel.kt
new file mode 100644
index 0000000000..807c69caff
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerViewModel.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.media
+
+import com.airbnb.mvrx.MavericksViewModelFactory
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import im.vector.app.core.di.MavericksAssistedViewModelFactory
+import im.vector.app.core.di.hiltMavericksViewModelFactory
+import im.vector.app.core.platform.VectorDummyViewState
+import im.vector.app.core.platform.VectorViewModel
+import im.vector.app.features.media.domain.usecase.DownloadMediaUseCase
+import im.vector.app.features.session.coroutineScope
+import kotlinx.coroutines.launch
+import org.matrix.android.sdk.api.session.Session
+
+class VectorAttachmentViewerViewModel @AssistedInject constructor(
+ @Assisted initialState: VectorDummyViewState,
+ private val session: Session,
+ private val downloadMediaUseCase: DownloadMediaUseCase
+) : VectorViewModel(initialState) {
+
+ @AssistedFactory
+ interface Factory : MavericksAssistedViewModelFactory {
+ override fun create(initialState: VectorDummyViewState): VectorAttachmentViewerViewModel
+ }
+
+ companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory()
+
+ var pendingAction: VectorAttachmentViewerAction? = null
+
+ override fun handle(action: VectorAttachmentViewerAction) {
+ when (action) {
+ is VectorAttachmentViewerAction.DownloadMedia -> handleDownloadAction(action)
+ }
+ }
+
+ private fun handleDownloadAction(action: VectorAttachmentViewerAction.DownloadMedia) {
+ // launch in the coroutine scope session to avoid binding the coroutine to the lifecycle of the VM
+ session.coroutineScope.launch {
+ // Success event is handled via a notification inside the use case
+ downloadMediaUseCase.execute(action.file)
+ .onFailure { _viewEvents.post(VectorAttachmentViewerViewEvents.ErrorDownloadingMedia(it)) }
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/media/domain/usecase/DownloadMediaUseCase.kt b/vector/src/main/java/im/vector/app/features/media/domain/usecase/DownloadMediaUseCase.kt
new file mode 100644
index 0000000000..b0401ccd30
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/media/domain/usecase/DownloadMediaUseCase.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.media.domain.usecase
+
+import android.content.Context
+import androidx.core.net.toUri
+import dagger.hilt.android.qualifiers.ApplicationContext
+import im.vector.app.core.intent.getMimeTypeFromUri
+import im.vector.app.core.utils.saveMedia
+import im.vector.app.features.notifications.NotificationUtils
+import kotlinx.coroutines.withContext
+import org.matrix.android.sdk.api.session.Session
+import java.io.File
+import javax.inject.Inject
+
+class DownloadMediaUseCase @Inject constructor(
+ @ApplicationContext private val appContext: Context,
+ private val session: Session,
+ private val notificationUtils: NotificationUtils
+) {
+
+ suspend fun execute(input: File): Result = withContext(session.coroutineDispatchers.io) {
+ runCatching {
+ saveMedia(
+ context = appContext,
+ file = input,
+ title = input.name,
+ mediaMimeType = getMimeTypeFromUri(appContext, input.toUri()),
+ notificationUtils = notificationUtils
+ )
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt
index f73e2ab0c3..ec034173fc 100644
--- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt
+++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt
@@ -65,7 +65,7 @@ class NotifiableEventResolver @Inject constructor(
if (event.getClearType() == EventType.STATE_ROOM_MEMBER) {
return resolveStateRoomEvent(event, session, canBeReplaced = false, isNoisy = isNoisy)
}
- val timelineEvent = session.getRoom(roomID)?.getTimeLineEvent(eventId) ?: return null
+ val timelineEvent = session.getRoom(roomID)?.getTimelineEvent(eventId) ?: return null
return when (event.getClearType()) {
EventType.MESSAGE,
EventType.ENCRYPTED -> {
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
index ca3c3644bd..63f1875235 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
@@ -46,6 +46,7 @@ import im.vector.app.features.login.LoginMode
import im.vector.app.features.login.ReAuthHelper
import im.vector.app.features.login.ServerType
import im.vector.app.features.login.SignMode
+import im.vector.app.features.settings.VectorDataStore
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixPatterns.getDomain
@@ -78,7 +79,8 @@ class OnboardingViewModel @AssistedInject constructor(
private val stringProvider: StringProvider,
private val homeServerHistoryService: HomeServerHistoryService,
private val vectorFeatures: VectorFeatures,
- private val analyticsTracker: AnalyticsTracker
+ private val analyticsTracker: AnalyticsTracker,
+ private val vectorDataStore: VectorDataStore,
) : VectorViewModel(initialState) {
@AssistedFactory
@@ -90,6 +92,7 @@ class OnboardingViewModel @AssistedInject constructor(
init {
getKnownCustomHomeServersUrls()
+ observeDataStore()
}
private fun getKnownCustomHomeServersUrls() {
@@ -98,6 +101,12 @@ class OnboardingViewModel @AssistedInject constructor(
}
}
+ private fun observeDataStore() = viewModelScope.launch {
+ vectorDataStore.forceLoginFallbackFlow.setOnEach { isForceLoginFallbackEnabled ->
+ copy(isForceLoginFallbackEnabled = isForceLoginFallbackEnabled)
+ }
+ }
+
// Store the last action, to redo it after user has trusted the untrusted certificate
private var lastAction: OnboardingAction? = null
private var currentHomeServerConnectionConfig: HomeServerConnectionConfig? = null
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt
index 7bad2682a9..39c5094d30 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt
@@ -62,7 +62,8 @@ data class OnboardingViewState(
// Supported types for the login. We cannot use a sealed class for LoginType because it is not serializable
@PersistState
val loginModeSupportedTypes: List = emptyList(),
- val knownCustomHomeServersUrls: List = emptyList()
+ val knownCustomHomeServersUrls: List = emptyList(),
+ val isForceLoginFallbackEnabled: Boolean = false,
) : MavericksState {
fun isLoading(): Boolean {
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt
index 1e792df427..0093cb20ea 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt
@@ -75,6 +75,8 @@ class FtueAuthVariant(
private val popEnterAnim = R.anim.no_anim
private val popExitAnim = R.anim.exit_fade_out
+ private var isForceLoginFallbackEnabled = false
+
private val topFragment: Fragment?
get() = supportFragmentManager.findFragmentById(views.loginFragmentContainer.id)
@@ -109,10 +111,6 @@ class FtueAuthVariant(
}
}
- override fun setIsLoading(isLoading: Boolean) {
- // do nothing
- }
-
private fun addFirstFragment() {
val splashFragment = when (vectorFeatures.isOnboardingSplashCarouselEnabled()) {
true -> FtueAuthSplashCarouselFragment::class.java
@@ -121,11 +119,25 @@ class FtueAuthVariant(
activity.addFragment(views.loginFragmentContainer, splashFragment)
}
+ private fun updateWithState(viewState: OnboardingViewState) {
+ isForceLoginFallbackEnabled = viewState.isForceLoginFallbackEnabled
+ views.loginLoading.isVisible = shouldShowLoading(viewState)
+ }
+
+ private fun shouldShowLoading(viewState: OnboardingViewState) =
+ if (vectorFeatures.isOnboardingPersonalizeEnabled()) {
+ viewState.isLoading()
+ } else {
+ // Keep loading when during success because of the delay when switching to the next Activity
+ viewState.isLoading() || viewState.isAuthTaskCompleted()
+ }
+
+ override fun setIsLoading(isLoading: Boolean) = Unit
+
private fun handleOnboardingViewEvents(viewEvents: OnboardingViewEvents) {
when (viewEvents) {
is OnboardingViewEvents.RegistrationFlowResult -> {
- // Check that all flows are supported by the application
- if (viewEvents.flowResult.missingStages.any { !it.isSupported() }) {
+ if (registrationShouldFallback(viewEvents)) {
// Display a popup to propose use web fallback
onRegistrationStageNotSupported()
} else {
@@ -136,11 +148,7 @@ class FtueAuthVariant(
// First ask for login and password
// I add a tag to indicate that this fragment is a registration stage.
// This way it will be automatically popped in when starting the next registration stage
- activity.addFragmentToBackstack(views.loginFragmentContainer,
- FtueAuthLoginFragment::class.java,
- tag = FRAGMENT_REGISTRATION_STAGE_TAG,
- option = commonOption
- )
+ openAuthLoginFragmentWithTag(FRAGMENT_REGISTRATION_STAGE_TAG)
}
}
}
@@ -228,13 +236,23 @@ class FtueAuthVariant(
}.exhaustive
}
- private fun updateWithState(viewState: OnboardingViewState) {
- views.loginLoading.isVisible = if (vectorFeatures.isOnboardingPersonalizeEnabled()) {
- viewState.isLoading()
- } else {
- // Keep loading when during success because of the delay when switching to the next Activity
- viewState.isLoading() || viewState.isAuthTaskCompleted()
- }
+ private fun registrationShouldFallback(registrationFlowResult: OnboardingViewEvents.RegistrationFlowResult) =
+ isForceLoginFallbackEnabled || registrationFlowResult.containsUnsupportedRegistrationFlow()
+
+ private fun OnboardingViewEvents.RegistrationFlowResult.containsUnsupportedRegistrationFlow() =
+ flowResult.missingStages.any { !it.isSupported() }
+
+ private fun onRegistrationStageNotSupported() {
+ MaterialAlertDialogBuilder(activity)
+ .setTitle(R.string.app_name)
+ .setMessage(activity.getString(R.string.login_registration_not_supported))
+ .setPositiveButton(R.string.yes) { _, _ ->
+ activity.addFragmentToBackstack(views.loginFragmentContainer,
+ FtueAuthWebFragment::class.java,
+ option = commonOption)
+ }
+ .setNegativeButton(R.string.no, null)
+ .show()
}
private fun onWebLoginError(onWebLoginError: OnboardingViewEvents.OnWebLoginError) {
@@ -264,29 +282,58 @@ class FtueAuthVariant(
// state.signMode could not be ready yet. So use value from the ViewEvent
when (OnboardingViewEvents.signMode) {
SignMode.Unknown -> error("Sign mode has to be set before calling this method")
- SignMode.SignUp -> {
- // This is managed by the OnboardingViewEvents
- }
- SignMode.SignIn -> {
- // It depends on the LoginMode
- when (state.loginMode) {
- LoginMode.Unknown,
- is LoginMode.Sso -> error("Developer error")
- is LoginMode.SsoAndPassword,
- LoginMode.Password -> activity.addFragmentToBackstack(views.loginFragmentContainer,
- FtueAuthLoginFragment::class.java,
- tag = FRAGMENT_LOGIN_TAG,
- option = commonOption)
- LoginMode.Unsupported -> onLoginModeNotSupported(state.loginModeSupportedTypes)
- }.exhaustive
- }
- SignMode.SignInWithMatrixId -> activity.addFragmentToBackstack(views.loginFragmentContainer,
- FtueAuthLoginFragment::class.java,
- tag = FRAGMENT_LOGIN_TAG,
- option = commonOption)
+ SignMode.SignUp -> Unit // This case is processed in handleOnboardingViewEvents
+ SignMode.SignIn -> handleSignInSelected(state)
+ SignMode.SignInWithMatrixId -> handleSignInWithMatrixId(state)
}.exhaustive
}
+ private fun handleSignInSelected(state: OnboardingViewState) {
+ if (isForceLoginFallbackEnabled) {
+ onLoginModeNotSupported(state.loginModeSupportedTypes)
+ } else {
+ disambiguateLoginMode(state)
+ }
+ }
+
+ private fun disambiguateLoginMode(state: OnboardingViewState) = when (state.loginMode) {
+ LoginMode.Unknown,
+ is LoginMode.Sso -> error("Developer error")
+ is LoginMode.SsoAndPassword,
+ LoginMode.Password -> openAuthLoginFragmentWithTag(FRAGMENT_LOGIN_TAG)
+ LoginMode.Unsupported -> onLoginModeNotSupported(state.loginModeSupportedTypes)
+ }
+
+ private fun openAuthLoginFragmentWithTag(tag: String) {
+ activity.addFragmentToBackstack(views.loginFragmentContainer,
+ FtueAuthLoginFragment::class.java,
+ tag = tag,
+ option = commonOption)
+ }
+
+ private fun onLoginModeNotSupported(supportedTypes: List) {
+ MaterialAlertDialogBuilder(activity)
+ .setTitle(R.string.app_name)
+ .setMessage(activity.getString(R.string.login_mode_not_supported, supportedTypes.joinToString { "'$it'" }))
+ .setPositiveButton(R.string.yes) { _, _ -> openAuthWebFragment() }
+ .setNegativeButton(R.string.no, null)
+ .show()
+ }
+
+ private fun handleSignInWithMatrixId(state: OnboardingViewState) {
+ if (isForceLoginFallbackEnabled) {
+ onLoginModeNotSupported(state.loginModeSupportedTypes)
+ } else {
+ openAuthLoginFragmentWithTag(FRAGMENT_LOGIN_TAG)
+ }
+ }
+
+ private fun openAuthWebFragment() {
+ activity.addFragmentToBackstack(views.loginFragmentContainer,
+ FtueAuthWebFragment::class.java,
+ option = commonOption)
+ }
+
/**
* Handle the SSO redirection here
*/
@@ -296,32 +343,6 @@ class FtueAuthVariant(
?.let { onboardingViewModel.handle(OnboardingAction.LoginWithToken(it)) }
}
- private fun onRegistrationStageNotSupported() {
- MaterialAlertDialogBuilder(activity)
- .setTitle(R.string.app_name)
- .setMessage(activity.getString(R.string.login_registration_not_supported))
- .setPositiveButton(R.string.yes) { _, _ ->
- activity.addFragmentToBackstack(views.loginFragmentContainer,
- FtueAuthWebFragment::class.java,
- option = commonOption)
- }
- .setNegativeButton(R.string.no, null)
- .show()
- }
-
- private fun onLoginModeNotSupported(supportedTypes: List) {
- MaterialAlertDialogBuilder(activity)
- .setTitle(R.string.app_name)
- .setMessage(activity.getString(R.string.login_mode_not_supported, supportedTypes.joinToString { "'$it'" }))
- .setPositiveButton(R.string.yes) { _, _ ->
- activity.addFragmentToBackstack(views.loginFragmentContainer,
- FtueAuthWebFragment::class.java,
- option = commonOption)
- }
- .setNegativeButton(R.string.no, null)
- .show()
- }
-
private fun handleRegistrationNavigation(flowResult: FlowResult) {
// Complete all mandatory stages first
val mandatoryStage = flowResult.missingStages.firstOrNull { it.mandatory }
diff --git a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt
index b67e779a33..fa7b5aa7bc 100644
--- a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt
+++ b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt
@@ -89,7 +89,7 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti
val rootThreadEventId = permalinkData.eventId?.let { eventId ->
val room = roomId?.let { session?.getRoom(it) }
- room?.getTimeLineEvent(eventId)?.root?.getRootThreadEventId()
+ room?.getTimelineEvent(eventId)?.root?.getRootThreadEventId()
}
openRoom(
navigationInterceptor,
diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt
index 7750e6d909..5c7ef72297 100644
--- a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt
@@ -68,7 +68,7 @@ class CreatePollViewModel @AssistedInject constructor(
}
private fun initializeEditedPoll(eventId: String) {
- val event = room.getTimeLineEvent(eventId) ?: return
+ val event = room.getTimelineEvent(eventId) ?: return
val content = event.getLastMessageContent() as? MessagePollContent ?: return
val pollType = content.pollCreationInfo?.kind ?: PollType.DISCLOSED
@@ -115,7 +115,7 @@ class CreatePollViewModel @AssistedInject constructor(
}
private fun sendEditedPoll(editedEventId: String, pollType: PollType, question: String, options: List) {
- val editedEvent = room.getTimeLineEvent(editedEventId) ?: return
+ val editedEvent = room.getTimelineEvent(editedEventId) ?: return
room.editPoll(editedEvent, pollType, question, options)
}
diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileAction.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileAction.kt
index 87801a7e95..e2298d9b53 100644
--- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileAction.kt
+++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileAction.kt
@@ -29,4 +29,5 @@ sealed class RoomMemberProfileAction : VectorViewModelAction {
object ShareRoomMemberProfile : RoomMemberProfileAction()
data class SetPowerLevel(val previousValue: Int, val newValue: Int, val askForValidation: Boolean) : RoomMemberProfileAction()
data class SetUserColorOverride(val newColorSpec: String) : RoomMemberProfileAction()
+ data class OpenOrCreateDm(val userId: String) : RoomMemberProfileAction()
}
diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt
index fcebe9adbb..7e919fb663 100644
--- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt
@@ -127,6 +127,7 @@ class RoomMemberProfileFragment @Inject constructor(
is RoomMemberProfileViewEvents.ShareRoomMemberProfile -> handleShareRoomMemberProfile(it.permalink)
is RoomMemberProfileViewEvents.ShowPowerLevelValidation -> handleShowPowerLevelAdminWarning(it)
is RoomMemberProfileViewEvents.ShowPowerLevelDemoteWarning -> handleShowPowerLevelDemoteWarning(it)
+ is RoomMemberProfileViewEvents.OpenRoom -> handleOpenRoom(it)
is RoomMemberProfileViewEvents.OnKickActionSuccess -> Unit
is RoomMemberProfileViewEvents.OnSetPowerLevelSuccess -> Unit
is RoomMemberProfileViewEvents.OnBanActionSuccess -> Unit
@@ -142,6 +143,10 @@ class RoomMemberProfileFragment @Inject constructor(
headerViews.memberProfileIdView.copyOnLongClick()
}
+ private fun handleOpenRoom(event: RoomMemberProfileViewEvents.OpenRoom) {
+ navigator.openRoom(requireContext(), event.roomId, null)
+ }
+
private fun handleShowPowerLevelDemoteWarning(event: RoomMemberProfileViewEvents.ShowPowerLevelDemoteWarning) {
EditPowerLevelDialogs.showDemoteWarning(requireActivity()) {
viewModel.handle(RoomMemberProfileAction.SetPowerLevel(event.currentValue, event.newValue, false))
@@ -297,8 +302,7 @@ class RoomMemberProfileFragment @Inject constructor(
}
override fun onOpenDmClicked() {
- roomDetailPendingActionStore.data = RoomDetailPendingAction.OpenOrCreateDm(fragmentArgs.userId)
- vectorBaseActivity.finish()
+ viewModel.handle(RoomMemberProfileAction.OpenOrCreateDm(fragmentArgs.userId))
}
override fun onJumpToReadReceiptClicked() {
diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewEvents.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewEvents.kt
index 9981d72e07..efe23eeff0 100644
--- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewEvents.kt
+++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewEvents.kt
@@ -39,4 +39,5 @@ sealed class RoomMemberProfileViewEvents : VectorViewEvents {
) : RoomMemberProfileViewEvents()
data class ShareRoomMemberProfile(val permalink: String) : RoomMemberProfileViewEvents()
+ data class OpenRoom(val roomId: String) : RoomMemberProfileViewEvents()
}
diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt
index c219c85185..a79a9f4c1d 100644
--- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt
@@ -32,6 +32,7 @@ import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.mvrx.runCatchingToAsync
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
+import im.vector.app.features.createdirect.DirectRoomHelper
import im.vector.app.features.displayname.getBestName
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
@@ -66,6 +67,7 @@ class RoomMemberProfileViewModel @AssistedInject constructor(
@Assisted private val initialState: RoomMemberProfileViewState,
private val stringProvider: StringProvider,
private val matrixItemColorProvider: MatrixItemColorProvider,
+ private val directRoomHelper: DirectRoomHelper,
private val session: Session
) : VectorViewModel(initialState) {
@@ -167,9 +169,25 @@ class RoomMemberProfileViewModel @AssistedInject constructor(
is RoomMemberProfileAction.KickUser -> handleKickAction(action)
RoomMemberProfileAction.InviteUser -> handleInviteAction()
is RoomMemberProfileAction.SetUserColorOverride -> handleSetUserColorOverride(action)
+ is RoomMemberProfileAction.OpenOrCreateDm -> handleOpenOrCreateDm(action)
}.exhaustive
}
+ private fun handleOpenOrCreateDm(action: RoomMemberProfileAction.OpenOrCreateDm) {
+ viewModelScope.launch {
+ _viewEvents.post(RoomMemberProfileViewEvents.Loading())
+ val roomId = try {
+ directRoomHelper.ensureDMExists(action.userId)
+ } catch (failure: Throwable) {
+ _viewEvents.post(RoomMemberProfileViewEvents.Failure(failure))
+ return@launch
+ }
+ if (roomId != initialState.roomId) {
+ _viewEvents.post(RoomMemberProfileViewEvents.OpenRoom(roomId = roomId))
+ }
+ }
+ }
+
private fun handleSetUserColorOverride(action: RoomMemberProfileAction.SetUserColorOverride) {
val newOverrideColorSpecs = session.accountDataService()
.getUserAccountDataEvent(UserAccountDataTypes.TYPE_OVERRIDE_COLORS)
diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorDataStore.kt b/vector/src/main/java/im/vector/app/features/settings/VectorDataStore.kt
index 6a5ef0ac99..a7981a8b2a 100644
--- a/vector/src/main/java/im/vector/app/features/settings/VectorDataStore.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/VectorDataStore.kt
@@ -59,4 +59,16 @@ class VectorDataStore @Inject constructor(
settings[forceDialPadDisplay] = force
}
}
+
+ private val forceLoginFallback = booleanPreferencesKey("force_login_fallback")
+
+ val forceLoginFallbackFlow: Flow = context.dataStore.data.map { preferences ->
+ preferences[forceLoginFallback].orFalse()
+ }
+
+ suspend fun setForceLoginFallbackFlow(force: Boolean) {
+ context.dataStore.edit { settings ->
+ settings[forceLoginFallback] = force
+ }
+ }
}
diff --git a/vector/src/main/res/layout/merge_image_attachment_overlay.xml b/vector/src/main/res/layout/merge_image_attachment_overlay.xml
index d8e2142f87..1a5c6d8bf4 100644
--- a/vector/src/main/res/layout/merge_image_attachment_overlay.xml
+++ b/vector/src/main/res/layout/merge_image_attachment_overlay.xml
@@ -67,6 +67,23 @@
app:layout_constraintTop_toBottomOf="@id/overlayCounterText"
tools:text="Bill 29 Jun at 19:42" />
+
+
()
+ val mimeType = "mimeType"
+ val name = "filename"
+ every { getMimeTypeFromUri(appContext, uri) } returns mimeType
+ file.givenName(name)
+ file.givenUri(uri)
+ coEvery { saveMedia(any(), any(), any(), any(), any()) } just runs
+
+ // When
+ val result = downloadMediaUseCase.execute(file.instance)
+
+ // Then
+ assert(result.isSuccess)
+ verifyAll {
+ file.instance.name
+ file.instance.toUri()
+ }
+ verify {
+ getMimeTypeFromUri(appContext, uri)
+ }
+ coVerify {
+ saveMedia(appContext, file.instance, name, mimeType, notificationUtils)
+ }
+ }
+
+ @Test
+ fun `given a file when calling execute then save the file in local with error`() = runBlockingTest {
+ // Given
+ val uri = mockk()
+ val mimeType = "mimeType"
+ val name = "filename"
+ val error = Throwable()
+ file.givenName(name)
+ file.givenUri(uri)
+ every { getMimeTypeFromUri(appContext, uri) } returns mimeType
+ coEvery { saveMedia(any(), any(), any(), any(), any()) } throws error
+
+ // When
+ val result = downloadMediaUseCase.execute(file.instance)
+
+ // Then
+ assert(result.isFailure && result.exceptionOrNull() == error)
+ verifyAll {
+ file.instance.name
+ file.instance.toUri()
+ }
+ verify {
+ getMimeTypeFromUri(appContext, uri)
+ }
+ coVerify {
+ saveMedia(appContext, file.instance, name, mimeType, notificationUtils)
+ }
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeFile.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeFile.kt
new file mode 100644
index 0000000000..652d3f93fd
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeFile.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.test.fakes
+
+import android.net.Uri
+import androidx.core.net.toUri
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.unmockkStatic
+import java.io.File
+
+class FakeFile {
+
+ val instance = mockk()
+
+ init {
+ mockkStatic(Uri::class)
+ }
+
+ /**
+ * To be called after tests.
+ */
+ fun tearDown() {
+ unmockkStatic(Uri::class)
+ }
+
+ fun givenName(name: String) {
+ every { instance.name } returns name
+ }
+
+ fun givenUri(uri: Uri) {
+ every { instance.toUri() } returns uri
+ }
+}