Commit 655d2b8a authored by ekager's avatar ekager Committed by Emily Kager
Browse files

For #12377 - Choice to hide no collections placeholder from homescreen

parent a1a211d6
......@@ -146,8 +146,11 @@ class HomeFragment : Fragment() {
private val store: BrowserStore
get() = requireComponents.core.store
private val onboarding by lazy { StrictMode.allowThreadDiskReads().resetPoliciesAfter {
FenixOnboarding(requireContext()) } }
private val onboarding by lazy {
StrictMode.allowThreadDiskReads().resetPoliciesAfter {
FenixOnboarding(requireContext())
}
}
private lateinit var homeFragmentStore: HomeFragmentStore
private var _sessionControlInteractor: SessionControlInteractor? = null
......@@ -193,7 +196,8 @@ class HomeFragment : Fragment() {
topSites = StrictMode.allowThreadDiskReads().resetPoliciesAfter {
components.core.topSiteStorage.cachedTopSites
},
tip = FenixTipManager(listOf(MigrationTipProvider(requireContext()))).getTip()
tip = FenixTipManager(listOf(MigrationTipProvider(requireContext()))).getTip(),
showCollectionPlaceholder = components.settings.showCollectionsPlaceholderOnHome
)
)
}
......@@ -201,6 +205,7 @@ class HomeFragment : Fragment() {
_sessionControlInteractor = SessionControlInteractor(
DefaultSessionControlController(
activity = activity,
settings = components.settings,
engine = components.core.engine,
metrics = components.analytics.metrics,
sessionManager = sessionManager,
......@@ -220,9 +225,9 @@ class HomeFragment : Fragment() {
updateLayout(view)
sessionControlView = SessionControlView(
view.sessionControlRecyclerView,
viewLifecycleOwner,
sessionControlInteractor,
homeViewModel,
requireComponents.core.store.state.normalTabs.isNotEmpty()
homeViewModel
)
updateSessionControlView(view)
......@@ -516,7 +521,8 @@ class HomeFragment : Fragment() {
collections = components.core.tabCollectionStorage.cachedTabCollections,
mode = currentMode.getCurrentMode(),
topSites = components.core.topSiteStorage.cachedTopSites,
tip = FenixTipManager(listOf(MigrationTipProvider(requireContext()))).getTip()
tip = FenixTipManager(listOf(MigrationTipProvider(requireContext()))).getTip(),
showCollectionPlaceholder = components.settings.showCollectionsPlaceholderOnHome
)
)
......
......@@ -41,13 +41,16 @@ data class Tab(
* @property mode The state of the [HomeFragment] UI.
* @property tabs The list of opened [Tab] in the [HomeFragment].
* @property topSites The list of [TopSite] in the [HomeFragment].
* @property tip The current [Tip] to show on the [HomeFragment].
* @property showCollectionPlaceholder If true, shows a placeholder when there are no collections.
*/
data class HomeFragmentState(
val collections: List<TabCollection>,
val expandedCollections: Set<Long>,
val mode: Mode,
val topSites: List<TopSite>,
val tip: Tip? = null
val tip: Tip? = null,
val showCollectionPlaceholder: Boolean
) : State
sealed class HomeFragmentAction : Action {
......@@ -55,7 +58,8 @@ sealed class HomeFragmentAction : Action {
val topSites: List<TopSite>,
val mode: Mode,
val collections: List<TabCollection>,
val tip: Tip? = null
val tip: Tip? = null,
val showCollectionPlaceholder: Boolean
) :
HomeFragmentAction()
......@@ -66,6 +70,7 @@ sealed class HomeFragmentAction : Action {
data class ModeChange(val mode: Mode) : HomeFragmentAction()
data class TopSitesChange(val topSites: List<TopSite>) : HomeFragmentAction()
data class RemoveTip(val tip: Tip) : HomeFragmentAction()
object RemoveCollectionsPlaceholder : HomeFragmentAction()
}
private fun homeFragmentStateReducer(
......@@ -93,6 +98,11 @@ private fun homeFragmentStateReducer(
is HomeFragmentAction.CollectionsChange -> state.copy(collections = action.collections)
is HomeFragmentAction.ModeChange -> state.copy(mode = action.mode)
is HomeFragmentAction.TopSitesChange -> state.copy(topSites = action.topSites)
is HomeFragmentAction.RemoveTip -> { state.copy(tip = null) }
is HomeFragmentAction.RemoveTip -> {
state.copy(tip = null)
}
is HomeFragmentAction.RemoveCollectionsPlaceholder -> {
state.copy(showCollectionPlaceholder = false)
}
}
}
......@@ -8,11 +8,13 @@ import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.top.sites.TopSite
import org.mozilla.fenix.components.Components
import org.mozilla.fenix.components.tips.Tip
import org.mozilla.fenix.home.OnboardingState
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionHeaderViewHolder
......@@ -135,7 +137,8 @@ class AdapterItemDiffCallback : DiffUtil.ItemCallback<AdapterItem>() {
class SessionControlAdapter(
private val interactor: SessionControlInteractor,
private val hasNormalTabsOpened: Boolean
private val viewLifecycleOwner: LifecycleOwner,
private val components: Components
) : ListAdapter<AdapterItem, RecyclerView.ViewHolder>(AdapterItemDiffCallback()) {
// This method triggers the ComplexMethod lint error when in fact it's quite simple.
......@@ -150,7 +153,12 @@ class SessionControlAdapter(
interactor
)
NoCollectionsMessageViewHolder.LAYOUT_ID ->
NoCollectionsMessageViewHolder(view, interactor, hasNormalTabsOpened)
NoCollectionsMessageViewHolder(
view,
viewLifecycleOwner,
components.core.store,
interactor
)
CollectionHeaderViewHolder.LAYOUT_ID -> CollectionHeaderViewHolder(view)
CollectionViewHolder.LAYOUT_ID -> CollectionViewHolder(view, interactor)
TabInCollectionViewHolder.LAYOUT_ID -> TabInCollectionViewHolder(
......
......@@ -37,6 +37,7 @@ import org.mozilla.fenix.home.HomeFragmentAction
import org.mozilla.fenix.home.HomeFragmentDirections
import org.mozilla.fenix.home.HomeFragmentStore
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.utils.Settings
import mozilla.components.feature.tab.collections.Tab as ComponentTab
/**
......@@ -144,11 +145,17 @@ interface SessionControlController {
* @see [CollectionInteractor.onAddTabsToCollectionTapped]
*/
fun handleCreateCollection()
/**
* @see [CollectionInteractor.onRemoveCollectionsPlaceholder]
*/
fun handleRemoveCollectionsPlaceholder()
}
@Suppress("TooManyFunctions", "LargeClass")
class DefaultSessionControlController(
private val activity: HomeActivity,
private val settings: Settings,
private val engine: Engine,
private val metrics: MetricController,
private val sessionManager: SessionManager,
......@@ -213,7 +220,11 @@ class DefaultSessionControlController(
metrics.track(Event.CollectionAllTabsRestored)
}
override fun handleCollectionRemoveTab(collection: TabCollection, tab: ComponentTab, wasSwiped: Boolean) {
override fun handleCollectionRemoveTab(
collection: TabCollection,
tab: ComponentTab,
wasSwiped: Boolean
) {
metrics.track(Event.CollectionTabRemoved)
if (collection.tabs.size == 1) {
......@@ -223,7 +234,13 @@ class DefaultSessionControlController(
)
val message =
activity.resources.getString(R.string.delete_tab_and_collection_dialog_message)
showDeleteCollectionPrompt(collection, title, message, wasSwiped, handleSwipedItemDeletionCancel)
showDeleteCollectionPrompt(
collection,
title,
message,
wasSwiped,
handleSwipedItemDeletionCancel
)
} else {
viewLifecycleScope.launch(Dispatchers.IO) {
tabCollectionStorage.removeTabFromCollection(collection, tab)
......@@ -369,6 +386,11 @@ class DefaultSessionControlController(
showTabTrayCollectionCreation()
}
override fun handleRemoveCollectionsPlaceholder() {
settings.showCollectionsPlaceholderOnHome = false
fragmentStore.dispatch(HomeFragmentAction.RemoveCollectionsPlaceholder)
}
private fun showShareFragment(shareSubject: String, data: List<ShareData>) {
val directions = HomeFragmentDirections.actionGlobalShareFragment(
shareSubject = shareSubject,
......
......@@ -93,6 +93,11 @@ interface CollectionInteractor {
* Opens the collection creator
*/
fun onAddTabsToCollectionTapped()
/**
* User has removed the collections placeholder from home.
*/
fun onRemoveCollectionsPlaceholder()
}
interface ToolbarInteractor {
......@@ -256,4 +261,8 @@ class SessionControlInteractor(
override fun onPaste(clipboardText: String) {
controller.handlePaste(clipboardText)
}
override fun onRemoveCollectionsPlaceholder() {
controller.handleRemoveCollectionsPlaceholder()
}
}
......@@ -5,6 +5,7 @@
package org.mozilla.fenix.home.sessioncontrol
import android.view.View
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
......@@ -13,6 +14,7 @@ import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.top.sites.TopSite
import org.mozilla.fenix.R
import org.mozilla.fenix.components.tips.Tip
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.home.HomeFragmentState
import org.mozilla.fenix.home.HomeScreenViewModel
import org.mozilla.fenix.home.Mode
......@@ -25,7 +27,8 @@ private fun normalModeAdapterItems(
topSites: List<TopSite>,
collections: List<TabCollection>,
expandedCollections: Set<Long>,
tip: Tip?
tip: Tip?,
showCollectionsPlaceholder: Boolean
): List<AdapterItem> {
val items = mutableListOf<AdapterItem>()
......@@ -36,8 +39,9 @@ private fun normalModeAdapterItems(
}
if (collections.isEmpty()) {
items.add(AdapterItem.CollectionHeader)
items.add(AdapterItem.NoCollectionsMessage)
if (showCollectionsPlaceholder) {
items.add(AdapterItem.NoCollectionsMessage)
}
} else {
showCollections(collections, expandedCollections, items)
}
......@@ -68,62 +72,77 @@ private fun onboardingAdapterItems(onboardingState: OnboardingState): List<Adapt
val items: MutableList<AdapterItem> = mutableListOf(AdapterItem.OnboardingHeader)
// Customize FxA items based on where we are with the account state:
items.addAll(when (onboardingState) {
OnboardingState.SignedOutNoAutoSignIn -> {
listOf(
AdapterItem.OnboardingManualSignIn
)
}
is OnboardingState.SignedOutCanAutoSignIn -> {
listOf(
AdapterItem.OnboardingAutomaticSignIn(onboardingState)
)
items.addAll(
when (onboardingState) {
OnboardingState.SignedOutNoAutoSignIn -> {
listOf(
AdapterItem.OnboardingManualSignIn
)
}
is OnboardingState.SignedOutCanAutoSignIn -> {
listOf(
AdapterItem.OnboardingAutomaticSignIn(onboardingState)
)
}
OnboardingState.SignedIn -> listOf()
}
OnboardingState.SignedIn -> listOf()
})
items.addAll(listOf(
AdapterItem.OnboardingSectionHeader {
val appName = it.getString(R.string.app_name)
it.getString(R.string.onboarding_feature_section_header, appName)
},
AdapterItem.OnboardingWhatsNew,
AdapterItem.OnboardingTrackingProtection,
AdapterItem.OnboardingThemePicker,
AdapterItem.OnboardingPrivateBrowsing,
AdapterItem.OnboardingToolbarPositionPicker,
AdapterItem.OnboardingPrivacyNotice,
AdapterItem.OnboardingFinish
))
)
items.addAll(
listOf(
AdapterItem.OnboardingSectionHeader {
val appName = it.getString(R.string.app_name)
it.getString(R.string.onboarding_feature_section_header, appName)
},
AdapterItem.OnboardingWhatsNew,
AdapterItem.OnboardingTrackingProtection,
AdapterItem.OnboardingThemePicker,
AdapterItem.OnboardingPrivateBrowsing,
AdapterItem.OnboardingToolbarPositionPicker,
AdapterItem.OnboardingPrivacyNotice,
AdapterItem.OnboardingFinish
)
)
return items
}
private fun HomeFragmentState.toAdapterList(): List<AdapterItem> = when (mode) {
is Mode.Normal -> normalModeAdapterItems(topSites, collections, expandedCollections, tip)
is Mode.Normal -> normalModeAdapterItems(
topSites,
collections,
expandedCollections,
tip,
showCollectionPlaceholder
)
is Mode.Private -> privateModeAdapterItems()
is Mode.Onboarding -> onboardingAdapterItems(mode.state)
}
private fun collectionTabItems(collection: TabCollection) = collection.tabs.mapIndexed { index, tab ->
private fun collectionTabItems(collection: TabCollection) =
collection.tabs.mapIndexed { index, tab ->
AdapterItem.TabInCollectionItem(collection, tab, index == collection.tabs.lastIndex)
}
}
class SessionControlView(
override val containerView: View?,
override val containerView: View,
viewLifecycleOwner: LifecycleOwner,
interactor: SessionControlInteractor,
private var homeScreenViewModel: HomeScreenViewModel,
private val hasNormalTabsOpened: Boolean
private var homeScreenViewModel: HomeScreenViewModel
) : LayoutContainer {
val view: RecyclerView = containerView as RecyclerView
private val sessionControlAdapter = SessionControlAdapter(interactor, hasNormalTabsOpened)
private val sessionControlAdapter = SessionControlAdapter(
interactor,
viewLifecycleOwner,
containerView.context.components
)
init {
view.apply {
adapter = sessionControlAdapter
layoutManager = LinearLayoutManager(containerView!!.context)
layoutManager = LinearLayoutManager(containerView.context)
val itemTouchHelper =
ItemTouchHelper(
SwipeToDeleteCallback(
......
......@@ -6,22 +6,50 @@ package org.mozilla.fenix.home.sessioncontrol.viewholders
import android.view.View
import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner
import kotlinx.android.synthetic.main.no_collections_message.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.R
import org.mozilla.fenix.utils.view.ViewHolder
import org.mozilla.fenix.ext.increaseTapArea
import org.mozilla.fenix.home.sessioncontrol.CollectionInteractor
import org.mozilla.fenix.utils.view.ViewHolder
@OptIn(ExperimentalCoroutinesApi::class)
open class NoCollectionsMessageViewHolder(
view: View,
interactor: CollectionInteractor,
hasNormalTabsOpened: Boolean
viewLifecycleOwner: LifecycleOwner,
store: BrowserStore,
interactor: CollectionInteractor
) : ViewHolder(view) {
init {
add_tabs_to_collections_button.setOnClickListener {
interactor.onAddTabsToCollectionTapped()
}
add_tabs_to_collections_button.isVisible = hasNormalTabsOpened
remove_collection_placeholder.increaseTapArea(
view.resources.getDimensionPixelSize(R.dimen.tap_increase_16)
)
remove_collection_placeholder.setOnClickListener {
interactor.onRemoveCollectionsPlaceholder()
}
add_tabs_to_collections_button.isVisible = store.state.normalTabs.isNotEmpty()
store.flowScoped(viewLifecycleOwner) { flow ->
flow.map { state -> state.normalTabs.size }
.ifChanged()
.collect { tabs ->
add_tabs_to_collections_button.isVisible = tabs > 0
}
}
}
companion object {
......
......@@ -249,6 +249,11 @@ class Settings(private val appContext: Context) : PreferencesHolder {
default = false
)
var showCollectionsPlaceholderOnHome by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_show_collections_placeholder_home),
default = true
)
val isCrashReportingEnabled: Boolean
get() = isCrashReportEnabledInBuild &&
preferences.getBoolean(
......
......@@ -2,42 +2,63 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/no_collections_wrapper"
android:background="@drawable/empty_session_control_background"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="12dp"
android:padding="16dp"
android:background="@drawable/empty_session_control_background"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:padding="16dp">
<TextView
android:id="@+id/no_collections_header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/no_collections_header1"
tools:drawableEnd="@drawable/ic_tab_collection"
android:text="@string/collections_header"
android:textAppearance="@style/HeaderTextStyle"
android:textSize="16sp"
app:fontFamily="@font/metropolis_semibold" />
app:fontFamily="@font/metropolis_semibold"
app:layout_constraintEnd_toStartOf="@id/remove_collection_placeholder"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/remove_collection_placeholder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/remove_home_collection_placeholder_content_description"
app:layout_constraintBottom_toBottomOf="@id/no_collections_header"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/no_collections_header"
app:srcCompat="@drawable/ic_close" />
<TextView
android:id="@+id/no_collections_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/no_collections_description1"
android:text="@string/no_collections_description2"
android:textAlignment="viewStart"
android:textSize="14sp"
android:textAlignment="viewStart" />
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/no_collections_header" />
<com.google.android.material.button.MaterialButton
android:id="@+id/add_tabs_to_collections_button"
style="@style/PositiveButton"
app:icon="@drawable/ic_tab_collection"
android:visibility="gone"
android:layout_marginTop="8dp"
android:text="@string/tabs_menu_save_to_collection1"
android:layout_marginTop="8dp"/>
</LinearLayout>
android:visibility="gone"
app:icon="@drawable/ic_tab_collection"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/no_collections_description" />
</androidx.constraintlayout.widget.ConstraintLayout>
......@@ -172,4 +172,5 @@
<!-- a11y -->
<dimen name="accessibility_min_height">48dp</dimen>
<dimen name="tap_increase_16">16dp</dimen>
</resources>
......@@ -195,4 +195,6 @@
<string name="pref_key_default_browser" translatable="false">pref_key_default_browser</string>
<string name="pref_key_login_exceptions" translatable="false">pref_key_login_exceptions</string>
<string name="pref_key_show_collections_placeholder_home" translatable="false">pref_key_show_collections_home</string>
</resources>
......@@ -725,10 +725,8 @@
<string name="collections_header">Collections</string>
<!-- Content description (not visible, for screen readers etc.): Opens the collection menu when pressed -->
<string name="collection_menu_button_content_description">Collection menu</string>
<!-- No Open Tabs Message Header -->
<string name="no_collections_header1">Collect the things that matter to you</string>
<!-- Label to describe what collections are to a new user without any collections -->
<string name="no_collections_description1">Group together similar searches, sites, and tabs for quick access later.</string>
<string name="no_collections_description2">Collect the things that matter to you.\nGroup together similar searches, sites, and tabs for quick access later.</string>
<!-- Title for the "select tabs" step of the collection creator -->
<string name="create_collection_select_tabs">Select Tabs</string>
<!-- Title for the "select collection" step of the collection creator -->
......@@ -1467,4 +1465,6 @@
<!-- Confirmation dialog button text when top sites limit is reached. -->
<string name="top_sites_max_limit_confirmation_button">OK, Got It</string>
<!-- Content description for close button in collection placeholder. -->
<string name="remove_home_collection_placeholder_content_description">Remove</string>
</resources>
......@@ -78,8 +78,10 @@ class DefaultSessionControlControllerTest {
collections = emptyList(),
expandedCollections = emptySet(),
mode = Mode.Normal,
topSites = emptyList()
topSites = emptyList(),
showCollectionPlaceholder = true
)
every { sessionManager.sessions } returns emptyList()
every { navController.currentDestination } returns mockk {
every { id } returns R.id.homeFragment
......@@ -94,6 +96,7 @@ class DefaultSessionControlControllerTest {
controller = DefaultSessionControlController(
activity = activity,
settings = settings,
engine = engine,
metrics = metrics,
sessionManager = sessionManager,
......@@ -414,4 +417,14 @@ class DefaultSessionControlControllerTest {
)
}
}
@Test
fun handleRemoveCollectionsPlaceholder() {
controller.handleRemoveCollectionsPlaceholder()
verify {
settings.showCollectionsPlaceholderOnHome = false
fragmentStore.dispatch(HomeFragmentAction.RemoveCollectionsPlaceholder)