Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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 @@ -181,7 +181,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 @@ -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,21 +83,17 @@ 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) {
(uiState.value as? Content)?.let {
if (it.loadedThumbnailSlugs.contains(layoutSlug)) {
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(true) +
categories.toLayoutCategories()
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())
} else {
val categoriesWithRecommendations =
listOf(recommendedCategory).toLayoutCategories(true) +
categories.toLayoutCategories()
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 All @@ -97,6 +99,7 @@ class HomePagePickerViewModelTest {
listOf(mockCategory),
mockedDesignDemoUrl,
"theme",
listOf("stable", "blog"),
"desktopThumbnail",
"tabletThumbnail",
"mobileThumbnail"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package org.wordpress.android.ui.sitecreation.theme

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.whenever
import kotlinx.coroutines.InternalCoroutinesApi
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.junit.MockitoJUnitRunner
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.viewmodel.ResourceProvider

@RunWith(MockitoJUnitRunner::class)
@InternalCoroutinesApi
class SiteDesignRecommendationProviderTest {
@Rule
@JvmField val rule = InstantTaskExecutorRule()

@Mock lateinit var resourceProvider: ResourceProvider

private lateinit var recommendationProvider: SiteDesignRecommendationProvider

private val mockedVerticalSlug = "mockedVerticalSlug"
private val mockedVerticalTitle = "mockedVerticalTitle"
private val recommendedDesignSlug = "recommendedDesignSlug"
private val blogCategory = StarterDesignCategory(
slug = "blog",
title = "Blog",
description = "Blogging designs",
emoji = ""
)
private val anotherCategory = StarterDesignCategory(
slug = "another",
title = "Another",
description = "Random designs",
emoji = ""
)
private val blogDesign = StarterDesign(
"blog",
"title",
1L,
listOf(blogCategory),
"url",
"theme",
listOf("any"),
"desktopThumbnail",
"tabletThumbnail",
"mobileThumbnail"
)
private val anotherDesign = StarterDesign(
recommendedDesignSlug,
"title",
1L,
listOf(anotherCategory),
"url",
"theme",
listOf("any", mockedVerticalSlug),
"desktopThumbnail",
"tabletThumbnail",
"mobileThumbnail"
)
private val allDesigns = listOf(blogDesign, anotherDesign)
private val allCategories = listOf(blogCategory, anotherCategory)

private val slugsArray = arrayOf("art", "automotive", "beauty", mockedVerticalSlug)
private val verticalArray = arrayOf("Art", "Automotive", "Beauty", mockedVerticalTitle)

private class ResponseHandler {
var layouts: List<LayoutModel>? = null
var categories: List<LayoutCategoryModel>? = null
fun handle(layouts: List<LayoutModel>, categories: List<LayoutCategoryModel>) {
this.layouts = layouts
this.categories = categories
}
}

@Before
fun setUp() {
recommendationProvider = SiteDesignRecommendationProvider(resourceProvider)
whenever(resourceProvider.getString(any())).thenReturn("Blogging")
whenever(resourceProvider.getString(any(), any())).thenReturn("Best for Blogging")
whenever(resourceProvider.getStringArray(R.array.site_creation_intents_slugs)).thenReturn(slugsArray)
whenever(resourceProvider.getStringArray(R.array.site_creation_intents_strings)).thenReturn(verticalArray)
}

@Test
fun `when no vertical is selected the blog category is recommended`() {
val handler = ResponseHandler()
recommendationProvider.handleResponse("", allDesigns, allCategories, handler::handle)
assertThat(requireNotNull(handler.categories?.filter { it.isRecommended }?.size)).isEqualTo(1)
assertThat(requireNotNull(handler.categories?.first()?.isRecommended)).isEqualTo(true)
assertThat(requireNotNull(handler.categories?.first()?.slug)).isEqualTo(blogCategory.slug)
}

@Test
fun `when a vertical is selected and there are no recommendations, the blog category is recommended`() {
val handler = ResponseHandler()
recommendationProvider.handleResponse("art", allDesigns, allCategories, handler::handle)
assertThat(requireNotNull(handler.categories?.filter { it.isRecommended }?.size)).isEqualTo(1)
assertThat(requireNotNull(handler.categories?.first()?.isRecommended)).isEqualTo(true)
assertThat(requireNotNull(handler.categories?.first()?.slug)).isEqualTo(blogCategory.slug)
}

@Test
fun `when a vertical is selected and there are recommendations, a recommended category is created`() {
val handler = ResponseHandler()
recommendationProvider.handleResponse(mockedVerticalTitle, allDesigns, allCategories, handler::handle)
assertThat(requireNotNull(handler.categories?.filter { it.isRecommended }?.size)).isEqualTo(1)
assertThat(requireNotNull(handler.categories?.first()?.isRecommended)).isEqualTo(true)
assertThat(requireNotNull(handler.categories?.first()?.slug)).isEqualTo("recommended_$mockedVerticalSlug")
}

@Test
fun `when a vertical is selected the associated design is recommended`() {
val handler = ResponseHandler()
recommendationProvider.handleResponse(mockedVerticalTitle, allDesigns, allCategories, handler::handle)
assertThat(requireNotNull(handler.layouts?.filter { it.slug == recommendedDesignSlug }?.size)).isEqualTo(1)
}

@Test
fun `when no vertical is selected and there is no blog category skip the recommendation`() {
val handler = ResponseHandler()
val noBlogDesigns = listOf(anotherDesign)
val noBlogCategories = listOf(anotherCategory)
recommendationProvider.handleResponse("", noBlogDesigns, noBlogCategories, handler::handle)
assertThat(requireNotNull(handler.categories?.filter { it.isRecommended }?.size)).isEqualTo(0)
}

@Test
fun `when a vertical is selected and there are no recommendations or blog category skip the recommendation`() {
val handler = ResponseHandler()
val noBlogDesigns = listOf(anotherDesign)
val noBlogCategories = listOf(anotherCategory)
recommendationProvider.handleResponse("art", noBlogDesigns, noBlogCategories, handler::handle)
assertThat(requireNotNull(handler.categories?.filter { it.isRecommended }?.size)).isEqualTo(0)
}
}
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ ext {
coroutinesVersion = '1.5.2'
androidxWorkVersion = "2.7.0"

fluxCVersion = 'trunk-1ee442f447044438c9b17068cb224cc162cd5912'
fluxCVersion = '2385-52aa9be4c288fb05c19435cb5f2112c8320b7e7e'

appCompatVersion = '1.0.2'
coreVersion = '1.3.2'
Expand Down