import _ from 'underscore' import { Actions, NylasAPI, Thread, DatabaseStore, FocusedContentStore, MutableQuerySubscription, } from 'nylas-exports' import SearchActions from './search-actions' class SearchQuerySubscription extends MutableQuerySubscription { constructor(searchQuery, accountIds) { super(null, {emitResultSet: true}) this._searchQuery = searchQuery this._accountIds = accountIds this.resetData() this._connections = [] this._unsubscribers = [ FocusedContentStore.listen(::this.onFocusedContentChanged), ] _.defer(() => this.performSearch()) } replaceRange = () => { // TODO } resetData() { this._searchStartedAt = null this._resultsReceivedAt = null this._firstThreadSelectedAt = null this._lastFocusedThread = null this._focusedThreadCount = 0 } performSearch() { this._searchStartedAt = Date.now() this.performLocalSearch() this.performRemoteSearch() } performLocalSearch() { let dbQuery = DatabaseStore.findAll(Thread) if (this._accountIds.length === 1) { dbQuery = dbQuery.where({accountId: this._accountIds[0]}) } dbQuery = dbQuery.search(this._searchQuery).limit(30) dbQuery.then((results) => { if (results.length > 0) { this.replaceQuery(dbQuery) } }) } performRemoteSearch() { const accountsSearched = new Set() let resultIds = [] const allAccountsSearched = () => accountsSearched.size === this._accountIds.length const resultsReturned = () => { // Don't emit a "result" until we have at least one thread to display. // Otherwise it will show "No Results Found" if (resultIds.length > 0 || allAccountsSearched()) { const currentResults = this._set && this._set.ids().length > 0 if (currentResults) { const currentResultIds = this._set.ids() resultIds = _.uniq(currentResultIds.concat(resultIds)) } const dbQuery = ( DatabaseStore.findAll(Thread) .where({id: resultIds}) .order(Thread.attributes.lastMessageReceivedTimestamp.descending()) ) this.replaceQuery(dbQuery) } } this._connections = this._accountIds.map((accountId) => { return NylasAPI.startLongConnection({ accountId, path: `/threads/search/streaming?q=${encodeURIComponent(this._searchQuery)}`, onResults: (results) => { if (!this._resultsReceivedAt) { this._resultsReceivedAt = Date.now() } const threads = results[0] resultIds = resultIds.concat(_.pluck(threads, 'id')) resultsReturned() }, onStatusChanged: (conn) => { if (conn.isClosed()) { accountsSearched.add(accountId) if (allAccountsSearched()) { SearchActions.searchCompleted() } resultsReturned() } }, }) }) } onFocusedContentChanged() { const thread = FocusedContentStore.focused('thread') const shouldRecordChange = ( thread && (this._lastFocusedThread || {}).id !== thread.id ) if (shouldRecordChange) { if (this._focusedThreadCount === 0) { this._firstThreadSelectedAt = Date.now() } this._focusedThreadCount += 1 this._lastFocusedThread = thread } } reportSearchMetrics() { if (!this._searchStartedAt) { return; } let timeToFirstServerResults = null; let timeToFirstThreadSelected = null; const searchQuery = this._searchQuery const timeInsideSearch = Math.round((Date.now() - this._searchStartedAt) / 1000) const selectedThreads = this._focusedThreadCount const didSelectAnyThreads = selectedThreads > 0 if (this._firstThreadSelectedAt) { timeToFirstThreadSelected = Math.round((this._firstThreadSelectedAt - this._searchStartedAt) / 1000) } if (this._resultsReceivedAt) { timeToFirstServerResults = Math.round((this._resultsReceivedAt - this._searchStartedAt) / 1000) } const data = { searchQuery, selectedThreads, timeInsideSearch, didSelectAnyThreads, timeToFirstServerResults, timeToFirstThreadSelected, } Actions.recordUserEvent("Search Performed", data) this.resetData() } onLastCallbackRemoved() { this.reportSearchMetrics(); this._connections.forEach((conn) => conn.end()) this._unsubscribers.forEach((unsub) => unsub()) } } export default SearchQuerySubscription