Skip to content
This repository has been archived by the owner on Dec 11, 2019. It is now read-only.

Commit

Permalink
Merge pull request #8824 from brave/bloodhound
Browse files Browse the repository at this point in the history
Bloodhound for URL bar suggestions & move suggestions to browser process
  • Loading branch information
bsclifton authored May 18, 2017
2 parents c5f4409 + f94239a commit 940b064
Show file tree
Hide file tree
Showing 36 changed files with 1,946 additions and 972 deletions.
43 changes: 43 additions & 0 deletions app/browser/reducers/urlBarSuggestionsReducer.js
Original file line number Diff line number Diff line change
@@ -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
32 changes: 32 additions & 0 deletions app/common/lib/fetchSearchSuggestions.js
Original file line number Diff line number Diff line change
@@ -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
138 changes: 138 additions & 0 deletions app/common/lib/siteSuggestions.js
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 940b064

Please sign in to comment.