mirror of
https://github.com/fork-maintainers/iceraven-browser
synced 2024-11-03 23:15:31 +00:00
[fenix] Close https://github.com/mozilla-mobile/fenix/issues/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.
This commit is contained in:
parent
e50350db14
commit
5000e54aaf
@ -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…
Reference in New Issue
Block a user