Close #18931: Implement add to collections in interactor

We moved the collection dialog code out from the old fragment, because it
had nothing to do with tabs tray, and into the collections package to be
re-usable in other parts of the app.

In addition, we also make use of it in the new tabs tray's
NavigationInteractor.
upstream-sync
Jonathan Almeida 3 years ago committed by Jonathan Almeida
parent 22e7410e4a
commit d342aeae48

@ -0,0 +1,135 @@
/* 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/. */
package org.mozilla.fenix.collections
import android.content.Context
import android.view.LayoutInflater
import android.widget.EditText
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.support.ktx.android.view.showKeyboard
import org.mozilla.fenix.R
import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.ext.getDefaultCollectionNumber
/**
* A lambda that is invoked when a confirmation button in a [CollectionsDialog] is clicked.
*
* A [TabCollection] of the selected collected is passed to the delegate when confirmed. If null,
* then a new collection is created.
*
* A list of [TabSessionState] is returned that will be put into the collections storage.
*/
typealias OnPositiveButtonClick = (collection: TabCollection?) -> List<TabSessionState>
/**
* A lambda that is invoked when a cancel button in a [CollectionsDialog] is clicked.
*/
typealias OnNegativeButtonClick = () -> Unit
/**
* A data class for creating a dialog to prompt adding/creating a collection. See also [show].
*
* @property onPositiveButtonClick Invoked when a user clicks on a confirmation button in the dialog.
* @property onNegativeButtonClick Invoked when a user clicks on a cancel button in the dialog.
*/
data class CollectionsDialog(
val storage: TabCollectionStorage,
val onPositiveButtonClick: OnPositiveButtonClick,
val onNegativeButtonClick: OnNegativeButtonClick
)
/**
* Create and display a [CollectionsDialog] using [AlertDialog].
*/
fun CollectionsDialog.show(
context: Context
) {
if (storage.cachedTabCollections.isEmpty()) {
showAddNewDialog(context, storage)
return
}
val collections = storage.cachedTabCollections.map { it.title }
val layout = LayoutInflater.from(context).inflate(R.layout.add_new_collection_dialog, null)
val list = layout.findViewById<RecyclerView>(R.id.recycler_view)
val builder = AlertDialog.Builder(context).setTitle(R.string.tab_tray_select_collection)
.setView(layout)
.setPositiveButton(android.R.string.ok) { dialog, _ ->
val selectedCollection =
(list.adapter as CollectionsListAdapter).getSelectedCollection()
val collection = storage.cachedTabCollections[selectedCollection]
val sessionList = onPositiveButtonClick.invoke(collection)
MainScope().launch {
storage.addTabsToCollection(collection, sessionList)
}
dialog.dismiss()
}.setNegativeButton(android.R.string.cancel) { dialog, _ ->
onNegativeButtonClick.invoke()
dialog.cancel()
}
val dialog = builder.create()
val collectionNames =
arrayOf(context.getString(R.string.tab_tray_add_new_collection)) + collections
val collectionsListAdapter = CollectionsListAdapter(collectionNames) {
dialog.dismiss()
showAddNewDialog(context, storage)
}
list.apply {
layoutManager = LinearLayoutManager(context)
adapter = collectionsListAdapter
}
dialog.show()
}
internal fun CollectionsDialog.showAddNewDialog(
context: Context,
collectionsStorage: TabCollectionStorage
) {
val layout = LayoutInflater.from(context).inflate(R.layout.name_collection_dialog, null)
val collectionNameEditText: EditText = layout.findViewById(R.id.collection_name)
collectionNameEditText.setText(
context.getString(
R.string.create_collection_default_name,
collectionsStorage.cachedTabCollections.getDefaultCollectionNumber()
)
)
AlertDialog.Builder(context)
.setTitle(R.string.tab_tray_add_new_collection)
.setView(layout).setPositiveButton(android.R.string.ok) { dialog, _ ->
val sessionList = onPositiveButtonClick.invoke(null)
MainScope().launch {
storage.createCollection(
collectionNameEditText.text.toString(),
sessionList
)
}
dialog.dismiss()
}
.setNegativeButton(android.R.string.cancel) { dialog, _ ->
onNegativeButtonClick.invoke()
dialog.cancel()
}
.create()
.show()
collectionNameEditText.setSelection(0, collectionNameEditText.text.length)
collectionNameEditText.showKeyboard()
}

@ -4,19 +4,25 @@
package org.mozilla.fenix.tabstray
import android.content.Context
import androidx.navigation.NavController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.concept.tabstray.Tab
import org.mozilla.fenix.collections.CollectionsDialog
import org.mozilla.fenix.collections.show
import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.bookmarks.BookmarksUseCase
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.home.HomeFragment
import org.mozilla.fenix.tabstray.ext.getTabSessionState
/**
* An interactor that helps with navigating to different parts of the app from the tabs tray.
@ -69,13 +75,15 @@ interface NavigationInteractor {
*/
@Suppress("LongParameterList")
class DefaultNavigationInteractor(
private val tabsTrayStore: TabsTrayStore,
private val context: Context,
private val browserStore: BrowserStore,
private val navController: NavController,
private val metrics: MetricController,
private val dismissTabTray: () -> Unit,
private val dismissTabTrayAndNavigateHome: (String) -> Unit,
private val bookmarksUseCase: BookmarksUseCase
private val bookmarksUseCase: BookmarksUseCase,
private val tabsTrayStore: TabsTrayStore,
private val collectionStorage: TabCollectionStorage
) : NavigationInteractor {
override fun onTabTrayDismissed() {
@ -126,7 +134,25 @@ class DefaultNavigationInteractor(
override fun onSaveToCollections(tabs: Collection<Tab>) {
metrics.track(Event.TabsTraySaveToCollectionPressed)
// TODO add this is a separate PR; it's quite a large change.
CollectionsDialog(
storage = collectionStorage,
onPositiveButtonClick = { existingCollection ->
tabsTrayStore.dispatch(TabsTrayAction.ExitSelectMode)
// If collection is null, a new one was created.
val event = if (existingCollection == null) {
Event.CollectionSaved(browserStore.state.normalTabs.size, tabs.size)
} else {
Event.CollectionTabsAdded(browserStore.state.normalTabs.size, tabs.size)
}
metrics.track(event)
browserStore.getTabSessionState(tabs)
},
onNegativeButtonClick = {
tabsTrayStore.dispatch(TabsTrayAction.ExitSelectMode)
}
).show(context)
}
override fun onSaveToBookmarks(tabs: Collection<Tab>) {

@ -93,13 +93,15 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
val navigationInteractor =
DefaultNavigationInteractor(
context = requireContext(),
tabsTrayStore = tabsTrayStore,
browserStore = requireComponents.core.store,
navController = findNavController(),
metrics = requireComponents.analytics.metrics,
dismissTabTray = ::dismissAllowingStateLoss,
dismissTabTrayAndNavigateHome = ::dismissTabTrayAndNavigateHome,
bookmarksUseCase = requireComponents.useCases.bookmarksUseCases
bookmarksUseCase = requireComponents.useCases.bookmarksUseCases,
collectionStorage = requireComponents.core.tabCollectionStorage
)
tabsTrayController = DefaultTabsTrayController(

@ -0,0 +1,19 @@
/* 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/. */
package org.mozilla.fenix.tabstray.ext
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.tabstray.Tab
/**
* Find and extract a list [TabSessionState] from the [BrowserStore] using the IDs from [tabs].
*/
fun BrowserStore.getTabSessionState(tabs: Collection<Tab>): List<TabSessionState> {
return tabs.mapNotNull {
state.findTab(it.id)
}
}

@ -4,11 +4,15 @@
package org.mozilla.fenix.tabstray
import android.content.Context
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.createTab as createStateTab
@ -17,6 +21,9 @@ import mozilla.components.concept.tabstray.Tab
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.collections.CollectionsDialog
import org.mozilla.fenix.collections.show
import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.bookmarks.BookmarksUseCase
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
@ -32,19 +39,23 @@ class NavigationInteractorTest {
private val dismissTabTray: () -> Unit = mockk(relaxed = true)
private val dismissTabTrayAndNavigateHome: (String) -> Unit = mockk(relaxed = true)
private val bookmarksUseCase: BookmarksUseCase = mockk(relaxed = true)
private val context: Context = mockk(relaxed = true)
private val collectionStorage: TabCollectionStorage = mockk(relaxed = true)
@Before
fun setup() {
store = BrowserStore(initialState = BrowserState(tabs = listOf(testTab)))
tabsTrayStore = TabsTrayStore()
navigationInteractor = DefaultNavigationInteractor(
tabsTrayStore,
context,
store,
navController,
metrics,
dismissTabTray,
dismissTabTrayAndNavigateHome,
bookmarksUseCase
bookmarksUseCase,
tabsTrayStore,
collectionStorage
)
}
@ -151,8 +162,13 @@ class NavigationInteractorTest {
@Test
fun `onSaveToCollections calls navigation on DefaultNavigationInteractor`() {
mockkStatic("org.mozilla.fenix.collections.CollectionsDialogKt")
every { any<CollectionsDialog>().show(any()) } answers { }
navigationInteractor.onSaveToCollections(emptyList())
verify(exactly = 1) { metrics.track(Event.TabsTraySaveToCollectionPressed) }
unmockkStatic("org.mozilla.fenix.collections.CollectionsDialogKt")
}
@Test

@ -0,0 +1,60 @@
/* 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/. */
package org.mozilla.fenix.tabstray.ext
import io.mockk.mockk
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.tabstray.Tab
import org.junit.Assert.assertEquals
import org.junit.Test
import org.mozilla.fenix.tabstray.browser.createTab
class BrowserStoreKtTest {
@Test
fun `WHEN session is found THEN return it`() {
val store = BrowserStore(
initialState = BrowserState(
listOf(
TabSessionState(id = "tab1", mockk(), lastAccess = 3),
TabSessionState(id = "tab2", mockk(), lastAccess = 5)
)
)
)
val tabs = listOf<Tab>(
createTab("tab1"),
createTab("tab2")
)
val result = store.getTabSessionState(tabs)
assertEquals(3, result[0].lastAccess)
assertEquals(5, result[1].lastAccess)
}
@Test
fun `WHEN session is not found THEN ignore it`() {
val store = BrowserStore(
initialState = BrowserState(
listOf(
TabSessionState(id = "tab2", mockk(), lastAccess = 5)
)
)
)
val tabs = listOf<Tab>(
createTab("tab1"),
createTab("tab2")
)
val result = store.getTabSessionState(tabs)
assertEquals(5, result[0].lastAccess)
assertEquals(1, result.size)
}
}
Loading…
Cancel
Save