diff --git a/app/browser/reducers/urlBarSuggestionsReducer.js b/app/browser/reducers/urlBarSuggestionsReducer.js new file mode 100644 index 00000000000..9c9403b72ce --- /dev/null +++ b/app/browser/reducers/urlBarSuggestionsReducer.js @@ -0,0 +1,43 @@ +/* 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/. */ + +'use strict' + +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: + if (Immutable.List.isList(action.siteDetail)) { + action.siteDetail.forEach((s) => { + add(s) + }) + } else { + add(action.siteDetail) + } + break + case appConstants.APP_SET_STATE: + init(Object.values(action.appState.get('sites').toJS())) + break + case appConstants.APP_URL_BAR_TEXT_CHANGED: + generateNewSuggestionsList(state, action.windowId, action.tabId, action.input) + generateNewSearchXHRResults(state, action.windowId, action.tabId, action.input) + break + case appConstants.APP_SEARCH_SUGGESTION_RESULTS_AVAILABLE: + state = state.set('searchResults', makeImmutable(action.searchResults)) + if (action.query) { + const windowId = tabState.windowId(state, action.tabId) + generateNewSuggestionsList(state, windowId, action.tabId, action.query) + } + break + } + return state +} + +module.exports = urlBarSuggestionsReducer diff --git a/app/common/lib/fetchSearchSuggestions.js b/app/common/lib/fetchSearchSuggestions.js new file mode 100644 index 00000000000..f1b4fc9477f --- /dev/null +++ b/app/common/lib/fetchSearchSuggestions.js @@ -0,0 +1,32 @@ +/* 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 appActions = require('../../../js/actions/appActions') +const {request} = require('../../../js/lib/request') +const debounce = require('../../../js/lib/debounce') + +const fetchSearchSuggestions = debounce((windowId, tabId, autocompleteURL, searchTerms) => { + autocompleteURL.replace('{searchTerms}', encodeURIComponent(searchTerms)) + request(autocompleteURL.replace('{searchTerms}', encodeURIComponent(searchTerms)), (err, response, body) => { + if (err) { + return + } + + let searchResults + let query + try { + const parsed = JSON.parse(body) + query = parsed[0] + searchResults = parsed[1] + } catch (e) { + console.warn(e) + return + } + + // Once we have the online suggestions, append them to the others + appActions.searchSuggestionResultsAvailable(tabId, query, searchResults) + }) +}, 10) + +module.exports = fetchSearchSuggestions diff --git a/app/common/lib/siteSuggestions.js b/app/common/lib/siteSuggestions.js new file mode 100644 index 00000000000..3bbdbb44f23 --- /dev/null +++ b/app/common/lib/siteSuggestions.js @@ -0,0 +1,138 @@ +/* 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 Bloodhound = require('bloodhound-js') +const {isUrl} = require('../../../js/lib/appUrlUtil') +const siteTags = require('../../../js/constants/siteTags') +const urlParse = require('../urlParse') + +let initialized = false +let engine +let lastQueryOptions + +// Same as sortByAccessCountWithAgeDecay but if one is a prefix of the +// other then it is considered always sorted first. +const sortForSuggestions = (s1, s2) => { + return lastQueryOptions.internalSort(s1, s2) +} + +const getSiteIdentity = (data) => { + if (typeof data === 'string') { + return data + } + return (data.location || '') + (data.partitionNumber ? '|' + data.partitionNumber : '') +} + +const init = (sites) => { + engine = new Bloodhound({ + local: sites.toJS ? sites.toJS() : sites, + sorter: sortForSuggestions, + queryTokenizer: tokenizeInput, + datumTokenizer: tokenizeInput, + identify: getSiteIdentity + }) + const promise = engine.initialize() + promise.then(() => { + initialized = true + }) + return promise +} + +const getPartsFromNonUrlInput = (input) => + input.toLowerCase().split(/[,-.\s\\/?&]/) + +const getTagToken = (tag) => '|' + tag + '|' + +const tokenizeInput = (data) => { + let url = data || '' + let parts = [] + + const isSiteObject = typeof data === 'object' && data !== null + if (isSiteObject) { + // When lastAccessTime is 1 it is a default built-in entry which we don't want + // to appear in suggestions. + if (data.lastAccessedTime === 1) { + return [] + } + url = data.location + if (data.title) { + parts = getPartsFromNonUrlInput(data.title) + } + if (data.tags) { + parts = parts.concat(data.tags.map(getTagToken)) + } + } else { + if (lastQueryOptions && !lastQueryOptions.historySuggestionsOn && lastQueryOptions.bookmarkSuggestionsOn) { + parts.push(getTagToken(siteTags.BOOKMARK)) + } + } + + if (url && isUrl(url)) { + const parsedUrl = urlParse(url.toLowerCase()) + // Cache parsed value for latter use when sorting + if (isSiteObject) { + data.parsedUrl = parsedUrl + } + if (parsedUrl.hash) { + parts.push(parsedUrl.hash.slice(1)) + } + if (parsedUrl.host) { + parts = parts.concat(parsedUrl.host.split('.')) + } + if (parsedUrl.pathname) { + parts = parts.concat(parsedUrl.pathname.split(/[.\s\\/]/)) + } + if (parsedUrl.query) { + parts = parts.concat(parsedUrl.query.split(/[&=]/)) + } + if (parsedUrl.protocol) { + parts = parts.concat(parsedUrl.protocol) + } + } else if (url) { + parts = parts.concat(getPartsFromNonUrlInput(url)) + } + return parts.filter(x => !!x) +} + +const add = (data) => { + if (!initialized) { + return + } + if (typeof data === 'string') { + engine.add(data) + } else { + engine.add(data.toJS ? data.toJS() : data) + } +} + +const query = (input, options = {}) => { + if (!initialized) { + return Promise.resolve([]) + } + + return new Promise((resolve, reject) => { + const {getSortForSuggestions} = require('./suggestion') + input = (input || '').toLowerCase() + lastQueryOptions = Object.assign({}, options, { + input, + internalSort: getSortForSuggestions(input) + }) + if (lastQueryOptions.historySuggestionsOn !== false || lastQueryOptions.bookmarkSuggestionsOn !== false) { + engine.search(input, function (results) { + resolve(results) + }, function (err) { + reject(err) + }) + } else { + resolve([]) + } + }) +} + +module.exports = { + init, + add, + tokenizeInput, + query +} diff --git a/app/common/lib/suggestion.js b/app/common/lib/suggestion.js new file mode 100644 index 00000000000..093273750a8 --- /dev/null +++ b/app/common/lib/suggestion.js @@ -0,0 +1,566 @@ +/* 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 urlParse = require('../../common/urlParse') +const appConfig = require('../../../js/constants/appConfig') +const _ = require('underscore') +const Immutable = require('immutable') +const {makeImmutable} = require('../../common/state/immutableUtil') +const {isUrl, aboutUrls, isNavigatableAboutPage, isSourceAboutUrl} = require('../../../js/lib/appUrlUtil') +const suggestionTypes = require('../../../js/constants/suggestionTypes') +const getSetting = require('../../../js/settings').getSetting +const settings = require('../../../js/constants/settings') +const config = require('../../../js/constants/config') +const top500 = require('../../../js/data/top500') +const fetchSearchSuggestions = require('./fetchSearchSuggestions') +const {getFrameByTabId, getTabsByWindowId} = require('../../common/state/tabState') +const {query} = require('./siteSuggestions') +const debounce = require('../../../js/lib/debounce') + +const sigmoid = (t) => { + return 1 / (1 + Math.pow(Math.E, -t)) +} + +const ONE_DAY = 1000 * 60 * 60 * 24 + +/* + * Calculate the sorting priority for a history item based on number of + * accesses and time since last access + * + * @param {number} count - The number of times this site has been accessed + * @param {number} currentTime - Current epoch millisecnds + * @param {boolean} lastAccessedTime - Epoch milliseconds of last access + * + */ +const sortingPriority = (count, currentTime, lastAccessedTime, ageDecayConstant) => { + // number of days since last access (with fractional component) + const ageInDays = (currentTime - (lastAccessedTime || currentTime)) / ONE_DAY + // decay factor based on age + const ageFactor = 1 - ((sigmoid(ageInDays / ageDecayConstant) - 0.5) * 2) + // sorting priority + // console.log(count, ageInDays, ageFactor, count * ageFactor) + return count * ageFactor +} + +/* + * Sort two history items by priority + * + * @param {ImmutableObject} s1 - first history item + * @param {ImmutableObject} s2 - second history item + * + * Return the relative order of two site entries taking into consideration + * the number of times the site has been accessed and the length of time + * since the last access. + * + * The base sort order is determined by the count attribute of the site + * entry. A modifier is then computed based on the length of time since + * the last access. A sigmoid function is used to weight more recent + * entries higher than entries in the past. This is not a linear function, + * entries in the far past with many counts will still be discounted + * heavily as the sigmoid modifier will cancel most of the count + * base parameter. + * + * Below is a sample comparison of two sites that have been accessed + * recently (but not at the identical time). Each site is accessed + * 9 times. The count is discounted by an aging factor calculated + * using the sigmoid decay function. + * + * http://www.gm.ca/gm/ + * + * ageInDays 0.17171469907407408 + * ageFactor 0.9982828546969802 + * count 9 + * priority 0.9982828546969802 + * + * http://www.gm.com/index.html + * + * ageInDays 0.17148791666666666 + * ageFactor 0.9982851225143763 + * count 9 + * priority 0.9982851225143763 + * + */ +const sortByAccessCountWithAgeDecay = (s1, s2) => { + const now = new Date() + const s1Priority = sortingPriority( + s1.count || 0, + now.getTime(), + s1.lastAccessedTime || now.getTime(), + appConfig.urlSuggestions.ageDecayConstant + ) + const s2Priority = sortingPriority( + s2.count || 0, + now.getTime(), + s2.lastAccessedTime || now.getTime(), + appConfig.urlSuggestions.ageDecayConstant + ) + return s2Priority - s1Priority +} + +/* + * Return true1 the url is 'simple' as in without query, search or + * hash components. Return false otherwise. + * + * @param {object} An already normalized simple URL + * + */ +const isSimpleDomainNameValue = (site) => isParsedUrlSimpleDomainNameValue(urlParse(getURL(site))) +const isParsedUrlSimpleDomainNameValue = (parsed) => { + if ((parsed.hash === null || parsed.hash === '#') && + parsed.search === null && parsed.query === null && parsed.pathname === '/') { + return true + } else { + return false + } +} + +/* + * Normalize a location for url suggestion sorting + * + * @param {string} location - history item location + * + */ +const normalizeLocation = (location) => { + if (typeof location === 'string') { + location = location.replace(/www\./, '') + location = location.replace(/^http:\/\//, '') + location = location.replace(/^https:\/\//, '') + } + return location +} + +/* + * Determines based on user input if the location should + * be normalized. If the user is typing http prefix then + * they are specifying something explicitly. + * + * @return true if urls being compared should be normalized + */ +const shouldNormalizeLocation = (input) => { + const prefixes = ['http://', 'https://', 'www.'] + return prefixes.every((prefix) => { + if (input.length > prefix.length) { + return true + } + for (let i = 0; i < Math.min(prefix.length, input.length); i++) { + if (input[i] !== prefix[i]) { + return true + } + } + return false + }) +} + +/* + * return a site representing the simple location for a + * set of related sites without a history item for the + * simple location. + * + * This is used to show a history suggestion for something + * like www.google.com if it has not been visited but + * there are two or more locations with that prefix containing + * path info or parameters + * + * @param {Array[Object]} sites - array of similar sites + */ +var virtualSite = (sites) => { + // array of sites without paths or query params + var simple = sites.filter((parsed) => { + return (parsed.hash === null && parsed.search === null && parsed.query === null && parsed.pathname === '/') + }) + // if there are no simple locations then we will build and return one + if (simple.length === 0) { + // we need to create a virtual history item + return Immutable.Map({ + location: sites[0].protocol + '//' + sites[0].host, + count: 0, + title: sites[0].host, + lastAccessedTime: (new Date()).getTime() + }) + } +} + +/* + * Create an array of simple locations from history + * The simple locations will be the root domain for a location + * without parameters or path + * + * @param {ImmutableList[ImmutableMap]} - history + */ +const createVirtualHistoryItems = (historySites) => { + historySites = makeImmutable(historySites || {}) + + // parse each history item + const parsedHistorySites = [] + historySites.forEach((site) => { + if (site && site.get('location')) { + parsedHistorySites.push( + urlParse(site.get('location')) + ) + } + }) + // group them by host + var grouped = _.groupBy(parsedHistorySites, (parsedSite) => { + return parsedSite.host || 'unknown' + }) + // find groups with more than 2 of the same host + var multiGroupKeys = _.filter(_.keys(grouped), (k) => { + return grouped[k].length > 0 + }) + // potentially create virtual history items + var virtualHistorySites = _.map(multiGroupKeys, (location) => { + return virtualSite(grouped[location]) + }) + virtualHistorySites = _.filter(virtualHistorySites, (site) => { + return !!site + }) + return Immutable.fromJS(_.object(virtualHistorySites.map((site) => { + return [site.get('location'), site] + }))) +} + +/** + * Returns a function that sorts 2 sites by their host. + * The result of that function is a postive, negative, or 0 result. + * 3 or -3 for a strong indicator of a superior result. + * 2 or -2 for a good indicator of a superior result. + * 1 or -1 for a weak indicator of a superior result. + * 0 if no determination can be made. + */ +const getSortByDomain = (userInputLower, userInputHost) => { + return (s1, s2) => { + // Check for matches on hostname which if found overrides + // any count or frequency calculation. + // Note that for parsed URLs that are not complete, the pathname contains + // what the user is entering as the host and the host is null. + const host1 = s1.parsedUrl.host || s1.parsedUrl.pathname + const host2 = s2.parsedUrl.host || s2.parsedUrl.pathname + + let pos1 = host1.indexOf(userInputHost) + let pos2 = host2.indexOf(userInputHost) + if (pos1 !== -1 && pos2 === -1) { + return -3 + } + if (pos1 === -1 && pos2 !== -1) { + return 3 + } + if (pos1 !== -1 && pos2 !== -1) { + // Try to match on the first position without taking into account decay sort. + // This is because autocomplete is based on matching prefixes. + if (pos1 === 0 && pos2 !== 0) { + return -2 + } + if (pos1 !== 0 && pos2 === 0) { + return 2 + } + + // Try the same to see if taking off www. helps. + if (!userInputLower.startsWith('www.')) { + pos1 = host1.indexOf('www.' + userInputLower) + pos2 = host2.indexOf('www.' + userInputLower) + if (pos1 === 0 && pos2 !== 0) { + return -1 + } + if (pos1 !== 0 && pos2 === 0) { + return 1 + } + } + + const sortBySimpleURLResult = sortBySimpleURL(s1, s2) + if (sortBySimpleURLResult !== 0) { + return sortBySimpleURLResult + } + } + // Can't determine what is the best match + return 0 + } +} + +/** + * Sorts 2 URLS by if they are a simple URL or not. + * Returns the normal -1, 1, or 0 result for sort functions. + */ +const sortBySimpleURL = (s1, s2) => { + // If one of the URLs is a simpleURL and the other isn't then sort the simple one first + const url1IsSimple = isParsedUrlSimpleDomainNameValue(s1.parsedUrl) + const url2IsSimple = isParsedUrlSimpleDomainNameValue(s2.parsedUrl) + if (url1IsSimple && !url2IsSimple) { + return -1 + } + if (!url1IsSimple && url2IsSimple) { + return 1 + } + const url1IsSecure = s1.parsedUrl.protocol === 'https:' + const url2IsSecure = s2.parsedUrl.protocol === 'https:' + if (url1IsSimple && url2IsSimple) { + if (url1IsSecure && !url2IsSecure) { + return -1 + } + if (!url1IsSecure && url2IsSecure) { + return 1 + } + } + return 0 +} + +/** + * Returns a function that sorts 2 sites by their host. + * The result of that function is a postive, negative, or 0 result. + */ +const getSortByPath = (userInputLower) => { + return (path1, path2) => { + const pos1 = path1.indexOf(userInputLower) + const pos2 = path2.indexOf(userInputLower) + if (pos1 !== -1 && pos2 === -1) { + return -1 + } + if (pos1 === -1 && pos2 !== -1) { + return 1 + } + // Can't determine what is the best match + return 0 + } +} + +// Same as sortByAccessCountWithAgeDecay but if one is a prefix of the +// other then it is considered always sorted first. +const getSortForSuggestions = (userInputLower) => { + userInputLower = userInputLower.replace(/^http:\/\//, '') + userInputLower = userInputLower.replace(/^https:\/\//, '') + const userInputParts = userInputLower.split('/') + const userInputHost = userInputParts[0] + const userInputValue = userInputParts[1] || '' + const sortByDomain = getSortByDomain(userInputLower, userInputHost) + const sortByPath = getSortByPath(userInputLower) + const {sortByAccessCountWithAgeDecay} = require('./suggestion') + + return (s1, s2) => { + s1.parsedUrl = s1.parsedUrl || urlParse(getURL(s1) || '') + s2.parsedUrl = s2.parsedUrl || urlParse(getURL(s2) || '') + + if (!userInputValue) { + const sortByDomainResult = sortByDomain(s1, s2) + if (sortByDomainResult !== 0) { + return sortByDomainResult + } + } + + const path1 = s1.parsedUrl.host + s1.parsedUrl.path + (s1.parsedUrl.hash || '') + const path2 = s2.parsedUrl.host + s2.parsedUrl.path + (s2.parsedUrl.hash || '') + const sortByPathResult = sortByPath(path1, path2) + if (sortByPathResult !== 0) { + return sortByPathResult + } + + return sortByAccessCountWithAgeDecay(s1, s2) + } +} + +// Currently we sort only sites that are not immutableJS and +const getURL = (x) => { + if (typeof x === 'string') { + return x + } + + if (x.get) { + return x.get('location') || x.get('url') + } + + return x.location || x.url +} + +const getMapListToElements = (urlLocationLower) => ({data, maxResults, type, + sortHandler = (x) => x, filterValue = (site) => { + return site.toLowerCase().indexOf(urlLocationLower) !== -1 + } +}) => { + const suggestionsList = Immutable.List() + const formatTitle = (x) => typeof x === 'object' && x !== null ? x.get('title') : x + const formatTabId = (x) => typeof x === 'object' && x !== null ? x.get('tabId') : x + // Filter out things which are already in our own list at a smaller index + // Filter out things which are already in the suggestions list + let filteredData = data.filter((site) => + suggestionsList.findIndex((x) => (x.location || '').toLowerCase() === (getURL(site) || '').toLowerCase()) === -1 || + // Tab autosuggestions should always be included since they will almost always be in history + type === suggestionTypes.TAB) + // Per suggestion provider filter + if (filterValue) { + filteredData = filteredData.filter(filterValue) + } + + return makeImmutable(filteredData + .sort(sortHandler) + .take(maxResults) + .map((site) => { + return Immutable.fromJS({ + title: formatTitle(site), + location: getURL(site), + tabId: formatTabId(site), + type + }) + })) +} + +const getHistorySuggestions = (state, urlLocationLower) => { + return new Promise((resolve, reject) => { + const sortHandler = getSortForSuggestions(urlLocationLower) + const mapListToElements = getMapListToElements(urlLocationLower) + const options = { + historySuggestionsOn: getSetting(settings.HISTORY_SUGGESTIONS), + bookmarkSuggestionsOn: getSetting(settings.BOOKMARK_SUGGESTIONS) + } + + query(urlLocationLower, options).then((results) => { + results = makeImmutable(results) + results = results.take(config.urlBarSuggestions.maxHistorySites) + results = results.concat(createVirtualHistoryItems(results)) + + const suggestionsList = mapListToElements({ + data: results, + maxResults: config.urlBarSuggestions.maxHistorySites, + type: options.historySuggestionsOn ? suggestionTypes.HISTORY : suggestionTypes.BOOKMARK, + sortHandler, + filterValue: null + }) + resolve(suggestionsList) + }) + }) +} + +const getAboutSuggestions = (state, urlLocationLower) => { + return new Promise((resolve, reject) => { + const mapListToElements = getMapListToElements(urlLocationLower) + const suggestionsList = mapListToElements({ + data: aboutUrls.keySeq().filter((x) => isNavigatableAboutPage(x)), + maxResults: config.urlBarSuggestions.maxAboutPages, + type: suggestionTypes.ABOUT_PAGES + }) + resolve(suggestionsList) + }) +} + +const getOpenedTabSuggestions = (state, windowId, urlLocationLower) => { + return new Promise((resolve, reject) => { + const sortHandler = getSortForSuggestions(urlLocationLower) + const mapListToElements = getMapListToElements(urlLocationLower) + const tabs = getTabsByWindowId(state, windowId) + let suggestionsList = Immutable.List() + if (getSetting(settings.OPENED_TAB_SUGGESTIONS)) { + suggestionsList = mapListToElements({ + data: tabs, + maxResults: config.urlBarSuggestions.maxOpenedFrames, + type: suggestionTypes.TAB, + sortHandler, + filterValue: (tab) => !isSourceAboutUrl(tab.get('url')) && + !tab.get('active') && + ( + (tab.get('title') && tab.get('title').toLowerCase().indexOf(urlLocationLower) !== -1) || + (tab.get('url') && tab.get('url').toLowerCase().indexOf(urlLocationLower) !== -1) + ) + }) + } + resolve(suggestionsList) + }) +} + +const getSearchSuggestions = (state, tabId, urlLocationLower) => { + return new Promise((resolve, reject) => { + const mapListToElements = getMapListToElements(urlLocationLower) + let suggestionsList = Immutable.List() + if (getSetting(settings.OFFER_SEARCH_SUGGESTIONS)) { + const searchResults = state.get('searchResults') + if (searchResults) { + suggestionsList = mapListToElements({ + data: searchResults, + maxResults: config.urlBarSuggestions.maxSearch, + type: suggestionTypes.SEARCH + }) + } + } + resolve(suggestionsList) + }) +} + +const getAlexaSuggestions = (state, urlLocationLower) => { + return new Promise((resolve, reject) => { + const mapListToElements = getMapListToElements(urlLocationLower) + const suggestionsList = mapListToElements({ + data: top500, + maxResults: config.urlBarSuggestions.maxTopSites, + type: suggestionTypes.TOP_SITE + }) + resolve(suggestionsList) + }) +} + +const generateNewSuggestionsList = debounce((state, windowId, tabId, urlLocation) => { + if (!urlLocation) { + return + } + const urlLocationLower = urlLocation.toLowerCase() + Promise.all([ + getHistorySuggestions(state, urlLocationLower), + getAboutSuggestions(state, urlLocationLower), + getOpenedTabSuggestions(state, windowId, urlLocationLower), + getSearchSuggestions(state, tabId, urlLocationLower), + getAlexaSuggestions(state, urlLocationLower) + ]).then(([...suggestionsLists]) => { + const appActions = require('../../../js/actions/appActions') + // Flatten only 1 level deep for perf only, nested will be objects within arrrays + appActions.urlBarSuggestionsChanged(windowId, makeImmutable(suggestionsLists).flatten(1)) + }) +}, 5) + +const generateNewSearchXHRResults = debounce((state, windowId, tabId, input) => { + const frame = getFrameByTabId(state, tabId) + if (!frame) { + // Frame info may not be available yet in app store + return + } + const frameSearchDetail = frame.getIn(['navbar', 'urlbar', 'searchDetail']) + const searchDetail = state.get('searchDetail') + if (!searchDetail && !frameSearchDetail) { + return + } + const autocompleteURL = frameSearchDetail + ? frameSearchDetail.get('autocomplete') + : searchDetail.get('autocompleteURL') + + const shouldDoSearchSuggestions = getSetting(settings.OFFER_SEARCH_SUGGESTIONS) && + autocompleteURL && + !isUrl(input) && + input.length !== 0 + + if (shouldDoSearchSuggestions) { + if (searchDetail) { + const replaceRE = new RegExp('^' + searchDetail.get('shortcut') + ' ', 'g') + input = input.replace(replaceRE, '') + } + fetchSearchSuggestions(windowId, tabId, autocompleteURL, input) + } else { + const appActions = require('../../../js/actions/appActions') + appActions.searchSuggestionResultsAvailable(tabId, undefined, Immutable.List()) + } +}, 10) + +module.exports = { + sortingPriority, + sortByAccessCountWithAgeDecay, + getSortForSuggestions, + getSortByPath, + sortBySimpleURL, + getSortByDomain, + isSimpleDomainNameValue, + normalizeLocation, + shouldNormalizeLocation, + createVirtualHistoryItems, + getMapListToElements, + getHistorySuggestions, + getAboutSuggestions, + getOpenedTabSuggestions, + getSearchSuggestions, + getAlexaSuggestions, + generateNewSuggestionsList, + generateNewSearchXHRResults +} diff --git a/app/common/state/navigationBarState.js b/app/common/state/navigationBarState.js index 248500c7a7a..bfe5c63d1e4 100644 --- a/app/common/state/navigationBarState.js +++ b/app/common/state/navigationBarState.js @@ -81,6 +81,11 @@ const api = { return api.getUrlBar(state, tabId).getIn(['suggestions', 'urlSuffix']) || '' }, + hasSuggestionMatch: (state, tabId) => { + state = validateState(state) + return api.getUrlBar(state, tabId).getIn(['suggestions', 'hasSuggestionMatch']) || false + }, + hasLocationValueSuffix: (state, tabId) => { state = validateState(state) return api.locationValueSuffix(state, tabId).length > 0 diff --git a/app/renderer/components/navigation/urlBar.js b/app/renderer/components/navigation/urlBar.js index 3b113af4c56..d972d8d753d 100644 --- a/app/renderer/components/navigation/urlBar.js +++ b/app/renderer/components/navigation/urlBar.js @@ -37,10 +37,14 @@ const UrlUtil = require('../../../../js/lib/urlutil') const {eventElHasAncestorWithClasses, isForSecondaryAction} = require('../../../../js/lib/eventUtil') const {getBaseUrl, isUrl, isIntermediateAboutPage} = require('../../../../js/lib/appUrlUtil') const {getCurrentWindowId} = require('../../currentWindow') +const {normalizeLocation} = require('../../../common/lib/suggestion') // Icons const iconNoScript = require('../../../../img/url-bar-no-script.svg') +// Stores +const appStoreRenderer = require('../../../../js/stores/appStoreRenderer') + class UrlBar extends React.Component { constructor (props) { super(props) @@ -56,20 +60,26 @@ class UrlBar extends React.Component { this.onContextMenu = this.onContextMenu.bind(this) this.keyPressed = false this.showAutocompleteResult = debounce(() => { - if (this.keyPressed || !this.urlInput || this.props.locationValueSuffix.length === 0) { + if (this.keyPressed || !this.urlInput) { return } - this.updateAutocomplete(this.lastVal, this.props.locationValue + this.props.locationValueSuffix) + this.updateAutocomplete(this.lastVal) }, 10) } + maybeUrlBarTextChanged (value) { + if (value !== this.props.locationValue) { + appActions.urlBarTextChanged(getCurrentWindowId(), this.props.activeTabId, value) + } + } + // restores the url bar to the current location restore () { const location = UrlUtil.getDisplayLocation(this.props.location, getSetting(settings.PDFJS_ENABLED)) if (this.urlInput) { this.setValue(location) } - windowActions.setNavBarUserInput(location) + this.maybeUrlBarTextChanged(location) } /** @@ -94,7 +104,6 @@ class UrlBar extends React.Component { if (this.props.autocompleteEnabled) { windowActions.urlBarAutocompleteEnabled(false) } - windowActions.setUrlBarSuggestions(undefined, null) windowActions.setRenderUrlBarSuggestions(false) } @@ -190,7 +199,14 @@ class UrlBar extends React.Component { this.hideAutoComplete() break case KeyCodes.TAB: - this.hideAutoComplete() + if (this.shouldRenderUrlBarSuggestions) { + if (e.shiftKey) { + windowActions.previousUrlBarSuggestionSelected() + } else { + windowActions.nextUrlBarSuggestionSelected() + } + e.preventDefault() + } break default: this.keyPressed = true @@ -226,9 +242,16 @@ class UrlBar extends React.Component { } } - updateAutocomplete (newValue, suggestion = this.lastVal + this.lastSuffix) { - if (suggestion.startsWith(newValue)) { - const newSuffix = suggestion.substring(newValue.length) + updateAutocomplete (newValue) { + let suggestion = '' + let suggestionNormalized = '' + if (this.props.suggestionList && this.props.suggestionList.size > 0) { + suggestion = this.props.suggestionList.getIn([this.activeIndex || 0, 'location']) || '' + suggestionNormalized = normalizeLocation(suggestion) + } + const newValueNormalized = normalizeLocation(newValue) + if (suggestionNormalized.startsWith(newValueNormalized) && suggestionNormalized.length > 0) { + const newSuffix = suggestionNormalized.substring(newValueNormalized.length) this.setValue(newValue, newSuffix) this.urlInput.setSelectionRange(newValue.length, newValue.length + newSuffix.length + 1) return true @@ -269,8 +292,6 @@ class UrlBar extends React.Component { onChange (e) { if (e.target.value !== this.lastVal + this.lastSuffix) { e.preventDefault() - // clear any current arrow or mouse hover selection - windowActions.setUrlBarSuggestions(undefined, null) this.setValue(e.target.value) } } @@ -292,7 +313,7 @@ class UrlBar extends React.Component { if (!this.keyPress) { // if this is a key press don't sent the update until keyUp so // showAutocompleteResult can handle the result - windowActions.setNavBarUserInput(val) + this.maybeUrlBarTextChanged(val) } } } @@ -301,16 +322,18 @@ class UrlBar extends React.Component { switch (e.keyCode) { case KeyCodes.UP: case KeyCodes.DOWN: + case KeyCodes.TAB: case KeyCodes.ESC: + case KeyCodes.LEFT: + case KeyCodes.SHIFT: + case KeyCodes.RIGHT: return } if (this.props.isSelected) { windowActions.setUrlBarSelected(false) } - // clear any current arrow or mouse hover selection - windowActions.setUrlBarSuggestions(undefined, null) this.keyPressed = false - windowActions.setNavBarUserInput(this.lastVal) + this.maybeUrlBarTextChanged(this.lastVal) } select () { @@ -365,12 +388,11 @@ class UrlBar extends React.Component { if (this.props.isFocused) { this.focus() } - windowActions.setUrlBarSuggestions(undefined, null) windowActions.setRenderUrlBarSuggestions(false) } else if (this.props.location !== prevProps.location) { // This is a url nav change this.setValue(UrlUtil.getDisplayLocation(this.props.location, pdfjsEnabled)) - } else if (this.props.hasLocationValueSuffix && + } else if (this.props.hasSuggestionMatch && this.props.isActive && this.props.locationValueSuffix !== this.lastSuffix) { this.showAutocompleteResult() @@ -463,7 +485,7 @@ class UrlBar extends React.Component { const activateSearchEngine = urlbar.getIn(['searchDetail', 'activateSearchEngine']) const urlbarSearchDetail = urlbar.get('searchDetail') - let searchURL = currentWindow.getIn(['searchDetail', 'searchURL']) + let searchURL = appStoreRenderer.state.getIn(['searchDetail', 'searchURL']) let searchShortcut = '' // remove shortcut from the search terms if (activateSearchEngine && urlbarSearchDetail !== null) { @@ -486,7 +508,7 @@ class UrlBar extends React.Component { props.title = activeFrame.get('title') || '' props.scriptsBlocked = activeFrame.getIn(['noScript', 'blocked']) props.isSecure = activeFrame.getIn(['security', 'isSecure']) - props.hasLocationValueSuffix = urlbar.getIn(['suggestions', 'urlSuffix']) + props.hasSuggestionMatch = urlbar.getIn(['suggestions', 'hasSuggestionMatch']) props.startLoadTime = activeFrame.get('startLoadTime') props.endLoadTime = activeFrame.get('endLoadTime') props.loading = activeFrame.get('loading') @@ -593,7 +615,7 @@ class UrlBar extends React.Component { ? : null } diff --git a/app/renderer/components/navigation/urlBarSuggestionItem.js b/app/renderer/components/navigation/urlBarSuggestionItem.js index 027277a9e65..ed7b9655244 100644 --- a/app/renderer/components/navigation/urlBarSuggestionItem.js +++ b/app/renderer/components/navigation/urlBarSuggestionItem.js @@ -20,21 +20,21 @@ class UrlBarSuggestionItem extends ImmutableComponent { return
  • { this.node = node }} className={cx({ selected: this.props.selected, suggestionItem: true, - [this.props.suggestion.type]: true + [this.props.suggestion.get('type')]: true })}> { - this.props.suggestion.type !== suggestionTypes.TOP_SITE && this.props.suggestion.title - ?
    {this.props.suggestion.title}
    + this.props.suggestion.get('type') !== suggestionTypes.TOP_SITE && this.props.suggestion.get('title') + ?
    {this.props.suggestion.get('title')}
    : null } { - this.props.suggestion.type !== suggestionTypes.SEARCH && this.props.suggestion.type !== suggestionTypes.ABOUT_PAGES - ?
    {this.props.suggestion.location}
    + this.props.suggestion.get('type') !== suggestionTypes.SEARCH && this.props.suggestion.get('type') !== suggestionTypes.ABOUT_PAGES + ?
    {this.props.suggestion.get('location')}
    : null }
  • diff --git a/app/renderer/components/navigation/urlBarSuggestions.js b/app/renderer/components/navigation/urlBarSuggestions.js index 4f06ea0ca7d..0f7e9948ca0 100644 --- a/app/renderer/components/navigation/urlBarSuggestions.js +++ b/app/renderer/components/navigation/urlBarSuggestions.js @@ -7,10 +7,12 @@ const ImmutableComponent = require('../immutableComponent') const UrlBarSuggestionItem = require('./urlBarSuggestionItem') const windowActions = require('../../../../js/actions/windowActions') +const appActions = require('../../../../js/actions/appActions') const suggestionTypes = require('../../../../js/constants/suggestionTypes') const cx = require('../../../../js/lib/classSet') const locale = require('../../../../js/l10n') const {isForSecondaryAction} = require('../../../../js/lib/eventUtil') +const {getCurrentWindowId} = require('../../currentWindow') class UrlBarSuggestions extends ImmutableComponent { constructor () { @@ -27,7 +29,7 @@ class UrlBarSuggestions extends ImmutableComponent { } blur () { - windowActions.setUrlBarSuggestions(null, null) + appActions.urlBarSuggestionsChanged(getCurrentWindowId(), null, null) } onSuggestionClicked (e) { @@ -36,12 +38,12 @@ class UrlBarSuggestions extends ImmutableComponent { render () { const suggestions = this.props.suggestionList - const bookmarkSuggestions = suggestions.filter((s) => s.type === suggestionTypes.BOOKMARK) - const historySuggestions = suggestions.filter((s) => s.type === suggestionTypes.HISTORY) - const aboutPagesSuggestions = suggestions.filter((s) => s.type === suggestionTypes.ABOUT_PAGES) - const tabSuggestions = suggestions.filter((s) => s.type === suggestionTypes.TAB) - const searchSuggestions = suggestions.filter((s) => s.type === suggestionTypes.SEARCH) - const topSiteSuggestions = suggestions.filter((s) => s.type === suggestionTypes.TOP_SITE) + const bookmarkSuggestions = suggestions.filter((s) => s.get('type') === suggestionTypes.BOOKMARK) + const historySuggestions = suggestions.filter((s) => s.get('type') === suggestionTypes.HISTORY) + const aboutPagesSuggestions = suggestions.filter((s) => s.get('type') === suggestionTypes.ABOUT_PAGES) + const tabSuggestions = suggestions.filter((s) => s.get('type') === suggestionTypes.TAB) + const searchSuggestions = suggestions.filter((s) => s.get('type') === suggestionTypes.SEARCH) + const topSiteSuggestions = suggestions.filter((s) => s.get('type') === suggestionTypes.TOP_SITE) let items = [] let index = 0 @@ -63,7 +65,7 @@ class UrlBarSuggestions extends ImmutableComponent { } items = items.concat(suggestions.map((suggestion, i) => { const currentIndex = index + i - const selected = this.activeIndex === currentIndex || (!this.activeIndex && currentIndex === 0 && this.props.hasLocationValueSuffix) + const selected = this.activeIndex === currentIndex || (!this.activeIndex && currentIndex === 0 && this.props.hasSuggestionMatch) return suggestions.size) { newIndex = null } - windowActions.setUrlBarSuggestions(suggestions, newIndex) + appActions.urlBarSuggestionsChanged(getCurrentWindowId(), suggestions, newIndex) } } diff --git a/app/renderer/fetchSearchSuggestions.js b/app/renderer/fetchSearchSuggestions.js deleted file mode 100644 index 13402756dc0..00000000000 --- a/app/renderer/fetchSearchSuggestions.js +++ /dev/null @@ -1,21 +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 windowActions = require('../../js/actions/windowActions') -const debounce = require('../../js/lib/debounce') - -const fetchSearchSuggestions = debounce((tabId, autocompleteURL, searchTerms) => { - const xhr = new window.XMLHttpRequest() - xhr.open('GET', autocompleteURL - .replace('{searchTerms}', encodeURIComponent(searchTerms)), true) - xhr.responseType = 'json' - xhr.send() - xhr.onload = () => { - // Once we have the online suggestions, append them to the others - windowActions.searchSuggestionResultsAvailable(tabId, Immutable.fromJS(xhr.response[1])) - } -}, 100) - -module.exports = fetchSearchSuggestions diff --git a/app/renderer/lib/suggestion.js b/app/renderer/lib/suggestion.js deleted file mode 100644 index fed465eefd4..00000000000 --- a/app/renderer/lib/suggestion.js +++ /dev/null @@ -1,210 +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 urlParse = require('../../common/urlParse') -const appConfig = require('../../../js/constants/appConfig') -const _ = require('underscore') -const Immutable = require('immutable') -const {makeImmutable} = require('../../common/state/immutableUtil') - -const sigmoid = (t) => { - return 1 / (1 + Math.pow(Math.E, -t)) -} - -const ONE_DAY = 1000 * 60 * 60 * 24 - -/* - * Calculate the sorting priority for a history item based on number of - * accesses and time since last access - * - * @param {number} count - The number of times this site has been accessed - * @param {number} currentTime - Current epoch millisecnds - * @param {boolean} lastAccessedTime - Epoch milliseconds of last access - * - */ -module.exports.sortingPriority = (count, currentTime, lastAccessedTime, ageDecayConstant) => { - // number of days since last access (with fractional component) - const ageInDays = (currentTime - (lastAccessedTime || currentTime)) / ONE_DAY - // decay factor based on age - const ageFactor = 1 - ((sigmoid(ageInDays / ageDecayConstant) - 0.5) * 2) - // sorting priority - // console.log(count, ageInDays, ageFactor, count * ageFactor) - return count * ageFactor -} - -/* - * Sort two history items by priority - * - * @param {ImmutableObject} s1 - first history item - * @param {ImmutableObject} s2 - second history item - * - * Return the relative order of two site entries taking into consideration - * the number of times the site has been accessed and the length of time - * since the last access. - * - * The base sort order is determined by the count attribute of the site - * entry. A modifier is then computed based on the length of time since - * the last access. A sigmoid function is used to weight more recent - * entries higher than entries in the past. This is not a linear function, - * entries in the far past with many counts will still be discounted - * heavily as the sigmoid modifier will cancel most of the count - * base parameter. - * - * Below is a sample comparison of two sites that have been accessed - * recently (but not at the identical time). Each site is accessed - * 9 times. The count is discounted by an aging factor calculated - * using the sigmoid decay function. - * - * http://www.gm.ca/gm/ - * - * ageInDays 0.17171469907407408 - * ageFactor 0.9982828546969802 - * count 9 - * priority 0.9982828546969802 - * - * http://www.gm.com/index.html - * - * ageInDays 0.17148791666666666 - * ageFactor 0.9982851225143763 - * count 9 - * priority 0.9982851225143763 - * - */ -module.exports.sortByAccessCountWithAgeDecay = (s1, s2) => { - const now = new Date() - const s1Priority = module.exports.sortingPriority( - s1.get('count') || 0, - now.getTime(), - s1.get('lastAccessedTime') || now.getTime(), - appConfig.urlSuggestions.ageDecayConstant - ) - const s2Priority = module.exports.sortingPriority( - s2.get('count') || 0, - now.getTime(), - s2.get('lastAccessedTime') || now.getTime(), - appConfig.urlSuggestions.ageDecayConstant - ) - return s2Priority - s1Priority -} - -/* - * Return a 1 if the url is 'simple' as in without query, search or - * hash components. Return 0 otherwise. - * - * @param {ImmutableObject} site - object represent history entry - * - */ -module.exports.simpleDomainNameValue = (site) => { - const parsed = urlParse(site.get('location')) - if (parsed.hash === null && parsed.search === null && parsed.query === null && parsed.pathname === '/') { - return 1 - } else { - return 0 - } -} - -/* - * Normalize a location for url suggestion sorting - * - * @param {string} location - history item location - * - */ -module.exports.normalizeLocation = (location) => { - if (typeof location === 'string') { - location = location.replace(/www\./, '') - location = location.replace(/^http:\/\//, '') - location = location.replace(/^https:\/\//, '') - } - return location -} - -/* - * Determines based on user input if the location should - * be normalized. If the user is typing http prefix then - * they are specifying something explicitly. - * - * @return true if urls being compared should be normalized - */ -module.exports.shouldNormalizeLocation = (input) => { - const prefixes = ['http://', 'https://', 'www.'] - return prefixes.every((prefix) => { - if (input.length > prefix.length) { - return true - } - for (let i = 0; i < Math.min(prefix.length, input.length); i++) { - if (input[i] !== prefix[i]) { - return true - } - } - return false - }) -} - -/* - * return a site representing the simple location for a - * set of related sites without a history item for the - * simple location. - * - * This is used to show a history suggestion for something - * like www.google.com if it has not been visited but - * there are two or more locations with that prefix containing - * path info or parameters - * - * @param {Array[Object]} sites - array of similar sites - */ -var virtualSite = (sites) => { - // array of sites without paths or query params - var simple = sites.filter((parsed) => { - return (parsed.hash === null && parsed.search === null && parsed.query === null && parsed.pathname === '/') - }) - // if there are no simple locations then we will build and return one - if (simple.length === 0) { - // we need to create a virtual history item - return Immutable.Map({ - location: sites[0].protocol + '//' + sites[0].host, - count: 0, - title: sites[0].host, - lastAccessedTime: (new Date()).getTime() - }) - } -} - -/* - * Create an array of simple locations from history - * The simple locations will be the root domain for a location - * without parameters or path - * - * @param {ImmutableList[ImmutableMap]} - history - */ -module.exports.createVirtualHistoryItems = (historySites) => { - historySites = makeImmutable(historySites || {}) - - // parse each history item - const parsedHistorySites = [] - historySites.forEach((site) => { - if (site && site.get('location')) { - parsedHistorySites.push( - urlParse(site.get('location')) - ) - } - }) - // group them by host - var grouped = _.groupBy(parsedHistorySites, (parsedSite) => { - return parsedSite.host || 'unknown' - }) - // find groups with more than 2 of the same host - var multiGroupKeys = _.filter(_.keys(grouped), (k) => { - return grouped[k].length > 0 - }) - // potentially create virtual history items - var virtualHistorySites = _.map(multiGroupKeys, (location) => { - return virtualSite(grouped[location]) - }) - virtualHistorySites = _.filter(virtualHistorySites, (site) => { - return !!site - }) - return Immutable.fromJS(_.object(virtualHistorySites.map((site) => { - return [site.location, site] - }))) -} diff --git a/app/renderer/reducers/urlBarReducer.js b/app/renderer/reducers/urlBarReducer.js index 1df5cd82a6b..d1b8b0c5ef1 100644 --- a/app/renderer/reducers/urlBarReducer.js +++ b/app/renderer/reducers/urlBarReducer.js @@ -5,22 +5,13 @@ 'use strict' const windowConstants = require('../../../js/constants/windowConstants') -const {aboutUrls, isNavigatableAboutPage, isSourceAboutUrl, isUrl, getSourceAboutUrl, getSourceMagnetUrl} = require('../../../js/lib/appUrlUtil') +const appConstants = require('../../../js/constants/appConstants') +const {isUrl, getSourceAboutUrl, getSourceMagnetUrl} = require('../../../js/lib/appUrlUtil') const {isURL, isPotentialPhishingUrl, getUrlFromInput} = require('../../../js/lib/urlutil') -const {getFrameByKey, getFrameKeyByTabId, activeFrameStatePath, frameStatePath, getActiveFrame, getFrameByTabId} = require('../../../js/state/frameStateUtil') -const getSetting = require('../../../js/settings').getSetting -const {isBookmark, isDefaultEntry, isHistoryEntry} = require('../../../js/state/siteUtil') -const fetchSearchSuggestions = require('../fetchSearchSuggestions') +const {getFrameByKey, activeFrameStatePath, frameStatePath, getActiveFrame, getFrameByTabId} = require('../../../js/state/frameStateUtil') const searchProviders = require('../../../js/data/searchProviders') -const settings = require('../../../js/constants/settings') const Immutable = require('immutable') -const config = require('../../../js/constants/config') -const top500 = require('../../../js/data/top500') -const suggestion = require('../lib/suggestion') -const suggestionTypes = require('../../../js/constants/suggestionTypes') -const {navigateSiteClickHandler, frameClickHandler} = require('../suggestionClickHandlers') -const appStoreRenderer = require('../../../js/stores/appStoreRenderer') - +const {navigateSiteClickHandler} = require('../suggestionClickHandlers') const navigationBarState = require('../../common/state/navigationBarState') const tabState = require('../../common/state/tabState') @@ -46,36 +37,6 @@ const updateSearchEngineInfoFromInput = (state, frameProps) => { return state } -const searchXHR = (state, frameProps, searchOnline) => { - const searchDetail = state.get('searchDetail') - const frameSearchDetail = frameProps.getIn(['navbar', 'urlbar', 'searchDetail']) - if (!searchDetail && !frameSearchDetail) { - return state - } - let autocompleteURL = frameSearchDetail - ? frameSearchDetail.get('autocomplete') - : searchDetail.get('autocompleteURL') - if (!getSetting(settings.OFFER_SEARCH_SUGGESTIONS) || !autocompleteURL) { - state = state.setIn(activeFrameStatePath(state).concat(['navbar', 'urlbar', 'suggestions', 'searchResults']), Immutable.fromJS([])) - return state - } - - let input = frameProps.getIn(['navbar', 'urlbar', 'location']) - if (!isUrl(input) && input.length > 0) { - if (searchDetail) { - const replaceRE = new RegExp('^' + searchDetail.get('shortcut') + ' ', 'g') - input = input.replace(replaceRE, '') - } - - if (searchOnline) { - fetchSearchSuggestions(frameProps.get('tabId'), autocompleteURL, input) - } - } else { - state = state.setIn(activeFrameStatePath(state).concat(['navbar', 'urlbar', 'suggestions', 'searchResults']), Immutable.fromJS([])) - } - return state -} - const setUrlSuggestions = (state, suggestionList) => { if (suggestionList !== undefined) { state = state.setIn(activeFrameStatePath(state).concat(['navbar', 'urlbar', 'suggestions', 'suggestionList']), suggestionList) @@ -110,218 +71,27 @@ const updateUrlSuffix = (state, suggestionList) => { } const suggestion = suggestionList && suggestionList.get(selectedIndex) let suffix = '' + let hasSuggestionMatch = false if (suggestion) { const autocompleteEnabled = state.getIn(activeFrameStatePath(state).concat(['navbar', 'urlbar', 'suggestions', 'autocompleteEnabled'])) if (autocompleteEnabled) { const location = state.getIn(activeFrameStatePath(state).concat(['navbar', 'urlbar', 'location'])) || '' - const index = suggestion.location.toLowerCase().indexOf(location.toLowerCase()) + const index = suggestion.get('location').toLowerCase().indexOf(location.toLowerCase()) if (index !== -1) { - const beforePrefix = suggestion.location.substring(0, index) + const beforePrefix = suggestion.get('location').substring(0, index) if (beforePrefix.endsWith('://') || beforePrefix.endsWith('://www.') || index === 0) { - suffix = suggestion.location.substring(index + location.length) + suffix = suggestion.get('location').substring(index + location.length) + hasSuggestionMatch = true } } } } state = state.setIn(activeFrameStatePath(state).concat(['navbar', 'urlbar', 'suggestions', 'urlSuffix']), suffix) + state = state.setIn(activeFrameStatePath(state).concat(['navbar', 'urlbar', 'suggestions', 'hasSuggestionMatch']), hasSuggestionMatch) return state } -const generateNewSuggestionsList = (state) => { - const activeFrameKey = state.get('activeFrameKey') - const urlLocation = state.getIn(activeFrameStatePath(state).concat(['navbar', 'urlbar', 'location'])) - const searchResults = state.getIn(activeFrameStatePath(state).concat(['navbar', 'urlbar', 'suggestions', 'searchResults'])) - const frameSearchDetail = state.getIn(activeFrameStatePath(state).concat(['navbar', 'urlbar', 'searchDetail'])) - const searchDetail = state.get('searchDetail') - - if (!urlLocation) { - return state - } - - const urlLocationLower = urlLocation.toLowerCase() - let suggestionsList = new Immutable.List() - const defaultme = (x) => x - const mapListToElements = ({data, maxResults, type, clickHandler = navigateSiteClickHandler.bind(this), - sortHandler = defaultme, formatTitle = defaultme, formatUrl = defaultme, - filterValue = (site) => { - return site.toLowerCase().indexOf(urlLocationLower) !== -1 - } - }) => { - // Filter out things which are already in our own list at a smaller index - // Filter out things which are already in the suggestions list - let filteredData = data.filter((site) => - suggestionsList.findIndex((x) => (x.location || '').toLowerCase() === (formatUrl(site) || '').toLowerCase()) === -1 || - // Tab autosuggestions should always be included since they will almost always be in history - type === suggestionTypes.TAB) - // Per suggestion provider filter - if (filterValue) { - filteredData = filteredData.filter(filterValue) - } - - return filteredData - .sort(sortHandler) - .take(maxResults) - .map((site) => { - return { - onClick: clickHandler.bind(null, site), - title: formatTitle(site), - location: formatUrl(site), - type - } - }) - } - - const shouldNormalize = suggestion.shouldNormalizeLocation(urlLocationLower) - const urlLocationLowerNormalized = suggestion.normalizeLocation(urlLocationLower) - const sortBasedOnLocationPos = (s1, s2) => { - const location1 = shouldNormalize ? suggestion.normalizeLocation(s1.get('location')) : s1.get('location') - const location2 = shouldNormalize ? suggestion.normalizeLocation(s2.get('location')) : s2.get('location') - const pos1 = location1.indexOf(urlLocationLowerNormalized) - const pos2 = location2.indexOf(urlLocationLowerNormalized) - if (pos1 === -1 && pos2 === -1) { - return 0 - } else if (pos1 === -1) { - return 1 - } else if (pos2 === -1) { - return -1 - } else { - if (pos1 - pos2 !== 0) { - return pos1 - pos2 - } else { - // sort site.com higher than site.com/somepath - const sdnv1 = suggestion.simpleDomainNameValue(s1) - const sdnv2 = suggestion.simpleDomainNameValue(s2) - if (sdnv1 !== sdnv2) { - return sdnv2 - sdnv1 - } else { - // If there's a tie on the match location, use the age - // decay modified access count - return suggestion.sortByAccessCountWithAgeDecay(s1, s2) - } - } - } - } - - // NOTE: Iterating sites can take a long time! Please be mindful when - // working with the history and bookmark suggestion code. - const historySuggestionsOn = getSetting(settings.HISTORY_SUGGESTIONS) - const bookmarkSuggestionsOn = getSetting(settings.BOOKMARK_SUGGESTIONS) - const shouldIterateSites = historySuggestionsOn || bookmarkSuggestionsOn - if (shouldIterateSites) { - // Note: Bookmark sites are now included in history. This will allow - // sites to appear in the auto-complete regardless of their bookmark - // status. If history is turned off, bookmarked sites will appear - // in the bookmark section. - const sitesFilter = (site) => { - const location = site.get('location') - if (!location) { - return false - } - const title = site.get('title') - return location.toLowerCase().indexOf(urlLocationLower) !== -1 || - (title && title.toLowerCase().indexOf(urlLocationLower) !== -1) - } - - let historySites = new Immutable.List() - let bookmarkSites = new Immutable.List() - const sites = appStoreRenderer.state.get('sites') - sites.forEach(site => { - if (!sitesFilter(site)) { - return - } - if (historySuggestionsOn && isHistoryEntry(site) && !isDefaultEntry(site)) { - historySites = historySites.push(site) - return - } - if (bookmarkSuggestionsOn && isBookmark(site) && !isDefaultEntry(site)) { - bookmarkSites = bookmarkSites.push(site) - } - }) - - if (historySites.size > 0) { - historySites = historySites.concat(suggestion.createVirtualHistoryItems(historySites)) - - suggestionsList = suggestionsList.concat(mapListToElements({ - data: historySites, - maxResults: config.urlBarSuggestions.maxHistorySites, - type: suggestionTypes.HISTORY, - clickHandler: navigateSiteClickHandler((site) => { - return site.get('location') - }), - sortHandler: sortBasedOnLocationPos, - formatTitle: (site) => site.get('title'), - formatUrl: (site) => site.get('location'), - filterValue: null - })) - } - - if (bookmarkSites.size > 0) { - suggestionsList = suggestionsList.concat(mapListToElements({ - data: bookmarkSites, - maxResults: config.urlBarSuggestions.maxBookmarkSites, - type: suggestionTypes.BOOKMARK, - clickHandler: navigateSiteClickHandler((site) => { - return site.get('location') - }), - sortHandler: sortBasedOnLocationPos, - formatTitle: (site) => site.get('title'), - formatUrl: (site) => site.get('location'), - filterValue: null - })) - } - } - - // about pages - suggestionsList = suggestionsList.concat(mapListToElements({ - data: aboutUrls.keySeq().filter((x) => isNavigatableAboutPage(x)), - maxResults: config.urlBarSuggestions.maxAboutPages, - type: suggestionTypes.ABOUT_PAGES, - clickHandler: navigateSiteClickHandler((x) => x)})) - - // opened frames - if (getSetting(settings.OPENED_TAB_SUGGESTIONS)) { - suggestionsList = suggestionsList.concat(mapListToElements({ - data: state.get('frames'), - maxResults: config.urlBarSuggestions.maxOpenedFrames, - type: suggestionTypes.TAB, - clickHandler: frameClickHandler, - sortHandler: sortBasedOnLocationPos, - formatTitle: (frame) => frame.get('title'), - formatUrl: (frame) => frame.get('location'), - filterValue: (frame) => !isSourceAboutUrl(frame.get('location')) && - frame.get('key') !== activeFrameKey && - ( - (frame.get('title') && frame.get('title').toLowerCase().indexOf(urlLocationLower) !== -1) || - (frame.get('location') && frame.get('location').toLowerCase().indexOf(urlLocationLower) !== -1) - ) - })) - } - - // Search suggestions - if (getSetting(settings.OFFER_SEARCH_SUGGESTIONS) && searchResults) { - suggestionsList = suggestionsList.concat(mapListToElements({ - data: searchResults, - maxResults: config.urlBarSuggestions.maxSearch, - type: suggestionTypes.SEARCH, - clickHandler: navigateSiteClickHandler((searchTerms) => { - let searchURL = frameSearchDetail - ? frameSearchDetail.get('search') : searchDetail.get('searchURL') - return searchURL.replace('{searchTerms}', encodeURIComponent(searchTerms)) - }) - })) - } - - // Alexa top 500 - suggestionsList = suggestionsList.concat(mapListToElements({ - data: top500, - maxResults: config.urlBarSuggestions.maxTopSites, - type: suggestionTypes.TOP_SITE, - clickHandler: navigateSiteClickHandler((x) => x)})) - - return setUrlSuggestions(state, suggestionsList) -} - const getLocation = (location) => { location = location.trim() location = getSourceAboutUrl(location) || @@ -363,9 +133,6 @@ const setNavBarUserInput = (state, location) => { state = updateNavBarInput(state, location) const activeFrameProps = getActiveFrame(state) state = updateSearchEngineInfoFromInput(state, activeFrameProps) - state = searchXHR(state, activeFrameProps, true) - state = generateNewSuggestionsList(state) - state = updateUrlSuffix(state, state.getIn(activeFrameStatePath(state).concat(['navbar', 'urlbar', 'suggestions', 'suggestionList']), Immutable.Map())) if (!location) { state = setRenderUrlBarSuggestions(state, false) } @@ -386,10 +153,15 @@ const setActive = (state, isActive) => { const urlBarReducer = (state, action) => { const tabId = state.getIn(activeFrameStatePath(state).concat(['tabId']), tabState.TAB_ID_NONE) - switch (action.actionType) { - case windowConstants.WINDOW_SET_NAVBAR_INPUT: - state = setNavBarUserInput(state, action.location) + case appConstants.APP_URL_BAR_TEXT_CHANGED: + state = setNavBarUserInput(state, action.input) + break + case appConstants.APP_URL_BAR_SUGGESTIONS_CHANGED: + if (action.selectedIndex !== undefined) { + state = state.setIn(activeFrameStatePath(state).concat(['navbar', 'urlbar', 'suggestions', 'selectedIndex']), action.selectedIndex) + } + state = setUrlSuggestions(state, action.suggestionList) break case windowConstants.WINDOW_SET_NAVIGATED: // For about: URLs, make sure we store the URL as about:something @@ -469,8 +241,8 @@ const urlBarReducer = (state, action) => { const selectedIndexPath = activeFrameStatePath(state).concat(['navbar', 'urlbar', 'suggestions', 'selectedIndex']) const suggestionList = state.getIn(activeFrameStatePath(state).concat(['navbar', 'urlbar', 'suggestions', 'suggestionList'])) const selectedIndex = state.getIn(selectedIndexPath) - const lastSuffix = state.getIn(activeFrameStatePath(state).concat(['navbar', 'urlbar', 'suggestions', 'urlSuffix'])) - if (!selectedIndex && selectedIndex !== 0 && !lastSuffix) { + const hasSuggestionMatch = state.getIn(activeFrameStatePath(state).concat(['navbar', 'urlbar', 'suggestions', 'hasSuggestionMatch'])) + if (!selectedIndex && selectedIndex !== 0 && !hasSuggestionMatch) { state = state.setIn(selectedIndexPath, 0) } else if (selectedIndex > 0) { state = state.setIn(selectedIndexPath, selectedIndex - 1) @@ -484,8 +256,8 @@ const urlBarReducer = (state, action) => { const selectedIndexPath = activeFrameStatePath(state).concat(['navbar', 'urlbar', 'suggestions', 'selectedIndex']) const suggestionList = state.getIn(activeFrameStatePath(state).concat(['navbar', 'urlbar', 'suggestions', 'suggestionList'])) const selectedIndex = state.getIn(selectedIndexPath) - const lastSuffix = state.getIn(activeFrameStatePath(state).concat(['navbar', 'urlbar', 'suggestions', 'urlSuffix'])) - if (!selectedIndex && selectedIndex !== 0 && !lastSuffix) { + const hasSuggestionMatch = state.getIn(activeFrameStatePath(state).concat(['navbar', 'urlbar', 'suggestions', 'hasSuggestionMatch'])) + if (!selectedIndex && selectedIndex !== 0 && !hasSuggestionMatch) { state = state.setIn(selectedIndexPath, 0) } else if (selectedIndex < suggestionList.size - 1) { state = state.setIn(selectedIndexPath, selectedIndex + 1) @@ -495,18 +267,9 @@ const urlBarReducer = (state, action) => { state = updateUrlSuffix(state, state.getIn(activeFrameStatePath(state).concat(['navbar', 'urlbar', 'suggestions', 'suggestionList']), suggestionList)) break } - case windowConstants.WINDOW_SEARCH_SUGGESTION_RESULTS_AVAILABLE: - const frameKey = getFrameKeyByTabId(state, action.tabId) - state = state.setIn(frameStatePath(state, frameKey).concat(['navbar', 'urlbar', 'suggestions', 'searchResults']), action.searchResults) - state = generateNewSuggestionsList(state) - break case windowConstants.WINDOW_URL_BAR_AUTOCOMPLETE_ENABLED: state = state.setIn(activeFrameStatePath(state).concat(['navbar', 'urlbar', 'suggestions', 'autocompleteEnabled']), action.enabled) break - case windowConstants.WINDOW_SET_URL_BAR_SUGGESTIONS: - state = state.setIn(activeFrameStatePath(state).concat(['navbar', 'urlbar', 'suggestions', 'selectedIndex']), action.selectedIndex) - state = setUrlSuggestions(state, action.suggestionList) - break case windowConstants.WINDOW_SET_URL_BAR_ACTIVE: state = setActive(state, action.isActive) break @@ -518,8 +281,10 @@ const urlBarReducer = (state, action) => { const suggestionList = state.getIn(activeFrameStatePath(state).concat(['navbar', 'urlbar', 'suggestions', 'suggestionList'])) if (suggestionList.size > 0) { // It's important this doesn't run sync or else the returned state below will overwrite anything done in the click handler - setImmediate(() => - suggestionList.get(selectedIndex).onClick(action.isForSecondaryAction, action.shiftKey)) + setImmediate(() => { + const suggestion = suggestionList.get(selectedIndex) + navigateSiteClickHandler(suggestion, action.isForSecondaryAction, action.shiftKey) + }) } break } diff --git a/app/renderer/suggestionClickHandlers.js b/app/renderer/suggestionClickHandlers.js index 72c41b6038e..eff83d0af23 100644 --- a/app/renderer/suggestionClickHandlers.js +++ b/app/renderer/suggestionClickHandlers.js @@ -7,16 +7,39 @@ const appActions = require('../../js/actions/appActions') const windowActions = require('../../js/actions/windowActions') const windowStore = require('../../js/stores/windowStore') +const appStoreRenderer = require('../../js/stores/appStoreRenderer') +const suggestionTypes = require('../../js/constants/suggestionTypes') +const {makeImmutable} = require('../common/state/immutableUtil') const {getActiveFrame} = require('../../js/state/frameStateUtil') -const navigateSiteClickHandler = (formatUrl) => (site, isForSecondaryAction, shiftKey) => { - const url = formatUrl(site) +const navigateSiteClickHandler = (suggestionData, isForSecondaryAction, shiftKey) => { // When clicked make sure to hide autocomplete windowActions.setRenderUrlBarSuggestions(false) + + suggestionData = makeImmutable(suggestionData) + const type = suggestionData.get('type') + let partitionNumber + let url + if (type === suggestionTypes.SEARCH) { + const frameSearchDetail = suggestionData.getIn(['navbar', 'urlbar', 'searchDetail']) + const searchDetail = appStoreRenderer.state.get('searchDetail') + let searchURL = frameSearchDetail + ? frameSearchDetail.get('search') : searchDetail.get('searchURL') + url = searchURL.replace('{searchTerms}', encodeURIComponent(suggestionData.get('location'))) + } else if (type === suggestionTypes.TAB) { + appActions.tabActivateRequested(suggestionData.get('tabId')) + return + } else if (type === suggestionTypes.TOP_SITE) { + url = suggestionData.get('location') + } else { + url = suggestionData.get('location') + partitionNumber = (suggestionData && suggestionData.get && suggestionData.get('partitionNumber')) || undefined + } + if (isForSecondaryAction) { appActions.createTabRequested({ url, - partitionNumber: (site && site.get && site.get('partitionNumber')) || undefined, + partitionNumber, active: !!shiftKey }) } else { @@ -26,10 +49,6 @@ const navigateSiteClickHandler = (formatUrl) => (site, isForSecondaryAction, shi } } -const frameClickHandler = (frameProps) => - appActions.tabActivateRequested(frameProps.get('tabId')) - module.exports = { - navigateSiteClickHandler, - frameClickHandler + navigateSiteClickHandler } diff --git a/app/sessionStore.js b/app/sessionStore.js index 71f6f8360a5..5deb3aee881 100644 --- a/app/sessionStore.js +++ b/app/sessionStore.js @@ -658,7 +658,8 @@ module.exports.defaultAppState = () => { httpsEverywhere: { count: 0 }, - defaultWindowParams: {} + defaultWindowParams: {}, + searchDetail: null } } diff --git a/docs/appActions.md b/docs/appActions.md index fbe64eda399..f5f8fa5bd23 100644 --- a/docs/appActions.md +++ b/docs/appActions.md @@ -1023,6 +1023,18 @@ Notifies autoplay has been blocked +### savePassword() + +Handle 'save-password' event from muon + + + +### updatePassword() + +Handle 'update-password' event from muon + + + ### deletePassword(passwordDetail) Deletes login credentials @@ -1045,6 +1057,57 @@ Delete legacy "never saved password" list +### urlBarTextChanged(windowId, tabId, input) + +Indicates that the urlbar text has changed, usually from user input + +**Parameters** + +**windowId**: `number`, The window ID the text is being changed inside of + +**tabId**: `number`, The tab ID the text is being changed inside of + +**input**: `string`, The text that was entered into the URL bar + + + +### searchSuggestionResultsAvailable(tabId, searchResults) + +New URL bar suggestion search results are available. +This is typically from a service like Duck Duck Go auto complete for the portion of text that the user typed in. + +**Parameters** + +**tabId**: `number`, the tab id for the action + +**searchResults**: , The search results for the currently entered URL bar text. + + + +### urlBarSuggestionsChanged(windowId, suggestionList, selectedIndex) + +Indicates URL bar suggestions and selected index. + +**Parameters** + +**windowId**: `number`, the window ID + +**suggestionList**: `Array.<Object>`, The list of suggestions for the entered URL bar text. This can be generated from history, bookmarks, etc. + +**selectedIndex**: `number`, The index for the selected item (users can select items with down arrow on their keyboard) + + + +### defaultSearchEngineLoaded(searchDetail) + +Dispatches a message to set the search engine details. + +**Parameters** + +**searchDetail**: `Object`, the search details + + + * * * diff --git a/docs/state.md b/docs/state.md index 5077232623e..9c4307bab9a 100644 --- a/docs/state.md +++ b/docs/state.md @@ -357,7 +357,11 @@ AppStore width: number, // session properties windowId: number // the muon id for the window - }] + }], + searchDetail: { + autocompleteURL: string, // ditto re: {searchTerms} + searchURL: string // with replacement var in string: {searchTerms} + }, } ``` @@ -465,7 +469,6 @@ WindowStore selected: boolean, // is the urlbar text selected suggestions: { autocompleteEnabled: boolean, // used to enable or disable autocomplete - searchResults: array, // autocomplete server results if enabled selectedIndex: number, // index of the item in focus shouldRender: boolean, // if the suggestions should render suggestionList: { @@ -624,10 +627,7 @@ WindowStore minPublisherVisits: number // e.g., 0 } }, - searchDetail: { - autocompleteURL: string, // ditto re: {searchTerms} - searchURL: string // with replacement var in string: {searchTerms} - }, + searchResults: array, // autocomplete server results if enabled ui: { bookmarksToolbar: { selectedFolderId: number // folderId from the siteDetail of the currently expanded folder diff --git a/docs/windowActions.md b/docs/windowActions.md index b7dc3227e48..b55e4f5ea9b 100644 --- a/docs/windowActions.md +++ b/docs/windowActions.md @@ -98,17 +98,6 @@ Dispatches a message to set the frame error state -### setNavBarUserInput(location) - -Dispatches a message to the store to set the user entered text for the URL bar. -Unlike setLocation and loadUrl, this does not modify the state of src and location. - -**Parameters** - -**location**: `string`, The text to set as the new navbar URL input - - - ### setFindbarShown(frameKey, shown) Shows/hides the find-in-page bar. @@ -291,18 +280,6 @@ Dispatches a message to the store to indicate that the specified frame should mo -### setUrlBarSuggestions(suggestionList, selectedIndex) - -Sets the URL bar suggestions and selected index. - -**Parameters** - -**suggestionList**: `Array.<Object>`, The list of suggestions for the entered URL bar text. This can be generated from history, bookmarks, etc. - -**selectedIndex**: `number`, The index for the selected item (users can select items with down arrow on their keyboard) - - - ### activeSuggestionClicked(isForSecondaryAction, shiftKey) The active URL bar suggestion was clicked @@ -342,19 +319,6 @@ This is sometimes only temporarily disabled, e.g. a user is pressing backspace. -### searchSuggestionResultsAvailable(tabId, searchResults) - -New URL bar suggestion search results are available. -This is typically from a service like Duck Duck Go auto complete for the portion of text that the user typed in. - -**Parameters** - -**tabId**: `number`, the tab id for the action - -**searchResults**: , The search results for the currently entered URL bar text. - - - ### setUrlBarSelected(isSelected) Marks the URL bar text as selected or not @@ -393,16 +357,6 @@ set from an IPC call. -### setSearchDetail(searchDetail) - -Dispatches a message to set the search engine details. - -**Parameters** - -**searchDetail**: `Object`, the search details - - - ### setFindDetail(frameKey, findDetail) Dispatches a message to set the find-in-page details. diff --git a/js/actions/appActions.js b/js/actions/appActions.js index 702047dc026..361a60ab69c 100644 --- a/js/actions/appActions.js +++ b/js/actions/appActions.js @@ -3,7 +3,7 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ 'use strict' -const AppDispatcher = require('../dispatcher/appDispatcher') +const {dispatch} = require('../dispatcher/appDispatcher') const appConstants = require('../constants/appConstants') const appActions = { @@ -14,7 +14,7 @@ const appActions = { * @param {object} appState - Initial app state object (not yet converted to ImmutableJS) */ setState: function (appState) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_SET_STATE, appState }) @@ -28,7 +28,7 @@ const appActions = { * @param {function} cb - Callback to call after the window is loaded, will only work if called from the main process. */ newWindow: function (frameOpts, browserOpts, restoredState, cb) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_NEW_WINDOW, frameOpts, browserOpts, @@ -38,35 +38,35 @@ const appActions = { }, windowReady: function (windowId) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_WINDOW_READY, windowId }) }, closeWindow: function (windowId) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_CLOSE_WINDOW, windowId }) }, windowClosed: function (windowValue) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_WINDOW_CLOSED, windowValue }) }, windowCreated: function (windowValue) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_WINDOW_CREATED, windowValue }) }, windowUpdated: function (windowValue) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_WINDOW_UPDATED, windowValue }) @@ -77,7 +77,7 @@ const appActions = { * @param {Object} frame */ frameChanged: function (frame) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_FRAME_CHANGED, frame }) @@ -88,7 +88,7 @@ const appActions = { * @param {Object} tabValue */ tabCreated: function (tabValue) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_TAB_CREATED, tabValue }) @@ -102,7 +102,7 @@ const appActions = { * @param {Number} windowId */ tabMoved: function (tabId, frameOpts, browserOpts, windowId) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_TAB_MOVED, tabId, frameOpts, @@ -116,7 +116,7 @@ const appActions = { * @param {Object} createProperties */ createTabRequested: function (createProperties, activateIfOpen = false, isRestore = false) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_CREATE_TAB_REQUESTED, createProperties, activateIfOpen, @@ -130,7 +130,7 @@ const appActions = { * @param {string} url - The url to load */ loadURLRequested: function (tabId, url) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_LOAD_URL_REQUESTED, tabId, url @@ -143,7 +143,7 @@ const appActions = { * @param {string} url - The url to load */ loadURLInActiveTabRequested: function (windowId, url) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_LOAD_URL_IN_ACTIVE_TAB_REQUESTED, windowId, url @@ -156,7 +156,7 @@ const appActions = { * @param {string} shareType - The type of share to do, must be one of: "email", "facebook", "pinterest", "twitter", "googlePlus", "linkedIn", "buffer", "reddit", or "digg" */ simpleShareActiveTabRequested: function (windowId, shareType) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_SIMPLE_SHARE_ACTIVE_TAB_REQUESTED, windowId, shareType @@ -169,7 +169,7 @@ const appActions = { * @param {Object} changeInfo from chrome-tabs-updated */ tabUpdated: function (tabValue, changeInfo) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_TAB_UPDATED, tabValue, changeInfo, @@ -185,7 +185,7 @@ const appActions = { * @param {Number} tabId - the tabId to activate */ tabActivateRequested: function (tabId) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_TAB_ACTIVATE_REQUESTED, tabId }) @@ -198,7 +198,7 @@ const appActions = { * @param {Number} index - the new index */ tabIndexChanged: function (tabId, index) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_TAB_INDEX_CHANGED, tabId, index @@ -212,7 +212,7 @@ const appActions = { * @param {Boolean} forceClosePinned - force close if pinned */ tabCloseRequested: function (tabId, forceClosePinned = false) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_TAB_CLOSE_REQUESTED, tabId, forceClosePinned @@ -224,7 +224,7 @@ const appActions = { * @param {number} tabId */ tabClosed: function (tabId, windowId) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_TAB_CLOSED, tabId, queryInfo: { @@ -243,7 +243,7 @@ const appActions = { * @param {boolean} skipSync - Set true if a site isn't eligible for Sync (e.g. if addSite was triggered by Sync) */ addSite: function (siteDetail, tag, originalSiteDetail, destinationDetail, skipSync) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_ADD_SITE, siteDetail, tag, @@ -260,7 +260,7 @@ const appActions = { * @param {boolean} skipSync - Set true if a site isn't eligible for Sync (e.g. if this removal was triggered by Sync) */ removeSite: function (siteDetail, tag, skipSync) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_REMOVE_SITE, siteDetail, tag, @@ -278,7 +278,7 @@ const appActions = { * @param {boolean} destinationIsParent - Whether or not the destinationDetail should be considered the new parent. */ moveSite: function (sourceKey, destinationKey, prepend, destinationIsParent) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_MOVE_SITE, sourceKey, destinationKey, @@ -294,7 +294,7 @@ const appActions = { * @param {Object} downloadDetail - Properties for the download */ mergeDownloadDetail: function (downloadId, downloadDetail) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_MERGE_DOWNLOAD_DETAIL, downloadId, downloadDetail @@ -305,7 +305,7 @@ const appActions = { * Dispatches a message to clear all completed downloads */ clearCompletedDownloads: function () { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_CLEAR_COMPLETED_DOWNLOADS }) }, @@ -314,7 +314,7 @@ const appActions = { * Dispatches a message indicating ledger recovery succeeded */ ledgerRecoverySucceeded: function () { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_LEDGER_RECOVERY_STATUS_CHANGED, recoverySucceeded: true }) @@ -324,7 +324,7 @@ const appActions = { * Dispatches a message indicating ledger recovery failed */ ledgerRecoveryFailed: function () { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_LEDGER_RECOVERY_STATUS_CHANGED, recoverySucceeded: false }) @@ -336,7 +336,7 @@ const appActions = { * @param {Array} position - [x, y] */ defaultWindowParamsChanged: function (size, position) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_DEFAULT_WINDOW_PARAMS_CHANGED, size, position @@ -351,7 +351,7 @@ const appActions = { * @param {string} etag - The etag of the reosurce from the http response */ setResourceETag: function (resourceName, etag) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_SET_DATA_FILE_ETAG, resourceName, etag @@ -364,7 +364,7 @@ const appActions = { * @param {number} lastCheck - The last check date of the reosurce from the http response */ setResourceLastCheck: function (resourceName, lastCheckVersion, lastCheckDate) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_SET_DATA_FILE_LAST_CHECK, resourceName, lastCheckVersion, @@ -378,7 +378,7 @@ const appActions = { * @param {boolean} enabled - true if the resource is enabled. */ setResourceEnabled: function (resourceName, enabled) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_SET_RESOURCE_ENABLED, resourceName, enabled @@ -390,7 +390,7 @@ const appActions = { * @param {string} resourceName - 'widevine' */ resourceReady: function (resourceName) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_RESOURCE_READY, resourceName }) @@ -402,7 +402,7 @@ const appActions = { * @param {number} count - number of blocked resources to add to the global count */ addResourceCount: function (resourceName, count) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_ADD_RESOURCE_COUNT, resourceName, count @@ -414,7 +414,7 @@ const appActions = { * epoch timestamp (milliseconds) */ setUpdateLastCheck: function () { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_UPDATE_LAST_CHECK }) }, @@ -426,7 +426,7 @@ const appActions = { * @param {object} metadata - Metadata from the pdate server, with info like release notes. */ setUpdateStatus: function (status, verbose, metadata) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_SET_UPDATE_STATUS, status, verbose, @@ -440,7 +440,7 @@ const appActions = { * @param {string} value - The value of the setting */ changeSetting: function (key, value) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_CHANGE_SETTING, key, value @@ -457,7 +457,7 @@ const appActions = { * @param {boolean} skipSync - Set true if a site isn't eligible for Sync (e.g. if addSite was triggered by Sync) */ changeSiteSetting: function (hostPattern, key, value, temp, skipSync) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_CHANGE_SITE_SETTING, hostPattern, key, @@ -476,7 +476,7 @@ const appActions = { * @param {boolean} skipSync - Set true if a site isn't eligible for Sync (e.g. if addSite was triggered by Sync) */ removeSiteSetting: function (hostPattern, key, temp, skipSync) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_REMOVE_SITE_SETTING, hostPattern, key, @@ -490,7 +490,7 @@ const appActions = { * @param {object} ledgerInfo - the current ledger state */ updateLedgerInfo: function (ledgerInfo) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_UPDATE_LEDGER_INFO, ledgerInfo }) @@ -501,7 +501,7 @@ const appActions = { * @param {object} locationInfo - the current location synopsis */ updateLocationInfo: function (locationInfo) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_UPDATE_LOCATION_INFO, locationInfo }) @@ -512,7 +512,7 @@ const appActions = { * @param {object} publisherInfo - the current publisher synopsis */ updatePublisherInfo: function (publisherInfo) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_UPDATE_PUBLISHER_INFO, publisherInfo }) @@ -523,7 +523,7 @@ const appActions = { * @param {{message: string, buttons: Array., frameOrigin: string, options: Object}} detail */ showNotification: function (detail) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_SHOW_NOTIFICATION, detail }) @@ -534,7 +534,7 @@ const appActions = { * @param {string} message */ hideNotification: function (message) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_HIDE_NOTIFICATION, message }) @@ -546,7 +546,7 @@ const appActions = { * @param {boolean} learn - true if the word should be learned, false if ignored */ addWord: function (word, learn) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_ADD_WORD, word, learn @@ -558,7 +558,7 @@ const appActions = { * @param {string} locale - The locale to set for the dictionary */ setDictionary: function (locale) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_SET_DICTIONARY, locale }) @@ -570,7 +570,7 @@ const appActions = { * @param {string} detail - login request info */ setLoginRequiredDetail: function (tabId, detail) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_SET_LOGIN_REQUIRED_DETAIL, tabId, detail @@ -578,7 +578,7 @@ const appActions = { }, setLoginResponseDetail: function (tabId, detail) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_SET_LOGIN_RESPONSE_DETAIL, tabId, detail @@ -590,7 +590,7 @@ const appActions = { * @param {object} clearDataDetail - the app data to clear as per doc/state.md's clearBrowsingDataDefaults */ onClearBrowsingData: function (clearDataDetail) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_ON_CLEAR_BROWSING_DATA, clearDataDetail }) @@ -601,7 +601,7 @@ const appActions = { * @param {object} selected - the browser data to import as per doc/state.md's importBrowserDataSelected */ importBrowserData: function (selected) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_IMPORT_BROWSER_DATA, selected }) @@ -613,7 +613,7 @@ const appActions = { * @param {object} originalDetail - the original address before editing */ addAutofillAddress: function (detail, originalDetail) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_ADD_AUTOFILL_ADDRESS, detail, originalDetail @@ -625,7 +625,7 @@ const appActions = { * @param {object} detail - the address to remove as per doc/state.md's autofillAddressDetail */ removeAutofillAddress: function (detail) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_REMOVE_AUTOFILL_ADDRESS, detail }) @@ -637,7 +637,7 @@ const appActions = { * @param {object} originalDetail - the original credit card before editing */ addAutofillCreditCard: function (detail, originalDetail) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_ADD_AUTOFILL_CREDIT_CARD, detail, originalDetail @@ -649,7 +649,7 @@ const appActions = { * @param {object} detail - the credit card to remove as per doc/state.md's autofillCreditCardDetail */ removeAutofillCreditCard: function (detail) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_REMOVE_AUTOFILL_CREDIT_CARD, detail }) @@ -661,7 +661,7 @@ const appActions = { * @param {Array} creditCardGuids - the guid array to access credit card entries in autofill DB */ autofillDataChanged: function (addressGuids, creditCardGuids) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_AUTOFILL_DATA_CHANGED, addressGuids, creditCardGuids @@ -674,7 +674,7 @@ const appActions = { * @param {Number} windowId - the unique id of the window */ windowBlurred: function (windowId) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_WINDOW_BLURRED, windowId: windowId }) @@ -686,7 +686,7 @@ const appActions = { * @param {Number} windowId - the unique id of the window */ windowFocused: function (windowId) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_WINDOW_FOCUSED, windowId: windowId }) @@ -697,7 +697,7 @@ const appActions = { * @param {Object} menubarTemplate - JSON used to build the menu */ setMenubarTemplate: function (menubarTemplate) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_SET_MENUBAR_TEMPLATE, menubarTemplate }) @@ -708,7 +708,7 @@ const appActions = { * after being disconnected */ networkConnected: function () { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_NETWORK_CONNECTED }) }, @@ -717,7 +717,7 @@ const appActions = { * Dispatches a message when the network is disconnected */ networkDisconnected: function () { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_NETWORK_DISCONNECTED }) }, @@ -728,7 +728,7 @@ const appActions = { * @param {boolean} useBrave - whether set Brave as default browser */ defaultBrowserUpdated: function (useBrave) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_DEFAULT_BROWSER_UPDATED, useBrave }) @@ -738,7 +738,7 @@ const appActions = { * Dispatch a message to indicate default browser check is complete */ defaultBrowserCheckComplete: function () { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_DEFAULT_BROWSER_CHECK_COMPLETE }) }, @@ -747,13 +747,13 @@ const appActions = { * Notify the AppStore to provide default history values. */ populateHistory: function () { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_POPULATE_HISTORY }) }, allowFlashOnce: function (tabId, url, isPrivate) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_ALLOW_FLASH_ONCE, tabId, url, @@ -762,7 +762,7 @@ const appActions = { }, allowFlashAlways: function (tabId, url) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_ALLOW_FLASH_ALWAYS, tabId, url @@ -773,7 +773,7 @@ const appActions = { * Dispatch a message to copy data URL to clipboard **/ dataURLCopied: function (dataURL, html, text) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_DATA_URL_COPIED, dataURL, html, @@ -785,7 +785,7 @@ const appActions = { * Dispatches a message when the app is shutting down. */ shuttingDown: function () { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_SHUTTING_DOWN }) }, @@ -796,7 +796,7 @@ const appActions = { * @param {string} downloadId - ID of the download being revealed */ downloadRevealed: function (downloadId) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_DOWNLOAD_REVEALED, downloadId }) @@ -807,7 +807,7 @@ const appActions = { * @param {string} downloadId - ID of the download being opened */ downloadOpened: function (downloadId) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_DOWNLOAD_OPENED, downloadId }) @@ -819,7 +819,7 @@ const appActions = { * @param {string} downloadAction - the action to perform from constants/electronDownloadItemActions.js */ downloadActionPerformed: function (downloadId, downloadAction) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_DOWNLOAD_ACTION_PERFORMED, downloadId, downloadAction @@ -831,7 +831,7 @@ const appActions = { * @param {string} downloadId - ID of the download item being copied to the clipboard */ downloadCopiedToClipboard: function (downloadId) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_DOWNLOAD_COPIED_TO_CLIPBOARD, downloadId }) @@ -842,7 +842,7 @@ const appActions = { * @param {string} downloadId - ID of the download item being deleted */ downloadDeleted: function (downloadId) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_DOWNLOAD_DELETED, downloadId }) @@ -853,7 +853,7 @@ const appActions = { * @param {string} downloadId - ID of the download item being cleared */ downloadCleared: function (downloadId) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_DOWNLOAD_CLEARED, downloadId }) @@ -864,7 +864,7 @@ const appActions = { * @param {string} downloadId - ID of the download item being redownloaded */ downloadRedownloaded: function (downloadId) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_DOWNLOAD_REDOWNLOADED, downloadId }) @@ -874,7 +874,7 @@ const appActions = { * Shows delete confirmation bar in download item panel */ showDownloadDeleteConfirmation: function () { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_SHOW_DOWNLOAD_DELETE_CONFIRMATION }) }, @@ -883,7 +883,7 @@ const appActions = { * Hides delete confirmation bar in download item panel */ hideDownloadDeleteConfirmation: function () { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_HIDE_DOWNLOAD_DELETE_CONFIRMATION }) }, @@ -893,7 +893,7 @@ const appActions = { * @param {string} text - clipboard text which is copied */ clipboardTextCopied: function (text) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_CLIPBOARD_TEXT_UPDATED, text }) @@ -904,7 +904,7 @@ const appActions = { * @param {number} tabId - The tabId */ toggleDevTools: function (tabId) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_TAB_TOGGLE_DEV_TOOLS, tabId }) @@ -916,7 +916,7 @@ const appActions = { * @param {object} options - object containing options such as acive, back, and forward booleans */ tabCloned: function (tabId, options) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_TAB_CLONED, tabId, options @@ -930,7 +930,7 @@ const appActions = { * @param {boolean} temporary */ noScriptExceptionsAdded: function (hostPattern, origins, temporary) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_ADD_NOSCRIPT_EXCEPTIONS, hostPattern, origins, @@ -944,7 +944,7 @@ const appActions = { * @param {Array.} objectPath */ setObjectId: function (objectId, objectPath) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_SET_OBJECT_ID, objectId, objectPath @@ -958,7 +958,7 @@ const appActions = { * @param {Object} devices {[deviceId]: {lastRecordTimestamp=, name=}} */ saveSyncDevices: function (devices) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_SAVE_SYNC_DEVICES, devices }) @@ -972,7 +972,7 @@ const appActions = { * @param {string=} seedQr */ saveSyncInitData: function (seed, deviceId, lastFetchTimestamp, seedQr) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_SAVE_SYNC_INIT_DATA, seed, deviceId, @@ -986,7 +986,7 @@ const appActions = { * @param {string|null} error */ setSyncSetupError: function (error) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_SET_SYNC_SETUP_ERROR, error }) @@ -998,7 +998,7 @@ const appActions = { * @param {Array.} records */ applySiteRecords: function (records) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_APPLY_SITE_RECORDS, records }) @@ -1008,7 +1008,7 @@ const appActions = { * Dispatch to populate the sync object id -> appState key path mapping cache */ createSyncCache: function () { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_CREATE_SYNC_CACHE }) }, @@ -1017,7 +1017,7 @@ const appActions = { * Dispatches a message to delete sync data. */ resetSyncData: function () { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_RESET_SYNC_DATA }) }, @@ -1028,7 +1028,7 @@ const appActions = { * @param {Object} detail - Object containing: title, message, buttons to show */ tabMessageBoxShown: function (tabId, detail) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_TAB_MESSAGE_BOX_SHOWN, tabId, detail @@ -1041,7 +1041,7 @@ const appActions = { * @param {Object} detail - Object containing: suppressCheckbox (boolean) */ tabMessageBoxDismissed: function (tabId, detail) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_TAB_MESSAGE_BOX_DISMISSED, tabId, detail @@ -1054,7 +1054,7 @@ const appActions = { * @param {Object} detail - Replacement object */ tabMessageBoxUpdated: function (tabId, detail) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_TAB_MESSAGE_BOX_UPDATED, tabId, detail @@ -1068,7 +1068,7 @@ const appActions = { * @param location {string} location where handler was triggered */ navigatorHandlerRegistered: function (partition, protocol, location) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_NAVIGATOR_HANDLER_REGISTERED, partition, protocol, @@ -1083,7 +1083,7 @@ const appActions = { * @param location {string} location where handler was triggered */ navigatorHandlerUnregistered: function (partition, protocol, location) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_NAVIGATOR_HANDLER_UNREGISTERED, partition, protocol, @@ -1095,7 +1095,7 @@ const appActions = { * Open dialog for default download path setting */ defaultDownloadPath: function () { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_DOWNLOAD_DEFAULT_PATH }) }, @@ -1107,7 +1107,7 @@ const appActions = { * @param publishers {Object} publishers from the synopsis */ enableUndefinedPublishers: function (publishers) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_ENABLE_UNDEFINED_PUBLISHERS, publishers }) @@ -1118,7 +1118,7 @@ const appActions = { * @param publishers {Object} updated publishers */ changeLedgerPinnedPercentages: function (publishers) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_CHANGE_LEDGER_PINNED_PERCENTAGES, publishers }) @@ -1131,7 +1131,7 @@ const appActions = { * @param {number} tabId - The tabId of the tab to pin */ tabPinned: function (tabId, pinned) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_TAB_PINNED, tabId, pinned @@ -1145,7 +1145,7 @@ const appActions = { * @param {object} tabValue - the created tab state */ newWebContentsAdded: function (windowId, frameOpts, tabValue) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_NEW_WEB_CONTENTS_ADDED, queryInfo: { windowId @@ -1162,7 +1162,7 @@ const appActions = { * @param {object} dragData - Data being transfered */ dragStarted: function (windowId, dragType, dragData) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_DRAG_STARTED, windowId, dragType, @@ -1176,7 +1176,7 @@ const appActions = { * @param {object} dragData - Data being transfered */ dragEnded: function () { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_DRAG_ENDED }) }, @@ -1185,7 +1185,7 @@ const appActions = { * Notifies the app that a drop operation occurred */ dataDropped: function (dropWindowId) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_DATA_DROPPED, dropWindowId }) @@ -1195,7 +1195,7 @@ const appActions = { * Notifies the app that a drop operation occurred */ draggedOver: function (draggedOverData) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_DRAGGED_OVER, draggedOverData }) @@ -1206,7 +1206,7 @@ const appActions = { * @param {number} tabId - Tab id used for an action */ onGoBack: function (tabId) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_ON_GO_BACK, tabId }) @@ -1217,7 +1217,7 @@ const appActions = { * @param {number} tabId - Tab id used for an action */ onGoForward: function (tabId) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_ON_GO_FORWARD, tabId }) @@ -1229,7 +1229,7 @@ const appActions = { * @param {number} index - Index in the history */ onGoToIndex: function (tabId, index) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_ON_GO_TO_INDEX, tabId, @@ -1243,7 +1243,7 @@ const appActions = { * @param {ClientRect} rect - Parent element position for this action */ onGoBackLong: function (tabId, rect) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_ON_GO_BACK_LONG, tabId, rect @@ -1256,7 +1256,7 @@ const appActions = { * @param {ClientRect} rect - Parent element position for this action */ onGoForwardLong: function (tabId, rect) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_ON_GO_FORWARD_LONG, tabId, rect @@ -1268,7 +1268,7 @@ const appActions = { * because ESC was pressed. */ dragCancelled: function () { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_DRAG_CANCELLED }) }, @@ -1278,17 +1278,17 @@ const appActions = { * @param {number} tabId - Tab id of current frame */ autoplayBlocked: function (tabId) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_AUTOPLAY_BLOCKED, tabId }) }, - /* + /** * Handle 'save-password' event from muon */ savePassword: function (username, origin, tabId) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_SAVE_PASSWORD, username, origin, @@ -1296,11 +1296,11 @@ const appActions = { }) }, - /* + /** * Handle 'update-password' event from muon */ updatePassword: function (username, origin, tabId) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_UPDATE_PASSWORD, username, origin, @@ -1313,7 +1313,7 @@ const appActions = { * @param {Object} passwordDetail - login details */ deletePassword: function (passwordDetail) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_REMOVE_PASSWORD, passwordDetail }) @@ -1323,7 +1323,7 @@ const appActions = { * Deletes all saved login credentials */ clearPasswords: function () { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_CLEAR_PASSWORDS }) }, @@ -1332,12 +1332,78 @@ const appActions = { * Delete legacy "never saved password" list */ deletePasswordSite: function (origin) { - AppDispatcher.dispatch({ + dispatch({ actionType: appConstants.APP_CHANGE_SITE_SETTING, hostPattern: origin, key: 'savePasswords' }) + }, + + /** + * Indicates that the urlbar text has changed, usually from user input + * + * @param {number} windowId - The window ID the text is being changed inside of + * @param {number} tabId - The tab ID the text is being changed inside of + * @param {string} input - The text that was entered into the URL bar + */ + urlBarTextChanged: function (windowId, tabId, input) { + dispatch({ + actionType: appConstants.APP_URL_BAR_TEXT_CHANGED, + input, + tabId, + windowId, + queryInfo: { + windowId + } + }) + }, + + /** + * New URL bar suggestion search results are available. + * This is typically from a service like Duck Duck Go auto complete for the portion of text that the user typed in. + * + * @param {number} tabId - the tab id for the action + * @param searchResults The search results for the currently entered URL bar text. + */ + searchSuggestionResultsAvailable: function (tabId, query, searchResults) { + dispatch({ + actionType: appConstants.APP_SEARCH_SUGGESTION_RESULTS_AVAILABLE, + tabId, + searchResults, + query + }) + }, + + /** + * Indicates URL bar suggestions and selected index. + * + * @param {number} windowId - the window ID + * @param {Object[]} suggestionList - The list of suggestions for the entered URL bar text. This can be generated from history, bookmarks, etc. + * @param {number} selectedIndex - The index for the selected item (users can select items with down arrow on their keyboard) + */ + urlBarSuggestionsChanged: function (windowId, suggestionList, selectedIndex) { + dispatch({ + actionType: appConstants.APP_URL_BAR_SUGGESTIONS_CHANGED, + suggestionList, + selectedIndex, + windowId, + queryInfo: { + windowId + } + }) + }, + + /** + * Dispatches a message to set the search engine details. + * @param {Object} searchDetail - the search details + */ + defaultSearchEngineLoaded: function (searchDetail) { + dispatch({ + actionType: appConstants.APP_DEFAULT_SEARCH_ENGINE_LOADED, + searchDetail + }) } + } module.exports = appActions diff --git a/js/actions/windowActions.js b/js/actions/windowActions.js index ad3413cad4a..6dad5af36a2 100644 --- a/js/actions/windowActions.js +++ b/js/actions/windowActions.js @@ -4,15 +4,10 @@ 'use strict' -const AppDispatcher = require('../dispatcher/appDispatcher') +const {dispatch} = require('../dispatcher/appDispatcher') const windowConstants = require('../constants/windowConstants') -function dispatch (action) { - AppDispatcher.dispatch(action) -} - const windowActions = { - /** * Dispatches an event to the main process to replace the window state * @@ -115,19 +110,6 @@ const windowActions = { }) }, - /** - * Dispatches a message to the store to set the user entered text for the URL bar. - * Unlike setLocation and loadUrl, this does not modify the state of src and location. - * - * @param {string} location - The text to set as the new navbar URL input - */ - setNavBarUserInput: function (location) { - dispatch({ - actionType: windowConstants.WINDOW_SET_NAVBAR_INPUT, - location - }) - }, - /** * Shows/hides the find-in-page bar. * @param {number} frameKey - Key of the frame that we want to modify @@ -352,20 +334,6 @@ const windowActions = { }) }, - /** - * Sets the URL bar suggestions and selected index. - * - * @param {Object[]} suggestionList - The list of suggestions for the entered URL bar text. This can be generated from history, bookmarks, etc. - * @param {number} selectedIndex - The index for the selected item (users can select items with down arrow on their keyboard) - */ - setUrlBarSuggestions: function (suggestionList, selectedIndex) { - dispatch({ - actionType: windowConstants.WINDOW_SET_URL_BAR_SUGGESTIONS, - suggestionList, - selectedIndex - }) - }, - /** * The active URL bar suggestion was clicked * @param {boolean} isForSecondaryAction - Whether the secondary action is expected @@ -426,21 +394,6 @@ const windowActions = { }) }, - /** - * New URL bar suggestion search results are available. - * This is typically from a service like Duck Duck Go auto complete for the portion of text that the user typed in. - * - * @param {number} tabId - the tab id for the action - * @param searchResults The search results for the currently entered URL bar text. - */ - searchSuggestionResultsAvailable: function (tabId, searchResults) { - dispatch({ - actionType: windowConstants.WINDOW_SEARCH_SUGGESTION_RESULTS_AVAILABLE, - tabId, - searchResults - }) - }, - /** * Marks the URL bar text as selected or not * @@ -509,17 +462,6 @@ const windowActions = { }) }, - /** - * Dispatches a message to set the search engine details. - * @param {Object} searchDetail - the search details - */ - setSearchDetail: function (searchDetail) { - dispatch({ - actionType: windowConstants.WINDOW_SET_SEARCH_DETAIL, - searchDetail - }) - }, - /** * Dispatches a message to set the find-in-page details. * @param {Object} frameKey - Frame key of the frame in question diff --git a/js/components/main.js b/js/components/main.js index 936d00b6ced..865cbf1ce43 100644 --- a/js/components/main.js +++ b/js/components/main.js @@ -288,7 +288,7 @@ class Main extends ImmutableComponent { if (this.lastLoadedSearchProviders === undefined || engine !== this.lastLoadedSearchProviders) { entries.forEach((entry) => { if (entry.name === engine) { - windowActions.setSearchDetail(Immutable.fromJS({ + appActions.defaultSearchEngineLoaded(Immutable.fromJS({ searchURL: entry.search, autocompleteURL: entry.autocomplete, platformClientId: entry.platformClientId diff --git a/js/constants/appConstants.js b/js/constants/appConstants.js index d103b4b858b..f1c518e3932 100644 --- a/js/constants/appConstants.js +++ b/js/constants/appConstants.js @@ -107,6 +107,10 @@ const appConstants = { APP_TAB_MESSAGE_BOX_UPDATED: _, APP_NAVIGATOR_HANDLER_REGISTERED: _, APP_NAVIGATOR_HANDLER_UNREGISTERED: _, + APP_URL_BAR_TEXT_CHANGED: _, + APP_URL_BAR_SUGGESTIONS_CHANGED: _, + APP_SEARCH_SUGGESTION_RESULTS_AVAILABLE: _, + APP_DEFAULT_SEARCH_ENGINE_LOADED: _, APP_CHANGE_LEDGER_PINNED_PERCENTAGES: _, APP_ENABLE_UNDEFINED_PUBLISHERS: _, APP_TAB_PINNED: _, diff --git a/js/constants/config.js b/js/constants/config.js index 3894edf56fb..159ed361399 100644 --- a/js/constants/config.js +++ b/js/constants/config.js @@ -28,8 +28,7 @@ module.exports = { defaultUrl: 'about:newtab', urlBarSuggestions: { maxOpenedFrames: 2, - maxBookmarkSites: 2, - maxHistorySites: 3, + maxHistorySites: 5, maxAboutPages: 2, maxSearch: 3, maxTopSites: 3 diff --git a/js/constants/windowConstants.js b/js/constants/windowConstants.js index 094c82bd44c..3fed146c5e9 100644 --- a/js/constants/windowConstants.js +++ b/js/constants/windowConstants.js @@ -20,10 +20,8 @@ const windowConstants = { WINDOW_WEBVIEW_LOAD_END: _, WINDOW_SET_FULL_SCREEN: _, WINDOW_SET_LINK_HOVER_PREVIEW: _, - WINDOW_SET_URL_BAR_SUGGESTIONS: _, WINDOW_SET_RENDER_URL_BAR_SUGGESTIONS: _, WINDOW_URL_BAR_AUTOCOMPLETE_ENABLED: _, - WINDOW_SEARCH_SUGGESTION_RESULTS_AVAILABLE: _, WINDOW_URL_BAR_SUGGESTIONS_CLEARED: _, WINDOW_PREVIOUS_URL_BAR_SUGGESTION_SELECTED: _, WINDOW_NEXT_URL_BAR_SUGGESTION_SELECTED: _, @@ -43,7 +41,6 @@ const windowConstants = { WINDOW_URL_BAR_ON_FOCUS: _, WINDOW_TAB_ON_FOCUS: _, WINDOW_SET_URL_BAR_SELECTED: _, - WINDOW_SET_SEARCH_DETAIL: _, WINDOW_SET_FIND_DETAIL: _, WINDOW_SET_BOOKMARK_DETAIL: _, // If set, also indicates that add/edit is shown WINDOW_SET_CONTEXT_MENU_DETAIL: _, // If set, also indicates that the context menu is shown diff --git a/js/contextMenus.js b/js/contextMenus.js index efde4f195ab..78c163e8627 100644 --- a/js/contextMenus.js +++ b/js/contextMenus.js @@ -957,7 +957,7 @@ const searchSelectionMenuItem = (location) => { if (location) { let activeFrame = windowStore.getState().get('activeFrameKey') let frame = windowStore.getFrame(activeFrame) - let searchUrl = windowStore.getState().getIn(['searchDetail', 'searchURL']).replace('{searchTerms}', encodeURIComponent(location)) + let searchUrl = appStoreRenderer.state.getIn(['searchDetail', 'searchURL']).replace('{searchTerms}', encodeURIComponent(location)) appActions.createTabRequested({ url: searchUrl, isPrivate: frame.get('isPrivate'), @@ -1064,7 +1064,7 @@ function mainTemplateInit (nodeProps, frame, tab) { click: () => { let activeFrame = windowStore.getState().get('activeFrameKey') let frame = windowStore.getFrame(activeFrame) - let searchUrl = windowStore.getState().getIn(['searchDetail', 'searchURL']) + let searchUrl = appStoreRenderer.state.getIn(['searchDetail', 'searchURL']) .replace('{searchTerms}', encodeURIComponent(nodeProps.srcURL)) .replace('?q', 'byimage?image_url') appActions.createTabRequested({ @@ -1399,8 +1399,8 @@ function onTabPageContextMenu (framePropsList, e) { function onUrlBarContextMenu (e) { e.stopPropagation() + const searchDetail = appStoreRenderer.state.get('searchDetail') const windowState = windowStore.getState() - const searchDetail = windowStore.getState().get('searchDetail') const activeFrame = getActiveFrame(windowState) const inputMenu = Menu.buildFromTemplate(urlBarTemplateInit(searchDetail, activeFrame, e)) inputMenu.popup(getCurrentWindow()) diff --git a/js/dispatcher/appDispatcher.js b/js/dispatcher/appDispatcher.js index 23c786da011..6b6b662866f 100644 --- a/js/dispatcher/appDispatcher.js +++ b/js/dispatcher/appDispatcher.js @@ -22,6 +22,7 @@ class AppDispatcher { this.callbacks = [] this.promises = [] this.dispatching = false + this.dispatch = this.dispatch.bind(this) } /** @@ -97,11 +98,15 @@ class AppDispatcher { const {getCurrentWindowId} = require('../../app/renderer/currentWindow') if (!payload.queryInfo || !payload.queryInfo.windowId || payload.queryInfo.windowId === getCurrentWindowId()) { this.dispatchToOwnRegisteredCallbacks(payload) + // We still want to tell the browser prcoess about app actions for payloads with a windowId + // specified for the current window, but we don't want the browser process to forward it back + // to us. + if (payload.queryInfo) { + payload.queryInfo.alreadyHandledByRenderer = payload.queryInfo.windowId === getCurrentWindowId() + } } cb() - if (!payload.queryInfo || !payload.queryInfo.windowId || payload.queryInfo.windowId !== getCurrentWindowId()) { - ipcCargo.push(payload) - } + ipcCargo.push(payload) return } } diff --git a/js/stores/appStore.js b/js/stores/appStore.js index 8fccd482889..bd8cf924104 100644 --- a/js/stores/appStore.js +++ b/js/stores/appStore.js @@ -395,6 +395,7 @@ const handleAppAction = (action) => { require('../../app/browser/reducers/windowsReducer'), require('../../app/browser/reducers/spellCheckReducer'), require('../../app/browser/reducers/clipboardReducer'), + require('../../app/browser/reducers/urlBarSuggestionsReducer'), require('../../app/browser/reducers/passwordManagerReducer'), require('../../app/browser/reducers/tabMessageBoxReducer'), require('../../app/browser/reducers/dragDropReducer'), @@ -891,6 +892,9 @@ const handleAppAction = (action) => { appState = appState.set('siteSettings', newSiteSettings) }) break + case appConstants.APP_DEFAULT_SEARCH_ENGINE_LOADED: + appState = appState.set('searchDetail', action.searchDetail) + break default: } diff --git a/js/stores/windowStore.js b/js/stores/windowStore.js index 8e4760d240c..dd4f59c6062 100644 --- a/js/stores/windowStore.js +++ b/js/stores/windowStore.js @@ -24,6 +24,7 @@ const Serializer = require('../dispatcher/serializer') const {updateTabPageIndex} = require('../../app/renderer/lib/tabUtil') const assert = require('assert') const contextMenuState = require('../../app/common/state/contextMenuState') +const appStoreRenderer = require('./appStoreRenderer') let windowState = Immutable.fromJS({ activeFrameKey: null, @@ -31,12 +32,12 @@ let windowState = Immutable.fromJS({ closedFrames: [], ui: { tabs: { + tabPageIndex: 0 }, mouseInTitlebar: false, menubar: { } - }, - searchDetail: null + } }) let lastEmittedState @@ -130,7 +131,7 @@ const newFrame = (state, frameOpts, openInForeground, insertionIndex, nextKey) = frameOpts.location = UrlUtil.getUrlFromInput(frameOpts.location) } else { // location is a search - const defaultURL = windowStore.getState().getIn(['searchDetail', 'searchURL']) + const defaultURL = appStoreRenderer.state.getIn(['searchDetail', 'searchURL']) if (defaultURL) { frameOpts.location = defaultURL .replace('{searchTerms}', encodeURIComponent(frameOpts.location)) @@ -424,11 +425,6 @@ const doAction = (action) => { activeShortcutDetails: action.activeShortcutDetails }) break - case windowConstants.WINDOW_SET_SEARCH_DETAIL: - windowState = windowState.merge({ - searchDetail: action.searchDetail - }) - break case windowConstants.WINDOW_SET_FIND_DETAIL: { const frameIndex = frameStateUtil.getFrameIndex(windowState, action.frameKey) @@ -801,7 +797,7 @@ const dispatchEventPayload = (e, payload) => { queryInfo.windowId = getCurrentWindowId() } // handle any ipc dispatches that are targeted to this window - if (queryInfo.windowId && queryInfo.windowId === getCurrentWindowId()) { + if (queryInfo.windowId && queryInfo.windowId === getCurrentWindowId() && !queryInfo.alreadyHandledByRenderer) { doAction(payload) } } diff --git a/package.json b/package.json index 6887629df43..78aab259ba7 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "ad-block": "^3.0.2", "aphrodite": "^1.0.0", "async": "^2.0.1", + "bloodhound-js": "^1.2.1", "clipboard-copy": "^1.0.0", "electron-localshortcut": "^0.6.0", "electron-squirrel-startup": "brave/electron-squirrel-startup", diff --git a/test/lib/brave.js b/test/lib/brave.js index 4ae9a9f2436..14f01991d65 100644 --- a/test/lib/brave.js +++ b/test/lib/brave.js @@ -114,6 +114,7 @@ var exports = { UP: '\ue013', PAGEDOWN: '\uE00F', END: '\uE010', + TAB: '\ue004', NULL: '\uE000' }, diff --git a/test/navbar-components/urlBarSuggestionsTest.js b/test/navbar-components/urlBarSuggestionsTest.js index 9f673a607e8..875a934bca8 100644 --- a/test/navbar-components/urlBarSuggestionsTest.js +++ b/test/navbar-components/urlBarSuggestionsTest.js @@ -46,7 +46,7 @@ describe('urlBarSuggestions', function () { it('show suggestion when single letter is typed in', function * () { yield this.app.client.ipcSend('shortcut-focus-url') .waitForElementFocus(urlInput) - .setInputText(urlInput, 'a') + .setInputText(urlInput, 'about:about') .waitForExist(urlBarSuggestions) }) @@ -66,6 +66,23 @@ describe('urlBarSuggestions', function () { .waitForElementCount(urlBarSuggestions, 0) }) + it('deactivated suggestions do not pop back up when left or shift is pressed', function * () { + yield this.app.client + .setInputText(urlInput, 'Page 1') + .waitForExist(urlBarSuggestions + ' li.suggestionItem[data-index="0"]') + .keys(Brave.keys.BACKSPACE) + .waitForElementCount(urlBarSuggestions, 0) + .keys(Brave.keys.LEFT) + .pause(50) + .keys(Brave.keys.SHIFT + Brave.keys.LEFT) + .pause(50) + .keys(Brave.keys.LEFT) + .pause(50) + .keys(Brave.keys.SHIFT) + .pause(50) + .waitForElementCount(urlBarSuggestions, 0) + }) + it('deactivates suggestions on delete', function * () { yield this.app.client .setInputText(urlInput, 'Page 1') @@ -103,13 +120,13 @@ describe('urlBarSuggestions', function () { .keys(Brave.keys.DOWN) .waitForExist(urlBarSuggestions + ' li.suggestionItem[data-index="1"].selected') .keys(Brave.keys.ENTER) - .tabByIndex(1).getUrl().should.become(this.page1Url) + .tabByIndex(1).getUrl().should.become(this.page2Url) }) it('selects a location auto complete result but not for titles', function * () { yield this.app.client .setValue(urlInput, 'http://') - .waitForInputText(urlInput, Brave.server.urlOrigin()) + .waitForInputText(urlInput, this.page1Url) .waitForExist(urlBarSuggestions + ' li.selected') .setValue(urlInput, 'Page') .waitForElementCount(urlBarSuggestions + ' li.selected', 0) @@ -124,9 +141,8 @@ describe('urlBarSuggestions', function () { // so that finally, if the rest of the 1st option is entered via keyboard, it overwrites the suggestion from the mouse yield this.app.client .keys(pagePartialUrl) - .waitForInputText(urlInput, page2Url) // after entering partial URL matching two options, 1st is tentatively filled in (_without_ moving cursor to end) + .waitForInputText(urlInput, page1Url) // after entering partial URL matching two options, 1st is tentatively filled in (_without_ moving cursor to end) .waitForExist(urlBarSuggestions + ' li.suggestionItem') - .moveToObject(urlBarSuggestions + ' li.suggestionItem:not(.selected)') .waitForInputText(urlInput, page1Url) // mousing over 2nd option tentatively completes URL with 2nd option (_without_ moving cursor to end) .keys('2.html') .waitForInputText(urlInput, page2Url) // without moving mouse, typing rest of 1st option URL overwrites the autocomplete from mouseover @@ -134,7 +150,7 @@ describe('urlBarSuggestions', function () { it('selection is not reset when pressing non-input key', function * () { const pagePartialUrl = Brave.server.url('page') - const page1Url = Brave.server.url('page1.html') + const page2Url = Brave.server.url('page2.html') aboutHistoryState.setHistory(Immutable.fromJS({ about: { history: { @@ -148,10 +164,10 @@ describe('urlBarSuggestions', function () { .setValue(urlInput, pagePartialUrl) .waitForVisible(urlBarSuggestions) .keys(Brave.keys.DOWN) - .waitForInputText(urlInput, page1Url) + .waitForInputText(urlInput, page2Url) .keys(Brave.keys.CONTROL) .keys(Brave.keys.CONTROL) - .waitForSelectedText('1.html') + .waitForSelectedText('2.html') }) it('non-prefixed active suggestion loads the suggestion when enter is pressed', function * () { @@ -187,7 +203,22 @@ describe('search suggestions', function () { it('Finds search suggestions and performs a search when selected', function * () { yield this.app.client .changeSetting(settings.OFFER_SEARCH_SUGGESTIONS, true) - .setInputText(urlInput, 'what is') + + // Until a refactor happens with search suggestions, + // they are a bit fragile if you aren't actually typing. + // So this for loop avoids an intermittent failure. + // I also couldn't use .typeText() because the autocomplete makes + // that hang when it checks for the value that was typed. + // The refactor needed is to allow urlbar suggestions to be built + // in parts and then rendered together, so that different suggestion + // types would be combined and rendered together as they are available. + const input = 'what is' + for (let i = 0; i < input.length; i++) { + yield this.app.client + .keys(input[i]) + .pause(50) + } + yield this.app.client .waitForVisible(urlBarSuggestions) .keys(Brave.keys.DOWN) .waitForExist(urlBarSuggestions + ' li.suggestionItem[data-index="0"]:not(.selected)') diff --git a/test/navbar-components/urlBarTest.js b/test/navbar-components/urlBarTest.js index 2ed7ccfdfa7..faea6c30676 100644 --- a/test/navbar-components/urlBarTest.js +++ b/test/navbar-components/urlBarTest.js @@ -265,6 +265,21 @@ describe('urlBar tests', function () { }) }) + describe('highlight suggestions with tab', function () { + it('autofills from selected suggestion', function * () { + // now type something + yield this.app.client + .keys('https://br') + .waitForInputText(urlInput, 'https://brave.com') + // hit down + .keys(Brave.keys.TAB) + .waitForInputText(urlInput, 'https://brave.com/test') + // hit up + .keys(Brave.keys.SHIFT + Brave.keys.TAB) + .waitForInputText(urlInput, 'https://brave.com') + }) + }) + describe('highlight suggestions', function () { it('autofills from selected suggestion', function * () { // now type something @@ -293,7 +308,7 @@ describe('urlBar tests', function () { it('does not show suggestions', function * () { yield this.app.client .keys('brave') - .waitForVisible(urlBarSuggestions, 1) + .waitForVisible(urlBarSuggestions) .ipcSend('shortcut-focus-url') .waitForElementFocus(urlInput) .waitForElementCount(urlBarSuggestions, 0) @@ -317,12 +332,12 @@ describe('urlBar tests', function () { .waitForVisible(urlBarSuggestions) // highlight for autocomplete brianbondy.com .moveToObject(urlBarSuggestions, 0, 100) - yield selectsText(this.app.client, 'rianbondy.com') - .keys('ra') + yield selectsText(this.app.client, 'rave.com/test3') + .keys('rian') .execute(function (urlBarSuggestions) { document.querySelector(urlBarSuggestions).scrollTop = 200 }, urlBarSuggestions) - yield selectsText(this.app.client, 've.com') + yield selectsText(this.app.client, 'bondy.com/test4') }) }) diff --git a/test/unit/app/browser/reducers/urlBarSuggestionsReducerTest.js b/test/unit/app/browser/reducers/urlBarSuggestionsReducerTest.js new file mode 100644 index 00000000000..b3cec29ea84 --- /dev/null +++ b/test/unit/app/browser/reducers/urlBarSuggestionsReducerTest.js @@ -0,0 +1,94 @@ +/* global describe, it, before, after, afterEach */ +const mockery = require('mockery') +const sinon = require('sinon') +const Immutable = require('immutable') +const assert = require('assert') + +const appConstants = require('../../../../../js/constants/appConstants') +require('../../../braveUnit') + +describe('urlBarSuggestionsReducer', function () { + let urlBarSuggestionsReducer + before(function () { + mockery.enable({ + warnOnReplace: false, + warnOnUnregistered: false, + useCleanCache: true + }) + + this.siteSuggestionsStub = { + init: sinon.stub(), + add: sinon.stub() + } + mockery.registerMock('../../common/lib/siteSuggestions', this.siteSuggestionsStub) + + this.suggestionStub = { + generateNewSuggestionsList: sinon.stub(), + generateNewSearchXHRResults: sinon.stub() + } + mockery.registerMock('../../common/lib/suggestion', this.suggestionStub) + + urlBarSuggestionsReducer = require('../../../../../app/browser/reducers/urlBarSuggestionsReducer') + }) + + after(function () { + mockery.disable() + }) + + afterEach(function () { + this.siteSuggestionsStub.init.reset() + this.siteSuggestionsStub.add.reset() + this.suggestionStub.generateNewSuggestionsList.reset() + this.suggestionStub.generateNewSearchXHRResults.reset() + }) + + const site1 = { + location: 'https://brave.com' + } + + const initState = Immutable.fromJS({ + sites: { + 'key': site1 + } + }) + + describe('APP_SET_STATE', function () { + it('inits the suggestions lib with sites', function () { + urlBarSuggestionsReducer(Immutable.Map(), {actionType: appConstants.APP_SET_STATE, appState: initState}) + const callCount = this.siteSuggestionsStub.init.calledOnce + assert.equal(callCount, 1) + assert.deepEqual(this.siteSuggestionsStub.init.args[0][0], [site1]) + }) + }) + describe('APP_ADD_SITE', function () { + it('adds a site in the suggestions lib', function () { + const newState = urlBarSuggestionsReducer(initState, {actionType: appConstants.APP_ADD_SITE, siteDetail: site1}) + const callCount = this.siteSuggestionsStub.add.calledOnce + assert.equal(callCount, 1) + assert.deepEqual(this.siteSuggestionsStub.add.args[0][0], site1) + assert.deepEqual(newState, initState) + }) + }) + describe('APP_URL_BAR_TEXT_CHANGED', function () { + it('regenerates suggestions', function () { + const windowId = 1 + const tabId = 42 + const input = 'hello world' + const newState = urlBarSuggestionsReducer(initState, {actionType: appConstants.APP_URL_BAR_TEXT_CHANGED, tabId, windowId, input}) + const callCount = this.suggestionStub.generateNewSuggestionsList.calledOnce + assert.equal(callCount, 1) + assert.deepEqual(this.suggestionStub.generateNewSuggestionsList.args[0][0], initState) + assert.deepEqual(this.suggestionStub.generateNewSuggestionsList.args[0][1], windowId) + assert.deepEqual(this.suggestionStub.generateNewSuggestionsList.args[0][2], tabId) + assert.deepEqual(this.suggestionStub.generateNewSuggestionsList.args[0][3], input) + assert.deepEqual(newState, initState) + + const xhrCallCount = this.suggestionStub.generateNewSearchXHRResults.calledOnce + assert.equal(xhrCallCount, 1) + assert.deepEqual(this.suggestionStub.generateNewSearchXHRResults.args[0][0], initState) + assert.deepEqual(this.suggestionStub.generateNewSearchXHRResults.args[0][1], windowId) + assert.deepEqual(this.suggestionStub.generateNewSearchXHRResults.args[0][2], tabId) + assert.deepEqual(this.suggestionStub.generateNewSearchXHRResults.args[0][3], input) + }) + }) +}) diff --git a/test/unit/app/common/lib/siteSuggestionsTest.js b/test/unit/app/common/lib/siteSuggestionsTest.js new file mode 100644 index 00000000000..96e5fc48586 --- /dev/null +++ b/test/unit/app/common/lib/siteSuggestionsTest.js @@ -0,0 +1,289 @@ +/* global describe, before, after, it */ +const {tokenizeInput, init, query, add} = require('../../../../../app/common/lib/siteSuggestions') +const assert = require('assert') +const Immutable = require('immutable') +const fakeElectron = require('../../../lib/fakeElectron') +const mockery = require('mockery') + +const site1 = { + location: 'https://www.bradrichter.co/bad_numbers/3', + title: 'Do not use 3 for items because it is prime' +} +const site2 = { + location: 'https://www.brave.com', + title: 'No really, take back the web' +} +const site3 = { + location: 'https://www.bradrichter.co/bad_numbers/5', + title: 'Do not use 5 it is so bad, try 6 instead. Much better.' +} + +const site4 = { + location: 'https://www.designers.com/brad', + title: 'Brad Saves The World!' +} + +// Compares 2 sites via deepEqual while first clearing out cached data +const siteEqual = (actual, expected) => { + assert.equal(actual.constructor, expected.constructor) + if (expected.constructor === Array) { + assert.equal(actual.length, expected.length) + for (let i = 0; i < actual.length; i++) { + const a = Object.assign({}, actual[i]) + delete a.parsedUrl + const e = Object.assign({}, expected[i]) + delete e.parsedUrl + assert.deepEqual(a, e) + } + } else { + const a = Object.assign({}, actual) + delete a.parsedUrl + const e = Object.assign({}, expected) + delete e.parsedUrl + assert.deepEqual(a, e) + } +} + +require('../../../braveUnit') + +describe('siteSuggestions lib', function () { + before(function () { + mockery.enable({ + warnOnReplace: false, + warnOnUnregistered: false, + useCleanCache: true + }) + mockery.registerMock('electron', fakeElectron) + }) + after(function () { + mockery.disable() + }) + describe('tokenizeInput', function () { + it('empty string has no tokens', function () { + assert.deepEqual(tokenizeInput(''), []) + }) + it('undefined has no tokens', function () { + assert.deepEqual(tokenizeInput(null), []) + }) + it('null has no tokens', function () { + assert.deepEqual(tokenizeInput(undefined), []) + }) + it('lowercases tokens', function () { + assert.deepEqual(tokenizeInput('BRaD HaTES PRIMES'), ['brad', 'hates', 'primes']) + }) + it('includes protocol', function () { + assert.deepEqual(tokenizeInput('https://bradrichter.co/I/hate/primes.html'), ['bradrichter', 'co', 'i', 'hate', 'primes', 'html', 'https:']) + }) + it('includes query', function () { + assert.deepEqual(tokenizeInput('https://bradrichter.co/I/hate/primes.html?test=abc&test2=abcd'), ['bradrichter', 'co', 'i', 'hate', 'primes', 'html', 'test', 'abc', 'test2', 'abcd', 'https:']) + }) + it('does not include hash', function () { + assert.deepEqual(tokenizeInput('https://bradrichter.co/I/hate/primes.html?test=abc#testing'), ['testing', 'bradrichter', 'co', 'i', 'hate', 'primes', 'html', 'test', 'abc', 'https:']) + }) + it('spaces get tokenized', function () { + assert.deepEqual(tokenizeInput('brad\thates primes'), ['brad', 'hates', 'primes']) + }) + it('periods get tokenized', function () { + assert.deepEqual(tokenizeInput('brad.hates.primes'), ['brad', 'hates', 'primes']) + }) + it('/ gets tokenized', function () { + assert.deepEqual(tokenizeInput('brad/hates/primes'), ['brad', 'hates', 'primes']) + }) + it('\\ gets tokenized', function () { + assert.deepEqual(tokenizeInput('brad\\hates\\primes'), ['brad', 'hates', 'primes']) + }) + it('can tokenize site objects', function () { + assert.deepEqual(tokenizeInput(site1), ['do', 'not', 'use', '3', 'for', 'items', 'because', 'it', 'is', 'prime', 'www', 'bradrichter', 'co', 'bad_numbers', '3', 'https:']) + }) + it('non URLs get tokenized', function () { + assert.deepEqual(tokenizeInput('hello world Greatest...Boss...Ever'), ['hello', 'world', 'greatest', 'boss', 'ever']) + }) + }) + + const checkResult = (inputQuery, expectedResults, cb) => { + query(inputQuery).then((results) => { + siteEqual(results, expectedResults) + cb() + }) + } + + describe('not initialized query', function () { + it('returns no results if not initialized', function (cb) { + checkResult('hello', [], cb) + }) + }) + describe('query', function () { + before(function (cb) { + const sites = [site1, site2, site3, site4] + init(sites).then(cb.bind(null, null)) + }) + it('can query with empty string', function (cb) { + checkResult('', [], cb) + }) + it('can query with null', function (cb) { + checkResult(null, [], cb) + }) + it('can query with undefined', function (cb) { + checkResult(undefined, [], cb) + }) + it('returns an empty array when there are no matches', function (cb) { + checkResult('hello', [], cb) + }) + it('returns matched result on an exact token', function (cb) { + checkResult('bradrichter', [site1, site3], cb) + }) + it('returns matched result on a token prefix', function (cb) { + checkResult('brad', [site1, site3, site4], cb) + }) + it('returns no results on input that has a token as a prefix', function (cb) { + checkResult('bradrichterhatesprimes.com', [], cb) + }) + it('can query on title', function (cb) { + checkResult('back', [site2], cb) + }) + it('can query on multiple tokens in different order', function (cb) { + checkResult('back really', [site2], cb) + }) + it('all tokens must match, not just some', function (cb) { + checkResult('brave brad', [], cb) + }) + }) + + describe('query', function () { + describe('sorts results by location', function () { + before(function (cb) { + const sites = Immutable.fromJS([{ + location: 'https://brave.com/twi' + }, { + location: 'https://twitter.com/brave' + }, { + location: 'https://twitter.com/brianbondy' + }, { + location: 'https://twitter.com/_brianclif' + }, { + location: 'https://twitter.com/cezaraugusto' + }, { + location: 'https://bbondy.com/twitter' + }, { + location: 'https://twitter.com' + }, { + location: 'https://twitter.com/i/moments' + }]) + init(sites).then(cb.bind(null, null)) + }) + it('orders shortest match first', function (cb) { + query('twitter.com').then((results) => { + siteEqual(results[0], { location: 'https://twitter.com' }) + cb() + }) + }) + it('matches prefixes first', function (cb) { + query('twi').then((results) => { + siteEqual(results[0], { location: 'https://twitter.com' }) + cb() + }) + }) + it('closest to the left match wins', function (cb) { + query('twitter.com brian').then((results) => { + siteEqual(results[0], { location: 'https://twitter.com/brianbondy' }) + cb() + }) + }) + it('matches based on tokens and not exactly', function (cb) { + query('twitter.com/moments').then((results) => { + siteEqual(results[0], { location: 'https://twitter.com/i/moments' }) + cb() + }) + }) + }) + describe('sorts results by count', function () { + before(function (cb) { + this.page2 = { + location: 'https://brave.com/page2', + count: 20 + } + const sites = Immutable.fromJS([{ + location: 'https://brave.com/page1', + count: 5 + }, this.page2, { + location: 'https://brave.com/page3', + count: 2 + }]) + init(sites).then(cb.bind(null, null)) + }) + it('highest count first', function (cb) { + query('https://brave.com/page').then((results) => { + siteEqual(results[0], this.page2) + cb() + }) + }) + }) + describe('respects lastAccessTime', function () { + before(function (cb) { + this.site = { + location: 'https://bravebrowser.com/page2', + lastAccessedTime: 1494958046427, // most recent + count: 1 + } + const sites = Immutable.fromJS([{ + location: 'https://bravez.com/page1', + lastAccessedTime: 1, + count: 1 + }, { + location: 'https://bravebrowser.com/page1', + lastAccessedTime: 1494957046426, + count: 1 + }, this.site, { + location: 'https://bravebrowser.com/page3', + lastAccessedTime: 1494957046437, + count: 1 + }]) + init(sites).then(cb.bind(null, null)) + }) + it('items with lastAccessTime of 1 get ignored (signifies preloaded default)', function (cb) { + query('https://bravez.com/page').then((results) => { + assert.equal(results.length, 0) + cb() + }) + }) + it('most recently accessed get sorted first', function (cb) { + query('bravebrowser').then((results) => { + siteEqual(results[0], this.site) + cb() + }) + }) + }) + }) + + describe('add sites after init', function () { + before(function (cb) { + const sites = [site1, site2, site3, site4] + init(sites).then(() => { + add({ + location: 'https://slack.com' + }) + }).then(cb.bind(null, null)) + }) + it('can be found', function (cb) { + checkResult('slack', [{ location: 'https://slack.com' }], cb) + }) + it('adding twice results in 1 result only', function (cb) { + add({ + location: 'https://slack.com' + }) + checkResult('slack', [{ location: 'https://slack.com' }], cb) + }) + it('can add simple strings', function (cb) { + add({ + location: 'https://slashdot.org' + }) + checkResult('slash', [{ location: 'https://slashdot.org' }], cb) + }) + it('can add Immutable objects', function (cb) { + add(Immutable.fromJS({ + location: 'https://microsoft.com' + })) + checkResult('micro', [{ location: 'https://microsoft.com' }], cb) + }) + }) +}) diff --git a/test/unit/app/common/lib/suggestionTest.js b/test/unit/app/common/lib/suggestionTest.js new file mode 100644 index 00000000000..1faf67ad6cc --- /dev/null +++ b/test/unit/app/common/lib/suggestionTest.js @@ -0,0 +1,301 @@ +/* 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/. */ +/* global describe, before, after, it */ + +const mockery = require('mockery') +const assert = require('assert') +const sinon = require('sinon') +const Immutable = require('immutable') +const urlParse = require('url').parse +const {makeImmutable} = require('../../../../../app/common/state/immutableUtil') +const fakeElectron = require('../../../lib/fakeElectron') +const _ = require('underscore') +let suggestion +require('../../../braveUnit') + +const AGE_DECAY = 50 + +const fakeImmutableUtil = Object.assign({ + makeImmutable: (obj) => { + return makeImmutable(obj) + }, + isList: (obj) => { + return Immutable.List.isList(obj) + } +}, require('../../../../../app/common/state/immutableUtil')) + +describe('suggestion unit tests', function () { + let makeImmutableSpy + + before(function () { + mockery.enable({ + warnOnReplace: false, + warnOnUnregistered: false, + useCleanCache: true + }) + + makeImmutableSpy = sinon.spy(fakeImmutableUtil, 'makeImmutable') + mockery.registerMock('../../common/state/immutableUtil', fakeImmutableUtil) + mockery.registerMock('electron', fakeElectron) + suggestion = require('../../../../../app/common/lib/suggestion') + }) + + after(function () { + makeImmutableSpy.restore() + mockery.disable() + }) + + describe('normalizeLocation', function () { + it('does nothing when input is not a string', function () { + assert.equal(suggestion.normalizeLocation(), undefined) + assert.equal(suggestion.normalizeLocation(undefined), undefined) + assert.equal(suggestion.normalizeLocation(null), null) + assert.equal(suggestion.normalizeLocation(3), 3) + assert.equal(suggestion.normalizeLocation(3.3), 3.3) + }) + + it('normalizes location', function () { + assert.ok(suggestion.normalizeLocation('https://www.site.com') === 'site.com', 'www. prefix removed') + assert.ok(suggestion.normalizeLocation('http://site.com') === 'site.com', 'location not modified') + }) + }) + + describe('sortingPriority', function () { + it('sorts sites correctly', function () { + assert.ok(suggestion.sortingPriority(10, 100, 50, AGE_DECAY) > suggestion.sortingPriority(10, 100, 40, AGE_DECAY), 'newer sites with equal access counts sort earlier') + assert.ok(suggestion.sortingPriority(10, 100, 50, AGE_DECAY) < suggestion.sortingPriority(11, 100, 40, AGE_DECAY), 'Sites with higher access counts sort earlier (unless time delay overriden)') + assert.ok(suggestion.sortingPriority(10, 10000000000, 10000000000, AGE_DECAY) > suggestion.sortingPriority(11, 10000000000, 1000000000, AGE_DECAY), 'much newer sites without lower counts sort with higher priority') + }) + }) + + describe('isSimpleDomainNameValue', function () { + it('sorts simple sites higher than complex sites', function () { + const siteSimple = Immutable.Map({ location: 'http://www.site.com' }) + const siteComplex = Immutable.Map({ location: 'http://www.site.com/?foo=bar#a' }) + assert.ok(suggestion.isSimpleDomainNameValue(siteSimple) === true, 'simple site returns 1') + assert.ok(suggestion.isSimpleDomainNameValue(siteComplex) === false, 'complex site returns 0') + }) + }) + + describe('shouldNormalizeLocation', function () { + it('Determines prefixes which should be normalized', function () { + const prefixes = ['http://', 'https://', 'www.'] + prefixes.forEach((prefix) => { + for (let i = 0; i < prefix.length; i++) { + const substring = prefix.substring(0, i + 1) + assert.equal(suggestion.shouldNormalizeLocation(substring), false) + } + }) + }) + + it('Determines prefixes which should NOT be normalized', function () { + const prefixes = ['httphttp', 'brave.com', 'www3', 'http://www.x'] + prefixes.forEach((prefix) => { + assert.equal(suggestion.shouldNormalizeLocation(prefix), true) + }) + }) + }) + + describe('createVirtualHistoryItems', function () { + const site1 = Immutable.Map({ + location: 'http://www.foo.com/1', + count: 0, + lastAccessedTime: 0, + title: 'www.foo/com/1' + }) + + const site2 = Immutable.Map({ + location: 'http://www.foo.com/2', + count: 0, + lastAccessedTime: 0, + title: 'www.foo/com/2' + }) + + const site3 = Immutable.Map({ + location: 'http://www.foo.com/3', + count: 0, + lastAccessedTime: 0, + title: 'www.foo/com/3' + }) + + it('handles input being null/undefined', function () { + const emptyResult = Immutable.Map() + assert.deepEqual(suggestion.createVirtualHistoryItems(), emptyResult) + assert.deepEqual(suggestion.createVirtualHistoryItems(undefined), emptyResult) + assert.deepEqual(suggestion.createVirtualHistoryItems(null), emptyResult) + }) + + it('handles entries with unparseable "location" field', function () { + const badInput = makeImmutable({ + site1: { + location: undefined + }, + site2: { + location: null + }, + site3: { + location: '' + }, + site4: { + location: 'httphttp://lol.com' + } + }) + assert.ok(suggestion.createVirtualHistoryItems(badInput)) + }) + + it('calls immutableUtil.makeImmutable', function () { + const callCount = makeImmutableSpy.withArgs({}).callCount + suggestion.createVirtualHistoryItems() + assert.equal(makeImmutableSpy.withArgs({}).callCount, callCount + 1) + }) + + it('shows virtual history item', function () { + var history = Immutable.List([site1, site2, site3]) + var virtual = suggestion.createVirtualHistoryItems(history).toJS() + var keys = _.keys(virtual) + assert.ok(keys.length > 0, 'virtual location created') + assert.ok(virtual[keys[0]].location === 'http://www.foo.com') + assert.ok(virtual[keys[0]].title === 'www.foo.com') + assert.ok(virtual[keys[0]].lastAccessedTime > 0) + }) + }) + + describe('sorting functions', function () { + describe('getSortByPath', function () { + before(function () { + this.sort = suggestion.getSortByPath('twitter') + }) + it('returns 0 when both paths contain the string', function () { + assert.equal(this.sort('https://brave.com/twitter', 'https://brianbondy.com/twitter'), 0) + }) + it('returns 0 when neihter path contain the string', function () { + assert.equal(this.sort('https://brave.com/facebook', 'https://brianbondy.com/facebook'), 0) + }) + it('returns -1 when the first site contains the string only', function () { + assert.equal(this.sort('https://brave.com/twitter', 'https://brianbondy.com/facebook'), -1) + }) + it('returns 1 when the second site contains the string only', function () { + assert.equal(this.sort('https://brave.com/facebook', 'https://brianbondy.com/twitter'), 1) + }) + it('matches even domain name for input string', function () { + assert.equal(this.sort('https://brave.com/facebook', 'https://twitter.com'), 1) + }) + }) + describe('sortBySimpleURL', function () { + before(function () { + this.sort = (url1, url2) => { + return suggestion.sortBySimpleURL( + { location: url1, parsedUrl: urlParse(url1) }, + { location: url2, parsedUrl: urlParse(url2) } + ) + } + }) + it('returns 0 when both paths are simple', function () { + assert.equal(this.sort('https://brave.com', 'https://brianbondy.com'), 0) + }) + it('returns 0 when neihter path is simple', function () { + assert.equal(this.sort('https://brave.com/facebook', 'https://brianbondy.com/facebook'), 0) + }) + it('returns -1 when the first site is simple only', function () { + assert.equal(this.sort('https://brave.com', 'https://brianbondy.com/facebook'), -1) + }) + it('returns 1 when the second site is simple only', function () { + assert.equal(this.sort('https://brave.com/facebook', 'https://brianbondy.com'), 1) + }) + it('trailing slash is considered simple', function () { + assert.equal(this.sort('https://brave.com/', 'https://twitter.com'), 0) + }) + it('trailing hash is considered simple', function () { + assert.equal(this.sort('https://brave.com/#', 'https://twitter.com'), 0) + }) + it('Prefers https sipmle URLs', function () { + assert(this.sort('https://brave.com', 'http://brave.com') < 0) + }) + }) + describe('getSortByDomain', function () { + before(function () { + const userInputLower = 'google.c' + const userInputParts = userInputLower.split('/') + const userInputHost = userInputParts[0] + const internalSort = suggestion.getSortByDomain(userInputLower, userInputHost) + this.sort = (url1, url2) => { + return internalSort( + { location: url1, parsedUrl: urlParse(url1) }, + { location: url2, parsedUrl: urlParse(url2) } + ) + } + }) + it('negative if only first has a matching domain', function () { + assert(this.sort('https://google.com', 'https://www.brianbondy.com') < 0) + }) + it('positive if only second has a matching domain', function () { + assert(this.sort('https://www.brianbondy.com', 'https://google.com') > 0) + }) + it('0 if both have a matching domain from index 0', function () { + assert.equal(this.sort('https://google.com', 'https://google.ca'), 0) + }) + it('0 if neither has a matching domain', function () { + assert.equal(this.sort('https://brianbondy.com', 'https://clifton.io/'), 0) + }) + it('negative if first site has a match from the start of domain', function () { + assert(this.sort('https://google.com', 'https://mygoogle.com') < 0) + }) + it('positive if second site has a match but without www.', function () { + assert(this.sort('https://www.google.com', 'https://google.com') > 0) + }) + it('negative if there is a pos 0 match not including www.', function () { + assert(this.sort('https://www.google.com', 'https://mygoogle.com') < 0) + }) + it('simple domain gets matched higher', function () { + assert(this.sort('https://www.google.com', 'https://www.google.com/extra') < 0) + }) + }) + describe('getSortForSuggestions', function () { + describe('with url entered as path', function () { + before(function () { + const userInputLower = 'brianbondy.com/projects' + const userInputParts = userInputLower.split('/') + const userInputHost = userInputParts[0] + const internalSort = suggestion.getSortForSuggestions(userInputLower, userInputHost) + this.sort = (url1, url2) => { + return internalSort( + { location: url1, parsedUrl: urlParse(url1) }, + { location: url2, parsedUrl: urlParse(url2) } + ) + } + }) + it('returns 0 when both urls are the same', function () { + assert.equal(this.sort('https://www.google.com', 'https://www.google.com'), 0) + }) + it('matches exact path if more specific path is specified', function () { + assert(this.sort('https://brianbondy.com', 'https://www.brianbondy.com/projects/2') > 0) + }) + }) + describe('with single string entered', function () { + before(function () { + const userInputLower = 'brianbondy.c' + const userInputParts = userInputLower.split('/') + const userInputHost = userInputParts[0] + const internalSort = suggestion.getSortForSuggestions(userInputLower, userInputHost) + this.sort = (url1, url2) => { + return internalSort( + { location: url1, parsedUrl: urlParse(url1) }, + { location: url2, parsedUrl: urlParse(url2) } + ) + } + }) + it('matches on domain name first', function () { + assert(this.sort('https://www.brianbondy.com', 'https://www.google.com/brianbondy.co') < 0) + }) + it('matches with or without protocol', function () { + assert(this.sort('https://www.2brianbondy.com', 'http://www.brianbondy.com') > 0) + assert(this.sort('https://brianbondy.com', 'www.brianbondy.com') < 0) + }) + it('non-wwww. matches before www.', function () { + assert(this.sort('https://brianbondy.com', 'www.brianbondy.com') < 0) + }) + }) + }) + }) +}) diff --git a/test/unit/app/renderer/components/navigation/urlBarSuggestionItemTest.js b/test/unit/app/renderer/components/navigation/urlBarSuggestionItemTest.js index a99da2394a3..b1cbb3c8c20 100644 --- a/test/unit/app/renderer/components/navigation/urlBarSuggestionItemTest.js +++ b/test/unit/app/renderer/components/navigation/urlBarSuggestionItemTest.js @@ -7,6 +7,7 @@ const mockery = require('mockery') const {mount} = require('enzyme') const assert = require('assert') const sinon = require('sinon') +const Immutable = require('immutable') const fakeElectron = require('../../../../lib/fakeElectron') const suggestionTypes = require('../../../../../../js/constants/suggestionTypes') let UrlBarSuggestionItem @@ -31,11 +32,11 @@ describe('UrlBarSuggestionItem component', function () { before(function () { this.onMouseOver = sinon.spy() this.onSuggestionClicked = sinon.spy() - this.suggestion = { + this.suggestion = Immutable.fromJS({ title: 'hello', type: suggestionType, location: 'http://www.brave.com' - } + }) this.result = mount( { - return makeImmutable(obj) - } -} - -describe('suggestion unit tests', function () { - let makeImmutableSpy - - before(function () { - mockery.enable({ - warnOnReplace: false, - warnOnUnregistered: false, - useCleanCache: true - }) - - makeImmutableSpy = sinon.spy(fakeImmutableUtil, 'makeImmutable') - mockery.registerMock('../../common/state/immutableUtil', fakeImmutableUtil) - suggestion = require('../../../../../app/renderer/lib/suggestion') - }) - - after(function () { - makeImmutableSpy.restore() - mockery.disable() - }) - - describe('normalizeLocation', function () { - it('does nothing when input is not a string', function () { - assert.equal(suggestion.normalizeLocation(), undefined) - assert.equal(suggestion.normalizeLocation(undefined), undefined) - assert.equal(suggestion.normalizeLocation(null), null) - assert.equal(suggestion.normalizeLocation(3), 3) - assert.equal(suggestion.normalizeLocation(3.3), 3.3) - }) - - it('normalizes location', function () { - assert.ok(suggestion.normalizeLocation('https://www.site.com') === 'site.com', 'www. prefix removed') - assert.ok(suggestion.normalizeLocation('http://site.com') === 'site.com', 'location not modified') - }) - }) - - describe('sortingPriority', function () { - it('sorts sites correctly', function () { - assert.ok(suggestion.sortingPriority(10, 100, 50, AGE_DECAY) > suggestion.sortingPriority(10, 100, 40, AGE_DECAY), 'newer sites with equal access counts sort earlier') - assert.ok(suggestion.sortingPriority(10, 100, 50, AGE_DECAY) < suggestion.sortingPriority(11, 100, 40, AGE_DECAY), 'Sites with higher access counts sort earlier (unless time delay overriden)') - assert.ok(suggestion.sortingPriority(10, 10000000000, 10000000000, AGE_DECAY) > suggestion.sortingPriority(11, 10000000000, 1000000000, AGE_DECAY), 'much newer sites without lower counts sort with higher priority') - }) - }) - - describe('simpleDomainNameValue', function () { - it('sorts simple sites higher than complex sites', function () { - const siteSimple = Immutable.Map({ location: 'http://www.site.com' }) - const siteComplex = Immutable.Map({ location: 'http://www.site.com/?foo=bar#a' }) - assert.ok(suggestion.simpleDomainNameValue(siteSimple) === 1, 'simple site returns 1') - assert.ok(suggestion.simpleDomainNameValue(siteComplex) === 0, 'complex site returns 0') - }) - }) - - describe('shouldNormalizeLocation', function () { - it('Determines prefixes which should be normalized', function () { - const prefixes = ['http://', 'https://', 'www.'] - prefixes.forEach((prefix) => { - for (let i = 0; i < prefix.length; i++) { - const substring = prefix.substring(0, i + 1) - assert.equal(suggestion.shouldNormalizeLocation(substring), false) - } - }) - }) - - it('Determines prefixes which should NOT be normalized', function () { - const prefixes = ['httphttp', 'brave.com', 'www3', 'http://www.x'] - prefixes.forEach((prefix) => { - assert.equal(suggestion.shouldNormalizeLocation(prefix), true) - }) - }) - }) - - describe('createVirtualHistoryItems', function () { - const site1 = Immutable.Map({ - location: 'http://www.foo.com/1', - count: 0, - lastAccessedTime: 0, - title: 'www.foo/com/1' - }) - - const site2 = Immutable.Map({ - location: 'http://www.foo.com/2', - count: 0, - lastAccessedTime: 0, - title: 'www.foo/com/2' - }) - - const site3 = Immutable.Map({ - location: 'http://www.foo.com/3', - count: 0, - lastAccessedTime: 0, - title: 'www.foo/com/3' - }) - - it('handles input being null/undefined', function () { - const emptyResult = Immutable.Map() - assert.deepEqual(suggestion.createVirtualHistoryItems(), emptyResult) - assert.deepEqual(suggestion.createVirtualHistoryItems(undefined), emptyResult) - assert.deepEqual(suggestion.createVirtualHistoryItems(null), emptyResult) - }) - - it('handles entries with unparseable "location" field', function () { - const badInput = makeImmutable({ - site1: { - location: undefined - }, - site2: { - location: null - }, - site3: { - location: '' - }, - site4: { - location: 'httphttp://lol.com' - } - }) - assert.ok(suggestion.createVirtualHistoryItems(badInput)) - }) - - it('calls immutableUtil.makeImmutable', function () { - const callCount = makeImmutableSpy.withArgs({}).callCount - suggestion.createVirtualHistoryItems() - assert.equal(makeImmutableSpy.withArgs({}).callCount, callCount + 1) - }) - - it('shows virtual history item', function () { - var history = Immutable.List([site1, site2, site3]) - var virtual = suggestion.createVirtualHistoryItems(history).toJS() - var keys = _.keys(virtual) - assert.ok(keys.length > 0, 'virtual location created') - assert.ok(virtual[keys[0]].location === 'http://www.foo.com') - assert.ok(virtual[keys[0]].title === 'www.foo.com') - assert.ok(virtual[keys[0]].lastAccessedTime > 0) - }) - }) -}) diff --git a/test/unit/app/renderer/reducers/urlBarReducerTest.js b/test/unit/app/renderer/reducers/urlBarReducerTest.js index 1baa6dd982d..a1c10b82e6d 100644 --- a/test/unit/app/renderer/reducers/urlBarReducerTest.js +++ b/test/unit/app/renderer/reducers/urlBarReducerTest.js @@ -2,9 +2,11 @@ const mockery = require('mockery') const Immutable = require('immutable') const assert = require('assert') +const sinon = require('sinon') const fakeElectron = require('../../../lib/fakeElectron') const windowConstants = require('../../../../../js/constants/windowConstants') +const appConstants = require('../../../../../js/constants/appConstants') require('../../../braveUnit') const windowState = Immutable.fromJS({ @@ -111,10 +113,10 @@ describe('urlBarReducer', function () { mockery.disable() }) - describe('WINDOW_SET_NAVBAR_INPUT', function () { + describe('APP_URL_BAR_TEXT_CHANGED', function () { before(function () { this.location = 'this test is brought to you by coffee.' - this.newState = urlBarReducer(windowState, {actionType: windowConstants.WINDOW_SET_NAVBAR_INPUT, location: this.location}) + this.newState = urlBarReducer(windowState, {actionType: appConstants.APP_URL_BAR_TEXT_CHANGED, input: this.location}) }) it('Changes urlbar state for active frame key', function () { @@ -211,7 +213,15 @@ describe('urlBarReducer', function () { suggestions: { shouldRender: true, selectedIndex: 2, - suggestionList: ['2.71828', '18284', '59045', '23536'], + suggestionList: [{ + location: 'https://brave.com/2.71828' + }, { + location: 'https://brave.com/18284' + }, { + location: 'https://brave.com/59045' + }, { + location: 'https://brave.com/23536' + }], searchResults: [ '3.1415926535' ] @@ -245,6 +255,10 @@ describe('urlBarReducer', function () { default: return true } }}) + this.suggestionClickHandlers = { + navigateSiteClickHandler: sinon.mock() + } + mockery.registerMock('../suggestionClickHandlers', this.suggestionClickHandlers) urlBarReducer = require('../../../../../app/renderer/reducers/urlBarReducer') }) @@ -280,15 +294,7 @@ describe('urlBarReducer', function () { }) }) - describe('WINDOW_SEARCH_SUGGESTION_RESULTS_AVAILABLE', function () { - it('turns off suggestions', function () { - const searchResults = Immutable.fromJS(['0110001001110010011010010110000101101110']) - const newState = urlBarReducer(windowState, {actionType: windowConstants.WINDOW_SEARCH_SUGGESTION_RESULTS_AVAILABLE, searchResults, tabId: 2}) - assert.deepEqual(newState.getIn(['frames', 1, 'navbar', 'urlbar', 'suggestions', 'searchResults']).toJS(), searchResults.toJS()) - }) - }) - - describe('WINDOW_SET_NAVBAR_INPUT', function () { + describe('APP_URL_BAR_TEXT_CHANGED', function () { // TODO }) @@ -299,10 +305,10 @@ describe('urlBarReducer', function () { }) }) - describe('WINDOW_SET_URL_BAR_SUGGESTIONS', function () { + describe('APP_URL_BAR_SUGGESTIONS_CHANGED', function () { it('suggestion results can be updated', function () { const suggestionList = Immutable.fromJS(['0.207879576']) - const newState = urlBarReducer(windowState, {actionType: windowConstants.WINDOW_SET_URL_BAR_SUGGESTIONS, suggestionList, selectedIndex: null}) + const newState = urlBarReducer(windowState, {actionType: appConstants.APP_URL_BAR_SUGGESTIONS_CHANGED, suggestionList, selectedIndex: null}) assert.equal(newState.getIn(['frames', 1, 'navbar', 'urlbar', 'suggestions', 'suggestionList']), suggestionList) assert.equal(newState.getIn(['frames', 1, 'navbar', 'urlbar', 'suggestions', 'selectedIndex']), null) }) @@ -332,10 +338,10 @@ describe('urlBarReducer', function () { }) describe('WINDOW_ACTIVE_URL_BAR_SUGGESTION_CLICKED', function () { - it('', function (cb) { - const inputState = windowState.setIn(['frames', 1, 'navbar', 'urlbar', 'suggestions', 'suggestionList', 2], {onClick: cb}) - // Thiis test will finish when the callback is called from the click event + it('callback is not called sync', function () { + const inputState = windowState.setIn(['frames', 1, 'navbar', 'urlbar', 'suggestions', 'suggestionList', Immutable.fromJS([{location: 'https://www.brave.com'}])]) urlBarReducer(inputState, {actionType: windowConstants.WINDOW_ACTIVE_URL_BAR_SUGGESTION_CLICKED}) + assert.equal(this.suggestionClickHandlers.navigateSiteClickHandler.callCount, 0) }) }) })