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

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

View File

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -60,7 +60,11 @@ data class MatrixConfiguration(
/**
* RoomDisplayNameFallbackProvider to provide default room display name.
*/
val roomDisplayNameFallbackProvider: RoomDisplayNameFallbackProvider
val roomDisplayNameFallbackProvider: RoomDisplayNameFallbackProvider,
/**
* True to enable presence information sync (if available). False to disable regardless of server setting.
*/
val presenceSyncEnabled: Boolean = true
) {
/**

View File

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

View File

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

View File

@ -17,6 +17,7 @@
package org.matrix.android.sdk.internal.session.sync.handler
import io.realm.Realm
import org.matrix.android.sdk.api.MatrixConfiguration
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.getPresenceContent
import org.matrix.android.sdk.api.session.sync.model.PresenceSyncResponse
@ -27,27 +28,29 @@ import org.matrix.android.sdk.internal.database.query.updateDirectUserPresence
import org.matrix.android.sdk.internal.database.query.updateUserPresence
import javax.inject.Inject
internal class PresenceSyncHandler @Inject constructor() {
internal class PresenceSyncHandler @Inject constructor(private val matrixConfiguration: MatrixConfiguration) {
fun handle(realm: Realm, presenceSyncResponse: PresenceSyncResponse?) {
presenceSyncResponse?.events
?.filter { event -> event.type == EventType.PRESENCE }
?.forEach { event ->
val content = event.getPresenceContent() ?: return@forEach
val userId = event.senderId ?: return@forEach
val userPresenceEntity = UserPresenceEntity(
userId = userId,
lastActiveAgo = content.lastActiveAgo,
statusMessage = content.statusMessage,
isCurrentlyActive = content.isCurrentlyActive,
avatarUrl = content.avatarUrl,
displayName = content.displayName
).also {
it.presence = content.presence
}
if (matrixConfiguration.presenceSyncEnabled) {
presenceSyncResponse?.events
?.filter { event -> event.type == EventType.PRESENCE }
?.forEach { event ->
val content = event.getPresenceContent() ?: return@forEach
val userId = event.senderId ?: return@forEach
val userPresenceEntity = UserPresenceEntity(
userId = userId,
lastActiveAgo = content.lastActiveAgo,
statusMessage = content.statusMessage,
isCurrentlyActive = content.isCurrentlyActive,
avatarUrl = content.avatarUrl,
displayName = content.displayName
).also {
it.presence = content.presence
}
storePresenceToDB(realm, userPresenceEntity)
}
storePresenceToDB(realm, userPresenceEntity)
}
}
}
/**

View File

@ -13,32 +13,35 @@ print("::group::Arguments")
print(f"{sys.argv}")
print("::endgroup::")
for xmlfile in xmlfiles:
tree = ET.parse(xmlfile)
try:
tree = ET.parse(xmlfile)
root = tree.getroot()
name = root.attrib['name']
time = root.attrib['time']
tests = int(root.attrib['tests'])
skipped = int(root.attrib['skipped'])
errors = int(root.attrib['errors'])
failures = int(root.attrib['failures'])
success = tests - failures - errors - skipped
total = tests - skipped
print(f"::group::{name} {success}/{total} ({skipped} skipped) in {time}")
for testcase in root:
if testcase.tag != "testcase":
continue
testname = testcase.attrib['classname']
message = testcase.attrib['name']
time = testcase.attrib['time']
child = testcase.find("failure")
if child is None:
print(f"{message} in {time}s")
else:
print(f"::error file={testname}::{message} in {time}s")
print(child.text)
body = f"passed={success} failures={failures} errors={errors} skipped={skipped}"
print(f"::set-output name={suitename}::={body}")
root = tree.getroot()
name = root.attrib['name']
time = root.attrib['time']
tests = int(root.attrib['tests'])
skipped = int(root.attrib['skipped'])
errors = int(root.attrib['errors'])
failures = int(root.attrib['failures'])
success = tests - failures - errors - skipped
total = tests - skipped
print(f"::group::{name} {success}/{total} ({skipped} skipped) in {time}")
for testcase in root:
if testcase.tag != "testcase":
continue
testname = testcase.attrib['classname']
message = testcase.attrib['name']
time = testcase.attrib['time']
child = testcase.find("failure")
if child is None:
print(f"{message} in {time}s")
else:
print(f"::error file={testname}::{message} in {time}s")
print(child.text)
body = f" passed={success} failures={failures} errors={errors} skipped={skipped}"
print(f"::set-output name={suitename}::={body}")
except FileNotFoundError:
print(f"::error::Unable to open test results file {xmlfile} - check if the tests completed")
print("::endgroup::")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -37,7 +37,7 @@ import androidx.core.view.isVisible
import im.vector.app.R
import im.vector.app.core.epoxy.onClick
import im.vector.app.core.utils.PERMISSIONS_EMPTY
import im.vector.app.core.utils.PERMISSIONS_FOR_LOCATION_SHARING
import im.vector.app.core.utils.PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING
import im.vector.app.core.utils.PERMISSIONS_FOR_PICKING_CONTACT
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.app.databinding.ViewAttachmentTypeSelectorBinding
@ -215,6 +215,6 @@ class AttachmentTypeSelectorView(context: Context,
STICKER(PERMISSIONS_EMPTY, R.string.tooltip_attachment_sticker),
CONTACT(PERMISSIONS_FOR_PICKING_CONTACT, R.string.tooltip_attachment_contact),
POLL(PERMISSIONS_EMPTY, R.string.tooltip_attachment_poll),
LOCATION(PERMISSIONS_FOR_LOCATION_SHARING, R.string.tooltip_attachment_location)
LOCATION(PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING, R.string.tooltip_attachment_location)
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.location
import android.app.Activity
import im.vector.app.core.utils.openAppSettingsPage
class DefaultLocationSharingNavigator constructor(val activity: Activity?) : LocationSharingNavigator {
override var goingToAppSettings: Boolean = false
override fun quit() {
activity?.finish()
}
override fun goToAppSettings() {
activity?.let {
goingToAppSettings = true
openAppSettingsPage(it)
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,23 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.location
interface LocationSharingNavigator {
var goingToAppSettings: Boolean
fun quit()
fun goToAppSettings()
}

View File

@ -38,6 +38,7 @@ import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.util.toMatrixItem
import timber.log.Timber
/**
* Sampling period to compare target location and user location.
@ -120,6 +121,7 @@ class LocationSharingViewModel @AssistedInject constructor(
is LocationSharingAction.PinnedLocationSharing -> handlePinnedLocationSharingAction(action)
is LocationSharingAction.LocationTargetChange -> handleLocationTargetChangeAction(action)
LocationSharingAction.ZoomToUserLocation -> handleZoomToUserLocationAction()
LocationSharingAction.StartLiveLocationSharing -> handleStartLiveLocationSharingAction()
}.exhaustive
}
@ -157,6 +159,11 @@ class LocationSharingViewModel @AssistedInject constructor(
}
}
private fun handleStartLiveLocationSharingAction() {
// TODO start sharing live location and update view state
Timber.d("live location sharing started")
}
override fun onLocationUpdate(locationData: LocationData) {
setState {
copy(lastKnownUserLocation = locationData)

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.location.live
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.Button
import androidx.constraintlayout.widget.ConstraintLayout
import im.vector.app.databinding.ViewLocationLiveStatusBinding
class LocationLiveStatusView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
private val binding = ViewLocationLiveStatusBinding.inflate(
LayoutInflater.from(context),
this
)
val stopButton: Button
get() = binding.locationLiveStatusStop
}

View File

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

View File

@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.file.FileService
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
@ -52,7 +53,10 @@ class RoomEventsAttachmentProvider(
override fun getAttachmentInfoAt(position: Int): AttachmentInfo {
return getItem(position).let {
val content = it.root.getClearContent().toModel<MessageContent>() as? MessageWithAttachmentContent
val clearContent = it.root.getClearContent()
val content = clearContent.toModel<MessageContent>()
?: clearContent.toModel<MessageStickerContent>()
as? MessageWithAttachmentContent
if (content is MessageImageContent) {
val data = ImageContentRenderer.Data(
eventId = it.eventId,
@ -66,6 +70,33 @@ class RoomEventsAttachmentProvider(
height = null,
allowNonMxcUrls = it.root.sendState.isSending()
)
if (content.mimeType == MimeTypes.Gif) {
AttachmentInfo.AnimatedImage(
uid = it.eventId,
url = content.url ?: "",
data = data
)
} else {
AttachmentInfo.Image(
uid = it.eventId,
url = content.url ?: "",
data = data
)
}
} else if (content is MessageStickerContent) {
val data = ImageContentRenderer.Data(
eventId = it.eventId,
filename = content.body,
mimeType = content.mimeType,
url = content.getFileUrl(),
elementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt(),
maxHeight = -1,
maxWidth = -1,
width = null,
height = null,
allowNonMxcUrls = false
)
if (content.mimeType == MimeTypes.Gif) {
AttachmentInfo.AnimatedImage(

View File

@ -22,63 +22,49 @@ import im.vector.app.features.login.LoginConfig
import im.vector.app.features.login.ServerType
import im.vector.app.features.login.SignMode
import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
import org.matrix.android.sdk.internal.network.ssl.Fingerprint
sealed class OnboardingAction : VectorViewModelAction {
data class OnGetStarted(val resetLoginConfig: Boolean, val onboardingFlow: OnboardingFlow) : OnboardingAction()
data class OnIAlreadyHaveAnAccount(val resetLoginConfig: Boolean, val onboardingFlow: OnboardingFlow) : OnboardingAction()
sealed interface OnboardingAction : VectorViewModelAction {
data class OnGetStarted(val resetLoginConfig: Boolean, val onboardingFlow: OnboardingFlow) : OnboardingAction
data class OnIAlreadyHaveAnAccount(val resetLoginConfig: Boolean, val onboardingFlow: OnboardingFlow) : OnboardingAction
data class UpdateServerType(val serverType: ServerType) : OnboardingAction()
data class UpdateHomeServer(val homeServerUrl: String) : OnboardingAction()
data class UpdateUseCase(val useCase: FtueUseCase) : OnboardingAction()
object ResetUseCase : OnboardingAction()
data class UpdateSignMode(val signMode: SignMode) : OnboardingAction()
data class LoginWithToken(val loginToken: String) : OnboardingAction()
data class WebLoginSuccess(val credentials: Credentials) : OnboardingAction()
data class InitWith(val loginConfig: LoginConfig?) : OnboardingAction()
data class ResetPassword(val email: String, val newPassword: String) : OnboardingAction()
object ResetPasswordMailConfirmed : OnboardingAction()
data class UpdateServerType(val serverType: ServerType) : OnboardingAction
data class UpdateHomeServer(val homeServerUrl: String) : OnboardingAction
data class UpdateUseCase(val useCase: FtueUseCase) : OnboardingAction
object ResetUseCase : OnboardingAction
data class UpdateSignMode(val signMode: SignMode) : OnboardingAction
data class LoginWithToken(val loginToken: String) : OnboardingAction
data class WebLoginSuccess(val credentials: Credentials) : OnboardingAction
data class InitWith(val loginConfig: LoginConfig?) : OnboardingAction
data class ResetPassword(val email: String, val newPassword: String) : OnboardingAction
object ResetPasswordMailConfirmed : OnboardingAction
// Login or Register, depending on the signMode
data class LoginOrRegister(val username: String, val password: String, val initialDeviceName: String) : OnboardingAction()
data class LoginOrRegister(val username: String, val password: String, val initialDeviceName: String) : OnboardingAction
object StopEmailValidationCheck : OnboardingAction
// Register actions
open class RegisterAction : OnboardingAction()
data class AddThreePid(val threePid: RegisterThreePid) : RegisterAction()
object SendAgainThreePid : RegisterAction()
// TODO Confirm Email (from link in the email, open in the phone, intercepted by the app)
data class ValidateThreePid(val code: String) : RegisterAction()
data class CheckIfEmailHasBeenValidated(val delayMillis: Long) : RegisterAction()
object StopEmailValidationCheck : RegisterAction()
data class CaptchaDone(val captchaResponse: String) : RegisterAction()
object AcceptTerms : RegisterAction()
object RegisterDummy : RegisterAction()
data class PostRegisterAction(val registerAction: RegisterAction) : OnboardingAction
// Reset actions
open class ResetAction : OnboardingAction()
sealed interface ResetAction : OnboardingAction
object ResetHomeServerType : ResetAction()
object ResetHomeServerUrl : ResetAction()
object ResetSignMode : ResetAction()
object ResetLogin : ResetAction()
object ResetResetPassword : ResetAction()
object ResetHomeServerType : ResetAction
object ResetHomeServerUrl : ResetAction
object ResetSignMode : ResetAction
object ResetLogin : ResetAction
object ResetResetPassword : ResetAction
// Homeserver history
object ClearHomeServerHistory : OnboardingAction()
object ClearHomeServerHistory : OnboardingAction
data class PostViewEvent(val viewEvent: OnboardingViewEvents) : OnboardingAction()
data class PostViewEvent(val viewEvent: OnboardingViewEvents) : OnboardingAction
data class UserAcceptCertificate(val fingerprint: Fingerprint) : OnboardingAction()
data class UserAcceptCertificate(val fingerprint: Fingerprint) : OnboardingAction
object PersonalizeProfile : OnboardingAction()
data class UpdateDisplayName(val displayName: String) : OnboardingAction()
object UpdateDisplayNameSkipped : OnboardingAction()
data class ProfilePictureSelected(val uri: Uri) : OnboardingAction()
object SaveSelectedProfilePicture : OnboardingAction()
object UpdateProfilePictureSkipped : OnboardingAction()
object PersonalizeProfile : OnboardingAction
data class UpdateDisplayName(val displayName: String) : OnboardingAction
object UpdateDisplayNameSkipped : OnboardingAction
data class ProfilePictureSelected(val uri: Uri) : OnboardingAction
object SaveSelectedProfilePicture : OnboardingAction
object UpdateProfilePictureSkipped : OnboardingAction
}

View File

@ -83,6 +83,7 @@ class OnboardingViewModel @AssistedInject constructor(
private val vectorFeatures: VectorFeatures,
private val analyticsTracker: AnalyticsTracker,
private val uriFilenameResolver: UriFilenameResolver,
private val registrationActionHandler: RegistrationActionHandler,
private val vectorOverrides: VectorOverrides
) : VectorViewModel<OnboardingViewState, OnboardingAction, OnboardingViewEvents>(initialState) {
@ -116,16 +117,16 @@ class OnboardingViewModel @AssistedInject constructor(
private val matrixOrgUrl = stringProvider.getString(R.string.matrix_org_server_url).ensureTrailingSlash()
private val registrationWizard: RegistrationWizard
get() = authenticationService.getRegistrationWizard()
val currentThreePid: String?
get() = registrationWizard?.currentThreePid
get() = registrationWizard.currentThreePid
// True when login and password has been sent with success to the homeserver
val isRegistrationStarted: Boolean
get() = authenticationService.isRegistrationStarted
private val registrationWizard: RegistrationWizard?
get() = authenticationService.getRegistrationWizard()
private val loginWizard: LoginWizard?
get() = authenticationService.getLoginWizard()
@ -153,7 +154,7 @@ class OnboardingViewModel @AssistedInject constructor(
is OnboardingAction.WebLoginSuccess -> handleWebLoginSuccess(action)
is OnboardingAction.ResetPassword -> handleResetPassword(action)
is OnboardingAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed()
is OnboardingAction.RegisterAction -> handleRegisterAction(action)
is OnboardingAction.PostRegisterAction -> handleRegisterAction(action.registerAction)
is OnboardingAction.ResetAction -> handleResetAction(action)
is OnboardingAction.UserAcceptCertificate -> handleUserAcceptCertificate(action)
OnboardingAction.ClearHomeServerHistory -> handleClearHomeServerHistory()
@ -164,6 +165,7 @@ class OnboardingViewModel @AssistedInject constructor(
is OnboardingAction.ProfilePictureSelected -> handleProfilePictureSelected(action)
OnboardingAction.SaveSelectedProfilePicture -> updateProfilePicture()
is OnboardingAction.PostViewEvent -> _viewEvents.post(action.viewEvent)
OnboardingAction.StopEmailValidationCheck -> cancelWaitForEmailValidation()
}.exhaustive
}
@ -266,131 +268,41 @@ class OnboardingViewModel @AssistedInject constructor(
}
}
private fun handleRegisterAction(action: OnboardingAction.RegisterAction) {
when (action) {
is OnboardingAction.CaptchaDone -> handleCaptchaDone(action)
is OnboardingAction.AcceptTerms -> handleAcceptTerms()
is OnboardingAction.RegisterDummy -> handleRegisterDummy()
is OnboardingAction.AddThreePid -> handleAddThreePid(action)
is OnboardingAction.SendAgainThreePid -> handleSendAgainThreePid()
is OnboardingAction.ValidateThreePid -> handleValidateThreePid(action)
is OnboardingAction.CheckIfEmailHasBeenValidated -> handleCheckIfEmailHasBeenValidated(action)
is OnboardingAction.StopEmailValidationCheck -> handleStopEmailValidationCheck()
}
}
private fun handleCheckIfEmailHasBeenValidated(action: OnboardingAction.CheckIfEmailHasBeenValidated) {
// We do not want the common progress bar to be displayed, so we do not change asyncRegistration value in the state
currentJob = executeRegistrationStep(withLoading = false) {
it.checkIfEmailHasBeenValidated(action.delayMillis)
}
}
private fun handleStopEmailValidationCheck() {
currentJob = null
}
private fun handleValidateThreePid(action: OnboardingAction.ValidateThreePid) {
currentJob = executeRegistrationStep {
it.handleValidateThreePid(action.code)
}
}
private fun executeRegistrationStep(withLoading: Boolean = true,
block: suspend (RegistrationWizard) -> RegistrationResult): Job {
if (withLoading) {
setState { copy(asyncRegistration = Loading()) }
}
return viewModelScope.launch {
try {
registrationWizard?.let { block(it) }
/*
// Simulate registration disabled
throw Failure.ServerError(MatrixError(
code = MatrixError.FORBIDDEN,
message = "Registration is disabled"
), 403))
*/
} catch (failure: Throwable) {
if (failure !is CancellationException) {
_viewEvents.post(OnboardingViewEvents.Failure(failure))
}
null
}
?.let { data ->
when (data) {
is RegistrationResult.Success -> onSessionCreated(data.session, isAccountCreated = true)
is RegistrationResult.FlowResponse -> onFlowResponse(data.flowResult)
}
}
setState {
copy(
asyncRegistration = Uninitialized
)
}
}
}
private fun handleAddThreePid(action: OnboardingAction.AddThreePid) {
setState { copy(asyncRegistration = Loading()) }
private fun handleRegisterAction(action: RegisterAction) {
currentJob = viewModelScope.launch {
try {
registrationWizard?.addThreePid(action.threePid)
} catch (failure: Throwable) {
_viewEvents.post(OnboardingViewEvents.Failure(failure))
if (action.hasLoadingState()) {
setState { copy(asyncRegistration = Loading()) }
}
setState {
copy(
asyncRegistration = Uninitialized
)
}
}
}
private fun handleSendAgainThreePid() {
setState { copy(asyncRegistration = Loading()) }
currentJob = viewModelScope.launch {
try {
registrationWizard?.sendAgainThreePid()
} catch (failure: Throwable) {
_viewEvents.post(OnboardingViewEvents.Failure(failure))
}
setState {
copy(
asyncRegistration = Uninitialized
)
}
}
}
private fun handleAcceptTerms() {
currentJob = executeRegistrationStep {
it.acceptTerms()
}
}
private fun handleRegisterDummy() {
currentJob = executeRegistrationStep {
it.dummy()
runCatching { registrationActionHandler.handleRegisterAction(registrationWizard, action) }
.fold(
onSuccess = {
when {
action.ignoresResult() -> {
// do nothing
}
else -> when (it) {
is RegistrationResult.Success -> onSessionCreated(it.session, isAccountCreated = true)
is RegistrationResult.FlowResponse -> onFlowResponse(it.flowResult)
}
}
},
onFailure = {
if (it !is CancellationException) {
_viewEvents.post(OnboardingViewEvents.Failure(it))
}
}
)
setState { copy(asyncRegistration = Uninitialized) }
}
}
private fun handleRegisterWith(action: OnboardingAction.LoginOrRegister) {
reAuthHelper.data = action.password
currentJob = executeRegistrationStep {
it.createAccount(
action.username,
action.password,
action.initialDeviceName
)
}
}
private fun handleCaptchaDone(action: OnboardingAction.CaptchaDone) {
currentJob = executeRegistrationStep {
it.performReCaptcha(action.captchaResponse)
}
handleRegisterAction(RegisterAction.CreateAccount(
action.username,
action.password,
action.initialDeviceName
))
}
private fun handleResetAction(action: OnboardingAction.ResetAction) {
@ -461,7 +373,7 @@ class OnboardingViewModel @AssistedInject constructor(
}
when (action.signMode) {
SignMode.SignUp -> startRegistrationFlow()
SignMode.SignUp -> handleRegisterAction(RegisterAction.StartRegistration)
SignMode.SignIn -> startAuthenticationFlow()
SignMode.SignInWithMatrixId -> _viewEvents.post(OnboardingViewEvents.OnSignModeSelected(SignMode.SignInWithMatrixId))
SignMode.Unknown -> Unit
@ -499,7 +411,7 @@ class OnboardingViewModel @AssistedInject constructor(
// If there is a pending email validation continue on this step
try {
if (registrationWizard?.isRegistrationStarted == true) {
if (registrationWizard.isRegistrationStarted) {
currentThreePid?.let {
handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnSendEmailSuccess(it)))
}
@ -730,12 +642,6 @@ class OnboardingViewModel @AssistedInject constructor(
}
}
private fun startRegistrationFlow() {
currentJob = executeRegistrationStep {
it.getRegistrationFlow()
}
}
private fun startAuthenticationFlow() {
// Ensure Wizard is ready
loginWizard
@ -745,8 +651,7 @@ class OnboardingViewModel @AssistedInject constructor(
private fun onFlowResponse(flowResult: FlowResult) {
// If dummy stage is mandatory, and password is already sent, do the dummy stage now
if (isRegistrationStarted &&
flowResult.missingStages.any { it is Stage.Dummy && it.mandatory }) {
if (isRegistrationStarted && flowResult.missingStages.any { it is Stage.Dummy && it.mandatory }) {
handleRegisterDummy()
} else {
// Notify the user
@ -754,6 +659,10 @@ class OnboardingViewModel @AssistedInject constructor(
}
}
private fun handleRegisterDummy() {
handleRegisterAction(RegisterAction.RegisterDummy)
}
private suspend fun onSessionCreated(session: Session, isAccountCreated: Boolean) {
val state = awaitState()
state.useCase?.let { useCase ->
@ -1006,6 +915,10 @@ class OnboardingViewModel @AssistedInject constructor(
private fun completePersonalization() {
_viewEvents.post(OnboardingViewEvents.OnPersonalizationComplete)
}
private fun cancelWaitForEmailValidation() {
currentJob = null
}
}
private fun LoginMode.supportsSignModeScreen(): Boolean {

View File

@ -0,0 +1,67 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.onboarding
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
import org.matrix.android.sdk.api.auth.registration.RegistrationResult
import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
import javax.inject.Inject
class RegistrationActionHandler @Inject constructor() {
suspend fun handleRegisterAction(registrationWizard: RegistrationWizard, action: RegisterAction): RegistrationResult {
return when (action) {
RegisterAction.StartRegistration -> registrationWizard.getRegistrationFlow()
is RegisterAction.CaptchaDone -> registrationWizard.performReCaptcha(action.captchaResponse)
is RegisterAction.AcceptTerms -> registrationWizard.acceptTerms()
is RegisterAction.RegisterDummy -> registrationWizard.dummy()
is RegisterAction.AddThreePid -> registrationWizard.addThreePid(action.threePid)
is RegisterAction.SendAgainThreePid -> registrationWizard.sendAgainThreePid()
is RegisterAction.ValidateThreePid -> registrationWizard.handleValidateThreePid(action.code)
is RegisterAction.CheckIfEmailHasBeenValidated -> registrationWizard.checkIfEmailHasBeenValidated(action.delayMillis)
is RegisterAction.CreateAccount -> registrationWizard.createAccount(action.username, action.password, action.initialDeviceName)
}
}
}
sealed interface RegisterAction {
object StartRegistration : RegisterAction
data class CreateAccount(val username: String, val password: String, val initialDeviceName: String) : RegisterAction
data class AddThreePid(val threePid: RegisterThreePid) : RegisterAction
object SendAgainThreePid : RegisterAction
// TODO Confirm Email (from link in the email, open in the phone, intercepted by the app)
data class ValidateThreePid(val code: String) : RegisterAction
data class CheckIfEmailHasBeenValidated(val delayMillis: Long) : RegisterAction
data class CaptchaDone(val captchaResponse: String) : RegisterAction
object AcceptTerms : RegisterAction
object RegisterDummy : RegisterAction
}
fun RegisterAction.ignoresResult() = when (this) {
is RegisterAction.AddThreePid -> true
is RegisterAction.SendAgainThreePid -> true
else -> false
}
fun RegisterAction.hasLoadingState() = when (this) {
is RegisterAction.CheckIfEmailHasBeenValidated -> false
else -> true
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,7 +17,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:visibility="gone"/>
android:visibility="gone" />
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/roomToolbar"
@ -46,20 +46,32 @@
<im.vector.app.features.sync.widget.SyncStateView
android:id="@+id/syncStateView"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appBarLayout" />
<im.vector.app.features.location.live.LocationLiveStatusView
android:id="@+id/locationLiveStatusIndicator"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/syncStateView"
tools:visibility="visible" />
<im.vector.app.features.call.conference.RemoveJitsiWidgetView
android:id="@+id/removeJitsiWidgetView"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?android:colorBackground"
android:minHeight="54dp"
android:visibility="visible"
app:layout_constraintTop_toBottomOf="@id/syncStateView" />
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/locationLiveStatusIndicator" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/timelineRecyclerView"
@ -86,19 +98,19 @@
app:closeIcon="@drawable/ic_close_24dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/removeJitsiWidgetView"/>
app:layout_constraintTop_toBottomOf="@id/removeJitsiWidgetView" />
<im.vector.app.core.ui.views.TypingMessageView
android:id="@+id/typingMessageView"
app:layout_constraintBottom_toTopOf="@id/composerLayout"
app:layout_constraintTop_toBottomOf="@id/timelineRecyclerView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_width="0dp"
android:layout_height="20dp"
android:paddingStart="20dp"
android:paddingEnd="20dp"
tools:visibility="visible"
android:layout_height="20dp"/>
app:layout_constraintBottom_toTopOf="@id/composerLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/timelineRecyclerView"
tools:visibility="visible" />
<im.vector.app.core.ui.views.NotificationAreaView
android:id="@+id/notificationAreaView"
@ -108,7 +120,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible"/>
tools:visibility="visible" />
<ViewStub
android:id="@+id/failedMessagesWarningStub"
@ -130,7 +142,7 @@
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
app:layout_constraintStart_toStartOf="parent" />
<im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView
android:id="@+id/voiceMessageRecorderView"

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

@ -0,0 +1,74 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.onboarding
import im.vector.app.test.fakes.FakeRegistrationWizard
import im.vector.app.test.fakes.FakeSession
import io.mockk.coVerifyAll
import kotlinx.coroutines.test.runBlockingTest
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
import org.matrix.android.sdk.api.auth.registration.RegistrationResult
import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
private val A_SESSION = FakeSession()
private val AN_EXPECTED_RESULT = RegistrationResult.Success(A_SESSION)
private const val A_USERNAME = "a username"
private const val A_PASSWORD = "a password"
private const val AN_INITIAL_DEVICE_NAME = "a device name"
private const val A_CAPTCHA_RESPONSE = "a captcha response"
private const val A_PID_CODE = "a pid code"
private const val EMAIL_VALIDATED_DELAY = 10000L
private val A_PID_TO_REGISTER = RegisterThreePid.Email("an email")
class RegistrationActionHandlerTest {
@Test
fun `when handling register action then delegates to wizard`() = runBlockingTest {
val cases = listOf(
case(RegisterAction.StartRegistration) { getRegistrationFlow() },
case(RegisterAction.CaptchaDone(A_CAPTCHA_RESPONSE)) { performReCaptcha(A_CAPTCHA_RESPONSE) },
case(RegisterAction.AcceptTerms) { acceptTerms() },
case(RegisterAction.RegisterDummy) { dummy() },
case(RegisterAction.AddThreePid(A_PID_TO_REGISTER)) { addThreePid(A_PID_TO_REGISTER) },
case(RegisterAction.SendAgainThreePid) { sendAgainThreePid() },
case(RegisterAction.ValidateThreePid(A_PID_CODE)) { handleValidateThreePid(A_PID_CODE) },
case(RegisterAction.CheckIfEmailHasBeenValidated(EMAIL_VALIDATED_DELAY)) { checkIfEmailHasBeenValidated(EMAIL_VALIDATED_DELAY) },
case(RegisterAction.CreateAccount(A_USERNAME, A_PASSWORD, AN_INITIAL_DEVICE_NAME)) {
createAccount(A_USERNAME, A_PASSWORD, AN_INITIAL_DEVICE_NAME)
}
)
cases.forEach { testSuccessfulActionDelegation(it) }
}
private suspend fun testSuccessfulActionDelegation(case: Case) {
val registrationActionHandler = RegistrationActionHandler()
val fakeRegistrationWizard = FakeRegistrationWizard()
fakeRegistrationWizard.givenSuccessFor(result = A_SESSION, case.expect)
val result = registrationActionHandler.handleRegisterAction(fakeRegistrationWizard, case.action)
coVerifyAll { case.expect(fakeRegistrationWizard) }
result shouldBeEqualTo AN_EXPECTED_RESULT
}
}
private fun case(action: RegisterAction, expect: suspend RegistrationWizard.() -> RegistrationResult) = Case(action, expect)
private class Case(val action: RegisterAction, val expect: suspend RegistrationWizard.() -> RegistrationResult)

View File

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

View File

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

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.test.fakes
import im.vector.app.features.onboarding.RegisterAction
import im.vector.app.features.onboarding.RegistrationActionHandler
import io.mockk.coEvery
import io.mockk.mockk
import org.matrix.android.sdk.api.auth.registration.RegistrationResult
import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
class FakeRegisterActionHandler {
val instance = mockk<RegistrationActionHandler>()
fun givenResultsFor(wizard: RegistrationWizard, result: List<Pair<RegisterAction, RegistrationResult>>) {
coEvery { instance.handleRegisterAction(wizard, any()) } answers { call ->
val actionArg = call.invocation.args[1] as RegisterAction
result.first { it.first == actionArg }.second
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,40 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.test.fixtures
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
import org.matrix.android.sdk.api.session.homeserver.RoomVersionCapabilities
fun aHomeServerCapabilities(
canChangePassword: Boolean = true,
canChangeDisplayName: Boolean = true,
canChangeAvatar: Boolean = true,
canChange3pid: Boolean = true,
maxUploadFileSize: Long = 100L,
lastVersionIdentityServerSupported: Boolean = false,
defaultIdentityServerUrl: String? = null,
roomVersions: RoomVersionCapabilities? = null
) = HomeServerCapabilities(
canChangePassword,
canChangeDisplayName,
canChangeAvatar,
canChange3pid,
maxUploadFileSize,
lastVersionIdentityServerSupported,
defaultIdentityServerUrl,
roomVersions
)