Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f502a2a
Updates FluxC reference
antonis May 10, 2022
a6f506f
Pass selected vertical to the design picker
antonis May 10, 2022
85d9074
Recommend designs based on the selected vertical
antonis May 10, 2022
7142471
Merge branch 'feature/site-design-revamp' into task/16503-recommend-v…
antonis May 10, 2022
9677bb9
Extracts the recommendation logic to a separate file
antonis May 10, 2022
fe387f4
Tests the recommended designs logic
antonis May 10, 2022
0052838
Adds a mechanism for randomising the order of layouts in specific cat…
antonis May 12, 2022
29060b9
Randomises the order of site designs except for the recommended category
antonis May 12, 2022
69d9798
Also save the ordered layouts per category for efficiency
antonis May 12, 2022
9daa723
Unit tests for which categories should be randomised
antonis May 12, 2022
c349fca
Merge branch 'feature/site-design-revamp' into task/16503-recommend-v…
antonis May 12, 2022
11ff1a1
Merge branch 'task/16503-recommend-vertical-designs' into task/16506-…
antonis May 12, 2022
e0980a7
Unselect the layout after the choose action if fired
antonis May 12, 2022
2fa545a
Merge branch 'task/16503-recommend-vertical-designs' into task/16506-…
antonis May 12, 2022
3ddba5e
Adds recommended property in the enhanced_site_creation_site_design_s…
antonis May 13, 2022
e80ad38
Merge branch 'feature/site-design-revamp' into task/16503-recommend-v…
antonis May 13, 2022
319e4c5
Merge branch 'task/16503-recommend-vertical-designs' into task/16506-…
antonis May 13, 2022
62812e7
Merge branch 'task/16503-recommend-vertical-designs' into task/16528-…
antonis May 13, 2022
91c1cab
Hides keyboard on back
antonis May 13, 2022
1020c4d
Merge pull request #16531 from wordpress-mobile/task/16506-randomise-…
ovitrif May 13, 2022
8a16b16
Merge pull request #16538 from wordpress-mobile/task/16528-track-reco…
ovitrif May 13, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import org.wordpress.android.fluxc.network.rest.wpcom.theme.StarterDesignCategor
class LayoutCategoryModel(
private val starterDesignCategory: StarterDesignCategory? = null,
private val blockLayoutCategory: GutenbergLayoutCategory? = null,
val isRecommended: Boolean = false
val isRecommended: Boolean = false,
val randomizeOrder: Boolean = false
) : Parcelable {
val slug: String
get() = starterDesignCategory?.slug ?: blockLayoutCategory?.slug ?: ""
Expand All @@ -24,8 +25,14 @@ class LayoutCategoryModel(
}

@JvmName("starterDesignToLayoutCategories")
fun List<StarterDesignCategory>.toLayoutCategories(recommended: Boolean = false) =
map { LayoutCategoryModel(starterDesignCategory = it, isRecommended = recommended) }
fun List<StarterDesignCategory>.toLayoutCategories(recommended: Boolean = false, randomizeOrder: Boolean = false) =
map {
LayoutCategoryModel(
starterDesignCategory = it,
isRecommended = recommended,
randomizeOrder = randomizeOrder
)
}

@JvmName("gutenbergLayoutToLayoutCategories")
fun List<GutenbergLayoutCategory>.toLayoutCategories() = map { LayoutCategoryModel(blockLayoutCategory = it) }
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ sealed class LayoutPickerUiState(
override val isToolbarVisible: Boolean = false,
val selectedCategoriesSlugs: ArrayList<String> = arrayListOf(),
val selectedLayoutSlug: String? = null,
val isSelectedLayoutRecommended: Boolean = false,
val loadedThumbnailSlugs: ArrayList<String> = arrayListOf(),
val categories: List<CategoryListItemUiState> = listOf(),
val layoutCategories: List<LayoutCategoryUiState> = listOf(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ abstract class LayoutPickerViewModel(

var nestedScrollStates: Bundle = Bundle()

// Map that holds the ordered/randomised layouts list per category (key: slug)
val orderedLayouts: MutableMap<String, List<LayoutModel>> = mutableMapOf()

abstract fun fetchLayouts(preferCache: Boolean = false)

open fun onPreviewChooseTapped() = onDismissPreview()
Expand Down Expand Up @@ -155,8 +158,16 @@ abstract class LayoutPickerViewModel(
}

selectedCategories.forEach { category ->

val layouts = layouts.getFilteredLayouts(category.slug).map { layout ->
val ordered = orderedLayouts[category.slug] ?: if (category.randomizeOrder) {
val randomised = layouts.getFilteredLayouts(category.slug).shuffled()
orderedLayouts[category.slug] = randomised
randomised
} else {
val ordered = layouts.getFilteredLayouts(category.slug)
orderedLayouts[category.slug] = ordered
ordered
}
val layouts = ordered.map { layout ->
val preview = when (_previewMode.value) {
MOBILE -> layout.previewMobile
TABLET -> layout.previewTablet
Expand All @@ -170,7 +181,7 @@ abstract class LayoutPickerViewModel(
mShotPreview = thumbnailPreview,
selected = layout.slug == state.selectedLayoutSlug,
tapOpensPreview = thumbnailTapOpensPreview,
onItemTapped = { onLayoutTapped(layoutSlug = layout.slug) },
onItemTapped = { onLayoutTapped(layoutSlug = layout.slug, category.isRecommended) },
onThumbnailReady = { onThumbnailReady(layoutSlug = layout.slug) }
)
}
Expand All @@ -194,7 +205,7 @@ abstract class LayoutPickerViewModel(
* Layout tapped
* @param layoutSlug the slug of the tapped layout
*/
open fun onLayoutTapped(layoutSlug: String) {
open fun onLayoutTapped(layoutSlug: String, isRecommended: Boolean = false) {
(uiState.value as? Content)?.let { state ->
if (!state.loadedThumbnailSlugs.contains(layoutSlug)) return // No action
if (layoutSlug == state.selectedLayoutSlug) { // deselect
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ class SiteCreationActivity : LocaleAwareActivity(),
finish()
})
mainViewModel.onBackPressedObservable.observe(this, Observer {
ActivityUtils.hideKeyboard(this)
super.onBackPressed()
})
siteCreationIntentsViewModel.onBackButtonPressed.observe(this, Observer {
Expand Down Expand Up @@ -181,7 +182,7 @@ class SiteCreationActivity : LocaleAwareActivity(),
val fragment = when (target.wizardStep) {
INTENTS -> SiteCreationIntentsFragment()
SITE_NAME -> SiteCreationSiteNameFragment.newInstance(target.wizardState.siteIntent)
SITE_DESIGNS -> HomePagePickerFragment()
SITE_DESIGNS -> HomePagePickerFragment.newInstance(target.wizardState.siteIntent)
DOMAINS -> SiteCreationDomainsFragment.newInstance(
screenTitle
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import org.wordpress.android.ui.sitecreation.misc.SiteCreationTracker.PROPERTY.C
import org.wordpress.android.ui.sitecreation.misc.SiteCreationTracker.PROPERTY.FILTER
import org.wordpress.android.ui.sitecreation.misc.SiteCreationTracker.PROPERTY.LOCATION
import org.wordpress.android.ui.sitecreation.misc.SiteCreationTracker.PROPERTY.PREVIEW_MODE
import org.wordpress.android.ui.sitecreation.misc.SiteCreationTracker.PROPERTY.RECOMMENDED
import org.wordpress.android.ui.sitecreation.misc.SiteCreationTracker.PROPERTY.SEARCH_TERM
import org.wordpress.android.ui.sitecreation.misc.SiteCreationTracker.PROPERTY.SEGMENT_ID
import org.wordpress.android.ui.sitecreation.misc.SiteCreationTracker.PROPERTY.SEGMENT_NAME
Expand Down Expand Up @@ -47,7 +48,8 @@ class SiteCreationTracker @Inject constructor(val tracker: AnalyticsTrackerWrapp
SELECTED_FILTERS("selected_filters"),
VERTICAL_SLUG("vertical_slug"),
VARIATION("variation"),
SITE_NAME("site_name")
SITE_NAME("site_name"),
RECOMMENDED("recommended")
}

private var designSelectionSkipped: Boolean = false
Expand Down Expand Up @@ -172,11 +174,11 @@ class SiteCreationTracker @Inject constructor(val tracker: AnalyticsTrackerWrapp
tracker.track(AnalyticsTracker.Stat.ENHANCED_SITE_CREATION_SITE_DESIGN_SKIPPED)
}

fun trackSiteDesignSelected(template: String) {
fun trackSiteDesignSelected(template: String, recommended: Boolean) {
designSelectionSkipped = false
tracker.track(
AnalyticsTracker.Stat.ENHANCED_SITE_CREATION_SITE_DESIGN_SELECTED,
mapOf(TEMPLATE.key to template)
mapOf(TEMPLATE.key to template, RECOMMENDED.key to recommended)
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ class HomePagePickerFragment : Fragment() {
@Inject lateinit var recommendedDimensionProvider: SiteDesignRecommendedDimensionProvider
private lateinit var viewModel: HomePagePickerViewModel

private val siteIntent: String?
get() = arguments?.getString(ARG_SITE_INTENT)

override fun onAttach(context: Context) {
super.onAttach(context)
(requireActivity().applicationContext as WordPress).component().inject(this)
Expand Down Expand Up @@ -128,7 +131,7 @@ class HomePagePickerFragment : Fragment() {
}
}

viewModel.start(displayUtils.isTablet())
viewModel.start(siteIntent, displayUtils.isTablet())
}

private fun HomePagePickerFragmentBinding.setHeaderVisibility(visible: Boolean) {
Expand Down Expand Up @@ -160,4 +163,18 @@ class HomePagePickerFragment : Fragment() {
})
viewModel.onAppBarOffsetChanged(0, scrollThreshold)
}

companion object {
private const val ARG_SITE_INTENT = "arg_site_intent"

fun newInstance(siteIntent: String?): HomePagePickerFragment {
val bundle = Bundle().apply {
putString(ARG_SITE_INTENT, siteIntent)
}

return HomePagePickerFragment().apply {
arguments = bundle
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,17 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import org.wordpress.android.R
import org.wordpress.android.fluxc.Dispatcher
import org.wordpress.android.fluxc.network.rest.wpcom.theme.StarterDesignCategory
import org.wordpress.android.modules.BG_THREAD
import org.wordpress.android.modules.UI_THREAD
import org.wordpress.android.ui.layoutpicker.LayoutCategoryModel
import org.wordpress.android.ui.sitecreation.misc.SiteCreationErrorType.INTERNET_UNAVAILABLE_ERROR
import org.wordpress.android.ui.sitecreation.misc.SiteCreationErrorType.UNKNOWN
import org.wordpress.android.ui.sitecreation.misc.SiteCreationTracker
import org.wordpress.android.ui.layoutpicker.LayoutPickerUiState.Content
import org.wordpress.android.ui.layoutpicker.LayoutPickerUiState.Loading
import org.wordpress.android.ui.layoutpicker.LayoutPickerUiState.Error
import org.wordpress.android.ui.layoutpicker.LayoutPickerViewModel
import org.wordpress.android.ui.layoutpicker.toLayoutCategories
import org.wordpress.android.ui.layoutpicker.toLayoutModels
import org.wordpress.android.ui.sitecreation.usecases.FetchHomePageLayoutsUseCase
import org.wordpress.android.util.NetworkUtilsWrapper
import org.wordpress.android.viewmodel.ResourceProvider
import org.wordpress.android.viewmodel.SingleLiveEvent
import javax.inject.Inject
import javax.inject.Named
Expand All @@ -36,14 +31,16 @@ class HomePagePickerViewModel @Inject constructor(
private val analyticsTracker: SiteCreationTracker,
@Named(BG_THREAD) override val bgDispatcher: CoroutineDispatcher,
@Named(UI_THREAD) override val mainDispatcher: CoroutineDispatcher,
private val resourceProvider: ResourceProvider
private val recommendationProvider: SiteDesignRecommendationProvider
) : LayoutPickerViewModel(mainDispatcher, bgDispatcher, networkUtils, analyticsTracker) {
private val _onDesignActionPressed = SingleLiveEvent<DesignSelectionAction>()
val onDesignActionPressed: LiveData<DesignSelectionAction> = _onDesignActionPressed

private val _onBackButtonPressed = SingleLiveEvent<Unit>()
val onBackButtonPressed: LiveData<Unit> = _onBackButtonPressed

private lateinit var vertical: String

override val useCachedData: Boolean = false
override val shouldUseMobileThumbnail = true
override val thumbnailTapOpensPreview = true
Expand All @@ -62,7 +59,8 @@ class HomePagePickerViewModel @Inject constructor(
dispatcher.unregister(fetchHomePageLayoutsUseCase)
}

fun start(isTablet: Boolean = false) {
fun start(intent: String? = null, isTablet: Boolean = false) {
vertical = intent ?: ""
initializePreviewMode(isTablet)
if (uiState.value !is Content) {
analyticsTracker.trackSiteDesignViewed(selectedPreviewMode().key)
Expand All @@ -85,41 +83,37 @@ class HomePagePickerViewModel @Inject constructor(
analyticsTracker.trackErrorShown(ERROR_CONTEXT, UNKNOWN, "Error fetching designs")
updateUiState(Error())
} else {
handleResponse(event.designs.toLayoutModels(), categoriesWithRecommendations(event.categories))
recommendationProvider.handleResponse(
vertical,
event.designs,
event.categories,
this@HomePagePickerViewModel::handleResponse
)
}
}
}
}

private fun categoriesWithRecommendations(categories: List<StarterDesignCategory>): List<LayoutCategoryModel> {
val defaultVertical = resourceProvider.getString(R.string.hpp_recommended_default_vertical)
val recommendedVertical = resourceProvider.getString(R.string.hpp_recommended_title, defaultVertical)
// TODO: The link with the selected vertical and actual fallback recommendations will be implemented separately
val recommendedCategory = categories.first { it.slug == "blog" }
.copy(title = recommendedVertical, description = recommendedVertical)
return listOf(recommendedCategory).toLayoutCategories(true) + categories.toLayoutCategories()
}

override fun onLayoutTapped(layoutSlug: String) {
override fun onLayoutTapped(layoutSlug: String, isRecommended: Boolean) {
(uiState.value as? Content)?.let {
if (it.loadedThumbnailSlugs.contains(layoutSlug)) {
updateUiState(it.copy(selectedLayoutSlug = layoutSlug))
updateUiState(it.copy(selectedLayoutSlug = layoutSlug, isSelectedLayoutRecommended = isRecommended))
onPreviewTapped()
loadLayouts()
}
}
}

override fun onPreviewChooseTapped() {
super.onPreviewChooseTapped()
onChooseTapped()
}

fun onChooseTapped() {
// TODO: adapt this to the new flow
selectedLayout?.let { layout ->
super.onPreviewChooseTapped()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mkevins Delaying the super call that dismisses the preview screen and unselects the layout seems to fix the flow.
I removed the TODO but I'll be happy to put it back if there is more to it 🙏

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fwiw, I added this comment before removing the bottom action buttons, and I left it in case anything else would need to be wired up when adding the new behavior. Delaying it sounds good to me (an alternative might be to pass it as a parameter, but I think this is simpler 🤷‍♂️ ).

One other consideration I had at the time was with regard to the tracks events, and whether we are differentiating between the user choosing from the main screen and the preview screen (in the old flow). In the new flow there is only one path to choose a theme, so maybe we just need to update the tracks description to reflect this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the feedback @mkevins 🙇

One other consideration I had at the time was with regard to the tracks events, and whether we are differentiating between the user choosing from the main screen and the preview screen (in the old flow). In the new flow there is only one path to choose a theme, so maybe we just need to update the tracks description to reflect this?

That's a good point. I think with the old flow we do not differentiate between choosing directly or choosing in the preview screen and emit the same event enhanced_site_creation_site_design_selected. Despite that I think it's a good idea to make clear in the description of what we track when we update it to add the new recommended property.

ps. I added a draft issue [Site Design Revamp] Update Tracks event (non-blocking) on the board (cc @twstokes)

val template = layout.slug
analyticsTracker.trackSiteDesignSelected(template)
val isRecommended = (uiState.value as? Content)?.isSelectedLayoutRecommended == true
analyticsTracker.trackSiteDesignSelected(template, isRecommended)
_onDesignActionPressed.value = DesignSelectionAction.Choose(template)
return
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package org.wordpress.android.ui.sitecreation.theme

import org.wordpress.android.R
import org.wordpress.android.fluxc.network.rest.wpcom.theme.StarterDesign
import org.wordpress.android.fluxc.network.rest.wpcom.theme.StarterDesignCategory
import org.wordpress.android.ui.layoutpicker.LayoutCategoryModel
import org.wordpress.android.ui.layoutpicker.LayoutModel
import org.wordpress.android.ui.layoutpicker.toLayoutCategories
import org.wordpress.android.ui.layoutpicker.toLayoutModels
import org.wordpress.android.viewmodel.ResourceProvider
import javax.inject.Inject

class SiteDesignRecommendationProvider @Inject constructor(private val resourceProvider: ResourceProvider) {
fun handleResponse(
vertical: String,
designs: List<StarterDesign>,
categories: List<StarterDesignCategory>,
responseHandler: (layouts: List<LayoutModel>, categories: List<LayoutCategoryModel>) -> Unit
) {
val verticalSlug: String? = if (vertical.isNullOrEmpty()) null else getVerticalSlug(vertical)
val hasRecommendations = !verticalSlug.isNullOrEmpty() &&
designs.any { it.group != null && it.group.contains(verticalSlug) }

if (hasRecommendations) {
val recommendedTitle = resourceProvider.getString(R.string.hpp_recommended_title, vertical)
// Create a new category for the recommendations
val recommendedCategory = StarterDesignCategory(
slug = "recommended_$verticalSlug", // The slug is not used but should not already exist
title = recommendedTitle,
description = recommendedTitle,
emoji = ""
)
val designsWithRecommendations = designs.map {
// Add the new category to the recommended designs so that they are filtered correctly
// in the `LayoutPickerViewModel.loadLayouts()` method
if (it.group.contains(verticalSlug)) {
it.copy(categories = it.categories + recommendedCategory)
} else {
it
}
}.toLayoutModels()
val categoriesWithRecommendations =
listOf(recommendedCategory).toLayoutCategories(recommended = true) +
categories.toLayoutCategories(randomizeOrder = true)
responseHandler(designsWithRecommendations, categoriesWithRecommendations)
} else {
// If no designs are recommended for the selected vertical recommend the blog category
val recommendedTitle = resourceProvider.getString(
R.string.hpp_recommended_title,
resourceProvider.getString(R.string.hpp_recommended_default_vertical)
)
val recommendedCategory = categories.firstOrNull { it.slug == "blog" }?.copy(
title = recommendedTitle,
description = recommendedTitle
)
if (recommendedCategory == null) {
// If there is no blog category do not show a recommendation
responseHandler(designs.toLayoutModels(), categories.toLayoutCategories(randomizeOrder = true))
} else {
val categoriesWithRecommendations =
listOf(recommendedCategory).toLayoutCategories(recommended = true, randomizeOrder = true) +
categories.toLayoutCategories(randomizeOrder = true)
responseHandler(designs.toLayoutModels(), categoriesWithRecommendations)
}
}
}

private fun getVerticalSlug(vertical: String): String? {
val slugsArray = resourceProvider.getStringArray(R.array.site_creation_intents_slugs)
val verticalArray = resourceProvider.getStringArray(R.array.site_creation_intents_strings)
if (slugsArray.size != verticalArray.size) {
throw IllegalStateException("Intents arrays size mismatch")
}
return slugsArray.getOrNull(verticalArray.indexOf(vertical))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class HomePagePickerViewModelTest {
@Mock lateinit var analyticsTracker: SiteCreationTracker
@Mock lateinit var resourceProvider: ResourceProvider

private lateinit var recommendationProvider: SiteDesignRecommendationProvider
private lateinit var viewModel: HomePagePickerViewModel

private val mockCategory = StarterDesignCategory(
Expand All @@ -67,14 +68,15 @@ class HomePagePickerViewModelTest {

@Before
fun setUp() {
recommendationProvider = SiteDesignRecommendationProvider(resourceProvider)
viewModel = HomePagePickerViewModel(
networkUtils,
dispatcher,
fetchHomePageLayoutsUseCase,
analyticsTracker,
NoDelayCoroutineDispatcher(),
NoDelayCoroutineDispatcher(),
resourceProvider
recommendationProvider
)
viewModel.uiState.observeForever(uiStateObserver)
viewModel.onDesignActionPressed.observeForever(onDesignActionObserver)
Expand Down Expand Up @@ -212,4 +214,22 @@ class HomePagePickerViewModelTest {
verify(onDesignActionObserver).onChanged(captor.capture())
assertThat(captor.value.template).isEqualTo(mockedDesignSlug)
}

@Test
fun `when the user chooses a recommended design the recommended information is emitted`() = mockResponse {
viewModel.start()
viewModel.onThumbnailReady(mockedDesignSlug)
viewModel.onLayoutTapped(mockedDesignSlug, true)
viewModel.onPreviewChooseTapped()
verify(analyticsTracker).trackSiteDesignSelected(mockedDesignSlug, true)
}

@Test
fun `when the user chooses a design that is not recommended the correct information is emitted`() = mockResponse {
viewModel.start()
viewModel.onThumbnailReady(mockedDesignSlug)
viewModel.onLayoutTapped(mockedDesignSlug, false)
viewModel.onPreviewChooseTapped()
verify(analyticsTracker).trackSiteDesignSelected(mockedDesignSlug, false)
}
}
Loading