From c974be57e14a1e9bbcb3aad01583c104e42fb5b5 Mon Sep 17 00:00:00 2001 From: Brian Clifton Date: Fri, 20 Mar 2020 09:24:43 -0700 Subject: [PATCH] Revert "Merge pull request #4325 from brave/ca-2971" This reverts commit 320369320bc49fb2352dfaf3c1f4266a5bec0bc9, reversing changes made to 80efc08d8f11a3a5d0c374e5bf869b4e7f2b9da1. --- .../actions/grid_sites_actions.ts | 69 --- .../actions/new_tab_actions.ts | 57 +- components/brave_new_tab_ui/api/bookmarks.ts | 47 -- components/brave_new_tab_ui/api/getActions.ts | 13 +- .../brave_new_tab_ui/api/initialData.ts | 2 +- .../api/topSites/bookmarks.ts | 39 ++ .../brave_new_tab_ui/api/topSites/dnd.ts | 42 ++ .../brave_new_tab_ui/api/topSites/grid.ts | 60 +++ .../api/{topSites.ts => topSites/index.ts} | 8 +- .../brave_new_tab_ui/apiEventsToStore.ts | 2 - .../components/default/clock/style.ts | 9 +- .../components/default/index.ts | 2 +- .../default/{gridSites => topSites}/index.ts | 17 +- .../components/default/widget/styles.ts | 7 +- .../constants/grid_sites_types.ts | 21 - .../constants/new_tab_types.ts | 20 +- .../brave_new_tab_ui/constants/new_tab_ui.ts | 6 - .../brave_new_tab_ui/containers/app.tsx | 19 +- .../containers/newTab/block.tsx | 179 +++++++ .../containers/newTab/gridSites.tsx | 88 ---- .../containers/newTab/gridTile.tsx | 118 ----- .../containers/newTab/index.tsx | 115 ++-- .../containers/newTab/notification.tsx | 47 +- .../brave_new_tab_ui/helpers/newTabUtils.ts | 81 +-- .../reducers/grid_sites_reducer.ts | 101 ---- components/brave_new_tab_ui/reducers/index.ts | 13 +- ...new_tab_reducer.ts => new_tab_reducer.tsx} | 185 ++++++- .../brave_new_tab_ui/state/gridSitesState.ts | 214 -------- .../new_tab_storage.ts => storage.ts} | 44 +- .../storage/grid_sites_storage.ts | 41 -- components/brave_new_tab_ui/store.ts | 19 +- .../brave_new_tab_ui/stories/default.tsx | 12 +- .../stories/default/data/storybookState.ts | 30 +- components/definitions/newTab.d.ts | 32 +- .../actions/new_tab_actions_test.ts | 164 ++++++ .../test/brave_new_tab_ui/api/data_test.ts | 12 +- .../api/topSites/bookmarks_test.ts | 52 ++ .../api/topSites/grid_test.ts | 51 ++ .../helpers/newTabUtils_test.ts | 196 +------ .../reducers/grid_sites_reducer_test.ts | 491 ------------------ .../reducers/new_tab_reducer_test.ts | 278 ++++++++-- .../state/gridSitesState_test.ts | 322 ------------ components/test/testData.ts | 4 +- package-lock.json | 158 +++--- package.json | 6 +- 45 files changed, 1374 insertions(+), 2119 deletions(-) delete mode 100644 components/brave_new_tab_ui/actions/grid_sites_actions.ts delete mode 100644 components/brave_new_tab_ui/api/bookmarks.ts create mode 100644 components/brave_new_tab_ui/api/topSites/bookmarks.ts create mode 100644 components/brave_new_tab_ui/api/topSites/dnd.ts create mode 100644 components/brave_new_tab_ui/api/topSites/grid.ts rename components/brave_new_tab_ui/api/{topSites.ts => topSites/index.ts} (59%) rename components/brave_new_tab_ui/components/default/{gridSites => topSites}/index.ts (83%) delete mode 100644 components/brave_new_tab_ui/constants/grid_sites_types.ts delete mode 100644 components/brave_new_tab_ui/constants/new_tab_ui.ts create mode 100644 components/brave_new_tab_ui/containers/newTab/block.tsx delete mode 100644 components/brave_new_tab_ui/containers/newTab/gridSites.tsx delete mode 100644 components/brave_new_tab_ui/containers/newTab/gridTile.tsx delete mode 100644 components/brave_new_tab_ui/reducers/grid_sites_reducer.ts rename components/brave_new_tab_ui/reducers/{new_tab_reducer.ts => new_tab_reducer.tsx} (59%) delete mode 100644 components/brave_new_tab_ui/state/gridSitesState.ts rename components/brave_new_tab_ui/{storage/new_tab_storage.ts => storage.ts} (69%) delete mode 100644 components/brave_new_tab_ui/storage/grid_sites_storage.ts create mode 100644 components/test/brave_new_tab_ui/actions/new_tab_actions_test.ts create mode 100644 components/test/brave_new_tab_ui/api/topSites/bookmarks_test.ts create mode 100644 components/test/brave_new_tab_ui/api/topSites/grid_test.ts delete mode 100644 components/test/brave_new_tab_ui/reducers/grid_sites_reducer_test.ts delete mode 100644 components/test/brave_new_tab_ui/state/gridSitesState_test.ts diff --git a/components/brave_new_tab_ui/actions/grid_sites_actions.ts b/components/brave_new_tab_ui/actions/grid_sites_actions.ts deleted file mode 100644 index b82ea788ee9f..000000000000 --- a/components/brave_new_tab_ui/actions/grid_sites_actions.ts +++ /dev/null @@ -1,69 +0,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/. - -// 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 b6361bcffe5a..e54e20912055 100644 --- a/components/brave_new_tab_ui/actions/new_tab_actions.ts +++ b/components/brave_new_tab_ui/actions/new_tab_actions.ts @@ -1,7 +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/. +/* 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' @@ -12,6 +11,56 @@ 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 deleted file mode 100644 index 9a4f2867cf05..000000000000 --- a/components/brave_new_tab_ui/api/bookmarks.ts +++ /dev/null @@ -1,47 +0,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/. - -/** - * 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 129e508400f8..8af578b6510f 100644 --- a/components/brave_new_tab_ui/api/getActions.ts +++ b/components/brave_new_tab_ui/api/getActions.ts @@ -1,22 +1,19 @@ -// 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/. +/* 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 & typeof gridSitesActions +let actions: typeof newTabActions export default function getActions () { if (actions) { return actions } - const allActions = Object.assign({}, newTabActions, gridSitesActions) - actions = bindActionCreators(allActions, store.dispatch.bind(store)) + actions = bindActionCreators(newTabActions, 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 2a382e1872d4..92256961362f 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: chrome.topSites.MostVisitedURL[] + topSites: topSitesAPI.TopSitesData, brandedWallpaperData: undefined | NewTab.BrandedWallpaper } diff --git a/components/brave_new_tab_ui/api/topSites/bookmarks.ts b/components/brave_new_tab_ui/api/topSites/bookmarks.ts new file mode 100644 index 000000000000..a933221c047f --- /dev/null +++ b/components/brave_new_tab_ui/api/topSites/bookmarks.ts @@ -0,0 +1,39 @@ +/* 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 new file mode 100644 index 000000000000..c204f2d11275 --- /dev/null +++ b/components/brave_new_tab_ui/api/topSites/dnd.ts @@ -0,0 +1,42 @@ +/* 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 new file mode 100644 index 000000000000..9a37884936bf --- /dev/null +++ b/components/brave_new_tab_ui/api/topSites/grid.ts @@ -0,0 +1,60 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// 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/api/topSites.ts b/components/brave_new_tab_ui/api/topSites/index.ts similarity index 59% rename from components/brave_new_tab_ui/api/topSites.ts rename to components/brave_new_tab_ui/api/topSites/index.ts index 13970960654c..b617b109e912 100644 --- a/components/brave_new_tab_ui/api/topSites.ts +++ b/components/brave_new_tab_ui/api/topSites/index.ts @@ -1,14 +1,16 @@ -// Copyright (c) 2020 The Brave Authors. All rights reserved. +// 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/. +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: chrome.topSites.MostVisitedURL[]) => { + chrome.topSites.get((topSites: NewTab.Site[]) => { resolve(topSites || []) }) }) diff --git a/components/brave_new_tab_ui/apiEventsToStore.ts b/components/brave_new_tab_ui/apiEventsToStore.ts index e2e5d9178612..f94d6e890294 100644 --- a/components/brave_new_tab_ui/apiEventsToStore.ts +++ b/components/brave_new_tab_ui/apiEventsToStore.ts @@ -38,8 +38,6 @@ 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 1f82e4b03815..3436fc9e01d3 100644 --- a/components/brave_new_tab_ui/components/default/clock/style.ts +++ b/components/brave_new_tab_ui/components/default/clock/style.ts @@ -1,9 +1,8 @@ -// 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/. +/* 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 styled from 'styled-components' export const StyledClock = styled<{}, 'div'>('div')` color: #FFFFFF; diff --git a/components/brave_new_tab_ui/components/default/index.ts b/components/brave_new_tab_ui/components/default/index.ts index 9306224ac3d2..76b819d65181 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 './gridSites' +import { ListWidget, Tile, TileActionsContainer, TileAction, TileFavicon } from './topSites' import { SiteRemovalNotification, SiteRemovalText, SiteRemovalAction } from './notification' import { ClockWidget } from './clock' import RewardsWidget from './rewards' diff --git a/components/brave_new_tab_ui/components/default/gridSites/index.ts b/components/brave_new_tab_ui/components/default/topSites/index.ts similarity index 83% rename from components/brave_new_tab_ui/components/default/gridSites/index.ts rename to components/brave_new_tab_ui/components/default/topSites/index.ts index dccc30c23753..b0fc8f9660aa 100644 --- a/components/brave_new_tab_ui/components/default/gridSites/index.ts +++ b/components/brave_new_tab_ui/components/default/topSites/index.ts @@ -1,9 +1,9 @@ -// 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/. +/* 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,8 +48,7 @@ interface TileActionProps { standalone?: boolean } -export const TileAction = styled('button')` - -webkit-appearance: none; +export const TileAction = styled('a')` box-sizing: border-box; transition: color 0.1s linear; color: #424242; @@ -63,9 +62,9 @@ export const TileAction = styled('button')` 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; @@ -89,8 +88,6 @@ export const Tile = styled('div')` width: 80px; height: 80px; font-size: 38px; - z-index: 3; - cursor: grab; &:hover { ${TileActionsContainer} { @@ -107,4 +104,4 @@ export const TileFavicon = styled<{}, 'img'>('img')` object-fit: contain; ` -export const ListWidget = List +export const ListWidget = createWidget(List) 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 5aeb73add962..19a15830fb09 100644 --- a/components/brave_new_tab_ui/components/default/widget/styles.ts +++ b/components/brave_new_tab_ui/components/default/widget/styles.ts @@ -1,7 +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/. +/* 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 deleted file mode 100644 index 77589f604b1c..000000000000 --- a/components/brave_new_tab_ui/constants/grid_sites_types.ts +++ /dev/null @@ -1,21 +0,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 { - 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 428557a447b9..d5d635e5c5f3 100644 --- a/components/brave_new_tab_ui/constants/new_tab_types.ts +++ b/components/brave_new_tab_ui/constants/new_tab_types.ts @@ -1,9 +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/. +/* 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 deleted file mode 100644 index b809e01b3f4e..000000000000 --- a/components/brave_new_tab_ui/constants/new_tab_ui.ts +++ /dev/null @@ -1,6 +0,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 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 0854ce6e08f2..cb27f05bfbdd 100644 --- a/components/brave_new_tab_ui/containers/app.tsx +++ b/components/brave_new_tab_ui/containers/app.tsx @@ -13,18 +13,16 @@ 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: typeof newTabActions & typeof gridSitesActions + actions: any newTabData: NewTab.State - gridSitesData: NewTab.GridSitesState } class DefaultPage extends React.Component { render () { - const { newTabData, gridSitesData, actions } = this.props + const { newTabData, actions } = this.props // don't render if user prefers an empty page if (this.props.newTabData.showEmptyPage && !this.props.newTabData.isIncognito) { @@ -36,7 +34,6 @@ class DefaultPage extends React.Component { : ( { } const mapStateToProps = (state: NewTab.ApplicationState) => ({ - newTabData: state.newTabData, - gridSitesData: state.gridSitesData + newTabData: state.newTabData }) -const mapDispatchToProps = (dispatch: Dispatch) => { - const allActions = Object.assign({}, newTabActions, gridSitesActions) - return { - actions: bindActionCreators(allActions, dispatch) - } -} +const mapDispatchToProps = (dispatch: Dispatch) => ({ + actions: bindActionCreators(newTabActions, 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 new file mode 100644 index 000000000000..00425a46943f --- /dev/null +++ b/components/brave_new_tab_ui/containers/newTab/block.tsx @@ -0,0 +1,179 @@ +/* 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 deleted file mode 100644 index c45ec95c57fa..000000000000 --- a/components/brave_new_tab_ui/containers/newTab/gridSites.tsx +++ /dev/null @@ -1,88 +0,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' - -// 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 deleted file mode 100644 index 0d8ca199c560..000000000000 --- a/components/brave_new_tab_ui/containers/newTab/gridTile.tsx +++ /dev/null @@ -1,118 +0,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 { 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 a76d951f1b08..eb0567814772 100644 --- a/components/brave_new_tab_ui/containers/newTab/index.tsx +++ b/components/brave_new_tab_ui/containers/newTab/index.tsx @@ -1,37 +1,29 @@ -// 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/. +/* 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 TopSitesGrid from './gridSites' +import Block from './block' import FooterInfo from './footerInfo' import SiteRemovalNotification from './notification' import { ClockWidget as Clock, + ListWidget as List, RewardsWidget as Rewards, 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 - gridSitesData: NewTab.GridSitesState - actions: typeof newTabActions & typeof gridSitesActions + actions: any saveShowBackgroundImage: (value: boolean) => void saveShowClock: (value: boolean) => void saveShowTopSites: (value: boolean) => void @@ -87,7 +79,7 @@ class NewTabPage extends React.Component { componentDidMount () { // if a notification is open at component mounting time, close it - this.props.actions.showGridSiteRemovedNotification(false) + this.props.actions.onHideSiteRemovalNotification() this.imageSource = GetBackgroundImageSrc(this.props) this.trackCachedImage() if (GetShouldShowBrandedWallpaperNotification(this.props)) { @@ -143,16 +135,32 @@ class NewTabPage extends React.Component { this.visibilityTimer.stopTracking() } - 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 + 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) } - const items = arrayMove(gridSitesData.gridSites, oldIndex, newIndex) - this.props.actions.gridSitesDataUpdated(items) + } + + onTogglePinnedTopSite (site: NewTab.Site) { + if (!site.pinned) { + this.props.actions.sitePinned(site.url) + } else { + this.props.actions.siteUnpinned(site.url) + } + } + + onIgnoredTopSite (site: NewTab.Site) { + this.props.actions.siteIgnored(site.url) } toggleShowBackgroundImage = () => { @@ -289,7 +297,7 @@ class NewTabPage extends React.Component { } render () { - const { newTabData, gridSitesData, actions } = this.props + const { newTabData, actions } = this.props const { showSettingsMenu } = this.state if (!newTabData) { @@ -298,7 +306,7 @@ class NewTabPage extends React.Component { const hasImage = this.imageSource !== undefined const isShowingBrandedWallpaper = newTabData.brandedWallpaperData ? true : false - const showTopSites = !!this.props.gridSitesData.gridSites.length && newTabData.showTopSites + const showTopSites = !!this.props.newTabData.gridSites.length && newTabData.showTopSites const cryptoContent = this.renderCryptoContent() return ( @@ -342,27 +350,40 @@ class NewTabPage extends React.Component { /> } - { - showTopSites - ? ( - - + { + this.props.newTabData.gridSites.map((site: NewTab.Site) => + - - ) : null + ) + } + } { - gridSitesData.shouldShowSiteRemovedNotification - ? ( - - - - ) : null + this.props.newTabData.showSiteRemovalNotification + ? + + + : null } {cryptoContent} @@ -405,4 +426,4 @@ class NewTabPage extends React.Component { } } -export default NewTabPage +export default DragDropContext(HTML5Backend)(NewTabPage) diff --git a/components/brave_new_tab_ui/containers/newTab/notification.tsx b/components/brave_new_tab_ui/containers/newTab/notification.tsx index 0c7ca31b5a11..4da2d7ac3840 100644 --- a/components/brave_new_tab_ui/containers/newTab/notification.tsx +++ b/components/brave_new_tab_ui/containers/newTab/notification.tsx @@ -1,7 +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/. +/* 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' @@ -18,48 +17,32 @@ 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: typeof newTabActions & typeof gridSitesActions + actions: any } export default class Notification extends React.Component { - onUndoRemoveTopSite = () => { - this.props.actions.undoRemoveGridSite() - this.props.actions.showGridSiteRemovedNotification(false) + onUndoIgnoredTopSite = () => { + this.props.actions.undoSiteIgnored() } - onUndoRemoveAllTopSites = () => { - this.props.actions.undoRemoveAllGridSites() - this.props.actions.showGridSiteRemovedNotification(false) + onUndoAllSiteIgnored = () => { + this.props.actions.undoAllSiteIgnored() } onHideSiteRemovalNotification = () => { - this.props.actions.showGridSiteRemovedNotification(false) + this.props.actions.onHideSiteRemovalNotification() } 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 3cf63ad520ae..a161aebd42ac 100644 --- a/components/brave_new_tab_ui/helpers/newTabUtils.ts +++ b/components/brave_new_tab_ui/helpers/newTabUtils.ts @@ -1,7 +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/. +/* 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) { @@ -10,69 +9,19 @@ export const isHttpOrHttps = (url?: string) => { return /^https?:/i.test(url) } -export const getCharForSite = ( - topSite: chrome.topSites.MostVisitedURL -): string => { - let hostname: string = '?' - if (!topSite.title) { +/** + * 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) { try { - hostname = new window.URL(topSite.url || '').hostname - // tslint:disable-next-line: no-empty - } catch (e) {} + name = new window.URL(site.url || '').hostname + } catch (e) { + console.warn('getCharForSite', { url: site.url || '' }) + } } - const name: string = topSite.title || hostname + name = site.title || name || '?' return name.charAt(0).toUpperCase() } - -export const generateGridSiteId = (currentIndex: number): string => { - return `topsite-${currentIndex}-${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 -): NewTab.Site => { - return { - ...topSite, - id: generateGridSiteId(index), - letter: getCharForSite(topSite), - favicon: generateGridSiteFavicon(topSite.url), - pinnedIndex: 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 -} diff --git a/components/brave_new_tab_ui/reducers/grid_sites_reducer.ts b/components/brave_new_tab_ui/reducers/grid_sites_reducer.ts deleted file mode 100644 index 2a6c0228db42..000000000000 --- a/components/brave_new_tab_ui/reducers/grid_sites_reducer.ts +++ /dev/null @@ -1,101 +0,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/. - -// 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: { - 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 1bd133ca5d04..f7b2126fe35a 100644 --- a/components/brave_new_tab_ui/reducers/index.ts +++ b/components/brave_new_tab_ui/reducers/index.ts @@ -1,15 +1,12 @@ -// 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/. +/* 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' -// Reducers +// Utils import newTabReducer from './new_tab_reducer' -import gridSitesReducer from './grid_sites_reducer' export default combineReducers({ - newTabData: newTabReducer, - gridSitesData: gridSitesReducer + newTabData: newTabReducer }) diff --git a/components/brave_new_tab_ui/reducers/new_tab_reducer.ts b/components/brave_new_tab_ui/reducers/new_tab_reducer.tsx similarity index 59% rename from components/brave_new_tab_ui/reducers/new_tab_reducer.ts rename to components/brave_new_tab_ui/reducers/new_tab_reducer.tsx index eb0696e69d3c..071d36aca6fa 100644 --- a/components/brave_new_tab_ui/reducers/new_tab_reducer.ts +++ b/components/brave_new_tab_ui/reducers/new_tab_reducer.tsx @@ -1,7 +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/. +/* 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' @@ -12,10 +11,13 @@ 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/new_tab_storage' +import * as storage from '../storage' import { getTotalContributions } from '../rewards-utils' const initialState = storage.load() @@ -28,7 +30,7 @@ function performSideEffect (fn: SideEffectFunction): void { window.setTimeout(() => fn(sideEffectState), 0) } -export const newTabReducer: Reducer = (state: NewTab.State | undefined, action) => { +export const newTabReducer: Reducer = (state: NewTab.State | undefined, action: any) => { console.timeStamp('reducer ' + action.type) if (state === undefined) { console.timeStamp('reducer init') @@ -46,8 +48,9 @@ export const newTabReducer: Reducer = (state: NewTab.S initialDataLoaded: true, ...initialDataPayload.preferences, stats: initialDataPayload.stats, - brandedWallpaperData: initialDataPayload.brandedWallpaperData, - ...initialDataPayload.privateTabData + ...initialDataPayload.privateTabData, + topSites: initialDataPayload.topSites, + brandedWallpaperData: initialDataPayload.brandedWallpaperData } // TODO(petemill): only get backgroundImage if no sponsored background this time. // ...We would also have to set the value at the action @@ -57,8 +60,18 @@ 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() @@ -68,6 +81,160 @@ 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 deleted file mode 100644 index babcd599e136..000000000000 --- a/components/brave_new_tab_ui/state/gridSitesState.ts +++ /dev/null @@ -1,214 +0,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 { - generateGridSiteProperties, - isExistingGridSite, - getGridSitesWhitelist, - isGridSitePinned, - isGridSiteBookmarked -} from '../helpers/newTabUtils' - -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)) - } - state = gridSitesReducerAddSiteOrSites(state, newGridSites) - 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 = state.gridSites - .filter((site: NewTab.Site) => { - // In updatedGridSites we only want sites not removed by the user - return state.removedSites - .every((removedSite: NewTab.Site) => removedSite.url !== site.url) - }) - 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/new_tab_storage.ts b/components/brave_new_tab_ui/storage.ts similarity index 69% rename from components/brave_new_tab_ui/storage/new_tab_storage.ts rename to components/brave_new_tab_ui/storage.ts index 1c03d8388c56..325bb48fe6ee 100644 --- a/components/brave_new_tab_ui/storage/new_tab_storage.ts +++ b/components/brave_new_tab_ui/storage.ts @@ -1,14 +1,13 @@ -// 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/. +/* 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 defaultState: NewTab.State = { +const defaultState: NewTab.State = { initialDataLoaded: false, textDirection: window.loadTimeData.getString('textdirection'), featureFlagBraveNTPBrandedWallpaper: window.loadTimeData.getBoolean('featureFlagBraveNTPBrandedWallpaper'), @@ -19,11 +18,16 @@ export const defaultState: NewTab.State = { showRewards: 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, @@ -57,6 +61,23 @@ 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 + } + + 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 @@ -76,8 +97,7 @@ const cleanData = (state: NewTab.State) => { export const load = (): NewTab.State => { const data: string | null = window.localStorage.getItem(keyName) let state = defaultState - let storedState - + let storedState: NewTab.PersistentState if (data) { try { storedState = JSON.parse(data) @@ -87,7 +107,7 @@ export const load = (): NewTab.State => { ...storedState } } catch (e) { - console.error('[NewTabData] Could not parse local storage data: ', e) + console.error('Could not parse local storage data: ', e) } } return cleanData(state) @@ -95,11 +115,7 @@ export const load = (): NewTab.State => { export const debouncedSave = debounce((data: NewTab.State) => { if (data) { - const dataToSave = { - showEmptyPage: data.showEmptyPage, - rewardsState: data.rewardsState, - currentStackWidget: data.currentStackWidget - } + const dataToSave = getPersistentData(data) window.localStorage.setItem(keyName, JSON.stringify(dataToSave)) } }, 50) diff --git a/components/brave_new_tab_ui/storage/grid_sites_storage.ts b/components/brave_new_tab_ui/storage/grid_sites_storage.ts deleted file mode 100644 index 874598815659..000000000000 --- a/components/brave_new_tab_ui/storage/grid_sites_storage.ts +++ /dev/null @@ -1,41 +0,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' - -const keyName = 'grid-sites-data' - -export const initialGridSitesState: NewTab.GridSitesState = { - gridSites: [], - removedSites: [], - shouldShowSiteRemovedNotification: false -} - -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/store.ts b/components/brave_new_tab_ui/store.ts index 1488da172606..ab32f65ee84e 100644 --- a/components/brave_new_tab_ui/store.ts +++ b/components/brave_new_tab_ui/store.ts @@ -1,15 +1,10 @@ -// 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/. +/* 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 { createStore, applyMiddleware } from 'redux' +import { createStore } from 'redux' -// Thunk for async actions -import thunk from 'redux-thunk' +// Utils +import reducers from './reducers' -// Feature core reducer -import rootReducer from './reducers' - -export default createStore(rootReducer, applyMiddleware(thunk)) +export default createStore(reducers) diff --git a/components/brave_new_tab_ui/stories/default.tsx b/components/brave_new_tab_ui/stories/default.tsx index 8d70baa7507e..9de8a68f8ab7 100644 --- a/components/brave_new_tab_ui/stories/default.tsx +++ b/components/brave_new_tab_ui/stories/default.tsx @@ -12,10 +12,9 @@ import BraveCoreThemeProvider from '../../common/BraveCoreThemeProvider' // Components import NewTabPage from '../containers/newTab' -import * as newTabActions from '../actions/new_tab_actions' -import * as gridSitesActions from '../actions/grid_sites_actions' +import * as actions from '../actions/new_tab_actions' import store from '../store' -import { getNewTabData, getGridSitesData } from './default/data/storybookState' +import { getNewTabData } from './default/data/storybookState' export default function Provider ({ story }: any) { return ( @@ -40,13 +39,10 @@ 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) => ({ ...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), showTopSites: boolean('Show top sites?', true), showRewards: boolean('Show rewards?', true), textDirection: select('Text direction', { ltr: 'ltr', rtl: 'rtl' } , 'ltr'), + gridSites: generateTopSites(defaultTopSitesData), stats: { ...state.stats, adsBlockedStat: number('Number of blocked items', 1337), @@ -68,10 +63,3 @@ export const getNewTabData = (state: NewTab.State = defaultState) => ({ 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 950ced9802d4..b02886c9c84a 100644 --- a/components/definitions/newTab.d.ts +++ b/components/definitions/newTab.d.ts @@ -18,7 +18,6 @@ declare namespace NewTab { } export interface ApplicationState { newTabData: State | undefined - gridSitesData: GridSitesState | undefined } export interface Image { @@ -31,13 +30,16 @@ declare namespace NewTab { } export interface Site { - id: string + index: number url: string title: string favicon: string letter: string - pinnedIndex: number | undefined - bookmarkInfo: chrome.bookmarks.BookmarkTreeNode | undefined + thumb: string + themeColor: string + computedThemeColor: string + pinned: boolean + bookmarked?: Bookmark } export interface Stats { @@ -58,23 +60,13 @@ declare namespace NewTab { export type StackWidget = 'rewards' | 'exchange' - export interface GridSitesState { - removedSites: Site[] - gridSites: Site[] - shouldShowSiteRemovedNotification: boolean - } - - export interface PageState { - showEmptyPage: boolean - } - - export interface RewardsState { - rewardsState: RewardsWidgetState - currentStackWidget: StackWidget - } - export interface PersistentState { + topSites: Site[] + ignoredTopSites: Site[] + pinnedTopSites: Site[] + gridSites: Site[] showEmptyPage: boolean + bookmarks: Record rewardsState: RewardsWidgetState currentStackWidget: StackWidget } @@ -89,7 +81,7 @@ declare namespace NewTab { isQwant: boolean backgroundImage?: Image gridLayoutSize?: 'small' - showGridSiteRemovedNotification?: boolean + showSiteRemovalNotification?: 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 new file mode 100644 index 000000000000..613897612c25 --- /dev/null +++ b/components/test/brave_new_tab_ui/actions/new_tab_actions_test.ts @@ -0,0 +1,164 @@ +/* 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 cda291f15d43..47299ed4b6b2 100644 --- a/components/test/brave_new_tab_ui/api/data_test.ts +++ b/components/test/brave_new_tab_ui/api/data_test.ts @@ -4,21 +4,19 @@ import getActions from '../../../brave_new_tab_ui/api/getActions' import { getTopSites } from '../../../brave_new_tab_ui/api/topSites' -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' +import * as actions from '../../../brave_new_tab_ui/actions/new_tab_actions' +import { types } from '../../../brave_new_tab_ui/constants/new_tab_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().showGridSiteRemovedNotification(true)).toEqual({ - payload: { shouldShow: true }, - type: topSitesTypes.GRID_SITES_SHOW_SITE_REMOVED_NOTIFICATION + expect(getActions().onHideSiteRemovalNotification()).toEqual({ + payload: undefined, + type: types.NEW_TAB_HIDE_SITE_REMOVAL_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 new file mode 100644 index 000000000000..0668c52425ec --- /dev/null +++ b/components/test/brave_new_tab_ui/api/topSites/bookmarks_test.ts @@ -0,0 +1,52 @@ +/* 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 new file mode 100644 index 000000000000..0b8ed76e427c --- /dev/null +++ b/components/test/brave_new_tab_ui/api/topSites/grid_test.ts @@ -0,0 +1,51 @@ +/* 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 2c7941dc7346..2b1f58ce03df 100644 --- a/components/test/brave_new_tab_ui/helpers/newTabUtils_test.ts +++ b/components/test/brave_new_tab_ui/helpers/newTabUtils_test.ts @@ -1,217 +1,73 @@ -// 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/. +/* 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 newTabUtils from '../../../brave_new_tab_ui/helpers/newTabUtils' +import { isHttpOrHttps, getCharForSite } 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(newTabUtils.isHttpOrHttps(url)).toBe(true) + expect(isHttpOrHttps(url)).toBe(true) }) it('matches https when defined as a protocol type', () => { const url = 'https://some-nice-safe-website.com' - expect(newTabUtils.isHttpOrHttps(url)).toBe(true) + expect(isHttpOrHttps(url)).toBe(true) }) it('does not match http when defined as an origin', () => { const url = 'file://http.some-website-tricking-you.com' - expect(newTabUtils.isHttpOrHttps(url)).toBe(false) + expect(isHttpOrHttps(url)).toBe(false) }) it('does not match https when defined as an origin', () => { const url = 'file://https.some-website-tricking-you.com' - expect(newTabUtils.isHttpOrHttps(url)).toBe(false) + expect(isHttpOrHttps(url)).toBe(false) }) it('does not match other protocol', () => { const url = 'ftp://some-old-website.com' - expect(newTabUtils.isHttpOrHttps(url)).toBe(false) + expect(isHttpOrHttps(url)).toBe(false) }) it('does not match when url is not defined', () => { const url = undefined - expect(newTabUtils.isHttpOrHttps(url)).toBe(false) + expect(isHttpOrHttps(url)).toBe(false) }) it('matches uppercase http', () => { const url = 'HTTP://SCREAMING-SAFE-WEBSITE.COM' - expect(newTabUtils.isHttpOrHttps(url)).toBe(true) + expect(isHttpOrHttps(url)).toBe(true) }) it('matches uppercase https', () => { const url = 'HTTP://SCREAMING-UNSAFE-WEBSITE.COM' - expect(newTabUtils.isHttpOrHttps(url)).toBe(true) + expect(isHttpOrHttps(url)).toBe(true) }) }) describe('getCharForSite', () => { it('returns the first letter of a given URL without subdomains', () => { - const url: chrome.topSites.MostVisitedURL = { - url: 'https://brave.com', - title: 'brave' - } - expect(newTabUtils.getCharForSite(url)).toBe('B') + const url = { url: 'https://brave.com' } + expect(getCharForSite(url)).toBe('B') }) it('returns the first letter of a given URL with subdomains', () => { - const url: chrome.topSites.MostVisitedURL = { - url: 'https://awesome-sub-domain.brave.com', - title: 'awesome' - } - expect(newTabUtils.getCharForSite(url)).toBe('A') + const url = { url: 'https://awesome-sub-domain.brave.com' } + expect(getCharForSite(url)).toBe('A') }) it('returns the first letter of a given URL with ports', () => { - const url: chrome.topSites.MostVisitedURL = { - url: 'https://brave.com:9999', - title: 'brave' - } - expect(newTabUtils.getCharForSite(url)).toBe('B') + const url = { url: 'https://brave.com:9999' } + expect(getCharForSite(url)).toBe('B') }) it('returns the first letter of a given URL with paths', () => { - const url: chrome.topSites.MostVisitedURL = { - url: 'https://brave.com/hello-test/', title: 'brave' - } - expect(newTabUtils.getCharForSite(url)).toBe('B') + const url = { url: 'https://brave.com/hello-test/' } + expect(getCharForSite(url)).toBe('B') }) it('returns the first letter of a given URL with queries', () => { - const url: chrome.topSites.MostVisitedURL = { - url: 'https://brave.com/?randomId', - title: 'brave' - } - expect(newTabUtils.getCharForSite(url)).toBe('B') + const url = { url: 'https://brave.com/?randomId' } + expect(getCharForSite(url)).toBe('B') }) it('returns the first letter of a given URL with parameters', () => { - const url: chrome.topSites.MostVisitedURL = { - url: 'https://brave.com/?randomId=123123123', - title: 'brave' - } - expect(newTabUtils.getCharForSite(url)).toBe('B') + const url = { url: 'https://brave.com/?randomId=123123123' } + expect(getCharForSite(url)).toBe('B') }) it('returns the first letter of a given URL with fragments', () => { - 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', () => { - const index: number = 1337 - // Test via startsWith to avoid calling Date.now() which - // will often fail all tests - const assertion: string = newTabUtils.generateGridSiteId(index) - expect(assertion.startsWith(`topsite-${index}`)) - .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('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) + const url = { url: 'https://brave.com/?randomId=123123123&hl=en#00h00m10s' } + expect(getCharForSite(url)).toBe('B') }) }) }) 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 deleted file mode 100644 index 961b8fdb6102..000000000000 --- a/components/test/brave_new_tab_ui/reducers/grid_sites_reducer_test.ts +++ /dev/null @@ -1,491 +0,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 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 c7bc5d584bae..b02b7278e39c 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,88 +2,272 @@ * 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 storage from '../../../brave_new_tab_ui/storage/new_tab_storage' +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 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, { type: {} }) + const returnedState = newTabReducer(undefined, {}) expect(returnedState).toEqual(expectedState) }) }) + describe('BOOKMARK_ADDED', () => { + let spy: jest.SpyInstance + beforeEach(() => { + spy = jest.spyOn(chrome.bookmarks, 'create') + }) + afterEach(() => { + spy.mockRestore() + }) - describe('NEW_TAB_SET_INITIAL_DATA', () => { - // TODO + 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_STATS_UPDATED', () => { - // 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_PRIVATE_TAB_DATA_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_DISMISS_BRANDED_WALLPAPER_NOTIFICATION', () => { - // 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_PREFERENCES_UPDATED', () => { - // 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('rewards features inside new tab page', () => { - describe('CREATE_WALLET', () => { - // TODO + describe('NEW_TAB_UNDO_SITE_IGNORED', () => { + let spy: jest.SpyInstance + beforeEach(() => { + spy = jest.spyOn(gridAPI, 'calculateGridSites') }) - describe('ON_ENABLED_MAIN', () => { - // TODO + afterEach(() => { + spy.mockRestore() }) - describe('CREATE_WALLET', () => { - // 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('ON_ENABLED_MAIN', () => { - // TODO + }) + describe('NEW_TAB_UNDO_ALL_SITE_IGNORED', () => { + let spy: jest.SpyInstance + beforeEach(() => { + spy = jest.spyOn(gridAPI, 'calculateGridSites') }) - describe('ON_WALLET_INITIALIZED', () => { - // TODO + afterEach(() => { + spy.mockRestore() }) - describe('WALLET_CORRUPT', () => { - // TODO + it('calls gridAPI.calculateGridSites', () => { + jest.useFakeTimers() + newTabReducer(fakeState, { + type: types.NEW_TAB_UNDO_ALL_SITE_IGNORED + }) + jest.runAllTimers() + expect(spy).toBeCalled() }) - describe('WALLET_CREATED', () => { - // 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('LEDGER_OK', () => { - // TODO + }) + describe('NEW_TAB_SITE_DRAGGED', () => { + let spy: jest.SpyInstance + beforeEach(() => { + spy = jest.spyOn(dndAPI, 'onDraggedSite') }) - describe('ON_ADS_ENABLED', () => { - // TODO + afterEach(() => { + spy.mockRestore() }) - describe('ON_ADS_ESTIMATED_EARNINGS', () => { - // 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_BALANCE_REPORT', () => { - // TODO + }) + describe('NEW_TAB_SITE_DRAG_END', () => { + let spy: jest.SpyInstance + beforeEach(() => { + spy = jest.spyOn(dndAPI, 'onDragEnd') }) - describe('DISMISS_NOTIFICATION', () => { - // TODO + afterEach(() => { + spy.mockRestore() }) - describe('ON_PROMOTIONS', () => { - // TODO + it('calls dndAPI.onDragEnd', () => { + newTabReducer(fakeState, { + type: types.NEW_TAB_SITE_DRAG_END + }) + expect(spy).toBeCalled() }) - describe('ON_BALANCE', () => { - // TODO + }) + describe('NEW_TAB_BOOKMARK_INFO_AVAILABLE', () => { + let spy: jest.SpyInstance + beforeEach(() => { + spy = jest.spyOn(bookmarksAPI, 'updateBookmarkInfo') }) - describe('ON_WALLET_EXISTS', () => { - // TODO + afterEach(() => { + spy.mockRestore() }) - describe('SET_PRE_INITIAL_REWARDS_DATA', () => { - // 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_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/' } ] + }) }) }) }) diff --git a/components/test/brave_new_tab_ui/state/gridSitesState_test.ts b/components/test/brave_new_tab_ui/state/gridSitesState_test.ts deleted file mode 100644 index c50437f8f5ec..000000000000 --- a/components/test/brave_new_tab_ui/state/gridSitesState_test.ts +++ /dev/null @@ -1,322 +0,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/. - -// 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 b284da35ac96..a38cb0e69ae9 100644 --- a/components/test/testData.ts +++ b/components/test/testData.ts @@ -44,13 +44,15 @@ export const newTabInitialState: NewTab.ApplicationState = { showBackgroundImage: false, showSettingsMenu: false, topSites: [], - excludedSites: [], + ignoredTopSites: [], + pinnedTopSites: [], 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 fcbd3cb4f57d..7db818a8df24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1361,6 +1361,7 @@ "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" }, @@ -1368,7 +1369,8 @@ "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==" + "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==", + "dev": true } } }, @@ -4432,7 +4434,8 @@ "acorn": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", - "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==" + "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", + "dev": true }, "buffer": { "version": "4.9.2", @@ -4752,7 +4755,7 @@ "@webassemblyjs/helper-module-context": "1.8.5", "@webassemblyjs/wasm-edit": "1.8.5", "@webassemblyjs/wasm-parser": "1.8.5", - "acorn": "^6.2.1", + "acorn": "^7.1.1", "ajv": "^6.10.2", "ajv-keywords": "^3.4.1", "chrome-trace-event": "^1.0.2", @@ -4773,12 +4776,6 @@ "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.10.2", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", @@ -5124,7 +5121,8 @@ "acorn": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", - "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==" + "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", + "dev": true }, "buffer": { "version": "4.9.2", @@ -5200,7 +5198,7 @@ "@webassemblyjs/helper-module-context": "1.8.5", "@webassemblyjs/wasm-edit": "1.8.5", "@webassemblyjs/wasm-parser": "1.8.5", - "acorn": "^6.2.1", + "acorn": "^7.1.1", "ajv": "^6.10.2", "ajv-keywords": "^3.4.1", "chrome-trace-event": "^1.0.2", @@ -5221,12 +5219,6 @@ "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.10.2", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", @@ -5616,15 +5608,6 @@ "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", @@ -5891,15 +5874,6 @@ "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", @@ -6250,6 +6224,11 @@ } } }, + "acorn": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", + "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==" + }, "acorn-dynamic-import": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz", @@ -6261,14 +6240,14 @@ "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.0.tgz", "integrity": "sha512-hMtHj3s5RnuhvHPowpBYvJVj3rAar82JiDQHvGs1zO0l10ocX/xEdBShNHTJaboucJUsScghp74pH3s7EnHHQw==", "requires": { - "acorn": "^6.0.1", + "acorn": "^7.1.1", "acorn-walk": "^6.0.1" }, "dependencies": { "acorn": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", - "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==" + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", + "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==" } } }, @@ -6503,11 +6482,6 @@ "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", @@ -6662,6 +6636,12 @@ "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", @@ -9337,7 +9317,8 @@ "acorn": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", - "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==" + "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", + "dev": true }, "ajv": { "version": "6.10.2", @@ -9481,7 +9462,7 @@ "@webassemblyjs/helper-module-context": "1.8.5", "@webassemblyjs/wasm-edit": "1.8.5", "@webassemblyjs/wasm-parser": "1.8.5", - "acorn": "^6.2.1", + "acorn": "^7.1.1", "ajv": "^6.10.2", "ajv-keywords": "^3.4.1", "chrome-trace-event": "^1.0.2", @@ -9500,14 +9481,6 @@ "terser-webpack-plugin": "^1.4.1", "watchpack": "^1.6.0", "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 - } } }, "webpack-sources": { @@ -10093,6 +10066,18 @@ "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", @@ -13935,14 +13920,14 @@ "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==", "dev": true, "requires": { - "acorn": "^6.0.1", + "acorn": "^7.1.1", "acorn-walk": "^6.0.1" }, "dependencies": { "acorn": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", - "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", + "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", "dev": true } } @@ -13977,7 +13962,7 @@ "dev": true, "requires": { "abab": "^2.0.0", - "acorn": "^7.1.0", + "acorn": "^7.1.1", "acorn-globals": "^4.3.2", "array-equal": "^1.0.0", "cssom": "^0.4.1", @@ -15274,7 +15259,7 @@ "integrity": "sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==", "requires": { "abab": "^2.0.0", - "acorn": "^5.5.3", + "acorn": "^7.1.1", "acorn-globals": "^4.1.0", "array-equal": "^1.0.0", "cssom": ">= 0.3.2 < 0.4.0", @@ -15301,11 +15286,6 @@ "xml-name-validator": "^3.0.0" }, "dependencies": { - "acorn": { - "version": "5.7.4", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz", - "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==" - }, "parse5": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", @@ -18289,6 +18269,7 @@ "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", @@ -18816,6 +18797,32 @@ } } }, + "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", @@ -18968,7 +18975,8 @@ "react-is": { "version": "16.8.3", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.3.tgz", - "integrity": "sha512-Y4rC1ZJmsxxkkPuMLwvKvlL1Zfpbcu+Bf4ZigkHup3v9EfdYhAlWAaVyA19olXq2o2mGn0w+dFKvk3pVVlYcIA==" + "integrity": "sha512-Y4rC1ZJmsxxkkPuMLwvKvlL1Zfpbcu+Bf4ZigkHup3v9EfdYhAlWAaVyA19olXq2o2mGn0w+dFKvk3pVVlYcIA==", + "dev": true }, "react-json-view": { "version": "1.19.1", @@ -19115,16 +19123,6 @@ "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", @@ -22268,7 +22266,7 @@ "@webassemblyjs/helper-module-context": "1.8.3", "@webassemblyjs/wasm-edit": "1.8.3", "@webassemblyjs/wasm-parser": "1.8.3", - "acorn": "^6.0.5", + "acorn": "^7.1.1", "acorn-dynamic-import": "^4.0.0", "ajv": "^6.1.0", "ajv-keywords": "^3.1.0", @@ -22291,9 +22289,9 @@ }, "dependencies": { "acorn": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", - "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", + "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", "dev": true }, "schema-utils": { diff --git a/package.json b/package.json index 82b59a3af2ab..cc15375a4cc5 100644 --- a/package.json +++ b/package.json @@ -276,7 +276,6 @@ "@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", @@ -288,7 +287,6 @@ "@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", @@ -311,6 +309,8 @@ "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", @@ -334,7 +334,6 @@ "@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,7 +341,6 @@ "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",