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