mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-16 02:05:06 +08:00
Merge branch 'develop' into sync-analytics-plan
This commit is contained in:
commit
e65070793d
5
.github/workflows/build.yml
vendored
5
.github/workflows/build.yml
vendored
@ -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
|
||||
|
@ -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
|
||||
|
46
.github/workflows/nightly.yml
vendored
46
.github/workflows/nightly.yml
vendored
@ -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 }}<br />{{name}} {{conclusion}} at {{completed_at}} <a href=\"{{html_url}}\">[details]</a>{{/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 }}<br />{{name}} {{conclusion}} at {{completed_at}} <a href=\"{{html_url}}\">[details]</a>{{/if}}{{/with}}{{/each}}"
|
12
.github/workflows/quality.yml
vendored
12
.github/workflows/quality.yml
vendored
@ -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
|
||||
|
84
.github/workflows/sanity_test.yml
vendored
84
.github/workflows/sanity_test.yml
vendored
@ -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}} <a href=\"{{html_url}}\">[details]</a>{{/if}}{{/with}}{{/each}}"
|
@ -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
|
||||
|
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
@ -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
|
||||
|
@ -14,7 +14,7 @@ It is a total rewrite of [Riot-Android](https://github.com/vector-im/riot-androi
|
||||
[<img src="resources/img/google-play-badge.png" alt="Get it on Google Play" height="60">](https://play.google.com/store/apps/details?id=im.vector.app)
|
||||
[<img src="resources/img/f-droid-badge.png" alt="Get it on F-Droid" height="60">](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
|
||||
|
@ -144,11 +144,6 @@ project(":library:diff-match-patch") {
|
||||
}
|
||||
}
|
||||
|
||||
// Global configurations across all modules
|
||||
ext {
|
||||
isThreadingEnabled = true
|
||||
}
|
||||
|
||||
//project(":matrix-sdk-android") {
|
||||
// sonarqube {
|
||||
// properties {
|
||||
|
1
changelog.d/4319.bugfix
Normal file
1
changelog.d/4319.bugfix
Normal file
@ -0,0 +1 @@
|
||||
Open direct message screen when clicking on DM button in the space members list
|
1
changelog.d/5005.feature
Normal file
1
changelog.d/5005.feature
Normal file
@ -0,0 +1 @@
|
||||
Add possibility to save media from Gallery + reorder choices in message context menu
|
1
changelog.d/5325.feature
Normal file
1
changelog.d/5325.feature
Normal file
@ -0,0 +1 @@
|
||||
Adds forceLoginFallback feature flag and usages to FTUE login and registration
|
1
changelog.d/5330.misc
Normal file
1
changelog.d/5330.misc
Normal file
@ -0,0 +1 @@
|
||||
Continue improving realm usage.
|
1
changelog.d/5330.sdk
Normal file
1
changelog.d/5330.sdk
Normal file
@ -0,0 +1 @@
|
||||
Change name of getTimeLineEvent and getTimeLineEventLive methods to getTimelineEvent and getTimelineEventLive.
|
1
changelog.d/5379.misc
Normal file
1
changelog.d/5379.misc
Normal file
@ -0,0 +1 @@
|
||||
Cleanup unused threads build configurations
|
@ -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 }
|
||||
|
@ -58,9 +58,9 @@ class FlowRoom(private val room: Room) {
|
||||
}
|
||||
|
||||
fun liveTimelineEvent(eventId: String): Flow<Optional<TimelineEvent>> {
|
||||
return room.getTimeLineEventLive(eventId).asFlow()
|
||||
return room.getTimelineEventLive(eventId).asFlow()
|
||||
.startWith(room.coroutineDispatchers.io) {
|
||||
room.getTimeLineEvent(eventId).toOptional()
|
||||
room.getTimelineEvent(eventId).toOptional()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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'
|
||||
|
@ -95,7 +95,7 @@ class PreShareKeysTest : InstrumentedTest {
|
||||
assertEquals(megolmSessionId, sentEvent.root.content.toModel<EncryptedEventContent>()?.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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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, "") }
|
||||
|
||||
|
@ -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, "") }
|
||||
}
|
||||
|
@ -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<Optional<TimelineEvent>>
|
||||
fun getTimelineEventLive(eventId: String): LiveData<Optional<TimelineEvent>>
|
||||
|
||||
/**
|
||||
* Returns a snapshot list of TimelineEvent with EventType.MESSAGE and MessageType.MSGTYPE_IMAGE or MessageType.MSGTYPE_VIDEO.
|
||||
|
@ -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<ReadReceiptEntity>, realm: Realm): List<ReadReceipt> {
|
||||
return readReceipts
|
||||
.mapNotNull {
|
||||
val roomMember = RoomMemberSummaryEntity.where(realm, roomId = it.roomId, userId = it.userId).findFirst()
|
||||
?: return@mapNotNull null
|
||||
ReadReceipt(roomMember.asDomain(), it.originServerTs.toLong())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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<Optional<TimelineEvent>> {
|
||||
return LiveTimelineEvent(monarchy, taskExecutor.executorScope, timelineEventMapper, roomId, eventId)
|
||||
override fun getTimelineEventLive(eventId: String): LiveData<Optional<TimelineEvent>> {
|
||||
return timelineEventDataSource.getTimelineEventLive(roomId, eventId)
|
||||
}
|
||||
|
||||
override fun getAttachmentMessages(): List<TimelineEvent> {
|
||||
// TODO pretty bad query.. maybe we should denormalize clear type in base?
|
||||
return realmSessionProvider.withRealm { realm ->
|
||||
realm.where<TimelineEventEntity>()
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ internal class RealmSendingEventsDataSource(
|
||||
|
||||
private val sendingTimelineEventsListener = RealmChangeListener<RealmList<TimelineEventEntity>> { 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<TimelineEventEntity>?) {
|
||||
// Makes sure to close the previous frozen realm
|
||||
frozenSendingTimelineEvents?.realm?.close()
|
||||
frozenSendingTimelineEvents = sendingEvents?.freeze()
|
||||
}
|
||||
|
||||
override fun buildSendingEvents(): List<TimelineEvent> {
|
||||
val builtSendingEvents = mutableListOf<TimelineEvent>()
|
||||
uiEchoManager.getInMemorySendingEvents()
|
||||
|
@ -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<Optional<TimelineEvent>> {
|
||||
return LiveTimelineEvent(monarchy, taskExecutor.executorScope, timelineEventMapper, roomId, eventId)
|
||||
}
|
||||
|
||||
fun getAttachmentMessages(roomId: String): List<TimelineEvent> {
|
||||
// TODO pretty bad query.. maybe we should denormalize clear type in base?
|
||||
return realmSessionProvider.withRealm { realm ->
|
||||
realm.where<TimelineEventEntity>()
|
||||
.equalTo(TimelineEventEntityFields.ROOM_ID, roomId)
|
||||
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
|
||||
.findAll()
|
||||
?.mapNotNull { timelineEventMapper.map(it).takeIf { it.root.isImageMessage() || it.root.isVideoMessage() } }
|
||||
.orEmpty()
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -53,7 +53,7 @@ class DebugFeaturesStateFactory @Inject constructor(
|
||||
label = "FTUE Personalize profile",
|
||||
key = DebugFeatureKeys.onboardingPersonalize,
|
||||
factory = VectorFeatures::isOnboardingPersonalizeEnabled
|
||||
)
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
|
@ -43,9 +43,13 @@ class DebugPrivateSettingsFragment : VectorBaseFragment<FragmentDebugPrivateSett
|
||||
views.forceDialPadTabDisplay.setOnCheckedChangeListener { _, isChecked ->
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -25,6 +25,12 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Force DialPad tab display" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/forceLoginFallback"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Force login and registration fallback" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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<*, *>
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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!!,
|
||||
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,14 +49,7 @@ abstract class BaseAttachmentProvider<Type>(
|
||||
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<Type>(
|
||||
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)
|
||||
|
@ -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? {
|
||||
|
@ -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()
|
||||
}
|
@ -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())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
@ -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<VectorDummyViewState, VectorAttachmentViewerAction, VectorAttachmentViewerViewEvents>(initialState) {
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : MavericksAssistedViewModelFactory<VectorAttachmentViewerViewModel, VectorDummyViewState> {
|
||||
override fun create(initialState: VectorDummyViewState): VectorAttachmentViewerViewModel
|
||||
}
|
||||
|
||||
companion object : MavericksViewModelFactory<VectorAttachmentViewerViewModel, VectorDummyViewState> 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)) }
|
||||
}
|
||||
}
|
||||
}
|
@ -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<Unit> = withContext(session.coroutineDispatchers.io) {
|
||||
runCatching {
|
||||
saveMedia(
|
||||
context = appContext,
|
||||
file = input,
|
||||
title = input.name,
|
||||
mediaMimeType = getMimeTypeFromUri(appContext, input.toUri()),
|
||||
notificationUtils = notificationUtils
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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 -> {
|
||||
|
@ -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<OnboardingViewState, OnboardingAction, OnboardingViewEvents>(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
|
||||
|
@ -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<String> = emptyList(),
|
||||
val knownCustomHomeServersUrls: List<String> = emptyList()
|
||||
val knownCustomHomeServersUrls: List<String> = emptyList(),
|
||||
val isForceLoginFallbackEnabled: Boolean = false,
|
||||
) : MavericksState {
|
||||
|
||||
fun isLoading(): Boolean {
|
||||
|
@ -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<String>) {
|
||||
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<String>) {
|
||||
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 }
|
||||
|
@ -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,
|
||||
|
@ -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<String>) {
|
||||
val editedEvent = room.getTimeLineEvent(editedEventId) ?: return
|
||||
val editedEvent = room.getTimelineEvent(editedEventId) ?: return
|
||||
room.editPoll(editedEvent, pollType, question, options)
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -39,4 +39,5 @@ sealed class RoomMemberProfileViewEvents : VectorViewEvents {
|
||||
) : RoomMemberProfileViewEvents()
|
||||
|
||||
data class ShareRoomMemberProfile(val permalink: String) : RoomMemberProfileViewEvents()
|
||||
data class OpenRoom(val roomId: String) : RoomMemberProfileViewEvents()
|
||||
}
|
||||
|
@ -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<RoomMemberProfileViewState, RoomMemberProfileAction, RoomMemberProfileViewEvents>(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)
|
||||
|
@ -59,4 +59,16 @@ class VectorDataStore @Inject constructor(
|
||||
settings[forceDialPadDisplay] = force
|
||||
}
|
||||
}
|
||||
|
||||
private val forceLoginFallback = booleanPreferencesKey("force_login_fallback")
|
||||
|
||||
val forceLoginFallbackFlow: Flow<Boolean> = context.dataStore.data.map { preferences ->
|
||||
preferences[forceLoginFallback].orFalse()
|
||||
}
|
||||
|
||||
suspend fun setForceLoginFallbackFlow(force: Boolean) {
|
||||
context.dataStore.edit { settings ->
|
||||
settings[forceLoginFallback] = force
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -67,6 +67,23 @@
|
||||
app:layout_constraintTop_toBottomOf="@id/overlayCounterText"
|
||||
tools:text="Bill 29 Jun at 19:42" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/overlayDownloadButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/action_download"
|
||||
android:focusable="true"
|
||||
android:padding="6dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/overlayTopBackground"
|
||||
app:layout_constraintEnd_toStartOf="@id/overlayShareButton"
|
||||
app:layout_constraintTop_toTopOf="@id/overlayTopBackground"
|
||||
app:srcCompat="@drawable/ic_material_save"
|
||||
app:tint="?colorOnPrimary"
|
||||
tools:ignore="MissingPrefix" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/overlayShareButton"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -0,0 +1,135 @@
|
||||
/*
|
||||
* 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 android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import com.airbnb.mvrx.test.MvRxTestRule
|
||||
import im.vector.app.core.intent.getMimeTypeFromUri
|
||||
import im.vector.app.core.utils.saveMedia
|
||||
import im.vector.app.features.notifications.NotificationUtils
|
||||
import im.vector.app.test.fakes.FakeFile
|
||||
import im.vector.app.test.fakes.FakeSession
|
||||
import io.mockk.MockKAnnotations
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import io.mockk.impl.annotations.OverrideMockKs
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.runs
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import io.mockk.verifyAll
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class DownloadMediaUseCaseTest {
|
||||
|
||||
@get:Rule
|
||||
val mvRxTestRule = MvRxTestRule()
|
||||
|
||||
@MockK
|
||||
lateinit var appContext: Context
|
||||
|
||||
private val session = FakeSession()
|
||||
|
||||
@MockK
|
||||
lateinit var notificationUtils: NotificationUtils
|
||||
|
||||
private val file = FakeFile()
|
||||
|
||||
@OverrideMockKs
|
||||
lateinit var downloadMediaUseCase: DownloadMediaUseCase
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockKAnnotations.init(this)
|
||||
mockkStatic("im.vector.app.core.utils.ExternalApplicationsUtilKt")
|
||||
mockkStatic("im.vector.app.core.intent.VectorMimeTypeKt")
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
unmockkStatic("im.vector.app.core.utils.ExternalApplicationsUtilKt")
|
||||
unmockkStatic("im.vector.app.core.intent.VectorMimeTypeKt")
|
||||
file.tearDown()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a file when calling execute then save the file in local with success`() = runBlockingTest {
|
||||
// Given
|
||||
val uri = mockk<Uri>()
|
||||
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<Uri>()
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
49
vector/src/test/java/im/vector/app/test/fakes/FakeFile.kt
Normal file
49
vector/src/test/java/im/vector/app/test/fakes/FakeFile.kt
Normal file
@ -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<File>()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user