Merge remote-tracking branch 'origin/develop' into feature/eric/audio-files-player

# Conflicts:
#	vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
This commit is contained in:
ericdecanini 2022-03-21 19:40:14 +01:00
commit 5a819bbafa
60 changed files with 1016 additions and 367 deletions

View File

@ -265,6 +265,7 @@ jobs:
failure_screenshots/ failure_screenshots/
codecov-units: codecov-units:
name: Unit tests with code coverage
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@ -290,6 +291,7 @@ jobs:
build/reports/jacoco/allCodeCoverageReport/allCodeCoverageReport.xml build/reports/jacoco/allCodeCoverageReport/allCodeCoverageReport.xml
sonarqube: sonarqube:
name: Sonarqube upload
runs-on: macos-latest runs-on: macos-latest
if: always() if: always()
needs: needs:
@ -319,6 +321,7 @@ jobs:
# Notify the channel about scheduled runs, do not notify for manually triggered runs # Notify the channel about scheduled runs, do not notify for manually triggered runs
notify: notify:
name: Notify matrix
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- integration-tests - integration-tests
@ -333,4 +336,4 @@ jobs:
matrix_access_token: ${{ secrets.ELEMENT_ANDROID_NOTIFICATION_ACCESS_TOKEN }} matrix_access_token: ${{ secrets.ELEMENT_ANDROID_NOTIFICATION_ACCESS_TOKEN }}
matrix_room_id: ${{ secrets.ELEMENT_ANDROID_INTERNAL_ROOM_ID }} 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}}" 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}}" html_template: "Nightly test run results: {{#each job_statuses }}{{#with this }}{{#if completed }}<br />{{icon conclusion}} {{name}} <font color='{{color conclusion}}'>{{conclusion}} at {{completed_at}} <a href=\"{{html_url}}\">[details]</a></font>{{/if}}{{/with}}{{/each}}"

1
changelog.d/5408.misc Normal file
View File

@ -0,0 +1 @@
Improved onboarding registration unit test coverage

1
changelog.d/5513.misc Normal file
View File

@ -0,0 +1 @@
Added online presence indicator attribute online to match offline styling

1
changelog.d/5536.feature Normal file
View File

@ -0,0 +1 @@
Live location sharing: adding build config field and show permission dialog

1
changelog.d/5551.bugfix Normal file
View File

@ -0,0 +1 @@
Fix local echos not being shown when re-opening rooms

1
changelog.d/5552.bugfix Normal file
View File

@ -0,0 +1 @@
Fix crash when closing a room while decrypting timeline events

1
changelog.d/5563.misc Normal file
View File

@ -0,0 +1 @@
Add a presence sync enabling build config

1
changelog.d/5571.feature Normal file
View File

@ -0,0 +1 @@
Live location sharing: Adding indicator view when enabled

2
changelog.d/5572.misc Normal file
View File

@ -0,0 +1,2 @@
Show stickers on click

View File

@ -122,6 +122,10 @@
<color name="vctr_presence_indicator_offline_light">@color/palette_gray_100</color> <color name="vctr_presence_indicator_offline_light">@color/palette_gray_100</color>
<color name="vctr_presence_indicator_offline_dark">@color/palette_gray_450</color> <color name="vctr_presence_indicator_offline_dark">@color/palette_gray_450</color>
<attr name="vctr_presence_indicator_online" format="color" />
<color name="vctr_presence_indicator_online_light">@color/palette_element_green</color>
<color name="vctr_presence_indicator_online_dark">@color/palette_element_green</color>
<!-- Location sharing colors --> <!-- Location sharing colors -->
<attr name="vctr_live_location" format="color" /> <attr name="vctr_live_location" format="color" />
<color name="vctr_live_location_light">@color/palette_prune</color> <color name="vctr_live_location_light">@color/palette_prune</color>

View File

@ -53,5 +53,4 @@
<color name="element_room_01">@color/palette_verde</color> <color name="element_room_01">@color/palette_verde</color>
<color name="element_room_02">@color/palette_azure</color> <color name="element_room_02">@color/palette_azure</color>
<color name="element_room_03">@color/palette_grape</color> <color name="element_room_03">@color/palette_grape</color>
</resources> </resources>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Widget.Vector.Button.Text.OnPrimary.LocationLive">
<item name="android:background">?selectableItemBackground</item>
<item name="android:textSize">12sp</item>
<item name="android:padding">0dp</item>
<item name="android:gravity">center</item>
</style>
</resources>

View File

@ -43,6 +43,7 @@
<!-- Presence Indicator colors --> <!-- Presence Indicator colors -->
<item name="vctr_presence_indicator_offline">@color/vctr_presence_indicator_offline_dark</item> <item name="vctr_presence_indicator_offline">@color/vctr_presence_indicator_offline_dark</item>
<item name="vctr_presence_indicator_online">@color/vctr_presence_indicator_online_dark</item>
<!-- Some aliases --> <!-- Some aliases -->
<item name="vctr_header_background">?vctr_system</item> <item name="vctr_header_background">?vctr_system</item>

View File

@ -43,6 +43,7 @@
<!-- Presence Indicator colors --> <!-- Presence Indicator colors -->
<item name="vctr_presence_indicator_offline">@color/vctr_presence_indicator_offline_light</item> <item name="vctr_presence_indicator_offline">@color/vctr_presence_indicator_offline_light</item>
<item name="vctr_presence_indicator_online">@color/vctr_presence_indicator_online_light</item>
<!-- Some aliases --> <!-- Some aliases -->
<item name="vctr_header_background">?vctr_system</item> <item name="vctr_header_background">?vctr_system</item>

View File

@ -60,7 +60,11 @@ data class MatrixConfiguration(
/** /**
* RoomDisplayNameFallbackProvider to provide default room display name. * 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
) { ) {
/** /**

View File

@ -55,6 +55,7 @@ internal class RealmSendingEventsDataSource(
roomEntity = RoomEntity.where(safeRealm, roomId = roomId).findFirst() roomEntity = RoomEntity.where(safeRealm, roomId = roomId).findFirst()
sendingTimelineEvents = roomEntity?.sendingTimelineEvents sendingTimelineEvents = roomEntity?.sendingTimelineEvents
sendingTimelineEvents?.addChangeListener(sendingTimelineEventsListener) sendingTimelineEvents?.addChangeListener(sendingTimelineEventsListener)
updateFrozenResults(sendingTimelineEvents)
} }
override fun stop() { override fun stop() {

View File

@ -100,9 +100,13 @@ internal class TimelineEventDecryptor @Inject constructor(
} }
executor?.execute { executor?.execute {
Realm.getInstance(realmConfiguration).use { realm -> Realm.getInstance(realmConfiguration).use { realm ->
try {
runBlocking { runBlocking {
processDecryptRequest(request, realm) processDecryptRequest(request, realm)
} }
} catch (e: InterruptedException) {
Timber.i("Decryption got interrupted")
}
} }
} }
} }

View File

@ -17,6 +17,7 @@
package org.matrix.android.sdk.internal.session.sync.handler package org.matrix.android.sdk.internal.session.sync.handler
import io.realm.Realm 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.EventType
import org.matrix.android.sdk.api.session.events.model.getPresenceContent import org.matrix.android.sdk.api.session.events.model.getPresenceContent
import org.matrix.android.sdk.api.session.sync.model.PresenceSyncResponse import org.matrix.android.sdk.api.session.sync.model.PresenceSyncResponse
@ -27,9 +28,10 @@ import org.matrix.android.sdk.internal.database.query.updateDirectUserPresence
import org.matrix.android.sdk.internal.database.query.updateUserPresence import org.matrix.android.sdk.internal.database.query.updateUserPresence
import javax.inject.Inject import javax.inject.Inject
internal class PresenceSyncHandler @Inject constructor() { internal class PresenceSyncHandler @Inject constructor(private val matrixConfiguration: MatrixConfiguration) {
fun handle(realm: Realm, presenceSyncResponse: PresenceSyncResponse?) { fun handle(realm: Realm, presenceSyncResponse: PresenceSyncResponse?) {
if (matrixConfiguration.presenceSyncEnabled) {
presenceSyncResponse?.events presenceSyncResponse?.events
?.filter { event -> event.type == EventType.PRESENCE } ?.filter { event -> event.type == EventType.PRESENCE }
?.forEach { event -> ?.forEach { event ->
@ -49,6 +51,7 @@ internal class PresenceSyncHandler @Inject constructor() {
storePresenceToDB(realm, userPresenceEntity) storePresenceToDB(realm, userPresenceEntity)
} }
} }
}
/** /**
* Store user presence to DB and update Direct Rooms and Room Member Summaries accordingly * Store user presence to DB and update Direct Rooms and Room Member Summaries accordingly

View File

@ -13,6 +13,7 @@ print("::group::Arguments")
print(f"{sys.argv}") print(f"{sys.argv}")
print("::endgroup::") print("::endgroup::")
for xmlfile in xmlfiles: for xmlfile in xmlfiles:
try:
tree = ET.parse(xmlfile) tree = ET.parse(xmlfile)
root = tree.getroot() root = tree.getroot()
@ -40,5 +41,7 @@ for xmlfile in xmlfiles:
print(child.text) print(child.text)
body = f" passed={success} failures={failures} errors={errors} skipped={skipped}" body = f" passed={success} failures={failures} errors={errors} skipped={skipped}"
print(f"::set-output name={suitename}::={body}") 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::") print("::endgroup::")

View File

@ -2056,7 +2056,9 @@
"disappear", "disappear",
"dissolve", "dissolve",
"liquid", "liquid",
"melt" "melt",
"hot",
"heat"
] ]
}, },
"winking-face": { "winking-face": {
@ -2351,7 +2353,10 @@
"disbelief", "disbelief",
"embarrass", "embarrass",
"scared", "scared",
"surprise" "surprise",
"silence",
"secret",
"shock"
] ]
}, },
"face-with-peeking-eye": { "face-with-peeking-eye": {
@ -2360,7 +2365,10 @@
"j": [ "j": [
"captivated", "captivated",
"peep", "peep",
"stare" "stare",
"scared",
"frightening",
"embarrassing"
] ]
}, },
"shushing-face": { "shushing-face": {
@ -2392,7 +2400,8 @@
"salute", "salute",
"sunny", "sunny",
"troops", "troops",
"yes" "yes",
"respect"
] ]
}, },
"zippermouth-face": { "zippermouth-face": {
@ -2467,7 +2476,10 @@
"disappear", "disappear",
"hide", "hide",
"introvert", "introvert",
"invisible" "invisible",
"lonely",
"isolation",
"depression"
] ]
}, },
"face-in-clouds": { "face-in-clouds": {
@ -2863,7 +2875,11 @@
"disappointed", "disappointed",
"meh", "meh",
"skeptical", "skeptical",
"unsure" "unsure",
"skeptic",
"confuse",
"frustrated",
"indifferent"
] ]
}, },
"worried-face": { "worried-face": {
@ -2969,7 +2985,9 @@
"cry", "cry",
"proud", "proud",
"resist", "resist",
"sad" "sad",
"touched",
"gratitude"
] ]
}, },
"frowning-face-with-open-mouth": { "frowning-face-with-open-mouth": {
@ -4065,7 +4083,9 @@
"j": [ "j": [
"hand", "hand",
"right", "right",
"rightward" "rightward",
"palm",
"offer"
] ]
}, },
"leftwards-hand": { "leftwards-hand": {
@ -4074,7 +4094,9 @@
"j": [ "j": [
"hand", "hand",
"left", "left",
"leftward" "leftward",
"palm",
"offer"
] ]
}, },
"palm-down-hand": { "palm-down-hand": {
@ -4083,7 +4105,8 @@
"j": [ "j": [
"dismiss", "dismiss",
"drop", "drop",
"shoo" "shoo",
"palm"
] ]
}, },
"palm-up-hand": { "palm-up-hand": {
@ -4093,7 +4116,9 @@
"beckon", "beckon",
"catch", "catch",
"come", "come",
"offer" "offer",
"lift",
"demand"
] ]
}, },
"ok-hand": { "ok-hand": {
@ -4290,7 +4315,8 @@
"b": "1FAF5", "b": "1FAF5",
"j": [ "j": [
"point", "point",
"you" "you",
"recruit"
] ]
}, },
"thumbs-up": { "thumbs-up": {
@ -4404,7 +4430,9 @@
"a": "⊛ Heart Hands", "a": "⊛ Heart Hands",
"b": "1FAF6", "b": "1FAF6",
"j": [ "j": [
"love" "love",
"appreciation",
"support"
] ]
}, },
"open-hands": { "open-hands": {
@ -4662,7 +4690,11 @@
"flirting", "flirting",
"nervous", "nervous",
"uncomfortable", "uncomfortable",
"worried" "worried",
"flirt",
"sexy",
"pain",
"worry"
] ]
}, },
"baby": { "baby": {
@ -6058,7 +6090,8 @@
"monarch", "monarch",
"noble", "noble",
"regal", "regal",
"royalty" "royalty",
"power"
] ]
}, },
"prince": { "prince": {
@ -6231,7 +6264,8 @@
"belly", "belly",
"bloated", "bloated",
"full", "full",
"pregnant" "pregnant",
"baby"
] ]
}, },
"pregnant-person": { "pregnant-person": {
@ -6241,7 +6275,8 @@
"belly", "belly",
"bloated", "bloated",
"full", "full",
"pregnant" "pregnant",
"baby"
] ]
}, },
"breastfeeding": { "breastfeeding": {
@ -6635,7 +6670,8 @@
"j": [ "j": [
"fairy tale", "fairy tale",
"fantasy", "fantasy",
"monster" "monster",
"mystical"
] ]
}, },
"person-getting-massage": { "person-getting-massage": {
@ -9374,7 +9410,8 @@
"b": "1FAB8", "b": "1FAB8",
"j": [ "j": [
"ocean", "ocean",
"reef" "reef",
"sea"
] ]
}, },
"snail": { "snail": {
@ -9587,7 +9624,9 @@
"Hinduism", "Hinduism",
"India", "India",
"purity", "purity",
"Vietnam" "Vietnam",
"calm",
"meditation"
] ]
}, },
"rosette": { "rosette": {
@ -9832,14 +9871,16 @@
"a": "⊛ Empty Nest", "a": "⊛ Empty Nest",
"b": "1FAB9", "b": "1FAB9",
"j": [ "j": [
"nesting" "nesting",
"bird"
] ]
}, },
"nest-with-eggs": { "nest-with-eggs": {
"a": "⊛ Nest with Eggs", "a": "⊛ Nest with Eggs",
"b": "1FABA", "b": "1FABA",
"j": [ "j": [
"nesting" "nesting",
"bird"
] ]
}, },
"grapes": { "grapes": {
@ -11187,7 +11228,9 @@
"drink", "drink",
"empty", "empty",
"glass", "glass",
"spill" "spill",
"cup",
"water"
] ]
}, },
"cup-with-straw": { "cup-with-straw": {
@ -12003,7 +12046,9 @@
"b": "1F6DD", "b": "1F6DD",
"j": [ "j": [
"amusement park", "amusement park",
"play" "play",
"fun",
"park"
] ]
}, },
"ferris-wheel": { "ferris-wheel": {
@ -12533,7 +12578,9 @@
"j": [ "j": [
"circle", "circle",
"tire", "tire",
"turn" "turn",
"car",
"transport"
] ]
}, },
"police-car-light": { "police-car-light": {
@ -14666,7 +14713,8 @@
"hand", "hand",
"Mary", "Mary",
"Miriam", "Miriam",
"protection" "protection",
"religion"
] ]
}, },
"video-game": { "video-game": {
@ -15864,7 +15912,9 @@
"b": "1FAAB", "b": "1FAAB",
"j": [ "j": [
"electronic", "electronic",
"low energy" "low energy",
"drained",
"dead"
] ]
}, },
"electric-plug": { "electric-plug": {
@ -17508,7 +17558,9 @@
"disability", "disability",
"hurt", "hurt",
"mobility aid", "mobility aid",
"stick" "stick",
"accessibility",
"assist"
] ]
}, },
"stethoscope": { "stethoscope": {
@ -17528,7 +17580,9 @@
"bones", "bones",
"doctor", "doctor",
"medical", "medical",
"skeleton" "skeleton",
"x-ray",
"medicine"
] ]
}, },
"door": { "door": {
@ -17733,7 +17787,10 @@
"burp", "burp",
"clean", "clean",
"soap", "soap",
"underwater" "underwater",
"fun",
"carbonation",
"sparkling"
] ]
}, },
"toothbrush": { "toothbrush": {
@ -17856,7 +17913,8 @@
"credentials", "credentials",
"ID", "ID",
"license", "license",
"security" "security",
"document"
] ]
}, },
"atm-sign": { "atm-sign": {

View File

@ -151,6 +151,7 @@ android {
buildConfigField "Boolean", "enableLocationSharing", "true" buildConfigField "Boolean", "enableLocationSharing", "true"
buildConfigField "String", "mapTilerKey", "\"fU3vlMsMn4Jb6dnEIFsx\"" buildConfigField "String", "mapTilerKey", "\"fU3vlMsMn4Jb6dnEIFsx\""
buildConfigField "Boolean", "PRESENCE_SYNC_ENABLED", "true"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@ -229,6 +230,7 @@ android {
buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false" buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false"
// Set to true if you want to enable strict mode in debug // Set to true if you want to enable strict mode in debug
buildConfigField "boolean", "ENABLE_STRICT_MODE_LOGS", "false" buildConfigField "boolean", "ENABLE_STRICT_MODE_LOGS", "false"
buildConfigField "Boolean", "ENABLE_LIVE_LOCATION_SHARING", "true"
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
} }
@ -238,6 +240,7 @@ android {
buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false" buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false"
buildConfigField "boolean", "ENABLE_STRICT_MODE_LOGS", "false" buildConfigField "boolean", "ENABLE_STRICT_MODE_LOGS", "false"
buildConfigField "Boolean", "ENABLE_LIVE_LOCATION_SHARING", "false"
postprocessing { postprocessing {
removeUnusedCode true removeUnusedCode true

View File

@ -45,6 +45,7 @@
<!-- Location Sharing --> <!-- Location Sharing -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!-- Jitsi SDK is now API23+ --> <!-- Jitsi SDK is now API23+ -->
<uses-sdk tools:overrideLibrary="org.jitsi.meet.sdk,com.oney.WebRTCModule,com.learnium.RNDeviceInfo,com.reactnativecommunity.asyncstorage,com.ocetnik.timer,com.calendarevents,com.reactnativecommunity.netinfo,com.kevinresol.react_native_default_preference,com.rnimmersive,com.corbt.keepawake,com.BV.LinearGradient,com.horcrux.svg,com.oblador.performance,com.reactnativecommunity.slider,com.brentvatne.react" /> <uses-sdk tools:overrideLibrary="org.jitsi.meet.sdk,com.oney.WebRTCModule,com.learnium.RNDeviceInfo,com.reactnativecommunity.asyncstorage,com.ocetnik.timer,com.calendarevents,com.reactnativecommunity.netinfo,com.kevinresol.react_native_default_preference,com.rnimmersive,com.corbt.keepawake,com.BV.LinearGradient,com.horcrux.svg,com.oblador.performance,com.reactnativecommunity.slider,com.brentvatne.react" />

View File

@ -116,7 +116,8 @@ object VectorStaticModule {
fun providesMatrixConfiguration(vectorRoomDisplayNameFallbackProvider: VectorRoomDisplayNameFallbackProvider): MatrixConfiguration { fun providesMatrixConfiguration(vectorRoomDisplayNameFallbackProvider: VectorRoomDisplayNameFallbackProvider): MatrixConfiguration {
return MatrixConfiguration( return MatrixConfiguration(
applicationFlavor = BuildConfig.FLAVOR_DESCRIPTION, applicationFlavor = BuildConfig.FLAVOR_DESCRIPTION,
roomDisplayNameFallbackProvider = vectorRoomDisplayNameFallbackProvider roomDisplayNameFallbackProvider = vectorRoomDisplayNameFallbackProvider,
presenceSyncEnabled = BuildConfig.PRESENCE_SYNC_ENABLED
) )
} }

View File

@ -25,10 +25,11 @@ import org.matrix.android.sdk.api.session.presence.model.UserPresence
@EpoxyModelClass(layout = R.layout.item_profile_matrix_item) @EpoxyModelClass(layout = R.layout.item_profile_matrix_item)
abstract class ProfileMatrixItemWithPowerLevelWithPresence : ProfileMatrixItemWithPowerLevel() { abstract class ProfileMatrixItemWithPowerLevelWithPresence : ProfileMatrixItemWithPowerLevel() {
@EpoxyAttribute var showPresence: Boolean = true
@EpoxyAttribute var userPresence: UserPresence? = null @EpoxyAttribute var userPresence: UserPresence? = null
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
holder.presenceImageView.render(userPresence = userPresence) holder.presenceImageView.render(showPresence, userPresence)
} }
} }

View File

@ -19,6 +19,7 @@ package im.vector.app.core.utils
import android.Manifest import android.Manifest
import android.app.Activity import android.app.Activity
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
@ -32,6 +33,7 @@ import im.vector.app.R
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
// Permissions sets // Permissions sets
val PERMISSIONS_EMPTY = emptyList<String>()
val PERMISSIONS_FOR_AUDIO_IP_CALL = listOf(Manifest.permission.RECORD_AUDIO) 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_VIDEO_IP_CALL = listOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
val PERMISSIONS_FOR_VOICE_MESSAGE = listOf(Manifest.permission.RECORD_AUDIO) 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_ROOM_AVATAR = listOf(Manifest.permission.CAMERA)
val PERMISSIONS_FOR_WRITING_FILES = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE) val PERMISSIONS_FOR_WRITING_FILES = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)
val PERMISSIONS_FOR_PICKING_CONTACT = listOf(Manifest.permission.READ_CONTACTS) 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_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) {
val PERMISSIONS_EMPTY = emptyList<String>() listOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
} else {
PERMISSIONS_EMPTY
}
// This is not ideal to store the value like that, but it works // This is not ideal to store the value like that, but it works
private var permissionDialogDisplayed = false private var permissionDialogDisplayed = false
@ -123,6 +128,7 @@ fun checkPermissions(permissionsToBeGranted: List<String>,
.setPositiveButton(R.string.ok) { _, _ -> .setPositiveButton(R.string.ok) { _, _ ->
activityResultLauncher.launch(missingPermissions.toTypedArray()) activityResultLauncher.launch(missingPermissions.toTypedArray())
} }
.setNegativeButton(R.string.action_not_now, null)
.show() .show()
} else { } else {
// some permissions are not granted, ask permissions // some permissions are not granted, ask permissions

View File

@ -37,7 +37,7 @@ import androidx.core.view.isVisible
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.onClick import im.vector.app.core.epoxy.onClick
import im.vector.app.core.utils.PERMISSIONS_EMPTY 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_PICKING_CONTACT
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.app.databinding.ViewAttachmentTypeSelectorBinding import im.vector.app.databinding.ViewAttachmentTypeSelectorBinding
@ -215,6 +215,6 @@ class AttachmentTypeSelectorView(context: Context,
STICKER(PERMISSIONS_EMPTY, R.string.tooltip_attachment_sticker), STICKER(PERMISSIONS_EMPTY, R.string.tooltip_attachment_sticker),
CONTACT(PERMISSIONS_FOR_PICKING_CONTACT, R.string.tooltip_attachment_contact), CONTACT(PERMISSIONS_FOR_PICKING_CONTACT, R.string.tooltip_attachment_contact),
POLL(PERMISSIONS_EMPTY, R.string.tooltip_attachment_poll), 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)
} }
} }

View File

@ -177,6 +177,7 @@ import im.vector.app.features.html.PillsPostProcessor
import im.vector.app.features.invite.VectorInviteView import im.vector.app.features.invite.VectorInviteView
import im.vector.app.features.location.LocationSharingMode import im.vector.app.features.location.LocationSharingMode
import im.vector.app.features.location.toLocationData 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.ImageContentRenderer
import im.vector.app.features.media.VideoContentRenderer import im.vector.app.features.media.VideoContentRenderer
import im.vector.app.features.notifications.NotificationDrawerManager import im.vector.app.features.notifications.NotificationDrawerManager
@ -206,6 +207,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.billcarsonfr.jsonviewer.JSonViewerDialog import org.billcarsonfr.jsonviewer.JSonViewerDialog
import org.commonmark.parser.Parser 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.Session
import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import org.matrix.android.sdk.api.session.events.model.EventType 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 pillsPostProcessorFactory: PillsPostProcessor.Factory,
private val callManager: WebRtcCallManager, private val callManager: WebRtcCallManager,
private val audioMessagePlaybackTracker: AudioMessagePlaybackTracker, private val audioMessagePlaybackTracker: AudioMessagePlaybackTracker,
private val clock: Clock private val clock: Clock,
private val matrixConfiguration: MatrixConfiguration
) : ) :
VectorBaseFragment<FragmentTimelineBinding>(), VectorBaseFragment<FragmentTimelineBinding>(),
TimelineEventController.Callback, TimelineEventController.Callback,
@ -1163,7 +1166,6 @@ class TimelineFragment @Inject constructor(
views.composerLayout.views.sendButton.contentDescription = getString(R.string.action_send) views.composerLayout.views.sendButton.contentDescription = getString(R.string.action_send)
} }
// TODO: Test this
private fun renderSpecialMode(event: TimelineEvent, private fun renderSpecialMode(event: TimelineEvent,
@DrawableRes iconRes: Int, @DrawableRes iconRes: Int,
@StringRes descriptionRes: Int, @StringRes descriptionRes: Int,
@ -1627,7 +1629,10 @@ class TimelineFragment @Inject constructor(
views.includeRoomToolbar.roomToolbarTitleView.text = roomSummary.displayName views.includeRoomToolbar.roomToolbarTitleView.text = roomSummary.displayName
avatarRenderer.render(roomSummary.toMatrixItem(), views.includeRoomToolbar.roomToolbarAvatarImageView) avatarRenderer.render(roomSummary.toMatrixItem(), views.includeRoomToolbar.roomToolbarAvatarImageView)
views.includeRoomToolbar.roomToolbarDecorationImageView.render(roomSummary.roomEncryptionTrustLevel) 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 views.includeRoomToolbar.roomToolbarPublicImageView.isVisible = roomSummary.isPublic && !roomSummary.isDirect
} }
} else { } else {
@ -1882,12 +1887,16 @@ class TimelineFragment @Inject constructor(
vectorBaseActivity.notImplemented("encrypted message click") 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<AttachmentData>) {
navigator.openMediaViewer( navigator.openMediaViewer(
activity = requireActivity(), activity = requireActivity(),
roomId = timelineArgs.roomId, roomId = timelineArgs.roomId,
mediaData = mediaData, mediaData = mediaData,
view = view view = view,
inMemory = inMemory
) { pairs -> ) { pairs ->
pairs.add(Pair(views.roomToolbar, ViewCompat.getTransitionName(views.roomToolbar) ?: "")) pairs.add(Pair(views.roomToolbar, ViewCompat.getTransitionName(views.roomToolbar) ?: ""))
pairs.add(Pair(views.composerLayout, ViewCompat.getTransitionName(views.composerLayout) ?: "")) pairs.add(Pair(views.composerLayout, ViewCompat.getTransitionName(views.composerLayout) ?: ""))

View File

@ -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.ReadReceiptData
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem 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.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.ImageContentRenderer
import im.vector.app.features.media.VideoContentRenderer import im.vector.app.features.media.VideoContentRenderer
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
@ -127,7 +128,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
fun onEventVisible(event: TimelineEvent) fun onEventVisible(event: TimelineEvent)
fun onRoomCreateLinkClicked(url: String) fun onRoomCreateLinkClicked(url: String)
fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View) 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<AttachmentData>)
fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View)
// fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) // fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent)

View File

@ -474,9 +474,12 @@ class MessageItemFactory @Inject constructor(
.apply { .apply {
if (messageContent.msgType == MessageType.MSGTYPE_STICKER_LOCAL) { if (messageContent.msgType == MessageType.MSGTYPE_STICKER_LOCAL) {
mode(ImageContentRenderer.Mode.STICKER) mode(ImageContentRenderer.Mode.STICKER)
clickListener { view ->
callback?.onImageMessageClicked(messageContent, data, view, listOf(data))
}
} else { } else {
clickListener { view -> clickListener { view ->
callback?.onImageMessageClicked(messageContent, data, view) callback?.onImageMessageClicked(messageContent, data, view, emptyList())
} }
} }
} }

View File

@ -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.detail.timeline.format.DisplayableEventFormatter
import im.vector.app.features.home.room.typing.TypingHelper import im.vector.app.features.home.room.typing.TypingHelper
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence 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.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomSummary 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 stringProvider: StringProvider,
private val typingHelper: TypingHelper, private val typingHelper: TypingHelper,
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val errorFormatter: ErrorFormatter) { private val errorFormatter: ErrorFormatter,
private val matrixConfiguration: MatrixConfiguration) {
fun create(roomSummary: RoomSummary, fun create(roomSummary: RoomSummary,
roomChangeMembershipStates: Map<String, ChangeMembershipState>, roomChangeMembershipStates: Map<String, ChangeMembershipState>,
@ -125,7 +127,7 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
// We do not display shield in the room list anymore // We do not display shield in the room list anymore
// .encryptionTrustLevel(roomSummary.roomEncryptionTrustLevel) // .encryptionTrustLevel(roomSummary.roomEncryptionTrustLevel)
.izPublic(roomSummary.isPublic) .izPublic(roomSummary.isPublic)
.showPresence(roomSummary.isDirect) .showPresence(roomSummary.isDirect && matrixConfiguration.presenceSyncEnabled)
.userPresence(roomSummary.directUserPresence) .userPresence(roomSummary.directUserPresence)
.matrixItem(roomSummary.toMatrixItem()) .matrixItem(roomSummary.toMatrixItem())
.lastEventTime(latestEventTime) .lastEventTime(latestEventTime)

View File

@ -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)
}
}
}

View File

@ -23,4 +23,5 @@ sealed class LocationSharingAction : VectorViewModelAction {
data class PinnedLocationSharing(val locationData: LocationData?) : LocationSharingAction() data class PinnedLocationSharing(val locationData: LocationData?) : LocationSharingAction()
data class LocationTargetChange(val locationData: LocationData) : LocationSharingAction() data class LocationTargetChange(val locationData: LocationData) : LocationSharingAction()
object ZoomToUserLocation : LocationSharingAction() object ZoomToUserLocation : LocationSharingAction()
object StartLiveLocationSharing : LocationSharingAction()
} }

View File

@ -27,9 +27,14 @@ import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.mapbox.mapboxsdk.maps.MapView import com.mapbox.mapboxsdk.maps.MapView
import im.vector.app.BuildConfig
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorBaseFragment 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.databinding.FragmentLocationSharingBinding
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider 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 viewModel: LocationSharingViewModel by fragmentViewModel()
private val locationSharingNavigator: LocationSharingNavigator by lazy { DefaultLocationSharingNavigator(activity) }
// Keep a ref to handle properly the onDestroy callback // Keep a ref to handle properly the onDestroy callback
private var mapView: WeakReference<MapView>? = null private var mapView: WeakReference<MapView>? = null
@ -76,8 +83,8 @@ class LocationSharingFragment @Inject constructor(
viewModel.observeViewEvents { viewModel.observeViewEvents {
when (it) { when (it) {
LocationSharingViewEvents.Close -> locationSharingNavigator.quit()
LocationSharingViewEvents.LocationNotAvailableError -> handleLocationNotAvailableError() LocationSharingViewEvents.LocationNotAvailableError -> handleLocationNotAvailableError()
LocationSharingViewEvents.Close -> activity?.finish()
is LocationSharingViewEvents.ZoomToUserLocation -> handleZoomToUserLocationEvent(it) is LocationSharingViewEvents.ZoomToUserLocation -> handleZoomToUserLocationEvent(it)
}.exhaustive }.exhaustive
} }
@ -86,6 +93,11 @@ class LocationSharingFragment @Inject constructor(
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
views.mapView.onResume() views.mapView.onResume()
if (locationSharingNavigator.goingToAppSettings) {
locationSharingNavigator.goingToAppSettings = false
// retry to start live location
tryStartLiveLocationSharing()
}
} }
override fun onPause() { override fun onPause() {
@ -137,12 +149,24 @@ class LocationSharingFragment @Inject constructor(
.setTitle(R.string.location_not_available_dialog_title) .setTitle(R.string.location_not_available_dialog_title)
.setMessage(R.string.location_not_available_dialog_content) .setMessage(R.string.location_not_available_dialog_content)
.setPositiveButton(R.string.ok) { _, _ -> .setPositiveButton(R.string.ok) { _, _ ->
activity?.finish() locationSharingNavigator.quit()
} }
.setCancelable(false) .setCancelable(false)
.show() .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() { private fun initLocateButton() {
views.mapView.locateButton.setOnClickListener { views.mapView.locateButton.setOnClickListener {
viewModel.handle(LocationSharingAction.ZoomToUserLocation) viewModel.handle(LocationSharingAction.ZoomToUserLocation)
@ -164,22 +188,58 @@ class LocationSharingFragment @Inject constructor(
viewModel.handle(LocationSharingAction.CurrentUserLocationSharing) viewModel.handle(LocationSharingAction.CurrentUserLocationSharing)
} }
views.shareLocationOptionsPicker.optionUserLive.debouncedClicks { 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) { private fun updateMap(state: LocationSharingViewState) {
// first, update the options view // first, update the options view
when (state.areTargetAndUserLocationEqual) { val options: Set<LocationSharingOption> = when (state.areTargetAndUserLocationEqual) {
// TODO activate USER_LIVE option when implemented true -> {
true -> views.shareLocationOptionsPicker.render( if (BuildConfig.ENABLE_LIVE_LOCATION_SHARING) {
LocationSharingOption.USER_CURRENT setOf(LocationSharingOption.USER_CURRENT, LocationSharingOption.USER_LIVE)
) } else {
false -> views.shareLocationOptionsPicker.render( setOf(LocationSharingOption.USER_CURRENT)
LocationSharingOption.PINNED
)
else -> views.shareLocationOptionsPicker.render()
} }
}
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 // then, update the map using the height of the options view after it has been rendered
views.shareLocationOptionsPicker.post { views.shareLocationOptionsPicker.post {
val mapState = state val mapState = state

View File

@ -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()
}

View File

@ -38,6 +38,7 @@ import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toMatrixItem
import timber.log.Timber
/** /**
* Sampling period to compare target location and user location. * Sampling period to compare target location and user location.
@ -120,6 +121,7 @@ class LocationSharingViewModel @AssistedInject constructor(
is LocationSharingAction.PinnedLocationSharing -> handlePinnedLocationSharingAction(action) is LocationSharingAction.PinnedLocationSharing -> handlePinnedLocationSharingAction(action)
is LocationSharingAction.LocationTargetChange -> handleLocationTargetChangeAction(action) is LocationSharingAction.LocationTargetChange -> handleLocationTargetChangeAction(action)
LocationSharingAction.ZoomToUserLocation -> handleZoomToUserLocationAction() LocationSharingAction.ZoomToUserLocation -> handleZoomToUserLocationAction()
LocationSharingAction.StartLiveLocationSharing -> handleStartLiveLocationSharingAction()
}.exhaustive }.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) { override fun onLocationUpdate(locationData: LocationData) {
setState { setState {
copy(lastKnownUserLocation = locationData) copy(lastKnownUserLocation = locationData)

View File

@ -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
}

View File

@ -58,7 +58,7 @@ class LocationSharingOptionPickerView @JvmOverloads constructor(
applyBackground() applyBackground()
} }
fun render(vararg options: LocationSharingOption) { fun render(options: Set<LocationSharingOption> = emptySet()) {
val optionsNumber = options.toSet().size val optionsNumber = options.toSet().size
val isPinnedVisible = options.contains(LocationSharingOption.PINNED) val isPinnedVisible = options.contains(LocationSharingOption.PINNED)
val isUserCurrentVisible = options.contains(LocationSharingOption.USER_CURRENT) val isUserCurrentVisible = options.contains(LocationSharingOption.USER_CURRENT)

View File

@ -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.file.FileService
import org.matrix.android.sdk.api.session.room.model.message.MessageContent 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.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.MessageVideoContent
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
@ -52,7 +53,10 @@ class RoomEventsAttachmentProvider(
override fun getAttachmentInfoAt(position: Int): AttachmentInfo { override fun getAttachmentInfoAt(position: Int): AttachmentInfo {
return getItem(position).let { return getItem(position).let {
val content = it.root.getClearContent().toModel<MessageContent>() as? MessageWithAttachmentContent val clearContent = it.root.getClearContent()
val content = clearContent.toModel<MessageContent>()
?: clearContent.toModel<MessageStickerContent>()
as? MessageWithAttachmentContent
if (content is MessageImageContent) { if (content is MessageImageContent) {
val data = ImageContentRenderer.Data( val data = ImageContentRenderer.Data(
eventId = it.eventId, eventId = it.eventId,
@ -66,6 +70,33 @@ class RoomEventsAttachmentProvider(
height = null, height = null,
allowNonMxcUrls = it.root.sendState.isSending() 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) { if (content.mimeType == MimeTypes.Gif) {
AttachmentInfo.AnimatedImage( AttachmentInfo.AnimatedImage(

View File

@ -22,63 +22,49 @@ import im.vector.app.features.login.LoginConfig
import im.vector.app.features.login.ServerType import im.vector.app.features.login.ServerType
import im.vector.app.features.login.SignMode import im.vector.app.features.login.SignMode
import org.matrix.android.sdk.api.auth.data.Credentials 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 import org.matrix.android.sdk.internal.network.ssl.Fingerprint
sealed class OnboardingAction : VectorViewModelAction { sealed interface OnboardingAction : VectorViewModelAction {
data class OnGetStarted(val resetLoginConfig: Boolean, val onboardingFlow: OnboardingFlow) : OnboardingAction() data class OnGetStarted(val resetLoginConfig: Boolean, val onboardingFlow: OnboardingFlow) : OnboardingAction
data class OnIAlreadyHaveAnAccount(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 UpdateServerType(val serverType: ServerType) : OnboardingAction
data class UpdateHomeServer(val homeServerUrl: String) : OnboardingAction() data class UpdateHomeServer(val homeServerUrl: String) : OnboardingAction
data class UpdateUseCase(val useCase: FtueUseCase) : OnboardingAction() data class UpdateUseCase(val useCase: FtueUseCase) : OnboardingAction
object ResetUseCase : OnboardingAction() object ResetUseCase : OnboardingAction
data class UpdateSignMode(val signMode: SignMode) : OnboardingAction() data class UpdateSignMode(val signMode: SignMode) : OnboardingAction
data class LoginWithToken(val loginToken: String) : OnboardingAction() data class LoginWithToken(val loginToken: String) : OnboardingAction
data class WebLoginSuccess(val credentials: Credentials) : OnboardingAction() data class WebLoginSuccess(val credentials: Credentials) : OnboardingAction
data class InitWith(val loginConfig: LoginConfig?) : OnboardingAction() data class InitWith(val loginConfig: LoginConfig?) : OnboardingAction
data class ResetPassword(val email: String, val newPassword: String) : OnboardingAction() data class ResetPassword(val email: String, val newPassword: String) : OnboardingAction
object ResetPasswordMailConfirmed : OnboardingAction() object ResetPasswordMailConfirmed : OnboardingAction
// Login or Register, depending on the signMode // 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 data class PostRegisterAction(val registerAction: RegisterAction) : OnboardingAction
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()
// Reset actions // Reset actions
open class ResetAction : OnboardingAction() sealed interface ResetAction : OnboardingAction
object ResetHomeServerType : ResetAction() object ResetHomeServerType : ResetAction
object ResetHomeServerUrl : ResetAction() object ResetHomeServerUrl : ResetAction
object ResetSignMode : ResetAction() object ResetSignMode : ResetAction
object ResetLogin : ResetAction() object ResetLogin : ResetAction
object ResetResetPassword : ResetAction() object ResetResetPassword : ResetAction
// Homeserver history // 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() object PersonalizeProfile : OnboardingAction
data class UpdateDisplayName(val displayName: String) : OnboardingAction() data class UpdateDisplayName(val displayName: String) : OnboardingAction
object UpdateDisplayNameSkipped : OnboardingAction() object UpdateDisplayNameSkipped : OnboardingAction
data class ProfilePictureSelected(val uri: Uri) : OnboardingAction() data class ProfilePictureSelected(val uri: Uri) : OnboardingAction
object SaveSelectedProfilePicture : OnboardingAction() object SaveSelectedProfilePicture : OnboardingAction
object UpdateProfilePictureSkipped : OnboardingAction() object UpdateProfilePictureSkipped : OnboardingAction
} }

View File

@ -83,6 +83,7 @@ class OnboardingViewModel @AssistedInject constructor(
private val vectorFeatures: VectorFeatures, private val vectorFeatures: VectorFeatures,
private val analyticsTracker: AnalyticsTracker, private val analyticsTracker: AnalyticsTracker,
private val uriFilenameResolver: UriFilenameResolver, private val uriFilenameResolver: UriFilenameResolver,
private val registrationActionHandler: RegistrationActionHandler,
private val vectorOverrides: VectorOverrides private val vectorOverrides: VectorOverrides
) : VectorViewModel<OnboardingViewState, OnboardingAction, OnboardingViewEvents>(initialState) { ) : VectorViewModel<OnboardingViewState, OnboardingAction, OnboardingViewEvents>(initialState) {
@ -116,16 +117,16 @@ class OnboardingViewModel @AssistedInject constructor(
private val matrixOrgUrl = stringProvider.getString(R.string.matrix_org_server_url).ensureTrailingSlash() private val matrixOrgUrl = stringProvider.getString(R.string.matrix_org_server_url).ensureTrailingSlash()
private val registrationWizard: RegistrationWizard
get() = authenticationService.getRegistrationWizard()
val currentThreePid: String? val currentThreePid: String?
get() = registrationWizard?.currentThreePid get() = registrationWizard.currentThreePid
// True when login and password has been sent with success to the homeserver // True when login and password has been sent with success to the homeserver
val isRegistrationStarted: Boolean val isRegistrationStarted: Boolean
get() = authenticationService.isRegistrationStarted get() = authenticationService.isRegistrationStarted
private val registrationWizard: RegistrationWizard?
get() = authenticationService.getRegistrationWizard()
private val loginWizard: LoginWizard? private val loginWizard: LoginWizard?
get() = authenticationService.getLoginWizard() get() = authenticationService.getLoginWizard()
@ -153,7 +154,7 @@ class OnboardingViewModel @AssistedInject constructor(
is OnboardingAction.WebLoginSuccess -> handleWebLoginSuccess(action) is OnboardingAction.WebLoginSuccess -> handleWebLoginSuccess(action)
is OnboardingAction.ResetPassword -> handleResetPassword(action) is OnboardingAction.ResetPassword -> handleResetPassword(action)
is OnboardingAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed() is OnboardingAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed()
is OnboardingAction.RegisterAction -> handleRegisterAction(action) is OnboardingAction.PostRegisterAction -> handleRegisterAction(action.registerAction)
is OnboardingAction.ResetAction -> handleResetAction(action) is OnboardingAction.ResetAction -> handleResetAction(action)
is OnboardingAction.UserAcceptCertificate -> handleUserAcceptCertificate(action) is OnboardingAction.UserAcceptCertificate -> handleUserAcceptCertificate(action)
OnboardingAction.ClearHomeServerHistory -> handleClearHomeServerHistory() OnboardingAction.ClearHomeServerHistory -> handleClearHomeServerHistory()
@ -164,6 +165,7 @@ class OnboardingViewModel @AssistedInject constructor(
is OnboardingAction.ProfilePictureSelected -> handleProfilePictureSelected(action) is OnboardingAction.ProfilePictureSelected -> handleProfilePictureSelected(action)
OnboardingAction.SaveSelectedProfilePicture -> updateProfilePicture() OnboardingAction.SaveSelectedProfilePicture -> updateProfilePicture()
is OnboardingAction.PostViewEvent -> _viewEvents.post(action.viewEvent) is OnboardingAction.PostViewEvent -> _viewEvents.post(action.viewEvent)
OnboardingAction.StopEmailValidationCheck -> cancelWaitForEmailValidation()
}.exhaustive }.exhaustive
} }
@ -266,131 +268,41 @@ class OnboardingViewModel @AssistedInject constructor(
} }
} }
private fun handleRegisterAction(action: OnboardingAction.RegisterAction) { private fun handleRegisterAction(action: 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()) }
currentJob = viewModelScope.launch { currentJob = viewModelScope.launch {
try { if (action.hasLoadingState()) {
registrationWizard?.addThreePid(action.threePid)
} catch (failure: Throwable) {
_viewEvents.post(OnboardingViewEvents.Failure(failure))
}
setState {
copy(
asyncRegistration = Uninitialized
)
}
}
}
private fun handleSendAgainThreePid() {
setState { copy(asyncRegistration = Loading()) } setState { copy(asyncRegistration = Loading()) }
currentJob = viewModelScope.launch {
try {
registrationWizard?.sendAgainThreePid()
} catch (failure: Throwable) {
_viewEvents.post(OnboardingViewEvents.Failure(failure))
} }
setState { runCatching { registrationActionHandler.handleRegisterAction(registrationWizard, action) }
copy( .fold(
asyncRegistration = Uninitialized 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 handleAcceptTerms() {
currentJob = executeRegistrationStep {
it.acceptTerms()
}
}
private fun handleRegisterDummy() {
currentJob = executeRegistrationStep {
it.dummy()
} }
} }
private fun handleRegisterWith(action: OnboardingAction.LoginOrRegister) { private fun handleRegisterWith(action: OnboardingAction.LoginOrRegister) {
reAuthHelper.data = action.password reAuthHelper.data = action.password
currentJob = executeRegistrationStep { handleRegisterAction(RegisterAction.CreateAccount(
it.createAccount(
action.username, action.username,
action.password, action.password,
action.initialDeviceName action.initialDeviceName
) ))
}
}
private fun handleCaptchaDone(action: OnboardingAction.CaptchaDone) {
currentJob = executeRegistrationStep {
it.performReCaptcha(action.captchaResponse)
}
} }
private fun handleResetAction(action: OnboardingAction.ResetAction) { private fun handleResetAction(action: OnboardingAction.ResetAction) {
@ -461,7 +373,7 @@ class OnboardingViewModel @AssistedInject constructor(
} }
when (action.signMode) { when (action.signMode) {
SignMode.SignUp -> startRegistrationFlow() SignMode.SignUp -> handleRegisterAction(RegisterAction.StartRegistration)
SignMode.SignIn -> startAuthenticationFlow() SignMode.SignIn -> startAuthenticationFlow()
SignMode.SignInWithMatrixId -> _viewEvents.post(OnboardingViewEvents.OnSignModeSelected(SignMode.SignInWithMatrixId)) SignMode.SignInWithMatrixId -> _viewEvents.post(OnboardingViewEvents.OnSignModeSelected(SignMode.SignInWithMatrixId))
SignMode.Unknown -> Unit SignMode.Unknown -> Unit
@ -499,7 +411,7 @@ class OnboardingViewModel @AssistedInject constructor(
// If there is a pending email validation continue on this step // If there is a pending email validation continue on this step
try { try {
if (registrationWizard?.isRegistrationStarted == true) { if (registrationWizard.isRegistrationStarted) {
currentThreePid?.let { currentThreePid?.let {
handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnSendEmailSuccess(it))) handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnSendEmailSuccess(it)))
} }
@ -730,12 +642,6 @@ class OnboardingViewModel @AssistedInject constructor(
} }
} }
private fun startRegistrationFlow() {
currentJob = executeRegistrationStep {
it.getRegistrationFlow()
}
}
private fun startAuthenticationFlow() { private fun startAuthenticationFlow() {
// Ensure Wizard is ready // Ensure Wizard is ready
loginWizard loginWizard
@ -745,8 +651,7 @@ class OnboardingViewModel @AssistedInject constructor(
private fun onFlowResponse(flowResult: FlowResult) { private fun onFlowResponse(flowResult: FlowResult) {
// If dummy stage is mandatory, and password is already sent, do the dummy stage now // If dummy stage is mandatory, and password is already sent, do the dummy stage now
if (isRegistrationStarted && if (isRegistrationStarted && flowResult.missingStages.any { it is Stage.Dummy && it.mandatory }) {
flowResult.missingStages.any { it is Stage.Dummy && it.mandatory }) {
handleRegisterDummy() handleRegisterDummy()
} else { } else {
// Notify the user // 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) { private suspend fun onSessionCreated(session: Session, isAccountCreated: Boolean) {
val state = awaitState() val state = awaitState()
state.useCase?.let { useCase -> state.useCase?.let { useCase ->
@ -1006,6 +915,10 @@ class OnboardingViewModel @AssistedInject constructor(
private fun completePersonalization() { private fun completePersonalization() {
_viewEvents.post(OnboardingViewEvents.OnPersonalizationComplete) _viewEvents.post(OnboardingViewEvents.OnPersonalizationComplete)
} }
private fun cancelWaitForEmailValidation() {
currentJob = null
}
} }
private fun LoginMode.supportsSignModeScreen(): Boolean { private fun LoginMode.supportsSignModeScreen(): Boolean {

View File

@ -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
}

View File

@ -39,6 +39,7 @@ import im.vector.app.databinding.FragmentLoginCaptchaBinding
import im.vector.app.features.login.JavascriptResponse import im.vector.app.features.login.JavascriptResponse
import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingAction
import im.vector.app.features.onboarding.OnboardingViewState import im.vector.app.features.onboarding.OnboardingViewState
import im.vector.app.features.onboarding.RegisterAction
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.MoshiProvider
import timber.log.Timber import timber.log.Timber
@ -181,7 +182,7 @@ class FtueAuthCaptchaFragment @Inject constructor(
val response = javascriptResponse?.response val response = javascriptResponse?.response
if (javascriptResponse?.action == "verifyCallback" && response != null) { if (javascriptResponse?.action == "verifyCallback" && response != null) {
viewModel.handle(OnboardingAction.CaptchaDone(response)) viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.CaptchaDone(response)))
} }
} }
return true return true

View File

@ -37,6 +37,7 @@ import im.vector.app.databinding.FragmentLoginGenericTextInputFormBinding
import im.vector.app.features.login.TextInputFormFragmentMode import im.vector.app.features.login.TextInputFormFragmentMode
import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingAction
import im.vector.app.features.onboarding.OnboardingViewEvents import im.vector.app.features.onboarding.OnboardingViewEvents
import im.vector.app.features.onboarding.RegisterAction
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@ -138,7 +139,7 @@ class FtueAuthGenericTextInputFormFragment @Inject constructor() : AbstractFtueA
private fun onOtherButtonClicked() { private fun onOtherButtonClicked() {
when (params.mode) { when (params.mode) {
TextInputFormFragmentMode.ConfirmMsisdn -> { TextInputFormFragmentMode.ConfirmMsisdn -> {
viewModel.handle(OnboardingAction.SendAgainThreePid) viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.SendAgainThreePid))
} }
else -> { else -> {
// Should not happen, button is not displayed // Should not happen, button is not displayed
@ -152,19 +153,19 @@ class FtueAuthGenericTextInputFormFragment @Inject constructor() : AbstractFtueA
if (text.isEmpty()) { if (text.isEmpty()) {
// Perform dummy action // Perform dummy action
viewModel.handle(OnboardingAction.RegisterDummy) viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.RegisterDummy))
} else { } else {
when (params.mode) { when (params.mode) {
TextInputFormFragmentMode.SetEmail -> { TextInputFormFragmentMode.SetEmail -> {
viewModel.handle(OnboardingAction.AddThreePid(RegisterThreePid.Email(text))) viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.AddThreePid(RegisterThreePid.Email(text))))
} }
TextInputFormFragmentMode.SetMsisdn -> { TextInputFormFragmentMode.SetMsisdn -> {
getCountryCodeOrShowError(text)?.let { countryCode -> getCountryCodeOrShowError(text)?.let { countryCode ->
viewModel.handle(OnboardingAction.AddThreePid(RegisterThreePid.Msisdn(text, countryCode))) viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.AddThreePid(RegisterThreePid.Msisdn(text, countryCode))))
} }
} }
TextInputFormFragmentMode.ConfirmMsisdn -> { TextInputFormFragmentMode.ConfirmMsisdn -> {
viewModel.handle(OnboardingAction.ValidateThreePid(text)) viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.ValidateThreePid(text)))
} }
} }
} }

View File

@ -25,6 +25,7 @@ import com.airbnb.mvrx.args
import im.vector.app.R import im.vector.app.R
import im.vector.app.databinding.FragmentLoginWaitForEmailBinding import im.vector.app.databinding.FragmentLoginWaitForEmailBinding
import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingAction
import im.vector.app.features.onboarding.RegisterAction
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.failure.is401 import org.matrix.android.sdk.api.failure.is401
import javax.inject.Inject import javax.inject.Inject
@ -54,7 +55,7 @@ class FtueAuthWaitForEmailFragment @Inject constructor() : AbstractFtueAuthFragm
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
viewModel.handle(OnboardingAction.CheckIfEmailHasBeenValidated(0)) viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.CheckIfEmailHasBeenValidated(0)))
} }
override fun onPause() { override fun onPause() {
@ -70,7 +71,7 @@ class FtueAuthWaitForEmailFragment @Inject constructor() : AbstractFtueAuthFragm
override fun onError(throwable: Throwable) { override fun onError(throwable: Throwable) {
if (throwable.is401()) { if (throwable.is401()) {
// Try again, with a delay // Try again, with a delay
viewModel.handle(OnboardingAction.CheckIfEmailHasBeenValidated(10_000)) viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.CheckIfEmailHasBeenValidated(10_000)))
} else { } else {
super.onError(throwable) super.onError(throwable)
} }

View File

@ -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.login.terms.PolicyController
import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingAction
import im.vector.app.features.onboarding.OnboardingViewState import im.vector.app.features.onboarding.OnboardingViewState
import im.vector.app.features.onboarding.RegisterAction
import im.vector.app.features.onboarding.ftueauth.AbstractFtueAuthFragment import im.vector.app.features.onboarding.ftueauth.AbstractFtueAuthFragment
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.internal.auth.registration.LocalizedFlowDataLoginTerms import org.matrix.android.sdk.internal.auth.registration.LocalizedFlowDataLoginTerms
@ -111,7 +112,7 @@ class FtueAuthTermsFragment @Inject constructor(
} }
private fun submit() { private fun submit() {
viewModel.handle(OnboardingAction.AcceptTerms) viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.AcceptTerms))
} }
override fun updateWithState(state: OnboardingViewState) { override fun updateWithState(state: OnboardingViewState) {

View File

@ -54,6 +54,7 @@ import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedA
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.parcelize.Parcelize 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.session.room.notification.RoomNotificationState
import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toMatrixItem
import timber.log.Timber import timber.log.Timber
@ -67,7 +68,8 @@ data class RoomProfileArgs(
class RoomProfileFragment @Inject constructor( class RoomProfileFragment @Inject constructor(
private val roomProfileController: RoomProfileController, private val roomProfileController: RoomProfileController,
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val roomDetailPendingActionStore: RoomDetailPendingActionStore private val roomDetailPendingActionStore: RoomDetailPendingActionStore,
private val matrixConfiguration: MatrixConfiguration
) : ) :
VectorBaseFragment<FragmentMatrixProfileBinding>(), VectorBaseFragment<FragmentMatrixProfileBinding>(),
RoomProfileController.Callback { RoomProfileController.Callback {
@ -222,7 +224,7 @@ class RoomProfileFragment @Inject constructor(
avatarRenderer.render(matrixItem, views.matrixProfileToolbarAvatarImageView) avatarRenderer.render(matrixItem, views.matrixProfileToolbarAvatarImageView)
headerViews.roomProfileDecorationImageView.render(it.roomEncryptionTrustLevel) headerViews.roomProfileDecorationImageView.render(it.roomEncryptionTrustLevel)
views.matrixProfileDecorationToolbarAvatarImageView.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 headerViews.roomProfilePublicImageView.isVisible = it.isPublic && !it.isDirect
} }
} }

View File

@ -27,6 +27,7 @@ import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import me.gujun.android.span.span 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.Event
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
@ -39,7 +40,8 @@ class RoomMemberListController @Inject constructor(
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val colorProvider: ColorProvider, private val colorProvider: ColorProvider,
private val roomMemberSummaryFilter: RoomMemberSummaryFilter private val roomMemberSummaryFilter: RoomMemberSummaryFilter,
private val matrixConfiguration: MatrixConfiguration
) : TypedEpoxyController<RoomMemberListViewState>() { ) : TypedEpoxyController<RoomMemberListViewState>() {
interface Callback { interface Callback {
@ -122,6 +124,7 @@ class RoomMemberListController @Inject constructor(
host: RoomMemberListController, host: RoomMemberListController,
data: RoomMemberListViewState) { data: RoomMemberListViewState) {
val powerLabel = stringProvider.getString(powerLevelCategory.titleRes) val powerLabel = stringProvider.getString(powerLevelCategory.titleRes)
val presenceSyncEnabled = matrixConfiguration.presenceSyncEnabled
profileMatrixItemWithPowerLevelWithPresence { profileMatrixItemWithPowerLevelWithPresence {
id(roomMember.userId) id(roomMember.userId)
@ -131,6 +134,7 @@ class RoomMemberListController @Inject constructor(
clickListener { clickListener {
host.callback?.onRoomMemberClicked(roomMember) host.callback?.onRoomMemberClicked(roomMember)
} }
showPresence(presenceSyncEnabled)
userPresence(roomMember.userPresence) userPresence(roomMember.userPresence)
powerLevelLabel( powerLevelLabel(
span { span {

View File

@ -16,7 +16,7 @@
<path <path
android:pathData="M0 0V12H11.8857V0" android:pathData="M0 0V12H11.8857V0"
android:fillColor="#0DBD8B" android:fillColor="?vctr_presence_indicator_online"
/> />
</group> </group>

View File

@ -46,20 +46,32 @@
<im.vector.app.features.sync.widget.SyncStateView <im.vector.app.features.sync.widget.SyncStateView
android:id="@+id/syncStateView" android:id="@+id/syncStateView"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appBarLayout" /> app:layout_constraintTop_toBottomOf="@id/appBarLayout" />
<im.vector.app.features.location.live.LocationLiveStatusView
android:id="@+id/locationLiveStatusIndicator"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/syncStateView"
tools:visibility="visible" />
<im.vector.app.features.call.conference.RemoveJitsiWidgetView <im.vector.app.features.call.conference.RemoveJitsiWidgetView
android:id="@+id/removeJitsiWidgetView" android:id="@+id/removeJitsiWidgetView"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?android:colorBackground" android:background="?android:colorBackground"
android:minHeight="54dp" android:minHeight="54dp"
android:visibility="visible" android:visibility="visible"
app:layout_constraintTop_toBottomOf="@id/syncStateView" /> app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/locationLiveStatusIndicator" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/timelineRecyclerView" android:id="@+id/timelineRecyclerView"
@ -90,15 +102,15 @@
<im.vector.app.core.ui.views.TypingMessageView <im.vector.app.core.ui.views.TypingMessageView
android:id="@+id/typingMessageView" android:id="@+id/typingMessageView"
app:layout_constraintBottom_toTopOf="@id/composerLayout"
app:layout_constraintTop_toBottomOf="@id/timelineRecyclerView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="20dp"
android:paddingStart="20dp" android:paddingStart="20dp"
android:paddingEnd="20dp" android:paddingEnd="20dp"
tools:visibility="visible" app:layout_constraintBottom_toTopOf="@id/composerLayout"
android:layout_height="20dp"/> app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/timelineRecyclerView"
tools:visibility="visible" />
<im.vector.app.core.ui.views.NotificationAreaView <im.vector.app.core.ui.views.NotificationAreaView
android:id="@+id/notificationAreaView" android:id="@+id/notificationAreaView"

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<androidx.constraintlayout.helper.widget.Flow
android:id="@+id/locationLiveStatusContainer"
android:layout_width="0dp"
android:layout_height="32dp"
android:background="?colorPrimary"
android:duplicateParentState="true"
android:paddingStart="9dp"
android:paddingEnd="12dp"
app:constraint_referenced_ids="locationLiveStatusIcon,locationLiveStatusTitle"
app:flow_horizontalBias="0"
app:flow_horizontalGap="8dp"
app:flow_horizontalStyle="packed"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<ImageView
android:id="@+id/locationLiveStatusIcon"
android:layout_width="wrap_content"
android:layout_height="13dp"
app:srcCompat="@drawable/ic_attachment_location_live_white"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/locationLiveStatusTitle"
style="@style/Widget.Vector.TextView.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/location_share_live_enabled"
android:textColor="?colorOnPrimary" />
<Button
android:id="@+id/locationLiveStatusStop"
style="@style/Widget.Vector.Button.Text.OnPrimary.LocationLive"
android:layout_width="60dp"
android:layout_height="0dp"
android:text="@string/location_share_live_stop"
app:layout_constraintBottom_toBottomOf="@id/locationLiveStatusContainer"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/locationLiveStatusContainer" />
</merge>

File diff suppressed because one or more lines are too long

View File

@ -2943,6 +2943,8 @@
<string name="a11y_location_share_option_user_live_icon">Share live location</string> <string name="a11y_location_share_option_user_live_icon">Share live location</string>
<string name="location_share_option_pinned">Share this location</string> <string name="location_share_option_pinned">Share this location</string>
<string name="a11y_location_share_option_pinned_icon">Share this location</string> <string name="a11y_location_share_option_pinned_icon">Share this location</string>
<string name="location_in_background_missing_permission_dialog_title">Allow access</string>
<string name="location_in_background_missing_permission_dialog_content">If youd like to share your Live location, ${app_name} needs location access all the time when the app is in the background.\nWe will only access your location for the duration that you choose.</string>
<string name="location_not_available_dialog_title">${app_name} could not access your location</string> <string name="location_not_available_dialog_title">${app_name} could not access your location</string>
<string name="location_not_available_dialog_content">${app_name} could not access your location. Please try again later.</string> <string name="location_not_available_dialog_content">${app_name} could not access your location. Please try again later.</string>
<string name="location_share_external">Open with</string> <string name="location_share_external">Open with</string>
@ -2950,6 +2952,8 @@
<string name="settings_enable_location_sharing_summary">Once enabled you will be able to send your location to any room</string> <string name="settings_enable_location_sharing_summary">Once enabled you will be able to send your location to any room</string>
<string name="labs_render_locations_in_timeline">Render user locations in the timeline</string> <string name="labs_render_locations_in_timeline">Render user locations in the timeline</string>
<string name="location_timeline_failed_to_load_map">Failed to load map</string> <string name="location_timeline_failed_to_load_map">Failed to load map</string>
<string name="location_share_live_enabled">Live location enabled</string>
<string name="location_share_live_stop">Stop</string>
<string name="message_bubbles">Show Message bubbles</string> <string name="message_bubbles">Show Message bubbles</string>

View File

@ -23,12 +23,14 @@ import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.test.MvRxTestRule import com.airbnb.mvrx.test.MvRxTestRule
import im.vector.app.features.login.ReAuthHelper import im.vector.app.features.login.ReAuthHelper
import im.vector.app.features.login.SignMode
import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakeActiveSessionHolder
import im.vector.app.test.fakes.FakeAnalyticsTracker import im.vector.app.test.fakes.FakeAnalyticsTracker
import im.vector.app.test.fakes.FakeAuthenticationService import im.vector.app.test.fakes.FakeAuthenticationService
import im.vector.app.test.fakes.FakeContext import im.vector.app.test.fakes.FakeContext
import im.vector.app.test.fakes.FakeHomeServerConnectionConfigFactory import im.vector.app.test.fakes.FakeHomeServerConnectionConfigFactory
import im.vector.app.test.fakes.FakeHomeServerHistoryService import im.vector.app.test.fakes.FakeHomeServerHistoryService
import im.vector.app.test.fakes.FakeRegisterActionHandler
import im.vector.app.test.fakes.FakeRegistrationWizard import im.vector.app.test.fakes.FakeRegistrationWizard
import im.vector.app.test.fakes.FakeSession import im.vector.app.test.fakes.FakeSession
import im.vector.app.test.fakes.FakeStringProvider import im.vector.app.test.fakes.FakeStringProvider
@ -36,20 +38,27 @@ import im.vector.app.test.fakes.FakeUri
import im.vector.app.test.fakes.FakeUriFilenameResolver import im.vector.app.test.fakes.FakeUriFilenameResolver
import im.vector.app.test.fakes.FakeVectorFeatures import im.vector.app.test.fakes.FakeVectorFeatures
import im.vector.app.test.fakes.FakeVectorOverrides import im.vector.app.test.fakes.FakeVectorOverrides
import im.vector.app.test.fixtures.aHomeServerCapabilities
import im.vector.app.test.test import im.vector.app.test.test
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runBlockingTest
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.matrix.android.sdk.api.auth.registration.FlowResult
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.Stage
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
private const val A_DISPLAY_NAME = "a display name" private const val A_DISPLAY_NAME = "a display name"
private const val A_PICTURE_FILENAME = "a-picture.png" private const val A_PICTURE_FILENAME = "a-picture.png"
private val AN_ERROR = RuntimeException("an error!") private val AN_ERROR = RuntimeException("an error!")
private val AN_UNSUPPORTED_PERSONALISATION_STATE = PersonalizationState( private val A_LOADABLE_REGISTER_ACTION = RegisterAction.StartRegistration
supportsChangingDisplayName = false, private val A_NON_LOADABLE_REGISTER_ACTION = RegisterAction.CheckIfEmailHasBeenValidated(delayMillis = -1L)
supportsChangingProfilePicture = false private val A_RESULT_IGNORED_REGISTER_ACTION = RegisterAction.AddThreePid(RegisterThreePid.Email("an email"))
) private val A_HOMESERVER_CAPABILITIES = aHomeServerCapabilities(canChangeDisplayName = true, canChangeAvatar = true)
private val AN_IGNORED_FLOW_RESULT = FlowResult(missingStages = emptyList(), completedStages = emptyList())
private val ANY_CONTINUING_REGISTRATION_RESULT = RegistrationResult.FlowResponse(AN_IGNORED_FLOW_RESULT)
class OnboardingViewModelTest { class OnboardingViewModelTest {
@ -63,6 +72,7 @@ class OnboardingViewModelTest {
private val fakeUriFilenameResolver = FakeUriFilenameResolver() private val fakeUriFilenameResolver = FakeUriFilenameResolver()
private val fakeActiveSessionHolder = FakeActiveSessionHolder(fakeSession) private val fakeActiveSessionHolder = FakeActiveSessionHolder(fakeSession)
private val fakeAuthenticationService = FakeAuthenticationService() private val fakeAuthenticationService = FakeAuthenticationService()
private val fakeRegisterActionHandler = FakeRegisterActionHandler()
lateinit var viewModel: OnboardingViewModel lateinit var viewModel: OnboardingViewModel
@ -72,7 +82,7 @@ class OnboardingViewModelTest {
} }
@Test @Test
fun `when handling PostViewEvent then emits contents as view event`() = runBlockingTest { fun `when handling PostViewEvent, then emits contents as view event`() = runBlockingTest {
val test = viewModel.test(this) val test = viewModel.test(this)
viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnTakeMeHome)) viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnTakeMeHome))
@ -83,7 +93,7 @@ class OnboardingViewModelTest {
} }
@Test @Test
fun `given supports changing display name when handling PersonalizeProfile then emits contents choose display name`() = runBlockingTest { fun `given supports changing display name, when handling PersonalizeProfile, then emits contents choose display name`() = runBlockingTest {
val initialState = initialState.copy(personalizationState = PersonalizationState(supportsChangingDisplayName = true, supportsChangingProfilePicture = false)) val initialState = initialState.copy(personalizationState = PersonalizationState(supportsChangingDisplayName = true, supportsChangingProfilePicture = false))
viewModel = createViewModel(initialState) viewModel = createViewModel(initialState)
val test = viewModel.test(this) val test = viewModel.test(this)
@ -96,7 +106,7 @@ class OnboardingViewModelTest {
} }
@Test @Test
fun `given only supports changing profile picture when handling PersonalizeProfile then emits contents choose profile picture`() = runBlockingTest { fun `given only supports changing profile picture, when handling PersonalizeProfile, then emits contents choose profile picture`() = runBlockingTest {
val initialState = initialState.copy(personalizationState = PersonalizationState(supportsChangingDisplayName = false, supportsChangingProfilePicture = true)) val initialState = initialState.copy(personalizationState = PersonalizationState(supportsChangingDisplayName = false, supportsChangingProfilePicture = true))
viewModel = createViewModel(initialState) viewModel = createViewModel(initialState)
val test = viewModel.test(this) val test = viewModel.test(this)
@ -109,34 +119,109 @@ class OnboardingViewModelTest {
} }
@Test @Test
fun `given homeserver does not support personalisation when registering account then updates state and emits account created event`() = runBlockingTest { fun `when handling SignUp then sets sign mode to sign up and starts registration`() = runBlockingTest {
fakeSession.fakeHomeServerCapabilitiesService.givenCapabilities(HomeServerCapabilities(canChangeDisplayName = false, canChangeAvatar = false)) givenRegistrationResultFor(RegisterAction.StartRegistration, ANY_CONTINUING_REGISTRATION_RESULT)
givenSuccessfullyCreatesAccount()
val test = viewModel.test(this) val test = viewModel.test(this)
viewModel.handle(OnboardingAction.RegisterDummy) viewModel.handle(OnboardingAction.UpdateSignMode(SignMode.SignUp))
test test
.assertStates( .assertStatesChanges(
initialState, initialState,
initialState.copy(asyncRegistration = Loading()), { copy(signMode = SignMode.SignUp) },
initialState.copy( { copy(asyncRegistration = Loading()) },
asyncLoginAction = Success(Unit), { copy(asyncRegistration = Uninitialized) }
asyncRegistration = Loading(),
personalizationState = AN_UNSUPPORTED_PERSONALISATION_STATE
),
initialState.copy(
asyncLoginAction = Success(Unit),
asyncRegistration = Uninitialized,
personalizationState = AN_UNSUPPORTED_PERSONALISATION_STATE
) )
.assertEvents(OnboardingViewEvents.RegistrationFlowResult(ANY_CONTINUING_REGISTRATION_RESULT.flowResult, isRegistrationStarted = true))
.finish()
}
@Test
fun `given register action requires more steps, when handling action, then posts next steps`() = runBlockingTest {
val test = viewModel.test(this)
givenRegistrationResultFor(A_LOADABLE_REGISTER_ACTION, ANY_CONTINUING_REGISTRATION_RESULT)
viewModel.handle(OnboardingAction.PostRegisterAction(A_LOADABLE_REGISTER_ACTION))
test
.assertStatesChanges(
initialState,
{ copy(asyncRegistration = Loading()) },
{ copy(asyncRegistration = Uninitialized) }
)
.assertEvents(OnboardingViewEvents.RegistrationFlowResult(ANY_CONTINUING_REGISTRATION_RESULT.flowResult, isRegistrationStarted = true))
.finish()
}
@Test
fun `given register action is non loadable, when handling action, then posts next steps without loading`() = runBlockingTest {
val test = viewModel.test(this)
givenRegistrationResultFor(A_NON_LOADABLE_REGISTER_ACTION, ANY_CONTINUING_REGISTRATION_RESULT)
viewModel.handle(OnboardingAction.PostRegisterAction(A_NON_LOADABLE_REGISTER_ACTION))
test
.assertState(initialState)
.assertEvents(OnboardingViewEvents.RegistrationFlowResult(ANY_CONTINUING_REGISTRATION_RESULT.flowResult, isRegistrationStarted = true))
.finish()
}
@Test
fun `given register action ignores result, when handling action, then does nothing on success`() = runBlockingTest {
val test = viewModel.test(this)
givenRegistrationResultFor(A_RESULT_IGNORED_REGISTER_ACTION, RegistrationResult.FlowResponse(AN_IGNORED_FLOW_RESULT))
viewModel.handle(OnboardingAction.PostRegisterAction(A_RESULT_IGNORED_REGISTER_ACTION))
test
.assertStatesChanges(
initialState,
{ copy(asyncRegistration = Loading()) },
{ copy(asyncRegistration = Uninitialized) }
)
.assertNoEvents()
.finish()
}
@Test
fun `when registering account, then updates state and emits account created event`() = runBlockingTest {
givenRegistrationResultFor(A_LOADABLE_REGISTER_ACTION, RegistrationResult.Success(fakeSession))
givenSuccessfullyCreatesAccount(A_HOMESERVER_CAPABILITIES)
val test = viewModel.test(this)
viewModel.handle(OnboardingAction.PostRegisterAction(A_LOADABLE_REGISTER_ACTION))
test
.assertStatesChanges(
initialState,
{ copy(asyncRegistration = Loading()) },
{ copy(asyncLoginAction = Success(Unit), personalizationState = A_HOMESERVER_CAPABILITIES.toPersonalisationState()) },
{ copy(asyncLoginAction = Success(Unit), asyncRegistration = Uninitialized) }
) )
.assertEvents(OnboardingViewEvents.OnAccountCreated) .assertEvents(OnboardingViewEvents.OnAccountCreated)
.finish() .finish()
} }
@Test @Test
fun `given changing profile picture is supported when updating display name then updates upstream user display name and moves to choose profile picture`() = runBlockingTest { fun `given registration has started and has dummy step to do, when handling action, then ignores other steps and executes dummy`() = runBlockingTest {
givenSuccessfulRegistrationForStartAndDummySteps(missingStages = listOf(Stage.Dummy(mandatory = true)))
val test = viewModel.test(this)
viewModel.handle(OnboardingAction.PostRegisterAction(A_LOADABLE_REGISTER_ACTION))
test
.assertStatesChanges(
initialState,
{ copy(asyncRegistration = Loading()) },
{ copy(asyncLoginAction = Success(Unit), personalizationState = A_HOMESERVER_CAPABILITIES.toPersonalisationState()) },
{ copy(asyncRegistration = Uninitialized) }
)
.assertEvents(OnboardingViewEvents.OnAccountCreated)
.finish()
}
@Test
fun `given changing profile picture is supported, when updating display name, then updates upstream user display name and moves to choose profile picture`() = runBlockingTest {
val personalisedInitialState = initialState.copy(personalizationState = PersonalizationState(supportsChangingProfilePicture = true)) val personalisedInitialState = initialState.copy(personalizationState = PersonalizationState(supportsChangingProfilePicture = true))
viewModel = createViewModel(personalisedInitialState) viewModel = createViewModel(personalisedInitialState)
val test = viewModel.test(this) val test = viewModel.test(this)
@ -144,14 +229,14 @@ class OnboardingViewModelTest {
viewModel.handle(OnboardingAction.UpdateDisplayName(A_DISPLAY_NAME)) viewModel.handle(OnboardingAction.UpdateDisplayName(A_DISPLAY_NAME))
test test
.assertStates(expectedSuccessfulDisplayNameUpdateStates(personalisedInitialState)) .assertStatesChanges(personalisedInitialState, expectedSuccessfulDisplayNameUpdateStates())
.assertEvents(OnboardingViewEvents.OnChooseProfilePicture) .assertEvents(OnboardingViewEvents.OnChooseProfilePicture)
.finish() .finish()
fakeSession.fakeProfileService.verifyUpdatedName(fakeSession.myUserId, A_DISPLAY_NAME) fakeSession.fakeProfileService.verifyUpdatedName(fakeSession.myUserId, A_DISPLAY_NAME)
} }
@Test @Test
fun `given changing profile picture is not supported when updating display name then updates upstream user display name and completes personalization`() = runBlockingTest { fun `given changing profile picture is not supported, when updating display name, then updates upstream user display name and completes personalization`() = runBlockingTest {
val personalisedInitialState = initialState.copy(personalizationState = PersonalizationState(supportsChangingProfilePicture = false)) val personalisedInitialState = initialState.copy(personalizationState = PersonalizationState(supportsChangingProfilePicture = false))
viewModel = createViewModel(personalisedInitialState) viewModel = createViewModel(personalisedInitialState)
val test = viewModel.test(this) val test = viewModel.test(this)
@ -159,31 +244,31 @@ class OnboardingViewModelTest {
viewModel.handle(OnboardingAction.UpdateDisplayName(A_DISPLAY_NAME)) viewModel.handle(OnboardingAction.UpdateDisplayName(A_DISPLAY_NAME))
test test
.assertStates(expectedSuccessfulDisplayNameUpdateStates(personalisedInitialState)) .assertStatesChanges(personalisedInitialState, expectedSuccessfulDisplayNameUpdateStates())
.assertEvents(OnboardingViewEvents.OnPersonalizationComplete) .assertEvents(OnboardingViewEvents.OnPersonalizationComplete)
.finish() .finish()
fakeSession.fakeProfileService.verifyUpdatedName(fakeSession.myUserId, A_DISPLAY_NAME) fakeSession.fakeProfileService.verifyUpdatedName(fakeSession.myUserId, A_DISPLAY_NAME)
} }
@Test @Test
fun `given upstream failure when handling display name update then emits failure event`() = runBlockingTest { fun `given upstream failure, when handling display name update, then emits failure event`() = runBlockingTest {
val test = viewModel.test(this) val test = viewModel.test(this)
fakeSession.fakeProfileService.givenSetDisplayNameErrors(AN_ERROR) fakeSession.fakeProfileService.givenSetDisplayNameErrors(AN_ERROR)
viewModel.handle(OnboardingAction.UpdateDisplayName(A_DISPLAY_NAME)) viewModel.handle(OnboardingAction.UpdateDisplayName(A_DISPLAY_NAME))
test test
.assertStates( .assertStatesChanges(
initialState, initialState,
initialState.copy(asyncDisplayName = Loading()), { copy(asyncDisplayName = Loading()) },
initialState.copy(asyncDisplayName = Fail(AN_ERROR)), { copy(asyncDisplayName = Fail(AN_ERROR)) },
) )
.assertEvents(OnboardingViewEvents.Failure(AN_ERROR)) .assertEvents(OnboardingViewEvents.Failure(AN_ERROR))
.finish() .finish()
} }
@Test @Test
fun `when handling profile picture selected then updates selected picture state`() = runBlockingTest { fun `when handling profile picture selected, then updates selected picture state`() = runBlockingTest {
val test = viewModel.test(this) val test = viewModel.test(this)
viewModel.handle(OnboardingAction.ProfilePictureSelected(fakeUri.instance)) viewModel.handle(OnboardingAction.ProfilePictureSelected(fakeUri.instance))
@ -198,7 +283,7 @@ class OnboardingViewModelTest {
} }
@Test @Test
fun `given a selected picture when handling save selected profile picture then updates upstream avatar and completes personalization`() = runBlockingTest { fun `given a selected picture, when handling save selected profile picture, then updates upstream avatar and completes personalization`() = runBlockingTest {
val initialStateWithPicture = givenPictureSelected(fakeUri.instance, A_PICTURE_FILENAME) val initialStateWithPicture = givenPictureSelected(fakeUri.instance, A_PICTURE_FILENAME)
viewModel = createViewModel(initialStateWithPicture) viewModel = createViewModel(initialStateWithPicture)
val test = viewModel.test(this) val test = viewModel.test(this)
@ -213,7 +298,7 @@ class OnboardingViewModelTest {
} }
@Test @Test
fun `given upstream update avatar fails when saving selected profile picture then emits failure event`() = runBlockingTest { fun `given upstream update avatar fails, when saving selected profile picture, then emits failure event`() = runBlockingTest {
fakeSession.fakeProfileService.givenUpdateAvatarErrors(AN_ERROR) fakeSession.fakeProfileService.givenUpdateAvatarErrors(AN_ERROR)
val initialStateWithPicture = givenPictureSelected(fakeUri.instance, A_PICTURE_FILENAME) val initialStateWithPicture = givenPictureSelected(fakeUri.instance, A_PICTURE_FILENAME)
viewModel = createViewModel(initialStateWithPicture) viewModel = createViewModel(initialStateWithPicture)
@ -228,7 +313,7 @@ class OnboardingViewModelTest {
} }
@Test @Test
fun `given no selected picture when saving selected profile picture then emits failure event`() = runBlockingTest { fun `given no selected picture, when saving selected profile picture, then emits failure event`() = runBlockingTest {
val test = viewModel.test(this) val test = viewModel.test(this)
viewModel.handle(OnboardingAction.SaveSelectedProfilePicture) viewModel.handle(OnboardingAction.SaveSelectedProfilePicture)
@ -240,7 +325,7 @@ class OnboardingViewModelTest {
} }
@Test @Test
fun `when handling profile picture skipped then completes personalization`() = runBlockingTest { fun `when handling profile skipped, then completes personalization`() = runBlockingTest {
val test = viewModel.test(this) val test = viewModel.test(this)
viewModel.handle(OnboardingAction.UpdateProfilePictureSkipped) viewModel.handle(OnboardingAction.UpdateProfilePictureSkipped)
@ -264,6 +349,7 @@ class OnboardingViewModelTest {
FakeVectorFeatures(), FakeVectorFeatures(),
FakeAnalyticsTracker(), FakeAnalyticsTracker(),
fakeUriFilenameResolver.instance, fakeUriFilenameResolver.instance,
fakeRegisterActionHandler.instance,
FakeVectorOverrides() FakeVectorOverrides()
) )
} }
@ -286,22 +372,42 @@ class OnboardingViewModelTest {
state.copy(asyncProfilePicture = Fail(cause)) state.copy(asyncProfilePicture = Fail(cause))
) )
private fun givenSuccessfullyCreatesAccount() { private fun expectedSuccessfulDisplayNameUpdateStates(): List<OnboardingViewState.() -> OnboardingViewState> {
return listOf(
{ copy(asyncDisplayName = Loading()) },
{ copy(asyncDisplayName = Success(Unit), personalizationState = personalizationState.copy(displayName = A_DISPLAY_NAME)) }
)
}
private fun givenSuccessfulRegistrationForStartAndDummySteps(missingStages: List<Stage>) {
val flowResult = FlowResult(missingStages = missingStages, completedStages = emptyList())
givenRegistrationResultsFor(listOf(
A_LOADABLE_REGISTER_ACTION to RegistrationResult.FlowResponse(flowResult),
RegisterAction.RegisterDummy to RegistrationResult.Success(fakeSession)
))
givenSuccessfullyCreatesAccount(A_HOMESERVER_CAPABILITIES)
}
private fun givenSuccessfullyCreatesAccount(homeServerCapabilities: HomeServerCapabilities) {
fakeSession.fakeHomeServerCapabilitiesService.givenCapabilities(homeServerCapabilities)
fakeActiveSessionHolder.expectSetsActiveSession(fakeSession) fakeActiveSessionHolder.expectSetsActiveSession(fakeSession)
val registrationWizard = FakeRegistrationWizard().also { it.givenSuccessfulDummy(fakeSession) }
fakeAuthenticationService.givenRegistrationWizard(registrationWizard)
fakeAuthenticationService.expectReset() fakeAuthenticationService.expectReset()
fakeSession.expectStartsSyncing() fakeSession.expectStartsSyncing()
} }
private fun expectedSuccessfulDisplayNameUpdateStates(personalisedInitialState: OnboardingViewState): List<OnboardingViewState> { private fun givenRegistrationResultFor(action: RegisterAction, result: RegistrationResult) {
return listOf( givenRegistrationResultsFor(listOf(action to result))
personalisedInitialState, }
personalisedInitialState.copy(asyncDisplayName = Loading()),
personalisedInitialState.copy( private fun givenRegistrationResultsFor(results: List<Pair<RegisterAction, RegistrationResult>>) {
asyncDisplayName = Success(Unit), fakeAuthenticationService.givenRegistrationStarted(true)
personalizationState = personalisedInitialState.personalizationState.copy(displayName = A_DISPLAY_NAME) val registrationWizard = FakeRegistrationWizard()
) fakeAuthenticationService.givenRegistrationWizard(registrationWizard)
) fakeRegisterActionHandler.givenResultsFor(registrationWizard, results)
} }
} }
private fun HomeServerCapabilities.toPersonalisationState() = PersonalizationState(
supportsChangingDisplayName = canChangeDisplayName,
supportsChangingProfilePicture = canChangeAvatar
)

View File

@ -0,0 +1,74 @@
/*
* 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 im.vector.app.test.fakes.FakeRegistrationWizard
import im.vector.app.test.fakes.FakeSession
import io.mockk.coVerifyAll
import kotlinx.coroutines.test.runBlockingTest
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
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
private val A_SESSION = FakeSession()
private val AN_EXPECTED_RESULT = RegistrationResult.Success(A_SESSION)
private const val A_USERNAME = "a username"
private const val A_PASSWORD = "a password"
private const val AN_INITIAL_DEVICE_NAME = "a device name"
private const val A_CAPTCHA_RESPONSE = "a captcha response"
private const val A_PID_CODE = "a pid code"
private const val EMAIL_VALIDATED_DELAY = 10000L
private val A_PID_TO_REGISTER = RegisterThreePid.Email("an email")
class RegistrationActionHandlerTest {
@Test
fun `when handling register action then delegates to wizard`() = runBlockingTest {
val cases = listOf(
case(RegisterAction.StartRegistration) { getRegistrationFlow() },
case(RegisterAction.CaptchaDone(A_CAPTCHA_RESPONSE)) { performReCaptcha(A_CAPTCHA_RESPONSE) },
case(RegisterAction.AcceptTerms) { acceptTerms() },
case(RegisterAction.RegisterDummy) { dummy() },
case(RegisterAction.AddThreePid(A_PID_TO_REGISTER)) { addThreePid(A_PID_TO_REGISTER) },
case(RegisterAction.SendAgainThreePid) { sendAgainThreePid() },
case(RegisterAction.ValidateThreePid(A_PID_CODE)) { handleValidateThreePid(A_PID_CODE) },
case(RegisterAction.CheckIfEmailHasBeenValidated(EMAIL_VALIDATED_DELAY)) { checkIfEmailHasBeenValidated(EMAIL_VALIDATED_DELAY) },
case(RegisterAction.CreateAccount(A_USERNAME, A_PASSWORD, AN_INITIAL_DEVICE_NAME)) {
createAccount(A_USERNAME, A_PASSWORD, AN_INITIAL_DEVICE_NAME)
}
)
cases.forEach { testSuccessfulActionDelegation(it) }
}
private suspend fun testSuccessfulActionDelegation(case: Case) {
val registrationActionHandler = RegistrationActionHandler()
val fakeRegistrationWizard = FakeRegistrationWizard()
fakeRegistrationWizard.givenSuccessFor(result = A_SESSION, case.expect)
val result = registrationActionHandler.handleRegisterAction(fakeRegistrationWizard, case.action)
coVerifyAll { case.expect(fakeRegistrationWizard) }
result shouldBeEqualTo AN_EXPECTED_RESULT
}
}
private fun case(action: RegisterAction, expect: suspend RegistrationWizard.() -> RegistrationResult) = Case(action, expect)
private class Case(val action: RegisterAction, val expect: suspend RegistrationWizard.() -> RegistrationResult)

View File

@ -55,6 +55,25 @@ class ViewModelTest<S, VE>(
return this return this
} }
fun assertStatesChanges(initial: S, vararg expected: S.() -> S): ViewModelTest<S, VE> {
return assertStatesChanges(initial, expected.toList())
}
/**
* Asserts the expected states are in the same order as the actual state emissions
* Each expected lambda is given the previous expected state, starting with the initial
*/
fun assertStatesChanges(initial: S, expected: List<S.() -> S>): ViewModelTest<S, VE> {
val reducedExpectedStates = expected.fold(mutableListOf(initial)) { acc, curr ->
val next = curr.invoke(acc.last())
acc.add(next)
acc
}
states.assertValues(reducedExpectedStates)
return this
}
fun assertStates(expected: List<S>): ViewModelTest<S, VE> { fun assertStates(expected: List<S>): ViewModelTest<S, VE> {
states.assertValues(expected) states.assertValues(expected)
return this return this

View File

@ -23,10 +23,15 @@ import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.registration.RegistrationWizard import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
class FakeAuthenticationService : AuthenticationService by mockk() { class FakeAuthenticationService : AuthenticationService by mockk() {
fun givenRegistrationWizard(registrationWizard: RegistrationWizard) { fun givenRegistrationWizard(registrationWizard: RegistrationWizard) {
every { getRegistrationWizard() } returns registrationWizard every { getRegistrationWizard() } returns registrationWizard
} }
fun givenRegistrationStarted(started: Boolean) {
every { isRegistrationStarted } returns started
}
fun expectReset() { fun expectReset() {
coJustRun { reset() } coJustRun { reset() }
} }

View File

@ -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.test.fakes
import im.vector.app.features.onboarding.RegisterAction
import im.vector.app.features.onboarding.RegistrationActionHandler
import io.mockk.coEvery
import io.mockk.mockk
import org.matrix.android.sdk.api.auth.registration.RegistrationResult
import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
class FakeRegisterActionHandler {
val instance = mockk<RegistrationActionHandler>()
fun givenResultsFor(wizard: RegistrationWizard, result: List<Pair<RegisterAction, RegistrationResult>>) {
coEvery { instance.handleRegisterAction(wizard, any()) } answers { call ->
val actionArg = call.invocation.args[1] as RegisterAction
result.first { it.first == actionArg }.second
}
}
}

View File

@ -22,9 +22,9 @@ import org.matrix.android.sdk.api.auth.registration.RegistrationResult
import org.matrix.android.sdk.api.auth.registration.RegistrationWizard import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
class FakeRegistrationWizard : RegistrationWizard by mockk() { class FakeRegistrationWizard : RegistrationWizard by mockk(relaxed = false) {
fun givenSuccessfulDummy(session: Session) { fun givenSuccessFor(result: Session, expect: suspend RegistrationWizard.() -> RegistrationResult) {
coEvery { dummy() } returns RegistrationResult.Success(session) coEvery { expect(this@FakeRegistrationWizard) } returns RegistrationResult.Success(result)
} }
} }

View File

@ -23,5 +23,5 @@ class FakeVectorFeatures : VectorFeatures {
override fun isOnboardingAlreadyHaveAccountSplashEnabled() = true override fun isOnboardingAlreadyHaveAccountSplashEnabled() = true
override fun isOnboardingSplashCarouselEnabled() = true override fun isOnboardingSplashCarouselEnabled() = true
override fun isOnboardingUseCaseEnabled() = true override fun isOnboardingUseCaseEnabled() = true
override fun isOnboardingPersonalizeEnabled() = false override fun isOnboardingPersonalizeEnabled() = true
} }

View File

@ -0,0 +1,40 @@
/*
* 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.fixtures
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
import org.matrix.android.sdk.api.session.homeserver.RoomVersionCapabilities
fun aHomeServerCapabilities(
canChangePassword: Boolean = true,
canChangeDisplayName: Boolean = true,
canChangeAvatar: Boolean = true,
canChange3pid: Boolean = true,
maxUploadFileSize: Long = 100L,
lastVersionIdentityServerSupported: Boolean = false,
defaultIdentityServerUrl: String? = null,
roomVersions: RoomVersionCapabilities? = null
) = HomeServerCapabilities(
canChangePassword,
canChangeDisplayName,
canChangeAvatar,
canChange3pid,
maxUploadFileSize,
lastVersionIdentityServerSupported,
defaultIdentityServerUrl,
roomVersions
)