diff --git a/app/browser/api/topSites.js b/app/browser/api/topSites.js index 48f3c9ccfce..a4fc6329a65 100644 --- a/app/browser/api/topSites.js +++ b/app/browser/api/topSites.js @@ -7,39 +7,33 @@ const Immutable = require('immutable') const appActions = require('../../../js/actions/appActions') const debounce = require('../../../js/lib/debounce') -const siteUtil = require('../../../js/state/siteUtil') +const historyState = require('../../common/state/historyState') const {isSourceAboutUrl} = require('../../../js/lib/appUrlUtil') -const aboutNewTabMaxEntries = 100 +const aboutNewTabMaxEntries = 18 let appStore let minCountOfTopSites let minAccessOfTopSites -const compareSites = (site1, site2) => { - if (!site1 || !site2) return false - return site1.get('location') === site2.get('location') && - site1.get('partitionNumber') === site2.get('partitionNumber') -} - const pinnedTopSites = (state) => { - return (state.getIn(['about', 'newtab', 'pinnedTopSites']) || Immutable.List()).setSize(18) + return state.getIn(['about', 'newtab', 'pinnedTopSites'], Immutable.List()) } const ignoredTopSites = (state) => { - return state.getIn(['about', 'newtab', 'ignoredTopSites']) || Immutable.List() + return state.getIn(['about', 'newtab', 'ignoredTopSites'], Immutable.List()) } -const isPinned = (state, siteProps) => { - return pinnedTopSites(state).filter((site) => compareSites(site, siteProps)).size > 0 +const isPinned = (state, siteKey) => { + return pinnedTopSites(state).find(site => site.get('key') === siteKey) } -const isIgnored = (state, siteProps) => { - return ignoredTopSites(state).filter((site) => compareSites(site, siteProps)).size > 0 +const isIgnored = (state, siteKey) => { + return ignoredTopSites(state).includes(siteKey) } const sortCountDescending = (left, right) => { - const leftCount = left.get('count') || 0 - const rightCount = right.get('count') || 0 + const leftCount = left.get('count', 0) + const rightCount = right.get('count', 0) if (leftCount < rightCount) { return 1 } @@ -55,25 +49,6 @@ const sortCountDescending = (left, right) => { return 0 } -const removeDuplicateDomains = (list) => { - const siteDomains = new Set() - return list.filter((site) => { - if (!site.get('location')) { - return false - } - try { - const hostname = require('../../common/urlParse')(site.get('location')).hostname - if (!siteDomains.has(hostname)) { - siteDomains.add(hostname) - return true - } - } catch (e) { - console.log('Error parsing hostname: ', e) - } - return false - }) -} - const calculateTopSites = (clearCache) => { if (clearCache) { clearTopSiteCacheData() @@ -91,18 +66,21 @@ const startCalculatingTopSiteData = debounce(() => { } const state = appStore.getState() // remove folders; sort by visit count; enforce a max limit - const sites = (state.get('sites') ? state.get('sites').toList() : new Immutable.List()) - .filter((site) => !siteUtil.isFolder(site) && - !siteUtil.isImportedBookmark(site) && - !isSourceAboutUrl(site.get('location')) && + const sites = historyState.getSites(state) + .filter((site, key) => !isSourceAboutUrl(site.get('location')) && + !isPinned(state, key) && + !isIgnored(state, key) && (minCountOfTopSites === undefined || (site.get('count') || 0) >= minCountOfTopSites) && - (minAccessOfTopSites === undefined || (site.get('lastAccessedTime') || 0) >= minAccessOfTopSites)) + (minAccessOfTopSites === undefined || (site.get('lastAccessedTime') || 0) >= minAccessOfTopSites) + ) .sort(sortCountDescending) .slice(0, aboutNewTabMaxEntries) + .map((site, key) => site.set('key', key)) + .toList() for (let i = 0; i < sites.size; i++) { - const count = sites.getIn([i, 'count']) || 0 - const access = sites.getIn([i, 'lastAccessedTime']) || 0 + const count = sites.getIn([i, 'count'], 0) + const access = sites.getIn([i, 'lastAccessedTime'], 0) if (minCountOfTopSites === undefined || count < minCountOfTopSites) { minCountOfTopSites = count } @@ -111,32 +89,7 @@ const startCalculatingTopSiteData = debounce(() => { } } - // Filter out pinned and ignored sites - let unpinnedSites = sites.filter((site) => !(isPinned(state, site) || isIgnored(state, site))) - unpinnedSites = removeDuplicateDomains(unpinnedSites) - - // Merge the pinned and unpinned lists together - // Pinned items have priority because the position is important - let gridSites = pinnedTopSites(state).map((pinnedSite) => { - // Fetch latest siteDetail objects from appState.sites using location/partition - if (pinnedSite) { - const matches = sites.filter((site) => compareSites(site, pinnedSite)) - if (matches.size > 0) return matches.first() - } - // Default to unpinned items - const firstSite = unpinnedSites.first() - unpinnedSites = unpinnedSites.shift() - return firstSite - }) - - // Include up to [aboutNewTabMaxEntries] entries so that folks - // can ignore sites and have new items fill those empty spaces - if (unpinnedSites.size > 0) { - gridSites = gridSites.concat(unpinnedSites) - } - - const finalData = gridSites.filter((site) => site != null) - appActions.topSiteDataAvailable(finalData) + appActions.topSiteDataAvailable(sites) }, 5 * 1000) const clearTopSiteCacheData = () => { diff --git a/app/browser/bookmarksExporter.js b/app/browser/bookmarksExporter.js index 598501ed3da..e22f3be31d0 100644 --- a/app/browser/bookmarksExporter.js +++ b/app/browser/bookmarksExporter.js @@ -11,20 +11,31 @@ const electron = require('electron') const dialog = electron.dialog const app = electron.app const BrowserWindow = electron.BrowserWindow -const getSetting = require('../../js/settings').getSetting + +// State +const bookmarkFoldersState = require('../common/state/bookmarkFoldersState') +const bookmarsState = require('../common/state/bookmarksState') + +// Constants const settings = require('../../js/constants/settings') const siteTags = require('../../js/constants/siteTags') + +// Utils +const {getSetting} = require('../../js/settings') const siteUtil = require('../../js/state/siteUtil') -const isWindows = process.platform === 'win32' +const platformUtil = require('../common/lib/platformUtil') + const indentLength = 2 const indentType = ' ' -function showDialog (sites) { +function showDialog (state) { const focusedWindow = BrowserWindow.getFocusedWindow() const fileName = moment().format('DD_MM_YYYY') + '.html' const defaultPath = path.join(getSetting(settings.DEFAULT_DOWNLOAD_SAVE_PATH) || app.getPath('downloads'), fileName) let personal = [] let other = [] + const bookmarks = bookmarsState.getBookmarks(state) + const bookmarkFolders = bookmarkFoldersState.getFolders(state) dialog.showSaveDialog(focusedWindow, { defaultPath: defaultPath, @@ -34,13 +45,14 @@ function showDialog (sites) { }] }, (fileName) => { if (fileName) { - personal = createBookmarkArray(sites) - other = createBookmarkArray(sites, -1, false) + personal = createBookmarkArray(bookmarks, bookmarkFolders) + other = createBookmarkArray(bookmarks, bookmarkFolders, -1, false) fs.writeFileSync(fileName, createBookmarkHTML(personal, other)) } }) } +// TODO refactor this based on the structure (now we send in bookmarks and folders) function createBookmarkArray (sites, parentFolderId, first = true, depth = 1) { const filteredBookmarks = parentFolderId ? sites.filter((site) => site.get('parentFolderId') === parentFolderId) @@ -54,13 +66,12 @@ function createBookmarkArray (sites, parentFolderId, first = true, depth = 1) { filteredBookmarks.forEach((site) => { if (site.get('tags').includes(siteTags.BOOKMARK) && site.get('location')) { - title = site.get('customTitle') || site.get('title') || site.get('location') + title = site.get('title') || site.get('location') payload.push(`${indentNext}
${title}`) } else if (siteUtil.isFolder(site)) { const folderId = site.get('folderId') - title = site.get('customTitle') || site.get('title') - payload.push(`${indentNext}

${title}

`) + payload.push(`${indentNext}

${site.get('title')}

`) payload = payload.concat(createBookmarkArray(sites, folderId, true, (depth + 1))) } }) @@ -71,7 +82,7 @@ function createBookmarkArray (sites, parentFolderId, first = true, depth = 1) { } function createBookmarkHTML (personal, other) { - const breakTag = (isWindows) ? '\r\n' : '\n' + const breakTag = (platformUtil.isWindows()) ? '\r\n' : '\n' const title = 'Bookmarks' return ` diff --git a/app/browser/menu.js b/app/browser/menu.js index a5b550a8619..f241dd0a3c3 100644 --- a/app/browser/menu.js +++ b/app/browser/menu.js @@ -17,7 +17,6 @@ const appConstants = require('../../js/constants/appConstants') const windowConstants = require('../../js/constants/windowConstants') const messages = require('../../js/constants/messages') const settings = require('../../js/constants/settings') -const siteTags = require('../../js/constants/siteTags') // State const {getByTabId} = require('../common/state/tabState') @@ -35,8 +34,8 @@ const frameStateUtil = require('../../js/state/frameStateUtil') const menuUtil = require('../common/lib/menuUtil') const {getSetting} = require('../../js/settings') const locale = require('../locale') -const {isLocationBookmarked} = require('../../js/state/siteUtil') const platformUtil = require('../common/lib/platformUtil') +const bookmarkUtil = require('../common/lib/bookmarkUtil') const isDarwin = platformUtil.isDarwin() const isLinux = platformUtil.isLinux() const isWindows = platformUtil.isWindows() @@ -376,7 +375,7 @@ const updateRecentlyClosedMenuItems = (state) => { } const isCurrentLocationBookmarked = (state) => { - return isLocationBookmarked(state, currentLocation) + return bookmarkUtil.isLocationBookmarked(state, currentLocation) } const createBookmarksSubmenu = (state) => { @@ -406,6 +405,7 @@ const createBookmarksSubmenu = (state) => { CommonMenu.exportBookmarksMenuItem() ] + // TODO we should send sites in, but first level bookmarks and folders const bookmarks = menuUtil.createBookmarkTemplateItems(state.get('sites')) if (bookmarks.length > 0) { submenu.push(CommonMenu.separatorMenuItem) @@ -698,33 +698,15 @@ const doAction = (state, action) => { createMenu(state) } break - case appConstants.APP_ADD_SITE: + case appConstants.APP_ADD_BOOKMARK: case appConstants.APP_EDIT_BOOKMARK: - { - if (action.tag === siteTags.BOOKMARK || action.tag === siteTags.BOOKMARK_FOLDER) { - createMenu(state) - } else if (action.siteDetail && action.siteDetail.constructor === Immutable.List && action.tag === undefined) { - let shouldRebuild = false - action.siteDetail.forEach((site) => { - const tag = site.getIn(['tags', 0]) - if (tag === siteTags.BOOKMARK || tag === siteTags.BOOKMARK_FOLDER) { - shouldRebuild = true - } - }) - if (shouldRebuild) { - createMenu(state) - } - } - break - } - case appConstants.APP_REMOVE_SITE: - { - if (action.tag === siteTags.BOOKMARK || action.tag === siteTags.BOOKMARK_FOLDER) { - createMenu(state) - } - break - } + case appConstants.APP_REMOVE_BOOKMARK: + case appConstants.APP_ADD_BOOKMARK_FOLDER: + case appConstants.APP_EDIT_BOOKMARK_FOLDER: + case appConstants.APP_REMOVE_BOOKMARK_FOLDER: + createMenu(state) + break case appConstants.APP_ON_CLEAR_BROWSING_DATA: { const defaults = state.get('clearBrowsingDataDefaults') diff --git a/app/browser/reducers/sitesReducer.js b/app/browser/reducers/sitesReducer.js index 6f5035404bf..4ff281e0bcf 100644 --- a/app/browser/reducers/sitesReducer.js +++ b/app/browser/reducers/sitesReducer.js @@ -4,18 +4,33 @@ 'use strict' +const Immutable = require('immutable') + +// Actions +const syncActions = require('../../../js/actions/syncActions') +const appActions = require('../../../js/actions/appActions') + +// State +const siteCache = require('../../common/state/siteCache') +const tabState = require('../../common/state/tabState') +const bookmarkFoldersState = require('../../common/state/bookmarkFoldersState') +const pinnedSitesState = require('../../common/state/pinnedSitesState') +const bookmarksState = require('../../common/state/bookmarksState') +const historyState = require('../../common/state/historyState') + +// Constants const appConstants = require('../../../js/constants/appConstants') +const settings = require('../../../js/constants/settings') + +// Utils const filtering = require('../../filtering') -const siteCache = require('../../common/state/siteCache') -const siteTags = require('../../../js/constants/siteTags') const siteUtil = require('../../../js/state/siteUtil') -const syncActions = require('../../../js/actions/syncActions') const syncUtil = require('../../../js/state/syncUtil') -const Immutable = require('immutable') -const settings = require('../../../js/constants/settings') +const pinnedSitesUtil = require('../../common/lib/pinnedSitesUtil') +const bookmarkFoldersUtil = require('../../common/lib/bookmarkFoldersUtil') +const bookmarkUtil = require('../../common/lib/bookmarkUtil') const {getSetting} = require('../../../js/settings') const writeActions = require('../../../js/constants/sync/proto').actions -const tabState = require('../../common/state/tabState') const syncEnabled = () => { return getSetting(settings.SYNC_ENABLED) === true @@ -25,7 +40,7 @@ const updateTabBookmarked = (state, tabValue) => { if (!tabValue || !tabValue.get('tabId')) { return state } - const bookmarked = siteUtil.isLocationBookmarked(state, tabValue.get('url')) + const bookmarked = bookmarkUtil.isLocationBookmarked(state, tabValue.get('url')) return tabState.updateTabValue(state, tabValue.set('bookmarked', bookmarked)) } @@ -48,64 +63,127 @@ const sitesReducer = (state, action, immutableAction) => { const temp = state.get('tempClearBrowsingData', Immutable.Map()) const clearData = defaults ? defaults.merge(temp) : temp if (clearData.get('browserHistory')) { - state = state.set('sites', siteUtil.clearHistory(state.get('sites'))) + state = historyState.clearSites() filtering.clearHistory() } break } - case appConstants.APP_ADD_SITE: + case appConstants.APP_ADD_HISTORY_SITE: { const isSyncEnabled = syncEnabled() - if (Immutable.List.isList(action.siteDetail)) { - action.siteDetail.forEach((s) => { - state = siteUtil.addSite(state, s, action.tag, action.skipSync) - if (isSyncEnabled) { - state = syncUtil.updateSiteCache(state, s) - } - }) - } else { - let sites = state.get('sites') - if (!action.siteDetail.get('folderId') && siteUtil.isFolder(action.siteDetail)) { - action.siteDetail = action.siteDetail.set('folderId', siteUtil.getNextFolderId(sites)) - } - state = siteUtil.addSite(state, action.siteDetail, action.tag, action.skipSync) - if (isSyncEnabled) { - state = syncUtil.updateSiteCache(state, action.siteDetail) - } + state = historyState.addSite(state, action.siteDetail) + if (isSyncEnabled) { + state = syncUtil.updateSiteCache(state, action.siteDetail) } break } case appConstants.APP_ADD_BOOKMARK: - case appConstants.APP_EDIT_BOOKMARK: { const isSyncEnabled = syncEnabled() - const sites = state.get('sites') const closestKey = action.closestKey - let site = action.siteDetail + let bookmark = action.siteDetail - if (site == null || action.tag == null) { + if (bookmark == null) { break } - if (!site.get('folderId') && action.tag === siteTags.BOOKMARK_FOLDER) { - site = site.set('folderId', siteUtil.getNextFolderId(sites)) + state = bookmarksState.addBookmark(state, bookmark) + + // TODO implement move + if (closestKey != null) { + const sourceKey = siteUtil.getSiteKey(bookmark) + state = siteUtil.moveSite(state, sourceKey, closestKey, false, false, true) + } + + if (isSyncEnabled) { + state = syncUtil.updateSiteCache(state, bookmark) + } + + state = updateActiveTabBookmarked(state) + break + } + case appConstants.APP_ADD_BOOKMARKS: + { + // TODO we need to do it like this until we implement action inside actions + action.bookmarkList.forEach(bookmark => appActions.addBookmark(bookmark)) + break + } + case appConstants.APP_EDIT_BOOKMARK: + { + const isSyncEnabled = syncEnabled() + let bookmark = action.siteDetail + + if (bookmark == null) { + break } - state = siteUtil.addSite(state, site, action.tag, action.editKey) + state = bookmarksState.editBookmark(state, bookmark, action.editKey) + if (isSyncEnabled) { + state = syncUtil.updateSiteCache(state, bookmark) + } + + state = updateActiveTabBookmarked(state) + break + } + case appConstants.APP_REMOVE_BOOKMARK: + { + state = bookmarksState.removeBookmark(state, action.bookmarkKey) + state = updateActiveTabBookmarked(state) + break + } + case appConstants.APP_ADD_BOOKMARK_FOLDER: + { + const isSyncEnabled = syncEnabled() + const closestKey = action.closestKey + let folder = action.folderDetails + + if (folder == null) { + break + } + + state = bookmarkFoldersState.addFolder(state, folder) + + // TODO move folder if (closestKey != null) { - const sourceKey = siteUtil.getSiteKey(site) + const sourceKey = siteUtil.getSiteKey(folder) state = siteUtil.moveSite(state, sourceKey, closestKey, false, false, true) } if (isSyncEnabled) { - state = syncUtil.updateSiteCache(state, site) + state = syncUtil.updateSiteCache(state, folder) + } + break + } + case appConstants.APP_ADD_BOOKMARK_FOLDERS: + { + // TODO we need to do it like this until we implement action inside actions + action.folderList.forEach(folder => appActions.addBookmarkFolder(folder)) + break + } + case appConstants.APP_EDIT_BOOKMARK_FOLDER: + { + const isSyncEnabled = syncEnabled() + let folder = action.folderDetails + + if (folder == null) { + break + } + + state = bookmarkFoldersState.editFolder(state, folder, action.editKey) + + if (isSyncEnabled) { + state = syncUtil.updateSiteCache(state, folder) } - state = updateActiveTabBookmarked(state) break } - case appConstants.APP_REMOVE_SITE: + case appConstants.APP_REMOVE_BOOKMARK_FOLDER: + { + state = bookmarkFoldersState.removeFolder(state, action.folderKey) + break + } + case appConstants.APP_REMOVE_HISTORY_SITE: const removeSiteSyncCallback = action.skipSync ? undefined : syncActions.removeSite state = siteUtil.removeSite(state, action.siteDetail, action.tag, true, removeSiteSyncCallback) if (syncEnabled()) { @@ -124,7 +202,7 @@ const sitesReducer = (state, action, immutableAction) => { } break case appConstants.APP_APPLY_SITE_RECORDS: - let nextFolderId = siteUtil.getNextFolderId(state.get('sites')) + let nextFolderId = bookmarkFoldersUtil.getNextFolderId(state.get('bookmarkFolders')) // Ensure that all folders are assigned folderIds action.records.forEach((record, i) => { if (record.action !== writeActions.DELETE && @@ -159,17 +237,17 @@ const sitesReducer = (state, action, immutableAction) => { if (immutableAction.getIn(['changeInfo', 'pinned']) != null) { const pinned = immutableAction.getIn(['changeInfo', 'pinned']) const tabId = immutableAction.getIn(['tabValue', 'tabId']) - const tab = state.get('tabs').find((tab) => tab.get('tabId') === tabId) + const tab = tabState.getByTabId(state, tabId) if (!tab) { console.warn('Trying to pin a tabId which does not exist:', tabId, 'tabs: ', state.get('tabs').toJS()) break } - const sites = state.get('sites') - const siteDetail = siteUtil.getDetailFromTab(tab, siteTags.PINNED, sites) + const sites = pinnedSitesState.getSites(state) + const siteDetail = pinnedSitesUtil.getDetailsFromTab(sites, tab) if (pinned) { - state = siteUtil.addSite(state, siteDetail, siteTags.PINNED) + state = pinnedSitesState.addPinnedSite(state, siteDetail) } else { - state = siteUtil.removeSite(state, siteDetail, siteTags.PINNED) + state = pinnedSitesState.removePinnedSite(state, siteDetail) } if (syncEnabled()) { state = syncUtil.updateSiteCache(state, siteDetail) @@ -180,11 +258,26 @@ const sitesReducer = (state, action, immutableAction) => { case appConstants.APP_CREATE_TAB_REQUESTED: { const createProperties = immutableAction.get('createProperties') if (createProperties.get('pinned')) { - state = siteUtil.addSite(state, - siteUtil.getDetailFromCreateProperties(createProperties), siteTags.PINNED) + state = pinnedSitesUtil.addPinnedSite(state, pinnedSitesUtil.getDetailFromProperties(createProperties)) } break } + case appConstants.APP_ON_PINNED_TAB_REORDER: + state = pinnedSitesState.reOrderSite( + state, + action.siteKey, + action.destinationKey, + action.prepend + ) + + // TODO add bookmark sort on a new state + + // TODO do we need this for pinned sites? + if (syncEnabled()) { + const newSite = state.getIn(['pinnedSites', action.siteKey]) + state = syncUtil.updateSiteCache(state, newSite) + } + break } return state } diff --git a/app/browser/reducers/urlBarSuggestionsReducer.js b/app/browser/reducers/urlBarSuggestionsReducer.js index 8873091a9ad..0a4f7b827f5 100644 --- a/app/browser/reducers/urlBarSuggestionsReducer.js +++ b/app/browser/reducers/urlBarSuggestionsReducer.js @@ -7,24 +7,18 @@ const appConstants = require('../../../js/constants/appConstants') const {generateNewSuggestionsList, generateNewSearchXHRResults} = require('../../common/lib/suggestion') const {init, add} = require('../../common/lib/siteSuggestions') -const Immutable = require('immutable') const {makeImmutable} = require('../../common/state/immutableUtil') const tabState = require('../../common/state/tabState') const urlBarSuggestionsReducer = (state, action) => { switch (action.actionType) { - case appConstants.APP_ADD_SITE: + case appConstants.APP_ADD_HISTORY_SITE: case appConstants.APP_ADD_BOOKMARK: case appConstants.APP_EDIT_BOOKMARK: - if (Immutable.List.isList(action.siteDetail)) { - action.siteDetail.forEach((s) => { - add(s) - }) - } else { - add(action.siteDetail) - } + add(action.siteDetail) break case appConstants.APP_SET_STATE: + // TODO do we need to merge bookmarks and history into one? init(Object.values(action.appState.get('sites').toJS())) break case appConstants.APP_URL_BAR_TEXT_CHANGED: diff --git a/app/browser/tabs.js b/app/browser/tabs.js index 06743fc16dc..82e7cab40ce 100644 --- a/app/browser/tabs.js +++ b/app/browser/tabs.js @@ -22,12 +22,14 @@ const messages = require('../../js/constants/messages') const aboutHistoryState = require('../common/state/aboutHistoryState') const appStore = require('../../js/stores/appStore') const appConfig = require('../../js/constants/appConfig') -const siteTags = require('../../js/constants/siteTags') const {newTabMode} = require('../common/constants/settingsEnums') const {cleanupWebContents, currentWebContents, getWebContents, updateWebContents} = require('./webContentsCache') const {FilterOptions} = require('ad-block') const {isResourceEnabled} = require('../filtering') const autofill = require('../autofill') +const bookmarksState = require('../common/state/bookmarksState') +const bookmarkFoldersState = require('../common/state/bookmarkFoldersState') +const historyState = require('../common/state/historyState') let currentPartitionNumber = 0 const incrementPartitionNumber = () => ++currentPartitionNumber @@ -134,21 +136,11 @@ ipcMain.on(messages.ABOUT_COMPONENT_INITIALIZED, (e) => { }) }) -const getBookmarksData = function (state) { - let bookmarkSites = new Immutable.OrderedMap() - let bookmarkFolderSites = new Immutable.OrderedMap() - state.get('sites').forEach((site, siteKey) => { - const tags = site.get('tags') - if (tags.includes(siteTags.BOOKMARK)) { - bookmarkSites = bookmarkSites.set(siteKey, site) - } - if (tags.includes(siteTags.BOOKMARK_FOLDER)) { - bookmarkFolderSites = bookmarkFolderSites.set(siteKey, site) - } - }) - const bookmarks = bookmarkSites.toList().toJS() - const bookmarkFolders = bookmarkFolderSites.toList().toJS() - return {bookmarks, bookmarkFolders} +const getBookmarksData = (state) => { + return { + bookmarks: bookmarksState.getBookmarks(state).toList().toJS(), + bookmarkFolders: bookmarkFoldersState.getFolders(state).toList().toJS() + } } const updateAboutDetails = (tab, tabValue) => { @@ -808,7 +800,7 @@ const api = { getHistoryEntries: (state, action) => { const tab = getWebContents(action.get('tabId')) - const sites = state ? state.get('sites') : null + const sites = state ? historyState.getSites(state) : null if (tab && !tab.isDestroyed()) { let history = { @@ -832,7 +824,7 @@ const api = { // TODO: return brave lion (or better: get icon from extension if possible as data URI) } else { if (sites) { - const site = sites.find(function (element) { return element.get('location') === url }) + const site = sites.find((element) => element.get('location') === url) if (site) { entry.icon = site.get('favicon') } diff --git a/app/browser/windows.js b/app/browser/windows.js index 028aee44eed..fec40cd6d14 100644 --- a/app/browser/windows.js +++ b/app/browser/windows.js @@ -9,14 +9,14 @@ const debounce = require('../../js/lib/debounce') const {getSetting} = require('../../js/settings') const locale = require('../locale') const LocalShortcuts = require('../localShortcuts') -const {getPinnedSiteProps} = require('../common/lib/windowsUtil') const {makeImmutable} = require('../common/state/immutableUtil') const {getPinnedTabsByWindowId} = require('../common/state/tabState') const messages = require('../../js/constants/messages') const settings = require('../../js/constants/settings') -const siteTags = require('../../js/constants/siteTags') const windowState = require('../common/state/windowState') const Immutable = require('immutable') +const pinnedSitesState = require('../common/state/pinnedSitesState') +const pinnedSitesUtil = require('../common/lib/pinnedSitesUtil') // TODO(bridiver) - set window uuid let currentWindows = {} @@ -69,8 +69,7 @@ const updatePinnedTabs = (win) => { const appStore = require('../../js/stores/appStore') const state = appStore.getState() const windowId = win.id - const pinnedSites = state.get('sites').toList().filter((site) => - site.get('tags').includes(siteTags.PINNED)).map(site => getPinnedSiteProps(site)) + const pinnedSites = pinnedSitesState.getSites(state).map(site => pinnedSitesUtil.getPinnedSiteProps(site)) const pinnedTabs = getPinnedTabsByWindowId(state, windowId) pinnedSites.filter((site) => diff --git a/app/common/lib/bookmarkFoldersUtil.js b/app/common/lib/bookmarkFoldersUtil.js new file mode 100644 index 00000000000..c30f9cb38cd --- /dev/null +++ b/app/common/lib/bookmarkFoldersUtil.js @@ -0,0 +1,55 @@ +/* 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/. */ + +const isFolderNameValid = (title) => { + return (title != null && title !== 0) && title.trim().length > 0 +} + +const getNextFolderIdItem = (folders) => + folders.max((folderA, folderB) => { + const folderIdA = folderA.get('folderId') + const folderIdB = folderB.get('folderId') + if (folderIdA === folderIdB) { + return 0 + } + if (folderIdA === undefined) { + return false + } + if (folderIdB === undefined) { + return true + } + return folderIdA > folderIdB + }) + +const getNextFolderId = (folders) => { + const defaultFolderId = 0 + if (!folders) { + return defaultFolderId + } + const maxIdItem = getNextFolderIdItem(folders) + return (maxIdItem ? (maxIdItem.get('folderId') || 0) : 0) + 1 +} + +const getNextFolderName = (folders, name) => { + if (!folders) { + return name + } + const site = folders.find((site) => site.get('title') === name) + if (!site) { + return name + } + const filenameFormat = /(.*) \((\d+)\)/ + let result = filenameFormat.exec(name) + if (!result) { + return getNextFolderName(folders, name + ' (1)') + } + + const nextNum = parseInt(result[2]) + 1 + return getNextFolderName(folders, result[1] + ' (' + nextNum + ')') +} + +module.exports = { + isFolderNameValid, + getNextFolderId, + getNextFolderName +} diff --git a/app/common/lib/bookmarkUtil.js b/app/common/lib/bookmarkUtil.js index a8c399db21f..1d415ac0f13 100644 --- a/app/common/lib/bookmarkUtil.js +++ b/app/common/lib/bookmarkUtil.js @@ -4,6 +4,9 @@ const Immutable = require('immutable') +// State +const bookmarksState = require('../state/bookmarksState') + // Constants const dragTypes = require('../../../js/constants/dragTypes') const {bookmarksToolbarMode} = require('../constants/settingsEnums') @@ -12,17 +15,12 @@ const settings = require('../../../js/constants/settings') // Utils const domUtil = require('../../renderer/lib/domUtil') const siteUtil = require('../../../js/state/siteUtil') +const siteCache = require('../state/siteCache') const {calculateTextWidth} = require('../../../js/lib/textCalculator') const {iconSize} = require('../../../js/constants/config') const {getSetting} = require('../../../js/settings') -function bookmarkHangerHeading (editMode, isFolder, isAdded) { - if (isFolder) { - return editMode - ? 'bookmarkFolderEditing' - : 'bookmarkFolderAdding' - } - +const bookmarkHangerHeading = (editMode, isAdded) => { if (isAdded) { return 'bookmarkAdded' } @@ -31,20 +29,8 @@ function bookmarkHangerHeading (editMode, isFolder, isAdded) { ? 'bookmarkEdit' : 'bookmarkCreateNew' } - -const displayBookmarkName = (detail) => { - const customTitle = detail.get('customTitle') - if (customTitle !== undefined && customTitle !== '') { - return customTitle || '' - } - return detail.get('title') || '' -} - -const isBookmarkNameValid = (title, location, isFolder, customTitle) => { - const newTitle = title || customTitle - return isFolder - ? (newTitle != null && newTitle !== 0) && newTitle.trim().length > 0 - : location != null && location.trim().length > 0 +const isBookmarkNameValid = (location) => { + return location != null && location.trim().length > 0 } const showOnlyFavicon = () => { @@ -115,7 +101,7 @@ const getToolbarBookmarks = (state) => { if (favicon && onlyFavicon) { widthAccountedFor += padding + iconWidth + currentChevronWidth } else { - const text = current.get('customTitle') || current.get('title') || current.get('location') + const text = current.get('title') || current.get('location') widthAccountedFor += Math.min(calculateTextWidth(text, `${fontSize} ${fontFamily}`) + padding + iconWidth + currentChevronWidth, maxWidth) } widthAccountedFor += margin @@ -133,12 +119,69 @@ const getToolbarBookmarks = (state) => { return lastValue } +const getDetailFromFrame = (frame) => { + return Immutable.fromJS({ + location: frame.get('location'), + title: frame.get('title'), + partitionNumber: frame.get('partitionNumber'), + favicon: frame.get('icon'), + themeColor: frame.get('themeColor') || frame.get('computedThemeColor') + }) +} + +/** + * Checks if a location is bookmarked. + * + * @param state The application state Immutable map + * @param {string} location + * @return {boolean} + */ +const isLocationBookmarked = (state, location) => { + const bookmarks = bookmarksState.getBookmarks(state) + const siteKeys = siteCache.getLocationSiteKeys(state, location) + + if (!siteKeys || siteKeys.length === 0) { + return false + } + + return siteKeys.some(key => bookmarks.has(key)) +} + +/** + * Converts a siteDetail to createProperties format + * @param {Object} bookmark - A bookmark detail as per app state + * @return {Object} A createProperties plain JS object, not ImmutableJS + */ +const toCreateProperties = (bookmark) => { + return { + url: bookmark.get('location'), + partitionNumber: bookmark.get('partitionNumber') + } +} + +/** + * Filters bookmarks relative to a parent folder + * @param state - The application state + * @param folderKey The folder key to filter to + */ +const getBookmarksByParentId = (state, folderKey) => { + const bookmarks = bookmarksState.getBookmarks(state) + if (!folderKey) { + return bookmarks + } + + return bookmarks.filter((bookmark) => bookmark.get('parentFolderId') === folderKey) +} + module.exports = { bookmarkHangerHeading, - displayBookmarkName, isBookmarkNameValid, showOnlyFavicon, showFavicon, getDNDBookmarkData, - getToolbarBookmarks + getToolbarBookmarks, + getDetailFromFrame, + isLocationBookmarked, + toCreateProperties, + getBookmarksByParentId } diff --git a/app/common/lib/historyUtil.js b/app/common/lib/historyUtil.js index c3cade0a637..b3101b3a58c 100644 --- a/app/common/lib/historyUtil.js +++ b/app/common/lib/historyUtil.js @@ -8,15 +8,13 @@ const {makeImmutable} = require('../state/immutableUtil') const siteUtil = require('../../../js/state/siteUtil') const aboutHistoryMaxEntries = 500 -module.exports.maxEntries = aboutHistoryMaxEntries - const sortTimeDescending = (left, right) => { if (left.get('lastAccessedTime') < right.get('lastAccessedTime')) return 1 if (left.get('lastAccessedTime') > right.get('lastAccessedTime')) return -1 return 0 } -module.exports.getHistory = (sites) => { +const getHistory = (sites) => { sites = makeImmutable(sites) ? makeImmutable(sites).toList() : new Immutable.List() return sites.filter((site) => siteUtil.isHistoryEntry(site)) .sort(sortTimeDescending) @@ -30,7 +28,7 @@ const getDayString = (entry, locale) => { : '' } -module.exports.groupEntriesByDay = (history, locale) => { +const groupEntriesByDay = (history, locale) => { const reduced = history.reduce((previousValue, currentValue, currentIndex, array) => { const result = currentIndex === 1 ? [] : previousValue if (currentIndex === 1) { @@ -60,7 +58,7 @@ module.exports.groupEntriesByDay = (history, locale) => { * Return an array with ALL entries. * Format is expected to be array containing one array per day. */ -module.exports.totalEntries = (entriesByDay) => { +const totalEntries = (entriesByDay) => { entriesByDay = makeImmutable(entriesByDay) || new Immutable.List() let result = new Immutable.List() @@ -69,3 +67,50 @@ module.exports.totalEntries = (entriesByDay) => { }) return result } + +const prepareHistoryEntry = (siteDetail) => { + return makeImmutable({ + lastAccessedTime: new Date().getTime(), + objectId: undefined, + title: siteDetail.get('title'), + location: siteDetail.get('location'), + themeColor: siteDetail.get('themeColor'), + favicon: siteDetail.get('favicon', siteDetail.get('icon')), + count: 1 + }) +} + +const mergeSiteDetails = (oldDetail, newDetail) => { + const objectId = newDetail.has('objectId') ? newDetail.get('objectId') : oldDetail.get('objectId', undefined) + + let site = makeImmutable({ + title: newDetail.get('title'), + location: newDetail.get('location'), + count: ~~oldDetail.get('count', 0) + 1, + lastAccessedTime: new Date().getTime(), + objectId + }) + + const themeColor = newDetail.has('themeColor') ? newDetail.get('themeColor') : oldDetail.get('themeColor') + if (themeColor) { + site = site.set('themeColor', themeColor) + } + + // we need to have a fallback to icon, because frame has icon for it + const favicon = (newDetail.has('favicon') || newDetail.has('icon')) + ? newDetail.get('favicon', newDetail.get('icon')) + : oldDetail.get('favicon') + if (favicon) { + site = site.set('favicon', favicon) + } + + return site +} + +module.exports = { + getHistory, + groupEntriesByDay, + totalEntries, + prepareHistoryEntry, + mergeSiteDetails +} diff --git a/app/common/lib/menuUtil.js b/app/common/lib/menuUtil.js index 65f24d89987..611c79d12cd 100644 --- a/app/common/lib/menuUtil.js +++ b/app/common/lib/menuUtil.js @@ -72,7 +72,7 @@ module.exports.setTemplateItemChecked = (template, label, checked) => { } return null } - +// TODO refactor to the new structure const createBookmarkTemplateItems = (bookmarks, parentFolderId) => { const filteredBookmarks = parentFolderId ? bookmarks.filter((bookmark) => bookmark.get('parentFolderId') === parentFolderId) @@ -89,7 +89,7 @@ const createBookmarkTemplateItems = (bookmarks, parentFolderId) => { // and as such there may need to be another mechanism or cache // // see: https://github.com/brave/browser-laptop/issues/3050 - label: site.get('customTitle') || site.get('title') || site.get('location'), + label: site.get('title') || site.get('location'), click: (item, focusedWindow, e) => { if (eventUtil.isForSecondaryAction(e)) { appActions.createTabRequested({ @@ -106,7 +106,7 @@ const createBookmarkTemplateItems = (bookmarks, parentFolderId) => { const folderId = site.get('folderId') const submenuItems = bookmarks.filter((bookmark) => bookmark.get('parentFolderId') === folderId) payload.push({ - label: site.get('customTitle') || site.get('title'), + label: site.get('title'), submenu: submenuItems.count() > 0 ? createBookmarkTemplateItems(bookmarks, folderId) : null }) } diff --git a/app/common/lib/pinnedSitesUtil.js b/app/common/lib/pinnedSitesUtil.js new file mode 100644 index 00000000000..d9662763128 --- /dev/null +++ b/app/common/lib/pinnedSitesUtil.js @@ -0,0 +1,115 @@ +/* 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/. */ + +const Immutable = require('immutable') +const siteUtil = require('../../../js/state/siteUtil') +const {makeImmutable} = require('../state/immutableUtil') + +const getSitesBySubkey = (sites, siteKey) => { + if (!sites || !siteKey) { + return makeImmutable([]) + } + const splitKey = siteKey.split('|', 2) + const partialKey = splitKey.join('|') + const matches = sites.filter((site, key) => { + return key.indexOf(partialKey) > -1 + }) + return matches.toList() +} + +const getDetailsFromTab = (sites, tab) => { + let location = tab.get('url') + const partitionNumber = tab.get('partitionNumber') + let parentFolderId + + // TODO check if needed https://github.com/brave/browser-laptop/pull/8588 + // we need to find which sites should be send in, I am guessing bookmarks + + // if site map is available, look up extra information: + // - original url (if redirected) + // - parent folder id + if (sites) { + // get all sites matching URL and partition (disregarding parentFolderId) + let siteKey = siteUtil.getSiteKey(makeImmutable({location, partitionNumber})) + let results = getSitesBySubkey(sites, siteKey) + + // only check for provisional location if entry is not found + if (results.size === 0) { + // if provisional location is different, grab any results which have that URL + // this may be different if the site was redirected + const provisionalLocation = tab.getIn(['frame', 'provisionalLocation']) + if (provisionalLocation && provisionalLocation !== location) { + siteKey = siteUtil.getSiteKey(makeImmutable({ + location: provisionalLocation, + partitionNumber + })) + results = results.merge(getSitesBySubkey(sites, siteKey)) + } + } + + // update details which get returned below + if (results.size > 0) { + location = results.getIn([0, 'location']) + parentFolderId = results.getIn([0, 'parentFolderId']) + } + } + + const siteDetail = { + location: location, + title: tab.get('title') + } + + // TODO I think that we don't need this one + if (partitionNumber) { + siteDetail.partitionNumber = partitionNumber + } + + if (parentFolderId) { + siteDetail.parentFolderId = parentFolderId + } + + return makeImmutable(siteDetail) +} + +const getDetailFromProperties = (createProperties) => { + const siteDetail = { + location: createProperties.get('url') + } + + if (createProperties.get('partitionNumber') !== undefined) { + siteDetail.partitionNumber = createProperties.get('partitionNumber') + } + return makeImmutable(siteDetail) +} + +const getDetailFromFrame = (frame) => { + const pinnedLocation = frame.get('pinnedLocation') + let location = frame.get('location') + if (pinnedLocation !== 'about:blank') { + location = pinnedLocation + } + + return makeImmutable({ + location, + title: frame.get('title'), + partitionNumber: frame.get('partitionNumber'), + favicon: frame.get('icon'), + themeColor: frame.get('themeColor') || frame.get('computedThemeColor') + }) +} + +const getPinnedSiteProps = site => { + return Immutable.fromJS({ + location: site.get('location'), + order: site.get('order'), + partitionNumber: site.get('partitionNumber', 0) + }) +} + +module.exports = { + getDetailsFromTab, + getDetailFromProperties, + getDetailFromFrame, + getPinnedSiteProps +} diff --git a/app/common/lib/siteSuggestions.js b/app/common/lib/siteSuggestions.js index 229d1bb4841..e2c40e21255 100644 --- a/app/common/lib/siteSuggestions.js +++ b/app/common/lib/siteSuggestions.js @@ -61,11 +61,8 @@ const tokenizeInput = (data) => { return [] } url = data.location - if (data.customTitle) { - parts = getPartsFromNonUrlInput(data.customTitle) - } if (data.title) { - parts = parts.concat(getPartsFromNonUrlInput(data.title)) + parts = getPartsFromNonUrlInput(data.title) } if (data.tags) { parts = parts.concat(data.tags.map(getTagToken)) diff --git a/app/common/lib/windowsUtil.js b/app/common/lib/windowsUtil.js deleted file mode 100644 index 4141a5a060d..00000000000 --- a/app/common/lib/windowsUtil.js +++ /dev/null @@ -1,17 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ - -const Immutable = require('immutable') - -const getPinnedSiteProps = site => { - return Immutable.fromJS({ - location: site.get('location'), - order: site.get('order'), - partitionNumber: site.get('partitionNumber') || 0 - }) -} - -module.exports = { - getPinnedSiteProps -} diff --git a/app/common/state/aboutHistoryState.js b/app/common/state/aboutHistoryState.js index 6f9048993e6..f52a4fede69 100644 --- a/app/common/state/aboutHistoryState.js +++ b/app/common/state/aboutHistoryState.js @@ -2,7 +2,9 @@ * 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/. */ +const Immutable = require('immutable') const {makeImmutable} = require('./immutableUtil') +const historyState = require('./historyState') const historyUtil = require('../lib/historyUtil') const aboutHistoryState = { @@ -10,10 +12,17 @@ const aboutHistoryState = { state = makeImmutable(state) return state.getIn(['about', 'history']) }, + setHistory: (state) => { state = makeImmutable(state) - state = state.setIn(['about', 'history', 'entries'], - historyUtil.getHistory(state.get('sites'))) + const sites = historyState.getSites(state) + state = state.setIn(['about', 'history', 'entries'], historyUtil.getHistory(sites)) + return state.setIn(['about', 'history', 'updatedStamp'], new Date().getTime()) + }, + + clearHistory: (state) => { + state = makeImmutable(state) + state = state.setIn(['about', 'history', 'entries'], Immutable.Map()) return state.setIn(['about', 'history', 'updatedStamp'], new Date().getTime()) } } diff --git a/app/common/state/aboutNewTabState.js b/app/common/state/aboutNewTabState.js index ea855edaf04..ac9a551bdd2 100644 --- a/app/common/state/aboutNewTabState.js +++ b/app/common/state/aboutNewTabState.js @@ -31,6 +31,13 @@ const aboutNewTabState = { topSites = makeImmutable(topSites) state = state.setIn(['about', 'newtab', 'sites'], topSites) return state.setIn(['about', 'newtab', 'updatedStamp'], new Date().getTime()) + }, + + clearTopSites: (state) => { + state = makeImmutable(state) + + state = state.setIn(['about', 'newtab', 'sites'], makeImmutable([])) + return state.setIn(['about', 'newtab', 'updatedStamp'], new Date().getTime()) } } diff --git a/app/common/state/bookmarkFoldersState.js b/app/common/state/bookmarkFoldersState.js new file mode 100644 index 00000000000..d9aa9ee81a9 --- /dev/null +++ b/app/common/state/bookmarkFoldersState.js @@ -0,0 +1,126 @@ +/* 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/. */ + +const assert = require('assert') +const Immutable = require('immutable') + +// Actions +const syncActions = require('../../../js/actions/syncActions') + +// Constants +const settings = require('../../../js/constants/settings') + +// State +const bookmarksState = require('./bookmarksState') + +// Utils +const bookmarkFoldersUtil = require('../lib/bookmarkFoldersUtil') +const siteUtil = require('../../../js/state/siteUtil') +const {makeImmutable, isMap} = require('./immutableUtil') +const {getSetting} = require('../../../js/settings') + +const validateState = function (state) { + state = makeImmutable(state) + assert.ok(isMap(state), 'state must be an Immutable.Map') + assert.ok(isMap(state.get('bookmarkFolders')), 'state must contain an Immutable.Map of bookmarkFolders') + return state +} + +const bookmarkFoldersState = { + getFolders: (state) => { + state = validateState(state) + return state.get('bookmarkFolders', Immutable.Map()) + }, + + getFolder: (state, folderKey) => { + state = validateState(state) + return state.getIn(['bookmarkFolders', folderKey], Immutable.Map()) + }, + + addFolder: (state, folderDetails) => { + state = validateState(state) + let folders = state.get('bookmarkFolders') + + const folderId = bookmarkFoldersUtil.getNextFolderId(folders) + + const newFolder = makeImmutable({ + title: folderDetails.get('title'), + folderId: ~~folderId, + parentFolderId: ~~folderDetails.get('parentFolderId', 0), + order: folders.size, // TODO order should be based on a parent + partitionNumber: ~~folderDetails.get('partitionNumber', 0), + objectId: null + }) + + state = state.setIn(['bookmarkFolders', folderId], newFolder) + return state + }, + + editFolder: (state, folderDetails, editKey) => { + state = validateState(state) + let sites = state.get('bookmarkFolders') + + const oldFolder = sites.get(editKey) + + const newFolder = oldFolder.merge(makeImmutable({ + title: folderDetails.get('title'), + parentFolderId: ~~folderDetails.get('parentFolderId', 0) + })) + + state = state.setIn(['bookmarkFolders', editKey], newFolder) + return state + }, + + removeFolder: (state, folderKey) => { + const folders = bookmarkFoldersState.getFolders(state) + const folder = bookmarkFoldersState.getFolder(state, folderKey) + + if (folder.isEmpty()) { + return state + } + + if (getSetting(settings.SYNC_ENABLED) === true) { + syncActions.removeSite(folder) + } + + const folderId = folder.get('folderId') + state = bookmarksState.removeBookmarksByParentId(state, folderId) + folders.filter(folder => folder.get('parentId') === folderKey) + .map(folder => { + state = bookmarkFoldersState.removeFolder(state, folder.get('folderId')) + }) + + // TODO add reorder function both folders and bookmarks + + return state.deleteIn(['sites', folderKey]) + }, + + getFoldersWithoutKey: (state, folderKey, parentId = 0, labelPrefix = '') => { + let folders = [] + const results = bookmarkFoldersState.getFolders(state) + .filter(site => site.get('parentFolderId', 0) === parentId) + .toList() + .sort(siteUtil.siteSort) + + const resultSize = results.size + for (let i = 0; i < resultSize; i++) { + const folder = results.get(i) + if (folder.get('folderId') === folderKey) { + continue + } + + const label = labelPrefix + folder.get('title') + folders.push({ + folderId: folder.get('folderId'), + label + }) + const subSites = bookmarkFoldersState.getFoldersWithoutKey(state, folderKey, folder.get('folderId'), (label || '') + ' / ') + folders = folders.concat(subSites) + } + + return folders + } +} + +module.exports = bookmarkFoldersState diff --git a/app/common/state/bookmarksState.js b/app/common/state/bookmarksState.js new file mode 100644 index 00000000000..208c8199f93 --- /dev/null +++ b/app/common/state/bookmarksState.js @@ -0,0 +1,149 @@ +/* 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/. */ + +const assert = require('assert') +const Immutable = require('immutable') + +// Constants +const settings = require('../../../js/constants/settings') + +// State +const historyState = require('./historyState') + +// Actions +const syncActions = require('../../../js/actions/syncActions') + +// Utils +const siteUtil = require('../../../js/state/siteUtil') +const UrlUtil = require('../../../js/lib/urlutil') +const siteCache = require('./siteCache') +const {getSetting} = require('../../../js/settings') +const {makeImmutable, isMap} = require('./immutableUtil') + +const validateState = function (state) { + state = makeImmutable(state) + assert.ok(isMap(state), 'state must be an Immutable.Map') + assert.ok(isMap(state.get('bookmarks')), 'state must contain an Immutable.Map of bookmarks') + return state +} + +const bookmarksState = { + getBookmarks: (state) => { + state = validateState(state) + return state.get('bookmarks') + }, + + getBookmark: (state, key) => { + state = validateState(state) + return state.getIn(['bookmarks', key], Immutable.Map()) + }, + + addBookmark: (state, bookmarkDetail) => { + let bookmarks = bookmarksState.getBookmarks(state) + + let location + if (bookmarkDetail.has('location')) { + location = UrlUtil.getLocationIfPDF(bookmarkDetail.get('location')) + bookmarkDetail = bookmarkDetail.set('location', location) + } + + const key = siteUtil.getSiteKey(bookmarkDetail) + const historyItem = historyState.getSite(state, key) + + let bookmark = makeImmutable({ + title: bookmarkDetail.get('title'), + location: bookmarkDetail.get('location'), + parentFolderId: ~~bookmarkDetail.get('parentFolderId', 0), + order: bookmarks.size, // TODO order should be based on a parentId + partitionNumber: ~~historyItem.get('partitionNumber', 0), + objectId: null, + favicon: historyItem.get('favicon'), + themeColor: historyItem.get('themeColor') + }) + + if (key === null) { + return state + } + + state = state.setIn(['bookmarks', key], bookmark) + state = siteCache.addLocationSiteKey(state, location, key) + return state + }, + + editBookmark: (state, bookmarkDetail, editKey) => { + const bookmark = bookmarksState.getBookmark(state, editKey) + let newBookmark = bookmark.merge(bookmarkDetail) + + let location + if (newBookmark.has('location')) { + location = UrlUtil.getLocationIfPDF(newBookmark.get('location')) + newBookmark = newBookmark.set('location', location) + } + + state = siteCache.removeLocationSiteKey(state, bookmark.get('location'), editKey) + state = state.deleteIn(['bookmarks', editKey]) + + const newKey = siteUtil.getSiteKey(newBookmark) + if (newKey === null) { + return state + } + + state = state.setIn(['bookmarks', newKey], newBookmark) + state = siteCache.addLocationSiteKey(state, location, newKey) + return state + }, + + removeBookmark: (state, bookmarkKey) => { + const bookmark = bookmarksState.getBookmark(state, bookmarkKey) + + if (bookmark.isEmpty()) { + return state + } + + if (getSetting(settings.SYNC_ENABLED) === true) { + syncActions.removeSite(bookmark) + } + + state = siteCache.removeLocationSiteKey(state, bookmark.get('location'), bookmarkKey) + + // TODO add reorder function + + return state.deleteIn(['bookmarks', bookmarkKey]) + }, + + removeBookmarksByParentId: (state, parentId) => { + let bookmarks = bookmarksState.getBookmarks(state) + + bookmarks = bookmarks.filter(bookmark => bookmark.get('parentId') !== parentId) + + return state.set('bookmarks', bookmarks) + }, + + /** + * Update the favicon URL for all entries in the state sites + * which match a given location. Currently, there should only be + * one match, but this will handle multiple. + * + * @param state The application state + * @param location URL for the entry needing an update + * @param favicon favicon URL + */ + updateSiteFavicon: (state, location, favicon) => { + if (UrlUtil.isNotURL(location)) { + return state + } + + const siteKeys = siteCache.getLocationSiteKeys(state, location) + if (!siteKeys || siteKeys.length === 0) { + return state + } + + siteKeys.forEach((siteKey) => { + state = state.setIn(['bookmarks', siteKey, 'favicon'], favicon) + }) + return state + } +} + +module.exports = bookmarksState diff --git a/app/common/state/historyState.js b/app/common/state/historyState.js new file mode 100644 index 00000000000..326b310223a --- /dev/null +++ b/app/common/state/historyState.js @@ -0,0 +1,65 @@ +/* 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/. */ + +const assert = require('assert') +const Immutable = require('immutable') +const siteUtil = require('../../../js/state/siteUtil') +const historyUtil = require('../lib/historyUtil') +const urlUtil = require('../../../js/lib/urlutil') +const siteCache = require('./siteCache') +const {makeImmutable, isMap} = require('./immutableUtil') + +const validateState = function (state) { + state = makeImmutable(state) + assert.ok(isMap(state), 'state must be an Immutable.Map') + assert.ok(isMap(state.get('historySites')), 'state must contain an Immutable.Map of historySites') + return state +} + +const historyState = { + getSites: (state) => { + state = validateState(state) + return state.get('historySites', Immutable.Map()) + }, + + getSite: (state, key) => { + state = validateState(state) + return state.getIn(['historySites', key], Immutable.Map()) + }, + + addSite: (state, siteDetail) => { + let sites = historyState.getSites(state) + let siteKey = siteUtil.getSiteKey(siteDetail) + siteDetail = makeImmutable(siteDetail) + + const oldSite = sites.get(siteKey) + let site + if (oldSite) { + site = historyUtil.mergeSiteDetails(oldSite, siteDetail) + } else { + let location + if (siteDetail.has('location')) { + location = urlUtil.getLocationIfPDF(siteDetail.get('location')) + siteDetail = siteDetail.set('location', location) + } + + siteKey = siteUtil.getSiteKey(siteDetail) + site = historyUtil.prepareHistoryEntry(siteDetail) + state = siteCache.addLocationSiteKey(state, location, siteKey) + } + + state = state.setIn(['historySites', siteKey], site) + return state + }, + + removeSite: () => { + // TODO implement + }, + + clearSites: (state) => { + return state.set('historySites', Immutable.Map()) + } +} + +module.exports = historyState diff --git a/app/common/state/pinnedSitesState.js b/app/common/state/pinnedSitesState.js new file mode 100644 index 00000000000..b231d780a53 --- /dev/null +++ b/app/common/state/pinnedSitesState.js @@ -0,0 +1,125 @@ +/* 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/. */ + +const assert = require('assert') +const Immutable = require('immutable') +const siteCache = require('./siteCache') +const siteUtil = require('../../../js/state/siteUtil') +const urlUtil = require('../../../js/lib/urlutil') +const {makeImmutable, isMap} = require('./immutableUtil') + +const validateState = function (state) { + state = makeImmutable(state) + assert.ok(isMap(state), 'state must be an Immutable.Map') + assert.ok(isMap(state.get('pinnedSites')), 'state must contain an Immutable.Map of pinnedSites') + return state +} + +const pinnedSiteState = { + getSites: (state) => { + state = validateState(state) + return state.get('pinnedSites') + }, + + /** + * Adds the specified siteDetail in appState.pinnedSites. + * @param {Immutable.Map} state The application state Immutable map + * @param {Immutable.Map} site The siteDetail that we want to add + */ + addPinnedSite: (state, site) => { + state = validateState(state) + const sites = pinnedSiteState.getSites(state) || Immutable.Map() + let location + if (site.has('location')) { + location = urlUtil.getLocationIfPDF(site.get('location')) + site = site.set('location', location) + } + + site = site.set('order', sites.size) + + const key = siteUtil.getSiteKey(site) + if (key === null) { + return state + } + + state = state.setIn(['pinnedSites', key], site) + state = siteCache.addLocationSiteKey(state, location, key) + return state + }, + + /** + * Removes the given pinned site from the pinnedSites + * + * @param {Immutable.Map} state The application state Immutable map + * @param {Immutable.Map} siteDetail The siteDetail to be removed + * @return {Immutable.Map} The new state Immutable object + */ + removePinnedSite: (state, siteDetail) => { + state = validateState(state) + const key = siteUtil.getSiteKey(siteDetail) + if (!key) { + return state + } + + const location = siteDetail.get('location') + state = siteCache.removeLocationSiteKey(state, location, key) + + const stateKey = ['pinnedSites', key] + let site = state.getIn(stateKey) + if (!site) { + return state + } + + // TODO update order, so that is up to date + + return state.deleteIn(stateKey, site) + }, + + /** + * Moves the specified pinned site from one location to another + * + * @param state The application state Immutable map + * @param sourceKey The site key to move + * @param destinationKey The site key to move to + * @param prepend Whether the destination detail should be prepended or not + * @return The new state Immutable object + */ + reOrderSite: (state, sourceKey, destinationKey, prepend) => { + state = validateState(state) + let sites = state.get('pinnedSites') + let sourceSite = sites.get(sourceKey, Immutable.Map()) + const destinationSite = sites.get(destinationKey, Immutable.Map()) + + if (sourceSite.isEmpty()) { + return state + } + + const sourceSiteIndex = sourceSite.get('order') + const destinationSiteIndex = destinationSite.get('order') + let newIndex = destinationSiteIndex + (prepend ? 0 : 1) + if (destinationSiteIndex > sourceSiteIndex) { + --newIndex + } + + state = state.set('pinnedSites', state.get('pinnedSites').map((site, index) => { + const siteOrder = site.get('order') + if (index === sourceKey) { + return site + } + + if (siteOrder >= newIndex && siteOrder < sourceSiteIndex) { + return site.set('order', siteOrder + 1) + } else if (siteOrder <= newIndex && siteOrder > sourceSiteIndex) { + return site.set('order', siteOrder - 1) + } + + return site + })) + + sourceSite = sourceSite.set('order', newIndex) + return state.setIn(['pinnedSites', sourceKey], sourceSite) + } +} + +module.exports = pinnedSiteState diff --git a/app/common/state/siteCache.js b/app/common/state/siteCache.js index beb1a207928..a436daae1f9 100644 --- a/app/common/state/siteCache.js +++ b/app/common/state/siteCache.js @@ -7,6 +7,8 @@ const siteUtil = require('../../../js/state/siteUtil') const appUrlUtil = require('../../../js/lib/appUrlUtil') const UrlUtil = require('../../../js/lib/urlutil') +// TODO what to do with this file? + const createLocationSiteKeysCache = (state) => { state = state.set('locationSiteKeysCache', new Immutable.Map()) state.get('sites').forEach((site, siteKey) => { diff --git a/app/common/state/siteState.js b/app/common/state/siteState.js deleted file mode 100644 index 0f74f8390a6..00000000000 --- a/app/common/state/siteState.js +++ /dev/null @@ -1,18 +0,0 @@ -const assert = require('assert') -const {makeImmutable, isMap, isList} = require('./immutableUtil') - -const validateState = function (state) { - state = makeImmutable(state) - assert.ok(isMap(state), 'state must be an Immutable.Map') - assert.ok(isList(state.get('sites')), 'state must contain an Immutable.List of sites') - return state -} - -const siteState = { - getSites: (state) => { - state = validateState(state) - return state.get('sites') - } -} - -module.exports = siteState diff --git a/app/common/state/tabState.js b/app/common/state/tabState.js index 4cc5ece7b24..3518a664dc0 100644 --- a/app/common/state/tabState.js +++ b/app/common/state/tabState.js @@ -13,7 +13,7 @@ const windowState = require('./windowState') const { makeImmutable, isMap, isList } = require('./immutableUtil') // this file should eventually replace frameStateUtil const frameStateUtil = require('../../../js/state/frameStateUtil') -const {isLocationBookmarked} = require('../../../js/state/siteUtil') +const bookmarkUtil = require('../lib/bookmarkUtil') const validateId = function (propName, id) { assert.ok(id, `${propName} cannot be null`) @@ -239,6 +239,13 @@ const tabState = { return state.get('tabs').filter((tab) => !!tab.get('pinned')) }, + isTabPinned: (state, tabId) => { + state = validateState(state) + tabId = validateId('tabId', tabId) + const tab = tabState.getByTabId(state, tabId) + return tab != null ? !!tab.get('pinned') : false + }, + getNonPinnedTabs: (state) => { state = validateState(state) return state.get('tabs').filter((tab) => !tab.get('pinned')) @@ -448,7 +455,7 @@ const tabState = { } const frameLocation = action.getIn(['frame', 'location']) - const frameBookmarked = isLocationBookmarked(state, frameLocation) + const frameBookmarked = bookmarkUtil.isLocationBookmarked(state, frameLocation) const frameValue = action.get('frame').set('bookmarked', frameBookmarked) tabValue = tabValue.set('frame', makeImmutable(frameValue)) return tabState.updateTabValue(state, tabValue) diff --git a/app/importer.js b/app/importer.js index b88c47d7fac..1af8fda354f 100644 --- a/app/importer.js +++ b/app/importer.js @@ -10,17 +10,29 @@ const importer = electron.importer const dialog = electron.dialog const BrowserWindow = electron.BrowserWindow const session = electron.session -const siteUtil = require('../js/state/siteUtil') -const AppStore = require('../js/stores/appStore') + +// Store +const appStore = require('../js/stores/appStore') + +// State +const tabState = require('./common/state/tabState') +const bookmarksState = require('./common/state/bookmarksState') +const bookmarkFoldersState = require('./common/state/bookmarkFoldersState') + +// Constants const siteTags = require('../js/constants/siteTags') -const appActions = require('../js/actions/appActions') const messages = require('../js/constants/messages') const settings = require('../js/constants/settings') -const getSetting = require('../js/settings').getSetting + +// Actions +const appActions = require('../js/actions/appActions') + +// Utils +const {getSetting} = require('../js/settings') const locale = require('./locale') const tabMessageBox = require('./browser/tabMessageBox') const {makeImmutable} = require('./common/state/immutableUtil') -const tabState = require('./common/state/tabState') +const bookmarkFoldersUtil = require('./common/lib/bookmarkFoldersUtil') var isImportingBookmarks = false var hasBookmarks @@ -33,10 +45,8 @@ exports.init = () => { exports.importData = (selected) => { if (selected.get('favorites')) { isImportingBookmarks = true - const sites = AppStore.getState().get('sites') - hasBookmarks = sites.find( - (site) => siteUtil.isBookmark(site) || siteUtil.isFolder(site) - ) + const state = appStore.getState() + hasBookmarks = bookmarksState.getBookmarks(state).size > 0 || bookmarkFoldersState.getFolders(state).size > 0 } if (selected !== undefined) { importer.importData(selected.toJS()) @@ -45,10 +55,8 @@ exports.importData = (selected) => { exports.importHTML = (selected) => { isImportingBookmarks = true - const sites = AppStore.getState().get('sites') - hasBookmarks = sites.find( - (site) => siteUtil.isBookmark(site) || siteUtil.isFolder(site) - ) + const state = appStore.getState() + hasBookmarks = bookmarksState.getBookmarks(state).size > 0 || bookmarkFoldersState.getFolders(state).size > 0 const files = dialog.showOpenDialog({ properties: ['openFile'], filters: [{ @@ -69,7 +77,7 @@ importer.on('update-supported-browsers', (e, detail) => { } }) -importer.on('add-history-page', (e, history, visitSource) => { +importer.on('add-history-page', (e, history) => { let sites = [] for (let i = 0; i < history.length; ++i) { const site = { @@ -79,12 +87,13 @@ importer.on('add-history-page', (e, history, visitSource) => { } sites.push(site) } - appActions.addSite(makeImmutable(sites)) + appActions.addHistorySite(makeImmutable(sites)) }) importer.on('add-homepage', (e, detail) => { }) +// TODO do we need creation time? const getParentFolderId = (path, pathMap, sites, topLevelFolderId, nextFolderIdObject) => { const pathLen = path.length if (!pathLen) { @@ -96,7 +105,7 @@ const getParentFolderId = (path, pathMap, sites, topLevelFolderId, nextFolderIdO parentFolderId = nextFolderIdObject.id++ pathMap[parentFolder] = parentFolderId const folder = { - customTitle: parentFolder, + title: parentFolder, folderId: parentFolderId, parentFolderId: getParentFolderId(path, pathMap, sites, topLevelFolderId, nextFolderIdObject), lastAccessedTime: 0, @@ -108,15 +117,18 @@ const getParentFolderId = (path, pathMap, sites, topLevelFolderId, nextFolderIdO return parentFolderId } +// TODO refactor to the new structure after sort is implemented importer.on('add-bookmarks', (e, bookmarks, topLevelFolder) => { - let nextFolderId = siteUtil.getNextFolderId(AppStore.getState().get('sites')) + const state = appStore.getState() + const bookmarkFolders = bookmarkFoldersState.getFolders(state) + let nextFolderId = bookmarkFoldersUtil.getNextFolderId(bookmarkFolders) let nextFolderIdObject = { id: nextFolderId } let pathMap = {} let sites = [] let topLevelFolderId = 0 topLevelFolderId = nextFolderIdObject.id++ sites.push({ - customTitle: siteUtil.getNextFolderName(AppStore.getState().get('sites'), topLevelFolder), + title: bookmarkFoldersUtil.getNextFolderName(bookmarkFolders, topLevelFolder), folderId: topLevelFolderId, parentFolderId: 0, lastAccessedTime: 0, @@ -130,7 +142,7 @@ importer.on('add-bookmarks', (e, bookmarks, topLevelFolder) => { const folderId = nextFolderIdObject.id++ pathMap[bookmarks[i].title] = folderId const folder = { - customTitle: bookmarks[i].title, + title: bookmarks[i].title, folderId: folderId, parentFolderId: parentFolderId, lastAccessedTime: 0, @@ -141,7 +153,6 @@ importer.on('add-bookmarks', (e, bookmarks, topLevelFolder) => { } else { const site = { title: bookmarks[i].title, - customTitle: bookmarks[i].title, location: bookmarks[i].url, parentFolderId: parentFolderId, lastAccessedTime: 0, @@ -178,6 +189,7 @@ importer.on('add-favicons', (e, detail) => { return site } }) + // TODO what should be done here? update all bookmarks with new fav icons? appActions.addSite(sites) }) @@ -208,7 +220,7 @@ importer.on('add-cookies', (e, cookies) => { }) const getActiveTabId = () => { - return tabState.getActiveTabId(AppStore.getState()) + return tabState.getActiveTabId(appStore.getState()) } const showImportWarning = function () { diff --git a/app/index.js b/app/index.js index 2ee37e47f22..262fd96253a 100644 --- a/app/index.js +++ b/app/index.js @@ -159,6 +159,7 @@ app.on('ready', () => { appActions.networkDisconnected() }) + // TODO what should we do here loadAppStatePromise.then((initialState) => { // Do this after loading the state // For tests we always want to load default app state @@ -301,7 +302,7 @@ app.on('ready', () => { }) ipcMain.on(messages.EXPORT_BOOKMARKS, () => { - BookmarksExporter.showDialog(appStore.getState().get('sites')) + BookmarksExporter.showDialog(appStore.getState()) }) // DO NOT TO THIS LIST - see above @@ -330,7 +331,7 @@ app.on('ready', () => { }) process.on(messages.EXPORT_BOOKMARKS, () => { - BookmarksExporter.showDialog(appStore.getState().get('sites')) + BookmarksExporter.showDialog(appStore.getState()) }) ready = true diff --git a/app/renderer/components/bookmarks/addEditBookmarkFolder.js b/app/renderer/components/bookmarks/addEditBookmarkFolder.js new file mode 100644 index 00000000000..36e6b78da52 --- /dev/null +++ b/app/renderer/components/bookmarks/addEditBookmarkFolder.js @@ -0,0 +1,103 @@ +/* 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/. */ + +const React = require('react') +const Immutable = require('immutable') +const {StyleSheet, css} = require('aphrodite/no-important') + +// Components +const ReduxComponent = require('../reduxComponent') +const Dialog = require('../common/dialog') +const AddEditBookmarkFolderForm = require('./addEditBookmarkFolderForm') +const {CommonFormBookmarkHanger} = require('../common/commonForm') + +// State +const bookmarkFoldersState = require('../../../common/state/bookmarkFoldersState') +const bookmarksState = require('../../../common/state/bookmarksState') + +// Actions +const windowActions = require('../../../../js/actions/windowActions') + +// Utils +const cx = require('../../../../js/lib/classSet') +const bookmarkFoldersUtil = require('../../../common/lib/bookmarkFoldersUtil') + +// Styles +const globalStyles = require('../styles/global') + +class AddEditBookmarkFolder extends React.Component { + constructor (props) { + super(props) + this.onClose = this.onClose.bind(this) + this.onClick = this.onClick.bind(this) + } + + onClose () { + windowActions.onBookmarkFolderClose() + } + + onClick (e) { + e.stopPropagation() + } + + mergeProps (state, ownProps) { + const currentWindow = state.get('currentWindow') + const bookmarkDetail = currentWindow.get('bookmarkFolderDetail', Immutable.Map()) + const folderDetails = bookmarkDetail.get('folderDetail') || Immutable.Map() + const editMode = bookmarkDetail.has('editKey') + + const props = {} + // used in renderer + props.heading = editMode + ? 'bookmarkFolderEditing' + : 'bookmarkFolderAdding' + props.parentFolderId = folderDetails.get('parentFolderId') + props.partitionNumber = folderDetails.get('partitionNumber') + props.folderName = folderDetails.get('title') + props.isFolderNameValid = bookmarkFoldersUtil.isFolderNameValid(folderDetails.get('title')) + props.folders = bookmarkFoldersState.getFoldersWithoutKey(state, folderDetails.get('folderId')) // TODO (nejc) improve, primitives only + props.editKey = bookmarkDetail.get('editKey', null) + props.closestKey = bookmarkDetail.get('closestKey', null) + props.hasBookmarks = bookmarksState.getBookmarks(state).size > 0 || bookmarkFoldersState.getFolders(state).size > 0 + + return props + } + + render () { + return + +
+ + +
+ } +} + +const styles = StyleSheet.create({ + // Copied from commonForm.js + commonFormSection: { + // PR #7985 + margin: `${globalStyles.spacing.dialogInsideMargin} 30px` + }, + commonFormTitle: { + color: globalStyles.color.braveOrange, + fontSize: '1.2em' + } +}) + +module.exports = ReduxComponent.connect(AddEditBookmarkFolder) diff --git a/app/renderer/components/bookmarks/addEditBookmarkFolderForm.js b/app/renderer/components/bookmarks/addEditBookmarkFolderForm.js new file mode 100644 index 00000000000..614daa610f3 --- /dev/null +++ b/app/renderer/components/bookmarks/addEditBookmarkFolderForm.js @@ -0,0 +1,235 @@ +/* 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/. */ + +const React = require('react') +const Immutable = require('immutable') +const {StyleSheet, css} = require('aphrodite/no-important') + +// Components +const BrowserButton = require('../common/browserButton') +const { + CommonFormSection, + CommonFormDropdown, + CommonFormButtonWrapper, + commonFormStyles +} = require('../common/commonForm') + +// Actions +const appActions = require('../../../../js/actions/appActions') +const windowActions = require('../../../../js/actions/windowActions') + +// Constants +const KeyCodes = require('../../../common/constants/keyCodes') +const settings = require('../../../../js/constants/settings') + +// Utils +const UrlUtil = require('../../../../js/lib/urlutil') +const {getSetting} = require('../../../../js/settings') +const bookmarkFoldersUtil = require('../../../common/lib/bookmarkFoldersUtil') + +// Styles +const globalStyles = require('../styles/global') +const commonStyles = require('../styles/commonStyles') + +class AddEditBookmarkFolderForm extends React.Component { + constructor (props) { + super(props) + this.onNameChange = this.onNameChange.bind(this) + this.onParentFolderChange = this.onParentFolderChange.bind(this) + this.onKeyDown = this.onKeyDown.bind(this) + this.onClose = this.onClose.bind(this) + this.onSave = this.onSave.bind(this) + this.onFolderRemove = this.onFolderRemove.bind(this) + this.state = { + title: props.folderName, + parentFolderId: props.parentFolderId, + isDisabled: props.isDisabled + } + } + + componentDidMount () { + setImmediate(() => { + this.folderName.select() + }) + } + + onKeyDown (e) { + switch (e.keyCode) { + case KeyCodes.ENTER: + this.onSave() + break + case KeyCodes.ESC: + this.onClose() + break + } + } + + onClose () { + windowActions.onBookmarkFolderClose() + } + + updateButtonStatus (newValue) { + if (newValue !== this.state.isDisabled) { + this.setState({ + isDisabled: newValue + }) + } + } + + onNameChange (e) { + let title = e.target.value + + this.setState({ + title: title + }) + + this.updateButtonStatus(!bookmarkFoldersUtil.isFolderNameValid(title)) + } + + onParentFolderChange (e) { + this.setState({ + parentFolderId: ~~e.target.value + }) + } + + onSave () { + // First check if the title of the bookmarkDetail is set + if (this.state.isDisabled) { + return false + } + + // show bookmark if hidden + if (!this.props.hasBookmarks && !getSetting(settings.SHOW_BOOKMARKS_TOOLBAR)) { + appActions.changeSetting(settings.SHOW_BOOKMARKS_TOOLBAR, true) + } + + let data = Immutable.fromJS({ + parentFolderId: this.state.parentFolderId + }) + + if (this.props.editKey != null) { + data = data.set('folderId', this.props.editKey) + } + + // handle title input + let title = this.state.title + if (typeof title === 'string' && UrlUtil.isURL(title)) { + const punycodeUrl = UrlUtil.getPunycodeUrl(title) + if (punycodeUrl.replace(/\/$/, '') !== title) { + title = punycodeUrl + } + } + data = data.set('title', title) + + if (this.props.editKey != null) { + appActions.editBookmarkFolder(data, this.props.editKey) + } else { + appActions.addBookmarkFolder(data, this.props.closestKey) + } + + this.onClose() + } + + onFolderRemove () { + appActions.removeBookmarkFolder(Immutable.fromJS({ + parentFolderId: this.props.parentFolderId, + partitionNumber: this.props.partitionNumber, + folderId: this.props.editKey + })) + this.onClose() + } + + render () { + return
+ +
+
+
+
+
+
+
+ + { + this.props.editKey != null + ? + : + } + + +
+ } +} + +const styles = StyleSheet.create({ + bookmarkHanger__label: { + display: 'block', + marginBottom: `calc(${globalStyles.spacing.dialogInsideMargin} / 3)` + }, + bookmarkHanger__marginRow: { + marginTop: `calc(${globalStyles.spacing.dialogInsideMargin} / 2)` + }, + + bookmark__sectionWrapper: { + display: 'flex', + flexFlow: 'column nowrap' + } +}) + +module.exports = AddEditBookmarkFolderForm diff --git a/app/renderer/components/bookmarks/addEditBookmarkForm.js b/app/renderer/components/bookmarks/addEditBookmarkForm.js index a744a8d50f4..2dd64b19ea4 100644 --- a/app/renderer/components/bookmarks/addEditBookmarkForm.js +++ b/app/renderer/components/bookmarks/addEditBookmarkForm.js @@ -22,7 +22,6 @@ const windowActions = require('../../../../js/actions/windowActions') // Constants const KeyCodes = require('../../../common/constants/keyCodes') -const siteTags = require('../../../../js/constants/siteTags') const settings = require('../../../../js/constants/settings') // Utils @@ -45,7 +44,7 @@ class AddEditBookmarkForm extends React.Component { this.onSave = this.onSave.bind(this) this.onRemoveBookmark = this.onRemoveBookmark.bind(this) this.state = { - title: props.bookmarkName, + title: props.title, location: props.location, parentFolderId: props.parentFolderId, isDisabled: props.isDisabled @@ -87,8 +86,6 @@ class AddEditBookmarkForm extends React.Component { this.setState({ title: title }) - - this.updateButtonStatus(!isBookmarkNameValid(title, this.state.location, this.props.isFolder)) } onLocationChange (e) { @@ -98,7 +95,7 @@ class AddEditBookmarkForm extends React.Component { location: location }) - this.updateButtonStatus(!isBookmarkNameValid(this.state.title, location, this.props.isFolder)) + this.updateButtonStatus(!isBookmarkNameValid(location)) } onParentFolderChange (e) { @@ -118,16 +115,10 @@ class AddEditBookmarkForm extends React.Component { appActions.changeSetting(settings.SHOW_BOOKMARKS_TOOLBAR, true) } - const tag = this.props.isFolder ? siteTags.BOOKMARK_FOLDER : siteTags.BOOKMARK let data = Immutable.fromJS({ - parentFolderId: this.state.parentFolderId, - title: this.props.currentTitle + parentFolderId: this.state.parentFolderId }) - if (this.props.isFolder && this.props.editKey != null) { - data = data.set('folderId', this.props.editKey) - } - // handle title input let title = this.state.title if (typeof title === 'string' && UrlUtil.isURL(title)) { @@ -136,10 +127,7 @@ class AddEditBookmarkForm extends React.Component { title = punycodeUrl } } - - if (this.props.currentTitle !== title || !title) { - data = data.set('customTitle', title || 0) - } + data = data.set('title', title) // handle location input let location = this.state.location @@ -152,23 +140,16 @@ class AddEditBookmarkForm extends React.Component { data = data.set('location', location) if (this.props.editKey != null) { - appActions.editBookmark(data, this.props.editKey, tag) + appActions.editBookmark(data, this.props.editKey) } else { - appActions.addBookmark(data, tag, this.props.closestKey) + appActions.addBookmark(data, this.props.closestKey) } this.onClose() } onRemoveBookmark () { - const tag = this.props.isFolder ? siteTags.BOOKMARK_FOLDER : siteTags.BOOKMARK - // TODO check if you need to add folderId as prop or you can use editKey - appActions.removeSite(Immutable.fromJS({ - parentFolderId: this.props.parentFolderId, - location: this.props.location, - partitionNumber: this.props.partitionNumber, - folderId: this.props.isFolder ? this.props.editKey : null - }), tag) + appActions.removeBookmark(this.props.editKey) this.onClose() } @@ -203,7 +184,7 @@ class AddEditBookmarkForm extends React.Component { { - !this.props.isFolder && !this.props.isAdded + !this.props.isAdded ?
siteUtil.isBookmark(site) || siteUtil.isFolder(site) - ) + props.hasBookmarks = bookmarksState.getBookmarks(state).size > 0 || bookmarkFoldersState.getFolders(state).size > 0 return props } @@ -132,14 +123,12 @@ class AddEditBookmarkHanger extends React.Component { [css(styles.commonFormTitle)]: true })} data-l10n-id={this.props.heading} /> 0) { Array.from(e.dataTransfer.items).forEach((item) => { - item.getAsString((name) => appActions.addSite({ location: item.type, title: name }, siteTags.BOOKMARK)) + item.getAsString((name) => appActions.addBookmark(Immutable.fromJS({ + location: item.type, + title: name + }))) }) return } @@ -92,7 +94,7 @@ class BookmarksToolbar extends React.Component { .map((x) => x.trim()) .filter((x) => !x.startsWith('#') && x.length > 0) .forEach((url) => - appActions.addSite({ location: url }, siteTags.BOOKMARK)) + appActions.addBookmark(Immutable.fromJS({ location: url }))) } onDragEnter (e) { diff --git a/app/renderer/components/frame/frame.js b/app/renderer/components/frame/frame.js index 5ad3ef64eff..b40b10d73f0 100644 --- a/app/renderer/components/frame/frame.js +++ b/app/renderer/components/frame/frame.js @@ -624,7 +624,8 @@ class Frame extends React.Component { // with setTitle. We either need to delay this call until the title is // or add a way to update it setTimeout(() => { - appActions.addSite(siteUtil.getDetailFromFrame(this.frame)) + // TODO add history site + appActions.addHistorySite(siteUtil.getDetailFromFrame(this.frame)) }, 250) } @@ -668,7 +669,7 @@ class Frame extends React.Component { url: e.validatedURL }) appActions.loadURLRequested(this.props.tabId, 'about:error') - appActions.removeSite(siteUtil.getDetailFromFrame(this.frame)) + appActions.removeHistorySite(siteUtil.getDetailFromFrame(this.frame)) } else if (isAborted(e.errorCode)) { // just stay put } else if (provisionLoadFailure) { diff --git a/app/renderer/components/main/main.js b/app/renderer/components/main/main.js index 5504295141d..c93ce69aed8 100644 --- a/app/renderer/components/main/main.js +++ b/app/renderer/components/main/main.js @@ -33,6 +33,7 @@ const WidevinePanel = require('./widevinePanel') const AutofillAddressPanel = require('../autofill/autofillAddressPanel') const AutofillCreditCardPanel = require('../autofill/autofillCreditCardPanel') const AddEditBookmarkHanger = require('../bookmarks/addEditBookmarkHanger') +const AddEditBookmarkFolder = require('../bookmarks/addEditBookmarkFolder') const LoginRequired = require('./loginRequired') const ReleaseNotes = require('./releaseNotes') const BookmarksToolbar = require('../bookmarks/bookmarksToolbar') @@ -534,21 +535,22 @@ class Main extends React.Component { props.isFullScreen = activeFrame.get('isFullScreen', false) props.isMaximized = isMaximized() || isFullScreen() props.captionButtonsVisible = isWindows - props.showContextMenu = !!currentWindow.get('contextMenuDetail') - props.showPopupWindow = !!currentWindow.get('popupWindowDetail') + props.showContextMenu = currentWindow.has('contextMenuDetail') + props.showPopupWindow = currentWindow.has('popupWindowDetail') props.showSiteInfo = currentWindow.getIn(['ui', 'siteInfo', 'isVisible']) && !isSourceAboutUrl(activeFrame.get('location')) props.showBravery = shieldState.braveShieldsEnabled(activeFrame) && !!currentWindow.get('braveryPanelDetail') - props.showClearData = !!currentWindow.getIn(['ui', 'isClearBrowsingDataPanelVisible']) - props.showImportData = !!currentWindow.get('importBrowserDataDetail') + props.showClearData = currentWindow.hasIn(['ui', 'isClearBrowsingDataPanelVisible']) + props.showImportData = currentWindow.has('importBrowserDataDetail') props.showWidevine = currentWindow.getIn(['widevinePanelDetail', 'shown']) && !isLinux - props.showAutoFillAddress = !!currentWindow.get('autofillAddressDetail') - props.showAutoFillCC = !!currentWindow.get('autofillCreditCardDetail') + props.showAutoFillAddress = currentWindow.has('autofillAddressDetail') + props.showAutoFillCC = currentWindow.has('autofillCreditCardDetail') props.showLogin = !!loginRequiredDetails - props.showBookmarkHanger = currentWindow.get('bookmarkDetail') && + props.showBookmarkHanger = currentWindow.has('bookmarkDetail') && !currentWindow.getIn(['bookmarkDetail', 'isBookmarkHanger']) - props.showNoScript = currentWindow.getIn(['ui', 'noScriptInfo', 'isVisible']) && + props.showBookmarkFolderDialog = currentWindow.has('bookmarkFolderDetail') + props.showNoScript = currentWindow.hasIn(['ui', 'noScriptInfo', 'isVisible']) && siteUtil.getOrigin(activeFrame.get('location')) props.showReleaseNotes = currentWindow.getIn(['ui', 'releaseNotes', 'isVisible']) props.showCheckDefault = isFocused() && defaultBrowserState.shouldDisplayDialog(state) @@ -657,6 +659,11 @@ class Main extends React.Component { ? : null } + { + this.props.showBookmarkFolderDialog + ? + : null + } { this.props.showNoScript ? diff --git a/app/renderer/components/navigation/urlBar.js b/app/renderer/components/navigation/urlBar.js index 8c8c2bc5b68..93d674942c0 100644 --- a/app/renderer/components/navigation/urlBar.js +++ b/app/renderer/components/navigation/urlBar.js @@ -167,7 +167,8 @@ class UrlBar extends React.Component { if (e.shiftKey) { const selectedIndex = this.props.urlbarLocationSuffix.length > 0 ? 1 : this.props.selectedIndex if (selectedIndex !== undefined) { - appActions.removeSite({ location: this.props.suggestionLocation }) + // TODO double check if this is correct + appActions.removeHistorySite({ location: this.props.suggestionLocation }) } } else { this.hideAutoComplete() diff --git a/app/renderer/components/tabs/content/closeTabIcon.js b/app/renderer/components/tabs/content/closeTabIcon.js index 577350c99cc..34bc48fa5bb 100644 --- a/app/renderer/components/tabs/content/closeTabIcon.js +++ b/app/renderer/components/tabs/content/closeTabIcon.js @@ -50,8 +50,9 @@ class CloseTabIcon extends React.Component { mergeProps (state, ownProps) { const currentWindow = state.get('currentWindow') const frameKey = ownProps.frameKey - const isPinnedTab = frameStateUtil.isPinned(currentWindow, frameKey) const frame = frameStateUtil.getFrameByKey(currentWindow, frameKey) || Immutable.Map() + const tabId = frame.get('tabId', tabState.TAB_ID_NONE) + const isPinnedTab = tabState.isTabPinned(state, tabId) const props = {} // used in renderer @@ -64,7 +65,7 @@ class CloseTabIcon extends React.Component { // used in functions props.frameKey = frameKey props.fixTabWidth = ownProps.fixTabWidth - props.tabId = frame.get('tabId', tabState.TAB_ID_NONE) + props.tabId = tabId props.hasFrame = !frame.isEmpty() return props diff --git a/app/renderer/components/tabs/content/favIcon.js b/app/renderer/components/tabs/content/favIcon.js index 354088de059..181300b4fb7 100644 --- a/app/renderer/components/tabs/content/favIcon.js +++ b/app/renderer/components/tabs/content/favIcon.js @@ -12,6 +12,7 @@ const TabIcon = require('./tabIcon') // State const tabContentState = require('../../../../common/state/tabContentState') +const tabState = require('../../../../common/state/tabState') // Utils const frameStateUtil = require('../../../../../js/state/frameStateUtil') @@ -34,12 +35,13 @@ class Favicon extends React.Component { const frameKey = ownProps.frameKey const frame = frameStateUtil.getFrameByKey(currentWindow, frameKey) || Immutable.Map() const isTabLoading = tabContentState.isTabLoading(currentWindow, frameKey) + const tabId = frame.get('tabId', tabState.TAB_ID_NONE) const props = {} // used in renderer props.isTabLoading = isTabLoading props.favicon = !isTabLoading && frame.get('icon') - props.isPinnedTab = frameStateUtil.isPinned(currentWindow, frameKey) + props.isPinnedTab = tabState.isTabPinned(state, tabId) props.tabIconColor = tabContentState.getTabIconColor(currentWindow, frameKey) props.isNarrowestView = tabContentState.isNarrowestView(currentWindow, frameKey) diff --git a/app/renderer/components/tabs/pinnedTabs.js b/app/renderer/components/tabs/pinnedTabs.js index 5615a69b1b5..4a9cc6b81b4 100644 --- a/app/renderer/components/tabs/pinnedTabs.js +++ b/app/renderer/components/tabs/pinnedTabs.js @@ -18,7 +18,6 @@ const windowActions = require('../../../../js/actions/windowActions') const windowStore = require('../../../../js/stores/windowStore') // Constants -const siteTags = require('../../../../js/constants/siteTags') const dragTypes = require('../../../../js/constants/dragTypes') // Utils @@ -26,6 +25,7 @@ const siteUtil = require('../../../../js/state/siteUtil') const dnd = require('../../../../js/dnd') const dndData = require('../../../../js/dndData') const frameStateUtil = require('../../../../js/state/frameStateUtil') +const pinnedSitesUtil = require('../../../common/lib/pinnedSitesUtil') const {isIntermediateAboutPage} = require('../../../../js/lib/appUrlUtil') class PinnedTabs extends React.Component { @@ -58,12 +58,14 @@ class PinnedTabs extends React.Component { if (!sourceDragData.get('pinnedLocation')) { appActions.tabPinned(sourceDragData.get('tabId'), true) } else { - const sourceDetails = siteUtil.getDetailFromFrame(sourceDragData, siteTags.PINNED) + const sourceDetails = pinnedSitesUtil.getDetailFromFrame(sourceDragData) const droppedOnFrame = this.dropFrame(droppedOnTab.props.frameKey) - const destinationDetails = siteUtil.getDetailFromFrame(droppedOnFrame, siteTags.PINNED) - appActions.moveSite(siteUtil.getSiteKey(sourceDetails), + const destinationDetails = pinnedSitesUtil.getDetailFromFrame(droppedOnFrame) + appActions.onPinnedTabReorder( + siteUtil.getSiteKey(sourceDetails), siteUtil.getSiteKey(destinationDetails), - isLeftSide) + isLeftSide + ) } } }, 0) diff --git a/app/renderer/components/tabs/tab.js b/app/renderer/components/tabs/tab.js index ebac3a874b5..d7a85e975c6 100644 --- a/app/renderer/components/tabs/tab.js +++ b/app/renderer/components/tabs/tab.js @@ -225,6 +225,7 @@ class Tab extends React.Component { const partition = typeof frame.get('partitionNumber') === 'string' ? frame.get('partitionNumber').replace(/^partition-/i, '') : frame.get('partitionNumber') + const tabId = frame.get('tabId', tabState.TAB_ID_NONE) const props = {} // used in renderer @@ -234,7 +235,7 @@ class Tab extends React.Component { props.notificationBarActive = notificationBarActive props.isActive = frameStateUtil.isFrameKeyActive(currentWindow, props.frameKey) props.tabWidth = currentWindow.getIn(['ui', 'tabs', 'fixTabWidth']) - props.isPinnedTab = frameStateUtil.isPinned(currentWindow, props.frameKey) + props.isPinnedTab = tabState.isTabPinned(state, tabId) props.canPlayAudio = tabContentState.canPlayAudio(currentWindow, props.frameKey) props.themeColor = tabContentState.getThemeColor(currentWindow, props.frameKey) props.isNarrowView = tabContentState.isNarrowView(currentWindow, props.frameKey) @@ -256,7 +257,7 @@ class Tab extends React.Component { props.totalTabs = state.get('tabs').size props.dragData = state.getIn(['dragData', 'type']) === dragTypes.TAB && state.get('dragData') props.hasTabInFullScreen = tabContentState.hasTabInFullScreen(currentWindow) - props.tabId = frame.get('tabId', tabState.TAB_ID_NONE) + props.tabId = tabId return props } diff --git a/app/renderer/reducers/contextMenuReducer.js b/app/renderer/reducers/contextMenuReducer.js index 36f3795b6a6..47528fc0331 100644 --- a/app/renderer/reducers/contextMenuReducer.js +++ b/app/renderer/reducers/contextMenuReducer.js @@ -237,18 +237,18 @@ const addFolderMenuItem = (closestDestinationDetail, isParent) => { return { label: locale.translation('addFolder'), click: () => { - let siteDetail = Immutable.fromJS({ tags: [siteTags.BOOKMARK_FOLDER] }) let closestKey = null + let folderDetails = Immutable.Map() if (closestDestinationDetail) { closestKey = siteUtil.getSiteKey(closestDestinationDetail) if (isParent) { - siteDetail = siteDetail.set('parentFolderId', (closestDestinationDetail.get('folderId') || closestDestinationDetail.get('parentFolderId'))) + folderDetails = folderDetails.set('parentFolderId', (closestDestinationDetail.get('folderId') || closestDestinationDetail.get('parentFolderId'))) } } - windowActions.addBookmark(siteDetail, closestKey) + windowActions.addBookmarkFolder(folderDetails, closestKey) } } } @@ -273,6 +273,7 @@ const openAllInNewTabsMenuItem = (folderDetail) => { } } +// TODO refactor it const siteDetailTemplateInit = (state, siteKey) => { let isHistoryEntry = false let multipleHistoryEntries = false @@ -359,21 +360,24 @@ const siteDetailTemplateInit = (state, siteKey) => { label: locale.translation(isFolder ? 'editFolder' : 'editBookmark'), click: () => { const editKey = siteUtil.getSiteKey(siteDetail) + // TODO use correct action windowActions.editBookmark(false, editKey) } }, CommonMenu.separatorMenuItem) } + // TODO sync with contexMenu file template.push( { label: locale.translation(deleteLabel), click: () => { - if (Immutable.List.isList(siteDetail)) { + console.log(deleteTag) + /* if (Immutable.List.isList(siteDetail)) { siteDetail.forEach((site) => appActions.removeSite(site, deleteTag)) } else { appActions.removeSite(siteDetail, deleteTag) - } + } */ } }) } @@ -383,7 +387,7 @@ const siteDetailTemplateInit = (state, siteKey) => { template.push(CommonMenu.separatorMenuItem) } template.push( - addBookmarkMenuItem('addBookmark', siteUtil.getDetailFromFrame(activeFrame, siteTags.BOOKMARK), siteDetail, true), + addBookmarkMenuItem('addBookmark', bookmarkUtil.getDetailFromFrame(activeFrame), siteDetail, true), addFolderMenuItem(siteDetail, true)) } @@ -400,11 +404,10 @@ const onSiteDetailMenu = (state, siteKey) => { return state } +// TODO refactor const showBookmarkFolderInit = (state, parentBookmarkFolderKey) => { const appState = appStoreRenderer.state - const allBookmarkItems = siteUtil.getBookmarks(appState.get('sites', Immutable.List())) - const parentBookmarkFolder = appState.getIn(['sites', parentBookmarkFolderKey]) - const items = siteUtil.filterSitesRelativeTo(allBookmarkItems, parentBookmarkFolder) + const items = bookmarkUtil.getBookmarksByParentId(appState, parentBookmarkFolderKey) if (items.size === 0) { return [{ l10nLabelId: 'emptyFolderItem', @@ -426,6 +429,7 @@ const showBookmarkFolderInit = (state, parentBookmarkFolderKey) => { return bookmarkItemsInit(state, items) } +// TODO refactor const bookmarkItemsInit = (state, items) => { const activeFrame = frameStateUtil.getActiveFrame(state) || Immutable.Map() const showFavicon = bookmarkUtil.showFavicon() @@ -441,7 +445,7 @@ const bookmarkItemsInit = (state, items) => { const templateItem = { bookmark: site, draggable: true, - label: site.get('customTitle', site.get('title', site.get('location'))), + label: site.get('title', site.get('location')), icon: showFavicon ? site.get('favicon') : undefined, faIcon, contextMenu: function () { @@ -453,7 +457,7 @@ const bookmarkItemsInit = (state, items) => { dragStart: function (e) { dnd.onDragStart(dragTypes.BOOKMARK, Immutable.fromJS({ location: site.get('location'), - title: site.get('customTitle', site.get('title')), + title: site.get('title'), bookmarkKey: siteKey }), e) }, @@ -481,6 +485,7 @@ const bookmarkItemsInit = (state, items) => { return menuUtil.sanitizeTemplateItems(template) } +// TODO refactor const onMoreBookmarksMenu = (state, action) => { action = validateAction(action) state = validateState(state) diff --git a/app/renderer/reducers/frameReducer.js b/app/renderer/reducers/frameReducer.js index cfe38fcc3d6..a10fac339eb 100644 --- a/app/renderer/reducers/frameReducer.js +++ b/app/renderer/reducers/frameReducer.js @@ -10,7 +10,6 @@ const Immutable = require('immutable') const appConstants = require('../../../js/constants/appConstants') const windowConstants = require('../../../js/constants/windowConstants') const config = require('../../../js/constants/config') -const siteTags = require('../../../js/constants/siteTags') // Actions const appActions = require('../../../js/actions/appActions') @@ -20,7 +19,7 @@ const frameStateUtil = require('../../../js/state/frameStateUtil') const {getCurrentWindowId} = require('../currentWindow') const {getSourceAboutUrl, getSourceMagnetUrl} = require('../../../js/lib/appUrlUtil') const {isURL, isPotentialPhishingUrl, getUrlFromInput} = require('../../../js/lib/urlutil') -const siteUtil = require('../../../js/state/siteUtil') +const bookmarkUtil = require('../../common/lib/bookmarkUtil') const setFullScreen = (state, action) => { const index = frameStateUtil.getIndexByTabId(state, action.tabId) @@ -229,8 +228,8 @@ const frameReducer = (state, action, immutableAction) => { // TODO make this an appAction that gets the bookmark data from tabState const frameProps = frameStateUtil.getFrameByTabId(state, action.tabId) if (frameProps) { - const bookmark = siteUtil.getDetailFromFrame(frameProps, siteTags.BOOKMARK) - appActions.addSite(bookmark, siteTags.BOOKMARK) + const bookmark = bookmarkUtil.getDetailFromFrame(frameProps) + appActions.addBookmark(bookmark) } break } diff --git a/app/sessionStore.js b/app/sessionStore.js index 40307409ab6..fda23dccbf5 100644 --- a/app/sessionStore.js +++ b/app/sessionStore.js @@ -21,34 +21,20 @@ const UpdateStatus = require('../js/constants/updateStatus') const settings = require('../js/constants/settings') const downloadStates = require('../js/constants/downloadStates') const siteUtil = require('../js/state/siteUtil') -const { topSites, pinnedTopSites } = require('../js/data/newTabData') -const { defaultSiteSettingsList } = require('../js/data/siteSettingsList') +const {pinnedTopSites} = require('../js/data/newTabData') +const {defaultSiteSettingsList} = require('../js/data/siteSettingsList') const sessionStorageVersion = 1 const filtering = require('./filtering') const autofill = require('./autofill') const {navigatableTypes} = require('../js/lib/appUrlUtil') const Channel = require('./channel') -const { makeImmutable } = require('./common/state/immutableUtil') +const {makeImmutable} = require('./common/state/immutableUtil') const tabState = require('./common/state/tabState') const windowState = require('./common/state/windowState') const getSetting = require('../js/settings').getSetting const sessionStorageName = `session-store-${sessionStorageVersion}` -const getTopSiteMap = () => { - if (Array.isArray(topSites) && topSites.length) { - let siteMap = {} - let order = 0 - topSites.forEach((site) => { - let key = siteUtil.getSiteKey(Immutable.fromJS(site)) - site.order = order++ - siteMap[key] = site - }) - return siteMap - } - return {} -} - const getTempStoragePath = (filename) => { const epochTimestamp = (new Date()).getTime().toString() filename = filename || 'tmp' @@ -327,7 +313,7 @@ module.exports.cleanAppData = (data, isShutdown) => { if (data.sites) { const clearHistory = isShutdown && getSetting(settings.SHUTDOWN_CLEAR_HISTORY) === true if (clearHistory) { - data.sites = siteUtil.clearHistory(Immutable.fromJS(data.sites)).toJS() + data.historySites = {} if (data.about) { delete data.about.history delete data.about.newtab @@ -496,6 +482,105 @@ module.exports.runPreMigrations = (data) => { } } + // TODO remove this second level additional checks + if (data.sites) { + // pinned sites + if (!data.pinnedSites) { + data.pinnedSites = {} + for (let key of Object.keys(data.sites)) { + const site = data.sites[key] + if (site.tags && site.tags.includes('pinned')) { + delete site.tags + data.pinnedSites[key] = site + } + } + } + + // default sites + let newTab = data.about.newtab + + // TODO remove this pinnedSites check + if (newTab && !data.pinnedSites) { + const ignoredSites = [] + const pinnedSites = [] + + if (newTab.ignoredTopSites) { + for (let site of newTab.ignoredTopSites) { + if (site) { + ignoredSites.push(`${site.location}|0|0`) + } + } + data.about.newtab.ignoredTopSites = ignoredSites + } + + if (newTab.pinnedTopSites) { + for (let site of newTab.pinnedTopSites) { + if (site) { + site.key = `${site.location}|0|0` + pinnedSites.push(site) + } + } + data.about.newtab.pinnedTopSites = pinnedSites + } + + data.about.newtab.sites = [] + } + + // bookmark folders + if (!data.bookmarkFolders) { + data.bookmarkFolders = {} + + for (let key of Object.keys(data.sites)) { + const folder = data.sites[key] + if (folder.tags && folder.tags.includes('bookmark-folder')) { + delete folder.tags + + if (folder.customTitle) { + folder.title = folder.customTitle + delete folder.customTitle + } + + data.bookmarkFolders[key] = folder + } + } + } + + // bookmarks + if (!data.bookmarks) { + data.bookmarks = {} + + for (let key of Object.keys(data.sites)) { + const bookmark = data.sites[key] + if (bookmark.tags && bookmark.tags.includes('bookmark')) { + delete bookmark.tags + + if (bookmark.customTitle) { + bookmark.title = bookmark.customTitle + delete bookmark.customTitle + } + + data.bookmarks[key] = bookmark + } + } + } + // TODO sort newly added bookmarks and folders + + // history + if (!data.historySites) { + data.historySites = {} + + for (let key of Object.keys(data.sites)) { + const site = data.sites[key] + if (site.lastAccessedTime || !site.tags || site.tags.length === 0) { + data.historySites[key] = site + } + } + } + + // TODO enable this delete when everything is done + // delete data.sites + } + return data } @@ -632,7 +717,11 @@ module.exports.defaultAppState = () => { lastConfirmedRecordTimestamp: 0 }, locationSiteKeysCache: undefined, - sites: getTopSiteMap(), + sites: {}, + pinnedSites: {}, + bookmarks: {}, + bookmarkFolders: {}, + historySites: {}, tabs: [], windows: [], extensions: {}, @@ -656,7 +745,7 @@ module.exports.defaultAppState = () => { about: { newtab: { gridLayoutSize: 'small', - sites: topSites, + sites: [], ignoredTopSites: [], pinnedTopSites: pinnedTopSites }, diff --git a/app/sync.js b/app/sync.js index 05122a0fc51..f5ebcc05c78 100644 --- a/app/sync.js +++ b/app/sync.js @@ -19,13 +19,14 @@ const appActions = require('../js/actions/appActions') const syncConstants = require('../js/constants/syncConstants') const appDispatcher = require('../js/dispatcher/appDispatcher') const AppStore = require('../js/stores/appStore') -const siteUtil = require('../js/state/siteUtil') const syncUtil = require('../js/state/syncUtil') const syncPendState = require('./common/state/syncPendState') const getSetting = require('../js/settings').getSetting const settings = require('../js/constants/settings') const extensions = require('./extensions') const {unescapeJSONPointer} = require('./common/lib/jsonUtil') +const bookmarkFoldersState = require('./common/state/bookmarkFoldersState') +const bookmarksState = require('./common/state/bookmarksState') const CATEGORY_MAP = syncUtil.CATEGORY_MAP const CATEGORY_NAMES = Object.keys(categories) @@ -33,7 +34,7 @@ const SYNC_ACTIONS = Object.values(syncConstants) // Fields that should trigger a sync SEND when changed const SYNC_FIELDS = { - sites: ['location', 'customTitle', 'folderId', 'parentFolderId', 'tags'], + sites: ['location', 'title', 'folderId', 'parentFolderId', 'tags'], siteSettings: Object.keys(syncUtil.siteSettingDefaults) } @@ -260,7 +261,8 @@ module.exports.onSyncReady = (isFirstRun, e) => { sendSyncRecords(e.sender, writeActions.CREATE, [deviceRecord]) deviceIdSent = true } - const sites = appState.get('sites') || new Immutable.List() + const bookmarks = bookmarksState.getBookmarks(appState) + const bookmarkFolders = bookmarkFoldersState.getFolders(appState) const seed = appState.get('seed') || new Immutable.List() /** @@ -273,38 +275,34 @@ module.exports.onSyncReady = (isFirstRun, e) => { */ const folderToObjectId = {} const bookmarksToSync = [] - const shouldSyncBookmark = (site) => { - if (!site) { - return false - } - if (siteUtil.isSiteBookmarked(sites, site) !== true && siteUtil.isFolder(site) !== true) { - return false - } + + const shouldSyncBookmark = (bookmark) => { // originalSeed is set on reset to prevent synced bookmarks on a device // from being re-synced. - const originalSeed = site.get('originalSeed') - if (site.get('objectId') && (!originalSeed || seed.equals(originalSeed))) { + const originalSeed = bookmark.get('originalSeed') + if (bookmark.get('objectId') && (!originalSeed || seed.equals(originalSeed))) { return false } - if (folderToObjectId[site.get('folderId')]) { return false } - return syncUtil.isSyncable('bookmark', site) + + return !folderToObjectId[bookmark.get('folderId')] } - const syncBookmark = (site) => { - const siteJS = site.toJS() - const parentFolderId = site.get('parentFolderId') + const syncBookmark = (bookmark) => { + const bookmarkJS = bookmark.toJS() + + const parentFolderId = bookmark.get('parentFolderId') if (typeof parentFolderId === 'number') { if (!folderToObjectId[parentFolderId]) { - const folderResult = siteUtil.getFolder(sites, parentFolderId) + const folderResult = bookmarkFolders.get(parentFolderId) if (folderResult) { syncBookmark(folderResult[1]) } } - siteJS.parentFolderObjectId = folderToObjectId[parentFolderId] + bookmarkJS.parentFolderObjectId = folderToObjectId[parentFolderId] } - const record = syncUtil.createSiteData(siteJS, appState) - const folderId = site.get('folderId') + const record = syncUtil.createSiteData(bookmarkJS, appState) + const folderId = bookmark.get('folderId') if (typeof folderId === 'number') { folderToObjectId[folderId] = record.objectId } @@ -313,12 +311,16 @@ module.exports.onSyncReady = (isFirstRun, e) => { } // Sync bookmarks that have not been synced yet. - sites.forEach((site) => { - if (shouldSyncBookmark(site) !== true) { + bookmarks.forEach((bookmark) => { + if (shouldSyncBookmark(bookmark) !== true) { return } - syncBookmark(site) + + syncBookmark(bookmark) }) + + // TODO add bookamrkFolder sync as well + sendSyncRecords(e.sender, writeActions.CREATE, bookmarksToSync) // Sync site settings that have not been synced yet @@ -490,7 +492,7 @@ module.exports.init = function (appState) { } if (!bookmarksToolbarShown && isFirstRun) { // syncing for the first time - const bookmarks = siteUtil.getBookmarks(AppStore.getState().get('sites')) + const bookmarks = bookmarksState.getBookmarks(AppStore.getState()) if (!bookmarks.size) { for (const record of records) { if (record && record.objectData === 'bookmark') { diff --git a/docs/state.md b/docs/state.md index cf2a9e3bd44..7c77232c4ac 100644 --- a/docs/state.md +++ b/docs/state.md @@ -53,6 +53,28 @@ AppStore timestamp: number } }, + bookmarks: { + [bookmarkKey]: { + favicon: string, // URL of the favicon + location: string, + objectId: Array., + originalSeed: Array., // bookmarks that have been synced before a sync profile reset + parentFolderId: number, + partitionNumber: number, // optionally specifies a specific session + skipSync: boolean, + title: string + } + }, + bookmarkFolders: { + [folderKey]: { + folderId: number, + objectId: Array., + originalSeed: Array., // only set for bookmarks that have been synced before a sync profile reset + parentFolderId: number, // set for bookmarks and bookmark folders only + skipSync: boolean, // Set for objects FETCHed by sync + title: string + } + }, clearBrowsingDataDefaults: { allSiteCookies: boolean, autocompleteData: boolean, @@ -118,6 +140,18 @@ AppStore flash: { enabled: boolean // enable flash }, + historySites: { + [siteKey]: { + favicon: string, // URL of the favicon + lastAccessedTime: number, // datetime.getTime() + location: string, + objectId: Array., + partitionNumber: number, // optionally specifies a specific session + skipSync: boolean, // Set for objects FETCHed by sync + title: string, + themeColor: string + } + }, menu: { template: object // used on Windows and by our tests: template object with Menubar control }, @@ -156,6 +190,13 @@ AppStore origin: string, // origin of the form username: string }], + pinnedSites: { + [siteKey]: { + location: string, + title: string, + order: number + } + }, settings: { // See defaults in js/constants/appConfig.js 'adblock.customRules': string, // custom rules in ABP filter syntax @@ -230,24 +271,6 @@ AppStore 'tabs.switch-to-new-tabs': boolean, // true if newly opened tabs should be focused immediately 'tabs.tabs-per-page': number // number of tabs per tab page }, - sites: { - [siteKey]: { - creationTime: number, //creation time of bookmark - customTitle: string, // User provided title for bookmark; overrides title - favicon: string, // URL of the favicon - folderId: number, // set for bookmark folders only - lastAccessedTime: number, // datetime.getTime() - location: string, - objectId: Array., - originalSeed: Array., // only set for bookmarks that have been synced before a sync profile reset - parentFolderId: number, // set for bookmarks and bookmark folders only - partitionNumber: number, // optionally specifies a specific session - skipSync: boolean, // Set for objects FETCHed by sync - tags: [string], // empty, 'bookmark', 'bookmark-folder', 'default', or 'reader' - themeColor: string, // CSS compatible color string - title: string - } // folder: folderId; bookmark/history: location + partitionNumber + parentFolderId - }, locationSiteKeyCache: { [location]: Array. // location -> site keys }, diff --git a/js/about/newtab.js b/js/about/newtab.js index e9a2b83010b..a3be7ff39d9 100644 --- a/js/about/newtab.js +++ b/js/about/newtab.js @@ -5,26 +5,36 @@ const path = require('path') const React = require('react') const Immutable = require('immutable') -const messages = require('../constants/messages') -const HTML5Backend = require('react-dnd-html5-backend') const {DragDropContext} = require('react-dnd') +const HTML5Backend = require('react-dnd-html5-backend') + +// Components const Stats = require('./newTabComponents/stats') const Clock = require('./newTabComponents/clock') const Block = require('./newTabComponents/block') const SiteRemovalNotification = require('./newTabComponents/siteRemovalNotification') const FooterInfo = require('./newTabComponents/footerInfo') +const NewPrivateTab = require('./newprivatetab') + +// Constants +const messages = require('../constants/messages') +const config = require('../constants/config') + +// Actions const aboutActions = require('./aboutActions') +const windowActions = require('../actions/windowActions') + +// Data +const backgrounds = require('../data/backgrounds') +const newTabData = require('../data/newTabData') + +// Utils const siteUtil = require('../state/siteUtil') const urlutils = require('../lib/urlutil') -const siteTags = require('../constants/siteTags') -const config = require('../constants/config') -const backgrounds = require('../data/backgrounds') const {random} = require('../../app/common/lib/randomUtil') -const NewPrivateTab = require('./newprivatetab') -const windowActions = require('../actions/windowActions') - const ipc = window.chrome.ipcRenderer +// Styles require('../../less/about/newtab.less') require('../../node_modules/font-awesome/css/font-awesome.css') @@ -32,15 +42,17 @@ class NewTabPage extends React.Component { constructor (props) { super(props) this.state = { - showSiteRemovalNotification: false, + showNotification: false, imageLoadFailed: false, updatedStamp: undefined, showEmptyPage: true, showImages: false, backgroundImage: undefined } - ipc.on(messages.NEWTAB_DATA_UPDATED, (e, newTabData) => { - const data = Immutable.fromJS(newTabData || {}) + this.staticData = Immutable.fromJS(newTabData.topSites) + + ipc.on(messages.NEWTAB_DATA_UPDATED, (e, newData) => { + let data = Immutable.fromJS(newData || {}) const updatedStamp = data.getIn(['newTabDetail', 'updatedStamp']) // Only update if the data has changed. @@ -50,6 +62,18 @@ class NewTabPage extends React.Component { return } + const topSites = data.getIn(['newTabDetail', 'sites'], Immutable.List()) + if (topSites.size < 18) { + const pinned = data.getIn(['newTabDetail', 'pinnedTopSites'], Immutable.List()) + const ignored = data.getIn(['newTabDetail', 'ignoredTopSites'], Immutable.List()) + const preDefined = this.staticData + .filter((site) => { + const key = site.get('key') + return !pinned.find(site => site.get('key') === key) && !ignored.includes(key) + }) + data = data.setIn(['newTabDetail', 'sites'], topSites.concat(preDefined)) + } + const showEmptyPage = !!data.get('showEmptyPage') const showImages = !!data.get('showImages') && !showEmptyPage this.setState({ @@ -63,83 +87,79 @@ class NewTabPage extends React.Component { }) }) } + get showImages () { return this.state.showImages && !!this.state.backgroundImage } + get randomBackgroundImage () { const image = Object.assign({}, backgrounds[Math.floor(random() * backgrounds.length)]) image.style = {backgroundImage: 'url(' + image.source + ')'} return image } + get fallbackImage () { const image = Object.assign({}, config.newtab.fallbackImage) const pathToImage = path.join(__dirname, '..', '..', image.source) image.style = {backgroundImage: 'url(' + `${pathToImage}` + ')'} return image } + get topSites () { - return this.state.newTabData.getIn(['newTabDetail', 'sites']) || Immutable.List() + return this.state.newTabData.getIn(['newTabDetail', 'sites']) } + get pinnedTopSites () { - return (this.state.newTabData.getIn(['newTabDetail', 'pinnedTopSites']) || Immutable.List()).setSize(18) + return this.state.newTabData.getIn(['newTabDetail', 'pinnedTopSites'], Immutable.List()) } + get ignoredTopSites () { - return this.state.newTabData.getIn(['newTabDetail', 'ignoredTopSites']) || Immutable.List() + return this.state.newTabData.getIn(['newTabDetail', 'ignoredTopSites'], Immutable.List()) } + get gridLayoutSize () { - return this.state.newTabData.getIn(['newTabDetail', 'gridLayoutSize']) || 'small' + return this.state.newTabData.getIn(['newTabDetail', 'gridLayoutSize'], 'small') } - isPinned (siteProps) { - return this.pinnedTopSites.filter((site) => { - if (!site || !site.get) return false - return site.get('location') === siteProps.get('location') && - site.get('partitionNumber') === siteProps.get('partitionNumber') - }).size > 0 + + isPinned (siteKey) { + return this.pinnedTopSites.find(site => site.get('key') === siteKey) } - isBookmarked (siteProps) { - return siteUtil.isBookmark(siteProps) + + isBookmarked (siteKey) { + // TODO add bookmark status to site that are send here + return siteUtil.isBookmark(null) } + get gridLayout () { const sizeToCount = {large: 18, medium: 12, small: 6} const count = sizeToCount[this.gridLayoutSize] - return this.topSites.take(count) + + let sites = this.pinnedTopSites.take(count) + + if (sites.size < count) { + sites = sites.concat(this.topSites.take(count - sites.size)) + } + + return sites } - showSiteRemovalNotification () { + + showNotification () { this.setState({ - showSiteRemovalNotification: true + showNotification: true }) } + hideSiteRemovalNotification () { this.setState({ - showSiteRemovalNotification: false + showNotification: false }) } - /** - * save number of rows on store. gridsLayout starts with 3 rows (large). - * Rows are reduced at each click and then reset to three again - */ - onChangeGridLayout () { - const gridLayoutSize = this.gridLayoutSize - const changeGridSizeTo = (size) => aboutActions.setNewTabDetail({gridLayoutSize: size}) - - if (gridLayoutSize === 'large') { - changeGridSizeTo('medium') - } else if (gridLayoutSize === 'medium') { - changeGridSizeTo('small') - } else if (gridLayoutSize === 'small') { - changeGridSizeTo('large') - } else { - changeGridSizeTo('large') - } - - return gridLayoutSize - } - - onDraggedSite (currentId, finalId) { + // TODO fix this function + onDraggedSite (siteKey, destinationKey) { let gridSites = this.topSites - const currentPosition = gridSites.filter((site) => site.get('location') === currentId).get(0) - const finalPosition = gridSites.filter((site) => site.get('location') === finalId).get(0) + const currentPosition = gridSites.get(siteKey) + const finalPosition = gridSites.get(destinationKey) const currentPositionIndex = gridSites.indexOf(currentPosition) const finalPositionIndex = gridSites.indexOf(finalPosition) @@ -154,63 +174,59 @@ class NewTabPage extends React.Component { pinnedTopSites = pinnedTopSites.splice(finalPositionIndex, 0, currentPosition) // If site is pinned, update pinnedTopSites list - const newTabState = {} + const newTabState = Immutable.Map() if (this.isPinned(currentPosition)) { - newTabState.pinnedTopSites = pinnedTopSites + newTabState.set('pinnedTopSites', pinnedTopSites) } - newTabState.sites = gridSites + newTabState.set('sites', gridSites) // Only update if there was an actual change - const stateDiff = Immutable.fromJS(newTabState) const existingState = this.state.newTabData || Immutable.fromJS({}) - const proposedState = existingState.mergeIn(['newTabDetail'], stateDiff) + const proposedState = existingState.mergeIn(['newTabDetail'], newTabState) if (!proposedState.isSubset(existingState)) { - aboutActions.setNewTabDetail(stateDiff) + aboutActions.setNewTabDetail(newTabState) } } - onToggleBookmark (siteProps) { - const siteDetail = siteUtil.getDetailFromFrame(siteProps, siteTags.BOOKMARK) - const editing = this.isBookmarked(siteProps) - const key = siteUtil.getSiteKey(siteDetail) + onToggleBookmark (siteKey) { + const editing = this.isBookmarked(siteKey) if (editing) { - windowActions.editBookmark(false, key) + windowActions.editBookmark(false, siteKey) } else { - windowActions.onBookmarkAdded(false, key) + windowActions.onBookmarkAdded(false, siteKey) } } - onPinnedTopSite (siteProps) { - const currentPosition = this.topSites.filter((site) => siteProps.get('location') === site.get('location')).get(0) - const currentPositionIndex = this.topSites.indexOf(currentPosition) + onPinnedTopSite (siteKey) { + let pinnedTopSites = this.pinnedTopSites + const siteProps = this.topSites.find(site => site.get('key') === siteKey) - // If pinned, leave it null. Otherwise stores site on ignoredTopSites list, retaining the same position - let pinnedTopSites = this.pinnedTopSites.splice(currentPositionIndex, 1, this.isPinned(siteProps) ? null : siteProps) + if (this.isPinned(siteKey)) { + pinnedTopSites = pinnedTopSites.filter(site => site.get('key') !== siteKey) + } else { + pinnedTopSites = pinnedTopSites.push(siteProps) + } aboutActions.setNewTabDetail({pinnedTopSites: pinnedTopSites}) } - onIgnoredTopSite (siteProps) { - this.showSiteRemovalNotification() + onIgnoredTopSite (siteKey) { + this.showNotification(siteKey) // If a pinnedTopSite is ignored, remove it from the pinned list as well const newTabState = {} - if (this.isPinned(siteProps)) { - const gridSites = this.topSites - const currentPosition = gridSites.filter((site) => siteProps.get('location') === site.get('location')).get(0) - const currentPositionIndex = gridSites.indexOf(currentPosition) - const pinnedTopSites = this.pinnedTopSites.splice(currentPositionIndex, 1, null) - newTabState.pinnedTopSites = pinnedTopSites + if (this.isPinned(siteKey)) { + newTabState.pinnedTopSites = this.pinnedTopSites.filter(site => site.get('key') !== siteKey) } - newTabState.ignoredTopSites = this.ignoredTopSites.push(siteProps) + newTabState.ignoredTopSites = this.ignoredTopSites.push(siteKey) aboutActions.setNewTabDetail(newTabState, true) } onUndoIgnoredTopSite () { // Remove last List's entry - const ignoredTopSites = this.ignoredTopSites.splice(-1, 1) + const ignoredTopSites = this.ignoredTopSites.pop() aboutActions.setNewTabDetail({ignoredTopSites: ignoredTopSites}, true) this.hideSiteRemovalNotification() } @@ -236,6 +252,12 @@ class NewTabPage extends React.Component { }) } + getLetterFromUrl (url) { + const hostname = urlutils.getHostname(url.get('location'), true) + const name = url.get('title') || hostname || '?' + return name.charAt(0).toUpperCase() + } + render () { // don't render if user prefers an empty page if (this.state.showEmptyPage && !this.props.isIncognito) { @@ -250,11 +272,6 @@ class NewTabPage extends React.Component { if (!this.state.newTabData) { return null } - const getLetterFromUrl = (url) => { - const hostname = urlutils.getHostname(url.get('location'), true) - const name = url.get('title') || hostname || '?' - return name.charAt(0).toUpperCase() - } const gridLayout = this.gridLayout const backgroundProps = {} let gradientClassName = 'gradient' @@ -278,24 +295,24 @@ class NewTabPage extends React.Component {
{ - this.state.showSiteRemovalNotification + this.state.showNotification ? (bookmark.get('parentFolderId') || 0) === (folderDetail.get('folderId') || 0) && siteUtil.isBookmark(bookmark)) + const bookmarks = bookmarksUtil.getBookmarksByParentId(appStoreRenderer.state, folderDetail.get('folderId')) // Only load the first 25 tabs as loaded bookmarks .forEach((bookmark, i) => { if (i <= 25) { appActions.createTabRequested( - Object.assign(siteUtil.toCreateProperties(bookmark), { + Object.assign(bookmarksUtil.toCreateProperties(bookmark), { active: false }), getSetting(SWITCH_TO_NEW_TABS)) } else { diff --git a/js/actions/windowActions.js b/js/actions/windowActions.js index 9f8c5f73149..0845201cd2b 100644 --- a/js/actions/windowActions.js +++ b/js/actions/windowActions.js @@ -530,6 +530,38 @@ const windowActions = { }) }, + /** + * Used for displaying bookmark folder dialog + * when adding bookmark site or folder + */ + addBookmarkFolder: function (folderDetails, closestKey) { + dispatch({ + actionType: windowConstants.WINDOW_ON_ADD_BOOKMARK_FOLDER, + folderDetails, + closestKey + }) + }, + + /** + * Used for displaying bookmark folder dialog + * when editing bookmark site or folder + */ + editBookmarkFolder: function (editKey) { + dispatch({ + actionType: windowConstants.WINDOW_ON_EDIT_BOOKMARK_FOLDER, + editKey + }) + }, + + /** + * Used for closing a bookmark dialog + */ + onBookmarkFolderClose: function () { + dispatch({ + actionType: windowConstants.WINDOW_ON_BOOKMARK_FOLDER_CLOSE + }) + }, + /** * Dispatches a message to set context menu detail. * If set, also indicates that the context menu is shown. diff --git a/js/constants/appConstants.js b/js/constants/appConstants.js index f8e12793899..a69f54873d5 100644 --- a/js/constants/appConstants.js +++ b/js/constants/appConstants.js @@ -13,9 +13,9 @@ const appConstants = { APP_WINDOW_UPDATED: _, APP_TAB_CLOSED: _, APP_TAB_UPDATED: _, - APP_ADD_SITE: _, + APP_ADD_HISTORY_SITE: _, APP_SET_STATE: _, - APP_REMOVE_SITE: _, + APP_REMOVE_HISTORY_SITE: _, APP_MOVE_SITE: _, APP_APPLY_SITE_RECORDS: _, APP_MERGE_DOWNLOAD_DETAIL: _, // Sets an individual download detail @@ -144,7 +144,14 @@ const appConstants = { APP_SPELLING_SUGGESTED: _, APP_LEARN_SPELLING: _, APP_FORGET_LEARNED_SPELLING: _, - APP_SET_VERSION_INFO: _ + APP_SET_VERSION_INFO: _, + APP_ON_PINNED_TAB_REORDER: _, + APP_ADD_BOOKMARK_FOLDER: _, + APP_EDIT_BOOKMARK_FOLDER: _, + APP_REMOVE_BOOKMARK_FOLDER: _, + APP_REMOVE_BOOKMARK: _, + APP_ADD_BOOKMARKS: _, + APP_ADD_BOOKMARK_FOLDERS: _ } module.exports = mapValuesByKeys(appConstants) diff --git a/js/constants/siteTags.js b/js/constants/siteTags.js index 4a86ba2fa59..581635ea4e1 100644 --- a/js/constants/siteTags.js +++ b/js/constants/siteTags.js @@ -4,13 +4,12 @@ const mapValuesByKeys = require('../lib/functional').mapValuesByKeys +// TODO remove + const _ = null const siteTags = { - DEFAULT: _, BOOKMARK: _, - BOOKMARK_FOLDER: _, - PINNED: _, - READING_LIST: _ + BOOKMARK_FOLDER: _ } module.exports = mapValuesByKeys(siteTags) diff --git a/js/constants/windowConstants.js b/js/constants/windowConstants.js index 002f927062f..7186fbf9f51 100644 --- a/js/constants/windowConstants.js +++ b/js/constants/windowConstants.js @@ -111,7 +111,10 @@ const windowConstants = { WINDOW_ON_ADD_BOOKMARK: _, WINDOW_ON_EDIT_BOOKMARK: _, WINDOW_ON_BOOKMARK_CLOSE: _, - WINDOW_ON_BOOKMARK_ADDED: _ + WINDOW_ON_BOOKMARK_ADDED: _, + WINDOW_ON_ADD_BOOKMARK_FOLDER: _, + WINDOW_ON_EDIT_BOOKMARK_FOLDER: _, + WINDOW_ON_BOOKMARK_FOLDER_CLOSE: _ } module.exports = mapValuesByKeys(windowConstants) diff --git a/js/contextMenus.js b/js/contextMenus.js index 50079596af9..840d38caedd 100644 --- a/js/contextMenus.js +++ b/js/contextMenus.js @@ -33,6 +33,7 @@ const urlParse = require('../app/common/urlParse') const {getCurrentWindow} = require('../app/renderer/currentWindow') const extensionState = require('../app/common/state/extensionState') const extensionActions = require('../app/common/actions/extensionActions') +const bookmarkUtil = require('../app/common/lib/bookmarkUtil') const {makeImmutable} = require('../app/common/state/immutableUtil') const isDarwin = process.platform === 'darwin' @@ -70,18 +71,18 @@ const addFolderMenuItem = (closestDestinationDetail, isParent) => { return { label: locale.translation('addFolder'), click: () => { - let siteDetail = Immutable.fromJS({ tags: [siteTags.BOOKMARK_FOLDER] }) let closestKey = null + let folderDetails = Immutable.Map() if (closestDestinationDetail) { closestKey = siteUtil.getSiteKey(closestDestinationDetail) if (isParent) { - siteDetail = siteDetail.set('parentFolderId', (closestDestinationDetail.get('folderId') || closestDestinationDetail.get('parentFolderId'))) + folderDetails = folderDetails.set('parentFolderId', (closestDestinationDetail.get('folderId') || closestDestinationDetail.get('parentFolderId'))) } } - windowActions.addBookmark(siteDetail, closestKey) + windowActions.addBookmarkFolder(folderDetails, closestKey) } } } @@ -133,8 +134,7 @@ function tabsToolbarTemplateInit (bookmarkTitle, bookmarkLink, closestDestinatio template.push(addBookmarkMenuItem('addBookmark', { title: bookmarkTitle, - location: bookmarkLink, - tags: [siteTags.BOOKMARK] + location: bookmarkLink }, closestDestinationDetail, isParent)) template.push(addFolderMenuItem(closestDestinationDetail, isParent)) @@ -225,6 +225,7 @@ function downloadsToolbarTemplateInit (downloadId, downloadItem) { return menuUtil.sanitizeTemplateItems(template) } +// TODO refactor it function siteDetailTemplateInit (siteDetail, activeFrame) { let isHistoryEntry = false let multipleHistoryEntries = false @@ -291,7 +292,7 @@ function siteDetailTemplateInit (siteDetail, activeFrame) { CommonMenu.separatorMenuItem) } } else { - template.push(openAllInNewTabsMenuItem(appStoreRenderer.state.get('sites'), siteDetail), + template.push(openAllInNewTabsMenuItem(siteDetail), CommonMenu.separatorMenuItem) } @@ -306,21 +307,24 @@ function siteDetailTemplateInit (siteDetail, activeFrame) { label: locale.translation(isFolder ? 'editFolder' : 'editBookmark'), click: () => { const editKey = siteUtil.getSiteKey(siteDetail) + // TODO use correct action windowActions.editBookmark(false, editKey) } }, CommonMenu.separatorMenuItem) } + // TODO we need to determinate which site do we want to remove template.push( { label: locale.translation(deleteLabel), click: () => { - if (Immutable.List.isList(siteDetail)) { + console.log(deleteTag) + /* if (Immutable.List.isList(siteDetail)) { siteDetail.forEach((site) => appActions.removeSite(site, deleteTag)) } else { appActions.removeSite(siteDetail, deleteTag) - } + } */ } }) } @@ -330,7 +334,7 @@ function siteDetailTemplateInit (siteDetail, activeFrame) { template.push(CommonMenu.separatorMenuItem) } template.push( - addBookmarkMenuItem('addBookmark', siteUtil.getDetailFromFrame(activeFrame, siteTags.BOOKMARK), siteDetail, true), + addBookmarkMenuItem('addBookmark', bookmarkUtil.getDetailFromFrame(activeFrame), siteDetail, true), addFolderMenuItem(siteDetail, true)) } @@ -724,11 +728,11 @@ const openInNewTabMenuItem = (url, isPrivate, partitionNumber, openerTabId) => { } } -const openAllInNewTabsMenuItem = (allSites, folderDetail) => { +const openAllInNewTabsMenuItem = (folderDetail) => { return { label: locale.translation('openAllInTabs'), click: () => { - bookmarkActions.openBookmarksInFolder(allSites, folderDetail) + bookmarkActions.openBookmarksInFolder(folderDetail) } } } @@ -1009,8 +1013,7 @@ function mainTemplateInit (nodeProps, frame, tab) { ) if (isLink) { template.push(addBookmarkMenuItem('bookmarkLink', { - location: nodeProps.linkURL, - tags: [siteTags.BOOKMARK] + location: nodeProps.linkURL }, false)) } } @@ -1024,8 +1027,7 @@ function mainTemplateInit (nodeProps, frame, tab) { if (!isImage) { if (isLink) { template.push(addBookmarkMenuItem('bookmarkLink', { - location: nodeProps.linkURL, - tags: [siteTags.BOOKMARK] + location: nodeProps.linkURL }, false)) } else { template.push( @@ -1055,7 +1057,7 @@ function mainTemplateInit (nodeProps, frame, tab) { } }, CommonMenu.separatorMenuItem, - addBookmarkMenuItem('bookmarkPage', siteUtil.getDetailFromFrame(frame, siteTags.BOOKMARK), false)) + addBookmarkMenuItem('bookmarkPage', bookmarkUtil.getDetailFromFrame(frame), false)) if (!isAboutPage) { template.push({ @@ -1204,8 +1206,7 @@ function mainTemplateInit (nodeProps, frame, tab) { template.push( CommonMenu.separatorMenuItem, addBookmarkMenuItem('addBookmark', { - location: nodeProps.linkURL, - tags: [siteTags.BOOKMARK] + location: nodeProps.linkURL }), addFolderMenuItem() ) diff --git a/js/data/newTabData.js b/js/data/newTabData.js index ccc0b4e286c..94431f18e7e 100644 --- a/js/data/newTabData.js +++ b/js/data/newTabData.js @@ -5,75 +5,64 @@ const {getBraveExtUrl} = require('../lib/appUrlUtil') const iconPath = getBraveExtUrl('img/newtab/defaultTopSitesIcon') -const now = 1 - module.exports.pinnedTopSites = [ - { - "count": 1, - "favicon": `${iconPath}/twitter.png`, - "lastAccessedTime": now, - "location": "https://twitter.com/brave", - "partitionNumber": 0, - "tags": ['default'], - "themeColor": "rgb(255, 255, 255)", - "title": "Brave Software (@brave) | Twitter" + { + 'key': 'https://twitter.com/brave/|0|0', + 'count': 0, + 'favicon': `${iconPath}/twitter.png`, + 'location': 'https://twitter.com/brave/', + 'themeColor': 'rgb(255, 255, 255)', + 'title': 'Brave Software (@brave) | Twitter' } ] module.exports.topSites = [ { - "count": 1, - "favicon": `${iconPath}/twitter.png`, - "lastAccessedTime": now, - "location": "https://twitter.com/brave", - "partitionNumber": 0, - "tags": ['default'], - "themeColor": "rgb(255, 255, 255)", - "title": "Brave Software (@brave) | Twitter" - }, { - "count": 1, - "favicon": `${iconPath}/facebook.png`, - "lastAccessedTime": now, - "location": "https://www.facebook.com/BraveSoftware/", - "partitionNumber": 0, - "tags": ['default'], - "themeColor": "rgb(59, 89, 152)", - "title": "Brave Software | Facebook" - }, { - "count": 1, - "favicon": `${iconPath}/youtube.png`, - "lastAccessedTime": now, - "location": "https://www.youtube.com/bravesoftware", - "partitionNumber": 0, - "tags": ['default'], - "themeColor": "#E62117", - "title": "Brave Browser - YouTube" - }, { - "count": 1, - "favicon": `${iconPath}/brave.ico`, - "lastAccessedTime": now, - "location": "https://brave.com/", - "partitionNumber": 0, - "tags": ['default'], - "themeColor": "rgb(255, 255, 255)", - "title": "Brave Software | Building a Better Web" - }, { - "count": 1, - "favicon": `${iconPath}/appstore.png`, - "lastAccessedTime": now, - "location": "https://itunes.apple.com/app/brave-web-browser/id1052879175?mt=8", - "partitionNumber": 0, - "tags": ['default'], - "themeColor": "rgba(255, 255, 255, 1)", - "title": "Brave Web Browser: Fast with built-in adblock on the App Store" - }, { - "count": 1, - "favicon": `${iconPath}/playstore.png`, - "lastAccessedTime": now, - "location": "https://play.google.com/store/apps/details?id=com.brave.browser", - "partitionNumber": 0, - "tags": ['default'], - "themeColor": "rgb(241, 241, 241)", - "title": "Brave Browser: Fast AdBlock – Apps para Android no Google Play" + 'key': 'https://twitter.com/brave/|0|0', + 'count': 0, + 'favicon': `${iconPath}/twitter.png`, + 'location': 'https://twitter.com/brave', + 'themeColor': 'rgb(255, 255, 255)', + 'title': 'Brave Software (@brave) | Twitter' + }, + { + 'key': 'https://www.facebook.com/BraveSoftware/|0|0', + 'count': 0, + 'favicon': `${iconPath}/facebook.png`, + 'location': 'https://www.facebook.com/BraveSoftware/', + 'themeColor': 'rgb(59, 89, 152)', + 'title': 'Brave Software | Facebook' + }, + { + 'key': 'https://www.youtube.com/bravesoftware/|0|0', + 'count': 0, + 'favicon': `${iconPath}/youtube.png`, + 'location': 'https://www.youtube.com/bravesoftware/', + 'themeColor': '#E62117', + 'title': 'Brave Browser - YouTube' + }, + { + 'key': 'https://brave.com/|0|0', + 'count': 0, + 'favicon': `${iconPath}/brave.ico`, + 'location': 'https://brave.com/', + 'themeColor': 'rgb(255, 255, 255)', + 'title': 'Brave Software | Building a Better Web' + }, + { + 'key': 'https://itunes.apple.com/app/brave-web-browser/id1052879175?mt=8|0|0', + 'count': 0, + 'favicon': `${iconPath}/appstore.png`, + 'location': 'https://itunes.apple.com/app/brave-web-browser/id1052879175?mt=8', + 'themeColor': 'rgba(255, 255, 255, 1)', + 'title': 'Brave Web Browser: Fast with built-in adblock on the App Store' + }, + { + 'key': 'https://play.google.com/store/apps/details?id=com.brave.browser|0|0', + 'count': 0, + 'favicon': `${iconPath}/playstore.png`, + 'location': 'https://play.google.com/store/apps/details?id=com.brave.browser', + 'themeColor': 'rgb(241, 241, 241)', + 'title': 'Brave Browser: Fast AdBlock – Apps para Android no Google Play' } ] diff --git a/js/lib/importer.js b/js/lib/importer.js index b0925cff8b2..35c5244542d 100644 --- a/js/lib/importer.js +++ b/js/lib/importer.js @@ -4,15 +4,14 @@ const fs = require('fs') const appActions = require('../actions/appActions') -const siteTags = require('../constants/siteTags') -const siteUtil = require('../state/siteUtil') const Immutable = require('immutable') const appStoreRenderer = require('../stores/appStoreRenderer') +const bookmarFoldersUtil = require('../../app/common/lib/bookmarkFoldersUtil') /** * Processes a single node from an exported HTML file from Firefox or Chrome * @param {Object} parserState - the current parser state - * @param {Object} node - The current DOM node which is being processed + * @param {Object} domNode - The current DOM node which is being processed */ function processBookmarkNode (parserState, domNode) { switch (domNode.tagName) { @@ -38,12 +37,11 @@ function processBookmarkNode (parserState, domNode) { title: domNode.innerText, folderId: parserState.nextFolderId, parentFolderId: parserState.parentFolderId, - lastAccessedTime: (domNode.getAttribute('LAST_MODIFIED') || domNode.getAttribute('ADD_DATE') || 0) * 1000, - tags: [siteTags.BOOKMARK_FOLDER] + lastAccessedTime: (domNode.getAttribute('LAST_MODIFIED') || domNode.getAttribute('ADD_DATE') || 0) * 1000 } parserState.lastFolderId = parserState.nextFolderId parserState.nextFolderId++ - parserState.sites.push(folder) + parserState.bookmarkFolders.push(folder) } else { parserState.lastFolderId = 0 parserState.foundBookmarksToolbar = true @@ -54,15 +52,14 @@ function processBookmarkNode (parserState, domNode) { if (domNode.href.startsWith('place:')) { break } - const site = { + const bookmarks = { title: domNode.innerText, location: domNode.href, favicon: domNode.getAttribute('ICON'), parentFolderId: parserState.parentFolderId, - lastAccessedTime: (domNode.getAttribute('LAST_MODIFIED') || domNode.getAttribute('ADD_DATE') || 0) * 1000, - tags: [siteTags.BOOKMARK] + lastAccessedTime: (domNode.getAttribute('LAST_MODIFIED') || domNode.getAttribute('ADD_DATE') || 0) * 1000 } - parserState.sites.push(site) + parserState.bookmarks.push(bookmarks) break } } @@ -91,17 +88,19 @@ module.exports.importFromHTML = (path) => { // Each window's appStoreRenderer holds a copy of the app state, but it's not // mutable, so this is only used for getting the current list of sites. const parserState = { - nextFolderId: siteUtil.getNextFolderId(appStoreRenderer.state.get('sites')), + nextFolderId: bookmarFoldersUtil.getNextFolderId(appStoreRenderer.state.get('bookmarkFolders')), lastFolderId: -1, parentFolderId: -1, - sites: [] + bookmarks: [], + bookmarkFolders: [] } // Process each of the nodes starting with the first node which is either DL or DT processBookmarkNode(parserState, doc.querySelector('dl, dt')) // Add the sites to the app store in the main process - appActions.addSite(Immutable.fromJS(parserState.sites)) + appActions.addBookmarks(Immutable.fromJS(parserState.bookmarks)) + appActions.addBookmarkFolders(Immutable.fromJS(parserState.bookmarkFolders)) resolve({importCount: parserState.sites.length}) }) }) diff --git a/js/lib/textCalculator.js b/js/lib/textCalculator.js index 8068219c1f4..c543d340b83 100644 --- a/js/lib/textCalculator.js +++ b/js/lib/textCalculator.js @@ -2,8 +2,8 @@ * 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/. */ -const ctx = document.createElement('canvas').getContext('2d') module.exports.calculateTextWidth = (text, font = '11px Arial') => { + const ctx = document.createElement('canvas').getContext('2d') ctx.font = font return ctx.measureText(text).width } diff --git a/js/state/frameStateUtil.js b/js/state/frameStateUtil.js index b71f871694e..e6a2e422142 100644 --- a/js/state/frameStateUtil.js +++ b/js/state/frameStateUtil.js @@ -58,6 +58,7 @@ function getSortedFrameKeys (state) { .map(frame => frame.get('key')) } +// TODO check if order is preserved, could be a bug function getPinnedFrames (state) { return state.get('frames').filter((frame) => frame.get('pinnedLocation')) } diff --git a/js/state/siteUtil.js b/js/state/siteUtil.js index 5320b15a9c7..08274c0da24 100644 --- a/js/state/siteUtil.js +++ b/js/state/siteUtil.js @@ -11,8 +11,7 @@ const UrlUtil = require('../lib/urlutil') const urlParse = require('../../app/common/urlParse') const {makeImmutable} = require('../../app/common/state/immutableUtil') -const defaultTags = new Immutable.List([siteTags.DEFAULT]) - +// TODO remove const isBookmark = (tags) => { if (!tags) { return false @@ -20,6 +19,7 @@ const isBookmark = (tags) => { return tags.includes(siteTags.BOOKMARK) } +// TODO remove const isBookmarkFolder = (tags) => { if (!tags) { return false @@ -28,14 +28,6 @@ const isBookmarkFolder = (tags) => { (tags && typeof tags !== 'string' && tags.includes(siteTags.BOOKMARK_FOLDER)) } -const isPinnedTab = (tags) => { - if (!tags) { - return false - } - return tags.includes(siteTags.PINNED) -} -module.exports.isPinnedTab = isPinnedTab - const reorderSite = (sites, order) => { sites = sites.map((site) => { const siteOrder = site.get('order') @@ -109,79 +101,13 @@ module.exports.getLocationFromSiteKey = function (siteKey) { * @param siteDetail The site to check if it's in the specified tag * @return true if the location is already bookmarked */ +// TODO remove this when sync is updated module.exports.isSiteBookmarked = function (sites, siteDetail) { const siteKey = module.exports.getSiteKey(siteDetail) const siteTags = sites.getIn([siteKey, 'tags']) return isBookmark(siteTags) } -/** - * Checks if a location is bookmarked. - * - * @param state The application state Immutable map - * @param {string} location - * @return {boolean} - */ -module.exports.isLocationBookmarked = function (state, location) { - const sites = state.get('sites') - const siteKeys = siteCache.getLocationSiteKeys(state, location) - if (!siteKeys || siteKeys.length === 0) { - return false - } - return siteKeys.some(key => { - const site = sites.get(key) - if (!site) { - return false - } - return isBookmark(site.get('tags')) - }) -} - -const getNextFolderIdItem = (sites) => - sites.max((siteA, siteB) => { - const folderIdA = siteA.get('folderId') - const folderIdB = siteB.get('folderId') - if (folderIdA === folderIdB) { - return 0 - } - if (folderIdA === undefined) { - return false - } - if (folderIdB === undefined) { - return true - } - return folderIdA > folderIdB - }) - -module.exports.getNextFolderId = (sites) => { - const defaultFolderId = 0 - if (!sites) { - return defaultFolderId - } - const maxIdItem = getNextFolderIdItem(sites) - return (maxIdItem ? (maxIdItem.get('folderId') || 0) : 0) + 1 -} - -module.exports.getNextFolderName = (sites, name) => { - if (!sites) { - return name - } - const site = sites.find((site) => - isBookmarkFolder(site.get('tags')) && - site.get('customTitle') === name - ) - if (!site) { - return name - } - const filenameFormat = /(.*) \((\d+)\)/ - let result = filenameFormat.exec(name) - if (!result) { - return module.exports.getNextFolderName(sites, name + ' (1)') - } - const nextNum = parseInt(result[2]) + 1 - return module.exports.getNextFolderName(sites, result[1] + ' (' + nextNum + ')') -} - const mergeSiteLastAccessedTime = (oldSiteDetail, newSiteDetail, tag) => { const newTime = newSiteDetail && newSiteDetail.get('lastAccessedTime') const oldTime = oldSiteDetail && oldSiteDetail.get('lastAccessedTime') @@ -199,16 +125,13 @@ const mergeSiteLastAccessedTime = (oldSiteDetail, newSiteDetail, tag) => { // Some details can be copied from the existing siteDetail if null // ex: parentFolderId, partitionNumber, and favicon +// TODO remove const mergeSiteDetails = (oldSiteDetail, newSiteDetail, tag, folderId, order) => { let tags = (oldSiteDetail && oldSiteDetail.get('tags')) || new Immutable.List() if (tag) { tags = tags.toSet().add(tag).toList() } - const customTitle = typeof newSiteDetail.get('customTitle') === 'string' - ? newSiteDetail.get('customTitle') - : (newSiteDetail.get('customTitle') || (oldSiteDetail && oldSiteDetail.get('customTitle'))) - const lastAccessedTime = mergeSiteLastAccessedTime(oldSiteDetail, newSiteDetail, tag) let site = makeImmutable({ @@ -229,9 +152,6 @@ const mergeSiteDetails = (oldSiteDetail, newSiteDetail, tag, folderId, order) => if (folderId) { site = site.set('folderId', Number(folderId)) } - if (typeof customTitle === 'string') { - site = site.set('customTitle', customTitle) - } if (newSiteDetail.get('parentFolderId') !== undefined || (oldSiteDetail && oldSiteDetail.get('parentFolderId'))) { let parentFolderId = newSiteDetail.get('parentFolderId') !== undefined ? newSiteDetail.get('parentFolderId') : oldSiteDetail.get('parentFolderId') @@ -273,6 +193,7 @@ const mergeSiteDetails = (oldSiteDetail, newSiteDetail, tag, folderId, order) => * does not to be re-uploaded * @return The new state Immutable object */ +// TODO remove module.exports.addSite = function (state, siteDetail, tag, oldKey, skipSync) { let sites = state.get('sites') // Get tag from siteDetail object if not passed via tag param @@ -291,8 +212,7 @@ module.exports.addSite = function (state, siteDetail, tag, oldKey, skipSync) { if (!oldSite && folderId) { // Remove duplicate folder (needed for import) const dupFolder = sites.find((site) => isBookmarkFolder(site.get('tags')) && - site.get('parentFolderId') === siteDetail.get('parentFolderId') && - site.get('customTitle') === siteDetail.get('customTitle')) + site.get('parentFolderId') === siteDetail.get('parentFolderId')) if (dupFolder) { state = module.exports.removeSite(state, dupFolder, siteTags.BOOKMARK_FOLDER, true) } @@ -371,6 +291,7 @@ module.exports.removeSiteByObjectId = function (state, objectId, objectData) { * @param {Function=} syncCallback * @return {Immutable.Map} The new state Immutable object */ +// TODO remove module.exports.removeSite = function (state, siteDetail, tag, reorder = true, syncCallback) { let sites = state.get('sites') const key = module.exports.getSiteKey(siteDetail) @@ -402,20 +323,11 @@ module.exports.removeSite = function (state, siteDetail, tag, reorder = true, sy return state } if (isBookmark(tag)) { - if (isPinnedTab(tags)) { - const tags = site.get('tags').filterNot((tag) => tag === siteTags.BOOKMARK) - site = site.set('tags', tags) - return state.setIn(stateKey, site) - } if (state.get('sites').size && reorder) { const order = state.getIn(stateKey.concat(['order'])) state = state.set('sites', reorderSite(state.get('sites'), order)) } return state.deleteIn(['sites', key]) - } else if (isPinnedTab(tag)) { - const tags = site.get('tags').filterNot((tag) => tag === siteTags.PINNED) - site = site.set('tags', tags) - return state.setIn(stateKey, site) } else { site = site.set('lastAccessedTime', undefined) return state.setIn(stateKey, site) @@ -470,6 +382,7 @@ module.exports.isMoveAllowed = (sites, sourceDetail, destinationDetail) => { * @param disallowReparent If set to true, parent folder will not be set * @return The new state Immutable object */ +// TODO remove module.exports.moveSite = function (state, sourceKey, destinationKey, prepend, destinationIsParent, disallowReparent) { let sites = state.get('sites') @@ -523,159 +436,22 @@ module.exports.moveSite = function (state, sourceKey, destinationKey, prepend, return state.setIn(['sites', destinationSiteKey], sourceSite) } -module.exports.getDetailFromFrame = function (frame, tag) { - const pinnedLocation = frame.get('pinnedLocation') - let location = frame.get('location') - if (pinnedLocation && pinnedLocation !== 'about:blank' && tag === siteTags.PINNED) { - location = frame.get('pinnedLocation') - } - +module.exports.getDetailFromFrame = function (frame) { return makeImmutable({ - location, + location: frame.get('location'), title: frame.get('title'), partitionNumber: frame.get('partitionNumber'), - tags: tag ? [tag] : [], favicon: frame.get('icon'), themeColor: frame.get('themeColor') || frame.get('computedThemeColor') }) } -const getSitesBySubkey = (sites, siteKey, tag) => { - if (!sites || !siteKey) { - return makeImmutable([]) - } - const splitKey = siteKey.split('|', 2) - const partialKey = splitKey.join('|') - const matches = sites.filter((site, key) => { - if (key.indexOf(partialKey) > -1 && (!tag || (tag && site.get('tags').includes(tag)))) { - return true - } - return false - }) - return matches.toList() -} - -module.exports.getDetailFromTab = function (tab, tag, sites) { - let location = tab.get('url') - const partitionNumber = tab.get('partitionNumber') - let parentFolderId - - // if site map is available, look up extra information: - // - original url (if redirected) - // - parent folder id - if (sites) { - let results = makeImmutable([]) - - // get all sites matching URL and partition (disregarding parentFolderId) - let siteKey = module.exports.getSiteKey(makeImmutable({location, partitionNumber})) - results = results.merge(getSitesBySubkey(sites, siteKey, tag)) - - // only check for provisional location if entry is not found - if (results.size === 0) { - // if provisional location is different, grab any results which have that URL - // this may be different if the site was redirected - const provisionalLocation = tab.getIn(['frame', 'provisionalLocation']) - if (provisionalLocation && provisionalLocation !== location) { - siteKey = module.exports.getSiteKey(makeImmutable({ - location: provisionalLocation, - partitionNumber - })) - results = results.merge(getSitesBySubkey(sites, siteKey, tag)) - } - } - - // update details which get returned below - if (results.size > 0) { - location = results.getIn([0, 'location']) - parentFolderId = results.getIn([0, 'parentFolderId']) - } - } - - const siteDetail = { - location: location, - title: tab.get('title'), - tags: tag ? [tag] : [] - } - if (partitionNumber) { - siteDetail.partitionNumber = partitionNumber - } - if (parentFolderId) { - siteDetail.parentFolderId = parentFolderId - } - return Immutable.fromJS(siteDetail) -} - -module.exports.getDetailFromCreateProperties = function (createProperties, tag) { - const siteDetail = { - location: createProperties.get('url'), - tags: tag ? [tag] : [] - } - if (createProperties.get('partitionNumber') !== undefined) { - siteDetail.partitionNumber = createProperties.get('partitionNumber') - } - return Immutable.fromJS(siteDetail) -} - -/** - * Update the favicon URL for all entries in the state sites - * which match a given location. Currently, there should only be - * one match, but this will handle multiple. - * - * @param state The application state - * @param location URL for the entry needing an update - * @param favicon favicon URL - */ -module.exports.updateSiteFavicon = function (state, location, favicon) { - if (UrlUtil.isNotURL(location)) { - return state - } - const siteKeys = siteCache.getLocationSiteKeys(state, location) - if (!siteKeys || siteKeys.length === 0) { - return state - } - siteKeys.forEach((siteKey) => { - state = state.setIn(['sites', siteKey, 'favicon'], favicon) - }) - return state -} - -/** - * Converts a siteDetail to createProperties format - * @param {Object} siteDetail - A site detail as per app state - * @return {Object} A createProperties plain JS object, not ImmutableJS - */ -module.exports.toCreateProperties = function (siteDetail) { - return { - url: siteDetail.get('location'), - partitionNumber: siteDetail.get('partitionNumber') - } -} - -/** - * Compares 2 site details - * @param siteDetail1 The first site detail to compare. - * @param siteDetail2 The second site detail to compare. - * @return true if the site details should be considered the same. - */ -module.exports.isEquivalent = function (siteDetail1, siteDetail2) { - const isFolder1 = module.exports.isFolder(siteDetail1) - const isFolder2 = module.exports.isFolder(siteDetail2) - if (isFolder1 !== isFolder2) { - return false - } - - // If they are both folders - if (isFolder1) { - return siteDetail1.get('folderId') === siteDetail2.get('folderId') - } - return siteDetail1.get('location') === siteDetail2.get('location') && siteDetail1.get('partitionNumber') === siteDetail2.get('partitionNumber') -} - /** * Determines if the site detail is a bookmark. * @param siteDetail The site detail to check. * @return true if the site detail has a bookmark tag. */ +// TODO remove module.exports.isBookmark = function (siteDetail) { if (siteDetail) { return isBookmark(siteDetail.get('tags')) @@ -688,6 +464,7 @@ module.exports.isBookmark = function (siteDetail) { * @param siteDetail The site detail to check. * @return true if the site detail is a folder. */ +// TODO remove module.exports.isFolder = function (siteDetail) { if (siteDetail) { return isBookmarkFolder(siteDetail.get('tags')) && siteDetail.get('folderId') !== undefined @@ -695,25 +472,16 @@ module.exports.isFolder = function (siteDetail) { return false } -/** - * Determines if the site detail is an imported bookmark. - * @param siteDetail The site detail to check. - * @return true if the site detail is a folder. - */ -module.exports.isImportedBookmark = function (siteDetail) { - return siteDetail.get('lastAccessedTime') === 0 -} - /** * Determines if the site detail is a history entry. * @param siteDetail The site detail to check. * @return true if the site detail is a history entry. */ +// TODO remove when sync is refactored module.exports.isHistoryEntry = function (siteDetail) { if (siteDetail && typeof siteDetail.get('location') === 'string') { const tags = siteDetail.get('tags') if (siteDetail.get('location').startsWith('about:') || - module.exports.isDefaultEntry(siteDetail) || isBookmarkFolder(tags)) { return false } @@ -722,63 +490,6 @@ module.exports.isHistoryEntry = function (siteDetail) { return false } -/** - * Determines if the site detail is one of default sites in about:newtab. - * @param {Immutable.Map} siteDetail The site detail to check. - * @returns {boolean} if the site detail is a default newtab entry. - */ -module.exports.isDefaultEntry = function (siteDetail) { - return Immutable.is(siteDetail.get('tags'), defaultTags) && - siteDetail.get('lastAccessedTime') === 1 -} - -/** - * Get a folder by folderId - * @returns {Immutable.List.} sites - * @param {number} folderId - * @returns {Array[, ]|undefined} - */ -module.exports.getFolder = function (sites, folderId) { - const entry = sites.findEntry((site, _path) => { - return module.exports.isFolder(site) && site.get('folderId') === folderId - }) - if (!entry) { return undefined } - return entry -} - -/** - * Obtains an array of folders - */ -module.exports.getFolders = function (sites, folderId, parentId, labelPrefix) { - parentId = parentId || 0 - let folders = [] - const results = sites - .filter(site => { - return (site.get('parentFolderId', 0) === parentId && module.exports.isFolder(site)) - }) - .toList() - .sort(module.exports.siteSort) - - const resultSize = results.size - for (let i = 0; i < resultSize; i++) { - const site = results.get(i) - if (site.get('folderId') === folderId) { - continue - } - - const label = (labelPrefix || '') + (site.get('customTitle') || site.get('title')) - folders.push({ - folderId: site.get('folderId'), - parentFolderId: site.get('parentFolderId'), - label - }) - const subsites = module.exports.getFolders(sites, folderId, site.get('folderId'), (label || '') + ' / ') - folders = folders.concat(subsites) - } - - return folders -} - /** * Filters out non recent sites based on the app setting for history size. * @param sites The application state's Immutable sites list. @@ -793,18 +504,6 @@ module.exports.filterOutNonRecents = function (sites) { return sitesWithTags.concat(topHistorySites) } -/** - * Filters sites relative to a parent site (folder). - * @param sites The application state's Immutable sites list. - * @param relSite The folder to filter to. - */ -module.exports.filterSitesRelativeTo = function (sites, relSite) { - if (!relSite.get('folderId')) { - return sites - } - return sites.filter((site) => site.get('parentFolderId') === relSite.get('folderId')) -} - /** * Clears history by * - filtering out entries which have no tags @@ -825,7 +524,7 @@ module.exports.clearHistory = function (sites) { * Returns all sites that have a bookmark tag. * @param sites The application state's Immutable sites list. */ - +// TODO remove when getToolbarBookmarks is refactored module.exports.getBookmarks = function (sites) { if (sites) { return sites.filter((site) => isBookmarkFolder(site.get('tags')) || isBookmark(site.get('tags'))) diff --git a/js/state/syncUtil.js b/js/state/syncUtil.js index 2439eef3d19..b19805be659 100644 --- a/js/state/syncUtil.js +++ b/js/state/syncUtil.js @@ -47,7 +47,19 @@ module.exports.siteSettingDefaults = { // Whitelist of valid browser-laptop site fields. In browser-laptop, site // is used for both bookmarks and history sites. -const SITE_FIELDS = ['objectId', 'location', 'title', 'customTitle', 'tags', 'favicon', 'themeColor', 'lastAccessedTime', 'creationTime', 'partitionNumber', 'folderId', 'parentFolderId'] +const SITE_FIELDS = [ + 'objectId', + 'location', + 'title', + 'tags', + 'favicon', + 'themeColor', + 'lastAccessedTime', + 'creationTime', + 'partitionNumber', + 'folderId', + 'parentFolderId' +] const pickFields = (object, fields) => { return fields.reduce((a, x) => { @@ -293,6 +305,7 @@ module.exports.getExistingObject = (categoryName, syncRecord) => { */ module.exports.createSiteCache = (appState) => { const objectsById = new Immutable.Map().withMutations(objectsById => { + // TODO what to do here? appState.get('sites').forEach((site, siteKey) => { const objectId = site.get('objectId') if (!objectId) { return true } @@ -461,7 +474,6 @@ module.exports.createSiteData = (site, appState) => { const siteData = { location: '', title: '', - customTitle: '', favicon: '', lastAccessedTime: 0, creationTime: 0 diff --git a/js/stores/appStore.js b/js/stores/appStore.js index 04a72501ba5..ff8fb1c6ba4 100644 --- a/js/stores/appStore.js +++ b/js/stores/appStore.js @@ -45,6 +45,8 @@ const extensionState = require('../../app/common/state/extensionState') const aboutNewTabState = require('../../app/common/state/aboutNewTabState') const aboutHistoryState = require('../../app/common/state/aboutHistoryState') const tabState = require('../../app/common/state/tabState') +const bookmarksState = require('../../app/common/state/bookmarksState') +var bookmarkFoldersState = require('../../app/common/state/bookmarkFoldersState.js') const isDarwin = process.platform === 'darwin' const isWindows = process.platform === 'win32' @@ -343,16 +345,6 @@ function setDefaultWindowSize () { const appStore = new AppStore() const emitChanges = debounce(appStore.emitChanges.bind(appStore), 5) -/** - * Clears out the top X non tagged sites. - * This is debounced to every 1 minute, the cleanup is not particularly intensive - * but there's no point to cleanup frequently. - */ -const filterOutNonRecents = debounce(() => { - appState = appState.set('sites', siteUtil.filterOutNonRecents(appState.get('sites'))) - emitChanges() -}, 60 * 1000) - /** * Useful for updating non-react preferences (electron properties, etc). * Called when any settings are modified (ex: via preferences). @@ -457,19 +449,13 @@ const handleAppAction = (action) => { case appConstants.APP_DATA_URL_COPIED: nativeImage.copyDataURL(action.dataURL, action.html, action.text) break - case appConstants.APP_ADD_SITE: - case appConstants.APP_ADD_BOOKMARK: - case appConstants.APP_EDIT_BOOKMARK: - const oldSiteSize = appState.get('sites').size - calculateTopSites(false) + // TODO we don't need this for bookmarks, refactor when doing history + case appConstants.APP_ADD_HISTORY_SITE: + calculateTopSites(true) appState = aboutHistoryState.setHistory(appState, action) - // If there was an item added then clear out the old history entries - if (oldSiteSize !== appState.get('sites').size) { - filterOutNonRecents() - } break case appConstants.APP_APPLY_SITE_RECORDS: - case appConstants.APP_REMOVE_SITE: + case appConstants.APP_REMOVE_HISTORY_SITE: calculateTopSites(true) appState = aboutHistoryState.setHistory(appState, action) break @@ -665,8 +651,8 @@ const handleAppAction = (action) => { const clearData = defaults ? defaults.merge(temp) : temp if (clearData.get('browserHistory')) { - calculateTopSites(true) - appState = aboutHistoryState.setHistory(appState) + appState = aboutNewTabState.clearTopSites(appState) + appState = aboutHistoryState.clearHistory(appState) syncActions.clearHistory() BrowserWindow.getAllWindows().forEach((wnd) => wnd.webContents.send(messages.CLEAR_CLOSED_FRAMES)) } @@ -800,7 +786,7 @@ const handleAppAction = (action) => { appState = appState.set('defaultBrowserCheckComplete', {}) break case windowConstants.WINDOW_SET_FAVICON: - appState = siteUtil.updateSiteFavicon(appState, action.frameProps.get('location'), action.favicon) + appState = bookmarksState.updateSiteFavicon(appState, action.frameProps.get('location'), action.favicon) if (action.frameProps.get('favicon') !== action.favicon) { calculateTopSites(false) } @@ -856,13 +842,22 @@ const handleAppAction = (action) => { const syncDefault = Immutable.fromJS(sessionStore.defaultAppState().sync) const originalSeed = appState.getIn(['sync', 'seed']) appState = appState.set('sync', syncDefault) - appState.get('sites').forEach((site, key) => { - if (site.has('objectId') && syncUtil.isSyncable('bookmark', site)) { + bookmarksState.getBookmarks(appState).forEach((site, key) => { + if (site.has('objectId')) { + // Remember which profile this bookmark was originally synced to. + // Since old bookmarks should be synced when a new profile is created, + // we have to keep track of which profile already has these bookmarks + // or else the old profile may have these bookmarks duplicated. #7405 + appState = appState.setIn(['bookmarks', key, 'originalSeed'], originalSeed) + } + }) + bookmarkFoldersState.getFolders(appState).forEach((site, key) => { + if (site.has('objectId')) { // Remember which profile this bookmark was originally synced to. // Since old bookmarks should be synced when a new profile is created, // we have to keep track of which profile already has these bookmarks // or else the old profile may have these bookmarks duplicated. #7405 - appState = appState.setIn(['sites', key, 'originalSeed'], originalSeed) + appState = appState.setIn(['bookmarks', key, 'originalSeed'], originalSeed) } }) appState.setIn(['sync', 'devices'], {}) diff --git a/js/stores/windowStore.js b/js/stores/windowStore.js index 4e09138b53f..bd592337fa6 100644 --- a/js/stores/windowStore.js +++ b/js/stores/windowStore.js @@ -471,18 +471,20 @@ const doAction = (action) => { windowState = windowState.delete('bookmarkDetail') break case windowConstants.WINDOW_ON_EDIT_BOOKMARK: - const siteDetail = appStoreRenderer.state.getIn(['sites', action.editKey]) + { + const siteDetail = appStoreRenderer.state.getIn(['bookmarks', action.editKey]) - windowState = windowState.setIn(['bookmarkDetail'], Immutable.fromJS({ - siteDetail: siteDetail, - editKey: action.editKey, - isBookmarkHanger: action.isHanger - })) - break + windowState = windowState.setIn(['bookmarkDetail'], Immutable.fromJS({ + siteDetail: siteDetail, + editKey: action.editKey, + isBookmarkHanger: action.isHanger + })) + break + } case windowConstants.WINDOW_ON_BOOKMARK_ADDED: { let editKey = action.editKey - const site = appStoreRenderer.state.getIn(['sites', editKey]) + const site = appStoreRenderer.state.getIn(['bookmarks', editKey]) let siteDetail = action.siteDetail if (site) { @@ -503,6 +505,25 @@ const doAction = (action) => { })) } break + case windowConstants.WINDOW_ON_ADD_BOOKMARK_FOLDER: + windowState = windowState.setIn(['bookmarkFolderDetail'], Immutable.fromJS({ + folderDetails: action.folderDetails, + closestKey: action.closestKey + })) + break + case windowConstants.WINDOW_ON_EDIT_BOOKMARK_FOLDER: + { + const folderDetails = appStoreRenderer.state.getIn(['bookmarkFolders', action.editKey]) + + windowState = windowState.setIn(['bookmarkFolderDetail'], Immutable.fromJS({ + folderDetails: folderDetails, + editKey: action.editKey + })) + break + } + case windowConstants.WINDOW_ON_BOOKMARK_FOLDER_CLOSE: + windowState = windowState.delete('bookmarkFolderDetail') + break case windowConstants.WINDOW_AUTOFILL_SELECTION_CLICKED: ipc.send('autofill-selection-clicked', action.tabId, action.value, action.frontEndId, action.index) windowState = windowState.delete('contextMenuDetail') diff --git a/test/unit/app/common/lib/pinnedSitesUtilTest.js b/test/unit/app/common/lib/pinnedSitesUtilTest.js new file mode 100644 index 00000000000..e2ef0ffeedf --- /dev/null +++ b/test/unit/app/common/lib/pinnedSitesUtilTest.js @@ -0,0 +1,39 @@ +/* global describe, it */ +const pinnedSitesUtil = require('../../../../../app/common/lib/pinnedSitesUtil') +const assert = require('assert') +const Immutable = require('immutable') + +require('../../../braveUnit') + +describe('pinnedSitesUtil', () => { + const location = 'https://css-tricks.com/' + const order = 9 + const partitionNumber = 5 + const expectedSiteProps = Immutable.fromJS({ + location, + order, + partitionNumber + }) + + let site = Immutable.fromJS({ + favicon: 'https://css-tricks.com/favicon.ico', + lastAccessedTime: 1493560182224, + location: location, + order: order, + partitionNumber: partitionNumber, + title: 'CSS-Tricks' + }) + + describe('getPinnedSiteProps', () => { + it('returns object with necessary fields', () => { + const result = pinnedSitesUtil.getPinnedSiteProps(site) + assert.deepEqual(expectedSiteProps, result) + }) + + it('set partitionNumber field to 0 in case of missing this field', () => { + const newSite = site.delete('partitionNumber') + const result = pinnedSitesUtil.getPinnedSiteProps(newSite) + assert.equal(0, result.get('partitionNumber')) + }) + }) +}) diff --git a/test/unit/app/common/lib/windowsUtilTest.js b/test/unit/app/common/lib/windowsUtilTest.js deleted file mode 100644 index 60ece75b82b..00000000000 --- a/test/unit/app/common/lib/windowsUtilTest.js +++ /dev/null @@ -1,40 +0,0 @@ -/* global describe, beforeEach, it */ -const windowsUtil = require('../../../../../app/common/lib/windowsUtil') -const assert = require('assert') -const Immutable = require('immutable') - -require('../../../braveUnit') - -describe('windowsUtil', () => { - const location = 'https://css-tricks.com/' - const order = 9 - const partitionNumber = 5 - const expectedSiteProps = Immutable.fromJS({ - location, - order, - partitionNumber - }) - let site - - describe('getPinnedSiteProps', () => { - beforeEach(() => { - site = Immutable.fromJS({ - favicon: 'https://css-tricks.com/favicon.ico', - lastAccessedTime: 1493560182224, - location: location, - order: order, - partitionNumber: partitionNumber, - title: 'CSS-Tricks' - }) - }) - it('returns object with necessary fields', () => { - const result = windowsUtil.getPinnedSiteProps(site) - assert.deepEqual(expectedSiteProps, result) - }) - it('set partitionNumber field to 0 in case of missing this field', () => { - site = site.delete('partitionNumber') - const result = windowsUtil.getPinnedSiteProps(site) - assert.equal(0, result.get('partitionNumber')) - }) - }) -}) diff --git a/test/unit/state/siteUtilTest.js b/test/unit/state/siteUtilTest.js index f45653f3ca0..0114b9feaf8 100644 --- a/test/unit/state/siteUtilTest.js +++ b/test/unit/state/siteUtilTest.js @@ -1335,67 +1335,6 @@ describe('siteUtil', function () { }) }) - describe('isEquivalent', function () { - it('returns true if both siteDetail objects are identical', function () { - const siteDetail1 = Immutable.fromJS({ - location: testUrl1, - partitionNumber: 0, - tags: [siteTags.BOOKMARK] - }) - const siteDetail2 = Immutable.fromJS({ - location: testUrl1, - partitionNumber: 0, - tags: [siteTags.BOOKMARK] - }) - assert.equal(siteUtil.isEquivalent(siteDetail1, siteDetail2), true) - }) - it('returns false if one object is a folder and the other is not', function () { - const siteDetail1 = Immutable.fromJS({ - tags: [siteTags.BOOKMARK] - }) - const siteDetail2 = Immutable.fromJS({ - tags: [siteTags.BOOKMARK_FOLDER], - folderId: 1 - }) - assert.equal(siteUtil.isEquivalent(siteDetail1, siteDetail2), false) - }) - it('returns false if both are folders and have a different folderId', function () { - const siteDetail1 = Immutable.fromJS({ - tags: [siteTags.BOOKMARK_FOLDER], - folderId: 0 - }) - const siteDetail2 = Immutable.fromJS({ - tags: [siteTags.BOOKMARK_FOLDER], - folderId: 1 - }) - assert.equal(siteUtil.isEquivalent(siteDetail1, siteDetail2), false) - }) - it('returns false if both are bookmarks and have a different location', function () { - const siteDetail1 = Immutable.fromJS({ - tags: [siteTags.BOOKMARK], - location: testUrl1 - }) - const siteDetail2 = Immutable.fromJS({ - tags: [siteTags.BOOKMARK], - location: 'http://example.com/' - }) - assert.equal(siteUtil.isEquivalent(siteDetail1, siteDetail2), false) - }) - it('returns false if both are bookmarks and have a different partitionNumber', function () { - const siteDetail1 = Immutable.fromJS({ - tags: [siteTags.BOOKMARK], - location: testUrl1, - partitionNumber: 0 - }) - const siteDetail2 = Immutable.fromJS({ - tags: [siteTags.BOOKMARK], - location: testUrl2, - partitionNumber: 1 - }) - assert.equal(siteUtil.isEquivalent(siteDetail1, siteDetail2), false) - }) - }) - describe('isFolder', function () { it('returns true if the input is a siteDetail and has a `BOOKMARK_FOLDER` tag and a folder ID', function () { const siteDetail = Immutable.fromJS({ @@ -1683,20 +1622,4 @@ describe('siteUtil', function () { assert.strictEqual(siteUtil.getOrigin('http://http/test'), 'http://http') }) }) - describe('isPinnedTab', function () { - it('detects pinned tab site', function () { - assert.strictEqual(siteUtil.isPinnedTab(siteTags.PINNED), true) - assert.strictEqual(siteUtil.isPinnedTab([siteTags.PINNED]), true) - }) - it('detects not pinned for no site tags', function () { - assert.strictEqual(siteUtil.isPinnedTab([]), false) - }) - it('detects not pinned for site tags which are not PINNED', function () { - assert.strictEqual(siteUtil.isPinnedTab(siteTags.BOOKMARK), false) - assert.strictEqual(siteUtil.isPinnedTab([siteTags.BOOKMARK]), false) - }) - it('detects pinned when bookmarked and pinned', function () { - assert.strictEqual(siteUtil.isPinnedTab([siteTags.PINNED, siteTags.BOOKMARK]), true) - }) - }) })