Skip to content

Commit

Permalink
Merge pull request #5375 from vector-im/feature/adm/display-personali…
Browse files Browse the repository at this point in the history
…sation-based-on-capabilities

FTUE - Capability based personalisation flow
  • Loading branch information
ouchadam committed Mar 14, 2022
2 parents 4939a98 + c06d3ff commit 82e1afd
Show file tree
Hide file tree
Showing 27 changed files with 615 additions and 66 deletions.
1 change: 1 addition & 0 deletions changelog.d/5375.wip
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Dynamically showing/hiding onboarding personalisation screens based on the users homeserver capabilities
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,17 @@ import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.preferencesDataStore
import im.vector.app.features.HomeserverCapabilitiesOverride
import im.vector.app.features.VectorOverrides
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import org.matrix.android.sdk.api.extensions.orFalse

private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "vector_overrides")
private val keyForceDialPadDisplay = booleanPreferencesKey("force_dial_pad_display")
private val keyForceLoginFallback = booleanPreferencesKey("force_login_fallback")
private val forceCanChangeDisplayName = booleanPreferencesKey("force_can_change_display_name")
private val forceCanChangeAvatar = booleanPreferencesKey("force_can_change_avatar")

class DebugVectorOverrides(private val context: Context) : VectorOverrides {

Expand All @@ -40,6 +44,13 @@ class DebugVectorOverrides(private val context: Context) : VectorOverrides {
preferences[keyForceLoginFallback].orFalse()
}

override val forceHomeserverCapabilities = context.dataStore.data.map { preferences ->
HomeserverCapabilitiesOverride(
canChangeDisplayName = preferences[forceCanChangeDisplayName],
canChangeAvatar = preferences[forceCanChangeAvatar]
)
}

suspend fun setForceDialPadDisplay(force: Boolean) {
context.dataStore.edit { settings ->
settings[keyForceDialPadDisplay] = force
Expand All @@ -51,4 +62,18 @@ class DebugVectorOverrides(private val context: Context) : VectorOverrides {
settings[keyForceLoginFallback] = force
}
}

suspend fun setHomeserverCapabilities(block: HomeserverCapabilitiesOverride.() -> HomeserverCapabilitiesOverride) {
val capabilitiesOverride = block(forceHomeserverCapabilities.firstOrNull() ?: HomeserverCapabilitiesOverride(null, null))
context.dataStore.edit { settings ->
when (capabilitiesOverride.canChangeDisplayName) {
null -> settings.remove(forceCanChangeDisplayName)
else -> settings[forceCanChangeDisplayName] = capabilitiesOverride.canChangeDisplayName
}
when (capabilitiesOverride.canChangeAvatar) {
null -> settings.remove(forceCanChangeAvatar)
else -> settings[forceCanChangeAvatar] = capabilitiesOverride.canChangeAvatar
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ class DebugPrivateSettingsFragment : VectorBaseFragment<FragmentDebugPrivateSett

override fun invalidate() = withState(viewModel) {
views.forceDialPadTabDisplay.isChecked = it.dialPadVisible
views.forceChangeDisplayNameCapability.bind(it.homeserverCapabilityOverrides.displayName) { option ->
viewModel.handle(DebugPrivateSettingsViewActions.SetDisplayNameCapabilityOverride(option))
}
views.forceChangeAvatarCapability.bind(it.homeserverCapabilityOverrides.avatar) { option ->
viewModel.handle(DebugPrivateSettingsViewActions.SetAvatarCapabilityOverride(option))
}
views.forceLoginFallback.isChecked = it.forceLoginFallback
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ package im.vector.app.features.debug.settings

import im.vector.app.core.platform.VectorViewModelAction

sealed class DebugPrivateSettingsViewActions : VectorViewModelAction {
data class SetDialPadVisibility(val force: Boolean) : DebugPrivateSettingsViewActions()
data class SetForceLoginFallbackEnabled(val force: Boolean) : DebugPrivateSettingsViewActions()
sealed interface DebugPrivateSettingsViewActions : VectorViewModelAction {
data class SetDialPadVisibility(val force: Boolean) : DebugPrivateSettingsViewActions
data class SetForceLoginFallbackEnabled(val force: Boolean) : DebugPrivateSettingsViewActions
data class SetDisplayNameCapabilityOverride(val option: BooleanHomeserverCapabilitiesOverride?) : DebugPrivateSettingsViewActions
data class SetAvatarCapabilityOverride(val option: BooleanHomeserverCapabilitiesOverride?) : DebugPrivateSettingsViewActions
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.debug.features.DebugVectorOverrides
import im.vector.app.features.debug.settings.DebugPrivateSettingsViewActions.SetAvatarCapabilityOverride
import im.vector.app.features.debug.settings.DebugPrivateSettingsViewActions.SetDisplayNameCapabilityOverride
import kotlinx.coroutines.launch

class DebugPrivateSettingsViewModel @AssistedInject constructor(
Expand All @@ -40,10 +43,10 @@ class DebugPrivateSettingsViewModel @AssistedInject constructor(
companion object : MavericksViewModelFactory<DebugPrivateSettingsViewModel, DebugPrivateSettingsViewState> by hiltMavericksViewModelFactory()

init {
observeVectorDataStore()
observeVectorOverrides()
}

private fun observeVectorDataStore() {
private fun observeVectorOverrides() {
debugVectorOverrides.forceDialPad.setOnEach {
copy(
dialPadVisible = it
Expand All @@ -52,13 +55,23 @@ class DebugPrivateSettingsViewModel @AssistedInject constructor(
debugVectorOverrides.forceLoginFallback.setOnEach {
copy(forceLoginFallback = it)
}
debugVectorOverrides.forceHomeserverCapabilities.setOnEach {
val activeDisplayNameOption = BooleanHomeserverCapabilitiesOverride.from(it.canChangeDisplayName)
val activeAvatarOption = BooleanHomeserverCapabilitiesOverride.from(it.canChangeAvatar)
copy(homeserverCapabilityOverrides = homeserverCapabilityOverrides.copy(
displayName = homeserverCapabilityOverrides.displayName.copy(activeOption = activeDisplayNameOption),
avatar = homeserverCapabilityOverrides.avatar.copy(activeOption = activeAvatarOption),
))
}
}

override fun handle(action: DebugPrivateSettingsViewActions) {
when (action) {
is DebugPrivateSettingsViewActions.SetDialPadVisibility -> handleSetDialPadVisibility(action)
is DebugPrivateSettingsViewActions.SetForceLoginFallbackEnabled -> handleSetForceLoginFallbackEnabled(action)
}
is SetDisplayNameCapabilityOverride -> handSetDisplayNameCapabilityOverride(action)
is SetAvatarCapabilityOverride -> handSetAvatarCapabilityOverride(action)
}.exhaustive
}

private fun handleSetDialPadVisibility(action: DebugPrivateSettingsViewActions.SetDialPadVisibility) {
Expand All @@ -72,4 +85,18 @@ class DebugPrivateSettingsViewModel @AssistedInject constructor(
debugVectorOverrides.setForceLoginFallback(action.force)
}
}

private fun handSetDisplayNameCapabilityOverride(action: SetDisplayNameCapabilityOverride) {
viewModelScope.launch {
val forceDisplayName = action.option.toBoolean()
debugVectorOverrides.setHomeserverCapabilities { copy(canChangeDisplayName = forceDisplayName) }
}
}

private fun handSetAvatarCapabilityOverride(action: SetAvatarCapabilityOverride) {
viewModelScope.launch {
val forceAvatar = action.option.toBoolean()
debugVectorOverrides.setHomeserverCapabilities { copy(canChangeAvatar = forceAvatar) }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,23 @@
package im.vector.app.features.debug.settings

import com.airbnb.mvrx.MavericksState
import im.vector.app.features.debug.settings.OverrideDropdownView.OverrideDropdown

data class DebugPrivateSettingsViewState(
val dialPadVisible: Boolean = false,
val forceLoginFallback: Boolean = false,
val homeserverCapabilityOverrides: HomeserverCapabilityOverrides = HomeserverCapabilityOverrides()
) : MavericksState

data class HomeserverCapabilityOverrides(
val displayName: OverrideDropdown<BooleanHomeserverCapabilitiesOverride> = OverrideDropdown(
label = "Override display name capability",
activeOption = null,
options = listOf(BooleanHomeserverCapabilitiesOverride.ForceEnabled, BooleanHomeserverCapabilitiesOverride.ForceDisabled)
),
val avatar: OverrideDropdown<BooleanHomeserverCapabilitiesOverride> = OverrideDropdown(
label = "Override avatar capability",
activeOption = null,
options = listOf(BooleanHomeserverCapabilitiesOverride.ForceEnabled, BooleanHomeserverCapabilitiesOverride.ForceDisabled)
)
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* 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.debug.settings

import android.content.Context
import android.util.AttributeSet
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.LinearLayout
import im.vector.app.R
import im.vector.app.databinding.ViewBooleanDropdownBinding

class OverrideDropdownView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : LinearLayout(context, attrs) {

private val binding = ViewBooleanDropdownBinding.inflate(
LayoutInflater.from(context),
this
)

init {
orientation = HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
}

fun <T : OverrideOption> bind(feature: OverrideDropdown<T>, listener: Listener<T>) {
binding.overrideLabel.text = feature.label

binding.overrideOptions.apply {
val arrayAdapter = ArrayAdapter<String>(context, android.R.layout.simple_spinner_dropdown_item)
val options = listOf("Inactive") + feature.options.map { it.label }
arrayAdapter.addAll(options)
adapter = arrayAdapter

feature.activeOption?.let {
setSelection(options.indexOf(it.label), false)
}

onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
when (position) {
0 -> listener.onOverrideSelected(option = null)
else -> listener.onOverrideSelected(feature.options[position - 1])
}
}

override fun onNothingSelected(parent: AdapterView<*>?) {
// do nothing
}
}
}
}

fun interface Listener<T> {
fun onOverrideSelected(option: T?)
}

data class OverrideDropdown<T : OverrideOption>(
val label: String,
val options: List<T>,
val activeOption: T?,
)
}

interface OverrideOption {
val label: String
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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.debug.settings

sealed interface BooleanHomeserverCapabilitiesOverride : OverrideOption {

companion object {
fun from(value: Boolean?) = when (value) {
null -> null
true -> ForceEnabled
false -> ForceDisabled
}
}

object ForceEnabled : BooleanHomeserverCapabilitiesOverride {
override val label = "Force enabled"
}

object ForceDisabled : BooleanHomeserverCapabilitiesOverride {
override val label = "Force disabled"
}
}

fun BooleanHomeserverCapabilitiesOverride?.toBoolean() = when (this) {
null -> null
BooleanHomeserverCapabilitiesOverride.ForceDisabled -> false
BooleanHomeserverCapabilitiesOverride.ForceEnabled -> true
}
18 changes: 18 additions & 0 deletions vector/src/debug/res/layout/fragment_debug_private_settings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,24 @@
android:layout_height="wrap_content"
android:text="Force login and registration fallback" />

<im.vector.app.features.debug.settings.OverrideDropdownView
android:id="@+id/forceChangeDisplayNameCapability"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="4dp" />

<im.vector.app.features.debug.settings.OverrideDropdownView
android:id="@+id/forceChangeAvatarCapability"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="4dp" />

</LinearLayout>

</ScrollView>
Expand Down
25 changes: 25 additions & 0 deletions vector/src/debug/res/layout/view_boolean_dropdown.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:parentTag="android.widget.LinearLayout">

<TextView
android:id="@+id/overrideLabel"
style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:gravity="center"
android:textColor="?vctr_content_primary"
tools:text="Login version" />

<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/overrideOptions"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />

</merge>
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,16 @@ import kotlinx.coroutines.flow.flowOf
interface VectorOverrides {
val forceDialPad: Flow<Boolean>
val forceLoginFallback: Flow<Boolean>
val forceHomeserverCapabilities: Flow<HomeserverCapabilitiesOverride>?
}

data class HomeserverCapabilitiesOverride(
val canChangeDisplayName: Boolean?,
val canChangeAvatar: Boolean?
)

class DefaultVectorOverrides : VectorOverrides {
override val forceDialPad = flowOf(false)
override val forceLoginFallback = flowOf(false)
override val forceHomeserverCapabilities: Flow<HomeserverCapabilitiesOverride>? = null
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ sealed class OnboardingAction : VectorViewModelAction {

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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,8 @@ sealed class OnboardingViewEvents : VectorViewEvents {
object OnAccountCreated : OnboardingViewEvents()
object OnAccountSignedIn : OnboardingViewEvents()
object OnTakeMeHome : OnboardingViewEvents()
object OnPersonalizeProfile : OnboardingViewEvents()
object OnDisplayNameUpdated : OnboardingViewEvents()
object OnDisplayNameSkipped : OnboardingViewEvents()
object OnChooseDisplayName : OnboardingViewEvents()
object OnChooseProfilePicture : OnboardingViewEvents()
object OnPersonalizationComplete : OnboardingViewEvents()
object OnBack : OnboardingViewEvents()
}
Loading

0 comments on commit 82e1afd

Please sign in to comment.