diff --git a/components/brave_new_tab_ui/actions/grid_sites_actions.ts b/components/brave_new_tab_ui/actions/grid_sites_actions.ts new file mode 100644 index 000000000000..b82ea788ee9f --- /dev/null +++ b/components/brave_new_tab_ui/actions/grid_sites_actions.ts @@ -0,0 +1,69 @@ +// Copyright (c) 2020 The Brave Authors. All rights reserved. +// 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/. + +// Types +import { types } from '../constants/grid_sites_types' +import { action } from 'typesafe-actions' +import { InitialData } from '../api/initialData' +import { Dispatch } from 'redux' + +// API +import { + fetchAllBookmarkTreeNodes, + updateBookmarkTreeNode +} from '../api/bookmarks' + +export const setFirstRenderGridSitesData = (initialData: InitialData) => { + return action(types.GRID_SITES_SET_FIRST_RENDER_DATA, initialData) +} + +export const gridSitesDataUpdated = (gridSites: NewTab.Site[]) => { + return action(types.GRID_SITES_DATA_UPDATED, { gridSites }) +} + +export const toggleGridSitePinned = (pinnedSite: NewTab.Site) => { + return action(types.GRID_SITES_TOGGLE_SITE_PINNED, { pinnedSite }) +} + +export const removeGridSite = (removedSite: NewTab.Site) => { + return action(types.GRID_SITES_REMOVE_SITE, { removedSite }) +} + +export const undoRemoveGridSite = () => { + return action(types.GRID_SITES_UNDO_REMOVE_SITE) +} + +export const undoRemoveAllGridSites = () => { + return action(types.GRID_SITES_UNDO_REMOVE_ALL_SITES) +} + +export const updateGridSitesBookmarkInfo = ( + sites: chrome.topSites.MostVisitedURL[] +) => { + return async (dispatch: Dispatch) => { + const bookmarkInfo = await fetchAllBookmarkTreeNodes(sites) + dispatch(action(types.GRID_SITES_UPDATE_SITE_BOOKMARK_INFO, { + bookmarkInfo + })) + } +} + +export const toggleGridSiteBookmarkInfo = (site: NewTab.Site) => { + return async (dispatch: Dispatch) => { + const bookmarkInfo = await updateBookmarkTreeNode(site) + dispatch(action(types.GRID_SITES_TOGGLE_SITE_BOOKMARK_INFO, { + url: site.url, + bookmarkInfo + })) + } +} + +export const addGridSites = (site: NewTab.Site) => { + return action(types.GRID_SITES_ADD_SITES, { site }) +} + +export const showGridSiteRemovedNotification = (shouldShow: boolean) => { + return action(types.GRID_SITES_SHOW_SITE_REMOVED_NOTIFICATION, { shouldShow }) +} diff --git a/components/brave_new_tab_ui/actions/new_tab_actions.ts b/components/brave_new_tab_ui/actions/new_tab_actions.ts index f525aad0fb76..83b8c9280693 100644 --- a/components/brave_new_tab_ui/actions/new_tab_actions.ts +++ b/components/brave_new_tab_ui/actions/new_tab_actions.ts @@ -1,6 +1,7 @@ -/* 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/. */ +// Copyright (c) 2020 The Brave Authors. All rights reserved. +// 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/. import { action } from 'typesafe-actions' @@ -11,56 +12,6 @@ import { Stats } from '../api/stats' import { PrivateTabData } from '../api/privateTabData' import { InitialData, InitialRewardsData, PreInitialRewardsData } from '../api/initialData' -export const bookmarkAdded = (url: string) => action(types.BOOKMARK_ADDED, { - url -}) - -export const bookmarkRemoved = (url: string) => action(types.BOOKMARK_REMOVED, { - url -}) - -export const sitePinned = (url: string) => action(types.NEW_TAB_SITE_PINNED, { - url -}) - -export const siteUnpinned = (url: string) => action(types.NEW_TAB_SITE_UNPINNED, { - url -}) - -export const siteIgnored = (url: string) => action(types.NEW_TAB_SITE_IGNORED, { - url -}) - -export const undoSiteIgnored = (url: string) => action(types.NEW_TAB_UNDO_SITE_IGNORED, { - url -}) - -export const undoAllSiteIgnored = (url: string) => action(types.NEW_TAB_UNDO_ALL_SITE_IGNORED, { - url -}) - -export const siteDragged = (fromUrl: string, toUrl: string, dragRight: boolean) => action(types.NEW_TAB_SITE_DRAGGED, { - fromUrl, - toUrl, - dragRight -}) - -export const siteDragEnd = (url: string, didDrop: boolean) => action(types.NEW_TAB_SITE_DRAG_END, { - url, - didDrop -}) - -export const onHideSiteRemovalNotification = () => action(types.NEW_TAB_HIDE_SITE_REMOVAL_NOTIFICATION) - -export const bookmarkInfoAvailable = (queryUrl: string, bookmarkTreeNode: NewTab.Bookmark) => action(types.NEW_TAB_BOOKMARK_INFO_AVAILABLE, { - queryUrl, - bookmarkTreeNode -}) - -export const gridSitesUpdated = (gridSites: NewTab.Site[]) => action(types.NEW_TAB_GRID_SITES_UPDATED, { - gridSites -}) - export const statsUpdated = (stats: Stats) => action(types.NEW_TAB_STATS_UPDATED, { stats diff --git a/components/brave_new_tab_ui/api/bookmarks.ts b/components/brave_new_tab_ui/api/bookmarks.ts new file mode 100644 index 000000000000..9a4f2867cf05 --- /dev/null +++ b/components/brave_new_tab_ui/api/bookmarks.ts @@ -0,0 +1,47 @@ +// Copyright (c) 2020 The Brave Authors. All rights reserved. +// 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/. + +/** + * Obtain the URLs bookmark info + */ +export const fetchBookmarkTreeNode = ( + url: string +): Promise => { + return new Promise(resolve => { + chrome.bookmarks.search( + url, + (bookmarkTreeNodes) => { + resolve(bookmarkTreeNodes[0]) + } + ) + }) +} + +/** + * Iterate over the sites array and obtain all URLs + * bookmark info + */ +export const fetchAllBookmarkTreeNodes = ( + sites: chrome.topSites.MostVisitedURL[] +): Promise => { + return Promise + .all(sites.map(site => fetchBookmarkTreeNode(site.url))) +} + +/** + * Update bookmark info based on user interaction + */ +export const updateBookmarkTreeNode = (site: NewTab.Site) => { + return new Promise(async resolve => { + const bookmarkInfo = await fetchBookmarkTreeNode(site.url) + // Toggle the bookmark state + if (bookmarkInfo) { + chrome.bookmarks.remove(bookmarkInfo.id) + } else { + chrome.bookmarks.create({ title: site.title, url: site.url }) + } + resolve(bookmarkInfo) + }) +} diff --git a/components/brave_new_tab_ui/api/getActions.ts b/components/brave_new_tab_ui/api/getActions.ts index 8af578b6510f..129e508400f8 100644 --- a/components/brave_new_tab_ui/api/getActions.ts +++ b/components/brave_new_tab_ui/api/getActions.ts @@ -1,19 +1,22 @@ -/* 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/. */ +// Copyright (c) 2020 The Brave Authors. All rights reserved. +// 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/. import { bindActionCreators } from 'redux' import * as newTabActions from '../actions/new_tab_actions' +import * as gridSitesActions from '../actions/grid_sites_actions' import store from '../store' /** * Get actions from the C++ back-end down to front-end components */ -let actions: typeof newTabActions +let actions: typeof newTabActions & typeof gridSitesActions export default function getActions () { if (actions) { return actions } - actions = bindActionCreators(newTabActions, store.dispatch.bind(store)) + const allActions = Object.assign({}, newTabActions, gridSitesActions) + actions = bindActionCreators(allActions, store.dispatch.bind(store)) return actions } diff --git a/components/brave_new_tab_ui/api/initialData.ts b/components/brave_new_tab_ui/api/initialData.ts index 92256961362f..2a382e1872d4 100644 --- a/components/brave_new_tab_ui/api/initialData.ts +++ b/components/brave_new_tab_ui/api/initialData.ts @@ -13,7 +13,7 @@ export type InitialData = { preferences: preferencesAPI.Preferences stats: statsAPI.Stats privateTabData: privateTabDataAPI.PrivateTabData - topSites: topSitesAPI.TopSitesData, + topSites: chrome.topSites.MostVisitedURL[] brandedWallpaperData: undefined | NewTab.BrandedWallpaper } diff --git a/components/brave_new_tab_ui/api/topSites/index.ts b/components/brave_new_tab_ui/api/topSites.ts similarity index 59% rename from components/brave_new_tab_ui/api/topSites/index.ts rename to components/brave_new_tab_ui/api/topSites.ts index b617b109e912..13970960654c 100644 --- a/components/brave_new_tab_ui/api/topSites/index.ts +++ b/components/brave_new_tab_ui/api/topSites.ts @@ -1,16 +1,14 @@ -// Copyright (c) 2019 The Brave Authors. All rights reserved. +// Copyright (c) 2020 The Brave Authors. All rights reserved. // 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/. -export type TopSitesData = NewTab.Site[] - /** * Obtains the top sites */ -export function getTopSites (): Promise { +export function getTopSites (): Promise { return new Promise(resolve => { - chrome.topSites.get((topSites: NewTab.Site[]) => { + chrome.topSites.get((topSites: chrome.topSites.MostVisitedURL[]) => { resolve(topSites || []) }) }) diff --git a/components/brave_new_tab_ui/api/topSites/bookmarks.ts b/components/brave_new_tab_ui/api/topSites/bookmarks.ts deleted file mode 100644 index a933221c047f..000000000000 --- a/components/brave_new_tab_ui/api/topSites/bookmarks.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* 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/. */ - -import getActions from '../getActions' - -/** - * Obtains the URL's bookmark info and calls an action with the result - */ -export const fetchBookmarkInfo = (url: string) => { - chrome.bookmarks.search(url.replace(/^https?:\/\//, ''), - (bookmarkTreeNodes) => getActions().bookmarkInfoAvailable(url, bookmarkTreeNodes[0] as NewTab.Bookmark) - ) -} - -/** - * Updates bookmark info for top sites based on their state - */ -export const updateBookmarkInfo = (state: NewTab.State, url: string, bookmarkTreeNode?: NewTab.Bookmark) => { - const bookmarks = state.bookmarks - const gridSites = state.gridSites.slice() - const topSites = state.topSites.slice() - const pinnedTopSites = state.pinnedTopSites.slice() - // The default empty object is just to avoid null checks below - const gridSite: Partial = gridSites.find((s) => s.url === url) || {} - const topSite: Partial = topSites.find((s) => s.url === url) || {} - const pinnedTopSite: Partial = pinnedTopSites.find((s) => s.url === url) || {} - - if (bookmarkTreeNode) { - bookmarks[url] = bookmarkTreeNode - gridSite.bookmarked = topSite.bookmarked = pinnedTopSite.bookmarked = bookmarkTreeNode - } else { - delete bookmarks[url] - gridSite.bookmarked = topSite.bookmarked = pinnedTopSite.bookmarked = undefined - } - state = { ...state, bookmarks, gridSites } - - return state -} diff --git a/components/brave_new_tab_ui/api/topSites/dnd.ts b/components/brave_new_tab_ui/api/topSites/dnd.ts deleted file mode 100644 index c204f2d11275..000000000000 --- a/components/brave_new_tab_ui/api/topSites/dnd.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* 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/. */ - -import * as gridAPI from './grid' - -export const onDraggedSite = (state: NewTab.State, url: string, destUrl: string) => { - const gridSitesWithoutPreview = gridAPI.getGridSites(state) - const currentPositionIndex = gridSitesWithoutPreview.findIndex(site => site.url === url) - const finalPositionIndex = gridSitesWithoutPreview.findIndex(site => site.url === destUrl) - let pinnedTopSites = state.pinnedTopSites.slice() - - // A site that is not pinned yet will become pinned - const pinnedMovingSite = pinnedTopSites.find(site => site.url === url) - if (!pinnedMovingSite) { - const movingTopSite = Object.assign({}, gridSitesWithoutPreview.find(site => site.url === url)) - movingTopSite.index = currentPositionIndex - movingTopSite.pinned = true - pinnedTopSites.push(movingTopSite) - } - - pinnedTopSites = pinnedTopSites.map((pinnedTopSite) => { - pinnedTopSite = Object.assign({}, pinnedTopSite) - const currentIndex = pinnedTopSite.index - if (currentIndex === currentPositionIndex) { - pinnedTopSite.index = finalPositionIndex - } else if (currentIndex > currentPositionIndex && pinnedTopSite.index <= finalPositionIndex) { - pinnedTopSite.index = pinnedTopSite.index - 1 - } else if (currentIndex < currentPositionIndex && pinnedTopSite.index >= finalPositionIndex) { - pinnedTopSite.index = pinnedTopSite.index + 1 - } - return pinnedTopSite - }) - state = { ...state, pinnedTopSites } - state = { ...state, gridSites: gridAPI.getGridSites(state) } - return state -} - -export const onDragEnd = (state: NewTab.State) => { - state = { ...state, gridSites: gridAPI.getGridSites(state) } - return state -} diff --git a/components/brave_new_tab_ui/api/topSites/grid.ts b/components/brave_new_tab_ui/api/topSites/grid.ts deleted file mode 100644 index 9a37884936bf..000000000000 --- a/components/brave_new_tab_ui/api/topSites/grid.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* 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/. */ - -// API -import getActions from '../getActions' -import * as bookmarksAPI from './bookmarks' -import { getCharForSite } from '../../helpers/newTabUtils' - -// Utils -import { debounce } from '../../../common/debounce' - -export const getGridSites = (state: NewTab.State, checkBookmarkInfo?: boolean) => { - const sizeToCount = { large: 18, medium: 12, small: 6 } - const count = sizeToCount[state.gridLayoutSize || 'small'] - const defaultChromeWebStoreUrl = 'https://chrome.google.com/webstore' - - // Start with top sites with filtered out ignored sites and pinned sites - let gridSites = state.topSites.slice() - .filter((site) => - !state.ignoredTopSites.find((ignoredSite) => ignoredSite.url === site.url) && - !state.pinnedTopSites.find((pinnedSite) => pinnedSite.url === site.url) && - // see https://github.com/brave/brave-browser/issues/5376 - !site.url.startsWith(defaultChromeWebStoreUrl) - ) - - // Then add in pinned sites at the specified index, these need to be added in the same - // order as the index they are. - const pinnedTopSites = state.pinnedTopSites - .slice() - .sort((x, y) => x.index - y.index) - pinnedTopSites.forEach((pinnedSite) => { - gridSites.splice(pinnedSite.index, 0, pinnedSite) - }) - - gridSites = gridSites.slice(0, count) - gridSites.forEach((gridSite: NewTab.Site) => { - gridSite.letter = getCharForSite(gridSite) - gridSite.thumb = `chrome://thumb/${gridSite.url}` - gridSite.favicon = `chrome://favicon/size/64@1x/${gridSite.url}` - gridSite.bookmarked = state.bookmarks[gridSite.url] - - if (checkBookmarkInfo && !gridSite.bookmarked) { - bookmarksAPI.fetchBookmarkInfo(gridSite.url) - } - }) - return gridSites -} - -/** - * Calculates the top sites grid and calls an action with the results - */ -export const calculateGridSites = debounce((state: NewTab.State) => { - // TODO(petemill): - // Instead of debouncing at the point of reducing actions to state, - // and having the reducer call this, it may be more understandable - // (and performant) to have this be a selector so that the calculation - // is only performed when the relevant state data is changed. - getActions().gridSitesUpdated(getGridSites(state, true)) -}, 10) diff --git a/components/brave_new_tab_ui/apiEventsToStore.ts b/components/brave_new_tab_ui/apiEventsToStore.ts index f94d6e890294..e2e5d9178612 100644 --- a/components/brave_new_tab_ui/apiEventsToStore.ts +++ b/components/brave_new_tab_ui/apiEventsToStore.ts @@ -38,6 +38,8 @@ export function wireApiEventsToStore () { setRewardsFetchInterval() } getActions().setInitialData(initialData) + getActions().setFirstRenderGridSitesData(initialData) + getActions().updateGridSitesBookmarkInfo(initialData.topSites) // Listen for API changes and dispatch to store statsAPI.addChangeListener(updateStats) preferencesAPI.addChangeListener(updatePreferences) diff --git a/components/brave_new_tab_ui/components/default/clock/style.ts b/components/brave_new_tab_ui/components/default/clock/style.ts index 3436fc9e01d3..1f82e4b03815 100644 --- a/components/brave_new_tab_ui/components/default/clock/style.ts +++ b/components/brave_new_tab_ui/components/default/clock/style.ts @@ -1,8 +1,9 @@ -/* 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/. */ +// Copyright (c) 2020 The Brave Authors. All rights reserved. +// 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/. -import styled from 'styled-components' +import styled from 'brave-ui/theme' export const StyledClock = styled<{}, 'div'>('div')` color: #FFFFFF; diff --git a/components/brave_new_tab_ui/components/default/topSites/index.ts b/components/brave_new_tab_ui/components/default/gridSites/index.ts similarity index 83% rename from components/brave_new_tab_ui/components/default/topSites/index.ts rename to components/brave_new_tab_ui/components/default/gridSites/index.ts index b0fc8f9660aa..dccc30c23753 100644 --- a/components/brave_new_tab_ui/components/default/topSites/index.ts +++ b/components/brave_new_tab_ui/components/default/gridSites/index.ts @@ -1,9 +1,9 @@ -/* 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/. */ +// Copyright (c) 2019 The Brave Authors. All rights reserved. +// 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/. import styled from 'brave-ui/theme' -import createWidget from '../widget' interface ListProps { blockNumber: number @@ -48,7 +48,8 @@ interface TileActionProps { standalone?: boolean } -export const TileAction = styled('a')` +export const TileAction = styled('button')` + -webkit-appearance: none; box-sizing: border-box; transition: color 0.1s linear; color: #424242; @@ -62,9 +63,9 @@ export const TileAction = styled('a')` left: ${p => p.standalone && '6px'}; border-radius: ${p => p.standalone && '4px'}; margin: 0; - text-decoration: none; display: block; cursor: pointer; + border: 0; &:hover { color: #000; @@ -88,6 +89,8 @@ export const Tile = styled('div')` width: 80px; height: 80px; font-size: 38px; + z-index: 3; + cursor: grab; &:hover { ${TileActionsContainer} { @@ -104,4 +107,4 @@ export const TileFavicon = styled<{}, 'img'>('img')` object-fit: contain; ` -export const ListWidget = createWidget(List) +export const ListWidget = List diff --git a/components/brave_new_tab_ui/components/default/index.ts b/components/brave_new_tab_ui/components/default/index.ts index e8992bba92bb..db27ad5f7387 100644 --- a/components/brave_new_tab_ui/components/default/index.ts +++ b/components/brave_new_tab_ui/components/default/index.ts @@ -5,7 +5,7 @@ import { StatsContainer, StatsItem } from './stats' import { SettingsMenu, SettingsRow, SettingsText, SettingsTitle, SettingsWrapper } from './settings' -import { ListWidget, Tile, TileActionsContainer, TileAction, TileFavicon } from './topSites' +import { ListWidget, Tile, TileActionsContainer, TileAction, TileFavicon } from './gridSites' import { SiteRemovalNotification, SiteRemovalText, SiteRemovalAction } from './notification' import { ClockWidget } from './clock' import RewardsWidget from './rewards' diff --git a/components/brave_new_tab_ui/components/default/widget/styles.ts b/components/brave_new_tab_ui/components/default/widget/styles.ts index 19a15830fb09..5aeb73add962 100644 --- a/components/brave_new_tab_ui/components/default/widget/styles.ts +++ b/components/brave_new_tab_ui/components/default/widget/styles.ts @@ -1,6 +1,7 @@ -/* 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/. */ +// Copyright (c) 2020 The Brave Authors. All rights reserved. +// 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/. import styled, { css } from 'brave-ui/theme' diff --git a/components/brave_new_tab_ui/constants/grid_sites_types.ts b/components/brave_new_tab_ui/constants/grid_sites_types.ts new file mode 100644 index 000000000000..77589f604b1c --- /dev/null +++ b/components/brave_new_tab_ui/constants/grid_sites_types.ts @@ -0,0 +1,21 @@ +// Copyright (c) 2020 The Brave Authors. All rights reserved. +// 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/. + +export const enum types { + GRID_SITES_SET_FIRST_RENDER_DATA = '@@topSites/GRID_SITES_SET_FIRST_RENDER_DATA', + GRID_SITES_DATA_UPDATED = '@@topSites/GRID_SITES_DATA_UPDATED', + GRID_SITES_TOGGLE_SITE_PINNED = '@@topSites/GRID_SITES_SITE_PINNED', + GRID_SITES_REMOVE_SITE = '@@topSites/GRID_SITES_REMOVE_SITE', + GRID_SITES_UNDO_REMOVE_SITE = '@@topSites/GRID_SITES_UNDO_REMOVE_SITE', + GRID_SITES_UNDO_REMOVE_ALL_SITES = + '@@topSites/GRID_SITES_UNDO_REMOVE_ALL_SITES', + GRID_SITES_UPDATE_SITE_BOOKMARK_INFO = + '@@topSites/GRID_SITES_UPDATE_SITE_BOOKMARK_INFO', + GRID_SITES_TOGGLE_SITE_BOOKMARK_INFO = + '@@topSites/GRID_SITES_TOGGLE_SITE_BOOKMARK_INFO', + GRID_SITES_ADD_SITES = '@@topSites/GRID_SITES_ADD_SITES', + GRID_SITES_SHOW_SITE_REMOVED_NOTIFICATION = + '@@topSites/GRID_SITES_SHOW_SITE_REMOVED_NOTIFICATION' +} diff --git a/components/brave_new_tab_ui/constants/new_tab_types.ts b/components/brave_new_tab_ui/constants/new_tab_types.ts index 868cedb8d26c..380ac34d3279 100644 --- a/components/brave_new_tab_ui/constants/new_tab_types.ts +++ b/components/brave_new_tab_ui/constants/new_tab_types.ts @@ -1,21 +1,9 @@ -/* 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/. */ +// Copyright (c) 2020 The Brave Authors. All rights reserved. +// 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/. export const enum types { - BOOKMARK_ADDED = '@@newtab/BOOKMARK_ADDED', - BOOKMARK_REMOVED = '@@newtab/BOOKMARK_REMOVED', - NEW_TAB_TOP_SITES_DATA_UPDATED = '@@newtab/NEW_TAB_TOP_SITES_DATA_UPDATED', - NEW_TAB_SITE_PINNED = '@@newtab/NEW_TAB_SITE_PINNED', - NEW_TAB_SITE_UNPINNED = '@@newtab/NEW_TAB_SITE_UNPINNED', - NEW_TAB_SITE_IGNORED = '@@newtab/NEW_TAB_SITE_IGNORED', - NEW_TAB_UNDO_SITE_IGNORED = '@@newtab/NEW_TAB_UNDO_SITE_IGNORED', - NEW_TAB_UNDO_ALL_SITE_IGNORED = '@@newtab/NEW_TAB_UNDO_ALL_SITE_IGNORED', - NEW_TAB_SITE_DRAGGED = '@@newtab/NEW_TAB_SITE_DRAGGED', - NEW_TAB_SITE_DRAG_END = '@@newtab/NEW_TAB_SITE_DRAG_END', - NEW_TAB_HIDE_SITE_REMOVAL_NOTIFICATION = '@@newtab/NEW_TAB_HIDE_SITE_REMOVAL_NOTIFICATION', - NEW_TAB_BOOKMARK_INFO_AVAILABLE = '@@newtab/NEW_TAB_BOOKMARK_INFO_AVAILABLE', - NEW_TAB_GRID_SITES_UPDATED = '@@newtab/NEW_TAB_GRID_SITES_UPDATED', NEW_TAB_STATS_UPDATED = '@@newtab/NEW_TAB_STATS_UPDATED', NEW_TAB_PRIVATE_TAB_DATA_UPDATED = '@@newtab/NEW_TAB_PRIVATE_TAB_DATA_UPDATED', NEW_TAB_PREFERENCES_UPDATED = '@@newtab/NEW_TAB_PREFERENCES_UPDATED', diff --git a/components/brave_new_tab_ui/constants/new_tab_ui.ts b/components/brave_new_tab_ui/constants/new_tab_ui.ts new file mode 100644 index 000000000000..b809e01b3f4e --- /dev/null +++ b/components/brave_new_tab_ui/constants/new_tab_ui.ts @@ -0,0 +1,6 @@ +// Copyright (c) 2020 The Brave Authors. All rights reserved. +// 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/. + +export const MAX_GRID_SIZE = 6 diff --git a/components/brave_new_tab_ui/containers/app.tsx b/components/brave_new_tab_ui/containers/app.tsx index 6fe6644450b5..02f1acac719c 100644 --- a/components/brave_new_tab_ui/containers/app.tsx +++ b/components/brave_new_tab_ui/containers/app.tsx @@ -13,16 +13,18 @@ import NewTabPage from './newTab' // Utils import * as newTabActions from '../actions/new_tab_actions' +import * as gridSitesActions from '../actions/grid_sites_actions' import * as PreferencesAPI from '../api/preferences' interface Props { - actions: any + actions: typeof newTabActions & typeof gridSitesActions newTabData: NewTab.State + gridSitesData: NewTab.GridSitesState } class DefaultPage extends React.Component { render () { - const { newTabData, actions } = this.props + const { newTabData, gridSitesData, actions } = this.props // don't render if user prefers an empty page if (this.props.newTabData.showEmptyPage && !this.props.newTabData.isIncognito) { @@ -34,6 +36,7 @@ class DefaultPage extends React.Component { : ( { } const mapStateToProps = (state: NewTab.ApplicationState) => ({ - newTabData: state.newTabData + newTabData: state.newTabData, + gridSitesData: state.gridSitesData }) -const mapDispatchToProps = (dispatch: Dispatch) => ({ - actions: bindActionCreators(newTabActions, dispatch) -}) +const mapDispatchToProps = (dispatch: Dispatch) => { + const allActions = Object.assign({}, newTabActions, gridSitesActions) + return { + actions: bindActionCreators(allActions, dispatch) + } +} export default connect( mapStateToProps, diff --git a/components/brave_new_tab_ui/containers/newTab/block.tsx b/components/brave_new_tab_ui/containers/newTab/block.tsx deleted file mode 100644 index 00425a46943f..000000000000 --- a/components/brave_new_tab_ui/containers/newTab/block.tsx +++ /dev/null @@ -1,179 +0,0 @@ -/* 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/. */ - -import * as React from 'react' -import { - DragSource, - DragSourceCollector, - DragSourceConnector, - DragSourceMonitor, - DragSourceSpec, - DropTarget, - DropTargetCollector, - DropTargetConnector, - DropTargetMonitor, - DropTargetSpec -} from 'react-dnd' - -// Feature-specific components -import { Tile, TileActionsContainer, TileAction, TileFavicon } from '../../components/default' - -// Icons -import { PinIcon, PinOIcon, BookmarkOIcon, BookmarkIcon, CloseStrokeIcon } from 'brave-ui/components/icons' - -const Types = { - BLOCK: 'block' -} - -const blockSource: DragSourceSpec = { - /** - * Required. Called when the dragging starts - * It's the only data available to the drop targets about the drag source - * @see http://gaearon.github.io/react-dnd/docs-drag-source.html#specification-methods - */ - beginDrag (props: Props) { - return { - id: props.id - } - }, - - endDrag (props: Props, monitor: DragSourceMonitor) { - const item: Props = monitor.getItem() as Props - const draggedId = item.id - const didDrop = monitor.didDrop() - props.onDragEnd(draggedId, didDrop) - } -} - -const blockTarget: DropTargetSpec = { - /** - * Optional. Called when an item is hovered over the component - * @see http://gaearon.github.io/react-dnd/docs-drop-target.html#specification-methods - */ - hover (props: Props, monitor: DropTargetMonitor) { - const item: Props = monitor.getItem() as Props - const draggedId = item.id - if (draggedId !== props.id) { - const dragRight = - monitor.getClientOffset().x - monitor.getInitialSourceClientOffset().x > - 0 - props.onDraggedSite(draggedId, props.id, dragRight) - } - } -} - -/** - * Both sourceCollect and targetCollect are called *Collecting Functions* - * They will be called by React DnD with a connector that lets you connect - * nodes to the DnD backend, and a monitor to query information about the drag state. - * It should return a plain object of props to inject into your component. - * - * @see http://gaearon.github.io/react-dnd/docs-drop-target.html#the-collecting-function - */ - -const sourceCollect: DragSourceCollector = ( - connect: DragSourceConnector, - monitor: DragSourceMonitor -) => { - return { - connectDragSource: connect.dragSource(), - isDragging: monitor.isDragging() - } -} - -const targetCollect: DropTargetCollector = (connect: DropTargetConnector) => { - return { - connectDropTarget: connect.dropTarget() - } -} - -interface Props { - id: string - onDragEnd: (draggedId: string, didDrop: boolean) => void - onDraggedSite: (draggedId: string, id: string, dragRight: boolean) => void - connectDragSource?: any - connectDropTarget?: any - onToggleBookmark: () => void - isBookmarked?: boolean - onPinnedTopSite: () => void - isPinned: boolean - onIgnoredTopSite: () => void - title: string - href: string - style: { - backgroundColor: string - } - favicon: string -} - -// TODO remove so many props NZ -class Block extends React.Component { - render () { - const { - connectDragSource, - connectDropTarget, - onToggleBookmark, - isBookmarked, - onPinnedTopSite, - isPinned, - onIgnoredTopSite, - title, - href, - style, - favicon - } = this.props - const starIcon = isBookmarked ? : - const pinIcon = isPinned ? : - - return connectDragSource( - connectDropTarget( -
- - - - {pinIcon} - - - {starIcon} - - - - - - { - isPinned - ? - : null - } - - - - -
- ) - ) - } -} - -/** - * Wraps the component to make it draggable - * Only the drop targets registered for the same type will - * react to the items produced by this drag source. - * - * @see http://gaearon.github.io/react-dnd/docs-drag-source.html - */ -const source = DragSource(Types.BLOCK, blockSource, sourceCollect)( - Block -) - -// Notice that we're exporting the DropTarget and not Block Class. -/** - * React to the compatible items being dragged, hovered, or dropped on it - * Works with the same parameters as DragSource() above. - * - * @see http://gaearon.github.io/react-dnd/docs-drop-target.html - */ -export default DropTarget(Types.BLOCK, blockTarget, targetCollect)( - source -) diff --git a/components/brave_new_tab_ui/containers/newTab/gridSites.tsx b/components/brave_new_tab_ui/containers/newTab/gridSites.tsx new file mode 100644 index 000000000000..c45ec95c57fa --- /dev/null +++ b/components/brave_new_tab_ui/containers/newTab/gridSites.tsx @@ -0,0 +1,88 @@ +// Copyright (c) 2020 The Brave Authors. All rights reserved. +// 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/. + +import * as React from 'react' + +// DnD utils +import { + SortableContainer, + SortEnd, + SortableContainerProps +} from 'react-sortable-hoc' +import arrayMove from 'array-move' + +// Feature-specific components +import { List } from '../../components/default/gridSites' +import createWidget from '../../components/default/widget' + +// Component groups +import GridSiteTile from './gridTile' + +// Helpers +import { isGridSitePinned } from '../../helpers/newTabUtils' + +// Constants +import { MAX_GRID_SIZE } from '../../constants/new_tab_ui' + +// Types +import * as newTabActions from '../../actions/new_tab_actions' +import * as gridSitesActions from '../../actions/grid_sites_actions' + +interface Props { + actions: typeof newTabActions & typeof gridSitesActions + gridSites: NewTab.Site[] +} + +type DynamicListProps = SortableContainerProps & { blockNumber: number } +const DynamicList = SortableContainer((props: DynamicListProps) => { + return +}) + +class TopSitesList extends React.PureComponent { + onSortEnd = ({ oldIndex, newIndex }: SortEnd) => { + // do not update topsites order if the drag destination is a pinned tile + if (this.props.gridSites[newIndex].pinnedIndex) { + return + } + const items = arrayMove(this.props.gridSites, oldIndex, newIndex) + this.props.actions.gridSitesDataUpdated(items) + } + + render () { + const { actions, gridSites } = this.props + return ( + <> + + { + // Grid sites are currently limited to 6 tiles + gridSites.slice(0, MAX_GRID_SIZE) + .map((siteData: NewTab.Site, index: number) => ( + + ))} + + + ) + } +} + +export default createWidget(TopSitesList) diff --git a/components/brave_new_tab_ui/containers/newTab/gridTile.tsx b/components/brave_new_tab_ui/containers/newTab/gridTile.tsx new file mode 100644 index 000000000000..0d8ca199c560 --- /dev/null +++ b/components/brave_new_tab_ui/containers/newTab/gridTile.tsx @@ -0,0 +1,118 @@ +// Copyright (c) 2020 The Brave Authors. All rights reserved. +// 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/. + +import * as React from 'react' + +import { SortableElement, SortableElementProps } from 'react-sortable-hoc' + +// Feature-specific components +import { + Tile, + TileActionsContainer, + TileAction, + TileFavicon +} from '../../components/default' + +// Helpers +import { + isGridSitePinned, + isGridSiteBookmarked +} from '../../helpers/newTabUtils' + +// Icons +import { + PinIcon, + PinOIcon, + BookmarkIcon, + BookmarkOIcon, + CloseStrokeIcon +} from 'brave-ui/components/icons' + +// Types +import * as newTabActions from '../../actions/new_tab_actions' +import * as gridSitesActions from '../../actions/grid_sites_actions' + +interface Props { + actions: typeof newTabActions & typeof gridSitesActions + siteData: NewTab.Site +} + +class TopSite extends React.PureComponent { + onTogglePinnedTopSite (site: NewTab.Site) { + this.props.actions.toggleGridSitePinned(site) + } + + onIgnoredTopSite (site: NewTab.Site) { + this.props.actions.removeGridSite(site) + this.props.actions.showGridSiteRemovedNotification(true) + } + + onToggleBookmark (site: NewTab.Site) { + this.props.actions.toggleGridSiteBookmarkInfo(site) + } + + render () { + const { siteData } = this.props + + return ( + + + + {isGridSitePinned(siteData) ? : } + + + { + isGridSiteBookmarked(siteData.bookmarkInfo) + ? + : + } + + { + // Disallow removing a pinned site + isGridSitePinned(siteData) + ? ( + + + + ) : ( + + + + ) + } + + { + // Add the permanent pinned icon if site is pinned + isGridSitePinned(siteData) + ? ( + + + + ) : null + } + + + ) + } +} + +type TopSiteSortableElementProps = SortableElementProps & Props +export default SortableElement( + (props: TopSiteSortableElementProps) => +) diff --git a/components/brave_new_tab_ui/containers/newTab/index.tsx b/components/brave_new_tab_ui/containers/newTab/index.tsx index c85df6ccff54..1995381a4e26 100644 --- a/components/brave_new_tab_ui/containers/newTab/index.tsx +++ b/components/brave_new_tab_ui/containers/newTab/index.tsx @@ -1,30 +1,38 @@ -/* 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/. */ +// Copyright (c) 2020 The Brave Authors. All rights reserved. +// 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/. import * as React from 'react' -import { DragDropContext } from 'react-dnd' -import HTML5Backend from 'react-dnd-html5-backend' // Components import Stats from './stats' -import Block from './block' +import TopSitesGrid from './gridSites' import FooterInfo from './footerInfo' import SiteRemovalNotification from './notification' import { ClockWidget as Clock, - ListWidget as List, RewardsWidget as Rewards, BinanceWidget as Binance, WidgetStack } from '../../components/default' import * as Page from '../../components/default/page' import BrandedWallpaperLogo from '../../components/default/brandedWallpaper/logo' + +// Helpers import VisibilityTimer from '../../helpers/visibilityTimer' +import arrayMove from 'array-move' +import { isGridSitePinned } from '../../helpers/newTabUtils' + +// Types +import { SortEnd } from 'react-sortable-hoc' +import * as newTabActions from '../../actions/new_tab_actions' +import * as gridSitesActions from '../../actions/grid_sites_actions' interface Props { newTabData: NewTab.State - actions: any + gridSitesData: NewTab.GridSitesState + actions: typeof newTabActions & typeof gridSitesActions saveShowBackgroundImage: (value: boolean) => void saveShowClock: (value: boolean) => void saveShowTopSites: (value: boolean) => void @@ -81,7 +89,7 @@ class NewTabPage extends React.Component { componentDidMount () { // if a notification is open at component mounting time, close it - this.props.actions.onHideSiteRemovalNotification() + this.props.actions.showGridSiteRemovedNotification(false) this.imageSource = GetBackgroundImageSrc(this.props) this.trackCachedImage() if (GetShouldShowBrandedWallpaperNotification(this.props)) { @@ -144,32 +152,16 @@ class NewTabPage extends React.Component { this.visibilityTimer.stopTracking() } - onDraggedSite = (fromUrl: string, toUrl: string, dragRight: boolean) => { - this.props.actions.siteDragged(fromUrl, toUrl, dragRight) - } - - onDragEnd = (url: string, didDrop: boolean) => { - this.props.actions.siteDragEnd(url, didDrop) - } - - onToggleBookmark (site: NewTab.Site) { - if (site.bookmarked === undefined) { - this.props.actions.bookmarkAdded(site.url) - } else { - this.props.actions.bookmarkRemoved(site.url) - } - } - - onTogglePinnedTopSite (site: NewTab.Site) { - if (!site.pinned) { - this.props.actions.sitePinned(site.url) - } else { - this.props.actions.siteUnpinned(site.url) + onSortEnd = ({ oldIndex, newIndex }: SortEnd) => { + const { gridSitesData } = this.props + // Do not update topsites order if the drag + // destination is a pinned tile + const gridSite = gridSitesData.gridSites[newIndex] + if (!gridSite || isGridSitePinned(gridSite)) { + return } - } - - onIgnoredTopSite (site: NewTab.Site) { - this.props.actions.siteIgnored(site.url) + const items = arrayMove(gridSitesData.gridSites, oldIndex, newIndex) + this.props.actions.gridSitesDataUpdated(items) } toggleShowBackgroundImage = () => { @@ -411,7 +403,7 @@ class NewTabPage extends React.Component { } render () { - const { newTabData, actions } = this.props + const { newTabData, gridSitesData, actions } = this.props const { showSettingsMenu } = this.state if (!newTabData) { @@ -420,7 +412,7 @@ class NewTabPage extends React.Component { const hasImage = this.imageSource !== undefined const isShowingBrandedWallpaper = newTabData.brandedWallpaperData ? true : false - const showTopSites = !!this.props.newTabData.gridSites.length && newTabData.showTopSites + const showTopSites = !!this.props.gridSitesData.gridSites.length && newTabData.showTopSites const cryptoContent = this.renderCryptoContent() return ( @@ -465,40 +457,27 @@ class NewTabPage extends React.Component { /> } - {showTopSites && - - { - this.props.newTabData.gridSites.map((site: NewTab.Site) => - + - ) - } - + + ) : null } { - this.props.newTabData.showSiteRemovalNotification - ? - - - : null + gridSitesData.shouldShowSiteRemovedNotification + ? ( + + + + ) : null } {cryptoContent} @@ -543,4 +522,4 @@ class NewTabPage extends React.Component { } } -export default DragDropContext(HTML5Backend)(NewTabPage) +export default NewTabPage diff --git a/components/brave_new_tab_ui/containers/newTab/notification.tsx b/components/brave_new_tab_ui/containers/newTab/notification.tsx index 4da2d7ac3840..0c7ca31b5a11 100644 --- a/components/brave_new_tab_ui/containers/newTab/notification.tsx +++ b/components/brave_new_tab_ui/containers/newTab/notification.tsx @@ -1,6 +1,7 @@ -/* 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/. */ +// Copyright (c) 2020 The Brave Authors. All rights reserved. +// 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/. import * as React from 'react' @@ -17,32 +18,48 @@ import { CloseStrokeIcon } from 'brave-ui/components/icons' // Utils import { getLocale } from '../../../common/locale' +// Types +import * as newTabActions from '../../actions/new_tab_actions' +import * as gridSitesActions from '../../actions/grid_sites_actions' + interface Props { - actions: any + actions: typeof newTabActions & typeof gridSitesActions } export default class Notification extends React.Component { - onUndoIgnoredTopSite = () => { - this.props.actions.undoSiteIgnored() + onUndoRemoveTopSite = () => { + this.props.actions.undoRemoveGridSite() + this.props.actions.showGridSiteRemovedNotification(false) } - onUndoAllSiteIgnored = () => { - this.props.actions.undoAllSiteIgnored() + onUndoRemoveAllTopSites = () => { + this.props.actions.undoRemoveAllGridSites() + this.props.actions.showGridSiteRemovedNotification(false) } onHideSiteRemovalNotification = () => { - this.props.actions.onHideSiteRemovalNotification() + this.props.actions.showGridSiteRemovedNotification(false) } render () { return ( - {getLocale('thumbRemoved')} - {getLocale('undoRemoved')} - {getLocale('restoreAll')} - - - + {getLocale('thumbRemoved')} + + {getLocale('undoRemoved')} + + + {getLocale('restoreAll')} + + + + ) } diff --git a/components/brave_new_tab_ui/helpers/newTabUtils.ts b/components/brave_new_tab_ui/helpers/newTabUtils.ts index a161aebd42ac..086eb48ab5da 100644 --- a/components/brave_new_tab_ui/helpers/newTabUtils.ts +++ b/components/brave_new_tab_ui/helpers/newTabUtils.ts @@ -1,6 +1,7 @@ -/* 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/. */ +// Copyright (c) 2020 The Brave Authors. All rights reserved. +// 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/. export const isHttpOrHttps = (url?: string) => { if (!url) { @@ -9,19 +10,108 @@ export const isHttpOrHttps = (url?: string) => { return /^https?:/i.test(url) } -/** - * Obtains a letter / char that represents the current site - * @param site - The site requested from the top site's list - */ -export const getCharForSite = (site: NewTab.Site) => { - let name - if (!site.title) { +export const getCharForSite = ( + topSite: chrome.topSites.MostVisitedURL +): string => { + let hostname: string = '?' + if (!topSite.title) { try { - name = new window.URL(site.url || '').hostname - } catch (e) { - console.warn('getCharForSite', { url: site.url || '' }) - } + hostname = new window.URL(topSite.url || '').hostname + // tslint:disable-next-line: no-empty + } catch (e) {} } - name = site.title || name || '?' + const name: string = topSite.title || hostname return name.charAt(0).toUpperCase() } + +export const generateGridSiteId = (): string => { + const randomNumber = Math.floor(Math.random() * 10000) + return `topsite-${randomNumber}-${Date.now()}` +} + +export const generateGridSiteFavicon = (url: string): string => { + return `chrome://favicon/size/64@1x/${url}` +} + +export const isGridSitePinned = ( + gridSite: NewTab.Site +): boolean => { + return gridSite.pinnedIndex !== undefined +} + +export const isGridSiteBookmarked = ( + bookmarkInfo: chrome.bookmarks.BookmarkTreeNode | undefined +): boolean => { + return bookmarkInfo !== undefined +} + +export const isExistingGridSite = ( + sitesData: NewTab.Site[], + topOrGridSite: chrome.topSites.MostVisitedURL | NewTab.Site +): boolean => { + return sitesData.some(site => site.url === topOrGridSite.url) +} + +export const generateGridSiteProperties = ( + index: number, + topSite: chrome.topSites.MostVisitedURL, + fromLegacyData?: boolean +): NewTab.Site => { + return { + title: topSite.title, + url: topSite.url, + id: generateGridSiteId(), + letter: getCharForSite(topSite), + favicon: generateGridSiteFavicon(topSite.url), + // In the legacy version of topSites the pinnedIndex + // was the site index itself. + pinnedIndex: fromLegacyData ? index : undefined, + bookmarkInfo: undefined + } +} + +export const getGridSitesWhitelist = ( + topSites: chrome.topSites.MostVisitedURL[] + ): chrome.topSites.MostVisitedURL[] => { + const defaultChromeWebStoreUrl: string = 'https://chrome.google.com/webstore' + const filteredGridSites: chrome.topSites.MostVisitedURL[] = topSites + .filter(site => { + // See https://github.com/brave/brave-browser/issues/5376 + return !site.url.startsWith(defaultChromeWebStoreUrl) + }) + return filteredGridSites +} + +export const generateGridSitesFromLegacyEntries = ( + legacyTopSites: NewTab.LegacySite[] | undefined +) => { + const newGridSites: NewTab.Site[] = [] + + if ( + // Due to a race condition, legacyTopSites can + // be undefined when first called. + legacyTopSites === undefined || + legacyTopSites.length === 0 + ) { + return [] + } + + for (const topSite of legacyTopSites) { + newGridSites + .push(generateGridSiteProperties(topSite.index, topSite, true)) + } + + return newGridSites +} + +export function filterFromExcludedSites ( + sitesData: NewTab.Site[], + removedSitesData: NewTab.Site[] +): NewTab.Site[] { + return sitesData + .filter((site: NewTab.Site) => { + // In updatedGridSites we only want sites not removed by the user + return removedSitesData + .every((removedSite: NewTab.Site) => removedSite.url !== site.url) + }) +} diff --git a/components/brave_new_tab_ui/reducers/grid_sites_reducer.ts b/components/brave_new_tab_ui/reducers/grid_sites_reducer.ts new file mode 100644 index 000000000000..625031e95dbe --- /dev/null +++ b/components/brave_new_tab_ui/reducers/grid_sites_reducer.ts @@ -0,0 +1,116 @@ +// Copyright (c) 2019 The Brave Authors. All rights reserved. +// 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/. + +// Redux API +import { Reducer } from 'redux' + +// Types +import { types } from '../constants/grid_sites_types' + +// API +import * as gridSitesState from '../state/gridSitesState' +import * as storage from '../storage/grid_sites_storage' + +const initialState = storage.load() + +export const gridSitesReducer: Reducer = ( + state: NewTab.GridSitesState | undefined, + action +) => { + if (state === undefined) { + state = initialState + } + + const payload = action.payload + const startingState = state + + switch (action.type) { + case types.GRID_SITES_SET_FIRST_RENDER_DATA: { + + // If there are legacy values from a previous + // storage, update first render data with it + state = gridSitesState + .gridSitesReducerSetFirstRenderDataFromLegacy( + state, + startingState.legacy + ) + // Now that we stored the legacy reference, delete it + // so it won't override gridSites in further updates + if (startingState.legacy) { + delete startingState.legacy + } + + // New profiles just store what comes from Chromium + state = gridSitesState + .gridSitesReducerSetFirstRenderData(state, payload.topSites) + break + } + + case types.GRID_SITES_DATA_UPDATED: { + state = gridSitesState + .gridSitesReducerDataUpdated(state, payload.gridSites) + break + } + + case types.GRID_SITES_TOGGLE_SITE_PINNED: { + state = gridSitesState + .gridSitesReducerToggleSitePinned(state, payload.pinnedSite) + break + } + + case types.GRID_SITES_REMOVE_SITE: { + state = gridSitesState + .gridSitesReducerRemoveSite(state, payload.removedSite) + break + } + + case types.GRID_SITES_UNDO_REMOVE_SITE: { + state = gridSitesState + .gridSitesReducerUndoRemoveSite(state) + break + } + + case types.GRID_SITES_UNDO_REMOVE_ALL_SITES: { + state = gridSitesState + .gridSitesReducerUndoRemoveAllSites(state) + break + } + + case types.GRID_SITES_UPDATE_SITE_BOOKMARK_INFO: { + state = gridSitesState + .gridSitesReducerUpdateSiteBookmarkInfo(state, payload.bookmarkInfo) + break + } + + case types.GRID_SITES_TOGGLE_SITE_BOOKMARK_INFO: { + state = gridSitesState + .gridSitesReducerToggleSiteBookmarkInfo( + state, + payload.url, + payload.bookmarkInfo + ) + break + } + + case types.GRID_SITES_ADD_SITES: { + state = gridSitesState.gridSitesReducerAddSiteOrSites(state, payload.site) + break + } + + case types.GRID_SITES_SHOW_SITE_REMOVED_NOTIFICATION: { + state = gridSitesState + .gridSitesReducerShowSiteRemovedNotification(state, payload.shouldShow) + break + } + } + + if (JSON.stringify(state) !== JSON.stringify(startingState)) { + storage.debouncedSave(state) + } + + return state +} + +export default gridSitesReducer diff --git a/components/brave_new_tab_ui/reducers/index.ts b/components/brave_new_tab_ui/reducers/index.ts index f7b2126fe35a..1bd133ca5d04 100644 --- a/components/brave_new_tab_ui/reducers/index.ts +++ b/components/brave_new_tab_ui/reducers/index.ts @@ -1,12 +1,15 @@ -/* 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/. */ +// Copyright (c) 2020 The Brave Authors. All rights reserved. +// 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/. import { combineReducers } from 'redux' -// Utils +// Reducers import newTabReducer from './new_tab_reducer' +import gridSitesReducer from './grid_sites_reducer' export default combineReducers({ - newTabData: newTabReducer + newTabData: newTabReducer, + gridSitesData: gridSitesReducer }) diff --git a/components/brave_new_tab_ui/reducers/new_tab_reducer.tsx b/components/brave_new_tab_ui/reducers/new_tab_reducer.ts similarity index 61% rename from components/brave_new_tab_ui/reducers/new_tab_reducer.tsx rename to components/brave_new_tab_ui/reducers/new_tab_reducer.ts index 42d3b9f7f540..262e5ae62082 100644 --- a/components/brave_new_tab_ui/reducers/new_tab_reducer.tsx +++ b/components/brave_new_tab_ui/reducers/new_tab_reducer.ts @@ -1,6 +1,7 @@ -/* 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/. */ +// Copyright (c) 2020 The Brave Authors. All rights reserved. +// 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/. import { Reducer } from 'redux' @@ -11,13 +12,10 @@ import { PrivateTabData } from '../api/privateTabData' // API import * as backgroundAPI from '../api/background' -import * as gridAPI from '../api/topSites/grid' import { InitialData, InitialRewardsData, PreInitialRewardsData } from '../api/initialData' -import * as bookmarksAPI from '../api/topSites/bookmarks' -import * as dndAPI from '../api/topSites/dnd' import { registerViewCount } from '../api/brandedWallpaper' import * as preferencesAPI from '../api/preferences' -import * as storage from '../storage' +import * as storage from '../storage/new_tab_storage' import { getTotalContributions } from '../rewards-utils' const initialState = storage.load() @@ -30,7 +28,7 @@ function performSideEffect (fn: SideEffectFunction): void { window.setTimeout(() => fn(sideEffectState), 0) } -export const newTabReducer: Reducer = (state: NewTab.State | undefined, action: any) => { +export const newTabReducer: Reducer = (state: NewTab.State | undefined, action) => { console.timeStamp('reducer ' + action.type) if (state === undefined) { console.timeStamp('reducer init') @@ -48,9 +46,8 @@ export const newTabReducer: Reducer = (state: NewTab.S initialDataLoaded: true, ...initialDataPayload.preferences, stats: initialDataPayload.stats, - ...initialDataPayload.privateTabData, - topSites: initialDataPayload.topSites, - brandedWallpaperData: initialDataPayload.brandedWallpaperData + brandedWallpaperData: initialDataPayload.brandedWallpaperData, + ...initialDataPayload.privateTabData } // TODO(petemill): only get backgroundImage if no sponsored background this time. // ...We would also have to set the value at the action @@ -60,18 +57,8 @@ export const newTabReducer: Reducer = (state: NewTab.S state.backgroundImage = backgroundAPI.randomBackgroundImage() } console.timeStamp('reducer initial data received') - // Assume 'top sites' data needs changing, so call 'calculate'. - // TODO(petemill): Starting another dispatch (which happens - // in `calculateGridSites`) before this reducer is finished - // is an anti-pattern and could introduce bugs. - // See for example the discussion at: - // https://stackoverflow.com/questions/36730793/can-i-dispatch-an-action-in-reducer - // This specific calculation would be better as a selector at - // UI render time. - // We at least schedule to run after the reducer has finished - // and the resulting new state is available. + performSideEffect(async function (state) { - gridAPI.calculateGridSites(state) if (!state.isIncognito) { try { await registerViewCount() @@ -81,160 +68,6 @@ export const newTabReducer: Reducer = (state: NewTab.S } }) break - case types.BOOKMARK_ADDED: - const topSite: NewTab.Site | undefined = state.topSites.find((site) => site.url === payload.url) - if (topSite) { - chrome.bookmarks.create({ - title: topSite.title, - url: topSite.url - }, () => { - bookmarksAPI.fetchBookmarkInfo(payload.url) - }) - } - break - case types.BOOKMARK_REMOVED: - const bookmarkInfo = state.bookmarks[payload.url] - if (bookmarkInfo) { - chrome.bookmarks.remove(bookmarkInfo.id, () => { - bookmarksAPI.fetchBookmarkInfo(payload.url) - }) - } - break - case types.NEW_TAB_SITE_PINNED: { - const topSiteIndex: number = state.topSites.findIndex((site) => site.url === payload.url) - const pinnedTopSite: NewTab.Site = Object.assign({}, state.topSites[topSiteIndex], { pinned: true }) - const pinnedTopSites: NewTab.Site[] = state.pinnedTopSites.slice() - - pinnedTopSite.index = topSiteIndex - pinnedTopSites.push(pinnedTopSite) - pinnedTopSites.sort((x, y) => x.index - y.index) - state = { - ...state, - pinnedTopSites - } - // Assume 'top sites' data needs changing, so call 'calculate'. - // TODO(petemill): Starting another dispatch (which happens - // in `calculateGridSites`) before this reducer is finished - // is an anti-pattern and could introduce bugs. This - // specific calculation would be better as a selector at - // UI render time. - // We at least schedule to run after the reducer has finished - // and the resulting new state is available. - performSideEffect((state) => { - gridAPI.calculateGridSites(state) - }) - break - } - - case types.NEW_TAB_SITE_UNPINNED: - const currentPositionIndex: number = state.pinnedTopSites.findIndex((site) => site.url === payload.url) - if (currentPositionIndex !== -1) { - const pinnedTopSites: NewTab.Site[] = state.pinnedTopSites.slice() - pinnedTopSites.splice(currentPositionIndex, 1) - state = { - ...state, - pinnedTopSites - } - } - // Assume 'top sites' data needs changing, so call 'calculate'. - // TODO(petemill): Starting another dispatch (which happens - // in `calculateGridSites`) before this reducer is finished - // is an anti-pattern and could introduce bugs. This - // specific calculation would be better as a selector at - // UI render time. - // We at least schedule to run after the reducer has finished - // and the resulting new state is available. - performSideEffect((state) => { - gridAPI.calculateGridSites(state) - }) - break - - case types.NEW_TAB_SITE_IGNORED: { - const topSiteIndex: number = state.topSites.findIndex((site) => site.url === payload.url) - const ignoredTopSites: NewTab.Site[] = state.ignoredTopSites.slice() - ignoredTopSites.push(state.topSites[topSiteIndex]) - state = { - ...state, - ignoredTopSites, - showSiteRemovalNotification: true - } - // Assume 'top sites' data needs changing, so call 'calculate'. - // TODO(petemill): Starting another dispatch (which happens - // in `calculateGridSites`) before this reducer is finished - // is an anti-pattern and could introduce bugs. This - // specific calculation would be better as a selector at - // UI render time. - // We at least schedule to run after the reducer has finished - // and the resulting new state is available. - performSideEffect((state) => { - gridAPI.calculateGridSites(state) - }) - break - } - - case types.NEW_TAB_UNDO_SITE_IGNORED: { - const ignoredTopSites: NewTab.Site[] = state.ignoredTopSites.slice() - ignoredTopSites.pop() - state = { - ...state, - ignoredTopSites, - showSiteRemovalNotification: false - } - // Assume 'top sites' data needs changing, so call 'calculate'. - // TODO(petemill): Starting another dispatch (which happens - // in `calculateGridSites`) before this reducer is finished - // is an anti-pattern and could introduce bugs. This - // specific calculation would be better as a selector at - // UI render time. - // We at least schedule to run after the reducer has finished - // and the resulting new state is available. - performSideEffect((state) => { - gridAPI.calculateGridSites(state) - }) - break - } - - case types.NEW_TAB_UNDO_ALL_SITE_IGNORED: - state = { - ...state, - ignoredTopSites: [], - showSiteRemovalNotification: false - } - // Assume 'top sites' data needs changing, so call 'calculate'. - // TODO(petemill): Starting another dispatch (which happens - // in `calculateGridSites`) before this reducer is finished - // is an anti-pattern and could introduce bugs. This - // specific calculation would be better as a selector at - // UI render time. - // We at least schedule to run after the reducer has finished - // and the resulting new state is available. - performSideEffect((state) => { - gridAPI.calculateGridSites(state) - }) - break - - case types.NEW_TAB_HIDE_SITE_REMOVAL_NOTIFICATION: - state = { - ...state, - showSiteRemovalNotification: false - } - break - - case types.NEW_TAB_SITE_DRAGGED: - state = dndAPI.onDraggedSite(state, payload.fromUrl, payload.toUrl) - break - - case types.NEW_TAB_SITE_DRAG_END: - state = dndAPI.onDragEnd(state) - break - - case types.NEW_TAB_BOOKMARK_INFO_AVAILABLE: - state = bookmarksAPI.updateBookmarkInfo(state, payload.queryUrl, payload.bookmarkTreeNode) - break - - case types.NEW_TAB_GRID_SITES_UPDATED: - state = { ...state, gridSites: payload.gridSites } - break case types.NEW_TAB_STATS_UPDATED: const stats: Stats = payload.stats diff --git a/components/brave_new_tab_ui/state/gridSitesState.ts b/components/brave_new_tab_ui/state/gridSitesState.ts new file mode 100644 index 000000000000..f4b1439cf8c1 --- /dev/null +++ b/components/brave_new_tab_ui/state/gridSitesState.ts @@ -0,0 +1,237 @@ +// Copyright (c) 2020 The Brave Authors. All rights reserved. +// 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/. + +import { + generateGridSiteProperties, + isExistingGridSite, + getGridSitesWhitelist, + isGridSitePinned, + isGridSiteBookmarked, + filterFromExcludedSites +} from '../helpers/newTabUtils' + +export function gridSitesReducerSetFirstRenderDataFromLegacy ( + state: NewTab.GridSitesState, + legacyState: NewTab.LegacyState | undefined +) { + if (legacyState === undefined) { + return state + } + const { ignoredTopSites, pinnedTopSites } = legacyState + + if (ignoredTopSites.length > 0) { + for (const ignoredTopSite of ignoredTopSites) { + state = gridSitesReducerRemoveSite(state, ignoredTopSite) + } + } + + if (pinnedTopSites.length > 0) { + state = gridSitesReducerAddSiteOrSites(state, pinnedTopSites) + } + return state +} + +export function gridSitesReducerSetFirstRenderData ( + state: NewTab.GridSitesState, + topSites: chrome.topSites.MostVisitedURL[] +): NewTab.GridSitesState { + const gridSitesWhitelist = getGridSitesWhitelist(topSites) + const newGridSites: NewTab.Site[] = [] + for (const [index, topSite] of gridSitesWhitelist.entries()) { + if (isExistingGridSite(state.gridSites, topSite)) { + // If topSite from Chromium exists in our gridSites list, + // skip and iterate over the next item. + continue + } + newGridSites.push(generateGridSiteProperties(index, topSite)) + } + + // If there are removed sites coming from a legacy storage, + // ensure they get filtered. + const sitesToAdd = filterFromExcludedSites(newGridSites, state.removedSites) + state = gridSitesReducerAddSiteOrSites(state, sitesToAdd) + + return state +} + +export function gridSitesReducerDataUpdated ( + state: NewTab.GridSitesState, + sitesData: NewTab.Site[] +): NewTab.GridSitesState { + let updatedGridSites: NewTab.Site[] = [] + let isolatedPinnedSites: NewTab.Site[] = [] + + // Separate pinned sites from un-pinned sites. This step is needed + // since the list length is unknown, so pinned items need the updated + // list to be full before looking for its index. + // See test "preserve pinnedIndex positions after random reordering" + for (const site of sitesData) { + if (site.pinnedIndex !== undefined) { + isolatedPinnedSites.push(site) + } else { + updatedGridSites.push(site) + } + } + // Get the pinned site and add it to the index specified by pinnedIndex. + // all items after it will be pushed one index up. + for (const pinnedSite of isolatedPinnedSites) { + if (pinnedSite.pinnedIndex !== undefined) { + updatedGridSites.splice(pinnedSite.pinnedIndex, 0, pinnedSite) + } + } + + state = { ...state, gridSites: updatedGridSites } + return state +} + +export function gridSitesReducerToggleSitePinned ( + state: NewTab.GridSitesState, + pinnedSite: NewTab.Site +): NewTab.GridSitesState { + const updatedGridSites: NewTab.Site[] = [] + for (const [index, gridSite] of state.gridSites.entries()) { + if (gridSite.url === pinnedSite.url) { + updatedGridSites.push({ + ...gridSite, + pinnedIndex: isGridSitePinned(gridSite) ? undefined : index + }) + } else { + updatedGridSites.push(gridSite) + } + } + state = gridSitesReducerDataUpdated(state, updatedGridSites) + return state +} + +export function gridSitesReducerRemoveSite ( + state: NewTab.GridSitesState, + removedSite: NewTab.Site +): NewTab.GridSitesState { + state = { + ...state, + removedSites: [ ...state.removedSites, removedSite ] + } + + const filterRemovedFromGridSites = + filterFromExcludedSites(state.gridSites, state.removedSites) + state = gridSitesReducerDataUpdated(state, filterRemovedFromGridSites) + return state +} + +export function gridSitesReducerUndoRemoveSite ( + state: NewTab.GridSitesState +): NewTab.GridSitesState { + if (state.removedSites.length < 0) { + return state + } + + // Remove and modify removed list + const removedItem: NewTab.Site | undefined = state.removedSites.pop() + + if ( + removedItem === undefined || + isExistingGridSite(state.gridSites, removedItem) + ) { + return state + } + + // Push item back into the grid list by adding the site + state = gridSitesReducerAddSiteOrSites(state, removedItem) + return state +} + +export function gridSitesReducerUndoRemoveAllSites ( + state: NewTab.GridSitesState +): NewTab.GridSitesState { + // Get all removed sites, assuming the are unique to gridSites + const allRemovedSites: NewTab.Site[] = state.removedSites + .filter((site: NewTab.Site) => !isExistingGridSite(state.gridSites, site)) + + // Remove all removed sites from the removed list + state = { ...state, removedSites: [] } + + // Put them back into grid + state = gridSitesReducerAddSiteOrSites(state, allRemovedSites) + return state +} + +export const gridSitesReducerUpdateSiteBookmarkInfo = ( + state: NewTab.GridSitesState, + bookmarkInfo: chrome.bookmarks.BookmarkTreeNode +): NewTab.GridSitesState => { + const updatedGridSites: NewTab.Site[] = [] + for (const [index, gridSite] of state.gridSites.entries()) { + const updatedBookmarkTreeNode = bookmarkInfo[index] + if ( + updatedBookmarkTreeNode !== undefined && + gridSite.url === updatedBookmarkTreeNode.url + ) { + updatedGridSites.push({ + ...gridSite, + bookmarkInfo: updatedBookmarkTreeNode + }) + } else { + updatedGridSites.push(gridSite) + } + } + state = gridSitesReducerDataUpdated(state, updatedGridSites) + return state +} + +export const gridSitesReducerToggleSiteBookmarkInfo = ( + state: NewTab.GridSitesState, + url: string, + bookmarkInfo: chrome.bookmarks.BookmarkTreeNode +): NewTab.GridSitesState => { + const updatedGridSites: NewTab.Site[] = [] + for (const gridSite of state.gridSites) { + if (url === gridSite.url) { + updatedGridSites.push({ + ...gridSite, + bookmarkInfo: isGridSiteBookmarked(bookmarkInfo) + ? undefined + // Add a transitory state for bookmarks. + // This will be overriden by a new mount and is used + // as a secondary render until data is ready, + : { title: gridSite.title, id: 'TEMPORARY' } + }) + } else { + updatedGridSites.push(gridSite) + } + } + state = gridSitesReducerDataUpdated(state, updatedGridSites) + return state +} + +export function gridSitesReducerAddSiteOrSites ( + state: NewTab.GridSitesState, + addedSites: NewTab.Site[] | NewTab.Site +): NewTab.GridSitesState { + const sitesToAdd: NewTab.Site[] = Array.isArray(addedSites) + ? addedSites + : [addedSites] + + if (sitesToAdd.length === 0) { + return state + } + const currentGridSitesWithNewItems: NewTab.Site[] = [ + // The order here is important: ensure recently added items + // come first so users can see it when grid is full. This is + // also useful to undo a site removal, which would re-populate + // the grid in the first positions. + ...sitesToAdd, + ...state.gridSites + ] + state = gridSitesReducerDataUpdated(state, currentGridSitesWithNewItems) + return state +} + +export function gridSitesReducerShowSiteRemovedNotification ( + state: NewTab.GridSitesState, + shouldShow: boolean +): NewTab.GridSitesState { + state = { ...state, shouldShowSiteRemovedNotification: shouldShow } + return state +} diff --git a/components/brave_new_tab_ui/storage/grid_sites_storage.ts b/components/brave_new_tab_ui/storage/grid_sites_storage.ts new file mode 100644 index 000000000000..155efe80f9c1 --- /dev/null +++ b/components/brave_new_tab_ui/storage/grid_sites_storage.ts @@ -0,0 +1,67 @@ +// Copyright (c) 2020 The Brave Authors. All rights reserved. +// 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/. + +// Utils +import { debounce } from '../../common/debounce' +import { keyName as newTabKeyName } from './new_tab_storage' +import { generateGridSitesFromLegacyEntries } from '../helpers/newTabUtils' +const keyName = 'grid-sites-data-v1' + +const newTabData: any = window.localStorage.getItem(newTabKeyName) +const parsedNewTabData = JSON.parse(newTabData) + +const getNewTabData = () => { + if (parsedNewTabData == null) { + return { + pinnedTopSites: [], + ignoredTopSites: [] + } + } + return parsedNewTabData +} + +export const initialGridSitesState: NewTab.GridSitesState = { + gridSites: [], + removedSites: [], + shouldShowSiteRemovedNotification: false, + legacy: { + // Store legacy pinnedTopSites so users + // migrating to this new storage won't lose + // data. Once this change hits the release channel + // we are safe to remove this bridge + pinnedTopSites: generateGridSitesFromLegacyEntries( + getNewTabData().pinnedTopSites + ), + ignoredTopSites: generateGridSitesFromLegacyEntries( + getNewTabData().ignoredTopSites + ) + } +} + +export const load = (): NewTab.GridSitesState => { + const data: string | null = window.localStorage.getItem(keyName) + let state = initialGridSitesState + let storedState: NewTab.GridSitesState + + if (data) { + try { + storedState = JSON.parse(data) + // add defaults for non-peristant data + state = { + ...state, + ...storedState + } + } catch (e) { + console.error('[GridSitesData] Could not parse local storage data: ', e) + } + } + return state +} + +export const debouncedSave = debounce((data: NewTab.GridSitesState) => { + if (data) { + window.localStorage.setItem(keyName, JSON.stringify(data)) + } +}, 50) diff --git a/components/brave_new_tab_ui/storage.ts b/components/brave_new_tab_ui/storage/new_tab_storage.ts similarity index 69% rename from components/brave_new_tab_ui/storage.ts rename to components/brave_new_tab_ui/storage/new_tab_storage.ts index 4d7f1f45fc82..3426d0ca14b5 100644 --- a/components/brave_new_tab_ui/storage.ts +++ b/components/brave_new_tab_ui/storage/new_tab_storage.ts @@ -1,13 +1,14 @@ -/* 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/. */ +// Copyright (c) 2020 The Brave Authors. All rights reserved. +// 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/. // Utils -import { debounce } from '../common/debounce' +import { debounce } from '../../common/debounce' -const keyName = 'new-tab-data' +export const keyName = 'new-tab-data' -const defaultState: NewTab.State = { +export const defaultState: NewTab.State = { initialDataLoaded: false, textDirection: window.loadTimeData.getString('textdirection'), featureFlagBraveNTPBrandedWallpaper: window.loadTimeData.getBoolean('featureFlagBraveNTPBrandedWallpaper'), @@ -19,16 +20,11 @@ const defaultState: NewTab.State = { showBinance: false, brandedWallpaperOptIn: false, isBrandedWallpaperNotificationDismissed: true, - topSites: [], - ignoredTopSites: [], - pinnedTopSites: [], - gridSites: [], showEmptyPage: false, isIncognito: chrome.extension.inIncognitoContext, useAlternativePrivateSearchEngine: false, isTor: false, isQwant: false, - bookmarks: {}, stats: { adsBlockedStat: 0, javascriptBlockedStat: 0, @@ -69,24 +65,6 @@ if (chrome.extension.inIncognitoContext) { defaultState.isQwant = window.loadTimeData.getBoolean('isQwant') } -const getPersistentData = (state: NewTab.State): NewTab.PersistentState => { - // Don't save items which we aren't the source - // of data for. - const peristantState: NewTab.PersistentState = { - topSites: state.topSites, - ignoredTopSites: state.ignoredTopSites, - pinnedTopSites: state.pinnedTopSites, - gridSites: state.gridSites, - showEmptyPage: state.showEmptyPage, - bookmarks: state.bookmarks, - rewardsState: state.rewardsState, - currentStackWidget: state.currentStackWidget, - binanceState: state.binanceState - } - - return peristantState -} - const cleanData = (state: NewTab.State) => { // We need to disable linter as we defined in d.ts that this values are number, // but we need this check to covert from old version to a new one @@ -106,7 +84,8 @@ const cleanData = (state: NewTab.State) => { export const load = (): NewTab.State => { const data: string | null = window.localStorage.getItem(keyName) let state = defaultState - let storedState: NewTab.PersistentState + let storedState + if (data) { try { storedState = JSON.parse(data) @@ -116,7 +95,7 @@ export const load = (): NewTab.State => { ...storedState } } catch (e) { - console.error('Could not parse local storage data: ', e) + console.error('[NewTabData] Could not parse local storage data: ', e) } } return cleanData(state) @@ -124,7 +103,12 @@ export const load = (): NewTab.State => { export const debouncedSave = debounce((data: NewTab.State) => { if (data) { - const dataToSave = getPersistentData(data) + const dataToSave = { + showEmptyPage: data.showEmptyPage, + rewardsState: data.rewardsState, + binanceState: data.binanceState, + currentStackWidget: data.currentStackWidget + } window.localStorage.setItem(keyName, JSON.stringify(dataToSave)) } }, 50) diff --git a/components/brave_new_tab_ui/store.ts b/components/brave_new_tab_ui/store.ts index ab32f65ee84e..1488da172606 100644 --- a/components/brave_new_tab_ui/store.ts +++ b/components/brave_new_tab_ui/store.ts @@ -1,10 +1,15 @@ -/* 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/. */ +// Copyright (c) 2020 The Brave Authors. All rights reserved. +// 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/. -import { createStore } from 'redux' +// Redux API +import { createStore, applyMiddleware } from 'redux' -// Utils -import reducers from './reducers' +// Thunk for async actions +import thunk from 'redux-thunk' -export default createStore(reducers) +// Feature core reducer +import rootReducer from './reducers' + +export default createStore(rootReducer, applyMiddleware(thunk)) diff --git a/components/brave_new_tab_ui/stories/default.tsx b/components/brave_new_tab_ui/stories/default.tsx index 69139700dc8d..0d8ff344bc97 100644 --- a/components/brave_new_tab_ui/stories/default.tsx +++ b/components/brave_new_tab_ui/stories/default.tsx @@ -12,9 +12,10 @@ import BraveCoreThemeProvider from '../../common/BraveCoreThemeProvider' // Components import NewTabPage from '../containers/newTab' -import * as actions from '../actions/new_tab_actions' +import * as newTabActions from '../actions/new_tab_actions' +import * as gridSitesActions from '../actions/grid_sites_actions' import store from '../store' -import { getNewTabData } from './default/data/storybookState' +import { getNewTabData, getGridSitesData } from './default/data/storybookState' export default function Provider ({ story }: any) { return ( @@ -39,10 +40,13 @@ storiesOf('New Tab/Containers', module) .addDecorator(story => ) .add('Default', () => { const doNothing = (value: boolean) => value + const newTabData = getNewTabData(store.getState().newTabData) + const gridSitesData = getGridSitesData(store.getState().gridSitesData) return ( ({ +export const getNewTabData = (state: NewTab.State = defaultState) => ({ ...state, - brandedWallpaperData: shouldShowBrandedWallpaperData(boolean('Show branded background image?', true)), - backgroundImage: select('Background image', generateStaticImages(images), generateStaticImages(images)['SpaceX']), + brandedWallpaperData: shouldShowBrandedWallpaperData( + boolean('Show branded background image?', true) + ), + backgroundImage: select( + 'Background image', + generateStaticImages(images), + generateStaticImages(images)['SpaceX'] + ), showBackgroundImage: boolean('Show background image?', true), showStats: boolean('Show stats?', true), showClock: boolean('Show clock?', true), @@ -55,7 +61,6 @@ export const getNewTabData = (state: NewTab.State) => ({ showRewards: boolean('Show rewards?', true), showBinance: boolean('Show Binance?', true), textDirection: select('Text direction', { ltr: 'ltr', rtl: 'rtl' } , 'ltr'), - gridSites: generateTopSites(defaultTopSitesData), stats: { ...state.stats, adsBlockedStat: number('Number of blocked items', 1337), @@ -64,3 +69,10 @@ export const getNewTabData = (state: NewTab.State) => ({ initialDataLoaded: true, currentStackWidget: 'rewards' as NewTab.StackWidget }) + +export const getGridSitesData = ( + state: NewTab.GridSitesState = initialGridSitesState +) => ({ + ...state, + gridSites: generateTopSites(defaultTopSitesData) +}) diff --git a/components/definitions/newTab.d.ts b/components/definitions/newTab.d.ts index 5817756841e5..ff8ac88d1cdd 100644 --- a/components/definitions/newTab.d.ts +++ b/components/definitions/newTab.d.ts @@ -18,6 +18,7 @@ declare namespace NewTab { } export interface ApplicationState { newTabData: State | undefined + gridSitesData: GridSitesState | undefined } export interface Image { @@ -30,6 +31,18 @@ declare namespace NewTab { } export interface Site { + id: string + url: string + title: string + favicon: string + letter: string + pinnedIndex: number | undefined + bookmarkInfo: chrome.bookmarks.BookmarkTreeNode | undefined + } + + // This is preserved for migration reasons. + // Do not tyoe new code using this interface. + export interface LegacySite { index: number url: string title: string @@ -60,13 +73,29 @@ declare namespace NewTab { export type StackWidget = 'rewards' | 'binance' - export interface PersistentState { - topSites: Site[] - ignoredTopSites: Site[] + export interface LegacyState { pinnedTopSites: Site[] + ignoredTopSites: Site[] + } + + export interface GridSitesState { + removedSites: Site[] gridSites: Site[] + shouldShowSiteRemovedNotification: boolean + legacy: LegacyState + } + + export interface PageState { + showEmptyPage: boolean + } + + export interface RewardsState { + rewardsState: RewardsWidgetState + currentStackWidget: StackWidget + } + + export interface PersistentState { showEmptyPage: boolean - bookmarks: Record rewardsState: RewardsWidgetState currentStackWidget: StackWidget binanceState: BinanceWidgetState @@ -82,7 +111,7 @@ declare namespace NewTab { isQwant: boolean backgroundImage?: Image gridLayoutSize?: 'small' - showSiteRemovalNotification?: boolean + showGridSiteRemovedNotification?: boolean showBackgroundImage: boolean showStats: boolean showClock: boolean diff --git a/components/test/brave_new_tab_ui/actions/new_tab_actions_test.ts b/components/test/brave_new_tab_ui/actions/new_tab_actions_test.ts deleted file mode 100644 index 613897612c25..000000000000 --- a/components/test/brave_new_tab_ui/actions/new_tab_actions_test.ts +++ /dev/null @@ -1,164 +0,0 @@ -/* 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/. */ - -import { types } from '../../../brave_new_tab_ui/constants/new_tab_types' -import { Stats } from '../../../brave_new_tab_ui/api/stats' -import { Preferences } from '../../../brave_new_tab_ui/api/preferences' -import * as actions from '../../../brave_new_tab_ui/actions/new_tab_actions' - -describe('newTabActions', () => { - // TODO(petemill): We possibly don't need a test for every action to - // just to check that the actions are passing their payloads correctly. - // These aren't valid tests to make sure the reducer expects what it gets - // since we can change the payload signature here and in the actions, - // and still get an error in the reducer. It's perhaps more useful to get - // a build time error by using Typescript types in the reducer for each - // action payload. - // https://redux.js.org/recipes/usage-with-typescript - it('bookmarkAdded', () => { - const url: string = 'https://brave.com' - expect(actions.bookmarkAdded(url)).toEqual({ - meta: undefined, - type: types.BOOKMARK_ADDED, - payload: { url } - }) - }) - it('bookmarkRemoved', () => { - const url: string = 'https://brave.com' - expect(actions.bookmarkRemoved(url)).toEqual({ - meta: undefined, - type: types.BOOKMARK_REMOVED, - payload: { url } - }) - }) - it('sitePinned', () => { - const url: string = 'https://brave.com' - expect(actions.sitePinned(url)).toEqual({ - meta: undefined, - type: types.NEW_TAB_SITE_PINNED, - payload: { url } - }) - }) - it('siteUnpinned', () => { - const url: string = 'https://brave.com' - expect(actions.siteUnpinned(url)).toEqual({ - meta: undefined, - type: types.NEW_TAB_SITE_UNPINNED, - payload: { url } - }) - }) - it('siteIgnored', () => { - const url: string = 'https://brave.com' - expect(actions.siteIgnored(url)).toEqual({ - meta: undefined, - type: types.NEW_TAB_SITE_IGNORED, - payload: { url } - }) - }) - it('undoSiteIgnored', () => { - const url: string = 'https://brave.com' - expect(actions.undoSiteIgnored(url)).toEqual({ - meta: undefined, - type: types.NEW_TAB_UNDO_SITE_IGNORED, - payload: { url } - }) - }) - it('undoAllSiteIgnored', () => { - const url: string = 'https://brave.com' - expect(actions.undoAllSiteIgnored(url)).toEqual({ - meta: undefined, - type: types.NEW_TAB_UNDO_ALL_SITE_IGNORED, - payload: { url } - }) - }) - it('siteDragged', () => { - const fromUrl: string = 'https://brave.com' - const toUrl: string = 'https://wikipedia.org' - const dragRight: boolean = true - expect(actions.siteDragged(fromUrl, toUrl, dragRight)).toEqual({ - meta: undefined, - type: types.NEW_TAB_SITE_DRAGGED, - payload: { fromUrl, toUrl, dragRight } - }) - }) - it('siteDragEnd', () => { - const url: string = 'https://brave.com' - const didDrop: boolean = false - expect(actions.siteDragEnd(url, didDrop)).toEqual({ - meta: undefined, - type: types.NEW_TAB_SITE_DRAG_END, - payload: { url, didDrop } - }) - }) - it('onHideSiteRemovalNotification', () => { - expect(actions.onHideSiteRemovalNotification()).toEqual({ - meta: undefined, - type: types.NEW_TAB_HIDE_SITE_REMOVAL_NOTIFICATION - }) - }) - it('bookmarkInfoAvailable', () => { - const queryUrl: string = 'https://brave.com' - const bookmarkTreeNode = { - dateAdded: 1557899510259, - id: '7', - index: 0, - parentId: '2', - title: 'Secure, Fast & Private Web Browser with Adblocker | Brave Browser', - url: 'http://brave.com/' - } - expect(actions.bookmarkInfoAvailable(queryUrl, bookmarkTreeNode)).toEqual({ - meta: undefined, - type: types.NEW_TAB_BOOKMARK_INFO_AVAILABLE, - payload: { queryUrl, bookmarkTreeNode } - }) - }) - it('gridSitesUpdated', () => { - const gridSites: Array = [ - { - bookmarked: undefined, - favicon: 'chrome://favicon/size/64@1x/http://brave.com/', - index: 0, - letter: 'B', - pinned: true, - thumb: 'chrome://thumb/http://brave.com/', - title: 'Secure, Fast & Private Web Browser with Adblocker | Brave Browser', - url: 'http://brave.com/' - } - ] - expect(actions.gridSitesUpdated(gridSites)).toEqual({ - meta: undefined, - type: types.NEW_TAB_GRID_SITES_UPDATED, - payload: { gridSites } - }) - }) - it('statsUpdated', () => { - const stats: Stats = { - adsBlockedStat: 1, - fingerprintingBlockedStat: 2, - httpsUpgradesStat: 3, - javascriptBlockedStat: 4, - trackersBlockedStat: 5 - } - expect(actions.statsUpdated(stats)).toEqual({ - meta: undefined, - payload: { - stats - }, - type: types.NEW_TAB_STATS_UPDATED - }) - }) - it('preferencesUpdated', () => { - const preferences: Preferences = { - showBackgroundImage: false, - showStats: false, - showClock: false, - showTopSites: false - } - expect(actions.preferencesUpdated(preferences)).toEqual({ - meta: undefined, - type: types.NEW_TAB_PREFERENCES_UPDATED, - payload: preferences - }) - }) -}) diff --git a/components/test/brave_new_tab_ui/api/data_test.ts b/components/test/brave_new_tab_ui/api/data_test.ts index 47299ed4b6b2..cda291f15d43 100644 --- a/components/test/brave_new_tab_ui/api/data_test.ts +++ b/components/test/brave_new_tab_ui/api/data_test.ts @@ -4,19 +4,21 @@ import getActions from '../../../brave_new_tab_ui/api/getActions' import { getTopSites } from '../../../brave_new_tab_ui/api/topSites' -import * as actions from '../../../brave_new_tab_ui/actions/new_tab_actions' -import { types } from '../../../brave_new_tab_ui/constants/new_tab_types' +import * as newTabActions from '../../../brave_new_tab_ui/actions/new_tab_actions' +import * as topSitesActions from '../../../brave_new_tab_ui/actions/grid_sites_actions' +import { types as topSitesTypes } from '../../../brave_new_tab_ui/constants/grid_sites_types' describe('new tab data api tests', () => { describe('getActions', () => { it('returns an object with the same keys mimicking the original new tab actions', () => { const assertion = getActions() + const actions = Object.assign({}, newTabActions, topSitesActions) expect(Object.keys(assertion)).toEqual(Object.keys(actions)) }) it('can call an action from getActions', () => { - expect(getActions().onHideSiteRemovalNotification()).toEqual({ - payload: undefined, - type: types.NEW_TAB_HIDE_SITE_REMOVAL_NOTIFICATION + expect(getActions().showGridSiteRemovedNotification(true)).toEqual({ + payload: { shouldShow: true }, + type: topSitesTypes.GRID_SITES_SHOW_SITE_REMOVED_NOTIFICATION }) }) }) diff --git a/components/test/brave_new_tab_ui/api/topSites/bookmarks_test.ts b/components/test/brave_new_tab_ui/api/topSites/bookmarks_test.ts deleted file mode 100644 index 0668c52425ec..000000000000 --- a/components/test/brave_new_tab_ui/api/topSites/bookmarks_test.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* 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/. */ - -import * as bookmarksAPI from '../../../../brave_new_tab_ui/api/topSites/bookmarks' - -const state = { - backgroundImage: {}, - bookmarks: {}, - gridSites: [], - ignoredTopSites: [], - isIncognito: false, - isQwant: false, - isTor: false, - pinnedTopSites: [], - showEmptyPage: false, - showSiteRemovalNotification: false, - stats: {}, - topSites: [], - useAlternativePrivateSearchEngine: false -} - -describe('new tab bookmarks api tests', () => { - describe('fetchBookmarkInfo', () => { - let spy: jest.SpyInstance - const url = 'https://brave.com' - beforeEach(() => { - spy = jest.spyOn(chrome.bookmarks, 'search') - }) - afterEach(() => { - spy.mockRestore() - }) - it('calls chrome.bookmarks.search', () => { - bookmarksAPI.fetchBookmarkInfo(url) - expect(spy).toBeCalled() - }) - }) - - describe('updateBookmarkInfo', () => { - const url = 'https://brave.com' - it('bookmarks the url if bookmark has a tree node', () => { - const updateBookmarkInfo = bookmarksAPI.updateBookmarkInfo(state, url, true) - const assertion = updateBookmarkInfo.bookmarks - expect(assertion).toEqual({ 'https://brave.com': true }) - }) - it('sets bookmark to undefined if tree node is not defined', () => { - const updateBookmarkInfo = bookmarksAPI.updateBookmarkInfo(state, url, false) - const assertion = updateBookmarkInfo.bookmarks - expect(assertion).toEqual({ 'https://brave.com': undefined }) - }) - }) -}) diff --git a/components/test/brave_new_tab_ui/api/topSites/grid_test.ts b/components/test/brave_new_tab_ui/api/topSites/grid_test.ts deleted file mode 100644 index 0b8ed76e427c..000000000000 --- a/components/test/brave_new_tab_ui/api/topSites/grid_test.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* 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/. */ - -import { getGridSites } from '../../../../brave_new_tab_ui/api/topSites/grid' - -describe('new tab grid api tests', () => { - const defaultState = { - topSites: [], - ignoredTopSites: [], - pinnedTopSites: [], - bookmarks: {} - } - describe('getGridSites', () => { - it('allows http sites', () => { - const url = 'http://cezaraugusto.net' - const newState = { - ...defaultState, - topSites: [ - { url } - ] - } - const assertion = getGridSites(newState, true) - expect(assertion[0]).toBe(newState.topSites[0]) - expect(assertion[0].url).toBe(url) - }) - it('allows https sites', () => { - const url = 'https://cezaraugusto.net' - const newState = { - ...defaultState, - topSites: [ - { url } - ] - } - const assertion = getGridSites(newState, true) - expect(assertion[0]).toBe(newState.topSites[0]) - expect(assertion[0].url).toBe(url) - }) - it('do not allow the default chrome topSites url', () => { - const url = 'https://chrome.google.com/webstore?hl=en' - const newState = { - ...defaultState, - topSites: [ - { url } - ] - } - const assertion = getGridSites(newState, true) - expect(assertion[0]).toBe(undefined) - }) - }) -}) diff --git a/components/test/brave_new_tab_ui/helpers/newTabUtils_test.ts b/components/test/brave_new_tab_ui/helpers/newTabUtils_test.ts index 2b1f58ce03df..e94627c52f23 100644 --- a/components/test/brave_new_tab_ui/helpers/newTabUtils_test.ts +++ b/components/test/brave_new_tab_ui/helpers/newTabUtils_test.ts @@ -1,73 +1,279 @@ -/* 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/. */ +// Copyright (c) 2020 The Brave Authors. All rights reserved. +// 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/. -import { isHttpOrHttps, getCharForSite } from '../../../brave_new_tab_ui/helpers/newTabUtils' +import * as newTabUtils from '../../../brave_new_tab_ui/helpers/newTabUtils' describe('new tab util files tests', () => { describe('isHttpOrHttps', () => { it('matches http when defined as a protocol type', () => { const url = 'http://some-boring-unsafe-website.com' - expect(isHttpOrHttps(url)).toBe(true) + expect(newTabUtils.isHttpOrHttps(url)).toBe(true) }) it('matches https when defined as a protocol type', () => { const url = 'https://some-nice-safe-website.com' - expect(isHttpOrHttps(url)).toBe(true) + expect(newTabUtils.isHttpOrHttps(url)).toBe(true) }) it('does not match http when defined as an origin', () => { const url = 'file://http.some-website-tricking-you.com' - expect(isHttpOrHttps(url)).toBe(false) + expect(newTabUtils.isHttpOrHttps(url)).toBe(false) }) it('does not match https when defined as an origin', () => { const url = 'file://https.some-website-tricking-you.com' - expect(isHttpOrHttps(url)).toBe(false) + expect(newTabUtils.isHttpOrHttps(url)).toBe(false) }) it('does not match other protocol', () => { const url = 'ftp://some-old-website.com' - expect(isHttpOrHttps(url)).toBe(false) + expect(newTabUtils.isHttpOrHttps(url)).toBe(false) }) it('does not match when url is not defined', () => { const url = undefined - expect(isHttpOrHttps(url)).toBe(false) + expect(newTabUtils.isHttpOrHttps(url)).toBe(false) }) it('matches uppercase http', () => { const url = 'HTTP://SCREAMING-SAFE-WEBSITE.COM' - expect(isHttpOrHttps(url)).toBe(true) + expect(newTabUtils.isHttpOrHttps(url)).toBe(true) }) it('matches uppercase https', () => { const url = 'HTTP://SCREAMING-UNSAFE-WEBSITE.COM' - expect(isHttpOrHttps(url)).toBe(true) + expect(newTabUtils.isHttpOrHttps(url)).toBe(true) }) }) describe('getCharForSite', () => { it('returns the first letter of a given URL without subdomains', () => { - const url = { url: 'https://brave.com' } - expect(getCharForSite(url)).toBe('B') + const url: chrome.topSites.MostVisitedURL = { + url: 'https://brave.com', + title: 'brave' + } + expect(newTabUtils.getCharForSite(url)).toBe('B') }) it('returns the first letter of a given URL with subdomains', () => { - const url = { url: 'https://awesome-sub-domain.brave.com' } - expect(getCharForSite(url)).toBe('A') + const url: chrome.topSites.MostVisitedURL = { + url: 'https://awesome-sub-domain.brave.com', + title: 'awesome' + } + expect(newTabUtils.getCharForSite(url)).toBe('A') }) it('returns the first letter of a given URL with ports', () => { - const url = { url: 'https://brave.com:9999' } - expect(getCharForSite(url)).toBe('B') + const url: chrome.topSites.MostVisitedURL = { + url: 'https://brave.com:9999', + title: 'brave' + } + expect(newTabUtils.getCharForSite(url)).toBe('B') }) it('returns the first letter of a given URL with paths', () => { - const url = { url: 'https://brave.com/hello-test/' } - expect(getCharForSite(url)).toBe('B') + const url: chrome.topSites.MostVisitedURL = { + url: 'https://brave.com/hello-test/', title: 'brave' + } + expect(newTabUtils.getCharForSite(url)).toBe('B') }) it('returns the first letter of a given URL with queries', () => { - const url = { url: 'https://brave.com/?randomId' } - expect(getCharForSite(url)).toBe('B') + const url: chrome.topSites.MostVisitedURL = { + url: 'https://brave.com/?randomId', + title: 'brave' + } + expect(newTabUtils.getCharForSite(url)).toBe('B') }) it('returns the first letter of a given URL with parameters', () => { - const url = { url: 'https://brave.com/?randomId=123123123' } - expect(getCharForSite(url)).toBe('B') + const url: chrome.topSites.MostVisitedURL = { + url: 'https://brave.com/?randomId=123123123', + title: 'brave' + } + expect(newTabUtils.getCharForSite(url)).toBe('B') }) it('returns the first letter of a given URL with fragments', () => { - const url = { url: 'https://brave.com/?randomId=123123123&hl=en#00h00m10s' } - expect(getCharForSite(url)).toBe('B') + const url: chrome.topSites.MostVisitedURL = { + url: 'https://brave.com/?randomId=123123123&hl=en#00h00m10s', + title: 'brave' + } + expect(newTabUtils.getCharForSite(url)).toBe('B') + }) + }) + + describe('generateGridSiteId', () => { + it('returns the id with the correct structure', () => { + // Test via startsWith to avoid calling Date.now() which + // will often fail all tests + const assertion: string = newTabUtils.generateGridSiteId() + expect(assertion.startsWith(`topsite-`)) + .toBe(true) + }) + }) + + describe('generateGridSiteFavicon', () => { + it('returns the correct schema for favicons', () => { + const url: string = 'https://brave.com' + expect(newTabUtils.generateGridSiteFavicon(url)) + .toBe(`chrome://favicon/size/64@1x/${url}`) + }) + }) + describe('isGridSitePinned', () => { + it('returns true if site.pinnedIndex is defined', () => { + const site: NewTab.Site = { + id: '', + url: 'https://brave.com', + title: 'brave', + favicon: '', + letter: '', + pinnedIndex: 1337, + bookmarkInfo: undefined + } + expect(newTabUtils.isGridSitePinned(site)).toBe(true) + }) + it('returns false if site.pinnedIndex is not defined', () => { + const site: NewTab.Site = { + id: '', + url: 'https://brave.com', + title: 'brave', + favicon: '', + letter: '', + pinnedIndex: undefined, + bookmarkInfo: undefined + } + expect(newTabUtils.isGridSitePinned(site)).toBe(false) + }) + }) + describe('isGridSiteBookmarked', () => { + it('return true if bookmarkInfo is defined', () => { + const site: NewTab.Site = { + id: '', + url: 'https://brave.com', + title: 'brave', + favicon: '', + letter: '', + pinnedIndex: undefined, + bookmarkInfo: { + dateAdded: 1337, + id: '', + index: 1337, + parentId: '', + title: 'brave', + url: 'https://brave.com' + } + } + expect(newTabUtils.isGridSiteBookmarked(site.bookmarkInfo)).toBe(true) + }) + it('returns false if bookmarkInfo is not defined', () => { + const site: NewTab.Site = { + id: '', + url: 'https://brave.com', + title: 'brave', + favicon: '', + letter: '', + pinnedIndex: undefined, + bookmarkInfo: undefined + } + expect(newTabUtils.isGridSiteBookmarked(site.bookmarkInfo)).toBe(false) + }) + }) + describe('isExistingGridSite', () => { + const sites: chrome.topSites.MostVisitedURL[] = [ + { url: 'https://brave.com', title: 'brave' }, + { url: 'https://twitter.com/brave', title: 'brave twitter' } + ] + + it('returns true if site exists in the list', () => { + expect(newTabUtils.isExistingGridSite(sites, sites[0])).toBe(true) + }) + it('returns false if site does not exist in the list', () => { + const newUrl: chrome.topSites.MostVisitedURL = { + url: 'https://brave.com/about', + title: 'about brave' + } + expect(newTabUtils.isExistingGridSite(sites, newUrl)).toBe(false) + }) + }) + describe('generateGridSiteProperties', () => { + it('generates grid sites data from top chromium sites api', () => { + const newUrl: chrome.topSites.MostVisitedURL = { + url: 'https://brave.com', + title: 'brave' + } + const assertion = newTabUtils.generateGridSiteProperties(1337, newUrl) + const expected = [ + 'title', 'url', 'id', 'letter', 'favicon', 'pinnedIndex', 'bookmarkInfo' + ] + expect(Object.keys(assertion).every(item => expected.includes(item))) + .toBe(true) + }) + describe('generateGridSitesFromLegacyEntries', () => { + const legacyUrlList: NewTab.LegacySite = [{ + index: 1337, + url: 'https://brave.com', + title: 'brave', + favicon: '', + letter: 'b', + thumb: '', + themeColor: '', + computedThemeColor: '', + pinned: undefined, + bookmarked: undefined + }] + + it('exclude all old properties of a top site', () => { + const assertion = newTabUtils.generateGridSitesFromLegacyEntries(legacyUrlList) + expect(assertion[0]).not.toHaveProperty('index') + expect(assertion[0]).not.toHaveProperty('thumb') + expect(assertion[0]).not.toHaveProperty('themeColor') + expect(assertion[0]).not.toHaveProperty('computedThemeColor') + expect(assertion[0]).not.toHaveProperty('pinned') + expect(assertion[0]).not.toHaveProperty('bookmarked') + }) + it('include all new properties of a top site', () => { + const assertion = newTabUtils.generateGridSitesFromLegacyEntries(legacyUrlList) + expect(assertion[0]).toHaveProperty('id') + expect(assertion[0]).toHaveProperty('pinnedIndex') + expect(assertion[0]).toHaveProperty('bookmarkInfo') + }) + it('set pinnedIndex to be the same as the top site index', () => { + const assertion = newTabUtils.generateGridSitesFromLegacyEntries(legacyUrlList) + expect(assertion[0].pinnedIndex).toBe(1337) + }) + }) + }) + describe('getGridSitesWhitelist', () => { + it('excludes https://chrome.google.com/webstore from list', () => { + const topSites: chrome.topSites.MostVisitedURL[] = [ + { url: 'https://chrome.google.com/webstore', title: 'store' } + ] + expect(newTabUtils.getGridSitesWhitelist(topSites)).toHaveLength(0) + }) + it('does not exclude an arbritary site from list', () => { + const topSites: chrome.topSites.MostVisitedURL[] = [ + { url: 'https://tmz.com', title: 'tmz' } + ] + expect(newTabUtils.getGridSitesWhitelist(topSites)).toHaveLength(1) + }) + }) + describe('filterFromExcludedSites', () => { + const sitesData: NewTab.Site = [{ + id: '', + url: 'https://brave.com', + title: 'brave', + favicon: '', + letter: '', + pinnedIndex: undefined, + bookmarkInfo: undefined + }] + it('filter sites already included in the sites list', () => { + const removedSitesData = [ ...sitesData ] + const assertion = newTabUtils.filterFromExcludedSites(sitesData, removedSitesData) + expect(assertion).toHaveLength(0) + }) + it('does filter sites not included in the sites list', () => { + const removedSitesData = [{ + id: '', + url: 'https://new_site.com', + title: 'new_site', + favicon: '', + letter: '', + pinnedIndex: undefined, + bookmarkInfo: undefined + }] + const assertion = newTabUtils.filterFromExcludedSites(sitesData, removedSitesData) + expect(assertion).toHaveLength(1) }) }) }) diff --git a/components/test/brave_new_tab_ui/reducers/grid_sites_reducer_test.ts b/components/test/brave_new_tab_ui/reducers/grid_sites_reducer_test.ts new file mode 100644 index 000000000000..961b8fdb6102 --- /dev/null +++ b/components/test/brave_new_tab_ui/reducers/grid_sites_reducer_test.ts @@ -0,0 +1,491 @@ +// Copyright (c) 2020 The Brave Authors. All rights reserved. +// 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/. + +import gridSitesReducer from '../../../brave_new_tab_ui/reducers/grid_sites_reducer' +import * as storage from '../../../brave_new_tab_ui/storage/grid_sites_storage' +import { types } from '../../../brave_new_tab_ui/constants/grid_sites_types' +import * as gridSitesState from '../../../brave_new_tab_ui/state/gridSitesState' + +const bookmarkInfo: chrome.bookmarks.BookmarkTreeNode = { + dateAdded: 123123, + id: '', + index: 1337, + parentId: '', + title: 'brave', + url: 'https://brave.com' +} +const topSites: chrome.topSites.MostVisitedURL[] = [{ + url: 'https://brave.com', + title: 'brave' +}, { + url: 'https://cezaraugusto.net', + title: 'cezar augusto' +}] +const gridSites: NewTab.Site[] = [{ + ...topSites[0], + id: 'topsite-0', + favicon: '', + letter: 'b', + pinnedIndex: undefined, + bookmarkInfo +}, { + ...topSites[1], + id: 'topsite-1', + favicon: '', + letter: 'c', + pinnedIndex: undefined, + bookmarkInfo: undefined +}] + +describe('gridSitesReducer', () => { + describe('Handle initial state', () => { + it('returns the initial state when state is undefined', () => { + const assertion = gridSitesReducer( + undefined, + { type: undefined, payload: undefined } + ) + + expect(assertion).toEqual(storage.initialGridSitesState) + }) + }) + + describe('GRID_SITES_SET_FIRST_RENDER_DATA', () => { + let gridSitesReducerSetFirstRenderDataStub: jest.SpyInstance + + beforeEach(() => { + gridSitesReducerSetFirstRenderDataStub = jest + .spyOn(gridSitesState, 'gridSitesReducerSetFirstRenderData') + }) + afterEach(() => { + gridSitesReducerSetFirstRenderDataStub.mockRestore() + }) + + it('calls gridSitesReducerSetFirstRenderData with the correct args', () => { + gridSitesReducer(undefined, { + type: types.GRID_SITES_SET_FIRST_RENDER_DATA, + payload: { topSites: [] } + }) + + expect(gridSitesReducerSetFirstRenderDataStub).toBeCalledTimes(1) + expect(gridSitesReducerSetFirstRenderDataStub) + .toBeCalledWith(storage.initialGridSitesState, []) + }) + it('populate state.gridSites list with Chromium topSites data', () => { + const assertion = gridSitesReducer(storage.initialGridSitesState, { + type: types.GRID_SITES_SET_FIRST_RENDER_DATA, + payload: { topSites } + }) + + expect(assertion.gridSites).toHaveLength(2) + }) + }) + describe('GRID_SITES_DATA_UPDATED', () => { + let gridSitesReducerDataUpdatedStub: jest.SpyInstance + + beforeEach(() => { + gridSitesReducerDataUpdatedStub = jest + .spyOn(gridSitesState, 'gridSitesReducerDataUpdated') + }) + afterEach(() => { + gridSitesReducerDataUpdatedStub.mockRestore() + }) + + it('calls gridSitesReducerDataUpdated with the correct args', () => { + gridSitesReducer(undefined, { + type: types.GRID_SITES_DATA_UPDATED, + payload: { gridSites } + }) + + expect(gridSitesReducerDataUpdatedStub).toBeCalledTimes(1) + expect(gridSitesReducerDataUpdatedStub) + .toBeCalledWith(storage.initialGridSitesState, gridSites) + }) + it('update state.gridSites list', () => { + const assertion = gridSitesReducer(storage.initialGridSitesState, { + type: types.GRID_SITES_DATA_UPDATED, + payload: { gridSites } + }) + + expect(assertion.gridSites).toHaveLength(2) + }) + }) + describe('GRID_SITES_TOGGLE_SITE_PINNED', () => { + let gridSitesReducerToggleSitePinnedStub: jest.SpyInstance + + beforeEach(() => { + gridSitesReducerToggleSitePinnedStub = jest + .spyOn(gridSitesState, 'gridSitesReducerToggleSitePinned') + }) + afterEach(() => { + gridSitesReducerToggleSitePinnedStub.mockRestore() + }) + + it('calls gridSitesReducerToggleSitePinned with the correct args', () => { + const site: NewTab.Site = gridSites[0] + gridSitesReducer(undefined, { + type: types.GRID_SITES_TOGGLE_SITE_PINNED, + payload: { pinnedSite: site } + }) + + expect(gridSitesReducerToggleSitePinnedStub).toBeCalledTimes(1) + expect(gridSitesReducerToggleSitePinnedStub) + .toBeCalledWith(storage.initialGridSitesState, site) + }) + it('set own pinnedIndex value if property is undefined', () => { + const pinnedSite: NewTab.Site = { ...gridSites[1], pinnedIndex: 1337 } + const newStateWithGridSites: NewTab.State = { + ...storage.initialGridSitesState, + gridSites: [pinnedSite] + } + + const assertion = gridSitesReducer(newStateWithGridSites, { + type: types.GRID_SITES_TOGGLE_SITE_PINNED, + payload: { pinnedSite } + }) + + expect(assertion.gridSites[0]) + .toHaveProperty('pinnedIndex', undefined) + }) + it('set own pinnedIndex value to undefined if property is defined', () => { + const newStateWithGridSites: NewTab.State = { + ...storage.initialGridSitesState, + gridSites + } + + const assertion = gridSitesReducer(newStateWithGridSites, { + type: types.GRID_SITES_TOGGLE_SITE_PINNED, + payload: { pinnedSite: gridSites[1] } + }) + + expect(assertion.gridSites[1]) + .toHaveProperty('pinnedIndex', 1) + }) + }) + describe('GRID_SITES_REMOVE_SITE', () => { + let gridSitesReducerRemoveSiteStub: jest.SpyInstance + + beforeEach(() => { + gridSitesReducerRemoveSiteStub = jest + .spyOn(gridSitesState, 'gridSitesReducerRemoveSite') + }) + afterEach(() => { + gridSitesReducerRemoveSiteStub.mockRestore() + }) + + it('calls gridSitesReducerRemoveSite with the correct args', () => { + const site: NewTab.Site = gridSites[0] + gridSitesReducer(undefined, { + type: types.GRID_SITES_REMOVE_SITE, + payload: { removedSite: site } + }) + + expect(gridSitesReducerRemoveSiteStub).toBeCalledTimes(1) + expect(gridSitesReducerRemoveSiteStub) + .toBeCalledWith(storage.initialGridSitesState, site) + }) + it('remove a site from state.gridSites list', () => { + const removedSite: NewTab.Site = gridSites[1] + const newStateWithGridSites: NewTab.State = { + ...storage.initialGridSitesState, + gridSites + } + + const assertion = gridSitesReducer(newStateWithGridSites, { + type: types.GRID_SITES_REMOVE_SITE, + payload: { removedSite: removedSite } + }) + + expect(assertion.gridSites).toHaveLength(1) + }) + }) + describe('GRID_SITES_UNDO_REMOVE_SITE', () => { + let gridSitesReducerUndoRemoveSiteStub: jest.SpyInstance + + beforeEach(() => { + gridSitesReducerUndoRemoveSiteStub = jest + .spyOn(gridSitesState, 'gridSitesReducerUndoRemoveSite') + }) + afterEach(() => { + gridSitesReducerUndoRemoveSiteStub.mockRestore() + }) + + it('calls gridSitesReducerUndoRemoveSite with the correct args', () => { + gridSitesReducer(undefined, { + type: types.GRID_SITES_UNDO_REMOVE_SITE, + payload: undefined + }) + + expect(gridSitesReducerUndoRemoveSiteStub).toBeCalledTimes(1) + expect(gridSitesReducerUndoRemoveSiteStub) + .toBeCalledWith(storage.initialGridSitesState) + }) + it('push an item from state.removedSites list back to state.gridSites list', () => { + const removedSite: NewTab.Site = { + ...gridSites[1], + url: 'https://example.com' + } + const newStateWithGridSites: NewTab.State = { + ...storage.initialGridSitesState, + gridSites, + removedSites: [removedSite] + } + const assertion = gridSitesReducer(newStateWithGridSites, { + type: types.GRID_SITES_UNDO_REMOVE_SITE, + payload: undefined + }) + + expect(assertion.gridSites).toHaveLength(3) + }) + it('do not push an item from state.gridSites if url exists inside the list', () => { + const removedSite: NewTab.Site = { ...gridSites[1] } + const newStateWithGridSites: NewTab.State = { + ...storage.initialGridSitesState, + gridSites, + removedSites: [removedSite] + } + const assertion = gridSitesReducer(newStateWithGridSites, { + type: types.GRID_SITES_UNDO_REMOVE_SITE, + payload: undefined + }) + + expect(assertion.gridSites).toHaveLength(2) + }) + }) + describe('GRID_SITES_UNDO_REMOVE_ALL_SITES', () => { + let gridSitesReducerUndoRemoveAllSitesStub: jest.SpyInstance + + beforeEach(() => { + gridSitesReducerUndoRemoveAllSitesStub = jest + .spyOn(gridSitesState, 'gridSitesReducerUndoRemoveAllSites') + }) + afterEach(() => { + gridSitesReducerUndoRemoveAllSitesStub.mockRestore() + }) + + it('calls gridSitesReducerUndoRemoveAllSites with the correct args', () => { + gridSitesReducer(undefined, { + type: types.GRID_SITES_UNDO_REMOVE_ALL_SITES, + payload: undefined + }) + + expect(gridSitesReducerUndoRemoveAllSitesStub).toBeCalledTimes(1) + expect(gridSitesReducerUndoRemoveAllSitesStub) + .toBeCalledWith(storage.initialGridSitesState) + }) + it('push all items from state.removedSites list back to state.gridSites list', () => { + const removedSites: NewTab.Site[] = [{ + ...gridSites[0], + url: 'https://example.com' + }, { + ...gridSites[1], + url: 'https://another-example.com' + }] + const newStateWithGridSites: NewTab.State = { + ...storage.initialGridSitesState, + gridSites, + removedSites: removedSites + } + + const assertion = gridSitesReducer(newStateWithGridSites, { + type: types.GRID_SITES_UNDO_REMOVE_ALL_SITES, + payload: undefined + }) + + expect(assertion.gridSites).toHaveLength(4) + }) + it('do not push any item to state.gridSites if url exists inside the list', () => { + const sites: NewTab.Sites[] = gridSites + const newStateWithGridSites: NewTab.State = { + ...storage.initialGridSitesState, + gridSites: sites, + removedSites: sites + } + + const assertion = gridSitesReducer(newStateWithGridSites, { + type: types.GRID_SITES_UNDO_REMOVE_ALL_SITES, + payload: undefined + }) + + expect(assertion.gridSites).toHaveLength(2) + }) + }) + describe('GRID_SITES_UPDATE_SITE_BOOKMARK_INFO', () => { + let gridSitesReducerUpdateSiteBookmarkInfoStub: jest.SpyInstance + + beforeEach(() => { + gridSitesReducerUpdateSiteBookmarkInfoStub = jest + .spyOn(gridSitesState, 'gridSitesReducerUpdateSiteBookmarkInfo') + }) + afterEach(() => { + gridSitesReducerUpdateSiteBookmarkInfoStub.mockRestore() + }) + + it('calls gridSitesReducerUpdateSiteBookmarkInfo with the correct args', () => { + const topSiteBookmarkInfo: NewTab.Site = gridSites[0].bookmarkInfo + gridSitesReducer(undefined, { + type: types.GRID_SITES_UPDATE_SITE_BOOKMARK_INFO, + payload: { bookmarkInfo: topSiteBookmarkInfo } + }) + + expect(gridSitesReducerUpdateSiteBookmarkInfoStub).toBeCalledTimes(1) + expect(gridSitesReducerUpdateSiteBookmarkInfoStub) + .toBeCalledWith(storage.initialGridSitesState, topSiteBookmarkInfo) + }) + it('update own bookmarkInfo with the specified value', () => { + const topSiteBookmarkInfo: NewTab.Site = gridSites[0].bookmarkInfo + const sites: NewTab.Sites[] = [{ + ...gridSites[0], bookmarkInfo: 'NEW_INFO' + }] + const newStateWithGridSites: NewTab.State = { + ...storage.initialGridSitesState, + gridSites: sites + } + + const assertion = gridSitesReducer(newStateWithGridSites, { + type: types.GRID_SITES_UPDATE_SITE_BOOKMARK_INFO, + payload: { bookmarkInfo: topSiteBookmarkInfo } + }) + + expect(assertion.gridSites[0]) + .toHaveProperty('bookmarkInfo', 'NEW_INFO') + }) + }) + describe('GRID_SITES_TOGGLE_SITE_BOOKMARK_INFO', () => { + let gridSitesReducerToggleSiteBookmarkInfoStub: jest.SpyInstance + + beforeEach(() => { + gridSitesReducerToggleSiteBookmarkInfoStub = jest + .spyOn(gridSitesState, 'gridSitesReducerToggleSiteBookmarkInfo') + }) + afterEach(() => { + gridSitesReducerToggleSiteBookmarkInfoStub.mockRestore() + }) + + it('calls gridSitesReducerToggleSiteBookmarkInfo with the correct args', () => { + const siteUrl: string = gridSites[0].url + const topSiteBookmarkInfo: chrome.bookmarks.BookmarkTreeNode + = bookmarkInfo + gridSitesReducer(undefined, { + type: types.GRID_SITES_TOGGLE_SITE_BOOKMARK_INFO, + payload: { + url: siteUrl, + bookmarkInfo: topSiteBookmarkInfo + } + }) + + expect(gridSitesReducerToggleSiteBookmarkInfoStub).toBeCalledTimes(1) + expect(gridSitesReducerToggleSiteBookmarkInfoStub) + .toBeCalledWith(storage.initialGridSitesState, siteUrl, topSiteBookmarkInfo) + }) + it('add own add bookmarkInfo if url has no data', () => { + const siteUrl: string = gridSites[0].url + const newStateWithGridSites: NewTab.State = { + ...storage.initialGridSitesState, + gridSites + } + + const assertion = gridSitesReducer(newStateWithGridSites, { + type: types.GRID_SITES_TOGGLE_SITE_BOOKMARK_INFO, + payload: { + url: siteUrl, + bookmarkInfo: undefined + } + }) + + expect(assertion.gridSites[0].bookmarkInfo).not.toBeUndefined() + }) + it('remove own bookmarkInfo if url has data', () => { + const siteUrl: string = gridSites[0].url + const topSiteBookmarkInfo: NewTab.Site = gridSites[0].bookmarkInfo + const newStateWithGridSites: NewTab.State = { + ...storage.initialGridSitesState, + gridSites + } + + const assertion = gridSitesReducer(newStateWithGridSites, { + type: types.GRID_SITES_TOGGLE_SITE_BOOKMARK_INFO, + payload: { + url: siteUrl, + bookmarkInfo: topSiteBookmarkInfo + } + }) + + expect(assertion.gridSites[0].bookmarkInfo).toBeUndefined() + }) + }) + describe('GRID_SITES_ADD_SITES', () => { + let gridSitesReducerAddSiteOrSitesStub: jest.SpyInstance + + beforeEach(() => { + gridSitesReducerAddSiteOrSitesStub = jest + .spyOn(gridSitesState, 'gridSitesReducerAddSiteOrSites') + }) + afterEach(() => { + gridSitesReducerAddSiteOrSitesStub.mockRestore() + }) + + it('calls gridSitesReducerAddSiteOrSites with the correct args', () => { + const site: NewTab.Site = gridSites[0] + gridSitesReducer(undefined, { + type: types.GRID_SITES_ADD_SITES, + payload: { site: site } + }) + + expect(gridSitesReducerAddSiteOrSitesStub).toBeCalledTimes(1) + expect(gridSitesReducerAddSiteOrSitesStub) + .toBeCalledWith(storage.initialGridSitesState, site) + }) + it('add sites to state.gridSites list', () => { + const newSite: NewTab.Site = { + ...gridSites[0], + url: 'https://example.com' + } + const newStateWithGridSites: NewTab.State = { + ...storage.initialGridSitesState, + gridSites + } + + const assertion = gridSitesReducer(newStateWithGridSites, { + type: types.GRID_SITES_ADD_SITES, + payload: { site: newSite } + }) + + expect(assertion.gridSites).toHaveLength(3) + }) + }) + describe('GRID_SITES_SHOW_SITE_REMOVED_NOTIFICATION', () => { + let gridSitesReducerShowSiteRemovedNotificationStub: jest.SpyInstance + + beforeEach(() => { + gridSitesReducerShowSiteRemovedNotificationStub = jest + .spyOn(gridSitesState, 'gridSitesReducerShowSiteRemovedNotification') + }) + afterEach(() => { + gridSitesReducerShowSiteRemovedNotificationStub.mockRestore() + }) + + it('calls gridSitesReducerShowSiteRemovedNotification with the correct args', () => { + const shouldShow: boolean = true + gridSitesReducer(undefined, { + type: types.GRID_SITES_SHOW_SITE_REMOVED_NOTIFICATION, + payload: { shouldShow } + }) + + expect(gridSitesReducerShowSiteRemovedNotificationStub).toBeCalledTimes(1) + expect(gridSitesReducerShowSiteRemovedNotificationStub) + .toBeCalledWith(storage.initialGridSitesState, shouldShow) + }) + it('update state with the specified payload value', () => { + const assertion = gridSitesReducer(storage.initialGridSitesState, { + type: types.GRID_SITES_SHOW_SITE_REMOVED_NOTIFICATION, + payload: { + shouldShow: true + } + }) + + expect(assertion.shouldShowSiteRemovedNotification).toBe(true) + }) + }) +}) diff --git a/components/test/brave_new_tab_ui/reducers/new_tab_reducer_test.ts b/components/test/brave_new_tab_ui/reducers/new_tab_reducer_test.ts index b02b7278e39c..c7bc5d584bae 100644 --- a/components/test/brave_new_tab_ui/reducers/new_tab_reducer_test.ts +++ b/components/test/brave_new_tab_ui/reducers/new_tab_reducer_test.ts @@ -2,272 +2,88 @@ * 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/. */ - // Constants -import { types } from '../../../brave_new_tab_ui/constants/new_tab_types' - // Reducer import newTabReducer from '../../../brave_new_tab_ui/reducers/new_tab_reducer' -// State -import { newTabInitialState } from '../../testData' - // API -import * as gridAPI from '../../../brave_new_tab_ui/api/topSites/grid' -import * as bookmarksAPI from '../../../brave_new_tab_ui/api/topSites/bookmarks' -import * as dndAPI from '../../../brave_new_tab_ui/api/topSites/dnd' -import * as storage from '../../../brave_new_tab_ui/storage' - -const initialState = newTabInitialState.newTabData +import * as storage from '../../../brave_new_tab_ui/storage/new_tab_storage' describe('newTabReducer', () => { - const url: string = 'http://brave.com/' - const topSites: Partial = [{ url }] - const pinnedTopSites: Partial = topSites - const ignoredTopSites: Partial = [{ url: 'https://github.com' }] - const bookmarks: Partial = { [url]: { id: 'bookmark_id' } } - const fakeState = { - ...initialState, - topSites, - bookmarks, - pinnedTopSites, - ignoredTopSites - } describe('initial state', () => { it('loads initial data', () => { const expectedState = storage.load() - const returnedState = newTabReducer(undefined, {}) + const returnedState = newTabReducer(undefined, { type: {} }) expect(returnedState).toEqual(expectedState) }) }) - describe('BOOKMARK_ADDED', () => { - let spy: jest.SpyInstance - beforeEach(() => { - spy = jest.spyOn(chrome.bookmarks, 'create') - }) - afterEach(() => { - spy.mockRestore() - }) - it('calls chrome.bookmarks.create if topSites url match payload url', () => { - newTabReducer(fakeState, { - type: types.BOOKMARK_ADDED, - payload: { url } - }) - expect(spy).toBeCalled() - }) - it('does not call chrome.bookmarks.create if url does not match', () => { - newTabReducer(fakeState, { - type: types.BOOKMARK_ADDED, - payload: { url: 'https://very-different-website-domain.com' } - }) - expect(spy).not.toBeCalled() - }) + describe('NEW_TAB_SET_INITIAL_DATA', () => { + // TODO }) - describe('BOOKMARK_REMOVED', () => { - let spy: jest.SpyInstance - beforeEach(() => { - spy = jest.spyOn(chrome.bookmarks, 'remove') - }) - afterEach(() => { - spy.mockRestore() - }) - - it('calls chrome.bookmarks.remove if bookmarkInfo exists', () => { - newTabReducer(fakeState, { - type: types.BOOKMARK_REMOVED, - payload: { url } - }) - expect(spy).toBeCalled() - }) - it('does not call chrome.bookmarks.remove if bookmarkInfo is undefined', () => { - const newTabInitialStateWithoutBookmarks = { ...initialState, bookmarks: {} } - newTabReducer(newTabInitialStateWithoutBookmarks, { - type: types.BOOKMARK_REMOVED, - payload: { url } - }) - expect(spy).not.toBeCalled() - }) + describe('NEW_TAB_STATS_UPDATED', () => { + // TODO }) - describe('NEW_TAB_SITE_PINNED', () => { - let spy: jest.SpyInstance - beforeEach(() => { - spy = jest.spyOn(gridAPI, 'calculateGridSites') - }) - afterEach(() => { - spy.mockRestore() - }) - it('calls gridAPI.calculateGridSites', () => { - jest.useFakeTimers() - newTabReducer(fakeState, { - type: types.NEW_TAB_SITE_PINNED, - payload: { url } - }) - jest.runAllTimers() - expect(spy).toBeCalled() - jest.useRealTimers() - }) + describe('NEW_TAB_PRIVATE_TAB_DATA_UPDATED', () => { + // TODO }) - describe('NEW_TAB_SITE_UNPINNED', () => { - let spy: jest.SpyInstance - beforeEach(() => { - spy = jest.spyOn(gridAPI, 'calculateGridSites') - }) - afterEach(() => { - spy.mockRestore() - }) - it('calls gridAPI.calculateGridSites', () => { - jest.useFakeTimers() - newTabReducer(fakeState, { - type: types.NEW_TAB_SITE_UNPINNED, - payload: { url } - }) - jest.runAllTimers() - expect(spy).toBeCalled() - jest.useRealTimers() - }) + describe('NEW_TAB_DISMISS_BRANDED_WALLPAPER_NOTIFICATION', () => { + // TODO }) - describe('NEW_TAB_SITE_IGNORED', () => { - let spy: jest.SpyInstance - beforeEach(() => { - spy = jest.spyOn(gridAPI, 'calculateGridSites') - }) - afterEach(() => { - spy.mockRestore() - }) - it('calls gridAPI.calculateGridSites', () => { - jest.useFakeTimers() - newTabReducer(fakeState, { - type: types.NEW_TAB_SITE_IGNORED, - payload: { url } - }) - jest.runAllTimers() - expect(spy).toBeCalled() - jest.useRealTimers() - }) + describe('NEW_TAB_PREFERENCES_UPDATED', () => { + // TODO }) - describe('NEW_TAB_UNDO_SITE_IGNORED', () => { - let spy: jest.SpyInstance - beforeEach(() => { - spy = jest.spyOn(gridAPI, 'calculateGridSites') + describe('rewards features inside new tab page', () => { + describe('CREATE_WALLET', () => { + // TODO }) - afterEach(() => { - spy.mockRestore() + describe('ON_ENABLED_MAIN', () => { + // TODO }) - it('calls gridAPI.calculateGridSites', () => { - jest.useFakeTimers() - newTabReducer(fakeState, { - type: types.NEW_TAB_UNDO_SITE_IGNORED, - payload: { url } - }) - jest.runAllTimers() - expect(spy).toBeCalled() - jest.useRealTimers() + describe('CREATE_WALLET', () => { + // TODO }) - }) - describe('NEW_TAB_UNDO_ALL_SITE_IGNORED', () => { - let spy: jest.SpyInstance - beforeEach(() => { - spy = jest.spyOn(gridAPI, 'calculateGridSites') + describe('ON_ENABLED_MAIN', () => { + // TODO }) - afterEach(() => { - spy.mockRestore() + describe('ON_WALLET_INITIALIZED', () => { + // TODO }) - it('calls gridAPI.calculateGridSites', () => { - jest.useFakeTimers() - newTabReducer(fakeState, { - type: types.NEW_TAB_UNDO_ALL_SITE_IGNORED - }) - jest.runAllTimers() - expect(spy).toBeCalled() + describe('WALLET_CORRUPT', () => { + // TODO }) - jest.useRealTimers() - }) - describe('NEW_TAB_HIDE_SITE_REMOVAL_NOTIFICATION', () => { - it('set showSiteRemovalNotification to false', () => { - const assertion = newTabReducer(fakeState, { - type: types.NEW_TAB_HIDE_SITE_REMOVAL_NOTIFICATION - }) - expect(assertion).toEqual({ - ...fakeState, - showSiteRemovalNotification: false - }) + describe('WALLET_CREATED', () => { + // TODO }) - }) - describe('NEW_TAB_SITE_DRAGGED', () => { - let spy: jest.SpyInstance - beforeEach(() => { - spy = jest.spyOn(dndAPI, 'onDraggedSite') + describe('LEDGER_OK', () => { + // TODO }) - afterEach(() => { - spy.mockRestore() + describe('ON_ADS_ENABLED', () => { + // TODO }) - it('calls dndAPI.onDraggedSite', () => { - newTabReducer(fakeState, { - type: types.NEW_TAB_SITE_DRAGGED, - payload: { - fromUrl: 'https://brave.com', - toUrl: 'https://github.com' - } - }) - expect(spy).toBeCalled() + describe('ON_ADS_ESTIMATED_EARNINGS', () => { + // TODO }) - }) - describe('NEW_TAB_SITE_DRAG_END', () => { - let spy: jest.SpyInstance - beforeEach(() => { - spy = jest.spyOn(dndAPI, 'onDragEnd') + describe('ON_BALANCE_REPORT', () => { + // TODO }) - afterEach(() => { - spy.mockRestore() + describe('DISMISS_NOTIFICATION', () => { + // TODO }) - it('calls dndAPI.onDragEnd', () => { - newTabReducer(fakeState, { - type: types.NEW_TAB_SITE_DRAG_END - }) - expect(spy).toBeCalled() + describe('ON_PROMOTIONS', () => { + // TODO }) - }) - describe('NEW_TAB_BOOKMARK_INFO_AVAILABLE', () => { - let spy: jest.SpyInstance - beforeEach(() => { - spy = jest.spyOn(bookmarksAPI, 'updateBookmarkInfo') + describe('ON_BALANCE', () => { + // TODO }) - afterEach(() => { - spy.mockRestore() + describe('ON_WALLET_EXISTS', () => { + // TODO }) - it('calls bookmarksAPI.updateBookmarkInfo', () => { - const queryUrl: string = 'https://brave.com' - const bookmarkTreeNode = { - dateAdded: 1557899510259, - id: '7', - index: 0, - parentId: '2', - title: 'Secure, Fast & Private Web Browser with Adblocker | Brave Browser', - url: 'http://brave.com/' - } - newTabReducer(fakeState, { - type: types.NEW_TAB_BOOKMARK_INFO_AVAILABLE, - payload: { - queryUrl, - bookmarkTreeNode - } - }) - expect(spy).toBeCalled() + describe('SET_PRE_INITIAL_REWARDS_DATA', () => { + // TODO }) - }) - describe('NEW_TAB_GRID_SITES_UPDATED', () => { - it('sets gridSites into gridSites state', () => { - const url: string = 'http://brave.com/' - const gridSites: Partial = [{ url }] - const assertion = newTabReducer(fakeState, { - type: types.NEW_TAB_GRID_SITES_UPDATED, - payload: { gridSites } - }) - expect(assertion).toEqual({ - ...fakeState, - gridSites: [ { url: 'http://brave.com/' } ] - }) + describe('SET_INITIAL_REWARDS_DATA', () => { + // TODO }) }) }) diff --git a/components/test/brave_new_tab_ui/state/gridSitesState_test.ts b/components/test/brave_new_tab_ui/state/gridSitesState_test.ts new file mode 100644 index 000000000000..c50437f8f5ec --- /dev/null +++ b/components/test/brave_new_tab_ui/state/gridSitesState_test.ts @@ -0,0 +1,322 @@ +// Copyright (c) 2020 The Brave Authors. All rights reserved. +// 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/. + +// State helpers +import * as gridSitesState from '../../../brave_new_tab_ui/state/gridSitesState' + +// Helpers +import { generateGridSiteProperties } from '../../../brave_new_tab_ui/helpers/newTabUtils' +import * as storage from '../../../brave_new_tab_ui/storage/grid_sites_storage' + +const newTopSite1: chrome.topSites.MostVisitedURL = { + url: 'https://brave.com', + title: 'brave!' +} + +const newTopSite2: chrome.topSites.MostVisitedURL = { + url: 'https://clifton.io', + title: 'BSC]]' +} + +const gridSites: NewTab.Site[] = [{ + ...newTopSite1, + ...generateGridSiteProperties(0, newTopSite1) +}, { + ...newTopSite2, + ...generateGridSiteProperties(1, newTopSite2) +}] + +describe('gridSitesState', () => { + describe('gridSitesReducerSetFirstRenderData', () => { + it('does not populate state.gridSites list if url already exist within the list', () => { + const newState: NewTab.State = { + ...storage.initialGridSitesState, + gridSites: [generateGridSiteProperties(0, newTopSite1)] + } + const assertion = gridSitesState + .gridSitesReducerSetFirstRenderData(newState, [ + newTopSite1, + newTopSite1, + newTopSite1, + newTopSite1 + ]) + + expect(assertion.gridSites).toHaveLength(1) + }) + it('populate state.gridSites list if urls are different', () => { + const assertion = gridSitesState + .gridSitesReducerSetFirstRenderData(storage.initialGridSitesState, [ + newTopSite1, + newTopSite2 + ]) + + expect(assertion.gridSites).toHaveLength(2) + }) + }) + describe('gridSitesReducerDataUpdated', () => { + it('update state.gridSites list', () => { + const assertion = gridSitesState + .gridSitesReducerDataUpdated(storage.initialGridSitesState, gridSites) + + expect(assertion.gridSites).toHaveLength(2) + }) + it('preserve own pinnedIndex position after a new site is added', () => { + const pinnedIndex: number = 1 + const pinnedSite: NewTab.Site = { + ...gridSites[0], + url: 'https://cezaraugusto.net', + title: `pinned position ${pinnedIndex}`, + pinnedIndex + } + const newGridSites: NewTab.Sites[] = [ + { + ...gridSites[0], + url: 'https://brave.com', + title: 'not pinned position 0' + }, + pinnedSite, + { + ...gridSites[0], + url: 'https://clifton.io', + title: 'not pinned position 2' + } + ] + + const newState: NewTab.State = { + ...storage.initialGridSitesState, + gridSites: newGridSites + } + // add a new site on top of gridSites after a tile + // have been pinned + const newestGridSites: NewTab.Site[] = [ + ...newGridSites, + { + ...gridSites[0], + url: 'https://petemill.com', + title: 'not pinned TBD position 0' + } + ] + const assertion = gridSitesState + .gridSitesReducerDataUpdated(newState, newestGridSites) + + expect(assertion.gridSites).toHaveLength(4) + // pinned tiles cannot be moved + expect(assertion.gridSites[pinnedIndex]).toEqual(pinnedSite) + }) + it('preserve pinnedIndex positions after random reordering', () => { + // just an utility for our test + // adapted from https://stackoverflow.com/a/6274381/4902448 + const shuffle = (arr: Array) => { + for (let i = arr.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)) + ;[arr[i], arr[j]] = [arr[j], arr[i]] + } + return arr + } + + const pinnedIndex: number = 1 + const pinnedSite: NewTab.Site = { + ...gridSites[0], + url: 'https://cezaraugusto.net', + title: `pinned position ${pinnedIndex}`, + pinnedIndex + } + const newGridSites: NewTab.Sites[] = [ + { + ...gridSites[0], + url: 'https://brave.com', + title: 'not pinned position 0' + }, + pinnedSite, + { + ...gridSites[0], + url: 'https://clifton.io', + title: 'not pinned position 2' + } + ] + + const newState: NewTab.State = { + ...storage.initialGridSitesState, + gridSites: newGridSites + } + + const assertion = gridSitesState + .gridSitesReducerDataUpdated(newState, shuffle(newGridSites)) + + // pinned tiles should preserve position after shuffle + expect(assertion.gridSites[pinnedIndex]).toEqual(pinnedSite) + }) + }) + describe('gridSitesReducerToggleSitePinned', () => { + it('set own pinnedIndex value if property is undefined', () => { + const expectedIndex: number = 1 + const newState: NewTab.State = { ...storage.initialGridSitesState, gridSites } + + const assertion = gridSitesState + .gridSitesReducerToggleSitePinned(newState, gridSites[expectedIndex]) + + expect(assertion.gridSites[expectedIndex]) + .toHaveProperty('pinnedIndex', expectedIndex) + }) + it('set own pinnedIndex value to undefined if property is defined', () => { + const pinnedSite: NewTab.Site = { ...gridSites[1], pinnedIndex: 1337 } + const newState: NewTab.State = { ...storage.initialGridSitesState, gridSites: [pinnedSite] } + + const assertion = gridSitesState + .gridSitesReducerToggleSitePinned(newState, pinnedSite) + + expect(assertion.gridSites[0]) + .toHaveProperty('pinnedIndex', undefined) + }) + it('does not add length to the list after a site is pinned', () => { + const pinnedSite: NewTab.Site = { + ...gridSites[1], + title: 'some site', + url: 'fake.com', + pinnedIndex: undefined + } + const newState: NewTab.State = { ...storage.initialGridSitesState, gridSites: [ ...gridSites, pinnedSite ] } + + expect(newState.gridSites).toHaveLength(3) + + const assertion = gridSitesState + .gridSitesReducerToggleSitePinned(newState, pinnedSite) + + expect(assertion.gridSites).toHaveLength(3) + }) + }) + describe('gridSitesReducerRemoveSite', () => { + it('remove a site from state.gridSites list', () => { + const removedSite: NewTab.Site = gridSites[1] + const newState: NewTab.State = { ...storage.initialGridSitesState, gridSites } + + const assertion = gridSitesState + .gridSitesReducerRemoveSite(newState, removedSite) + + expect(assertion.gridSites).toHaveLength(1) + }) + }) + describe('gridSitesReducerUndoRemoveSite', () => { + it('push an item from the state.removedSites list back to state.gridSites list', () => { + const removedSite: NewTab.Site = { ...gridSites[1], url: 'https://example.com' } + const newState: NewTab.State = { + ...storage.initialGridSitesState, + gridSites, + removedSites: [removedSite] + } + + const assertion = gridSitesState + .gridSitesReducerUndoRemoveSite(newState) + + expect(assertion.gridSites).toHaveLength(3) + }) + it('do not push an item from state.gridSites if url exists inside the list', () => { + const removedSite: NewTab.Site = { ...gridSites[1] } + const newState: NewTab.State = { + ...storage.initialGridSitesState, + gridSites, + removedSites: [removedSite] + } + + const assertion = gridSitesState + .gridSitesReducerUndoRemoveSite(newState) + + expect(assertion.gridSites).toHaveLength(2) + }) + }) + describe('gridSitesReducerUndoRemoveAllSites', () => { + it('push all items from state.removedSites list back to state.gridSites list', () => { + const removedSites: NewTab.Site[] = [{ + ...gridSites[0], + url: 'https://example.com' + }, { + ...gridSites[1], + url: 'https://another-example.com' + }] + + const newState: NewTab.State = { + ...storage.initialGridSitesState, + gridSites, + removedSites: removedSites + } + + const assertion = gridSitesState + .gridSitesReducerUndoRemoveAllSites(newState) + + expect(assertion.gridSites).toHaveLength(4) + }) + it('do not push any item to state.gridSites if url exists inside the list', () => { + const sites: NewTab.Sites[] = gridSites + const newState: NewTab.State = { + ...storage.initialGridSitesState, + gridSites: sites, + removedSites: sites + } + + const assertion = gridSitesState + .gridSitesReducerUndoRemoveAllSites(newState) + + expect(assertion.gridSites).toHaveLength(2) + }) + }) + describe('gridSitesReducerUpdateSiteBookmarkInfo', () => { + it('update own bookmarkInfo with the specified value', () => { + const topSiteUrl: NewTab.Site = gridSites[0].url + const sites: NewTab.Sites[] = [{ ...gridSites[0], bookmarkInfo: 'NEW_INFO' }] + const newState: NewTab.State = { ...storage.initialGridSitesState, gridSites: sites } + + const assertion = gridSitesState + .gridSitesReducerUpdateSiteBookmarkInfo(newState, topSiteUrl) + + expect(assertion.gridSites[0]) + .toHaveProperty('bookmarkInfo', 'NEW_INFO') + }) + }) + describe('gridSitesReducerToggleTopSiteBookmarked', () => { + it('add own add bookmarkInfo if url has no data', () => { + const siteUrl: string = gridSites[0].url + const newState: NewTab.State = { ...storage.initialGridSitesState, gridSites } + + const assertion = gridSitesState + .gridSitesReducerToggleSiteBookmarkInfo(newState, siteUrl, undefined) + + expect(assertion.gridSites[0].bookmarkInfo).not.toBeUndefined() + }) + it('remove own bookmarkInfo if url has data', () => { + const siteUrl: string = gridSites[0].url + const topSiteBookmarkInfo: chrome.bookmarks.BookmarkTreeNode = { + title: 'cool bookmark', + id: '' + } + const newState: NewTab.State = { ...storage.initialGridSitesState, gridSites } + + const assertion = gridSitesState + .gridSitesReducerToggleSiteBookmarkInfo(newState, siteUrl, topSiteBookmarkInfo) + + expect(assertion.gridSites[0].bookmarkInfo).toBeUndefined() + }) + }) + describe('gridSitesReducerAddSiteOrSites', () => { + it('add sites to state.gridSites list', () => { + const newSite: NewTab.Site = { ...gridSites[0], url: 'https://example.com' } + const newState: NewTab.State = { ...storage.initialGridSitesState, gridSites } + + const assertion = gridSitesState + .gridSitesReducerAddSiteOrSites(newState, newSite) + + expect(assertion.gridSites).toHaveLength(3) + }) + }) + describe('gridSitesReducerShowSiteRemovedNotification', () => { + it('update state with the specified payload value', () => { + const shouldShow: boolean = true + + const assertion = gridSitesState + .gridSitesReducerShowSiteRemovedNotification(storage.initialGridSitesState, shouldShow) + + expect(assertion.shouldShowSiteRemovedNotification).toBe(true) + }) + }) +}) diff --git a/components/test/testData.ts b/components/test/testData.ts index a38cb0e69ae9..b284da35ac96 100644 --- a/components/test/testData.ts +++ b/components/test/testData.ts @@ -44,15 +44,13 @@ export const newTabInitialState: NewTab.ApplicationState = { showBackgroundImage: false, showSettingsMenu: false, topSites: [], - ignoredTopSites: [], - pinnedTopSites: [], + excludedSites: [], gridSites: [], showEmptyPage: false, isIncognito: new ChromeEvent(), useAlternativePrivateSearchEngine: false, isTor: false, isQwant: false, - bookmarks: {}, stats: { adsBlockedStat: 0, javascriptBlockedStat: 0, diff --git a/package-lock.json b/package-lock.json index 1dacf555994c..1f2f43ac8d1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1361,7 +1361,6 @@ "version": "7.3.1", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.3.1.tgz", "integrity": "sha512-7jGW8ppV0ant637pIqAcFfQDDH1orEPGJb8aXfUozuCU3QqX7rX4DA8iwrbPrR1hcH0FTTHz47yQnk+bl5xHQA==", - "dev": true, "requires": { "regenerator-runtime": "^0.12.0" }, @@ -1369,8 +1368,7 @@ "regenerator-runtime": { "version": "0.12.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz", - "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==", - "dev": true + "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==" } } }, @@ -4432,10 +4430,9 @@ } }, "acorn": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", - "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", - "dev": true + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", + "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==" }, "buffer": { "version": "4.9.2", @@ -4838,6 +4835,12 @@ "webpack-sources": "^1.4.1" }, "dependencies": { + "acorn": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", + "dev": true + }, "ajv": { "version": "6.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", @@ -5212,10 +5215,9 @@ } }, "acorn": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", - "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", - "dev": true + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", + "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==" }, "buffer": { "version": "4.9.2", @@ -5409,6 +5411,12 @@ "webpack-sources": "^1.4.1" }, "dependencies": { + "acorn": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", + "dev": true + }, "ajv": { "version": "6.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", @@ -5759,6 +5767,15 @@ "loader-utils": "^1.2.3" } }, + "@types/array-move": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/array-move/-/array-move-2.0.0.tgz", + "integrity": "sha512-M1Sb7db3XP65S2j5CWvzce2z0ORRfT/Bhd6mYu++nP6ZhRsntMixavFyxgf9NkQa37bveuIfpb0RKYRA8XVcGQ==", + "dev": true, + "requires": { + "array-move": "*" + } + }, "@types/babel__core": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.0.tgz", @@ -6025,6 +6042,15 @@ "redux": "^4.0.0" } }, + "@types/react-sortable-hoc": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@types/react-sortable-hoc/-/react-sortable-hoc-0.7.1.tgz", + "integrity": "sha512-K27j2M0yzi8F1E/UylqImXbGrIh0L6eu31U905gzZpEImJpBUHkmMWWJCO1Aehw31PMV8I1Pqt9LkCbNF685DA==", + "dev": true, + "requires": { + "react-sortable-hoc": "*" + } + }, "@types/redux-logger": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/@types/redux-logger/-/redux-logger-3.0.7.tgz", @@ -6634,6 +6660,11 @@ "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz", "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=" }, + "array-move": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/array-move/-/array-move-2.2.1.tgz", + "integrity": "sha512-qQpEHBnVT6HAFgEVUwRdHVd8TYJThrZIT5wSXpEUTPwBaYhPLclw12mEpyUvRWVdl1VwPOqnIy6LqTFN3cSeUQ==" + }, "array-reduce": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz", @@ -6788,12 +6819,6 @@ "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" }, - "autobind-decorator": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/autobind-decorator/-/autobind-decorator-2.4.0.tgz", - "integrity": "sha512-OGYhWUO72V6DafbF8PM8rm3EPbfuyMZcJhtm5/n26IDwO18pohE4eNazLoCGhPiXOCD0gEGmrbU3849QvM8bbw==", - "dev": true - }, "autoprefixer": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.6.1.tgz", @@ -9528,10 +9553,9 @@ } }, "acorn": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", - "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", - "dev": true + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", + "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==" }, "ajv": { "version": "6.12.0", @@ -9694,6 +9718,23 @@ "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", "dev": true }, + "terser-webpack-plugin": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz", + "integrity": "sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==", + "dev": true, + "requires": { + "cacache": "^12.0.2", + "find-cache-dir": "^2.1.0", + "is-wsl": "^1.1.0", + "schema-utils": "^1.0.0", + "serialize-javascript": "^2.1.2", + "source-map": "^0.6.1", + "terser": "^4.1.2", + "webpack-sources": "^1.4.0", + "worker-farm": "^1.7.0" + } + }, "vm-browserify": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", @@ -9731,22 +9772,11 @@ "webpack-sources": "^1.4.1" }, "dependencies": { - "terser-webpack-plugin": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz", - "integrity": "sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==", - "dev": true, - "requires": { - "cacache": "^12.0.2", - "find-cache-dir": "^2.1.0", - "is-wsl": "^1.1.0", - "schema-utils": "^1.0.0", - "serialize-javascript": "^2.1.2", - "source-map": "^0.6.1", - "terser": "^4.1.2", - "webpack-sources": "^1.4.0", - "worker-farm": "^1.7.0" - } + "acorn": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", + "dev": true } } }, @@ -10331,18 +10361,6 @@ "integrity": "sha1-44Mx8IRLukm5qctxx3FYWqsbxlo=", "dev": true }, - "dnd-core": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-4.0.5.tgz", - "integrity": "sha1-O4PRONDV4mXHPsl43sXh7UQdxmU=", - "dev": true, - "requires": { - "asap": "^2.0.6", - "invariant": "^2.2.4", - "lodash": "^4.17.10", - "redux": "^4.0.0" - } - }, "doctrine": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-0.7.2.tgz", @@ -18566,7 +18584,6 @@ "version": "15.7.2", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "dev": true, "requires": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -19121,32 +19138,6 @@ } } }, - "react-dnd": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-5.0.0.tgz", - "integrity": "sha1-xKF8cBCeRW2tiQa+g45u6PMrBrU=", - "dev": true, - "requires": { - "dnd-core": "^4.0.5", - "hoist-non-react-statics": "^2.5.0", - "invariant": "^2.1.0", - "lodash": "^4.17.10", - "recompose": "^0.27.1", - "shallowequal": "^1.0.2" - } - }, - "react-dnd-html5-backend": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-5.0.1.tgz", - "integrity": "sha1-C1eNecXAExfHBBTI1xf2MrkZ1PE=", - "dev": true, - "requires": { - "autobind-decorator": "^2.1.0", - "dnd-core": "^4.0.5", - "lodash": "^4.17.10", - "shallowequal": "^1.0.2" - } - }, "react-docgen": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-4.1.1.tgz", @@ -19299,8 +19290,7 @@ "react-is": { "version": "16.8.3", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.3.tgz", - "integrity": "sha512-Y4rC1ZJmsxxkkPuMLwvKvlL1Zfpbcu+Bf4ZigkHup3v9EfdYhAlWAaVyA19olXq2o2mGn0w+dFKvk3pVVlYcIA==", - "dev": true + "integrity": "sha512-Y4rC1ZJmsxxkkPuMLwvKvlL1Zfpbcu+Bf4ZigkHup3v9EfdYhAlWAaVyA19olXq2o2mGn0w+dFKvk3pVVlYcIA==" }, "react-json-view": { "version": "1.19.1", @@ -19447,6 +19437,16 @@ "react-transition-group": "^2.2.1" } }, + "react-sortable-hoc": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/react-sortable-hoc/-/react-sortable-hoc-1.10.1.tgz", + "integrity": "sha512-eVyv5rrK6qY9bG60bboRY78In7OpdRRg+hxp4QMLIjC/UJaFSU7exTYd0764GtXvBqh+b+faYGzren5/ffRYKw==", + "requires": { + "@babel/runtime": "^7.2.0", + "invariant": "^2.2.4", + "prop-types": "^15.5.7" + } + }, "react-syntax-highlighter": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-8.1.0.tgz", diff --git a/package.json b/package.json index 0a13da0418a6..1b62da6421f0 100644 --- a/package.json +++ b/package.json @@ -276,6 +276,7 @@ "@storybook/addon-options": "^5.1.9", "@storybook/addons": "^5.1.9", "@storybook/react": "^5.1.9", + "@types/array-move": "^2.0.0", "@types/bluebird": "^3.5.25", "@types/chrome": "0.0.69", "@types/enzyme": "^3.1.12", @@ -287,6 +288,7 @@ "@types/react-dnd": "^2.0.36", "@types/react-dom": "^16.0.7", "@types/react-redux": "6.0.4", + "@types/react-sortable-hoc": "^0.7.1", "@types/redux-logger": "^3.0.7", "@types/storybook__addon-centered": "^3.3.2", "@types/storybook__addon-knobs": "^5.0.2", @@ -310,8 +312,6 @@ "mz": "^2.7.0", "react": "^16.2.0", "react-beautiful-dnd": "^11.0.3", - "react-dnd": "^5.0.0", - "react-dnd-html5-backend": "^5.0.1", "react-dom": "^16.3.0", "react-redux": "^5.0.6", "redux": "^4.0.0", @@ -335,6 +335,7 @@ "@types/jszip": "^3.1.6", "@types/parse-torrent": "^5.8.3", "@types/webtorrent": "^0.98.5", + "array-move": "^2.2.1", "bignumber.js": "^7.2.1", "bluebird": "^3.5.1", "clipboard-copy": "^2.0.0", @@ -342,6 +343,7 @@ "parse-domain": "^2.3.4", "prettier-bytes": "^1.0.4", "qr-image": "^3.2.0", + "react-sortable-hoc": "^1.10.1", "redux-logger": "^3.0.6", "redux-thunk": "^2.3.0", "throttleit": "^1.0.0",