diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index d8c1bb6c49..455545aeef 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -265,6 +265,7 @@ jobs: failure_screenshots/ codecov-units: + name: Unit tests with code coverage runs-on: macos-latest steps: - uses: actions/checkout@v3 @@ -290,6 +291,7 @@ jobs: build/reports/jacoco/allCodeCoverageReport/allCodeCoverageReport.xml sonarqube: + name: Sonarqube upload runs-on: macos-latest if: always() needs: @@ -319,6 +321,7 @@ jobs: # Notify the channel about scheduled runs, do not notify for manually triggered runs notify: + name: Notify matrix runs-on: ubuntu-latest needs: - integration-tests @@ -333,4 +336,4 @@ jobs: matrix_access_token: ${{ secrets.ELEMENT_ANDROID_NOTIFICATION_ACCESS_TOKEN }} matrix_room_id: ${{ secrets.ELEMENT_ANDROID_INTERNAL_ROOM_ID }} text_template: "Nightly test run: {{#each job_statuses }}{{#with this }}{{#if completed }} {{name}} {{conclusion}} at {{completed_at}}, {{/if}}{{/with}}{{/each}}" - html_template: "Nightly test run results: {{#each job_statuses }}{{#with this }}{{#if completed }}
{{name}} {{conclusion}} at {{completed_at}} [details]{{/if}}{{/with}}{{/each}}" + html_template: "Nightly test run results: {{#each job_statuses }}{{#with this }}{{#if completed }}
{{icon conclusion}} {{name}} {{conclusion}} at {{completed_at}} [details]{{/if}}{{/with}}{{/each}}" diff --git a/changelog.d/5408.misc b/changelog.d/5408.misc new file mode 100644 index 0000000000..3807ee1da8 --- /dev/null +++ b/changelog.d/5408.misc @@ -0,0 +1 @@ +Improved onboarding registration unit test coverage \ No newline at end of file diff --git a/changelog.d/5513.misc b/changelog.d/5513.misc new file mode 100644 index 0000000000..767a9f1843 --- /dev/null +++ b/changelog.d/5513.misc @@ -0,0 +1 @@ +Added online presence indicator attribute online to match offline styling diff --git a/changelog.d/5536.feature b/changelog.d/5536.feature new file mode 100644 index 0000000000..bd0160f2fe --- /dev/null +++ b/changelog.d/5536.feature @@ -0,0 +1 @@ +Live location sharing: adding build config field and show permission dialog diff --git a/changelog.d/5540.bugfix b/changelog.d/5540.bugfix new file mode 100644 index 0000000000..8887cf4074 --- /dev/null +++ b/changelog.d/5540.bugfix @@ -0,0 +1 @@ +Fixes crash when tapping the timeline verification surround box instead of the buttons \ No newline at end of file diff --git a/changelog.d/5551.bugfix b/changelog.d/5551.bugfix new file mode 100644 index 0000000000..22f9d51e18 --- /dev/null +++ b/changelog.d/5551.bugfix @@ -0,0 +1 @@ +Fix local echos not being shown when re-opening rooms diff --git a/changelog.d/5552.bugfix b/changelog.d/5552.bugfix new file mode 100644 index 0000000000..5061e642f0 --- /dev/null +++ b/changelog.d/5552.bugfix @@ -0,0 +1 @@ +Fix crash when closing a room while decrypting timeline events diff --git a/changelog.d/5563.misc b/changelog.d/5563.misc new file mode 100644 index 0000000000..c0867365f6 --- /dev/null +++ b/changelog.d/5563.misc @@ -0,0 +1 @@ +Add a presence sync enabling build config diff --git a/changelog.d/5571.feature b/changelog.d/5571.feature new file mode 100644 index 0000000000..04b62b8940 --- /dev/null +++ b/changelog.d/5571.feature @@ -0,0 +1 @@ +Live location sharing: Adding indicator view when enabled diff --git a/changelog.d/5572.misc b/changelog.d/5572.misc new file mode 100644 index 0000000000..d37d8fe07d --- /dev/null +++ b/changelog.d/5572.misc @@ -0,0 +1,2 @@ +Show stickers on click + diff --git a/library/jsonviewer/build.gradle b/library/jsonviewer/build.gradle index 15f46754b3..0cad8ac171 100644 --- a/library/jsonviewer/build.gradle +++ b/library/jsonviewer/build.gradle @@ -59,7 +59,7 @@ dependencies { implementation libs.jetbrains.coroutinesCore implementation libs.jetbrains.coroutinesAndroid - testImplementation 'org.json:json:20211205' + testImplementation 'org.json:json:20220320' testImplementation libs.tests.junit androidTestImplementation libs.androidx.junit androidTestImplementation libs.androidx.espressoCore diff --git a/library/ui-styles/src/main/res/values/colors.xml b/library/ui-styles/src/main/res/values/colors.xml index 75b03a7d2e..d887e7774e 100644 --- a/library/ui-styles/src/main/res/values/colors.xml +++ b/library/ui-styles/src/main/res/values/colors.xml @@ -122,6 +122,10 @@ @color/palette_gray_100 @color/palette_gray_450 + + @color/palette_element_green + @color/palette_element_green + @color/palette_prune diff --git a/library/ui-styles/src/main/res/values/palette_mobile.xml b/library/ui-styles/src/main/res/values/palette_mobile.xml index ec2f1d0814..5610771f8a 100644 --- a/library/ui-styles/src/main/res/values/palette_mobile.xml +++ b/library/ui-styles/src/main/res/values/palette_mobile.xml @@ -53,5 +53,4 @@ @color/palette_verde @color/palette_azure @color/palette_grape - \ No newline at end of file diff --git a/library/ui-styles/src/main/res/values/styles_location.xml b/library/ui-styles/src/main/res/values/styles_location.xml new file mode 100644 index 0000000000..5563d28342 --- /dev/null +++ b/library/ui-styles/src/main/res/values/styles_location.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/library/ui-styles/src/main/res/values/theme_dark.xml b/library/ui-styles/src/main/res/values/theme_dark.xml index 06670ccd68..7177687fdd 100644 --- a/library/ui-styles/src/main/res/values/theme_dark.xml +++ b/library/ui-styles/src/main/res/values/theme_dark.xml @@ -43,6 +43,7 @@ @color/vctr_presence_indicator_offline_dark + @color/vctr_presence_indicator_online_dark ?vctr_system diff --git a/library/ui-styles/src/main/res/values/theme_light.xml b/library/ui-styles/src/main/res/values/theme_light.xml index c184464320..c90c021591 100644 --- a/library/ui-styles/src/main/res/values/theme_light.xml +++ b/library/ui-styles/src/main/res/values/theme_light.xml @@ -43,6 +43,7 @@ @color/vctr_presence_indicator_offline_light + @color/vctr_presence_indicator_online_light ?vctr_system diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt index c87f21d7ac..a97e7d8cbe 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt @@ -60,7 +60,11 @@ data class MatrixConfiguration( /** * RoomDisplayNameFallbackProvider to provide default room display name. */ - val roomDisplayNameFallbackProvider: RoomDisplayNameFallbackProvider + val roomDisplayNameFallbackProvider: RoomDisplayNameFallbackProvider, + /** + * True to enable presence information sync (if available). False to disable regardless of server setting. + */ + val presenceSyncEnabled: Boolean = true ) { /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt index 1262c09d97..cb61222de7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt @@ -55,6 +55,7 @@ internal class RealmSendingEventsDataSource( roomEntity = RoomEntity.where(safeRealm, roomId = roomId).findFirst() sendingTimelineEvents = roomEntity?.sendingTimelineEvents sendingTimelineEvents?.addChangeListener(sendingTimelineEventsListener) + updateFrozenResults(sendingTimelineEvents) } override fun stop() { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt index bacac58d84..3ddd877b78 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt @@ -100,8 +100,12 @@ internal class TimelineEventDecryptor @Inject constructor( } executor?.execute { Realm.getInstance(realmConfiguration).use { realm -> - runBlocking { - processDecryptRequest(request, realm) + try { + runBlocking { + processDecryptRequest(request, realm) + } + } catch (e: InterruptedException) { + Timber.i("Decryption got interrupted") } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/PresenceSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/PresenceSyncHandler.kt index fe173a35c3..e5bed12181 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/PresenceSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/PresenceSyncHandler.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.sync.handler import io.realm.Realm +import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.getPresenceContent import org.matrix.android.sdk.api.session.sync.model.PresenceSyncResponse @@ -27,27 +28,29 @@ import org.matrix.android.sdk.internal.database.query.updateDirectUserPresence import org.matrix.android.sdk.internal.database.query.updateUserPresence import javax.inject.Inject -internal class PresenceSyncHandler @Inject constructor() { +internal class PresenceSyncHandler @Inject constructor(private val matrixConfiguration: MatrixConfiguration) { fun handle(realm: Realm, presenceSyncResponse: PresenceSyncResponse?) { - presenceSyncResponse?.events - ?.filter { event -> event.type == EventType.PRESENCE } - ?.forEach { event -> - val content = event.getPresenceContent() ?: return@forEach - val userId = event.senderId ?: return@forEach - val userPresenceEntity = UserPresenceEntity( - userId = userId, - lastActiveAgo = content.lastActiveAgo, - statusMessage = content.statusMessage, - isCurrentlyActive = content.isCurrentlyActive, - avatarUrl = content.avatarUrl, - displayName = content.displayName - ).also { - it.presence = content.presence - } + if (matrixConfiguration.presenceSyncEnabled) { + presenceSyncResponse?.events + ?.filter { event -> event.type == EventType.PRESENCE } + ?.forEach { event -> + val content = event.getPresenceContent() ?: return@forEach + val userId = event.senderId ?: return@forEach + val userPresenceEntity = UserPresenceEntity( + userId = userId, + lastActiveAgo = content.lastActiveAgo, + statusMessage = content.statusMessage, + isCurrentlyActive = content.isCurrentlyActive, + avatarUrl = content.avatarUrl, + displayName = content.displayName + ).also { + it.presence = content.presence + } - storePresenceToDB(realm, userPresenceEntity) - } + storePresenceToDB(realm, userPresenceEntity) + } + } } /** diff --git a/tools/ci/render_test_output.py b/tools/ci/render_test_output.py index 1e7940ce04..f955b93cf9 100755 --- a/tools/ci/render_test_output.py +++ b/tools/ci/render_test_output.py @@ -13,32 +13,35 @@ print("::group::Arguments") print(f"{sys.argv}") print("::endgroup::") for xmlfile in xmlfiles: - tree = ET.parse(xmlfile) + try: + tree = ET.parse(xmlfile) - root = tree.getroot() - name = root.attrib['name'] - time = root.attrib['time'] - tests = int(root.attrib['tests']) - skipped = int(root.attrib['skipped']) - errors = int(root.attrib['errors']) - failures = int(root.attrib['failures']) - success = tests - failures - errors - skipped - total = tests - skipped - print(f"::group::{name} {success}/{total} ({skipped} skipped) in {time}") - - for testcase in root: - if testcase.tag != "testcase": - continue - testname = testcase.attrib['classname'] - message = testcase.attrib['name'] - time = testcase.attrib['time'] - child = testcase.find("failure") - if child is None: - print(f"{message} in {time}s") - else: - print(f"::error file={testname}::{message} in {time}s") - print(child.text) - body = f"passed={success} failures={failures} errors={errors} skipped={skipped}" - print(f"::set-output name={suitename}::={body}") + root = tree.getroot() + name = root.attrib['name'] + time = root.attrib['time'] + tests = int(root.attrib['tests']) + skipped = int(root.attrib['skipped']) + errors = int(root.attrib['errors']) + failures = int(root.attrib['failures']) + success = tests - failures - errors - skipped + total = tests - skipped + print(f"::group::{name} {success}/{total} ({skipped} skipped) in {time}") + + for testcase in root: + if testcase.tag != "testcase": + continue + testname = testcase.attrib['classname'] + message = testcase.attrib['name'] + time = testcase.attrib['time'] + child = testcase.find("failure") + if child is None: + print(f"{message} in {time}s") + else: + print(f"::error file={testname}::{message} in {time}s") + print(child.text) + body = f" passed={success} failures={failures} errors={errors} skipped={skipped}" + print(f"::set-output name={suitename}::={body}") + except FileNotFoundError: + print(f"::error::Unable to open test results file {xmlfile} - check if the tests completed") print("::endgroup::") diff --git a/tools/emojis/emoji_picker_datasource_formatted.json b/tools/emojis/emoji_picker_datasource_formatted.json index c1aa590003..a1b944a7eb 100644 --- a/tools/emojis/emoji_picker_datasource_formatted.json +++ b/tools/emojis/emoji_picker_datasource_formatted.json @@ -2056,7 +2056,9 @@ "disappear", "dissolve", "liquid", - "melt" + "melt", + "hot", + "heat" ] }, "winking-face": { @@ -2351,7 +2353,10 @@ "disbelief", "embarrass", "scared", - "surprise" + "surprise", + "silence", + "secret", + "shock" ] }, "face-with-peeking-eye": { @@ -2360,7 +2365,10 @@ "j": [ "captivated", "peep", - "stare" + "stare", + "scared", + "frightening", + "embarrassing" ] }, "shushing-face": { @@ -2392,7 +2400,8 @@ "salute", "sunny", "troops", - "yes" + "yes", + "respect" ] }, "zippermouth-face": { @@ -2467,7 +2476,10 @@ "disappear", "hide", "introvert", - "invisible" + "invisible", + "lonely", + "isolation", + "depression" ] }, "face-in-clouds": { @@ -2863,7 +2875,11 @@ "disappointed", "meh", "skeptical", - "unsure" + "unsure", + "skeptic", + "confuse", + "frustrated", + "indifferent" ] }, "worried-face": { @@ -2969,7 +2985,9 @@ "cry", "proud", "resist", - "sad" + "sad", + "touched", + "gratitude" ] }, "frowning-face-with-open-mouth": { @@ -4065,7 +4083,9 @@ "j": [ "hand", "right", - "rightward" + "rightward", + "palm", + "offer" ] }, "leftwards-hand": { @@ -4074,7 +4094,9 @@ "j": [ "hand", "left", - "leftward" + "leftward", + "palm", + "offer" ] }, "palm-down-hand": { @@ -4083,7 +4105,8 @@ "j": [ "dismiss", "drop", - "shoo" + "shoo", + "palm" ] }, "palm-up-hand": { @@ -4093,7 +4116,9 @@ "beckon", "catch", "come", - "offer" + "offer", + "lift", + "demand" ] }, "ok-hand": { @@ -4290,7 +4315,8 @@ "b": "1FAF5", "j": [ "point", - "you" + "you", + "recruit" ] }, "thumbs-up": { @@ -4404,7 +4430,9 @@ "a": "⊛ Heart Hands", "b": "1FAF6", "j": [ - "love" + "love", + "appreciation", + "support" ] }, "open-hands": { @@ -4662,7 +4690,11 @@ "flirting", "nervous", "uncomfortable", - "worried" + "worried", + "flirt", + "sexy", + "pain", + "worry" ] }, "baby": { @@ -6058,7 +6090,8 @@ "monarch", "noble", "regal", - "royalty" + "royalty", + "power" ] }, "prince": { @@ -6231,7 +6264,8 @@ "belly", "bloated", "full", - "pregnant" + "pregnant", + "baby" ] }, "pregnant-person": { @@ -6241,7 +6275,8 @@ "belly", "bloated", "full", - "pregnant" + "pregnant", + "baby" ] }, "breastfeeding": { @@ -6635,7 +6670,8 @@ "j": [ "fairy tale", "fantasy", - "monster" + "monster", + "mystical" ] }, "person-getting-massage": { @@ -9374,7 +9410,8 @@ "b": "1FAB8", "j": [ "ocean", - "reef" + "reef", + "sea" ] }, "snail": { @@ -9587,7 +9624,9 @@ "Hinduism", "India", "purity", - "Vietnam" + "Vietnam", + "calm", + "meditation" ] }, "rosette": { @@ -9832,14 +9871,16 @@ "a": "⊛ Empty Nest", "b": "1FAB9", "j": [ - "nesting" + "nesting", + "bird" ] }, "nest-with-eggs": { "a": "⊛ Nest with Eggs", "b": "1FABA", "j": [ - "nesting" + "nesting", + "bird" ] }, "grapes": { @@ -11187,7 +11228,9 @@ "drink", "empty", "glass", - "spill" + "spill", + "cup", + "water" ] }, "cup-with-straw": { @@ -12003,7 +12046,9 @@ "b": "1F6DD", "j": [ "amusement park", - "play" + "play", + "fun", + "park" ] }, "ferris-wheel": { @@ -12533,7 +12578,9 @@ "j": [ "circle", "tire", - "turn" + "turn", + "car", + "transport" ] }, "police-car-light": { @@ -14666,7 +14713,8 @@ "hand", "Mary", "Miriam", - "protection" + "protection", + "religion" ] }, "video-game": { @@ -15864,7 +15912,9 @@ "b": "1FAAB", "j": [ "electronic", - "low energy" + "low energy", + "drained", + "dead" ] }, "electric-plug": { @@ -17508,7 +17558,9 @@ "disability", "hurt", "mobility aid", - "stick" + "stick", + "accessibility", + "assist" ] }, "stethoscope": { @@ -17528,7 +17580,9 @@ "bones", "doctor", "medical", - "skeleton" + "skeleton", + "x-ray", + "medicine" ] }, "door": { @@ -17733,7 +17787,10 @@ "burp", "clean", "soap", - "underwater" + "underwater", + "fun", + "carbonation", + "sparkling" ] }, "toothbrush": { @@ -17856,7 +17913,8 @@ "credentials", "ID", "license", - "security" + "security", + "document" ] }, "atm-sign": { diff --git a/vector/build.gradle b/vector/build.gradle index 2d9c097da8..aeaad19e02 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -151,6 +151,7 @@ android { buildConfigField "Boolean", "enableLocationSharing", "true" buildConfigField "String", "mapTilerKey", "\"fU3vlMsMn4Jb6dnEIFsx\"" + buildConfigField "Boolean", "PRESENCE_SYNC_ENABLED", "true" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -229,6 +230,7 @@ android { buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false" // Set to true if you want to enable strict mode in debug buildConfigField "boolean", "ENABLE_STRICT_MODE_LOGS", "false" + buildConfigField "Boolean", "ENABLE_LIVE_LOCATION_SHARING", "true" signingConfig signingConfigs.debug } @@ -238,6 +240,7 @@ android { buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false" buildConfigField "boolean", "ENABLE_STRICT_MODE_LOGS", "false" + buildConfigField "Boolean", "ENABLE_LIVE_LOCATION_SHARING", "false" postprocessing { removeUnusedCode true diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 58b1bc177c..1d99fba91a 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -45,6 +45,7 @@ + diff --git a/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt b/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt index a5575ef536..e3a84f95de 100644 --- a/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt @@ -116,7 +116,8 @@ object VectorStaticModule { fun providesMatrixConfiguration(vectorRoomDisplayNameFallbackProvider: VectorRoomDisplayNameFallbackProvider): MatrixConfiguration { return MatrixConfiguration( applicationFlavor = BuildConfig.FLAVOR_DESCRIPTION, - roomDisplayNameFallbackProvider = vectorRoomDisplayNameFallbackProvider + roomDisplayNameFallbackProvider = vectorRoomDisplayNameFallbackProvider, + presenceSyncEnabled = BuildConfig.PRESENCE_SYNC_ENABLED ) } diff --git a/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileMatrixItemWithPowerLevelWithPresence.kt b/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileMatrixItemWithPowerLevelWithPresence.kt index 92216cbb38..923fa80b55 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileMatrixItemWithPowerLevelWithPresence.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileMatrixItemWithPowerLevelWithPresence.kt @@ -25,10 +25,11 @@ import org.matrix.android.sdk.api.session.presence.model.UserPresence @EpoxyModelClass(layout = R.layout.item_profile_matrix_item) abstract class ProfileMatrixItemWithPowerLevelWithPresence : ProfileMatrixItemWithPowerLevel() { + @EpoxyAttribute var showPresence: Boolean = true @EpoxyAttribute var userPresence: UserPresence? = null override fun bind(holder: Holder) { super.bind(holder) - holder.presenceImageView.render(userPresence = userPresence) + holder.presenceImageView.render(showPresence, userPresence) } } diff --git a/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt b/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt index dabf11b9d3..eada3a4f25 100644 --- a/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt +++ b/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt @@ -19,6 +19,7 @@ package im.vector.app.core.utils import android.Manifest import android.app.Activity import android.content.pm.PackageManager +import android.os.Build import androidx.activity.ComponentActivity import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts @@ -32,6 +33,7 @@ import im.vector.app.R import im.vector.app.core.platform.VectorBaseActivity // Permissions sets +val PERMISSIONS_EMPTY = emptyList() val PERMISSIONS_FOR_AUDIO_IP_CALL = listOf(Manifest.permission.RECORD_AUDIO) val PERMISSIONS_FOR_VIDEO_IP_CALL = listOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA) val PERMISSIONS_FOR_VOICE_MESSAGE = listOf(Manifest.permission.RECORD_AUDIO) @@ -40,9 +42,12 @@ val PERMISSIONS_FOR_MEMBERS_SEARCH = listOf(Manifest.permission.READ_CONTACTS) val PERMISSIONS_FOR_ROOM_AVATAR = listOf(Manifest.permission.CAMERA) val PERMISSIONS_FOR_WRITING_FILES = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE) val PERMISSIONS_FOR_PICKING_CONTACT = listOf(Manifest.permission.READ_CONTACTS) -val PERMISSIONS_FOR_LOCATION_SHARING = listOf(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION) - -val PERMISSIONS_EMPTY = emptyList() +val PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING = listOf(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION) +val PERMISSIONS_FOR_BACKGROUND_LOCATION_SHARING = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + listOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION) +} else { + PERMISSIONS_EMPTY +} // This is not ideal to store the value like that, but it works private var permissionDialogDisplayed = false @@ -123,6 +128,7 @@ fun checkPermissions(permissionsToBeGranted: List, .setPositiveButton(R.string.ok) { _, _ -> activityResultLauncher.launch(missingPermissions.toTypedArray()) } + .setNegativeButton(R.string.action_not_now, null) .show() } else { // some permissions are not granted, ask permissions diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt index a15bd52174..7fcbb6bae6 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt @@ -37,7 +37,7 @@ import androidx.core.view.isVisible import im.vector.app.R import im.vector.app.core.epoxy.onClick import im.vector.app.core.utils.PERMISSIONS_EMPTY -import im.vector.app.core.utils.PERMISSIONS_FOR_LOCATION_SHARING +import im.vector.app.core.utils.PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING import im.vector.app.core.utils.PERMISSIONS_FOR_PICKING_CONTACT import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO import im.vector.app.databinding.ViewAttachmentTypeSelectorBinding @@ -215,6 +215,6 @@ class AttachmentTypeSelectorView(context: Context, STICKER(PERMISSIONS_EMPTY, R.string.tooltip_attachment_sticker), CONTACT(PERMISSIONS_FOR_PICKING_CONTACT, R.string.tooltip_attachment_contact), POLL(PERMISSIONS_EMPTY, R.string.tooltip_attachment_poll), - LOCATION(PERMISSIONS_FOR_LOCATION_SHARING, R.string.tooltip_attachment_location) + LOCATION(PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING, R.string.tooltip_attachment_location) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 584c46dd97..2de37be8fb 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -177,6 +177,7 @@ import im.vector.app.features.html.PillsPostProcessor import im.vector.app.features.invite.VectorInviteView import im.vector.app.features.location.LocationSharingMode import im.vector.app.features.location.toLocationData +import im.vector.app.features.media.AttachmentData import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.VideoContentRenderer import im.vector.app.features.notifications.NotificationDrawerManager @@ -206,6 +207,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.billcarsonfr.jsonviewer.JSonViewerDialog import org.commonmark.parser.Parser +import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.events.model.EventType @@ -260,7 +262,8 @@ class TimelineFragment @Inject constructor( private val pillsPostProcessorFactory: PillsPostProcessor.Factory, private val callManager: WebRtcCallManager, private val voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker, - private val clock: Clock + private val clock: Clock, + private val matrixConfiguration: MatrixConfiguration ) : VectorBaseFragment(), TimelineEventController.Callback, @@ -1611,7 +1614,10 @@ class TimelineFragment @Inject constructor( views.includeRoomToolbar.roomToolbarTitleView.text = roomSummary.displayName avatarRenderer.render(roomSummary.toMatrixItem(), views.includeRoomToolbar.roomToolbarAvatarImageView) views.includeRoomToolbar.roomToolbarDecorationImageView.render(roomSummary.roomEncryptionTrustLevel) - views.includeRoomToolbar.roomToolbarPresenceImageView.render(roomSummary.isDirect, roomSummary.directUserPresence) + views.includeRoomToolbar.roomToolbarPresenceImageView.render( + roomSummary.isDirect && matrixConfiguration.presenceSyncEnabled, + roomSummary.directUserPresence + ) views.includeRoomToolbar.roomToolbarPublicImageView.isVisible = roomSummary.isPublic && !roomSummary.isDirect } } else { @@ -1771,13 +1777,11 @@ class TimelineFragment @Inject constructor( } is RoomDetailAction.ResumeVerification -> { val otherUserId = data.otherUserId ?: return - VerificationBottomSheet().apply { - setArguments(VerificationBottomSheet.VerificationArgs( - otherUserId = otherUserId, - verificationId = data.transactionId, - roomId = timelineArgs.roomId - )) - }.show(parentFragmentManager, "REQ") + VerificationBottomSheet.withArgs( + roomId = timelineArgs.roomId, + otherUserId = otherUserId, + transactionId = data.transactionId, + ).show(parentFragmentManager, "REQ") } } } @@ -1868,12 +1872,16 @@ class TimelineFragment @Inject constructor( vectorBaseActivity.notImplemented("encrypted message click") } - override fun onImageMessageClicked(messageImageContent: MessageImageInfoContent, mediaData: ImageContentRenderer.Data, view: View) { + override fun onImageMessageClicked(messageImageContent: MessageImageInfoContent, + mediaData: ImageContentRenderer.Data, + view: View, + inMemory: List) { navigator.openMediaViewer( activity = requireActivity(), roomId = timelineArgs.roomId, mediaData = mediaData, - view = view + view = view, + inMemory = inMemory ) { pairs -> pairs.add(Pair(views.roomToolbar, ViewCompat.getTransitionName(views.roomToolbar) ?: "")) pairs.add(Pair(views.composerLayout, ViewCompat.getTransitionName(views.composerLayout) ?: "")) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index fb47fb5136..a14888362b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -57,6 +57,7 @@ import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryEve import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever +import im.vector.app.features.media.AttachmentData import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.VideoContentRenderer import im.vector.app.features.settings.VectorPreferences @@ -127,7 +128,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec fun onEventVisible(event: TimelineEvent) fun onRoomCreateLinkClicked(url: String) fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View) - fun onImageMessageClicked(messageImageContent: MessageImageInfoContent, mediaData: ImageContentRenderer.Data, view: View) + fun onImageMessageClicked(messageImageContent: MessageImageInfoContent, + mediaData: ImageContentRenderer.Data, + view: View, + inMemory: List) fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) // fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index b7a6a88122..f6bb39f20c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -514,9 +514,12 @@ class MessageItemFactory @Inject constructor( .apply { if (messageContent.msgType == MessageType.MSGTYPE_STICKER_LOCAL) { mode(ImageContentRenderer.Mode.STICKER) + clickListener { view -> + callback?.onImageMessageClicked(messageContent, data, view, listOf(data)) + } } else { clickListener { view -> - callback?.onImageMessageClicked(messageContent, data, view) + callback?.onImageMessageClicked(messageContent, data, view, emptyList()) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt index 6326d9c97a..ca2a747b3b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt @@ -29,6 +29,7 @@ import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter import im.vector.app.features.home.room.typing.TypingHelper import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence +import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomSummary @@ -41,7 +42,8 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor private val stringProvider: StringProvider, private val typingHelper: TypingHelper, private val avatarRenderer: AvatarRenderer, - private val errorFormatter: ErrorFormatter) { + private val errorFormatter: ErrorFormatter, + private val matrixConfiguration: MatrixConfiguration) { fun create(roomSummary: RoomSummary, roomChangeMembershipStates: Map, @@ -125,7 +127,7 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor // We do not display shield in the room list anymore // .encryptionTrustLevel(roomSummary.roomEncryptionTrustLevel) .izPublic(roomSummary.isPublic) - .showPresence(roomSummary.isDirect) + .showPresence(roomSummary.isDirect && matrixConfiguration.presenceSyncEnabled) .userPresence(roomSummary.directUserPresence) .matrixItem(roomSummary.toMatrixItem()) .lastEventTime(latestEventTime) diff --git a/vector/src/main/java/im/vector/app/features/location/DefaultLocationSharingNavigator.kt b/vector/src/main/java/im/vector/app/features/location/DefaultLocationSharingNavigator.kt new file mode 100644 index 0000000000..8f424af9ec --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/DefaultLocationSharingNavigator.kt @@ -0,0 +1,36 @@ +/* + * 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.location + +import android.app.Activity +import im.vector.app.core.utils.openAppSettingsPage + +class DefaultLocationSharingNavigator constructor(val activity: Activity?) : LocationSharingNavigator { + + override var goingToAppSettings: Boolean = false + + override fun quit() { + activity?.finish() + } + + override fun goToAppSettings() { + activity?.let { + goingToAppSettings = true + openAppSettingsPage(it) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt index ec47c23ea7..d7d686ee60 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt @@ -23,4 +23,5 @@ sealed class LocationSharingAction : VectorViewModelAction { data class PinnedLocationSharing(val locationData: LocationData?) : LocationSharingAction() data class LocationTargetChange(val locationData: LocationData) : LocationSharingAction() object ZoomToUserLocation : LocationSharingAction() + object StartLiveLocationSharing : LocationSharingAction() } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt index e9e96e676c..c4dccc1b73 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt @@ -27,9 +27,14 @@ import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.mapbox.mapboxsdk.maps.MapView +import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.utils.PERMISSIONS_FOR_BACKGROUND_LOCATION_SHARING +import im.vector.app.core.utils.PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING +import im.vector.app.core.utils.checkPermissions +import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.databinding.FragmentLocationSharingBinding import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider @@ -49,6 +54,8 @@ class LocationSharingFragment @Inject constructor( private val viewModel: LocationSharingViewModel by fragmentViewModel() + private val locationSharingNavigator: LocationSharingNavigator by lazy { DefaultLocationSharingNavigator(activity) } + // Keep a ref to handle properly the onDestroy callback private var mapView: WeakReference? = null @@ -76,8 +83,8 @@ class LocationSharingFragment @Inject constructor( viewModel.observeViewEvents { when (it) { + LocationSharingViewEvents.Close -> locationSharingNavigator.quit() LocationSharingViewEvents.LocationNotAvailableError -> handleLocationNotAvailableError() - LocationSharingViewEvents.Close -> activity?.finish() is LocationSharingViewEvents.ZoomToUserLocation -> handleZoomToUserLocationEvent(it) }.exhaustive } @@ -86,6 +93,11 @@ class LocationSharingFragment @Inject constructor( override fun onResume() { super.onResume() views.mapView.onResume() + if (locationSharingNavigator.goingToAppSettings) { + locationSharingNavigator.goingToAppSettings = false + // retry to start live location + tryStartLiveLocationSharing() + } } override fun onPause() { @@ -137,12 +149,24 @@ class LocationSharingFragment @Inject constructor( .setTitle(R.string.location_not_available_dialog_title) .setMessage(R.string.location_not_available_dialog_content) .setPositiveButton(R.string.ok) { _, _ -> - activity?.finish() + locationSharingNavigator.quit() } .setCancelable(false) .show() } + private fun handleMissingBackgroundLocationPermission() { + MaterialAlertDialogBuilder(requireActivity()) + .setTitle(R.string.location_in_background_missing_permission_dialog_title) + .setMessage(R.string.location_in_background_missing_permission_dialog_content) + .setPositiveButton(R.string.settings) { _, _ -> + locationSharingNavigator.goToAppSettings() + } + .setNegativeButton(R.string.action_not_now, null) + .setCancelable(false) + .show() + } + private fun initLocateButton() { views.mapView.locateButton.setOnClickListener { viewModel.handle(LocationSharingAction.ZoomToUserLocation) @@ -164,22 +188,58 @@ class LocationSharingFragment @Inject constructor( viewModel.handle(LocationSharingAction.CurrentUserLocationSharing) } views.shareLocationOptionsPicker.optionUserLive.debouncedClicks { - // TODO + tryStartLiveLocationSharing() } } + private val foregroundLocationResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> + if (allGranted && checkPermissions(PERMISSIONS_FOR_BACKGROUND_LOCATION_SHARING, requireActivity(), backgroundLocationResultLauncher)) { + startLiveLocationSharing() + } else if (deniedPermanently) { + handleMissingBackgroundLocationPermission() + } + } + + private val backgroundLocationResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> + if (allGranted) { + startLiveLocationSharing() + } else if (deniedPermanently) { + handleMissingBackgroundLocationPermission() + } + } + + private fun tryStartLiveLocationSharing() { + // we need to re-check foreground location to be sure it has not changed after landing on this screen + if (checkPermissions(PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING, requireActivity(), foregroundLocationResultLauncher) && + checkPermissions( + PERMISSIONS_FOR_BACKGROUND_LOCATION_SHARING, + requireActivity(), + backgroundLocationResultLauncher, + R.string.location_in_background_missing_permission_dialog_content + )) { + startLiveLocationSharing() + } + } + + private fun startLiveLocationSharing() { + viewModel.handle(LocationSharingAction.StartLiveLocationSharing) + } + private fun updateMap(state: LocationSharingViewState) { // first, update the options view - when (state.areTargetAndUserLocationEqual) { - // TODO activate USER_LIVE option when implemented - true -> views.shareLocationOptionsPicker.render( - LocationSharingOption.USER_CURRENT - ) - false -> views.shareLocationOptionsPicker.render( - LocationSharingOption.PINNED - ) - else -> views.shareLocationOptionsPicker.render() + val options: Set = when (state.areTargetAndUserLocationEqual) { + true -> { + if (BuildConfig.ENABLE_LIVE_LOCATION_SHARING) { + setOf(LocationSharingOption.USER_CURRENT, LocationSharingOption.USER_LIVE) + } else { + setOf(LocationSharingOption.USER_CURRENT) + } + } + false -> setOf(LocationSharingOption.PINNED) + else -> emptySet() } + views.shareLocationOptionsPicker.render(options) + // then, update the map using the height of the options view after it has been rendered views.shareLocationOptionsPicker.post { val mapState = state diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingNavigator.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingNavigator.kt new file mode 100644 index 0000000000..8927da9239 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingNavigator.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.location + +interface LocationSharingNavigator { + var goingToAppSettings: Boolean + fun quit() + fun goToAppSettings() +} diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt index 25bc482412..639666e63f 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt @@ -38,6 +38,7 @@ import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.util.toMatrixItem +import timber.log.Timber /** * Sampling period to compare target location and user location. @@ -120,6 +121,7 @@ class LocationSharingViewModel @AssistedInject constructor( is LocationSharingAction.PinnedLocationSharing -> handlePinnedLocationSharingAction(action) is LocationSharingAction.LocationTargetChange -> handleLocationTargetChangeAction(action) LocationSharingAction.ZoomToUserLocation -> handleZoomToUserLocationAction() + LocationSharingAction.StartLiveLocationSharing -> handleStartLiveLocationSharingAction() }.exhaustive } @@ -157,6 +159,11 @@ class LocationSharingViewModel @AssistedInject constructor( } } + private fun handleStartLiveLocationSharingAction() { + // TODO start sharing live location and update view state + Timber.d("live location sharing started") + } + override fun onLocationUpdate(locationData: LocationData) { setState { copy(lastKnownUserLocation = locationData) diff --git a/vector/src/main/java/im/vector/app/features/location/live/LocationLiveStatusView.kt b/vector/src/main/java/im/vector/app/features/location/live/LocationLiveStatusView.kt new file mode 100644 index 0000000000..a4c58c9e5b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/live/LocationLiveStatusView.kt @@ -0,0 +1,39 @@ +/* + * 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.location.live + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.Button +import androidx.constraintlayout.widget.ConstraintLayout +import im.vector.app.databinding.ViewLocationLiveStatusBinding + +class LocationLiveStatusView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr) { + + private val binding = ViewLocationLiveStatusBinding.inflate( + LayoutInflater.from(context), + this + ) + + val stopButton: Button + get() = binding.locationLiveStatusStop +} diff --git a/vector/src/main/java/im/vector/app/features/location/option/LocationSharingOptionPickerView.kt b/vector/src/main/java/im/vector/app/features/location/option/LocationSharingOptionPickerView.kt index 1aea1ff613..8a603a1a56 100644 --- a/vector/src/main/java/im/vector/app/features/location/option/LocationSharingOptionPickerView.kt +++ b/vector/src/main/java/im/vector/app/features/location/option/LocationSharingOptionPickerView.kt @@ -58,7 +58,7 @@ class LocationSharingOptionPickerView @JvmOverloads constructor( applyBackground() } - fun render(vararg options: LocationSharingOption) { + fun render(options: Set = emptySet()) { val optionsNumber = options.toSet().size val isPinnedVisible = options.contains(LocationSharingOption.PINNED) val isUserCurrentVisible = options.contains(LocationSharingOption.USER_CURRENT) diff --git a/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt b/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt index 1e0a3a2ad9..781a176550 100644 --- a/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt +++ b/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt @@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.file.FileService import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent import org.matrix.android.sdk.api.session.room.model.message.getFileUrl @@ -52,7 +53,10 @@ class RoomEventsAttachmentProvider( override fun getAttachmentInfoAt(position: Int): AttachmentInfo { return getItem(position).let { - val content = it.root.getClearContent().toModel() as? MessageWithAttachmentContent + val clearContent = it.root.getClearContent() + val content = clearContent.toModel() + ?: clearContent.toModel() + as? MessageWithAttachmentContent if (content is MessageImageContent) { val data = ImageContentRenderer.Data( eventId = it.eventId, @@ -66,6 +70,33 @@ class RoomEventsAttachmentProvider( height = null, allowNonMxcUrls = it.root.sendState.isSending() + ) + if (content.mimeType == MimeTypes.Gif) { + AttachmentInfo.AnimatedImage( + uid = it.eventId, + url = content.url ?: "", + data = data + ) + } else { + AttachmentInfo.Image( + uid = it.eventId, + url = content.url ?: "", + data = data + ) + } + } else if (content is MessageStickerContent) { + val data = ImageContentRenderer.Data( + eventId = it.eventId, + filename = content.body, + mimeType = content.mimeType, + url = content.getFileUrl(), + elementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt(), + maxHeight = -1, + maxWidth = -1, + width = null, + height = null, + allowNonMxcUrls = false + ) if (content.mimeType == MimeTypes.Gif) { AttachmentInfo.AnimatedImage( diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt index 4f16231747..7fa75d1544 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt @@ -22,63 +22,49 @@ import im.vector.app.features.login.LoginConfig import im.vector.app.features.login.ServerType import im.vector.app.features.login.SignMode import org.matrix.android.sdk.api.auth.data.Credentials -import org.matrix.android.sdk.api.auth.registration.RegisterThreePid import org.matrix.android.sdk.internal.network.ssl.Fingerprint -sealed class OnboardingAction : VectorViewModelAction { - data class OnGetStarted(val resetLoginConfig: Boolean, val onboardingFlow: OnboardingFlow) : OnboardingAction() - data class OnIAlreadyHaveAnAccount(val resetLoginConfig: Boolean, val onboardingFlow: OnboardingFlow) : OnboardingAction() +sealed interface OnboardingAction : VectorViewModelAction { + data class OnGetStarted(val resetLoginConfig: Boolean, val onboardingFlow: OnboardingFlow) : OnboardingAction + data class OnIAlreadyHaveAnAccount(val resetLoginConfig: Boolean, val onboardingFlow: OnboardingFlow) : OnboardingAction - data class UpdateServerType(val serverType: ServerType) : OnboardingAction() - data class UpdateHomeServer(val homeServerUrl: String) : OnboardingAction() - data class UpdateUseCase(val useCase: FtueUseCase) : OnboardingAction() - object ResetUseCase : OnboardingAction() - data class UpdateSignMode(val signMode: SignMode) : OnboardingAction() - data class LoginWithToken(val loginToken: String) : OnboardingAction() - data class WebLoginSuccess(val credentials: Credentials) : OnboardingAction() - data class InitWith(val loginConfig: LoginConfig?) : OnboardingAction() - data class ResetPassword(val email: String, val newPassword: String) : OnboardingAction() - object ResetPasswordMailConfirmed : OnboardingAction() + data class UpdateServerType(val serverType: ServerType) : OnboardingAction + data class UpdateHomeServer(val homeServerUrl: String) : OnboardingAction + data class UpdateUseCase(val useCase: FtueUseCase) : OnboardingAction + object ResetUseCase : OnboardingAction + data class UpdateSignMode(val signMode: SignMode) : OnboardingAction + data class LoginWithToken(val loginToken: String) : OnboardingAction + data class WebLoginSuccess(val credentials: Credentials) : OnboardingAction + data class InitWith(val loginConfig: LoginConfig?) : OnboardingAction + data class ResetPassword(val email: String, val newPassword: String) : OnboardingAction + object ResetPasswordMailConfirmed : OnboardingAction // Login or Register, depending on the signMode - data class LoginOrRegister(val username: String, val password: String, val initialDeviceName: String) : OnboardingAction() + data class LoginOrRegister(val username: String, val password: String, val initialDeviceName: String) : OnboardingAction + object StopEmailValidationCheck : OnboardingAction - // Register actions - open class RegisterAction : OnboardingAction() - - data class AddThreePid(val threePid: RegisterThreePid) : RegisterAction() - object SendAgainThreePid : RegisterAction() - - // TODO Confirm Email (from link in the email, open in the phone, intercepted by the app) - data class ValidateThreePid(val code: String) : RegisterAction() - - data class CheckIfEmailHasBeenValidated(val delayMillis: Long) : RegisterAction() - object StopEmailValidationCheck : RegisterAction() - - data class CaptchaDone(val captchaResponse: String) : RegisterAction() - object AcceptTerms : RegisterAction() - object RegisterDummy : RegisterAction() + data class PostRegisterAction(val registerAction: RegisterAction) : OnboardingAction // Reset actions - open class ResetAction : OnboardingAction() + sealed interface ResetAction : OnboardingAction - object ResetHomeServerType : ResetAction() - object ResetHomeServerUrl : ResetAction() - object ResetSignMode : ResetAction() - object ResetLogin : ResetAction() - object ResetResetPassword : ResetAction() + object ResetHomeServerType : ResetAction + object ResetHomeServerUrl : ResetAction + object ResetSignMode : ResetAction + object ResetLogin : ResetAction + object ResetResetPassword : ResetAction // Homeserver history - object ClearHomeServerHistory : OnboardingAction() + object ClearHomeServerHistory : OnboardingAction - data class PostViewEvent(val viewEvent: OnboardingViewEvents) : OnboardingAction() + data class PostViewEvent(val viewEvent: OnboardingViewEvents) : OnboardingAction - data class UserAcceptCertificate(val fingerprint: Fingerprint) : OnboardingAction() + data class UserAcceptCertificate(val fingerprint: Fingerprint) : OnboardingAction - object PersonalizeProfile : OnboardingAction() - data class UpdateDisplayName(val displayName: String) : OnboardingAction() - object UpdateDisplayNameSkipped : OnboardingAction() - data class ProfilePictureSelected(val uri: Uri) : OnboardingAction() - object SaveSelectedProfilePicture : OnboardingAction() - object UpdateProfilePictureSkipped : OnboardingAction() + object PersonalizeProfile : OnboardingAction + data class UpdateDisplayName(val displayName: String) : OnboardingAction + object UpdateDisplayNameSkipped : OnboardingAction + data class ProfilePictureSelected(val uri: Uri) : OnboardingAction + object SaveSelectedProfilePicture : OnboardingAction + object UpdateProfilePictureSkipped : OnboardingAction } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt index 36020fbe61..6659058b4e 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt @@ -83,6 +83,7 @@ class OnboardingViewModel @AssistedInject constructor( private val vectorFeatures: VectorFeatures, private val analyticsTracker: AnalyticsTracker, private val uriFilenameResolver: UriFilenameResolver, + private val registrationActionHandler: RegistrationActionHandler, private val vectorOverrides: VectorOverrides ) : VectorViewModel(initialState) { @@ -116,16 +117,16 @@ class OnboardingViewModel @AssistedInject constructor( private val matrixOrgUrl = stringProvider.getString(R.string.matrix_org_server_url).ensureTrailingSlash() + private val registrationWizard: RegistrationWizard + get() = authenticationService.getRegistrationWizard() + val currentThreePid: String? - get() = registrationWizard?.currentThreePid + get() = registrationWizard.currentThreePid // True when login and password has been sent with success to the homeserver val isRegistrationStarted: Boolean get() = authenticationService.isRegistrationStarted - private val registrationWizard: RegistrationWizard? - get() = authenticationService.getRegistrationWizard() - private val loginWizard: LoginWizard? get() = authenticationService.getLoginWizard() @@ -153,7 +154,7 @@ class OnboardingViewModel @AssistedInject constructor( is OnboardingAction.WebLoginSuccess -> handleWebLoginSuccess(action) is OnboardingAction.ResetPassword -> handleResetPassword(action) is OnboardingAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed() - is OnboardingAction.RegisterAction -> handleRegisterAction(action) + is OnboardingAction.PostRegisterAction -> handleRegisterAction(action.registerAction) is OnboardingAction.ResetAction -> handleResetAction(action) is OnboardingAction.UserAcceptCertificate -> handleUserAcceptCertificate(action) OnboardingAction.ClearHomeServerHistory -> handleClearHomeServerHistory() @@ -164,6 +165,7 @@ class OnboardingViewModel @AssistedInject constructor( is OnboardingAction.ProfilePictureSelected -> handleProfilePictureSelected(action) OnboardingAction.SaveSelectedProfilePicture -> updateProfilePicture() is OnboardingAction.PostViewEvent -> _viewEvents.post(action.viewEvent) + OnboardingAction.StopEmailValidationCheck -> cancelWaitForEmailValidation() }.exhaustive } @@ -266,131 +268,41 @@ class OnboardingViewModel @AssistedInject constructor( } } - private fun handleRegisterAction(action: OnboardingAction.RegisterAction) { - when (action) { - is OnboardingAction.CaptchaDone -> handleCaptchaDone(action) - is OnboardingAction.AcceptTerms -> handleAcceptTerms() - is OnboardingAction.RegisterDummy -> handleRegisterDummy() - is OnboardingAction.AddThreePid -> handleAddThreePid(action) - is OnboardingAction.SendAgainThreePid -> handleSendAgainThreePid() - is OnboardingAction.ValidateThreePid -> handleValidateThreePid(action) - is OnboardingAction.CheckIfEmailHasBeenValidated -> handleCheckIfEmailHasBeenValidated(action) - is OnboardingAction.StopEmailValidationCheck -> handleStopEmailValidationCheck() - } - } - - private fun handleCheckIfEmailHasBeenValidated(action: OnboardingAction.CheckIfEmailHasBeenValidated) { - // We do not want the common progress bar to be displayed, so we do not change asyncRegistration value in the state - currentJob = executeRegistrationStep(withLoading = false) { - it.checkIfEmailHasBeenValidated(action.delayMillis) - } - } - - private fun handleStopEmailValidationCheck() { - currentJob = null - } - - private fun handleValidateThreePid(action: OnboardingAction.ValidateThreePid) { - currentJob = executeRegistrationStep { - it.handleValidateThreePid(action.code) - } - } - - private fun executeRegistrationStep(withLoading: Boolean = true, - block: suspend (RegistrationWizard) -> RegistrationResult): Job { - if (withLoading) { - setState { copy(asyncRegistration = Loading()) } - } - return viewModelScope.launch { - try { - registrationWizard?.let { block(it) } - /* - // Simulate registration disabled - throw Failure.ServerError(MatrixError( - code = MatrixError.FORBIDDEN, - message = "Registration is disabled" - ), 403)) - */ - } catch (failure: Throwable) { - if (failure !is CancellationException) { - _viewEvents.post(OnboardingViewEvents.Failure(failure)) - } - null - } - ?.let { data -> - when (data) { - is RegistrationResult.Success -> onSessionCreated(data.session, isAccountCreated = true) - is RegistrationResult.FlowResponse -> onFlowResponse(data.flowResult) - } - } - - setState { - copy( - asyncRegistration = Uninitialized - ) - } - } - } - - private fun handleAddThreePid(action: OnboardingAction.AddThreePid) { - setState { copy(asyncRegistration = Loading()) } + private fun handleRegisterAction(action: RegisterAction) { currentJob = viewModelScope.launch { - try { - registrationWizard?.addThreePid(action.threePid) - } catch (failure: Throwable) { - _viewEvents.post(OnboardingViewEvents.Failure(failure)) + if (action.hasLoadingState()) { + setState { copy(asyncRegistration = Loading()) } } - setState { - copy( - asyncRegistration = Uninitialized - ) - } - } - } - - private fun handleSendAgainThreePid() { - setState { copy(asyncRegistration = Loading()) } - currentJob = viewModelScope.launch { - try { - registrationWizard?.sendAgainThreePid() - } catch (failure: Throwable) { - _viewEvents.post(OnboardingViewEvents.Failure(failure)) - } - setState { - copy( - asyncRegistration = Uninitialized - ) - } - } - } - - private fun handleAcceptTerms() { - currentJob = executeRegistrationStep { - it.acceptTerms() - } - } - - private fun handleRegisterDummy() { - currentJob = executeRegistrationStep { - it.dummy() + runCatching { registrationActionHandler.handleRegisterAction(registrationWizard, action) } + .fold( + onSuccess = { + when { + action.ignoresResult() -> { + // do nothing + } + else -> when (it) { + is RegistrationResult.Success -> onSessionCreated(it.session, isAccountCreated = true) + is RegistrationResult.FlowResponse -> onFlowResponse(it.flowResult) + } + } + }, + onFailure = { + if (it !is CancellationException) { + _viewEvents.post(OnboardingViewEvents.Failure(it)) + } + } + ) + setState { copy(asyncRegistration = Uninitialized) } } } private fun handleRegisterWith(action: OnboardingAction.LoginOrRegister) { reAuthHelper.data = action.password - currentJob = executeRegistrationStep { - it.createAccount( - action.username, - action.password, - action.initialDeviceName - ) - } - } - - private fun handleCaptchaDone(action: OnboardingAction.CaptchaDone) { - currentJob = executeRegistrationStep { - it.performReCaptcha(action.captchaResponse) - } + handleRegisterAction(RegisterAction.CreateAccount( + action.username, + action.password, + action.initialDeviceName + )) } private fun handleResetAction(action: OnboardingAction.ResetAction) { @@ -461,7 +373,7 @@ class OnboardingViewModel @AssistedInject constructor( } when (action.signMode) { - SignMode.SignUp -> startRegistrationFlow() + SignMode.SignUp -> handleRegisterAction(RegisterAction.StartRegistration) SignMode.SignIn -> startAuthenticationFlow() SignMode.SignInWithMatrixId -> _viewEvents.post(OnboardingViewEvents.OnSignModeSelected(SignMode.SignInWithMatrixId)) SignMode.Unknown -> Unit @@ -499,7 +411,7 @@ class OnboardingViewModel @AssistedInject constructor( // If there is a pending email validation continue on this step try { - if (registrationWizard?.isRegistrationStarted == true) { + if (registrationWizard.isRegistrationStarted) { currentThreePid?.let { handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnSendEmailSuccess(it))) } @@ -730,12 +642,6 @@ class OnboardingViewModel @AssistedInject constructor( } } - private fun startRegistrationFlow() { - currentJob = executeRegistrationStep { - it.getRegistrationFlow() - } - } - private fun startAuthenticationFlow() { // Ensure Wizard is ready loginWizard @@ -745,8 +651,7 @@ class OnboardingViewModel @AssistedInject constructor( private fun onFlowResponse(flowResult: FlowResult) { // If dummy stage is mandatory, and password is already sent, do the dummy stage now - if (isRegistrationStarted && - flowResult.missingStages.any { it is Stage.Dummy && it.mandatory }) { + if (isRegistrationStarted && flowResult.missingStages.any { it is Stage.Dummy && it.mandatory }) { handleRegisterDummy() } else { // Notify the user @@ -754,6 +659,10 @@ class OnboardingViewModel @AssistedInject constructor( } } + private fun handleRegisterDummy() { + handleRegisterAction(RegisterAction.RegisterDummy) + } + private suspend fun onSessionCreated(session: Session, isAccountCreated: Boolean) { val state = awaitState() state.useCase?.let { useCase -> @@ -1006,6 +915,10 @@ class OnboardingViewModel @AssistedInject constructor( private fun completePersonalization() { _viewEvents.post(OnboardingViewEvents.OnPersonalizationComplete) } + + private fun cancelWaitForEmailValidation() { + currentJob = null + } } private fun LoginMode.supportsSignModeScreen(): Boolean { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/RegistrationActionHandler.kt b/vector/src/main/java/im/vector/app/features/onboarding/RegistrationActionHandler.kt new file mode 100644 index 0000000000..b4998d2ba0 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/RegistrationActionHandler.kt @@ -0,0 +1,67 @@ +/* + * 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.onboarding + +import org.matrix.android.sdk.api.auth.registration.RegisterThreePid +import org.matrix.android.sdk.api.auth.registration.RegistrationResult +import org.matrix.android.sdk.api.auth.registration.RegistrationWizard +import javax.inject.Inject + +class RegistrationActionHandler @Inject constructor() { + + suspend fun handleRegisterAction(registrationWizard: RegistrationWizard, action: RegisterAction): RegistrationResult { + return when (action) { + RegisterAction.StartRegistration -> registrationWizard.getRegistrationFlow() + is RegisterAction.CaptchaDone -> registrationWizard.performReCaptcha(action.captchaResponse) + is RegisterAction.AcceptTerms -> registrationWizard.acceptTerms() + is RegisterAction.RegisterDummy -> registrationWizard.dummy() + is RegisterAction.AddThreePid -> registrationWizard.addThreePid(action.threePid) + is RegisterAction.SendAgainThreePid -> registrationWizard.sendAgainThreePid() + is RegisterAction.ValidateThreePid -> registrationWizard.handleValidateThreePid(action.code) + is RegisterAction.CheckIfEmailHasBeenValidated -> registrationWizard.checkIfEmailHasBeenValidated(action.delayMillis) + is RegisterAction.CreateAccount -> registrationWizard.createAccount(action.username, action.password, action.initialDeviceName) + } + } +} + +sealed interface RegisterAction { + object StartRegistration : RegisterAction + data class CreateAccount(val username: String, val password: String, val initialDeviceName: String) : RegisterAction + + data class AddThreePid(val threePid: RegisterThreePid) : RegisterAction + object SendAgainThreePid : RegisterAction + + // TODO Confirm Email (from link in the email, open in the phone, intercepted by the app) + data class ValidateThreePid(val code: String) : RegisterAction + + data class CheckIfEmailHasBeenValidated(val delayMillis: Long) : RegisterAction + + data class CaptchaDone(val captchaResponse: String) : RegisterAction + object AcceptTerms : RegisterAction + object RegisterDummy : RegisterAction +} + +fun RegisterAction.ignoresResult() = when (this) { + is RegisterAction.AddThreePid -> true + is RegisterAction.SendAgainThreePid -> true + else -> false +} + +fun RegisterAction.hasLoadingState() = when (this) { + is RegisterAction.CheckIfEmailHasBeenValidated -> false + else -> true +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCaptchaFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCaptchaFragment.kt index e2e390ae2d..4773332138 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCaptchaFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCaptchaFragment.kt @@ -39,6 +39,7 @@ import im.vector.app.databinding.FragmentLoginCaptchaBinding import im.vector.app.features.login.JavascriptResponse import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingViewState +import im.vector.app.features.onboarding.RegisterAction import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.internal.di.MoshiProvider import timber.log.Timber @@ -181,7 +182,7 @@ class FtueAuthCaptchaFragment @Inject constructor( val response = javascriptResponse?.response if (javascriptResponse?.action == "verifyCallback" && response != null) { - viewModel.handle(OnboardingAction.CaptchaDone(response)) + viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.CaptchaDone(response))) } } return true diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthGenericTextInputFormFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthGenericTextInputFormFragment.kt index bd5054f646..2800530152 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthGenericTextInputFormFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthGenericTextInputFormFragment.kt @@ -37,6 +37,7 @@ import im.vector.app.databinding.FragmentLoginGenericTextInputFormBinding import im.vector.app.features.login.TextInputFormFragmentMode import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingViewEvents +import im.vector.app.features.onboarding.RegisterAction import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.parcelize.Parcelize @@ -138,7 +139,7 @@ class FtueAuthGenericTextInputFormFragment @Inject constructor() : AbstractFtueA private fun onOtherButtonClicked() { when (params.mode) { TextInputFormFragmentMode.ConfirmMsisdn -> { - viewModel.handle(OnboardingAction.SendAgainThreePid) + viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.SendAgainThreePid)) } else -> { // Should not happen, button is not displayed @@ -152,19 +153,19 @@ class FtueAuthGenericTextInputFormFragment @Inject constructor() : AbstractFtueA if (text.isEmpty()) { // Perform dummy action - viewModel.handle(OnboardingAction.RegisterDummy) + viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.RegisterDummy)) } else { when (params.mode) { TextInputFormFragmentMode.SetEmail -> { - viewModel.handle(OnboardingAction.AddThreePid(RegisterThreePid.Email(text))) + viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.AddThreePid(RegisterThreePid.Email(text)))) } TextInputFormFragmentMode.SetMsisdn -> { getCountryCodeOrShowError(text)?.let { countryCode -> - viewModel.handle(OnboardingAction.AddThreePid(RegisterThreePid.Msisdn(text, countryCode))) + viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.AddThreePid(RegisterThreePid.Msisdn(text, countryCode)))) } } TextInputFormFragmentMode.ConfirmMsisdn -> { - viewModel.handle(OnboardingAction.ValidateThreePid(text)) + viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.ValidateThreePid(text))) } } } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWaitForEmailFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWaitForEmailFragment.kt index 94758c7fad..ec72f52b9e 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWaitForEmailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWaitForEmailFragment.kt @@ -25,6 +25,7 @@ import com.airbnb.mvrx.args import im.vector.app.R import im.vector.app.databinding.FragmentLoginWaitForEmailBinding import im.vector.app.features.onboarding.OnboardingAction +import im.vector.app.features.onboarding.RegisterAction import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.failure.is401 import javax.inject.Inject @@ -54,7 +55,7 @@ class FtueAuthWaitForEmailFragment @Inject constructor() : AbstractFtueAuthFragm override fun onResume() { super.onResume() - viewModel.handle(OnboardingAction.CheckIfEmailHasBeenValidated(0)) + viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.CheckIfEmailHasBeenValidated(0))) } override fun onPause() { @@ -70,7 +71,7 @@ class FtueAuthWaitForEmailFragment @Inject constructor() : AbstractFtueAuthFragm override fun onError(throwable: Throwable) { if (throwable.is401()) { // Try again, with a delay - viewModel.handle(OnboardingAction.CheckIfEmailHasBeenValidated(10_000)) + viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.CheckIfEmailHasBeenValidated(10_000))) } else { super.onError(throwable) } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/terms/FtueAuthTermsFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/terms/FtueAuthTermsFragment.kt index 5ce9a5350d..03598d3a47 100755 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/terms/FtueAuthTermsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/terms/FtueAuthTermsFragment.kt @@ -32,6 +32,7 @@ import im.vector.app.features.login.terms.LoginTermsViewState import im.vector.app.features.login.terms.PolicyController import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingViewState +import im.vector.app.features.onboarding.RegisterAction import im.vector.app.features.onboarding.ftueauth.AbstractFtueAuthFragment import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.internal.auth.registration.LocalizedFlowDataLoginTerms @@ -111,7 +112,7 @@ class FtueAuthTermsFragment @Inject constructor( } private fun submit() { - viewModel.handle(OnboardingAction.AcceptTerms) + viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.AcceptTerms)) } override fun updateWithState(state: OnboardingViewState) { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt index 251b99e318..b13ef2a5d1 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt @@ -54,6 +54,7 @@ import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedA import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.parcelize.Parcelize +import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState import org.matrix.android.sdk.api.util.toMatrixItem import timber.log.Timber @@ -67,7 +68,8 @@ data class RoomProfileArgs( class RoomProfileFragment @Inject constructor( private val roomProfileController: RoomProfileController, private val avatarRenderer: AvatarRenderer, - private val roomDetailPendingActionStore: RoomDetailPendingActionStore + private val roomDetailPendingActionStore: RoomDetailPendingActionStore, + private val matrixConfiguration: MatrixConfiguration ) : VectorBaseFragment(), RoomProfileController.Callback { @@ -222,7 +224,7 @@ class RoomProfileFragment @Inject constructor( avatarRenderer.render(matrixItem, views.matrixProfileToolbarAvatarImageView) headerViews.roomProfileDecorationImageView.render(it.roomEncryptionTrustLevel) views.matrixProfileDecorationToolbarAvatarImageView.render(it.roomEncryptionTrustLevel) - headerViews.roomProfilePresenceImageView.render(it.isDirect, it.directUserPresence) + headerViews.roomProfilePresenceImageView.render(it.isDirect && matrixConfiguration.presenceSyncEnabled, it.directUserPresence) headerViews.roomProfilePublicImageView.isVisible = it.isPublic && !it.isDirect } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt index 86ce25a809..6e0bb12642 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt @@ -27,6 +27,7 @@ import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.features.home.AvatarRenderer import me.gujun.android.span.span +import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary @@ -39,7 +40,8 @@ class RoomMemberListController @Inject constructor( private val avatarRenderer: AvatarRenderer, private val stringProvider: StringProvider, private val colorProvider: ColorProvider, - private val roomMemberSummaryFilter: RoomMemberSummaryFilter + private val roomMemberSummaryFilter: RoomMemberSummaryFilter, + private val matrixConfiguration: MatrixConfiguration ) : TypedEpoxyController() { interface Callback { @@ -122,6 +124,7 @@ class RoomMemberListController @Inject constructor( host: RoomMemberListController, data: RoomMemberListViewState) { val powerLabel = stringProvider.getString(powerLevelCategory.titleRes) + val presenceSyncEnabled = matrixConfiguration.presenceSyncEnabled profileMatrixItemWithPowerLevelWithPresence { id(roomMember.userId) @@ -131,6 +134,7 @@ class RoomMemberListController @Inject constructor( clickListener { host.callback?.onRoomMemberClicked(roomMember) } + showPresence(presenceSyncEnabled) userPresence(roomMember.userPresence) powerLevelLabel( span { diff --git a/vector/src/main/res/drawable/ic_presence_online.xml b/vector/src/main/res/drawable/ic_presence_online.xml index 2184f359b2..e5229de3fd 100644 --- a/vector/src/main/res/drawable/ic_presence_online.xml +++ b/vector/src/main/res/drawable/ic_presence_online.xml @@ -16,7 +16,7 @@ diff --git a/vector/src/main/res/layout/fragment_timeline.xml b/vector/src/main/res/layout/fragment_timeline.xml index 6ba326bcc4..14eac04db2 100644 --- a/vector/src/main/res/layout/fragment_timeline.xml +++ b/vector/src/main/res/layout/fragment_timeline.xml @@ -17,7 +17,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="48dp" - android:visibility="gone"/> + android:visibility="gone" /> + + + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/locationLiveStatusIndicator" /> + app:layout_constraintTop_toBottomOf="@id/removeJitsiWidgetView" /> + app:layout_constraintBottom_toTopOf="@id/composerLayout" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/timelineRecyclerView" + tools:visibility="visible" /> + tools:visibility="visible" /> + app:layout_constraintStart_toStartOf="parent" /> + + + + + + + + +