Backed out changeset d40105caed00 (bug 1906405) for causing xpcshell failures on...
[gecko.git] / mobile / android / fenix / app / src / main / java / org / mozilla / fenix / home / recentvisits / view / RecentlyVisited.kt
blob74fcb80722f1e27d08793ffc11434f63b8c93fd0
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 package org.mozilla.fenix.home.recentvisits.view
7 import androidx.compose.foundation.ExperimentalFoundationApi
8 import androidx.compose.foundation.background
9 import androidx.compose.foundation.combinedClickable
10 import androidx.compose.foundation.horizontalScroll
11 import androidx.compose.foundation.layout.Arrangement
12 import androidx.compose.foundation.layout.Box
13 import androidx.compose.foundation.layout.ExperimentalLayoutApi
14 import androidx.compose.foundation.layout.FlowColumn
15 import androidx.compose.foundation.layout.Row
16 import androidx.compose.foundation.layout.fillMaxWidth
17 import androidx.compose.foundation.layout.padding
18 import androidx.compose.foundation.layout.widthIn
19 import androidx.compose.foundation.rememberScrollState
20 import androidx.compose.foundation.shape.RoundedCornerShape
21 import androidx.compose.material.Card
22 import androidx.compose.runtime.Composable
23 import androidx.compose.runtime.derivedStateOf
24 import androidx.compose.runtime.getValue
25 import androidx.compose.runtime.mutableStateOf
26 import androidx.compose.runtime.remember
27 import androidx.compose.runtime.setValue
28 import androidx.compose.ui.Alignment
29 import androidx.compose.ui.ExperimentalComposeUiApi
30 import androidx.compose.ui.Modifier
31 import androidx.compose.ui.graphics.Color
32 import androidx.compose.ui.res.painterResource
33 import androidx.compose.ui.res.stringResource
34 import androidx.compose.ui.semantics.semantics
35 import androidx.compose.ui.semantics.testTag
36 import androidx.compose.ui.semantics.testTagsAsResourceId
37 import androidx.compose.ui.tooling.preview.Preview
38 import androidx.compose.ui.unit.dp
39 import mozilla.components.support.ktx.kotlin.trimmed
40 import org.mozilla.fenix.R
41 import org.mozilla.fenix.compose.ContextualMenu
42 import org.mozilla.fenix.compose.Divider
43 import org.mozilla.fenix.compose.MenuItem
44 import org.mozilla.fenix.compose.annotation.LightDarkPreview
45 import org.mozilla.fenix.compose.ext.thenConditional
46 import org.mozilla.fenix.compose.list.FaviconListItem
47 import org.mozilla.fenix.compose.list.IconListItem
48 import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem
49 import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryGroup
50 import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryHighlight
51 import org.mozilla.fenix.theme.FirefoxTheme
53 // Number of recently visited items per column.
54 private const val VISITS_PER_COLUMN = 3
56 private val recentlyVisitedItemMaxWidth = 320.dp
58 private val horizontalArrangementSpacing = 32.dp
59 private val contentPadding = 16.dp
61 /**
62  * A list of recently visited items.
63  *
64  * @param recentVisits List of [RecentlyVisitedItem] to display.
65  * @param menuItems List of [RecentVisitMenuItem] shown long clicking a [RecentlyVisitedItem].
66  * @param backgroundColor The background [Color] of each item.
67  * @param onRecentVisitClick Invoked when the user clicks on a recent visit. The first parameter is
68  * the [RecentlyVisitedItem] that was clicked and the second parameter is the "page" or column number
69  * the item resides in.
70  */
71 @OptIn(ExperimentalLayoutApi::class)
72 @Composable
73 fun RecentlyVisited(
74     recentVisits: List<RecentlyVisitedItem>,
75     menuItems: List<RecentVisitMenuItem>,
76     backgroundColor: Color = FirefoxTheme.colors.layer2,
77     onRecentVisitClick: (RecentlyVisitedItem, pageNumber: Int) -> Unit = { _, _ -> },
78 ) {
79     val isSingleColumn by remember(recentVisits) { derivedStateOf { recentVisits.size <= VISITS_PER_COLUMN } }
81     Row(
82         modifier = Modifier
83             .fillMaxWidth()
84             .thenConditional(
85                 modifier = Modifier.horizontalScroll(state = rememberScrollState()),
86                 predicate = { !isSingleColumn },
87             )
88             .padding(
89                 horizontal = contentPadding,
90                 vertical = 8.dp,
91             ),
92     ) {
93         Card(
94             modifier = Modifier.fillMaxWidth(),
95             shape = RoundedCornerShape(8.dp),
96             backgroundColor = backgroundColor,
97             elevation = 6.dp,
98         ) {
99             FlowColumn(
100                 modifier = Modifier.fillMaxWidth(),
101                 maxItemsInEachColumn = VISITS_PER_COLUMN,
102                 horizontalArrangement = Arrangement.spacedBy(horizontalArrangementSpacing),
103             ) {
104                 recentVisits.forEachIndexed { index, recentVisit ->
105                     // Don't display the divider when its the last item in a column or the last item
106                     // in the table.
107                     val showDivider = (index + 1) % VISITS_PER_COLUMN != 0 &&
108                         index != recentVisits.lastIndex
109                     val pageIndex = index / VISITS_PER_COLUMN
110                     val pageNumber = pageIndex + 1
112                     Box(
113                         modifier = if (isSingleColumn) {
114                             Modifier.fillMaxWidth()
115                         } else {
116                             Modifier.widthIn(max = recentlyVisitedItemMaxWidth)
117                         },
118                     ) {
119                         when (recentVisit) {
120                             is RecentHistoryHighlight -> RecentlyVisitedHistoryHighlight(
121                                 recentVisit = recentVisit,
122                                 menuItems = menuItems,
123                                 onRecentVisitClick = {
124                                     onRecentVisitClick(it, pageNumber)
125                                 },
126                             )
128                             is RecentHistoryGroup -> RecentlyVisitedHistoryGroup(
129                                 recentVisit = recentVisit,
130                                 menuItems = menuItems,
131                                 onRecentVisitClick = {
132                                     onRecentVisitClick(it, pageNumber)
133                                 },
134                             )
135                         }
137                         if (showDivider) {
138                             Divider(
139                                 modifier = Modifier
140                                     .align(Alignment.BottomCenter)
141                                     .padding(horizontal = contentPadding),
142                             )
143                         }
144                     }
145                 }
146             }
147         }
148     }
152  * A recently visited history group.
154  * @param recentVisit The [RecentHistoryGroup] to display.
155  * @param menuItems List of [RecentVisitMenuItem] to display in a recent visit dropdown menu.
156  * @param onRecentVisitClick Invoked when the user clicks on a recent visit.
157  */
158 @OptIn(
159     ExperimentalFoundationApi::class,
160     ExperimentalComposeUiApi::class,
162 @Composable
163 private fun RecentlyVisitedHistoryGroup(
164     recentVisit: RecentHistoryGroup,
165     menuItems: List<RecentVisitMenuItem>,
166     onRecentVisitClick: (RecentHistoryGroup) -> Unit = { _ -> },
167 ) {
168     var isMenuExpanded by remember { mutableStateOf(false) }
169     val captionId = if (recentVisit.historyMetadata.size == 1) {
170         R.string.history_search_group_site_1
171     } else {
172         R.string.history_search_group_sites_1
173     }
175     Box {
176         IconListItem(
177             label = recentVisit.title.trimmed(),
178             modifier = Modifier
179                 .combinedClickable(
180                     onClick = { onRecentVisitClick(recentVisit) },
181                     onLongClick = { isMenuExpanded = true },
182                 ),
183             beforeIconPainter = painterResource(R.drawable.ic_multiple_tabs),
184             description = stringResource(id = captionId, recentVisit.historyMetadata.size),
185         )
187         ContextualMenu(
188             showMenu = isMenuExpanded,
189             onDismissRequest = { isMenuExpanded = false },
190             menuItems = menuItems.map { item -> MenuItem(item.title) { item.onClick(recentVisit) } },
191             modifier = Modifier.semantics {
192                 testTagsAsResourceId = true
193                 testTag = "recent.visit.menu"
194             },
195         )
196     }
200  * A recently visited history item.
202  * @param recentVisit The [RecentHistoryHighlight] to display.
203  * @param menuItems List of [RecentVisitMenuItem] to display in a recent visit dropdown menu.
204  * @param onRecentVisitClick Invoked when the user clicks on a recent visit.
205  */
206 @OptIn(
207     ExperimentalFoundationApi::class,
208     ExperimentalComposeUiApi::class,
210 @Composable
211 private fun RecentlyVisitedHistoryHighlight(
212     recentVisit: RecentHistoryHighlight,
213     menuItems: List<RecentVisitMenuItem>,
214     onRecentVisitClick: (RecentHistoryHighlight) -> Unit = { _ -> },
215 ) {
216     var isMenuExpanded by remember { mutableStateOf(false) }
218     Box {
219         FaviconListItem(
220             label = recentVisit.title.trimmed(),
221             url = recentVisit.url,
222             modifier = Modifier
223                 .combinedClickable(
224                     onClick = { onRecentVisitClick(recentVisit) },
225                     onLongClick = { isMenuExpanded = true },
226                 ),
227         )
229         ContextualMenu(
230             showMenu = isMenuExpanded,
231             onDismissRequest = { isMenuExpanded = false },
232             menuItems = menuItems.map { item -> MenuItem(item.title) { item.onClick(recentVisit) } },
233             modifier = Modifier.semantics {
234                 testTagsAsResourceId = true
235                 testTag = "recent.visit.menu"
236             },
237         )
238     }
241 @Composable
242 @LightDarkPreview
243 private fun RecentlyVisitedMultipleColumnsPreview() {
244     FirefoxTheme {
245         Box(
246             modifier = Modifier
247                 .background(color = FirefoxTheme.colors.layer1)
248                 .padding(vertical = contentPadding),
249         ) {
250             RecentlyVisited(
251                 recentVisits = listOf(
252                     RecentHistoryGroup(title = "running shoes"),
253                     RecentHistoryGroup(title = "mozilla"),
254                     RecentHistoryGroup(title = "firefox"),
255                     RecentHistoryGroup(title = "pocket"),
256                     RecentHistoryHighlight(title = "Mozilla", url = "www.mozilla.com"),
257                 ),
258                 menuItems = emptyList(),
259             )
260         }
261     }
264 @Composable
265 @LightDarkPreview
266 private fun RecentlyVisitedSingleColumnPreview() {
267     FirefoxTheme {
268         Box(
269             modifier = Modifier
270                 .background(color = FirefoxTheme.colors.layer1)
271                 .padding(vertical = contentPadding),
272         ) {
273             RecentlyVisited(
274                 recentVisits = listOf(
275                     RecentHistoryGroup(title = "running shoes"),
276                     RecentHistoryHighlight(title = "Mozilla", url = "www.mozilla.com"),
277                 ),
278                 menuItems = emptyList(),
279             )
280         }
281     }
284 @Composable
285 @Preview(widthDp = 250)
286 private fun RecentlyVisitedSingleColumnSmallPreview() {
287     RecentlyVisitedSingleColumnPreview()