diff --git a/app/internal_packages/thread-list/lib/thread-list-store.es6 b/app/internal_packages/thread-list/lib/thread-list-store.es6 index 306b975912..696a168058 100644 --- a/app/internal_packages/thread-list/lib/thread-list-store.es6 +++ b/app/internal_packages/thread-list/lib/thread-list-store.es6 @@ -1,10 +1,4 @@ -import { - Rx, - Actions, - WorkspaceStore, - FocusedContentStore, - FocusedPerspectiveStore, -} from 'mailspring-exports'; +import { Rx, Actions, FocusedContentStore, FocusedPerspectiveStore } from 'mailspring-exports'; import { ListTabular } from 'mailspring-component-kit'; import ThreadListDataSource from './thread-list-data-source'; import { ipcRenderer } from 'electron'; @@ -13,8 +7,10 @@ import MailspringStore from 'mailspring-store'; class ThreadListStore extends MailspringStore { constructor() { super(); + this._limitSearchDate = true; this.listenTo(FocusedPerspectiveStore, this._onPerspectiveChanged); ipcRenderer.on('refresh-start-of-day', this._onRefreshStartOfDay); + Actions.expandSearchDate.listen(this._onExpandSearchDate); this.createListDataSource(); } _onRefreshStartOfDay = () => { @@ -25,6 +21,33 @@ class ThreadListStore extends MailspringStore { setTimeout(this.createListDataSource, 100); } }; + limitSearchDate = () => { + return this._limitSearchDate; + }; + _onExpandSearchDate = () => { + const currentPerspective = FocusedPerspectiveStore.current(); + if (currentPerspective && currentPerspective.isSearchMailbox && this._limitSearchDate) { + this._limitSearchDate = false; + if (typeof this._dataSourceUnlisten === 'function') { + this._dataSourceUnlisten(); + } + + if (this._dataSource) { + this._dataSource.cleanup(); + this._dataSource = null; + } + + const threadsSubscription = FocusedPerspectiveStore.current().threads(false); + console.warn('subscription changed'); + if (threadsSubscription) { + this._dataSource = new ThreadListDataSource(threadsSubscription); + this._dataSourceUnlisten = this._dataSource.listen(this._onDataChanged); + } else { + this._dataSource = new ListTabular.DataSource.Empty(); + } + this.trigger(this); + } + }; dataSource = () => { return this._dataSource; @@ -63,6 +86,7 @@ class ThreadListStore extends MailspringStore { // Inbound Events _onPerspectiveChanged = () => { + this._limitSearchDate = true; if (AppEnv.isMainWindow()) { this.createListDataSource(); } else { @@ -71,6 +95,15 @@ class ThreadListStore extends MailspringStore { }; _onDataChanged = ({ previous, next } = {}) => { + // If in SearchMailBox, and we returned less than 100 results, + // we want to expand our search scope + const currentPerspective = FocusedPerspectiveStore.current(); + if (next && currentPerspective && currentPerspective.isSearchMailbox) { + if (next.empty() || next._ids.length < 100) { + //defer until next cycle, give UI time to display no results page if no results are found + setTimeout(() => Actions.expandSearchDate()); + } + } // This code keeps the focus and keyboard cursor in sync with the thread list. // When the thread list changes, it looks to see if the focused thread is gone, // or no longer matches the query criteria and advances the focus to the next diff --git a/app/internal_packages/thread-list/lib/thread-list.jsx b/app/internal_packages/thread-list/lib/thread-list.jsx index f6c12d449e..96065adb2b 100644 --- a/app/internal_packages/thread-list/lib/thread-list.jsx +++ b/app/internal_packages/thread-list/lib/thread-list.jsx @@ -127,7 +127,10 @@ class ThreadList extends React.Component { footer={this._getFooter()} stores={[ThreadListStore]} getStateFromStores={() => { - return { dataSource: ThreadListStore.dataSource() }; + return { + dataSource: ThreadListStore.dataSource(), + limitSearchDate: ThreadListStore.limitSearchDate(), + }; }} > diff --git a/app/internal_packages/thread-search/lib/search-bar-util.es6 b/app/internal_packages/thread-search/lib/search-bar-util.es6 index 938205f133..7ea788f622 100644 --- a/app/internal_packages/thread-search/lib/search-bar-util.es6 +++ b/app/internal_packages/thread-search/lib/search-bar-util.es6 @@ -30,6 +30,8 @@ export const wrapInQuotes = s => `"${s.replace(/"/g, '')}"`; export const getThreadSuggestions = async (term, accountIds) => { let dbQuery = DatabaseStore.findAll(Thread) .where({ state: 0 }) + .background() + .setQueryType(Constant.QUERY_TYPE.SEARCH_SUBJECT) .where([Thread.attributes.subject.like(term)]) .order(Thread.attributes.lastMessageTimestamp.descending()) .limit(10); diff --git a/app/internal_packages/thread-search/lib/search-mailbox-perspective.es6 b/app/internal_packages/thread-search/lib/search-mailbox-perspective.es6 index b65c21a1df..86b064cb1b 100644 --- a/app/internal_packages/thread-search/lib/search-mailbox-perspective.es6 +++ b/app/internal_packages/thread-search/lib/search-mailbox-perspective.es6 @@ -10,7 +10,7 @@ import { import SearchQuerySubscription from './search-query-subscription'; class SearchMailboxPerspective extends MailboxPerspective { - constructor(sourcePerspective, searchQuery) { + constructor(sourcePerspective, searchQuery, limitSearchDate) { super(sourcePerspective.accountIds); if (typeof searchQuery !== 'string') { throw new Error('SearchMailboxPerspective: Expected a `string` search query'); @@ -43,8 +43,8 @@ class SearchMailboxPerspective extends MailboxPerspective { return super.isEqual(other) && other.searchQuery === this.searchQuery; } - threads() { - return new SearchQuerySubscription(this.searchQuery, this.accountIds); + threads(limitSearchDate = true) { + return new SearchQuerySubscription(this.searchQuery, this.accountIds, limitSearchDate); } canReceiveThreadsFromAccountIds() { diff --git a/app/internal_packages/thread-search/lib/search-query-subscription.es6 b/app/internal_packages/thread-search/lib/search-query-subscription.es6 index 415dc44411..6cf34e5958 100644 --- a/app/internal_packages/thread-search/lib/search-query-subscription.es6 +++ b/app/internal_packages/thread-search/lib/search-query-subscription.es6 @@ -15,10 +15,11 @@ import { const utf7 = require('utf7').imap; class SearchQuerySubscription extends MutableQuerySubscription { - constructor(searchQuery, accountIds) { + constructor(searchQuery, accountIds, limitSearchDate = true) { super(null, { emitResultSet: true }); this._searchQuery = searchQuery; this._accountIds = accountIds; + this._limitSearchDate = limitSearchDate; this._connections = []; this._extDisposables = []; @@ -42,7 +43,16 @@ class SearchQuerySubscription extends MutableQuerySubscription { } let parsedQuery = null; try { - parsedQuery = SearchQueryParser.parse(this._searchQuery); + let tmpSearchQuery = this._searchQuery; + if ( + this._limitSearchDate && + !tmpSearchQuery.toLocaleUpperCase().includes('BEFORE:') && + !tmpSearchQuery.toLocaleUpperCase().includes('AFTER:') && + !tmpSearchQuery.toLocaleUpperCase().includes('SINCE:') + ) { + tmpSearchQuery = `( ${tmpSearchQuery} ) AND SINCE: "a month ago"`; + } + parsedQuery = SearchQueryParser.parse(tmpSearchQuery); // const firstInQueryExpression = parsedQuery.getFirstInQueryExpression(); // if (!firstInQueryExpression) { // const defaultFolder = new Set(); diff --git a/app/internal_packages/thread-search/lib/search-store.es6 b/app/internal_packages/thread-search/lib/search-store.es6 index adabb09ccb..664fb16c2c 100644 --- a/app/internal_packages/thread-search/lib/search-store.es6 +++ b/app/internal_packages/thread-search/lib/search-store.es6 @@ -21,12 +21,14 @@ class SearchStore extends MailspringStore { this._searchQuery = FocusedPerspectiveStore.current().searchQuery || ''; this._isSearching = false; + this._limitSearchDate = true; this.listenTo(WorkspaceStore, this._onWorkspaceChange); this.listenTo(FocusedPerspectiveStore, this._onPerspectiveChanged); this.listenTo(Actions.searchQuerySubmitted, this._onQuerySubmitted); this.listenTo(Actions.searchQueryChanged, this._onQueryChanged); this.listenTo(Actions.searchCompleted, this._onSearchCompleted); + // this.listenTo(Actions.expandSearchDate, this._onExpandSearchDate); } query() { @@ -108,12 +110,20 @@ class SearchStore extends MailspringStore { this._searchQuery = FocusedPerspectiveStore.current().searchQuery || ''; this.trigger(); }; + _onExpandSearchDate = () => { + if (this._limitSearchDate) { + this._limitSearchDate = false; + this._processAndSubmitQuery(); + this._throttleOnQuerySubmitted(this._searchQuery, true); + } + }; _onQueryChanged = query => { if (query !== this._searchQuery) { this._searchQuery = query; + this._limitSearchDate = true; this.trigger(); - this._processAndSubmitQuery(); + // this._processAndSubmitQuery(); this._throttleOnQuerySubmitted(query, true); } }; @@ -151,6 +161,9 @@ class SearchStore extends MailspringStore { _onQuerySubmitted = (query, forceQuery) => { if (query !== this._searchQuery || forceQuery) { this._searchQuery = query; + if (query !== this._searchQuery) { + this._limitSearchDate = true; + } this._preSearchQuery = query; this.trigger(); this._processAndSubmitQuery(forceQuery); diff --git a/app/src/components/list-tabular.jsx b/app/src/components/list-tabular.jsx index bd514ea605..b59724eb3b 100644 --- a/app/src/components/list-tabular.jsx +++ b/app/src/components/list-tabular.jsx @@ -18,12 +18,7 @@ import ListDataSource from './list-data-source'; import ListSelection from './list-selection'; import ListTabularItem from './list-tabular-item'; import IFrameSearcher from '../searchable-components/iframe-searcher'; -// const { -// GenericQueryExpression, -// AndQueryExpression, -// SubjectQueryExpression, -// FromQueryExpression, -// } = SearchQueryAST; + const ConfigProfileKey = 'core.appearance.profile'; class ListColumn { @@ -165,7 +160,17 @@ class ListTabular extends Component { UNSAFE_componentWillReceiveProps(nextProps) { if (nextProps.dataSource !== this.props.dataSource) { - this.setupDataSource(nextProps.dataSource); + const currentPerspective = FocusedPerspectiveStore.current(); + if ( + currentPerspective && + currentPerspective.isSearchMailbox && + !nextProps.limitSearchDate && + this.props.limitSearchDate + ) { + this.setupDataSource(nextProps.dataSource, true); + } else { + this.setupDataSource(nextProps.dataSource); + } } if (nextProps.itemHeight !== this.props.itemHeight) { this.updateRangeState(nextProps); @@ -179,7 +184,19 @@ class ListTabular extends Component { // If our view has been swapped out for an entirely different one, // reset our scroll position to the top. if (prevProps.dataSource !== this.props.dataSource) { - this._scrollRegion.scrollTop = 0; + const currentPerspective = FocusedPerspectiveStore.current(); + if ( + !( + currentPerspective && + currentPerspective.isSearchMailbox && + prevProps.limitSearchDate && + !this.props.limitSearchDate + ) + ) { + this._scrollRegion.scrollTop = 0; + } else { + console.log('limit search date changed, ignoring retaining scroll'); + } } if (!this.updateRangeStateFiring) { @@ -288,14 +305,19 @@ class ListTabular extends Component { } }; - setupDataSource(dataSource) { + setupDataSource(dataSource, retainRange = false) { this._unlisten(); this._unlisten = dataSource.listen(() => { if (this.mounted) { this._safeSetState(this.buildStateForRange()); } }); - this._safeSetState(this.buildStateForRange({ start: -1, end: -1, dataSource })); + if (retainRange) { + console.log('retained range'); + this._safeSetState(this.buildStateForRange({ dataSource, retainRange })); + } else { + this._safeSetState(this.buildStateForRange({ start: -1, end: -1, dataSource })); + } } getRowsToRender() { @@ -374,7 +396,15 @@ class ListTabular extends Component { start = this.state.renderedRangeStart, end = this.state.renderedRangeEnd, dataSource = this.props.dataSource, + retainRange, } = args; + if (retainRange) { + dataSource.setRetainedRange({ + start, + end, + }); + return; + } const items = {}; let animatingOut = {}; diff --git a/app/src/components/multiselect-list.jsx b/app/src/components/multiselect-list.jsx index b8b7b51dd2..210837d321 100644 --- a/app/src/components/multiselect-list.jsx +++ b/app/src/components/multiselect-list.jsx @@ -31,6 +31,7 @@ class MultiselectList extends React.Component { static propTypes = { dataSource: PropTypes.object, + limitSearchDate: PropTypes.bool, className: PropTypes.string.isRequired, columns: PropTypes.array.isRequired, columnCheckProvider: PropTypes.func, @@ -180,6 +181,7 @@ class MultiselectList extends React.Component { ref="list" columns={this.state.computedColumns} dataSource={this.props.dataSource} + limitSearchDate={this.props.limitSearchDate} itemPropsProvider={this.itemPropsProvider} onSelect={this._onClickItem} onComponentDidUpdate={this.props.onComponentDidUpdate} diff --git a/app/src/constant.es6 b/app/src/constant.es6 index 298170daf6..9a010f50cb 100644 --- a/app/src/constant.es6 +++ b/app/src/constant.es6 @@ -1,4 +1,5 @@ export const QUERY_TYPE = { + BACKGROUND: 'BACKGROUND', SEARCH_PERSPECTIVE: 'SEARCH_PERSPECTIVE', SEARCH_SUBJECT: 'SEARCH_SUBJECT', }; diff --git a/app/src/flux/actions.es6 b/app/src/flux/actions.es6 index e3cfea93a8..d169b75b98 100644 --- a/app/src/flux/actions.es6 +++ b/app/src/flux/actions.es6 @@ -611,6 +611,7 @@ class Actions { static searchQuerySubmitted = ActionScopeWindow; static searchQueryChanged = ActionScopeWindow; static searchCompleted = ActionScopeWindow; + static expandSearchDate = ActionScopeWindow; static updateChatPanelHeight = ActionScopeMainWindow; static expandChatPanelFiller = ActionScopeMainWindow; diff --git a/app/src/flux/models/query-subscription.es6 b/app/src/flux/models/query-subscription.es6 index 3eaea10c71..09a85465c3 100644 --- a/app/src/flux/models/query-subscription.es6 +++ b/app/src/flux/models/query-subscription.es6 @@ -2,6 +2,7 @@ import DatabaseStore from '../stores/database-store'; import QueryRange from './query-range'; import MutableQueryResultSet from './mutable-query-result-set'; import Thread from './thread'; +import { QUERY_TYPE } from '../../constant'; const isMessageView = AppEnv.isDisableThreading(); @@ -242,11 +243,14 @@ export default class QuerySubscription { _getQueryForRange = (range, fetchEntireModels) => { let rangeQuery = null; if (!range.isInfinite()) { - rangeQuery = rangeQuery || this._query.clone(); + rangeQuery = this._query.clone(); rangeQuery.offset(range.offset).limit(range.limit); } if (!fetchEntireModels) { rangeQuery = rangeQuery || this._query.clone(); + if (rangeQuery.queryType() && rangeQuery.isBackground()) { + rangeQuery.setQueryType(QUERY_TYPE.BACKGROUND); + } rangeQuery.idsOnly(); } rangeQuery = rangeQuery || this._query; diff --git a/app/src/flux/models/query.es6 b/app/src/flux/models/query.es6 index 7e1d305aa3..4b5e37e334 100644 --- a/app/src/flux/models/query.es6 +++ b/app/src/flux/models/query.es6 @@ -196,6 +196,9 @@ export default class ModelQuery { this._background = true; return this; } + isBackground() { + return this._background; + } setQueryType(queryType) { this._queryType = queryType; return this; diff --git a/app/src/flux/stores/database-agent.js b/app/src/flux/stores/database-agent.js index cf342f8bea..5b823cce3a 100644 --- a/app/src/flux/stores/database-agent.js +++ b/app/src/flux/stores/database-agent.js @@ -7,11 +7,11 @@ const dbs = {}; const deathDelay = 50000; const args = process.argv.slice(2); -if (args.length > 0) { +if (args.length > 1) { LOG.transports.file.file = path.join( args[0], 'ui-log', - `ui-log-database-agent-${Date.now()}.log` + `ui-log-database-agent-${args[1]}-${Date.now()}.log` ); LOG.transports.console.level = false; LOG.transports.file.maxSize = 20485760; diff --git a/app/src/flux/stores/database-store.es6 b/app/src/flux/stores/database-store.es6 index c9137d26a1..e3991c7034 100644 --- a/app/src/flux/stores/database-store.es6 +++ b/app/src/flux/stores/database-store.es6 @@ -11,6 +11,7 @@ import MailspringStore from '../../global/mailspring-store'; import Utils from '../models/utils'; import Query from '../models/query'; import DatabaseChangeRecord from './database-change-record'; +import { QUERY_TYPE } from '../../constant'; const debug = createDebug('app:RxDB'); const debugVerbose = createDebug('app:RxDB:all'); @@ -189,6 +190,9 @@ class DatabaseStore extends MailspringStore { this._open = false; this._waiting = []; this._preparedStatementCache = LRU({ max: 500 }); + this._agent = {}; + this._agent[QUERY_TYPE.BACKGROUND] = null; + this._agent[QUERY_TYPE.SEARCH_PERSPECTIVE] = null; this.setupEmitter(); this._emitter.setMaxListeners(100); @@ -312,7 +316,7 @@ class DatabaseStore extends MailspringStore { } if (msec > SLOW_QUERY_THRESH_HOLD) { - const msgPrefix = `DatabaseStore._executeInBackground took more than ${SLOW_QUERY_THRESH_HOLD}ms - `; + const msgPrefix = `DatabaseStore._executeInBackground ${queryType} took more than ${SLOW_QUERY_THRESH_HOLD}ms - `; this._prettyConsoleLog( `${msgPrefix}${msec}msec (${backgroundTime}msec in background): ${query}` ); @@ -407,28 +411,14 @@ class DatabaseStore extends MailspringStore { return results; } - _executeInBackground(query, values, dbKey = 'main', queryType = null) { - let _queryForLog = query; - if (AppEnv.enabledBackgroundQueryLog) { - console.log(`-------------------background query for ${dbKey}----------------`); - AppEnv.logDebug(`background query - ${query}`); - console.log(`--------------------background query for ${dbKey} end---------------`); - } - const sendToAgent = data => { - if (!this._agent) { - AppEnv.logError(`Agent not available for background db query, ${data.id} not send`); - return; - } - AppEnv.logDebug(`Sending query for ${data.id} to agent`); - this._agent.send(data); - }; - if (!this._agent) { + _registerAndStartAgent = (queryForLog, queryType = QUERY_TYPE.BACKGROUND) => { + if (!this._agent[queryType]) { AppEnv.logDebug(`DBStore:Agent not available, starting agent`); this._agentOpenQueries = {}; this._agentQueues = {}; - this._agent = childProcess.fork( + this._agent[queryType] = childProcess.fork( path.join(path.dirname(__filename), 'database-agent.js'), - [AppEnv.getConfigDirPath()], + [AppEnv.getConfigDirPath(), queryType], { silent: true, } @@ -442,44 +432,44 @@ class DatabaseStore extends MailspringStore { this._agentOpenQueries = {}; this._agentQueues = {}; }; - this._agent.stdout.on('data', data => console.log(data.toString())); - this._agent.stderr.on('data', data => { + this._agent[queryType].stdout.on('data', data => console.log(data.toString())); + this._agent[queryType].stderr.on('data', data => { AppEnv.reportError(new Error(`database-store._executeInBackground error`), { errorData: data.toString(), - query: _queryForLog, + query: queryForLog, }); - console.error(data.toString(), _queryForLog); + console.error(data.toString(), queryForLog); }); - this._agent.on('disconnect', () => { + this._agent[queryType].on('disconnect', () => { AppEnv.logError(`database background agent disconnected`); debug(`Query Agent: disconnected`); - if (this._agent) { - this._agent.kill('SIGTERM'); + if (this._agent[queryType]) { + this._agent[queryType].kill('SIGTERM'); } - this._agent = null; + this._agent[queryType] = null; clearOpenQueries(); }); - this._agent.on('exit', code => { + this._agent[queryType].on('exit', code => { AppEnv.logDebug(`database background agent exited with code ${code}`); debug(`Query Agent: exited with code ${code}`); - this._agent = null; + this._agent[queryType] = null; clearOpenQueries(); }); - this._agent.on('close', code => { + this._agent[queryType].on('close', code => { AppEnv.logDebug(`database background agent closed with code ${code}`); debug(`Query Agent: closed with code ${code}`); - this._agent = null; + this._agent[queryType] = null; clearOpenQueries(); }); - this._agent.on('error', err => { + this._agent[queryType].on('error', err => { AppEnv.reportError( new Error(`Query Agent: failed to start or receive message: ${err.toString()}`) ); - this._agent.kill('SIGTERM'); - this._agent = null; + this._agent[queryType].kill('SIGTERM'); + this._agent[queryType] = null; clearOpenQueries(); }); - this._agent.on('message', ({ type, id, results, agentTime, queryType }) => { + this._agent[queryType].on('message', ({ type, id, results, agentTime, queryType }) => { const result = { results, backgroundTime: agentTime }; if (!queryType && type === 'results' && this._agentOpenQueries[id]) { this._agentOpenQueries[id](result); @@ -489,6 +479,12 @@ class DatabaseStore extends MailspringStore { this._agentQueues[queryType].length > 0 ? this._agentQueues[queryType][0] : null; const newItem = this._agentQueues[queryType].length > 1 ? this._agentQueues[queryType][1] : null; + if (newItem && newItem.data) { + AppEnv.logDebug( + `DBStore:old item ${id} new item in queue, ${newItem.data.id} query type ${queryType}, sending` + ); + this._sendToAgent(newItem.data, queryType); + } if (item && item.id === id && item.resolve) { AppEnv.logDebug( `DBStore:Background results for ${id} of type ${queryType} match, resolving` @@ -499,12 +495,6 @@ class DatabaseStore extends MailspringStore { `DBStore:Background results for ${id} of type ${queryType} not resolve` ); } - if (newItem && newItem.data) { - AppEnv.logDebug( - `DBStore:old item ${id} new item in queue, ${newItem.data.id} query type ${queryType}, sending` - ); - sendToAgent(newItem.data); - } if (newItem) { this._agentQueues[queryType] = [newItem]; } else { @@ -513,6 +503,24 @@ class DatabaseStore extends MailspringStore { } }); } + }; + _sendToAgent = (data, queryType = QUERY_TYPE.BACKGROUND) => { + if (!this._agent[queryType]) { + AppEnv.logError(`Agent not available for background db query, ${data.id} not send`); + return; + } + AppEnv.logDebug(`Sending query for ${data.id} to agent`); + this._agent[queryType].send(data); + }; + + _executeInBackground(query, values, dbKey = 'main', queryType = QUERY_TYPE.BACKGROUND) { + if (AppEnv.enabledBackgroundQueryLog) { + console.log(`-------------------background query for ${dbKey}----------------`); + AppEnv.logDebug(`background query - ${query}`); + console.log(`--------------------background query for ${dbKey} end---------------`); + } + this._registerAndStartAgent(query, queryType); + return new Promise(resolve => { const id = Utils.generateTempId(); let ignore = false; @@ -543,7 +551,7 @@ class DatabaseStore extends MailspringStore { } if (!ignore) { AppEnv.logDebug(`DBStore:No pending request for ${id} of type ${queryType}, sending`); - sendToAgent(data); + this._sendToAgent(data, queryType); return; } AppEnv.logDebug(`DBStore:Request pending, request for ${id} of type ${queryType} not send`);