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 [Get it on Google Play](https://play.google.com/store/apps/details?id=im.vector.app) [Get it on F-Droid](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 + } +}