mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-16 02:05:06 +08:00
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:
commit
5a819bbafa
5
.github/workflows/nightly.yml
vendored
5
.github/workflows/nightly.yml
vendored
@ -265,6 +265,7 @@ jobs:
|
||||
failure_screenshots/
|
||||
|
||||
codecov-units:
|
||||
name: Unit tests with code coverage
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@ -290,6 +291,7 @@ jobs:
|
||||
build/reports/jacoco/allCodeCoverageReport/allCodeCoverageReport.xml
|
||||
|
||||
sonarqube:
|
||||
name: Sonarqube upload
|
||||
runs-on: macos-latest
|
||||
if: always()
|
||||
needs:
|
||||
@ -319,6 +321,7 @@ jobs:
|
||||
|
||||
# Notify the channel about scheduled runs, do not notify for manually triggered runs
|
||||
notify:
|
||||
name: Notify matrix
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- integration-tests
|
||||
@ -333,4 +336,4 @@ jobs:
|
||||
matrix_access_token: ${{ secrets.ELEMENT_ANDROID_NOTIFICATION_ACCESS_TOKEN }}
|
||||
matrix_room_id: ${{ secrets.ELEMENT_ANDROID_INTERNAL_ROOM_ID }}
|
||||
text_template: "Nightly test run: {{#each job_statuses }}{{#with this }}{{#if completed }} {{name}} {{conclusion}} at {{completed_at}}, {{/if}}{{/with}}{{/each}}"
|
||||
html_template: "Nightly test run results: {{#each job_statuses }}{{#with this }}{{#if completed }}<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
1
changelog.d/5408.misc
Normal file
@ -0,0 +1 @@
|
||||
Improved onboarding registration unit test coverage
|
1
changelog.d/5513.misc
Normal file
1
changelog.d/5513.misc
Normal file
@ -0,0 +1 @@
|
||||
Added online presence indicator attribute online to match offline styling
|
1
changelog.d/5536.feature
Normal file
1
changelog.d/5536.feature
Normal file
@ -0,0 +1 @@
|
||||
Live location sharing: adding build config field and show permission dialog
|
1
changelog.d/5551.bugfix
Normal file
1
changelog.d/5551.bugfix
Normal file
@ -0,0 +1 @@
|
||||
Fix local echos not being shown when re-opening rooms
|
1
changelog.d/5552.bugfix
Normal file
1
changelog.d/5552.bugfix
Normal file
@ -0,0 +1 @@
|
||||
Fix crash when closing a room while decrypting timeline events
|
1
changelog.d/5563.misc
Normal file
1
changelog.d/5563.misc
Normal file
@ -0,0 +1 @@
|
||||
Add a presence sync enabling build config
|
1
changelog.d/5571.feature
Normal file
1
changelog.d/5571.feature
Normal file
@ -0,0 +1 @@
|
||||
Live location sharing: Adding indicator view when enabled
|
2
changelog.d/5572.misc
Normal file
2
changelog.d/5572.misc
Normal file
@ -0,0 +1,2 @@
|
||||
Show stickers on click
|
||||
|
@ -122,6 +122,10 @@
|
||||
<color name="vctr_presence_indicator_offline_light">@color/palette_gray_100</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 -->
|
||||
<attr name="vctr_live_location" format="color" />
|
||||
<color name="vctr_live_location_light">@color/palette_prune</color>
|
||||
|
@ -53,5 +53,4 @@
|
||||
<color name="element_room_01">@color/palette_verde</color>
|
||||
<color name="element_room_02">@color/palette_azure</color>
|
||||
<color name="element_room_03">@color/palette_grape</color>
|
||||
|
||||
</resources>
|
11
library/ui-styles/src/main/res/values/styles_location.xml
Normal file
11
library/ui-styles/src/main/res/values/styles_location.xml
Normal 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>
|
@ -43,6 +43,7 @@
|
||||
|
||||
<!-- Presence Indicator colors -->
|
||||
<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 -->
|
||||
<item name="vctr_header_background">?vctr_system</item>
|
||||
|
@ -43,6 +43,7 @@
|
||||
|
||||
<!-- Presence Indicator colors -->
|
||||
<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 -->
|
||||
<item name="vctr_header_background">?vctr_system</item>
|
||||
|
@ -60,7 +60,11 @@ data class MatrixConfiguration(
|
||||
/**
|
||||
* RoomDisplayNameFallbackProvider to provide default room display name.
|
||||
*/
|
||||
val roomDisplayNameFallbackProvider: RoomDisplayNameFallbackProvider
|
||||
val roomDisplayNameFallbackProvider: RoomDisplayNameFallbackProvider,
|
||||
/**
|
||||
* True to enable presence information sync (if available). False to disable regardless of server setting.
|
||||
*/
|
||||
val presenceSyncEnabled: Boolean = true
|
||||
) {
|
||||
|
||||
/**
|
||||
|
@ -55,6 +55,7 @@ internal class RealmSendingEventsDataSource(
|
||||
roomEntity = RoomEntity.where(safeRealm, roomId = roomId).findFirst()
|
||||
sendingTimelineEvents = roomEntity?.sendingTimelineEvents
|
||||
sendingTimelineEvents?.addChangeListener(sendingTimelineEventsListener)
|
||||
updateFrozenResults(sendingTimelineEvents)
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
|
@ -100,8 +100,12 @@ internal class TimelineEventDecryptor @Inject constructor(
|
||||
}
|
||||
executor?.execute {
|
||||
Realm.getInstance(realmConfiguration).use { realm ->
|
||||
runBlocking {
|
||||
processDecryptRequest(request, realm)
|
||||
try {
|
||||
runBlocking {
|
||||
processDecryptRequest(request, realm)
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
Timber.i("Decryption got interrupted")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@
|
||||
package org.matrix.android.sdk.internal.session.sync.handler
|
||||
|
||||
import io.realm.Realm
|
||||
import org.matrix.android.sdk.api.MatrixConfiguration
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.getPresenceContent
|
||||
import org.matrix.android.sdk.api.session.sync.model.PresenceSyncResponse
|
||||
@ -27,27 +28,29 @@ import org.matrix.android.sdk.internal.database.query.updateDirectUserPresence
|
||||
import org.matrix.android.sdk.internal.database.query.updateUserPresence
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class PresenceSyncHandler @Inject constructor() {
|
||||
internal class PresenceSyncHandler @Inject constructor(private val matrixConfiguration: MatrixConfiguration) {
|
||||
|
||||
fun handle(realm: Realm, presenceSyncResponse: PresenceSyncResponse?) {
|
||||
presenceSyncResponse?.events
|
||||
?.filter { event -> event.type == EventType.PRESENCE }
|
||||
?.forEach { event ->
|
||||
val content = event.getPresenceContent() ?: return@forEach
|
||||
val userId = event.senderId ?: return@forEach
|
||||
val userPresenceEntity = UserPresenceEntity(
|
||||
userId = userId,
|
||||
lastActiveAgo = content.lastActiveAgo,
|
||||
statusMessage = content.statusMessage,
|
||||
isCurrentlyActive = content.isCurrentlyActive,
|
||||
avatarUrl = content.avatarUrl,
|
||||
displayName = content.displayName
|
||||
).also {
|
||||
it.presence = content.presence
|
||||
}
|
||||
if (matrixConfiguration.presenceSyncEnabled) {
|
||||
presenceSyncResponse?.events
|
||||
?.filter { event -> event.type == EventType.PRESENCE }
|
||||
?.forEach { event ->
|
||||
val content = event.getPresenceContent() ?: return@forEach
|
||||
val userId = event.senderId ?: return@forEach
|
||||
val userPresenceEntity = UserPresenceEntity(
|
||||
userId = userId,
|
||||
lastActiveAgo = content.lastActiveAgo,
|
||||
statusMessage = content.statusMessage,
|
||||
isCurrentlyActive = content.isCurrentlyActive,
|
||||
avatarUrl = content.avatarUrl,
|
||||
displayName = content.displayName
|
||||
).also {
|
||||
it.presence = content.presence
|
||||
}
|
||||
|
||||
storePresenceToDB(realm, userPresenceEntity)
|
||||
}
|
||||
storePresenceToDB(realm, userPresenceEntity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -13,32 +13,35 @@ print("::group::Arguments")
|
||||
print(f"{sys.argv}")
|
||||
print("::endgroup::")
|
||||
for xmlfile in xmlfiles:
|
||||
tree = ET.parse(xmlfile)
|
||||
try:
|
||||
tree = ET.parse(xmlfile)
|
||||
|
||||
root = tree.getroot()
|
||||
name = root.attrib['name']
|
||||
time = root.attrib['time']
|
||||
tests = int(root.attrib['tests'])
|
||||
skipped = int(root.attrib['skipped'])
|
||||
errors = int(root.attrib['errors'])
|
||||
failures = int(root.attrib['failures'])
|
||||
success = tests - failures - errors - skipped
|
||||
total = tests - skipped
|
||||
print(f"::group::{name} {success}/{total} ({skipped} skipped) in {time}")
|
||||
|
||||
for testcase in root:
|
||||
if testcase.tag != "testcase":
|
||||
continue
|
||||
testname = testcase.attrib['classname']
|
||||
message = testcase.attrib['name']
|
||||
time = testcase.attrib['time']
|
||||
child = testcase.find("failure")
|
||||
if child is None:
|
||||
print(f"{message} in {time}s")
|
||||
else:
|
||||
print(f"::error file={testname}::{message} in {time}s")
|
||||
print(child.text)
|
||||
body = f"passed={success} failures={failures} errors={errors} skipped={skipped}"
|
||||
print(f"::set-output name={suitename}::={body}")
|
||||
root = tree.getroot()
|
||||
name = root.attrib['name']
|
||||
time = root.attrib['time']
|
||||
tests = int(root.attrib['tests'])
|
||||
skipped = int(root.attrib['skipped'])
|
||||
errors = int(root.attrib['errors'])
|
||||
failures = int(root.attrib['failures'])
|
||||
success = tests - failures - errors - skipped
|
||||
total = tests - skipped
|
||||
print(f"::group::{name} {success}/{total} ({skipped} skipped) in {time}")
|
||||
|
||||
for testcase in root:
|
||||
if testcase.tag != "testcase":
|
||||
continue
|
||||
testname = testcase.attrib['classname']
|
||||
message = testcase.attrib['name']
|
||||
time = testcase.attrib['time']
|
||||
child = testcase.find("failure")
|
||||
if child is None:
|
||||
print(f"{message} in {time}s")
|
||||
else:
|
||||
print(f"::error file={testname}::{message} in {time}s")
|
||||
print(child.text)
|
||||
body = f" passed={success} failures={failures} errors={errors} skipped={skipped}"
|
||||
print(f"::set-output name={suitename}::={body}")
|
||||
except FileNotFoundError:
|
||||
print(f"::error::Unable to open test results file {xmlfile} - check if the tests completed")
|
||||
print("::endgroup::")
|
||||
|
||||
|
@ -2056,7 +2056,9 @@
|
||||
"disappear",
|
||||
"dissolve",
|
||||
"liquid",
|
||||
"melt"
|
||||
"melt",
|
||||
"hot",
|
||||
"heat"
|
||||
]
|
||||
},
|
||||
"winking-face": {
|
||||
@ -2351,7 +2353,10 @@
|
||||
"disbelief",
|
||||
"embarrass",
|
||||
"scared",
|
||||
"surprise"
|
||||
"surprise",
|
||||
"silence",
|
||||
"secret",
|
||||
"shock"
|
||||
]
|
||||
},
|
||||
"face-with-peeking-eye": {
|
||||
@ -2360,7 +2365,10 @@
|
||||
"j": [
|
||||
"captivated",
|
||||
"peep",
|
||||
"stare"
|
||||
"stare",
|
||||
"scared",
|
||||
"frightening",
|
||||
"embarrassing"
|
||||
]
|
||||
},
|
||||
"shushing-face": {
|
||||
@ -2392,7 +2400,8 @@
|
||||
"salute",
|
||||
"sunny",
|
||||
"troops",
|
||||
"yes"
|
||||
"yes",
|
||||
"respect"
|
||||
]
|
||||
},
|
||||
"zippermouth-face": {
|
||||
@ -2467,7 +2476,10 @@
|
||||
"disappear",
|
||||
"hide",
|
||||
"introvert",
|
||||
"invisible"
|
||||
"invisible",
|
||||
"lonely",
|
||||
"isolation",
|
||||
"depression"
|
||||
]
|
||||
},
|
||||
"face-in-clouds": {
|
||||
@ -2863,7 +2875,11 @@
|
||||
"disappointed",
|
||||
"meh",
|
||||
"skeptical",
|
||||
"unsure"
|
||||
"unsure",
|
||||
"skeptic",
|
||||
"confuse",
|
||||
"frustrated",
|
||||
"indifferent"
|
||||
]
|
||||
},
|
||||
"worried-face": {
|
||||
@ -2969,7 +2985,9 @@
|
||||
"cry",
|
||||
"proud",
|
||||
"resist",
|
||||
"sad"
|
||||
"sad",
|
||||
"touched",
|
||||
"gratitude"
|
||||
]
|
||||
},
|
||||
"frowning-face-with-open-mouth": {
|
||||
@ -4065,7 +4083,9 @@
|
||||
"j": [
|
||||
"hand",
|
||||
"right",
|
||||
"rightward"
|
||||
"rightward",
|
||||
"palm",
|
||||
"offer"
|
||||
]
|
||||
},
|
||||
"leftwards-hand": {
|
||||
@ -4074,7 +4094,9 @@
|
||||
"j": [
|
||||
"hand",
|
||||
"left",
|
||||
"leftward"
|
||||
"leftward",
|
||||
"palm",
|
||||
"offer"
|
||||
]
|
||||
},
|
||||
"palm-down-hand": {
|
||||
@ -4083,7 +4105,8 @@
|
||||
"j": [
|
||||
"dismiss",
|
||||
"drop",
|
||||
"shoo"
|
||||
"shoo",
|
||||
"palm"
|
||||
]
|
||||
},
|
||||
"palm-up-hand": {
|
||||
@ -4093,7 +4116,9 @@
|
||||
"beckon",
|
||||
"catch",
|
||||
"come",
|
||||
"offer"
|
||||
"offer",
|
||||
"lift",
|
||||
"demand"
|
||||
]
|
||||
},
|
||||
"ok-hand": {
|
||||
@ -4290,7 +4315,8 @@
|
||||
"b": "1FAF5",
|
||||
"j": [
|
||||
"point",
|
||||
"you"
|
||||
"you",
|
||||
"recruit"
|
||||
]
|
||||
},
|
||||
"thumbs-up": {
|
||||
@ -4404,7 +4430,9 @@
|
||||
"a": "⊛ Heart Hands",
|
||||
"b": "1FAF6",
|
||||
"j": [
|
||||
"love"
|
||||
"love",
|
||||
"appreciation",
|
||||
"support"
|
||||
]
|
||||
},
|
||||
"open-hands": {
|
||||
@ -4662,7 +4690,11 @@
|
||||
"flirting",
|
||||
"nervous",
|
||||
"uncomfortable",
|
||||
"worried"
|
||||
"worried",
|
||||
"flirt",
|
||||
"sexy",
|
||||
"pain",
|
||||
"worry"
|
||||
]
|
||||
},
|
||||
"baby": {
|
||||
@ -6058,7 +6090,8 @@
|
||||
"monarch",
|
||||
"noble",
|
||||
"regal",
|
||||
"royalty"
|
||||
"royalty",
|
||||
"power"
|
||||
]
|
||||
},
|
||||
"prince": {
|
||||
@ -6231,7 +6264,8 @@
|
||||
"belly",
|
||||
"bloated",
|
||||
"full",
|
||||
"pregnant"
|
||||
"pregnant",
|
||||
"baby"
|
||||
]
|
||||
},
|
||||
"pregnant-person": {
|
||||
@ -6241,7 +6275,8 @@
|
||||
"belly",
|
||||
"bloated",
|
||||
"full",
|
||||
"pregnant"
|
||||
"pregnant",
|
||||
"baby"
|
||||
]
|
||||
},
|
||||
"breastfeeding": {
|
||||
@ -6635,7 +6670,8 @@
|
||||
"j": [
|
||||
"fairy tale",
|
||||
"fantasy",
|
||||
"monster"
|
||||
"monster",
|
||||
"mystical"
|
||||
]
|
||||
},
|
||||
"person-getting-massage": {
|
||||
@ -9374,7 +9410,8 @@
|
||||
"b": "1FAB8",
|
||||
"j": [
|
||||
"ocean",
|
||||
"reef"
|
||||
"reef",
|
||||
"sea"
|
||||
]
|
||||
},
|
||||
"snail": {
|
||||
@ -9587,7 +9624,9 @@
|
||||
"Hinduism",
|
||||
"India",
|
||||
"purity",
|
||||
"Vietnam"
|
||||
"Vietnam",
|
||||
"calm",
|
||||
"meditation"
|
||||
]
|
||||
},
|
||||
"rosette": {
|
||||
@ -9832,14 +9871,16 @@
|
||||
"a": "⊛ Empty Nest",
|
||||
"b": "1FAB9",
|
||||
"j": [
|
||||
"nesting"
|
||||
"nesting",
|
||||
"bird"
|
||||
]
|
||||
},
|
||||
"nest-with-eggs": {
|
||||
"a": "⊛ Nest with Eggs",
|
||||
"b": "1FABA",
|
||||
"j": [
|
||||
"nesting"
|
||||
"nesting",
|
||||
"bird"
|
||||
]
|
||||
},
|
||||
"grapes": {
|
||||
@ -11187,7 +11228,9 @@
|
||||
"drink",
|
||||
"empty",
|
||||
"glass",
|
||||
"spill"
|
||||
"spill",
|
||||
"cup",
|
||||
"water"
|
||||
]
|
||||
},
|
||||
"cup-with-straw": {
|
||||
@ -12003,7 +12046,9 @@
|
||||
"b": "1F6DD",
|
||||
"j": [
|
||||
"amusement park",
|
||||
"play"
|
||||
"play",
|
||||
"fun",
|
||||
"park"
|
||||
]
|
||||
},
|
||||
"ferris-wheel": {
|
||||
@ -12533,7 +12578,9 @@
|
||||
"j": [
|
||||
"circle",
|
||||
"tire",
|
||||
"turn"
|
||||
"turn",
|
||||
"car",
|
||||
"transport"
|
||||
]
|
||||
},
|
||||
"police-car-light": {
|
||||
@ -14666,7 +14713,8 @@
|
||||
"hand",
|
||||
"Mary",
|
||||
"Miriam",
|
||||
"protection"
|
||||
"protection",
|
||||
"religion"
|
||||
]
|
||||
},
|
||||
"video-game": {
|
||||
@ -15864,7 +15912,9 @@
|
||||
"b": "1FAAB",
|
||||
"j": [
|
||||
"electronic",
|
||||
"low energy"
|
||||
"low energy",
|
||||
"drained",
|
||||
"dead"
|
||||
]
|
||||
},
|
||||
"electric-plug": {
|
||||
@ -17508,7 +17558,9 @@
|
||||
"disability",
|
||||
"hurt",
|
||||
"mobility aid",
|
||||
"stick"
|
||||
"stick",
|
||||
"accessibility",
|
||||
"assist"
|
||||
]
|
||||
},
|
||||
"stethoscope": {
|
||||
@ -17528,7 +17580,9 @@
|
||||
"bones",
|
||||
"doctor",
|
||||
"medical",
|
||||
"skeleton"
|
||||
"skeleton",
|
||||
"x-ray",
|
||||
"medicine"
|
||||
]
|
||||
},
|
||||
"door": {
|
||||
@ -17733,7 +17787,10 @@
|
||||
"burp",
|
||||
"clean",
|
||||
"soap",
|
||||
"underwater"
|
||||
"underwater",
|
||||
"fun",
|
||||
"carbonation",
|
||||
"sparkling"
|
||||
]
|
||||
},
|
||||
"toothbrush": {
|
||||
@ -17856,7 +17913,8 @@
|
||||
"credentials",
|
||||
"ID",
|
||||
"license",
|
||||
"security"
|
||||
"security",
|
||||
"document"
|
||||
]
|
||||
},
|
||||
"atm-sign": {
|
||||
|
@ -151,6 +151,7 @@ android {
|
||||
|
||||
buildConfigField "Boolean", "enableLocationSharing", "true"
|
||||
buildConfigField "String", "mapTilerKey", "\"fU3vlMsMn4Jb6dnEIFsx\""
|
||||
buildConfigField "Boolean", "PRESENCE_SYNC_ENABLED", "true"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@ -229,6 +230,7 @@ android {
|
||||
buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false"
|
||||
// Set to true if you want to enable strict mode in debug
|
||||
buildConfigField "boolean", "ENABLE_STRICT_MODE_LOGS", "false"
|
||||
buildConfigField "Boolean", "ENABLE_LIVE_LOCATION_SHARING", "true"
|
||||
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
@ -238,6 +240,7 @@ android {
|
||||
|
||||
buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false"
|
||||
buildConfigField "boolean", "ENABLE_STRICT_MODE_LOGS", "false"
|
||||
buildConfigField "Boolean", "ENABLE_LIVE_LOCATION_SHARING", "false"
|
||||
|
||||
postprocessing {
|
||||
removeUnusedCode true
|
||||
|
@ -45,6 +45,7 @@
|
||||
<!-- Location Sharing -->
|
||||
<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_BACKGROUND_LOCATION" />
|
||||
|
||||
<!-- 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" />
|
||||
|
@ -116,7 +116,8 @@ object VectorStaticModule {
|
||||
fun providesMatrixConfiguration(vectorRoomDisplayNameFallbackProvider: VectorRoomDisplayNameFallbackProvider): MatrixConfiguration {
|
||||
return MatrixConfiguration(
|
||||
applicationFlavor = BuildConfig.FLAVOR_DESCRIPTION,
|
||||
roomDisplayNameFallbackProvider = vectorRoomDisplayNameFallbackProvider
|
||||
roomDisplayNameFallbackProvider = vectorRoomDisplayNameFallbackProvider,
|
||||
presenceSyncEnabled = BuildConfig.PRESENCE_SYNC_ENABLED
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -25,10 +25,11 @@ import org.matrix.android.sdk.api.session.presence.model.UserPresence
|
||||
@EpoxyModelClass(layout = R.layout.item_profile_matrix_item)
|
||||
abstract class ProfileMatrixItemWithPowerLevelWithPresence : ProfileMatrixItemWithPowerLevel() {
|
||||
|
||||
@EpoxyAttribute var showPresence: Boolean = true
|
||||
@EpoxyAttribute var userPresence: UserPresence? = null
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
holder.presenceImageView.render(userPresence = userPresence)
|
||||
holder.presenceImageView.render(showPresence, userPresence)
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ package im.vector.app.core.utils
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
@ -32,6 +33,7 @@ import im.vector.app.R
|
||||
import im.vector.app.core.platform.VectorBaseActivity
|
||||
|
||||
// Permissions sets
|
||||
val PERMISSIONS_EMPTY = emptyList<String>()
|
||||
val PERMISSIONS_FOR_AUDIO_IP_CALL = listOf(Manifest.permission.RECORD_AUDIO)
|
||||
val PERMISSIONS_FOR_VIDEO_IP_CALL = listOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
|
||||
val PERMISSIONS_FOR_VOICE_MESSAGE = listOf(Manifest.permission.RECORD_AUDIO)
|
||||
@ -40,9 +42,12 @@ val PERMISSIONS_FOR_MEMBERS_SEARCH = listOf(Manifest.permission.READ_CONTACTS)
|
||||
val PERMISSIONS_FOR_ROOM_AVATAR = listOf(Manifest.permission.CAMERA)
|
||||
val PERMISSIONS_FOR_WRITING_FILES = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
val PERMISSIONS_FOR_PICKING_CONTACT = listOf(Manifest.permission.READ_CONTACTS)
|
||||
val PERMISSIONS_FOR_LOCATION_SHARING = listOf(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
|
||||
val PERMISSIONS_EMPTY = emptyList<String>()
|
||||
val PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING = listOf(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
val PERMISSIONS_FOR_BACKGROUND_LOCATION_SHARING = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
listOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
||||
} else {
|
||||
PERMISSIONS_EMPTY
|
||||
}
|
||||
|
||||
// This is not ideal to store the value like that, but it works
|
||||
private var permissionDialogDisplayed = false
|
||||
@ -123,6 +128,7 @@ fun checkPermissions(permissionsToBeGranted: List<String>,
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
activityResultLauncher.launch(missingPermissions.toTypedArray())
|
||||
}
|
||||
.setNegativeButton(R.string.action_not_now, null)
|
||||
.show()
|
||||
} else {
|
||||
// some permissions are not granted, ask permissions
|
||||
|
@ -37,7 +37,7 @@ import androidx.core.view.isVisible
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.onClick
|
||||
import im.vector.app.core.utils.PERMISSIONS_EMPTY
|
||||
import im.vector.app.core.utils.PERMISSIONS_FOR_LOCATION_SHARING
|
||||
import im.vector.app.core.utils.PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING
|
||||
import im.vector.app.core.utils.PERMISSIONS_FOR_PICKING_CONTACT
|
||||
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
|
||||
import im.vector.app.databinding.ViewAttachmentTypeSelectorBinding
|
||||
@ -215,6 +215,6 @@ class AttachmentTypeSelectorView(context: Context,
|
||||
STICKER(PERMISSIONS_EMPTY, R.string.tooltip_attachment_sticker),
|
||||
CONTACT(PERMISSIONS_FOR_PICKING_CONTACT, R.string.tooltip_attachment_contact),
|
||||
POLL(PERMISSIONS_EMPTY, R.string.tooltip_attachment_poll),
|
||||
LOCATION(PERMISSIONS_FOR_LOCATION_SHARING, R.string.tooltip_attachment_location)
|
||||
LOCATION(PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING, R.string.tooltip_attachment_location)
|
||||
}
|
||||
}
|
||||
|
@ -177,6 +177,7 @@ import im.vector.app.features.html.PillsPostProcessor
|
||||
import im.vector.app.features.invite.VectorInviteView
|
||||
import im.vector.app.features.location.LocationSharingMode
|
||||
import im.vector.app.features.location.toLocationData
|
||||
import im.vector.app.features.media.AttachmentData
|
||||
import im.vector.app.features.media.ImageContentRenderer
|
||||
import im.vector.app.features.media.VideoContentRenderer
|
||||
import im.vector.app.features.notifications.NotificationDrawerManager
|
||||
@ -206,6 +207,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.billcarsonfr.jsonviewer.JSonViewerDialog
|
||||
import org.commonmark.parser.Parser
|
||||
import org.matrix.android.sdk.api.MatrixConfiguration
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
@ -260,7 +262,8 @@ class TimelineFragment @Inject constructor(
|
||||
private val pillsPostProcessorFactory: PillsPostProcessor.Factory,
|
||||
private val callManager: WebRtcCallManager,
|
||||
private val audioMessagePlaybackTracker: AudioMessagePlaybackTracker,
|
||||
private val clock: Clock
|
||||
private val clock: Clock,
|
||||
private val matrixConfiguration: MatrixConfiguration
|
||||
) :
|
||||
VectorBaseFragment<FragmentTimelineBinding>(),
|
||||
TimelineEventController.Callback,
|
||||
@ -1163,7 +1166,6 @@ class TimelineFragment @Inject constructor(
|
||||
views.composerLayout.views.sendButton.contentDescription = getString(R.string.action_send)
|
||||
}
|
||||
|
||||
// TODO: Test this
|
||||
private fun renderSpecialMode(event: TimelineEvent,
|
||||
@DrawableRes iconRes: Int,
|
||||
@StringRes descriptionRes: Int,
|
||||
@ -1627,7 +1629,10 @@ class TimelineFragment @Inject constructor(
|
||||
views.includeRoomToolbar.roomToolbarTitleView.text = roomSummary.displayName
|
||||
avatarRenderer.render(roomSummary.toMatrixItem(), views.includeRoomToolbar.roomToolbarAvatarImageView)
|
||||
views.includeRoomToolbar.roomToolbarDecorationImageView.render(roomSummary.roomEncryptionTrustLevel)
|
||||
views.includeRoomToolbar.roomToolbarPresenceImageView.render(roomSummary.isDirect, roomSummary.directUserPresence)
|
||||
views.includeRoomToolbar.roomToolbarPresenceImageView.render(
|
||||
roomSummary.isDirect && matrixConfiguration.presenceSyncEnabled,
|
||||
roomSummary.directUserPresence
|
||||
)
|
||||
views.includeRoomToolbar.roomToolbarPublicImageView.isVisible = roomSummary.isPublic && !roomSummary.isDirect
|
||||
}
|
||||
} else {
|
||||
@ -1882,12 +1887,16 @@ class TimelineFragment @Inject constructor(
|
||||
vectorBaseActivity.notImplemented("encrypted message click")
|
||||
}
|
||||
|
||||
override fun onImageMessageClicked(messageImageContent: MessageImageInfoContent, mediaData: ImageContentRenderer.Data, view: View) {
|
||||
override fun onImageMessageClicked(messageImageContent: MessageImageInfoContent,
|
||||
mediaData: ImageContentRenderer.Data,
|
||||
view: View,
|
||||
inMemory: List<AttachmentData>) {
|
||||
navigator.openMediaViewer(
|
||||
activity = requireActivity(),
|
||||
roomId = timelineArgs.roomId,
|
||||
mediaData = mediaData,
|
||||
view = view
|
||||
view = view,
|
||||
inMemory = inMemory
|
||||
) { pairs ->
|
||||
pairs.add(Pair(views.roomToolbar, ViewCompat.getTransitionName(views.roomToolbar) ?: ""))
|
||||
pairs.add(Pair(views.composerLayout, ViewCompat.getTransitionName(views.composerLayout) ?: ""))
|
||||
|
@ -57,6 +57,7 @@ import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryEve
|
||||
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
|
||||
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem
|
||||
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
|
||||
import im.vector.app.features.media.AttachmentData
|
||||
import im.vector.app.features.media.ImageContentRenderer
|
||||
import im.vector.app.features.media.VideoContentRenderer
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
@ -127,7 +128,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
fun onEventVisible(event: TimelineEvent)
|
||||
fun onRoomCreateLinkClicked(url: String)
|
||||
fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View)
|
||||
fun onImageMessageClicked(messageImageContent: MessageImageInfoContent, mediaData: ImageContentRenderer.Data, view: View)
|
||||
fun onImageMessageClicked(messageImageContent: MessageImageInfoContent,
|
||||
mediaData: ImageContentRenderer.Data,
|
||||
view: View,
|
||||
inMemory: List<AttachmentData>)
|
||||
fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View)
|
||||
|
||||
// fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent)
|
||||
|
@ -474,9 +474,12 @@ class MessageItemFactory @Inject constructor(
|
||||
.apply {
|
||||
if (messageContent.msgType == MessageType.MSGTYPE_STICKER_LOCAL) {
|
||||
mode(ImageContentRenderer.Mode.STICKER)
|
||||
clickListener { view ->
|
||||
callback?.onImageMessageClicked(messageContent, data, view, listOf(data))
|
||||
}
|
||||
} else {
|
||||
clickListener { view ->
|
||||
callback?.onImageMessageClicked(messageContent, data, view)
|
||||
callback?.onImageMessageClicked(messageContent, data, view, emptyList())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter
|
||||
import im.vector.app.features.home.room.typing.TypingHelper
|
||||
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
|
||||
import org.matrix.android.sdk.api.MatrixConfiguration
|
||||
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
@ -41,7 +42,8 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
|
||||
private val stringProvider: StringProvider,
|
||||
private val typingHelper: TypingHelper,
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val errorFormatter: ErrorFormatter) {
|
||||
private val errorFormatter: ErrorFormatter,
|
||||
private val matrixConfiguration: MatrixConfiguration) {
|
||||
|
||||
fun create(roomSummary: RoomSummary,
|
||||
roomChangeMembershipStates: Map<String, ChangeMembershipState>,
|
||||
@ -125,7 +127,7 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
|
||||
// We do not display shield in the room list anymore
|
||||
// .encryptionTrustLevel(roomSummary.roomEncryptionTrustLevel)
|
||||
.izPublic(roomSummary.isPublic)
|
||||
.showPresence(roomSummary.isDirect)
|
||||
.showPresence(roomSummary.isDirect && matrixConfiguration.presenceSyncEnabled)
|
||||
.userPresence(roomSummary.directUserPresence)
|
||||
.matrixItem(roomSummary.toMatrixItem())
|
||||
.lastEventTime(latestEventTime)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -23,4 +23,5 @@ sealed class LocationSharingAction : VectorViewModelAction {
|
||||
data class PinnedLocationSharing(val locationData: LocationData?) : LocationSharingAction()
|
||||
data class LocationTargetChange(val locationData: LocationData) : LocationSharingAction()
|
||||
object ZoomToUserLocation : LocationSharingAction()
|
||||
object StartLiveLocationSharing : LocationSharingAction()
|
||||
}
|
||||
|
@ -27,9 +27,14 @@ import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.mapbox.mapboxsdk.maps.MapView
|
||||
import im.vector.app.BuildConfig
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.core.utils.PERMISSIONS_FOR_BACKGROUND_LOCATION_SHARING
|
||||
import im.vector.app.core.utils.PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING
|
||||
import im.vector.app.core.utils.checkPermissions
|
||||
import im.vector.app.core.utils.registerForPermissionsResult
|
||||
import im.vector.app.databinding.FragmentLocationSharingBinding
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
|
||||
@ -49,6 +54,8 @@ class LocationSharingFragment @Inject constructor(
|
||||
|
||||
private val viewModel: LocationSharingViewModel by fragmentViewModel()
|
||||
|
||||
private val locationSharingNavigator: LocationSharingNavigator by lazy { DefaultLocationSharingNavigator(activity) }
|
||||
|
||||
// Keep a ref to handle properly the onDestroy callback
|
||||
private var mapView: WeakReference<MapView>? = null
|
||||
|
||||
@ -76,8 +83,8 @@ class LocationSharingFragment @Inject constructor(
|
||||
|
||||
viewModel.observeViewEvents {
|
||||
when (it) {
|
||||
LocationSharingViewEvents.Close -> locationSharingNavigator.quit()
|
||||
LocationSharingViewEvents.LocationNotAvailableError -> handleLocationNotAvailableError()
|
||||
LocationSharingViewEvents.Close -> activity?.finish()
|
||||
is LocationSharingViewEvents.ZoomToUserLocation -> handleZoomToUserLocationEvent(it)
|
||||
}.exhaustive
|
||||
}
|
||||
@ -86,6 +93,11 @@ class LocationSharingFragment @Inject constructor(
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
views.mapView.onResume()
|
||||
if (locationSharingNavigator.goingToAppSettings) {
|
||||
locationSharingNavigator.goingToAppSettings = false
|
||||
// retry to start live location
|
||||
tryStartLiveLocationSharing()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
@ -137,12 +149,24 @@ class LocationSharingFragment @Inject constructor(
|
||||
.setTitle(R.string.location_not_available_dialog_title)
|
||||
.setMessage(R.string.location_not_available_dialog_content)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
activity?.finish()
|
||||
locationSharingNavigator.quit()
|
||||
}
|
||||
.setCancelable(false)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun handleMissingBackgroundLocationPermission() {
|
||||
MaterialAlertDialogBuilder(requireActivity())
|
||||
.setTitle(R.string.location_in_background_missing_permission_dialog_title)
|
||||
.setMessage(R.string.location_in_background_missing_permission_dialog_content)
|
||||
.setPositiveButton(R.string.settings) { _, _ ->
|
||||
locationSharingNavigator.goToAppSettings()
|
||||
}
|
||||
.setNegativeButton(R.string.action_not_now, null)
|
||||
.setCancelable(false)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun initLocateButton() {
|
||||
views.mapView.locateButton.setOnClickListener {
|
||||
viewModel.handle(LocationSharingAction.ZoomToUserLocation)
|
||||
@ -164,22 +188,58 @@ class LocationSharingFragment @Inject constructor(
|
||||
viewModel.handle(LocationSharingAction.CurrentUserLocationSharing)
|
||||
}
|
||||
views.shareLocationOptionsPicker.optionUserLive.debouncedClicks {
|
||||
// TODO
|
||||
tryStartLiveLocationSharing()
|
||||
}
|
||||
}
|
||||
|
||||
private val foregroundLocationResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently ->
|
||||
if (allGranted && checkPermissions(PERMISSIONS_FOR_BACKGROUND_LOCATION_SHARING, requireActivity(), backgroundLocationResultLauncher)) {
|
||||
startLiveLocationSharing()
|
||||
} else if (deniedPermanently) {
|
||||
handleMissingBackgroundLocationPermission()
|
||||
}
|
||||
}
|
||||
|
||||
private val backgroundLocationResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently ->
|
||||
if (allGranted) {
|
||||
startLiveLocationSharing()
|
||||
} else if (deniedPermanently) {
|
||||
handleMissingBackgroundLocationPermission()
|
||||
}
|
||||
}
|
||||
|
||||
private fun tryStartLiveLocationSharing() {
|
||||
// we need to re-check foreground location to be sure it has not changed after landing on this screen
|
||||
if (checkPermissions(PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING, requireActivity(), foregroundLocationResultLauncher) &&
|
||||
checkPermissions(
|
||||
PERMISSIONS_FOR_BACKGROUND_LOCATION_SHARING,
|
||||
requireActivity(),
|
||||
backgroundLocationResultLauncher,
|
||||
R.string.location_in_background_missing_permission_dialog_content
|
||||
)) {
|
||||
startLiveLocationSharing()
|
||||
}
|
||||
}
|
||||
|
||||
private fun startLiveLocationSharing() {
|
||||
viewModel.handle(LocationSharingAction.StartLiveLocationSharing)
|
||||
}
|
||||
|
||||
private fun updateMap(state: LocationSharingViewState) {
|
||||
// first, update the options view
|
||||
when (state.areTargetAndUserLocationEqual) {
|
||||
// TODO activate USER_LIVE option when implemented
|
||||
true -> views.shareLocationOptionsPicker.render(
|
||||
LocationSharingOption.USER_CURRENT
|
||||
)
|
||||
false -> views.shareLocationOptionsPicker.render(
|
||||
LocationSharingOption.PINNED
|
||||
)
|
||||
else -> views.shareLocationOptionsPicker.render()
|
||||
val options: Set<LocationSharingOption> = when (state.areTargetAndUserLocationEqual) {
|
||||
true -> {
|
||||
if (BuildConfig.ENABLE_LIVE_LOCATION_SHARING) {
|
||||
setOf(LocationSharingOption.USER_CURRENT, LocationSharingOption.USER_LIVE)
|
||||
} else {
|
||||
setOf(LocationSharingOption.USER_CURRENT)
|
||||
}
|
||||
}
|
||||
false -> setOf(LocationSharingOption.PINNED)
|
||||
else -> emptySet()
|
||||
}
|
||||
views.shareLocationOptionsPicker.render(options)
|
||||
|
||||
// then, update the map using the height of the options view after it has been rendered
|
||||
views.shareLocationOptionsPicker.post {
|
||||
val mapState = state
|
||||
|
@ -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()
|
||||
}
|
@ -38,6 +38,7 @@ import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Sampling period to compare target location and user location.
|
||||
@ -120,6 +121,7 @@ class LocationSharingViewModel @AssistedInject constructor(
|
||||
is LocationSharingAction.PinnedLocationSharing -> handlePinnedLocationSharingAction(action)
|
||||
is LocationSharingAction.LocationTargetChange -> handleLocationTargetChangeAction(action)
|
||||
LocationSharingAction.ZoomToUserLocation -> handleZoomToUserLocationAction()
|
||||
LocationSharingAction.StartLiveLocationSharing -> handleStartLiveLocationSharingAction()
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
@ -157,6 +159,11 @@ class LocationSharingViewModel @AssistedInject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleStartLiveLocationSharingAction() {
|
||||
// TODO start sharing live location and update view state
|
||||
Timber.d("live location sharing started")
|
||||
}
|
||||
|
||||
override fun onLocationUpdate(locationData: LocationData) {
|
||||
setState {
|
||||
copy(lastKnownUserLocation = locationData)
|
||||
|
@ -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
|
||||
}
|
@ -58,7 +58,7 @@ class LocationSharingOptionPickerView @JvmOverloads constructor(
|
||||
applyBackground()
|
||||
}
|
||||
|
||||
fun render(vararg options: LocationSharingOption) {
|
||||
fun render(options: Set<LocationSharingOption> = emptySet()) {
|
||||
val optionsNumber = options.toSet().size
|
||||
val isPinnedVisible = options.contains(LocationSharingOption.PINNED)
|
||||
val isUserCurrentVisible = options.contains(LocationSharingOption.USER_CURRENT)
|
||||
|
@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.file.FileService
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
|
||||
@ -52,7 +53,10 @@ class RoomEventsAttachmentProvider(
|
||||
|
||||
override fun getAttachmentInfoAt(position: Int): AttachmentInfo {
|
||||
return getItem(position).let {
|
||||
val content = it.root.getClearContent().toModel<MessageContent>() as? MessageWithAttachmentContent
|
||||
val clearContent = it.root.getClearContent()
|
||||
val content = clearContent.toModel<MessageContent>()
|
||||
?: clearContent.toModel<MessageStickerContent>()
|
||||
as? MessageWithAttachmentContent
|
||||
if (content is MessageImageContent) {
|
||||
val data = ImageContentRenderer.Data(
|
||||
eventId = it.eventId,
|
||||
@ -66,6 +70,33 @@ class RoomEventsAttachmentProvider(
|
||||
height = null,
|
||||
allowNonMxcUrls = it.root.sendState.isSending()
|
||||
|
||||
)
|
||||
if (content.mimeType == MimeTypes.Gif) {
|
||||
AttachmentInfo.AnimatedImage(
|
||||
uid = it.eventId,
|
||||
url = content.url ?: "",
|
||||
data = data
|
||||
)
|
||||
} else {
|
||||
AttachmentInfo.Image(
|
||||
uid = it.eventId,
|
||||
url = content.url ?: "",
|
||||
data = data
|
||||
)
|
||||
}
|
||||
} else if (content is MessageStickerContent) {
|
||||
val data = ImageContentRenderer.Data(
|
||||
eventId = it.eventId,
|
||||
filename = content.body,
|
||||
mimeType = content.mimeType,
|
||||
url = content.getFileUrl(),
|
||||
elementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt(),
|
||||
maxHeight = -1,
|
||||
maxWidth = -1,
|
||||
width = null,
|
||||
height = null,
|
||||
allowNonMxcUrls = false
|
||||
|
||||
)
|
||||
if (content.mimeType == MimeTypes.Gif) {
|
||||
AttachmentInfo.AnimatedImage(
|
||||
|
@ -22,63 +22,49 @@ import im.vector.app.features.login.LoginConfig
|
||||
import im.vector.app.features.login.ServerType
|
||||
import im.vector.app.features.login.SignMode
|
||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
|
||||
import org.matrix.android.sdk.internal.network.ssl.Fingerprint
|
||||
|
||||
sealed class OnboardingAction : VectorViewModelAction {
|
||||
data class OnGetStarted(val resetLoginConfig: Boolean, val onboardingFlow: OnboardingFlow) : OnboardingAction()
|
||||
data class OnIAlreadyHaveAnAccount(val resetLoginConfig: Boolean, val onboardingFlow: OnboardingFlow) : OnboardingAction()
|
||||
sealed interface OnboardingAction : VectorViewModelAction {
|
||||
data class OnGetStarted(val resetLoginConfig: Boolean, val onboardingFlow: OnboardingFlow) : OnboardingAction
|
||||
data class OnIAlreadyHaveAnAccount(val resetLoginConfig: Boolean, val onboardingFlow: OnboardingFlow) : OnboardingAction
|
||||
|
||||
data class UpdateServerType(val serverType: ServerType) : OnboardingAction()
|
||||
data class UpdateHomeServer(val homeServerUrl: String) : OnboardingAction()
|
||||
data class UpdateUseCase(val useCase: FtueUseCase) : OnboardingAction()
|
||||
object ResetUseCase : OnboardingAction()
|
||||
data class UpdateSignMode(val signMode: SignMode) : OnboardingAction()
|
||||
data class LoginWithToken(val loginToken: String) : OnboardingAction()
|
||||
data class WebLoginSuccess(val credentials: Credentials) : OnboardingAction()
|
||||
data class InitWith(val loginConfig: LoginConfig?) : OnboardingAction()
|
||||
data class ResetPassword(val email: String, val newPassword: String) : OnboardingAction()
|
||||
object ResetPasswordMailConfirmed : OnboardingAction()
|
||||
data class UpdateServerType(val serverType: ServerType) : OnboardingAction
|
||||
data class UpdateHomeServer(val homeServerUrl: String) : OnboardingAction
|
||||
data class UpdateUseCase(val useCase: FtueUseCase) : OnboardingAction
|
||||
object ResetUseCase : OnboardingAction
|
||||
data class UpdateSignMode(val signMode: SignMode) : OnboardingAction
|
||||
data class LoginWithToken(val loginToken: String) : OnboardingAction
|
||||
data class WebLoginSuccess(val credentials: Credentials) : OnboardingAction
|
||||
data class InitWith(val loginConfig: LoginConfig?) : OnboardingAction
|
||||
data class ResetPassword(val email: String, val newPassword: String) : OnboardingAction
|
||||
object ResetPasswordMailConfirmed : OnboardingAction
|
||||
|
||||
// Login or Register, depending on the signMode
|
||||
data class LoginOrRegister(val username: String, val password: String, val initialDeviceName: String) : OnboardingAction()
|
||||
data class LoginOrRegister(val username: String, val password: String, val initialDeviceName: String) : OnboardingAction
|
||||
object StopEmailValidationCheck : OnboardingAction
|
||||
|
||||
// Register actions
|
||||
open class RegisterAction : OnboardingAction()
|
||||
|
||||
data class AddThreePid(val threePid: RegisterThreePid) : RegisterAction()
|
||||
object SendAgainThreePid : RegisterAction()
|
||||
|
||||
// TODO Confirm Email (from link in the email, open in the phone, intercepted by the app)
|
||||
data class ValidateThreePid(val code: String) : RegisterAction()
|
||||
|
||||
data class CheckIfEmailHasBeenValidated(val delayMillis: Long) : RegisterAction()
|
||||
object StopEmailValidationCheck : RegisterAction()
|
||||
|
||||
data class CaptchaDone(val captchaResponse: String) : RegisterAction()
|
||||
object AcceptTerms : RegisterAction()
|
||||
object RegisterDummy : RegisterAction()
|
||||
data class PostRegisterAction(val registerAction: RegisterAction) : OnboardingAction
|
||||
|
||||
// Reset actions
|
||||
open class ResetAction : OnboardingAction()
|
||||
sealed interface ResetAction : OnboardingAction
|
||||
|
||||
object ResetHomeServerType : ResetAction()
|
||||
object ResetHomeServerUrl : ResetAction()
|
||||
object ResetSignMode : ResetAction()
|
||||
object ResetLogin : ResetAction()
|
||||
object ResetResetPassword : ResetAction()
|
||||
object ResetHomeServerType : ResetAction
|
||||
object ResetHomeServerUrl : ResetAction
|
||||
object ResetSignMode : ResetAction
|
||||
object ResetLogin : ResetAction
|
||||
object ResetResetPassword : ResetAction
|
||||
|
||||
// Homeserver history
|
||||
object ClearHomeServerHistory : OnboardingAction()
|
||||
object ClearHomeServerHistory : OnboardingAction
|
||||
|
||||
data class PostViewEvent(val viewEvent: OnboardingViewEvents) : OnboardingAction()
|
||||
data class PostViewEvent(val viewEvent: OnboardingViewEvents) : OnboardingAction
|
||||
|
||||
data class UserAcceptCertificate(val fingerprint: Fingerprint) : OnboardingAction()
|
||||
data class UserAcceptCertificate(val fingerprint: Fingerprint) : OnboardingAction
|
||||
|
||||
object PersonalizeProfile : OnboardingAction()
|
||||
data class UpdateDisplayName(val displayName: String) : OnboardingAction()
|
||||
object UpdateDisplayNameSkipped : OnboardingAction()
|
||||
data class ProfilePictureSelected(val uri: Uri) : OnboardingAction()
|
||||
object SaveSelectedProfilePicture : OnboardingAction()
|
||||
object UpdateProfilePictureSkipped : OnboardingAction()
|
||||
object PersonalizeProfile : OnboardingAction
|
||||
data class UpdateDisplayName(val displayName: String) : OnboardingAction
|
||||
object UpdateDisplayNameSkipped : OnboardingAction
|
||||
data class ProfilePictureSelected(val uri: Uri) : OnboardingAction
|
||||
object SaveSelectedProfilePicture : OnboardingAction
|
||||
object UpdateProfilePictureSkipped : OnboardingAction
|
||||
}
|
||||
|
@ -83,6 +83,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||
private val vectorFeatures: VectorFeatures,
|
||||
private val analyticsTracker: AnalyticsTracker,
|
||||
private val uriFilenameResolver: UriFilenameResolver,
|
||||
private val registrationActionHandler: RegistrationActionHandler,
|
||||
private val vectorOverrides: VectorOverrides
|
||||
) : VectorViewModel<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 registrationWizard: RegistrationWizard
|
||||
get() = authenticationService.getRegistrationWizard()
|
||||
|
||||
val currentThreePid: String?
|
||||
get() = registrationWizard?.currentThreePid
|
||||
get() = registrationWizard.currentThreePid
|
||||
|
||||
// True when login and password has been sent with success to the homeserver
|
||||
val isRegistrationStarted: Boolean
|
||||
get() = authenticationService.isRegistrationStarted
|
||||
|
||||
private val registrationWizard: RegistrationWizard?
|
||||
get() = authenticationService.getRegistrationWizard()
|
||||
|
||||
private val loginWizard: LoginWizard?
|
||||
get() = authenticationService.getLoginWizard()
|
||||
|
||||
@ -153,7 +154,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||
is OnboardingAction.WebLoginSuccess -> handleWebLoginSuccess(action)
|
||||
is OnboardingAction.ResetPassword -> handleResetPassword(action)
|
||||
is OnboardingAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed()
|
||||
is OnboardingAction.RegisterAction -> handleRegisterAction(action)
|
||||
is OnboardingAction.PostRegisterAction -> handleRegisterAction(action.registerAction)
|
||||
is OnboardingAction.ResetAction -> handleResetAction(action)
|
||||
is OnboardingAction.UserAcceptCertificate -> handleUserAcceptCertificate(action)
|
||||
OnboardingAction.ClearHomeServerHistory -> handleClearHomeServerHistory()
|
||||
@ -164,6 +165,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||
is OnboardingAction.ProfilePictureSelected -> handleProfilePictureSelected(action)
|
||||
OnboardingAction.SaveSelectedProfilePicture -> updateProfilePicture()
|
||||
is OnboardingAction.PostViewEvent -> _viewEvents.post(action.viewEvent)
|
||||
OnboardingAction.StopEmailValidationCheck -> cancelWaitForEmailValidation()
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
@ -266,131 +268,41 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRegisterAction(action: OnboardingAction.RegisterAction) {
|
||||
when (action) {
|
||||
is OnboardingAction.CaptchaDone -> handleCaptchaDone(action)
|
||||
is OnboardingAction.AcceptTerms -> handleAcceptTerms()
|
||||
is OnboardingAction.RegisterDummy -> handleRegisterDummy()
|
||||
is OnboardingAction.AddThreePid -> handleAddThreePid(action)
|
||||
is OnboardingAction.SendAgainThreePid -> handleSendAgainThreePid()
|
||||
is OnboardingAction.ValidateThreePid -> handleValidateThreePid(action)
|
||||
is OnboardingAction.CheckIfEmailHasBeenValidated -> handleCheckIfEmailHasBeenValidated(action)
|
||||
is OnboardingAction.StopEmailValidationCheck -> handleStopEmailValidationCheck()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCheckIfEmailHasBeenValidated(action: OnboardingAction.CheckIfEmailHasBeenValidated) {
|
||||
// We do not want the common progress bar to be displayed, so we do not change asyncRegistration value in the state
|
||||
currentJob = executeRegistrationStep(withLoading = false) {
|
||||
it.checkIfEmailHasBeenValidated(action.delayMillis)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleStopEmailValidationCheck() {
|
||||
currentJob = null
|
||||
}
|
||||
|
||||
private fun handleValidateThreePid(action: OnboardingAction.ValidateThreePid) {
|
||||
currentJob = executeRegistrationStep {
|
||||
it.handleValidateThreePid(action.code)
|
||||
}
|
||||
}
|
||||
|
||||
private fun executeRegistrationStep(withLoading: Boolean = true,
|
||||
block: suspend (RegistrationWizard) -> RegistrationResult): Job {
|
||||
if (withLoading) {
|
||||
setState { copy(asyncRegistration = Loading()) }
|
||||
}
|
||||
return viewModelScope.launch {
|
||||
try {
|
||||
registrationWizard?.let { block(it) }
|
||||
/*
|
||||
// Simulate registration disabled
|
||||
throw Failure.ServerError(MatrixError(
|
||||
code = MatrixError.FORBIDDEN,
|
||||
message = "Registration is disabled"
|
||||
), 403))
|
||||
*/
|
||||
} catch (failure: Throwable) {
|
||||
if (failure !is CancellationException) {
|
||||
_viewEvents.post(OnboardingViewEvents.Failure(failure))
|
||||
}
|
||||
null
|
||||
}
|
||||
?.let { data ->
|
||||
when (data) {
|
||||
is RegistrationResult.Success -> onSessionCreated(data.session, isAccountCreated = true)
|
||||
is RegistrationResult.FlowResponse -> onFlowResponse(data.flowResult)
|
||||
}
|
||||
}
|
||||
|
||||
setState {
|
||||
copy(
|
||||
asyncRegistration = Uninitialized
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAddThreePid(action: OnboardingAction.AddThreePid) {
|
||||
setState { copy(asyncRegistration = Loading()) }
|
||||
private fun handleRegisterAction(action: RegisterAction) {
|
||||
currentJob = viewModelScope.launch {
|
||||
try {
|
||||
registrationWizard?.addThreePid(action.threePid)
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(OnboardingViewEvents.Failure(failure))
|
||||
if (action.hasLoadingState()) {
|
||||
setState { copy(asyncRegistration = Loading()) }
|
||||
}
|
||||
setState {
|
||||
copy(
|
||||
asyncRegistration = Uninitialized
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSendAgainThreePid() {
|
||||
setState { copy(asyncRegistration = Loading()) }
|
||||
currentJob = viewModelScope.launch {
|
||||
try {
|
||||
registrationWizard?.sendAgainThreePid()
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(OnboardingViewEvents.Failure(failure))
|
||||
}
|
||||
setState {
|
||||
copy(
|
||||
asyncRegistration = Uninitialized
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAcceptTerms() {
|
||||
currentJob = executeRegistrationStep {
|
||||
it.acceptTerms()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRegisterDummy() {
|
||||
currentJob = executeRegistrationStep {
|
||||
it.dummy()
|
||||
runCatching { registrationActionHandler.handleRegisterAction(registrationWizard, action) }
|
||||
.fold(
|
||||
onSuccess = {
|
||||
when {
|
||||
action.ignoresResult() -> {
|
||||
// do nothing
|
||||
}
|
||||
else -> when (it) {
|
||||
is RegistrationResult.Success -> onSessionCreated(it.session, isAccountCreated = true)
|
||||
is RegistrationResult.FlowResponse -> onFlowResponse(it.flowResult)
|
||||
}
|
||||
}
|
||||
},
|
||||
onFailure = {
|
||||
if (it !is CancellationException) {
|
||||
_viewEvents.post(OnboardingViewEvents.Failure(it))
|
||||
}
|
||||
}
|
||||
)
|
||||
setState { copy(asyncRegistration = Uninitialized) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRegisterWith(action: OnboardingAction.LoginOrRegister) {
|
||||
reAuthHelper.data = action.password
|
||||
currentJob = executeRegistrationStep {
|
||||
it.createAccount(
|
||||
action.username,
|
||||
action.password,
|
||||
action.initialDeviceName
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCaptchaDone(action: OnboardingAction.CaptchaDone) {
|
||||
currentJob = executeRegistrationStep {
|
||||
it.performReCaptcha(action.captchaResponse)
|
||||
}
|
||||
handleRegisterAction(RegisterAction.CreateAccount(
|
||||
action.username,
|
||||
action.password,
|
||||
action.initialDeviceName
|
||||
))
|
||||
}
|
||||
|
||||
private fun handleResetAction(action: OnboardingAction.ResetAction) {
|
||||
@ -461,7 +373,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||
}
|
||||
|
||||
when (action.signMode) {
|
||||
SignMode.SignUp -> startRegistrationFlow()
|
||||
SignMode.SignUp -> handleRegisterAction(RegisterAction.StartRegistration)
|
||||
SignMode.SignIn -> startAuthenticationFlow()
|
||||
SignMode.SignInWithMatrixId -> _viewEvents.post(OnboardingViewEvents.OnSignModeSelected(SignMode.SignInWithMatrixId))
|
||||
SignMode.Unknown -> Unit
|
||||
@ -499,7 +411,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||
|
||||
// If there is a pending email validation continue on this step
|
||||
try {
|
||||
if (registrationWizard?.isRegistrationStarted == true) {
|
||||
if (registrationWizard.isRegistrationStarted) {
|
||||
currentThreePid?.let {
|
||||
handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnSendEmailSuccess(it)))
|
||||
}
|
||||
@ -730,12 +642,6 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun startRegistrationFlow() {
|
||||
currentJob = executeRegistrationStep {
|
||||
it.getRegistrationFlow()
|
||||
}
|
||||
}
|
||||
|
||||
private fun startAuthenticationFlow() {
|
||||
// Ensure Wizard is ready
|
||||
loginWizard
|
||||
@ -745,8 +651,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||
|
||||
private fun onFlowResponse(flowResult: FlowResult) {
|
||||
// If dummy stage is mandatory, and password is already sent, do the dummy stage now
|
||||
if (isRegistrationStarted &&
|
||||
flowResult.missingStages.any { it is Stage.Dummy && it.mandatory }) {
|
||||
if (isRegistrationStarted && flowResult.missingStages.any { it is Stage.Dummy && it.mandatory }) {
|
||||
handleRegisterDummy()
|
||||
} else {
|
||||
// Notify the user
|
||||
@ -754,6 +659,10 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRegisterDummy() {
|
||||
handleRegisterAction(RegisterAction.RegisterDummy)
|
||||
}
|
||||
|
||||
private suspend fun onSessionCreated(session: Session, isAccountCreated: Boolean) {
|
||||
val state = awaitState()
|
||||
state.useCase?.let { useCase ->
|
||||
@ -1006,6 +915,10 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||
private fun completePersonalization() {
|
||||
_viewEvents.post(OnboardingViewEvents.OnPersonalizationComplete)
|
||||
}
|
||||
|
||||
private fun cancelWaitForEmailValidation() {
|
||||
currentJob = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun LoginMode.supportsSignModeScreen(): Boolean {
|
||||
|
@ -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
|
||||
}
|
@ -39,6 +39,7 @@ import im.vector.app.databinding.FragmentLoginCaptchaBinding
|
||||
import im.vector.app.features.login.JavascriptResponse
|
||||
import im.vector.app.features.onboarding.OnboardingAction
|
||||
import im.vector.app.features.onboarding.OnboardingViewState
|
||||
import im.vector.app.features.onboarding.RegisterAction
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
import timber.log.Timber
|
||||
@ -181,7 +182,7 @@ class FtueAuthCaptchaFragment @Inject constructor(
|
||||
|
||||
val response = javascriptResponse?.response
|
||||
if (javascriptResponse?.action == "verifyCallback" && response != null) {
|
||||
viewModel.handle(OnboardingAction.CaptchaDone(response))
|
||||
viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.CaptchaDone(response)))
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
@ -37,6 +37,7 @@ import im.vector.app.databinding.FragmentLoginGenericTextInputFormBinding
|
||||
import im.vector.app.features.login.TextInputFormFragmentMode
|
||||
import im.vector.app.features.onboarding.OnboardingAction
|
||||
import im.vector.app.features.onboarding.OnboardingViewEvents
|
||||
import im.vector.app.features.onboarding.RegisterAction
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.parcelize.Parcelize
|
||||
@ -138,7 +139,7 @@ class FtueAuthGenericTextInputFormFragment @Inject constructor() : AbstractFtueA
|
||||
private fun onOtherButtonClicked() {
|
||||
when (params.mode) {
|
||||
TextInputFormFragmentMode.ConfirmMsisdn -> {
|
||||
viewModel.handle(OnboardingAction.SendAgainThreePid)
|
||||
viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.SendAgainThreePid))
|
||||
}
|
||||
else -> {
|
||||
// Should not happen, button is not displayed
|
||||
@ -152,19 +153,19 @@ class FtueAuthGenericTextInputFormFragment @Inject constructor() : AbstractFtueA
|
||||
|
||||
if (text.isEmpty()) {
|
||||
// Perform dummy action
|
||||
viewModel.handle(OnboardingAction.RegisterDummy)
|
||||
viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.RegisterDummy))
|
||||
} else {
|
||||
when (params.mode) {
|
||||
TextInputFormFragmentMode.SetEmail -> {
|
||||
viewModel.handle(OnboardingAction.AddThreePid(RegisterThreePid.Email(text)))
|
||||
viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.AddThreePid(RegisterThreePid.Email(text))))
|
||||
}
|
||||
TextInputFormFragmentMode.SetMsisdn -> {
|
||||
getCountryCodeOrShowError(text)?.let { countryCode ->
|
||||
viewModel.handle(OnboardingAction.AddThreePid(RegisterThreePid.Msisdn(text, countryCode)))
|
||||
viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.AddThreePid(RegisterThreePid.Msisdn(text, countryCode))))
|
||||
}
|
||||
}
|
||||
TextInputFormFragmentMode.ConfirmMsisdn -> {
|
||||
viewModel.handle(OnboardingAction.ValidateThreePid(text))
|
||||
viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.ValidateThreePid(text)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ import com.airbnb.mvrx.args
|
||||
import im.vector.app.R
|
||||
import im.vector.app.databinding.FragmentLoginWaitForEmailBinding
|
||||
import im.vector.app.features.onboarding.OnboardingAction
|
||||
import im.vector.app.features.onboarding.RegisterAction
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.matrix.android.sdk.api.failure.is401
|
||||
import javax.inject.Inject
|
||||
@ -54,7 +55,7 @@ class FtueAuthWaitForEmailFragment @Inject constructor() : AbstractFtueAuthFragm
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
viewModel.handle(OnboardingAction.CheckIfEmailHasBeenValidated(0))
|
||||
viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.CheckIfEmailHasBeenValidated(0)))
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
@ -70,7 +71,7 @@ class FtueAuthWaitForEmailFragment @Inject constructor() : AbstractFtueAuthFragm
|
||||
override fun onError(throwable: Throwable) {
|
||||
if (throwable.is401()) {
|
||||
// Try again, with a delay
|
||||
viewModel.handle(OnboardingAction.CheckIfEmailHasBeenValidated(10_000))
|
||||
viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.CheckIfEmailHasBeenValidated(10_000)))
|
||||
} else {
|
||||
super.onError(throwable)
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ import im.vector.app.features.login.terms.LoginTermsViewState
|
||||
import im.vector.app.features.login.terms.PolicyController
|
||||
import im.vector.app.features.onboarding.OnboardingAction
|
||||
import im.vector.app.features.onboarding.OnboardingViewState
|
||||
import im.vector.app.features.onboarding.RegisterAction
|
||||
import im.vector.app.features.onboarding.ftueauth.AbstractFtueAuthFragment
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.matrix.android.sdk.internal.auth.registration.LocalizedFlowDataLoginTerms
|
||||
@ -111,7 +112,7 @@ class FtueAuthTermsFragment @Inject constructor(
|
||||
}
|
||||
|
||||
private fun submit() {
|
||||
viewModel.handle(OnboardingAction.AcceptTerms)
|
||||
viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.AcceptTerms))
|
||||
}
|
||||
|
||||
override fun updateWithState(state: OnboardingViewState) {
|
||||
|
@ -54,6 +54,7 @@ import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedA
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.matrix.android.sdk.api.MatrixConfiguration
|
||||
import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
import timber.log.Timber
|
||||
@ -67,7 +68,8 @@ data class RoomProfileArgs(
|
||||
class RoomProfileFragment @Inject constructor(
|
||||
private val roomProfileController: RoomProfileController,
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val roomDetailPendingActionStore: RoomDetailPendingActionStore
|
||||
private val roomDetailPendingActionStore: RoomDetailPendingActionStore,
|
||||
private val matrixConfiguration: MatrixConfiguration
|
||||
) :
|
||||
VectorBaseFragment<FragmentMatrixProfileBinding>(),
|
||||
RoomProfileController.Callback {
|
||||
@ -222,7 +224,7 @@ class RoomProfileFragment @Inject constructor(
|
||||
avatarRenderer.render(matrixItem, views.matrixProfileToolbarAvatarImageView)
|
||||
headerViews.roomProfileDecorationImageView.render(it.roomEncryptionTrustLevel)
|
||||
views.matrixProfileDecorationToolbarAvatarImageView.render(it.roomEncryptionTrustLevel)
|
||||
headerViews.roomProfilePresenceImageView.render(it.isDirect, it.directUserPresence)
|
||||
headerViews.roomProfilePresenceImageView.render(it.isDirect && matrixConfiguration.presenceSyncEnabled, it.directUserPresence)
|
||||
headerViews.roomProfilePublicImageView.isVisible = it.isPublic && !it.isDirect
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ import im.vector.app.core.resources.ColorProvider
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import me.gujun.android.span.span
|
||||
import org.matrix.android.sdk.api.MatrixConfiguration
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
||||
@ -39,7 +40,8 @@ class RoomMemberListController @Inject constructor(
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val stringProvider: StringProvider,
|
||||
private val colorProvider: ColorProvider,
|
||||
private val roomMemberSummaryFilter: RoomMemberSummaryFilter
|
||||
private val roomMemberSummaryFilter: RoomMemberSummaryFilter,
|
||||
private val matrixConfiguration: MatrixConfiguration
|
||||
) : TypedEpoxyController<RoomMemberListViewState>() {
|
||||
|
||||
interface Callback {
|
||||
@ -122,6 +124,7 @@ class RoomMemberListController @Inject constructor(
|
||||
host: RoomMemberListController,
|
||||
data: RoomMemberListViewState) {
|
||||
val powerLabel = stringProvider.getString(powerLevelCategory.titleRes)
|
||||
val presenceSyncEnabled = matrixConfiguration.presenceSyncEnabled
|
||||
|
||||
profileMatrixItemWithPowerLevelWithPresence {
|
||||
id(roomMember.userId)
|
||||
@ -131,6 +134,7 @@ class RoomMemberListController @Inject constructor(
|
||||
clickListener {
|
||||
host.callback?.onRoomMemberClicked(roomMember)
|
||||
}
|
||||
showPresence(presenceSyncEnabled)
|
||||
userPresence(roomMember.userPresence)
|
||||
powerLevelLabel(
|
||||
span {
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
<path
|
||||
android:pathData="M0 0V12H11.8857V0"
|
||||
android:fillColor="#0DBD8B"
|
||||
android:fillColor="?vctr_presence_indicator_online"
|
||||
/>
|
||||
|
||||
</group>
|
||||
|
@ -17,7 +17,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="48dp"
|
||||
android:visibility="gone"/>
|
||||
android:visibility="gone" />
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/roomToolbar"
|
||||
@ -46,20 +46,32 @@
|
||||
|
||||
<im.vector.app.features.sync.widget.SyncStateView
|
||||
android:id="@+id/syncStateView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
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
|
||||
android:id="@+id/removeJitsiWidgetView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:colorBackground"
|
||||
android:minHeight="54dp"
|
||||
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
|
||||
android:id="@+id/timelineRecyclerView"
|
||||
@ -86,19 +98,19 @@
|
||||
app:closeIcon="@drawable/ic_close_24dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/removeJitsiWidgetView"/>
|
||||
app:layout_constraintTop_toBottomOf="@id/removeJitsiWidgetView" />
|
||||
|
||||
<im.vector.app.core.ui.views.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_height="20dp"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
tools:visibility="visible"
|
||||
android:layout_height="20dp"/>
|
||||
app:layout_constraintBottom_toTopOf="@id/composerLayout"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/timelineRecyclerView"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<im.vector.app.core.ui.views.NotificationAreaView
|
||||
android:id="@+id/notificationAreaView"
|
||||
@ -108,7 +120,7 @@
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:visibility="visible"/>
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/failedMessagesWarningStub"
|
||||
@ -130,7 +142,7 @@
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"/>
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView
|
||||
android:id="@+id/voiceMessageRecorderView"
|
||||
|
49
vector/src/main/res/layout/view_location_live_status.xml
Normal file
49
vector/src/main/res/layout/view_location_live_status.xml
Normal 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
@ -2943,6 +2943,8 @@
|
||||
<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="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 you’d 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_content">${app_name} could not access your location. Please try again later.</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="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_share_live_enabled">Live location enabled</string>
|
||||
<string name="location_share_live_stop">Stop</string>
|
||||
|
||||
<string name="message_bubbles">Show Message bubbles</string>
|
||||
|
||||
|
@ -23,12 +23,14 @@ import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import com.airbnb.mvrx.test.MvRxTestRule
|
||||
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.FakeAnalyticsTracker
|
||||
import im.vector.app.test.fakes.FakeAuthenticationService
|
||||
import im.vector.app.test.fakes.FakeContext
|
||||
import im.vector.app.test.fakes.FakeHomeServerConnectionConfigFactory
|
||||
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.FakeSession
|
||||
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.FakeVectorFeatures
|
||||
import im.vector.app.test.fakes.FakeVectorOverrides
|
||||
import im.vector.app.test.fixtures.aHomeServerCapabilities
|
||||
import im.vector.app.test.test
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
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
|
||||
|
||||
private const val A_DISPLAY_NAME = "a display name"
|
||||
private const val A_PICTURE_FILENAME = "a-picture.png"
|
||||
private val AN_ERROR = RuntimeException("an error!")
|
||||
private val AN_UNSUPPORTED_PERSONALISATION_STATE = PersonalizationState(
|
||||
supportsChangingDisplayName = false,
|
||||
supportsChangingProfilePicture = false
|
||||
)
|
||||
private val A_LOADABLE_REGISTER_ACTION = RegisterAction.StartRegistration
|
||||
private val A_NON_LOADABLE_REGISTER_ACTION = RegisterAction.CheckIfEmailHasBeenValidated(delayMillis = -1L)
|
||||
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 {
|
||||
|
||||
@ -63,6 +72,7 @@ class OnboardingViewModelTest {
|
||||
private val fakeUriFilenameResolver = FakeUriFilenameResolver()
|
||||
private val fakeActiveSessionHolder = FakeActiveSessionHolder(fakeSession)
|
||||
private val fakeAuthenticationService = FakeAuthenticationService()
|
||||
private val fakeRegisterActionHandler = FakeRegisterActionHandler()
|
||||
|
||||
lateinit var viewModel: OnboardingViewModel
|
||||
|
||||
@ -72,7 +82,7 @@ class OnboardingViewModelTest {
|
||||
}
|
||||
|
||||
@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)
|
||||
|
||||
viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnTakeMeHome))
|
||||
@ -83,7 +93,7 @@ class OnboardingViewModelTest {
|
||||
}
|
||||
|
||||
@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))
|
||||
viewModel = createViewModel(initialState)
|
||||
val test = viewModel.test(this)
|
||||
@ -96,7 +106,7 @@ class OnboardingViewModelTest {
|
||||
}
|
||||
|
||||
@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))
|
||||
viewModel = createViewModel(initialState)
|
||||
val test = viewModel.test(this)
|
||||
@ -109,34 +119,109 @@ class OnboardingViewModelTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given homeserver does not support personalisation when registering account then updates state and emits account created event`() = runBlockingTest {
|
||||
fakeSession.fakeHomeServerCapabilitiesService.givenCapabilities(HomeServerCapabilities(canChangeDisplayName = false, canChangeAvatar = false))
|
||||
givenSuccessfullyCreatesAccount()
|
||||
fun `when handling SignUp then sets sign mode to sign up and starts registration`() = runBlockingTest {
|
||||
givenRegistrationResultFor(RegisterAction.StartRegistration, ANY_CONTINUING_REGISTRATION_RESULT)
|
||||
val test = viewModel.test(this)
|
||||
|
||||
viewModel.handle(OnboardingAction.RegisterDummy)
|
||||
viewModel.handle(OnboardingAction.UpdateSignMode(SignMode.SignUp))
|
||||
|
||||
test
|
||||
.assertStates(
|
||||
.assertStatesChanges(
|
||||
initialState,
|
||||
initialState.copy(asyncRegistration = Loading()),
|
||||
initialState.copy(
|
||||
asyncLoginAction = Success(Unit),
|
||||
asyncRegistration = Loading(),
|
||||
personalizationState = AN_UNSUPPORTED_PERSONALISATION_STATE
|
||||
),
|
||||
initialState.copy(
|
||||
asyncLoginAction = Success(Unit),
|
||||
asyncRegistration = Uninitialized,
|
||||
personalizationState = AN_UNSUPPORTED_PERSONALISATION_STATE
|
||||
)
|
||||
{ copy(signMode = SignMode.SignUp) },
|
||||
{ copy(asyncRegistration = Loading()) },
|
||||
{ copy(asyncRegistration = Uninitialized) }
|
||||
)
|
||||
.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)
|
||||
.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 {
|
||||
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))
|
||||
viewModel = createViewModel(personalisedInitialState)
|
||||
val test = viewModel.test(this)
|
||||
@ -144,14 +229,14 @@ class OnboardingViewModelTest {
|
||||
viewModel.handle(OnboardingAction.UpdateDisplayName(A_DISPLAY_NAME))
|
||||
|
||||
test
|
||||
.assertStates(expectedSuccessfulDisplayNameUpdateStates(personalisedInitialState))
|
||||
.assertStatesChanges(personalisedInitialState, expectedSuccessfulDisplayNameUpdateStates())
|
||||
.assertEvents(OnboardingViewEvents.OnChooseProfilePicture)
|
||||
.finish()
|
||||
fakeSession.fakeProfileService.verifyUpdatedName(fakeSession.myUserId, A_DISPLAY_NAME)
|
||||
}
|
||||
|
||||
@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))
|
||||
viewModel = createViewModel(personalisedInitialState)
|
||||
val test = viewModel.test(this)
|
||||
@ -159,31 +244,31 @@ class OnboardingViewModelTest {
|
||||
viewModel.handle(OnboardingAction.UpdateDisplayName(A_DISPLAY_NAME))
|
||||
|
||||
test
|
||||
.assertStates(expectedSuccessfulDisplayNameUpdateStates(personalisedInitialState))
|
||||
.assertStatesChanges(personalisedInitialState, expectedSuccessfulDisplayNameUpdateStates())
|
||||
.assertEvents(OnboardingViewEvents.OnPersonalizationComplete)
|
||||
.finish()
|
||||
fakeSession.fakeProfileService.verifyUpdatedName(fakeSession.myUserId, A_DISPLAY_NAME)
|
||||
}
|
||||
|
||||
@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)
|
||||
fakeSession.fakeProfileService.givenSetDisplayNameErrors(AN_ERROR)
|
||||
|
||||
viewModel.handle(OnboardingAction.UpdateDisplayName(A_DISPLAY_NAME))
|
||||
|
||||
test
|
||||
.assertStates(
|
||||
.assertStatesChanges(
|
||||
initialState,
|
||||
initialState.copy(asyncDisplayName = Loading()),
|
||||
initialState.copy(asyncDisplayName = Fail(AN_ERROR)),
|
||||
{ copy(asyncDisplayName = Loading()) },
|
||||
{ copy(asyncDisplayName = Fail(AN_ERROR)) },
|
||||
)
|
||||
.assertEvents(OnboardingViewEvents.Failure(AN_ERROR))
|
||||
.finish()
|
||||
}
|
||||
|
||||
@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)
|
||||
|
||||
viewModel.handle(OnboardingAction.ProfilePictureSelected(fakeUri.instance))
|
||||
@ -198,7 +283,7 @@ class OnboardingViewModelTest {
|
||||
}
|
||||
|
||||
@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)
|
||||
viewModel = createViewModel(initialStateWithPicture)
|
||||
val test = viewModel.test(this)
|
||||
@ -213,7 +298,7 @@ class OnboardingViewModelTest {
|
||||
}
|
||||
|
||||
@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)
|
||||
val initialStateWithPicture = givenPictureSelected(fakeUri.instance, A_PICTURE_FILENAME)
|
||||
viewModel = createViewModel(initialStateWithPicture)
|
||||
@ -228,7 +313,7 @@ class OnboardingViewModelTest {
|
||||
}
|
||||
|
||||
@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)
|
||||
|
||||
viewModel.handle(OnboardingAction.SaveSelectedProfilePicture)
|
||||
@ -240,7 +325,7 @@ class OnboardingViewModelTest {
|
||||
}
|
||||
|
||||
@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)
|
||||
|
||||
viewModel.handle(OnboardingAction.UpdateProfilePictureSkipped)
|
||||
@ -264,6 +349,7 @@ class OnboardingViewModelTest {
|
||||
FakeVectorFeatures(),
|
||||
FakeAnalyticsTracker(),
|
||||
fakeUriFilenameResolver.instance,
|
||||
fakeRegisterActionHandler.instance,
|
||||
FakeVectorOverrides()
|
||||
)
|
||||
}
|
||||
@ -286,22 +372,42 @@ class OnboardingViewModelTest {
|
||||
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)
|
||||
val registrationWizard = FakeRegistrationWizard().also { it.givenSuccessfulDummy(fakeSession) }
|
||||
fakeAuthenticationService.givenRegistrationWizard(registrationWizard)
|
||||
fakeAuthenticationService.expectReset()
|
||||
fakeSession.expectStartsSyncing()
|
||||
}
|
||||
|
||||
private fun expectedSuccessfulDisplayNameUpdateStates(personalisedInitialState: OnboardingViewState): List<OnboardingViewState> {
|
||||
return listOf(
|
||||
personalisedInitialState,
|
||||
personalisedInitialState.copy(asyncDisplayName = Loading()),
|
||||
personalisedInitialState.copy(
|
||||
asyncDisplayName = Success(Unit),
|
||||
personalizationState = personalisedInitialState.personalizationState.copy(displayName = A_DISPLAY_NAME)
|
||||
)
|
||||
)
|
||||
private fun givenRegistrationResultFor(action: RegisterAction, result: RegistrationResult) {
|
||||
givenRegistrationResultsFor(listOf(action to result))
|
||||
}
|
||||
|
||||
private fun givenRegistrationResultsFor(results: List<Pair<RegisterAction, RegistrationResult>>) {
|
||||
fakeAuthenticationService.givenRegistrationStarted(true)
|
||||
val registrationWizard = FakeRegistrationWizard()
|
||||
fakeAuthenticationService.givenRegistrationWizard(registrationWizard)
|
||||
fakeRegisterActionHandler.givenResultsFor(registrationWizard, results)
|
||||
}
|
||||
}
|
||||
|
||||
private fun HomeServerCapabilities.toPersonalisationState() = PersonalizationState(
|
||||
supportsChangingDisplayName = canChangeDisplayName,
|
||||
supportsChangingProfilePicture = canChangeAvatar
|
||||
)
|
||||
|
@ -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)
|
@ -55,6 +55,25 @@ class ViewModelTest<S, VE>(
|
||||
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> {
|
||||
states.assertValues(expected)
|
||||
return this
|
||||
|
@ -23,10 +23,15 @@ import org.matrix.android.sdk.api.auth.AuthenticationService
|
||||
import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
|
||||
|
||||
class FakeAuthenticationService : AuthenticationService by mockk() {
|
||||
|
||||
fun givenRegistrationWizard(registrationWizard: RegistrationWizard) {
|
||||
every { getRegistrationWizard() } returns registrationWizard
|
||||
}
|
||||
|
||||
fun givenRegistrationStarted(started: Boolean) {
|
||||
every { isRegistrationStarted } returns started
|
||||
}
|
||||
|
||||
fun expectReset() {
|
||||
coJustRun { reset() }
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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.session.Session
|
||||
|
||||
class FakeRegistrationWizard : RegistrationWizard by mockk() {
|
||||
class FakeRegistrationWizard : RegistrationWizard by mockk(relaxed = false) {
|
||||
|
||||
fun givenSuccessfulDummy(session: Session) {
|
||||
coEvery { dummy() } returns RegistrationResult.Success(session)
|
||||
fun givenSuccessFor(result: Session, expect: suspend RegistrationWizard.() -> RegistrationResult) {
|
||||
coEvery { expect(this@FakeRegistrationWizard) } returns RegistrationResult.Success(result)
|
||||
}
|
||||
}
|
||||
|
@ -23,5 +23,5 @@ class FakeVectorFeatures : VectorFeatures {
|
||||
override fun isOnboardingAlreadyHaveAccountSplashEnabled() = true
|
||||
override fun isOnboardingSplashCarouselEnabled() = true
|
||||
override fun isOnboardingUseCaseEnabled() = true
|
||||
override fun isOnboardingPersonalizeEnabled() = false
|
||||
override fun isOnboardingPersonalizeEnabled() = true
|
||||
}
|
||||
|
40
vector/src/test/java/im/vector/app/test/fixtures/HomeserverCapabilityFixture.kt
vendored
Normal file
40
vector/src/test/java/im/vector/app/test/fixtures/HomeserverCapabilityFixture.kt
vendored
Normal 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
|
||||
)
|
Loading…
Reference in New Issue
Block a user