diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index ead5b079e082..2c78f56ec2fb 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -2,7 +2,7 @@ 20.1 ----- - +* [*] Site creation: enhances the design selection screen with recommended designs [https://github.com/wordpress-mobile/WordPress-Android/pull/16468] 20.0 ----- diff --git a/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java b/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java index d308a19cf0f2..7d93340908d3 100644 --- a/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java +++ b/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java @@ -194,14 +194,8 @@ import org.wordpress.android.ui.reader.views.ReaderSiteSearchResultView; import org.wordpress.android.ui.reader.views.ReaderTagHeaderView; import org.wordpress.android.ui.reader.views.ReaderWebView; -import org.wordpress.android.ui.sitecreation.SiteCreationActivity; -import org.wordpress.android.ui.sitecreation.domains.SiteCreationDomainsFragment; -import org.wordpress.android.ui.sitecreation.previews.SiteCreationPreviewFragment; import org.wordpress.android.ui.sitecreation.services.SiteCreationService; -import org.wordpress.android.ui.sitecreation.sitename.SiteCreationSiteNameFragment; import org.wordpress.android.ui.sitecreation.theme.DesignPreviewFragment; -import org.wordpress.android.ui.sitecreation.theme.HomePagePickerFragment; -import org.wordpress.android.ui.sitecreation.verticals.SiteCreationIntentsFragment; import org.wordpress.android.ui.stats.StatsConnectJetpackActivity; import org.wordpress.android.ui.stats.refresh.lists.StatsListFragment; import org.wordpress.android.ui.stats.refresh.lists.widget.alltime.AllTimeWidgetBlockListProviderFactory; @@ -260,12 +254,6 @@ public interface AppComponent { void inject(PostSignupInterstitialActivity object); - void inject(SiteCreationActivity object); - - void inject(SiteCreationDomainsFragment object); - - void inject(SiteCreationPreviewFragment object); - void inject(JetpackConnectionResultActivity object); void inject(StatsConnectJetpackActivity object); @@ -546,12 +534,6 @@ public interface AppComponent { void inject(ModalLayoutPickerFragment object); - void inject(HomePagePickerFragment object); - - void inject(SiteCreationIntentsFragment object); - - void inject(SiteCreationSiteNameFragment object); - void inject(SubfilterBottomSheetFragment object); void inject(AddContentAdapter object); diff --git a/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java b/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java index 9ade09c229be..706058c64ce1 100644 --- a/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java +++ b/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java @@ -64,12 +64,6 @@ import org.wordpress.android.ui.reader.viewmodels.ReaderPostListViewModel; import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel; import org.wordpress.android.ui.reader.viewmodels.SubfilterPageViewModel; -import org.wordpress.android.ui.sitecreation.SiteCreationMainVM; -import org.wordpress.android.ui.sitecreation.domains.SiteCreationDomainsViewModel; -import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel; -import org.wordpress.android.ui.sitecreation.sitename.SiteCreationSiteNameViewModel; -import org.wordpress.android.ui.sitecreation.theme.HomePagePickerViewModel; -import org.wordpress.android.ui.sitecreation.verticals.SiteCreationIntentsViewModel; import org.wordpress.android.ui.stats.refresh.lists.DaysListViewModel; import org.wordpress.android.ui.stats.refresh.lists.InsightsDetailListViewModel; import org.wordpress.android.ui.stats.refresh.lists.InsightsListViewModel; @@ -261,31 +255,6 @@ abstract class ViewModelModule { @ViewModelKey(HistoryViewModel.class) abstract ViewModel historyViewModel(HistoryViewModel viewModel); - @Binds - @IntoMap - @ViewModelKey(SiteCreationIntentsViewModel.class) - abstract ViewModel siteCreationIntentsViewModel(SiteCreationIntentsViewModel viewModel); - - @Binds - @IntoMap - @ViewModelKey(SiteCreationSiteNameViewModel.class) - abstract ViewModel siteCreationSiteNameViewModel(SiteCreationSiteNameViewModel viewModel); - - @Binds - @IntoMap - @ViewModelKey(SiteCreationDomainsViewModel.class) - abstract ViewModel siteCreationDomainsViewModel(SiteCreationDomainsViewModel viewModel); - - @Binds - @IntoMap - @ViewModelKey(SiteCreationMainVM.class) - abstract ViewModel siteCreationMainVM(SiteCreationMainVM viewModel); - - @Binds - @IntoMap - @ViewModelKey(SitePreviewViewModel.class) - abstract ViewModel newSitePreviewViewModel(SitePreviewViewModel viewModel); - @Binds @IntoMap @ViewModelKey(PostListViewModel.class) @@ -351,11 +320,6 @@ abstract class ViewModelModule { @ViewModelKey(ModalLayoutPickerViewModel.class) abstract ViewModel mlpViewModel(ModalLayoutPickerViewModel viewModel); - @Binds - @IntoMap - @ViewModelKey(HomePagePickerViewModel.class) - abstract ViewModel hppViewModel(HomePagePickerViewModel viewModel); - @Binds @IntoMap @ViewModelKey(PostSignupInterstitialViewModel.class) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutCategoryAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutCategoryAdapter.kt index a6845f54657d..5f1ca5813e22 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutCategoryAdapter.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutCategoryAdapter.kt @@ -4,12 +4,29 @@ import android.os.Bundle import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView.Adapter +import org.wordpress.android.ui.layoutpicker.LayoutCategoryViewType.DEFAULT +import org.wordpress.android.ui.layoutpicker.LayoutCategoryViewType.FOOTER +import org.wordpress.android.ui.layoutpicker.LayoutCategoryViewType.RECOMMENDED + +enum class LayoutCategoryViewType { + DEFAULT, + RECOMMENDED, + FOOTER, +} /** * Renders the layout categories */ -class LayoutCategoryAdapter(private var nestedScrollStates: Bundle) : Adapter() { +class LayoutCategoryAdapter( + private var nestedScrollStates: Bundle, + private val thumbDimensionProvider: ThumbDimensionProvider, + private val recommendedDimensionProvider: ThumbDimensionProvider? = null, + private val showRowDividers: Boolean = true, + private val useLargeCategoryHeading: Boolean = false, + private val footerLayoutResId: Int? = null +) : Adapter() { private var items: List = listOf() + private val shouldShowFooter get() = footerLayoutResId != null && items.isNotEmpty() fun update(newItems: List) { val diffResult = DiffUtil.calculateDiff( @@ -22,19 +39,38 @@ class LayoutCategoryAdapter(private var nestedScrollStates: Bundle) : Adapter FOOTER.ordinal + items[position].isRecommended -> RECOMMENDED.ordinal + else -> DEFAULT.ordinal + } } - override fun onViewRecycled(holder: LayoutsItemViewHolder) { + override fun onViewRecycled(holder: LayoutsRowViewHolder) { super.onViewRecycled(holder) - holder.onRecycled() + (holder as? LayoutsItemViewHolder)?.onRecycled() } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = - LayoutsItemViewHolder(parent = parent, nestedScrollStates = nestedScrollStates) + when (viewType) { + FOOTER.ordinal -> LayoutsFooterViewHolder(parent, footerLayoutResId!!) + else -> + LayoutsItemViewHolder( + parent = parent, + nestedScrollStates = nestedScrollStates, + thumbDimensionProvider = thumbDimensionProvider, + recommendedDimensionProvider = recommendedDimensionProvider, + showRowDividers = showRowDividers, + useLargeCategoryHeading = useLargeCategoryHeading + ) + } fun onRestoreInstanceState(savedInstanceState: Bundle) { nestedScrollStates = savedInstanceState diff --git a/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutCategoryDiffCallback.kt b/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutCategoryDiffCallback.kt index ec4fd12cab99..138a44d73871 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutCategoryDiffCallback.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutCategoryDiffCallback.kt @@ -11,7 +11,8 @@ class LayoutCategoryDiffCallback( ) : Callback() { object Payload override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - return newList[newItemPosition].slug == oldList[oldItemPosition].slug + return newList[newItemPosition].isRecommended == oldList[oldItemPosition].isRecommended && + newList[newItemPosition].slug == oldList[oldItemPosition].slug } override fun getOldListSize(): Int = oldList.size @@ -19,7 +20,8 @@ class LayoutCategoryDiffCallback( override fun getNewListSize(): Int = newList.size override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - return newList[newItemPosition].layouts == oldList[oldItemPosition].layouts + return newList[newItemPosition].isRecommended == oldList[oldItemPosition].isRecommended && + newList[newItemPosition].layouts == oldList[oldItemPosition].layouts } override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutCategoryModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutCategoryModel.kt index 3477e34053a2..95a300198517 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutCategoryModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutCategoryModel.kt @@ -10,7 +10,9 @@ import org.wordpress.android.fluxc.network.rest.wpcom.theme.StarterDesignCategor @SuppressLint("ParcelCreator") class LayoutCategoryModel( private val starterDesignCategory: StarterDesignCategory? = null, - private val blockLayoutCategory: GutenbergLayoutCategory? = null + private val blockLayoutCategory: GutenbergLayoutCategory? = null, + val isRecommended: Boolean = false, + val randomizeOrder: Boolean = false ) : Parcelable { val slug: String get() = starterDesignCategory?.slug ?: blockLayoutCategory?.slug ?: "" @@ -23,7 +25,14 @@ class LayoutCategoryModel( } @JvmName("starterDesignToLayoutCategories") -fun List.toLayoutCategories() = map { LayoutCategoryModel(starterDesignCategory = it) } +fun List.toLayoutCategories(recommended: Boolean = false, randomizeOrder: Boolean = false) = + map { + LayoutCategoryModel( + starterDesignCategory = it, + isRecommended = recommended, + randomizeOrder = randomizeOrder + ) + } @JvmName("gutenbergLayoutToLayoutCategories") fun List.toLayoutCategories() = map { LayoutCategoryModel(blockLayoutCategory = it) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutCategoryUiState.kt b/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutCategoryUiState.kt index 33ba6f20171e..a8b429ddb132 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutCategoryUiState.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutCategoryUiState.kt @@ -6,10 +6,12 @@ package org.wordpress.android.ui.layoutpicker * @param title the layout category title * @param description the layout category description * @param layouts the layouts list + * @param isRecommended defines if the category is recommended (optional with default `false`) */ data class LayoutCategoryUiState( val slug: String, val title: String, val description: String, - val layouts: List + val layouts: List, + val isRecommended: Boolean = false ) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutListItemUiState.kt b/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutListItemUiState.kt index f775acdc946f..b0520102d3be 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutListItemUiState.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutListItemUiState.kt @@ -9,13 +9,18 @@ data class LayoutListItemUiState( val slug: String, val title: String, val preview: String, + val mShotPreview: String, val selected: Boolean, val onItemTapped: (() -> Unit), - val onThumbnailReady: (() -> Unit) + val onThumbnailReady: (() -> Unit), + private val tapOpensPreview: Boolean ) { val contentDescriptionResId: Int - get() = if (selected) R.string.mlp_selected_description else R.string.mlp_notselected_description - + get() = when { + tapOpensPreview -> R.string.hpp_preview_tapped_theme + selected -> R.string.mlp_selected_description + else -> R.string.mlp_notselected_description + } val selectedOverlayVisible: Boolean - get() = selected + get() = !tapOpensPreview && selected } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutPickerTracker.kt b/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutPickerTracker.kt index dde4ded7b34f..54c778aafe78 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutPickerTracker.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutPickerTracker.kt @@ -3,7 +3,9 @@ package org.wordpress.android.ui.layoutpicker interface LayoutPickerTracker { fun trackPreviewModeChanged(mode: String) - fun trackThumbnailModeTapped(mode: String) + // NOOP implementation provided to make override optional in subclasses + fun trackThumbnailModeTapped(mode: String) { + } fun trackPreviewModeTapped(mode: String) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutPickerUiState.kt b/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutPickerUiState.kt index 44f2dbb00651..8f39db02d30a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutPickerUiState.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutPickerUiState.kt @@ -17,6 +17,7 @@ sealed class LayoutPickerUiState( override val isToolbarVisible: Boolean = false, val selectedCategoriesSlugs: ArrayList = arrayListOf(), val selectedLayoutSlug: String? = null, + val isSelectedLayoutRecommended: Boolean = false, val loadedThumbnailSlugs: ArrayList = arrayListOf(), val categories: List = listOf(), val layoutCategories: List = listOf(), diff --git a/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutPickerViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutPickerViewModel.kt index bf809b2335ec..e359e128656f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutPickerViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutPickerViewModel.kt @@ -64,9 +64,14 @@ abstract class LayoutPickerViewModel( } abstract val useCachedData: Boolean + abstract val shouldUseMobileThumbnail: Boolean + open val thumbnailTapOpensPreview = false var nestedScrollStates: Bundle = Bundle() + // Map that holds the ordered/randomised layouts list per category (key: slug) + val orderedLayouts: MutableMap> = mutableMapOf() + abstract fun fetchLayouts(preferCache: Boolean = false) open fun onPreviewChooseTapped() = onDismissPreview() @@ -141,7 +146,7 @@ abstract class LayoutPickerViewModel( } } - private fun loadLayouts() { + fun loadLayouts() { val state = uiState.value as? Content ?: Content() launch(bgDispatcher) { val listItems = ArrayList() @@ -153,18 +158,30 @@ 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 + else -> layout.preview + } + val thumbnailPreview = if (shouldUseMobileThumbnail) layout.previewMobile else preview LayoutListItemUiState( slug = layout.slug, title = layout.title, - preview = when (_previewMode.value) { - MOBILE -> layout.previewMobile - TABLET -> layout.previewTablet - else -> layout.preview - }, + preview = preview, + mShotPreview = thumbnailPreview, selected = layout.slug == state.selectedLayoutSlug, - onItemTapped = { onLayoutTapped(layoutSlug = layout.slug) }, + tapOpensPreview = thumbnailTapOpensPreview, + onItemTapped = { onLayoutTapped(layoutSlug = layout.slug, category.isRecommended) }, onThumbnailReady = { onThumbnailReady(layoutSlug = layout.slug) } ) } @@ -173,7 +190,8 @@ abstract class LayoutPickerViewModel( category.slug, category.title, category.description, - layouts + layouts, + category.isRecommended ) ) } @@ -187,7 +205,7 @@ abstract class LayoutPickerViewModel( * Layout tapped * @param layoutSlug the slug of the tapped layout */ - 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 diff --git a/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutViewHolder.kt index bc615cd39068..14c5c927cc31 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutViewHolder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutViewHolder.kt @@ -3,6 +3,7 @@ package org.wordpress.android.ui.layoutpicker import android.graphics.drawable.Drawable import android.view.LayoutInflater import android.view.ViewGroup +import androidx.core.view.updateLayoutParams import androidx.recyclerview.widget.RecyclerView import org.wordpress.android.R import org.wordpress.android.databinding.ModalLayoutPickerLayoutsCardBinding @@ -16,13 +17,14 @@ import org.wordpress.android.util.extensions.setVisible */ class LayoutViewHolder( private val parent: ViewGroup, - private val binding: ModalLayoutPickerLayoutsCardBinding + private val binding: ModalLayoutPickerLayoutsCardBinding, + private val thumbDimensionProvider: ThumbDimensionProvider ) : RecyclerView.ViewHolder(binding.root) { fun onBind( uiState: LayoutListItemUiState, imageManager: ImageManager ) { - imageManager.loadWithResultListener(binding.preview, MShot(uiState.preview), + imageManager.loadWithResultListener(binding.preview, MShot(uiState.mShotPreview), object : RequestListener { override fun onLoadFailed(e: Exception?, model: Any?) { } @@ -33,6 +35,10 @@ class LayoutViewHolder( }) binding.selectedOverlay.setVisible(uiState.selectedOverlayVisible) + binding.preview.updateLayoutParams { + height = thumbDimensionProvider.previewHeight + width = thumbDimensionProvider.previewWidth + } binding.preview.contentDescription = parent.context.getString(uiState.contentDescriptionResId, uiState.title) binding.preview.context?.let { ctx -> binding.layoutContainer.strokeWidth = if (uiState.selectedOverlayVisible) { @@ -47,13 +53,13 @@ class LayoutViewHolder( } companion object { - fun from(parent: ViewGroup): LayoutViewHolder { + fun from(parent: ViewGroup, thumbDimensionProvider: ThumbDimensionProvider): LayoutViewHolder { val binding = ModalLayoutPickerLayoutsCardBinding.inflate( LayoutInflater.from(parent.context), parent, false ) - return LayoutViewHolder(parent, binding) + return LayoutViewHolder(parent, binding, thumbDimensionProvider) } } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutsAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutsAdapter.kt index fc72126862a7..b285c91f2d28 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutsAdapter.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutsAdapter.kt @@ -11,7 +11,10 @@ import javax.inject.Inject /** * Renders the Layout cards */ -class LayoutsAdapter(context: Context) : RecyclerView.Adapter() { +class LayoutsAdapter( + context: Context, + private val thumbDimensionProvider: ThumbDimensionProvider +) : RecyclerView.Adapter() { @Inject lateinit var imageManager: ImageManager private var layouts: List = listOf() @@ -26,7 +29,8 @@ class LayoutsAdapter(context: Context) : RecyclerView.Adapter( diffResult.dispatchUpdatesTo(this) } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = LayoutViewHolder.from(parent) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + LayoutViewHolder.from(parent, thumbDimensionProvider) override fun getItemCount(): Int = layouts.size diff --git a/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutsItemViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutsRowViewHolder.kt similarity index 58% rename from WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutsItemViewHolder.kt rename to WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutsRowViewHolder.kt index 9ecbfdc503da..feba881c130a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutsItemViewHolder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutsRowViewHolder.kt @@ -2,38 +2,65 @@ package org.wordpress.android.ui.layoutpicker import android.os.Bundle import android.os.Parcelable +import android.util.TypedValue import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup import android.widget.TextView +import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.OnScrollListener import org.wordpress.android.R +import org.wordpress.android.R.dimen +import org.wordpress.android.util.extensions.setVisible + +sealed class LayoutsRowViewHolder(view: View) : RecyclerView.ViewHolder(view) + +class LayoutsFooterViewHolder(parent: ViewGroup, footerLayoutResId: Int) : + LayoutsRowViewHolder( + LayoutInflater.from(parent.context).inflate(footerLayoutResId, parent, false) + ) /** * Modal Layout Picker layouts view holder */ +@Suppress("LongParameterList") class LayoutsItemViewHolder( parent: ViewGroup, private val prefetchItemCount: Int = 4, - private var nestedScrollStates: Bundle -) : RecyclerView.ViewHolder( + private val showRowDividers: Boolean, + private val useLargeCategoryHeading: Boolean, + private var nestedScrollStates: Bundle, + private val thumbDimensionProvider: ThumbDimensionProvider, + private val recommendedDimensionProvider: ThumbDimensionProvider? +) : LayoutsRowViewHolder( LayoutInflater.from( parent.context ).inflate(R.layout.modal_layout_picker_layouts_row, parent, false) ) { + private val rowDivider: View = itemView.findViewById(R.id.layouts_row_separator_line) private val title: TextView = itemView.findViewById(R.id.title) + private val subtitle: TextView = itemView.findViewById(R.id.subtitle) private var currentItem: LayoutCategoryUiState? = null private val recycler: RecyclerView by lazy { + val dimensionProvider = if (currentItem?.isRecommended == true && recommendedDimensionProvider != null) { + recommendedDimensionProvider + } else { + thumbDimensionProvider + } + itemView.updateLayoutParams { + height = dimensionProvider.rowHeight + } itemView.findViewById(R.id.layouts_recycler_view).apply { layoutManager = LinearLayoutManager( context, RecyclerView.HORIZONTAL, false ).apply { initialPrefetchItemCount = prefetchItemCount } - setRecycledViewPool(RecyclerView.RecycledViewPool()) - adapter = LayoutsAdapter(parent.context) + adapter = LayoutsAdapter(parent.context, dimensionProvider) addOnScrollListener(object : OnScrollListener() { override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { @@ -48,7 +75,17 @@ class LayoutsItemViewHolder( fun bind(category: LayoutCategoryUiState) { currentItem = category + rowDivider.isVisible = showRowDividers title.text = category.description + + title.setTextSize( + TypedValue.COMPLEX_UNIT_PX, + title.resources.getDimensionPixelSize( + if (useLargeCategoryHeading) dimen.text_sz_extra_large else dimen.text_sz_large + ).toFloat() + ) + + subtitle.setVisible(category.isRecommended) (recycler.adapter as LayoutsAdapter).setData(category.layouts) restoreScrollState(recycler, category.title) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/ThumbDimensionProvider.kt b/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/ThumbDimensionProvider.kt index 09d257055200..7d1dd30a50ed 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/ThumbDimensionProvider.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/ThumbDimensionProvider.kt @@ -1,15 +1,9 @@ package org.wordpress.android.ui.layoutpicker -import org.wordpress.android.R -import org.wordpress.android.viewmodel.ContextProvider -import javax.inject.Inject - -class ThumbDimensionProvider @Inject constructor(private val contextProvider: ContextProvider) { +interface ThumbDimensionProvider { val previewWidth: Int - get() = contextProvider.getContext().resources.getDimensionPixelSize(R.dimen.mlp_layout_card_width) - val previewHeight: Int - get() = contextProvider.getContext().resources.getDimensionPixelSize(R.dimen.mlp_layout_card_height) - - val scale: Double = 1.0 // Passing 1.0 and the rendered pixels per device in previewWidth + val rowHeight: Int + val scale: Double + get() = 1.0 // Passing 1.0 and the rendered pixels per device in previewWidth } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mlp/ModalLayoutPickerDimensionProvider.kt b/WordPress/src/main/java/org/wordpress/android/ui/mlp/ModalLayoutPickerDimensionProvider.kt new file mode 100644 index 000000000000..d0c74d29dc92 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/mlp/ModalLayoutPickerDimensionProvider.kt @@ -0,0 +1,18 @@ +package org.wordpress.android.ui.mlp + +import org.wordpress.android.R.dimen +import org.wordpress.android.ui.layoutpicker.ThumbDimensionProvider +import org.wordpress.android.viewmodel.ContextProvider +import javax.inject.Inject + +class ModalLayoutPickerDimensionProvider @Inject constructor(private val contextProvider: ContextProvider) : + ThumbDimensionProvider { + override val previewWidth: Int + get() = contextProvider.getContext().resources.getDimensionPixelSize(dimen.mlp_layout_card_width) + + override val previewHeight: Int + get() = contextProvider.getContext().resources.getDimensionPixelSize(dimen.mlp_layout_card_height) + + override val rowHeight: Int + get() = contextProvider.getContext().resources.getDimensionPixelSize(dimen.mlp_layouts_row_height) +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mlp/ModalLayoutPickerFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/mlp/ModalLayoutPickerFragment.kt index b3b172fce2ff..d61ffaf33e34 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mlp/ModalLayoutPickerFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mlp/ModalLayoutPickerFragment.kt @@ -6,6 +6,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.view.ViewCompat +import androidx.core.view.updateLayoutParams import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -38,6 +39,7 @@ import javax.inject.Inject class ModalLayoutPickerFragment : FullscreenBottomSheetDialogFragment() { @Inject internal lateinit var uiHelper: UiHelpers @Inject lateinit var viewModelFactory: ViewModelProvider.Factory + @Inject lateinit var thumbDimensionProvider: ModalLayoutPickerDimensionProvider private lateinit var viewModel: ModalLayoutPickerViewModel private lateinit var previewModeSelectorPopup: PreviewModeSelectorPopup @@ -73,7 +75,7 @@ class ModalLayoutPickerFragment : FullscreenBottomSheetDialogFragment() { layoutsRecyclerView.apply { layoutManager = LinearLayoutManager(requireActivity()) - adapter = LayoutCategoryAdapter(viewModel.nestedScrollStates) + adapter = LayoutCategoryAdapter(viewModel.nestedScrollStates, thumbDimensionProvider) } modalLayoutPickerTitlebar.backButton.setOnClickListener { @@ -96,6 +98,14 @@ class ModalLayoutPickerFragment : FullscreenBottomSheetDialogFragment() { viewModel.onThumbnailModePressed() } + modalLayoutPickerLayoutsSkeleton.layoutsSkeleton.updateLayoutParams { + height = thumbDimensionProvider.rowHeight + } + modalLayoutPickerLayoutsSkeleton.skeletonCardView.updateLayoutParams { + height = thumbDimensionProvider.previewHeight + width = thumbDimensionProvider.previewWidth + } + setScrollListener() setupViewModel(savedInstanceState) @@ -118,7 +128,7 @@ class ModalLayoutPickerFragment : FullscreenBottomSheetDialogFragment() { private fun ModalLayoutPickerFragmentBinding.setupViewModel(savedInstanceState: Bundle?) { viewModel.loadSavedState(savedInstanceState) - viewModel.uiState.observe(this@ModalLayoutPickerFragment, { uiState -> + viewModel.uiState.observe(this@ModalLayoutPickerFragment) { uiState -> setHeaderVisibility(uiState.isHeaderVisible) setDescriptionVisibility(uiState.isDescriptionVisible) setButtonsVisibility(uiState.buttonsUiState) @@ -135,13 +145,13 @@ class ModalLayoutPickerFragment : FullscreenBottomSheetDialogFragment() { uiState.subtitle?.let { modalLayoutPickerError.actionableEmptyView.subtitle.setText(it) } } } - }) + } - viewModel.onThumbnailModeButtonPressed.observe(viewLifecycleOwner, { + viewModel.onThumbnailModeButtonPressed.observe(viewLifecycleOwner) { previewModeSelectorPopup.show(viewModel) - }) + } - viewModel.onPreviewActionPressed.observe(viewLifecycleOwner, { action -> + viewModel.onPreviewActionPressed.observe(viewLifecycleOwner) { action -> activity?.supportFragmentManager?.let { fm -> when (action) { is Show -> { @@ -153,11 +163,11 @@ class ModalLayoutPickerFragment : FullscreenBottomSheetDialogFragment() { } } } - }) + } - viewModel.onCategorySelectionChanged.observeEvent(this@ModalLayoutPickerFragment, { + viewModel.onCategorySelectionChanged.observeEvent(this@ModalLayoutPickerFragment) { layoutsRecyclerView.smoothScrollToPosition(0) - }) + } } private fun ModalLayoutPickerFragmentBinding.setHeaderVisibility(visible: Boolean) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/SiteCreationActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/SiteCreationActivity.kt index ad532624cebe..fb330975a50c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/SiteCreationActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/SiteCreationActivity.kt @@ -4,11 +4,12 @@ import android.app.Activity import android.content.Intent import android.os.Bundle import android.view.MenuItem +import androidx.activity.viewModels import androidx.fragment.app.Fragment import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProvider +import kotlinx.coroutines.cancel +import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.R -import org.wordpress.android.WordPress import org.wordpress.android.ui.ActivityLauncher import org.wordpress.android.ui.LocaleAwareActivity import org.wordpress.android.ui.accounts.HelpActivity.Origin @@ -48,6 +49,7 @@ import org.wordpress.android.util.wizard.WizardNavigationTarget import javax.inject.Inject @Suppress("TooManyFunctions") +@AndroidEntryPoint class SiteCreationActivity : LocaleAwareActivity(), IntentsScreenListener, SiteNameScreenListener, @@ -56,27 +58,19 @@ class SiteCreationActivity : LocaleAwareActivity(), OnHelpClickedListener, BasicDialogPositiveClickInterface, BasicDialogNegativeClickInterface { - @Inject internal lateinit var viewModelFactory: ViewModelProvider.Factory @Inject internal lateinit var uiHelpers: UiHelpers @Inject internal lateinit var siteNameFeatureConfig: SiteNameFeatureConfig - private lateinit var mainViewModel: SiteCreationMainVM - private lateinit var hppViewModel: HomePagePickerViewModel - private lateinit var siteCreationIntentsViewModel: SiteCreationIntentsViewModel - private lateinit var siteCreationSiteNameViewModel: SiteCreationSiteNameViewModel + private val mainViewModel: SiteCreationMainVM by viewModels() + private val hppViewModel: HomePagePickerViewModel by viewModels() + private val siteCreationIntentsViewModel: SiteCreationIntentsViewModel by viewModels() + private val siteCreationSiteNameViewModel: SiteCreationSiteNameViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - (application as WordPress).component().inject(this) setContentView(R.layout.site_creation_activity) - mainViewModel = ViewModelProvider(this, viewModelFactory).get(SiteCreationMainVM::class.java) - hppViewModel = ViewModelProvider(this, viewModelFactory).get(HomePagePickerViewModel::class.java) - siteCreationIntentsViewModel = ViewModelProvider(this, viewModelFactory) - .get(SiteCreationIntentsViewModel::class.java) - siteCreationSiteNameViewModel = ViewModelProvider(this, viewModelFactory) - .get(SiteCreationSiteNameViewModel::class.java) val siteCreationSource = intent.extras?.getString(ARG_CREATE_SITE_SOURCE) mainViewModel.start(savedInstanceState, SiteCreationSource.fromString(siteCreationSource)) - hppViewModel.loadSavedState(savedInstanceState) + mainViewModel.preloadThumbnails(this) observeVMState() } @@ -84,7 +78,6 @@ class SiteCreationActivity : LocaleAwareActivity(), override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) mainViewModel.writeToBundle(outState) - hppViewModel.writeToBundle(outState) } private fun observeVMState() { @@ -124,6 +117,7 @@ class SiteCreationActivity : LocaleAwareActivity(), finish() }) mainViewModel.onBackPressedObservable.observe(this, Observer { + ActivityUtils.hideKeyboard(this) super.onBackPressed() }) siteCreationIntentsViewModel.onBackButtonPressed.observe(this, Observer { @@ -181,7 +175,11 @@ class SiteCreationActivity : LocaleAwareActivity(), val fragment = when (target.wizardStep) { INTENTS -> SiteCreationIntentsFragment() SITE_NAME -> SiteCreationSiteNameFragment.newInstance(target.wizardState.siteIntent) - SITE_DESIGNS -> HomePagePickerFragment() + SITE_DESIGNS -> { + // Cancel preload job before displaying the theme picker. + mainViewModel.preloadingJob?.cancel("Preload did not complete before theme picker was shown.") + HomePagePickerFragment.newInstance(target.wizardState.siteIntent) + } DOMAINS -> SiteCreationDomainsFragment.newInstance( screenTitle ) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/SiteCreationMainVM.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/SiteCreationMainVM.kt index a3e7269cb170..ffc73de036f9 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/SiteCreationMainVM.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/SiteCreationMainVM.kt @@ -1,14 +1,22 @@ package org.wordpress.android.ui.sitecreation import android.annotation.SuppressLint +import android.content.Context import android.os.Bundle import android.os.Parcelable import androidx.annotation.StringRes import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.parcelize.Parcelize import org.wordpress.android.R +import org.wordpress.android.fluxc.Dispatcher +import org.wordpress.android.networking.MShot import org.wordpress.android.ui.sitecreation.SiteCreationMainVM.SiteCreationScreenTitle.ScreenTitleEmpty import org.wordpress.android.ui.sitecreation.SiteCreationMainVM.SiteCreationScreenTitle.ScreenTitleGeneral import org.wordpress.android.ui.sitecreation.SiteCreationMainVM.SiteCreationScreenTitle.ScreenTitleStepCount @@ -18,8 +26,11 @@ import org.wordpress.android.ui.sitecreation.SiteCreationStep.SITE_PREVIEW import org.wordpress.android.ui.sitecreation.misc.SiteCreationSource import org.wordpress.android.ui.sitecreation.misc.SiteCreationTracker import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.CreateSiteState +import org.wordpress.android.ui.sitecreation.usecases.FetchHomePageLayoutsUseCase import org.wordpress.android.ui.utils.UiString.UiStringRes +import org.wordpress.android.util.NetworkUtilsWrapper import org.wordpress.android.util.experiments.SiteNameABExperiment +import org.wordpress.android.util.image.ImageManager import org.wordpress.android.util.wizard.WizardManager import org.wordpress.android.util.wizard.WizardNavigationTarget import org.wordpress.android.util.wizard.WizardState @@ -45,16 +56,32 @@ data class SiteCreationState( typealias NavigationTarget = WizardNavigationTarget +@HiltViewModel class SiteCreationMainVM @Inject constructor( private val tracker: SiteCreationTracker, private val wizardManager: WizardManager, - private val siteNameABExperiment: SiteNameABExperiment + private val siteNameABExperiment: SiteNameABExperiment, + private val networkUtils: NetworkUtilsWrapper, + private val dispatcher: Dispatcher, + private val fetchHomePageLayoutsUseCase: FetchHomePageLayoutsUseCase, + private val imageManager: ImageManager ) : ViewModel() { + init { + dispatcher.register(fetchHomePageLayoutsUseCase) + } + + override fun onCleared() { + super.onCleared() + dispatcher.unregister(fetchHomePageLayoutsUseCase) + } + private var isStarted = false private var siteCreationCompleted = false private lateinit var siteCreationState: SiteCreationState + internal var preloadingJob: Job? = null + val navigationTargetObservable: SingleEventObservable by lazy { SingleEventObservable( Transformations.map(wizardManager.navigatorLiveData) { @@ -95,6 +122,20 @@ class SiteCreationMainVM @Inject constructor( } } + fun preloadThumbnails(context: Context) { + if (preloadingJob == null) { + preloadingJob = viewModelScope.launch(Dispatchers.IO) { + if (networkUtils.isNetworkAvailable()) { + val response = fetchHomePageLayoutsUseCase.fetchStarterDesigns() + for (design in response.designs) { + imageManager.preload(context, MShot(design.previewMobile)) + } + } + preloadingJob = null + } + } + } + fun writeToBundle(outState: Bundle) { outState.putBoolean(KEY_SITE_CREATION_COMPLETED, siteCreationCompleted) outState.putInt(KEY_CURRENT_STEP, wizardManager.currentStep) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/SiteCreationDomainsFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/SiteCreationDomainsFragment.kt index 826aec51190c..d9639402fb91 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/SiteCreationDomainsFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/SiteCreationDomainsFragment.kt @@ -4,11 +4,11 @@ import android.content.Context import android.os.Bundle import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.ViewModelProvider +import androidx.fragment.app.activityViewModels import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.R -import org.wordpress.android.WordPress import org.wordpress.android.databinding.SiteCreationDomainsScreenBinding import org.wordpress.android.databinding.SiteCreationFormScreenBinding import org.wordpress.android.ui.accounts.HelpActivity @@ -21,11 +21,11 @@ import org.wordpress.android.util.DisplayUtilsWrapper import javax.inject.Inject @Suppress("TooManyFunctions") +@AndroidEntryPoint class SiteCreationDomainsFragment : SiteCreationBaseFormFragment() { private var searchInputWithHeader: SearchInputWithHeader? = null - private lateinit var viewModel: SiteCreationDomainsViewModel + private val viewModel: SiteCreationDomainsViewModel by activityViewModels() - @Inject internal lateinit var viewModelFactory: ViewModelProvider.Factory @Inject internal lateinit var uiHelpers: UiHelpers @Inject internal lateinit var displayUtils: DisplayUtilsWrapper @@ -41,11 +41,6 @@ class SiteCreationDomainsFragment : SiteCreationBaseFormFragment() { } } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - (requireActivity().application as WordPress).component().inject(this) - } - override fun getContentLayout(): Int { return R.layout.site_creation_domains_screen } @@ -84,9 +79,6 @@ class SiteCreationDomainsFragment : SiteCreationBaseFormFragment() { } private fun SiteCreationDomainsScreenBinding.initViewModel() { - viewModel = ViewModelProvider(this@SiteCreationDomainsFragment, viewModelFactory) - .get(SiteCreationDomainsViewModel::class.java) - viewModel.uiState.observe(this@SiteCreationDomainsFragment, { uiState -> uiState?.let { searchInputWithHeader?.updateHeader(requireActivity(), uiState.headerUiState) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/SiteCreationDomainsViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/SiteCreationDomainsViewModel.kt index 81561c556371..d5708b498fbc 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/SiteCreationDomainsViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/SiteCreationDomainsViewModel.kt @@ -5,6 +5,7 @@ import androidx.annotation.VisibleForTesting import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -45,6 +46,7 @@ import kotlin.properties.Delegates private const val THROTTLE_DELAY = 500L private const val ERROR_CONTEXT = "domains" +@HiltViewModel class SiteCreationDomainsViewModel @Inject constructor( private val networkUtils: NetworkUtilsWrapper, private val dispatcher: Dispatcher, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/misc/SiteCreationTracker.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/misc/SiteCreationTracker.kt index 927007df38c5..09f2fd446f3e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/misc/SiteCreationTracker.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/misc/SiteCreationTracker.kt @@ -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 @@ -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 @@ -167,23 +169,16 @@ class SiteCreationTracker @Inject constructor(val tracker: AnalyticsTrackerWrapp ) } - override fun trackThumbnailModeTapped(mode: String) { - tracker.track( - AnalyticsTracker.Stat.ENHANCED_SITE_CREATION_SITE_DESIGN_THUMBNAIL_MODE_BUTTON_TAPPED, - mapOf(PREVIEW_MODE.key to mode) - ) - } - fun trackSiteDesignSkipped() { designSelectionSkipped = true 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) ) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/previews/SiteCreationPreviewFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/previews/SiteCreationPreviewFragment.kt index e47eb09b2e96..4e83741a90dd 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/previews/SiteCreationPreviewFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/previews/SiteCreationPreviewFragment.kt @@ -15,7 +15,8 @@ import android.view.View.OnLayoutChangeListener import android.view.animation.DecelerateInterpolator import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat -import androidx.lifecycle.ViewModelProvider +import androidx.fragment.app.viewModels +import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.R import org.wordpress.android.WordPress import org.wordpress.android.databinding.FullscreenErrorWithRetryBinding @@ -46,6 +47,7 @@ private const val ARG_DATA = "arg_site_creation_data" private const val SLIDE_IN_ANIMATION_DURATION = 450L @Suppress("TooManyFunctions") +@AndroidEntryPoint class SiteCreationPreviewFragment : SiteCreationBaseFormFragment(), ErrorManagedWebViewClientListener { /** @@ -53,12 +55,11 @@ class SiteCreationPreviewFragment : SiteCreationBaseFormFragment(), * automatically shows system notifications when site creation is in progress and the app is in the background. */ private var serviceEventConnection: ServiceEventConnection? = null - private lateinit var viewModel: SitePreviewViewModel + private val viewModel: SitePreviewViewModel by viewModels() private var animatorSet: AnimatorSet? = null private val isLandscape: Boolean get() = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE - @Inject internal lateinit var viewModelFactory: ViewModelProvider.Factory @Inject internal lateinit var uiHelpers: UiHelpers private var binding: SiteCreationPreviewScreenBinding? = null @@ -75,7 +76,6 @@ class SiteCreationPreviewFragment : SiteCreationBaseFormFragment(), override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - (requireNotNull(activity).application as WordPress).component().inject(this) if (savedInstanceState == null) { // we need to manually clear the SiteCreationService state so we don't for example receive sticky events // from the previous run of the SiteCreation flow. @@ -121,10 +121,6 @@ class SiteCreationPreviewFragment : SiteCreationBaseFormFragment(), } private fun SiteCreationPreviewScreenDefaultBinding.initViewModel() { - viewModel = ViewModelProvider( - this@SiteCreationPreviewFragment, - viewModelFactory - ).get(SitePreviewViewModel::class.java) viewModel.uiState.observe(this@SiteCreationPreviewFragment, { uiState -> uiState?.let { when (uiState) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/previews/SitePreviewViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/previews/SitePreviewViewModel.kt index 1584ee1d3e2d..3ddbf1614779 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/previews/SitePreviewViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/previews/SitePreviewViewModel.kt @@ -6,6 +6,7 @@ import android.os.Parcelable import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.parcelize.Parcelize import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -67,6 +68,7 @@ private val loadingTexts = listOf( UiStringRes(R.string.new_site_creation_creating_site_loading_4) ) +@HiltViewModel class SitePreviewViewModel @Inject constructor( private val dispatcher: Dispatcher, private val siteStore: SiteStore, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/sitename/SiteCreationSiteNameFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/sitename/SiteCreationSiteNameFragment.kt index 700c85f516ea..74a4253cecae 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/sitename/SiteCreationSiteNameFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/sitename/SiteCreationSiteNameFragment.kt @@ -1,6 +1,5 @@ package org.wordpress.android.ui.sitecreation.sitename -import android.content.Context import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -9,11 +8,11 @@ import android.view.inputmethod.EditorInfo import androidx.core.view.isInvisible import androidx.core.widget.doOnTextChanged import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProvider +import androidx.fragment.app.activityViewModels +import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.R import org.wordpress.android.R.color import org.wordpress.android.R.string -import org.wordpress.android.WordPress import org.wordpress.android.databinding.SiteCreationSiteNameFragmentBinding import org.wordpress.android.ui.sitecreation.sitename.SiteCreationSiteNameViewModel.SiteNameUiState import org.wordpress.android.ui.utils.HtmlMessageUtils @@ -27,20 +26,15 @@ import javax.inject.Inject * Implements the Site Name UI */ @Suppress("TooManyFunctions") +@AndroidEntryPoint class SiteCreationSiteNameFragment : Fragment() { - @Inject internal lateinit var viewModelFactory: ViewModelProvider.Factory @Inject internal lateinit var uiHelper: UiHelpers @Inject internal lateinit var displayUtils: DisplayUtilsWrapper @Inject internal lateinit var htmlMessageUtils: HtmlMessageUtils - private lateinit var viewModel: SiteCreationSiteNameViewModel + private val viewModel: SiteCreationSiteNameViewModel by activityViewModels() private var binding: SiteCreationSiteNameFragmentBinding? = null - override fun onAttach(context: Context) { - super.onAttach(context) - (requireActivity().applicationContext as WordPress).component().inject(this) - } - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -52,9 +46,6 @@ class SiteCreationSiteNameFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - viewModel = ViewModelProvider(requireActivity(), viewModelFactory) - .get(SiteCreationSiteNameViewModel::class.java) - val binding = SiteCreationSiteNameFragmentBinding.bind(view) this.binding = binding with(binding) { @@ -80,7 +71,6 @@ class SiteCreationSiteNameFragment : Fragment() { siteCreationSiteNameHeader.title?.text = headerTitleWithIntentColoredBlueIfSpecified siteCreationSiteNameHeader.subtitle?.setText(R.string.new_site_creation_site_name_header_subtitle) siteCreationSiteNameTitlebar.appBarTitle.setText(R.string.new_site_creation_site_name_title) - siteCreationSiteNameTitlebar.appBarTitle.isInvisible = !displayUtils.isPhoneLandscape() viewModel.uiState.value?.siteName.let { input.setText(it) } input.requestFocus() ActivityUtils.showKeyboard(input) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/sitename/SiteCreationSiteNameViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/sitename/SiteCreationSiteNameViewModel.kt index 702d17ac0737..e32449c615ba 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/sitename/SiteCreationSiteNameViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/sitename/SiteCreationSiteNameViewModel.kt @@ -3,6 +3,7 @@ package org.wordpress.android.ui.sitecreation.sitename import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -13,6 +14,7 @@ import javax.inject.Inject import javax.inject.Named import kotlin.coroutines.CoroutineContext +@HiltViewModel class SiteCreationSiteNameViewModel @Inject constructor( private val analyticsTracker: SiteCreationTracker, @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/theme/HomePagePickerFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/theme/HomePagePickerFragment.kt index d301592cdf65..aab9091630d8 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/theme/HomePagePickerFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/theme/HomePagePickerFragment.kt @@ -1,54 +1,49 @@ package org.wordpress.android.ui.sitecreation.theme -import android.content.Context import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.view.ViewCompat -import androidx.core.view.isInvisible +import androidx.core.view.isGone +import androidx.core.view.updateLayoutParams import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView import com.google.android.material.appbar.AppBarLayout +import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.R -import org.wordpress.android.WordPress import org.wordpress.android.databinding.HomePagePickerFragmentBinding -import org.wordpress.android.ui.PreviewModeSelectorPopup -import org.wordpress.android.ui.layoutpicker.CategoriesAdapter import org.wordpress.android.ui.layoutpicker.LayoutCategoryAdapter import org.wordpress.android.ui.layoutpicker.LayoutPickerUiState import org.wordpress.android.ui.layoutpicker.LayoutPickerViewModel.DesignPreviewAction.Dismiss import org.wordpress.android.ui.layoutpicker.LayoutPickerViewModel.DesignPreviewAction.Show import org.wordpress.android.ui.sitecreation.theme.DesignPreviewFragment.Companion.DESIGN_PREVIEW_TAG import org.wordpress.android.ui.utils.UiHelpers -import org.wordpress.android.util.AniUtils import org.wordpress.android.util.DisplayUtilsWrapper import org.wordpress.android.util.ToastUtils import org.wordpress.android.util.config.SiteNameFeatureConfig -import org.wordpress.android.util.image.ImageManager import org.wordpress.android.util.extensions.setVisible -import org.wordpress.android.viewmodel.observeEvent +import org.wordpress.android.util.image.ImageManager import javax.inject.Inject /** * Implements the Home Page Picker UI */ @Suppress("TooManyFunctions") +@AndroidEntryPoint class HomePagePickerFragment : Fragment() { @Inject lateinit var imageManager: ImageManager @Inject lateinit var displayUtils: DisplayUtilsWrapper @Inject internal lateinit var uiHelper: UiHelpers @Inject lateinit var siteNameFeatureConfig: SiteNameFeatureConfig @Inject lateinit var viewModelFactory: ViewModelProvider.Factory - private lateinit var viewModel: HomePagePickerViewModel - private lateinit var previewModeSelectorPopup: PreviewModeSelectorPopup + @Inject lateinit var thumbDimensionProvider: SiteDesignPickerDimensionProvider + @Inject lateinit var recommendedDimensionProvider: SiteDesignRecommendedDimensionProvider + private val viewModel: HomePagePickerViewModel by activityViewModels() - override fun onAttach(context: Context) { - super.onAttach(context) - (requireActivity().applicationContext as WordPress).component().inject(this) - } + private val siteIntent: String? + get() = arguments?.getString(ARG_SITE_INTENT) override fun onCreateView( inflater: LayoutInflater, @@ -61,64 +56,65 @@ class HomePagePickerFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - viewModel = ViewModelProvider(requireActivity(), viewModelFactory).get(HomePagePickerViewModel::class.java) + savedInstanceState?.let { + viewModel.loadSavedState(it) + } with(HomePagePickerFragmentBinding.bind(view)) { - categoriesRecyclerView.apply { - layoutManager = LinearLayoutManager( - context, - RecyclerView.HORIZONTAL, - false - ) - setRecycledViewPool(RecyclerView.RecycledViewPool()) - adapter = CategoriesAdapter() - ViewCompat.setNestedScrollingEnabled(this, false) - } - + modalLayoutPickerCategoriesSkeleton.root.isGone = true + categoriesRecyclerView.isGone = true layoutsRecyclerView.apply { layoutManager = LinearLayoutManager(requireActivity()) - adapter = LayoutCategoryAdapter(viewModel.nestedScrollStates) + adapter = LayoutCategoryAdapter( + viewModel.nestedScrollStates, + thumbDimensionProvider, + recommendedDimensionProvider, + showRowDividers = false, + useLargeCategoryHeading = true, + footerLayoutResId = R.layout.home_page_picker_footer + ) } setupUi() setupViewModel() setupActionListeners() - previewModeSelectorPopup = PreviewModeSelectorPopup( - requireActivity(), - homePagePickerTitlebar.previewTypeSelectorButton - ) } } + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + viewModel.writeToBundle(outState) + } + private fun HomePagePickerFragmentBinding.setupUi() { - homePagePickerTitlebar.title.isInvisible = !displayUtils.isPhoneLandscape() - modalLayoutPickerHeaderSection.modalLayoutPickerTitleRow?.header?.setText(R.string.hpp_title) - modalLayoutPickerHeaderSection.modalLayoutPickerSubtitleRow?.description?.setText(R.string.hpp_subtitle) - if (siteNameFeatureConfig.isEnabled()) { - homePagePickerBottomToolbar.chooseButton.setText(R.string.hpp_choose_and_create_site) + siteCreationThemeHeader.title?.setText(R.string.hpp_title) + siteCreationThemeHeader.subtitle?.isGone = true + modalLayoutPickerLayoutsSkeleton.layoutsSkeleton.updateLayoutParams { + height = recommendedDimensionProvider.rowHeight + } + modalLayoutPickerLayoutsSkeleton.skeletonCardView.updateLayoutParams { + height = recommendedDimensionProvider.previewHeight + width = recommendedDimensionProvider.previewWidth } } private fun HomePagePickerFragmentBinding.setupViewModel() { - viewModel.uiState.observe(viewLifecycleOwner, { uiState -> + viewModel.uiState.observe(viewLifecycleOwner) { uiState -> setHeaderVisibility(uiState.isHeaderVisible) - setDescriptionVisibility(uiState.isDescriptionVisible) setContentVisibility(uiState.loadingSkeletonVisible, uiState.errorViewVisible) - setToolbarVisibility(uiState.isToolbarVisible) when (uiState) { is LayoutPickerUiState.Loading -> { // Nothing more to do here } is LayoutPickerUiState.Content -> { - (categoriesRecyclerView.adapter as CategoriesAdapter).setData(uiState.categories) (layoutsRecyclerView.adapter as? LayoutCategoryAdapter)?.update(uiState.layoutCategories) } is LayoutPickerUiState.Error -> { uiState.toast?.let { ToastUtils.showToast(requireContext(), it) } } } - }) + } - viewModel.onPreviewActionPressed.observe(viewLifecycleOwner, { action -> + viewModel.onPreviewActionPressed.observe(viewLifecycleOwner) { action -> activity?.supportFragmentManager?.let { fm -> when (action) { is Show -> { @@ -127,58 +123,36 @@ class HomePagePickerFragment : Fragment() { } is Dismiss -> { (fm.findFragmentByTag(DESIGN_PREVIEW_TAG) as? DesignPreviewFragment)?.dismiss() + (viewModel.uiState.value as? LayoutPickerUiState.Content)?.let { + viewModel.updateUiState(it.copy(selectedLayoutSlug = null)) + viewModel.loadLayouts() + } } } } - }) - - viewModel.onThumbnailModeButtonPressed.observe(viewLifecycleOwner, { - previewModeSelectorPopup.show(viewModel) - }) - - viewModel.onCategorySelectionChanged.observeEvent(viewLifecycleOwner, { - layoutsRecyclerView.smoothScrollToPosition(0) - }) + } - viewModel.start(displayUtils.isTablet()) + viewModel.start(siteIntent, displayUtils.isTablet()) } private fun HomePagePickerFragmentBinding.setHeaderVisibility(visible: Boolean) { uiHelper.fadeInfadeOutViews( - homePagePickerTitlebar.title, - modalLayoutPickerHeaderSection.modalLayoutPickerTitleRow?.header, + homePagePickerTitlebar.appBarTitle, + siteCreationThemeHeader.title, visible ) } - /** - * Sets the header description visibility - * @param visible if true the description is visible else invisible - */ - private fun HomePagePickerFragmentBinding.setDescriptionVisibility(visible: Boolean) { - modalLayoutPickerHeaderSection.modalLayoutPickerSubtitleRow?.description?.visibility = - if (visible) View.VISIBLE else View.INVISIBLE - } - private fun HomePagePickerFragmentBinding.setContentVisibility(skeleton: Boolean, error: Boolean) { - modalLayoutPickerCategoriesSkeleton.categoriesSkeleton.setVisible(skeleton) - categoriesRecyclerView.setVisible(!skeleton && !error) modalLayoutPickerLayoutsSkeleton.layoutsSkeleton.setVisible(skeleton) layoutsRecyclerView.setVisible(!skeleton && !error) errorView.setVisible(error) } - private fun HomePagePickerFragmentBinding.setToolbarVisibility(visible: Boolean) { - AniUtils.animateBottomBar(homePagePickerBottomToolbar.bottomToolbar, visible) - } - private fun HomePagePickerFragmentBinding.setupActionListeners() { - homePagePickerBottomToolbar.previewButton.setOnClickListener { viewModel.onPreviewTapped() } - homePagePickerBottomToolbar.chooseButton.setOnClickListener { viewModel.onChooseTapped() } homePagePickerTitlebar.skipButton.setOnClickListener { viewModel.onSkippedTapped() } errorView.button.setOnClickListener { viewModel.onRetryClicked() } homePagePickerTitlebar.backButton.setOnClickListener { viewModel.onBackPressed() } - homePagePickerTitlebar.previewTypeSelectorButton.setOnClickListener { viewModel.onThumbnailModePressed() } setScrollListener() } @@ -190,4 +164,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 + } + } + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/theme/HomePagePickerViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/theme/HomePagePickerViewModel.kt index 074a87baa8de..dc83d75e7b68 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/theme/HomePagePickerViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/theme/HomePagePickerViewModel.kt @@ -1,6 +1,7 @@ package org.wordpress.android.ui.sitecreation.theme import androidx.lifecycle.LiveData +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext import org.wordpress.android.R @@ -14,8 +15,6 @@ 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.SingleLiveEvent @@ -26,13 +25,15 @@ const val defaultTemplateSlug = "default" private const val ERROR_CONTEXT = "design" +@HiltViewModel class HomePagePickerViewModel @Inject constructor( override val networkUtils: NetworkUtilsWrapper, private val dispatcher: Dispatcher, private val fetchHomePageLayoutsUseCase: FetchHomePageLayoutsUseCase, private val analyticsTracker: SiteCreationTracker, @Named(BG_THREAD) override val bgDispatcher: CoroutineDispatcher, - @Named(UI_THREAD) override val mainDispatcher: CoroutineDispatcher + @Named(UI_THREAD) override val mainDispatcher: CoroutineDispatcher, + private val recommendationProvider: SiteDesignRecommendationProvider ) : LayoutPickerViewModel(mainDispatcher, bgDispatcher, networkUtils, analyticsTracker) { private val _onDesignActionPressed = SingleLiveEvent() val onDesignActionPressed: LiveData = _onDesignActionPressed @@ -40,7 +41,11 @@ class HomePagePickerViewModel @Inject constructor( private val _onBackButtonPressed = SingleLiveEvent() val onBackButtonPressed: LiveData = _onBackButtonPressed + private var vertical: String = "" + override val useCachedData: Boolean = false + override val shouldUseMobileThumbnail = true + override val thumbnailTapOpensPreview = true sealed class DesignSelectionAction(val template: String) { object Skip : DesignSelectionAction(defaultTemplateSlug) @@ -56,9 +61,13 @@ class HomePagePickerViewModel @Inject constructor( dispatcher.unregister(fetchHomePageLayoutsUseCase) } - fun start(isTablet: Boolean = false) { + fun start(intent: String? = null, isTablet: Boolean = false) { + val verticalChanged = vertical != intent + if (verticalChanged) { + vertical = intent ?: "" + } initializePreviewMode(isTablet) - if (uiState.value !is Content) { + if (uiState.value !is Content || verticalChanged) { analyticsTracker.trackSiteDesignViewed(selectedPreviewMode().key) fetchLayouts() } @@ -79,21 +88,37 @@ class HomePagePickerViewModel @Inject constructor( analyticsTracker.trackErrorShown(ERROR_CONTEXT, UNKNOWN, "Error fetching designs") updateUiState(Error()) } else { - handleResponse(event.designs.toLayoutModels(), event.categories.toLayoutCategories()) + recommendationProvider.handleResponse( + vertical, + event.designs, + event.categories, + this@HomePagePickerViewModel::handleResponse + ) } } } } + override fun onLayoutTapped(layoutSlug: String, isRecommended: Boolean) { + (uiState.value as? Content)?.let { + if (it.loadedThumbnailSlugs.contains(layoutSlug)) { + updateUiState(it.copy(selectedLayoutSlug = layoutSlug, isSelectedLayoutRecommended = isRecommended)) + onPreviewTapped() + loadLayouts() + } + } + } + override fun onPreviewChooseTapped() { - super.onPreviewChooseTapped() onChooseTapped() } fun onChooseTapped() { selectedLayout?.let { layout -> + super.onPreviewChooseTapped() 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 } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/theme/SiteDesignPickerDimensionProvider.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/theme/SiteDesignPickerDimensionProvider.kt new file mode 100644 index 000000000000..4964a9993287 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/theme/SiteDesignPickerDimensionProvider.kt @@ -0,0 +1,18 @@ +package org.wordpress.android.ui.sitecreation.theme + +import org.wordpress.android.R.dimen +import org.wordpress.android.ui.layoutpicker.ThumbDimensionProvider +import org.wordpress.android.viewmodel.ContextProvider +import javax.inject.Inject + +class SiteDesignPickerDimensionProvider @Inject constructor(private val contextProvider: ContextProvider) : + ThumbDimensionProvider { + override val previewWidth: Int + get() = contextProvider.getContext().resources.getDimensionPixelSize(dimen.hpp_layout_card_width) + + override val previewHeight: Int + get() = contextProvider.getContext().resources.getDimensionPixelSize(dimen.hpp_layout_card_height) + + override val rowHeight: Int + get() = contextProvider.getContext().resources.getDimensionPixelSize(dimen.hpp_layouts_row_height) +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/theme/SiteDesignRecommendationProvider.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/theme/SiteDesignRecommendationProvider.kt new file mode 100644 index 000000000000..6d2b1ce7bd46 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/theme/SiteDesignRecommendationProvider.kt @@ -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, + categories: List, + responseHandler: (layouts: List, categories: List) -> 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)) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/theme/SiteDesignRecommendedDimensionProvider.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/theme/SiteDesignRecommendedDimensionProvider.kt new file mode 100644 index 000000000000..db5ddbcc73a5 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/theme/SiteDesignRecommendedDimensionProvider.kt @@ -0,0 +1,18 @@ +package org.wordpress.android.ui.sitecreation.theme + +import org.wordpress.android.R.dimen +import org.wordpress.android.ui.layoutpicker.ThumbDimensionProvider +import org.wordpress.android.viewmodel.ContextProvider +import javax.inject.Inject + +class SiteDesignRecommendedDimensionProvider @Inject constructor(private val contextProvider: ContextProvider) : + ThumbDimensionProvider { + override val previewWidth: Int + get() = contextProvider.getContext().resources.getDimensionPixelSize(dimen.hpp_recommended_card_width) + + override val previewHeight: Int + get() = contextProvider.getContext().resources.getDimensionPixelSize(dimen.hpp_recommended_card_height) + + override val rowHeight: Int + get() = contextProvider.getContext().resources.getDimensionPixelSize(dimen.hpp_recommended_row_height) +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/usecases/FetchHomePageLayoutsUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/usecases/FetchHomePageLayoutsUseCase.kt index 5bfe99fca143..332bebd53f03 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/usecases/FetchHomePageLayoutsUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/usecases/FetchHomePageLayoutsUseCase.kt @@ -7,7 +7,7 @@ import org.wordpress.android.fluxc.generated.ThemeActionBuilder import org.wordpress.android.fluxc.store.ThemeStore import org.wordpress.android.fluxc.store.ThemeStore.FetchStarterDesignsPayload import org.wordpress.android.fluxc.store.ThemeStore.OnStarterDesignsFetched -import org.wordpress.android.ui.layoutpicker.ThumbDimensionProvider +import org.wordpress.android.ui.sitecreation.theme.SiteDesignPickerDimensionProvider import org.wordpress.android.util.config.BetaSiteDesignsFeatureConfig import javax.inject.Inject import kotlin.coroutines.Continuation @@ -17,7 +17,7 @@ import kotlin.coroutines.suspendCoroutine class FetchHomePageLayoutsUseCase @Inject constructor( val dispatcher: Dispatcher, @Suppress("unused") val themeStore: ThemeStore, - private val thumbDimensionProvider: ThumbDimensionProvider, + private val thumbDimensionProvider: SiteDesignPickerDimensionProvider, private val betaSiteDesigns: BetaSiteDesignsFeatureConfig ) { enum class GROUP(val key: String) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/verticals/SiteCreationIntentsFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/verticals/SiteCreationIntentsFragment.kt index 63037bd67cd5..f14e8bfbfc2c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/verticals/SiteCreationIntentsFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/verticals/SiteCreationIntentsFragment.kt @@ -1,18 +1,16 @@ package org.wordpress.android.ui.sitecreation.verticals -import android.content.Context import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProvider +import androidx.fragment.app.activityViewModels import com.google.android.material.appbar.AppBarLayout +import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.R -import org.wordpress.android.WordPress import org.wordpress.android.databinding.SiteCreationIntentsFragmentBinding import org.wordpress.android.ui.sitecreation.verticals.SiteCreationIntentsViewModel.IntentsUiState import org.wordpress.android.ui.utils.UiHelpers @@ -23,19 +21,14 @@ import javax.inject.Inject * Implements the Site Intent Question UI */ @Suppress("TooManyFunctions") +@AndroidEntryPoint class SiteCreationIntentsFragment : Fragment() { - @Inject internal lateinit var viewModelFactory: ViewModelProvider.Factory @Inject internal lateinit var uiHelper: UiHelpers @Inject internal lateinit var displayUtils: DisplayUtilsWrapper - private lateinit var viewModel: SiteCreationIntentsViewModel + private val viewModel: SiteCreationIntentsViewModel by activityViewModels() private var binding: SiteCreationIntentsFragmentBinding? = null - override fun onAttach(context: Context) { - super.onAttach(context) - (requireActivity().applicationContext as WordPress).component().inject(this) - } - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -47,9 +40,6 @@ class SiteCreationIntentsFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - viewModel = ViewModelProvider(requireActivity(), viewModelFactory) - .get(SiteCreationIntentsViewModel::class.java) - val binding = SiteCreationIntentsFragmentBinding.bind(view) this.binding = binding with(binding) { @@ -68,7 +58,6 @@ class SiteCreationIntentsFragment : Fragment() { } private fun SiteCreationIntentsFragmentBinding.setupUi() { - siteCreationIntentsTitlebar.appBarTitle.isInvisible = !displayUtils.isPhoneLandscape() recyclerView.itemAnimator = null recyclerView.adapter = SiteCreationIntentsAdapter() siteCreationIntentsHeader.title?.setText(R.string.new_site_creation_intents_header_title) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/verticals/SiteCreationIntentsViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/verticals/SiteCreationIntentsViewModel.kt index 712d496552a1..e80bf7dea85b 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/verticals/SiteCreationIntentsViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/verticals/SiteCreationIntentsViewModel.kt @@ -4,6 +4,7 @@ import android.content.res.Resources import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -17,6 +18,7 @@ import javax.inject.Inject import javax.inject.Named import kotlin.coroutines.CoroutineContext +@HiltViewModel class SiteCreationIntentsViewModel @Inject constructor( private val analyticsTracker: SiteCreationTracker, private val searchResultsProvider: VerticalsSearchResultsProvider, diff --git a/WordPress/src/main/java/org/wordpress/android/util/image/ImageManager.kt b/WordPress/src/main/java/org/wordpress/android/util/image/ImageManager.kt index 33f2c2df96e3..e26966fdcae6 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/image/ImageManager.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/image/ImageManager.kt @@ -296,6 +296,21 @@ class ImageManager @Inject constructor( .clearOnDetach() } + /** + * Preloads an [MShot]. + * + * This is needed because the mshot service redirects to a loading gif image when the thumbnail is not ready. + * The loading is handled by [org.wordpress.android.networking.GlideMShotsLoader] + */ + fun preload(context: Context, design: MShot) { + if (!context.isAvailable()) return + GlideApp.with(context) + .downloadOnly() + .load(design) + .submit() + .get() // This makes each call blocking, so subsequent calls can be cancelled if needed. + } + /** * Loads an [MShot] into an [ImageView] and attaches a [RequestListener]. * diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/mlp/ModalLayoutPickerViewModel.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/mlp/ModalLayoutPickerViewModel.kt index 6b0a09c48a0e..2327a2d97793 100644 --- a/WordPress/src/main/java/org/wordpress/android/viewmodel/mlp/ModalLayoutPickerViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/mlp/ModalLayoutPickerViewModel.kt @@ -20,7 +20,7 @@ import org.wordpress.android.ui.layoutpicker.LayoutPickerUiState.Content import org.wordpress.android.ui.layoutpicker.LayoutPickerUiState.Error import org.wordpress.android.ui.layoutpicker.LayoutPickerUiState.Loading import org.wordpress.android.ui.layoutpicker.LayoutPickerViewModel -import org.wordpress.android.ui.layoutpicker.ThumbDimensionProvider +import org.wordpress.android.ui.mlp.ModalLayoutPickerDimensionProvider import org.wordpress.android.ui.layoutpicker.toLayoutCategories import org.wordpress.android.ui.layoutpicker.toLayoutModels import org.wordpress.android.ui.mlp.ModalLayoutPickerTracker @@ -42,7 +42,7 @@ class ModalLayoutPickerViewModel @Inject constructor( private val siteStore: SiteStore, private val selectedSiteRepository: SelectedSiteRepository, private val supportedBlocksProvider: SupportedBlocksProvider, - private val thumbDimensionProvider: ThumbDimensionProvider, + private val thumbDimensionProvider: ModalLayoutPickerDimensionProvider, private val displayUtilsWrapper: DisplayUtilsWrapper, override val networkUtils: NetworkUtilsWrapper, private val analyticsTracker: ModalLayoutPickerTracker, @@ -66,6 +66,7 @@ class ModalLayoutPickerViewModel @Inject constructor( } override val useCachedData: Boolean = true + override val shouldUseMobileThumbnail = false override val selectedLayout: LayoutModel? get() = (uiState.value as? Content)?.let { state -> diff --git a/WordPress/src/main/res/drawable/ic_info_outline_grey_dark_24dp.xml b/WordPress/src/main/res/drawable/ic_info_outline_grey_dark_24dp.xml new file mode 100644 index 000000000000..42d91533cf21 --- /dev/null +++ b/WordPress/src/main/res/drawable/ic_info_outline_grey_dark_24dp.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/WordPress/src/main/res/drawable/top_divider_background.xml b/WordPress/src/main/res/drawable/top_divider_background.xml new file mode 100644 index 000000000000..83dccbf33ba7 --- /dev/null +++ b/WordPress/src/main/res/drawable/top_divider_background.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/WordPress/src/main/res/layout-sw600dp/home_page_picker_bottom_toolbar.xml b/WordPress/src/main/res/layout-sw600dp/home_page_picker_bottom_toolbar.xml deleted file mode 100644 index b9ea6d9eb1b5..000000000000 --- a/WordPress/src/main/res/layout-sw600dp/home_page_picker_bottom_toolbar.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - diff --git a/WordPress/src/main/res/layout/home_page_picker_bottom_toolbar.xml b/WordPress/src/main/res/layout/home_page_picker_bottom_toolbar.xml deleted file mode 100644 index b937dd3838f2..000000000000 --- a/WordPress/src/main/res/layout/home_page_picker_bottom_toolbar.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - diff --git a/WordPress/src/main/res/layout/home_page_picker_footer.xml b/WordPress/src/main/res/layout/home_page_picker_footer.xml new file mode 100644 index 000000000000..77b0de766ef0 --- /dev/null +++ b/WordPress/src/main/res/layout/home_page_picker_footer.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/WordPress/src/main/res/layout/home_page_picker_fragment.xml b/WordPress/src/main/res/layout/home_page_picker_fragment.xml index afb5d4960868..58390be957d7 100644 --- a/WordPress/src/main/res/layout/home_page_picker_fragment.xml +++ b/WordPress/src/main/res/layout/home_page_picker_fragment.xml @@ -22,8 +22,8 @@ app:layout_scrollFlags="scroll|exitUntilCollapsed"> - - diff --git a/WordPress/src/main/res/layout/home_page_picker_titlebar.xml b/WordPress/src/main/res/layout/home_page_picker_titlebar.xml index c5584a073539..5ba7b522689c 100644 --- a/WordPress/src/main/res/layout/home_page_picker_titlebar.xml +++ b/WordPress/src/main/res/layout/home_page_picker_titlebar.xml @@ -14,24 +14,15 @@ android:tint="?attr/colorOnSurface" /> - -