From af0f389503b9b09855515f16fea03457fe38a02d Mon Sep 17 00:00:00 2001 From: Ben Gotow Date: Sun, 24 Jun 2018 23:49:27 -0500 Subject: [PATCH] Add date (since: yesterday, before: last week) handling to search bar --- app/src/date-utils.es6 | 39 +++++++++++++++++++ app/src/services/search/search-query-ast.es6 | 27 +++++++++++++ .../search/search-query-backend-imap.es6 | 4 ++ .../search/search-query-backend-local.es6 | 19 +++++++++ .../services/search/search-query-parser.es6 | 16 ++++++++ app/static/components/search-bar.less | 1 + 6 files changed, 106 insertions(+) diff --git a/app/src/date-utils.es6 b/app/src/date-utils.es6 index b3f527bf4..d38ac67a2 100644 --- a/app/src/date-utils.es6 +++ b/app/src/date-utils.es6 @@ -47,6 +47,7 @@ function isPastDate(inputDateObj, currentDate) { return inputMoment.isBefore(currentMoment); } +let _chronoPast = null; let _chronoFuture = null; let _chrono = null; @@ -93,6 +94,42 @@ function getChronoFuture() { return _chronoFuture; } +function getChronoPast() { + if (_chronoPast) { + return _chronoPast; + } + + const chrono = getChrono(); + const EnforcePastDate = new chrono.Refiner(); + EnforcePastDate.refine = (text, results) => { + results.forEach(result => { + const current = Object.assign({}, result.start.knownValues, result.start.impliedValues); + + if (result.start.isCertain('weekday') && !result.start.isCertain('day')) { + if (!isPastDate(current, result.ref)) { + result.start.imply('day', result.start.impliedValues.day - 7); + } + } + + if (result.start.isCertain('day') && !result.start.isCertain('month')) { + if (!isPastDate(current, result.ref)) { + result.start.imply('month', result.start.impliedValues.month - 1); + } + } + if (result.start.isCertain('month') && !result.start.isCertain('year')) { + if (!isPastDate(current, result.ref)) { + result.start.imply('year', result.start.impliedValues.year - 1); + } + } + }); + return results; + }; + + _chronoPast = new chrono.Chrono(chrono.options.casualOption()); + _chronoPast.refiners.push(EnforcePastDate); + return _chronoPast; +} + const DateUtils = { // Localized format: ddd, MMM D, YYYY h:mmA DATE_FORMAT_LONG: 'llll', @@ -187,6 +224,8 @@ const DateUtils = { return morning(now.add(1, 'month').date(1)); }, + getChronoPast, + parseDateString(dateLikeString) { const parsed = getChrono().parse(dateLikeString); const gotTime = { start: false, end: false }; diff --git a/app/src/services/search/search-query-ast.es6 b/app/src/services/search/search-query-ast.es6 index 16e6f1909..44b11a855 100644 --- a/app/src/services/search/search-query-ast.es6 +++ b/app/src/services/search/search-query-ast.es6 @@ -19,6 +19,9 @@ class SearchQueryExpressionVisitor { visitFrom(node) { throw new Error('Abstract function not implemented!', node); } + visitDate(node) { + throw new Error('Abstract function not implemented!', node); + } visitTo(node) { throw new Error('Abstract function not implemented!', node); } @@ -147,6 +150,29 @@ class FromQueryExpression extends QueryExpression { } } +class DateQueryExpression extends QueryExpression { + constructor(text, direction = 'before') { + super(); + this.text = text; + this.direction = direction; + } + + accept(visitor) { + visitor.visitDate(this); + } + + _computeIsMatchCompatible() { + return false; + } + + equals(other) { + if (!(other instanceof DateQueryExpression)) { + return false; + } + return this.text.equals(other.text); + } +} + class ToQueryExpression extends QueryExpression { constructor(text) { super(); @@ -368,5 +394,6 @@ module.exports = { StarredStatusQueryExpression, MatchQueryExpression, InQueryExpression, + DateQueryExpression, HasAttachmentQueryExpression, }; diff --git a/app/src/services/search/search-query-backend-imap.es6 b/app/src/services/search/search-query-backend-imap.es6 index 6449cadab..458b2c8ee 100644 --- a/app/src/services/search/search-query-backend-imap.es6 +++ b/app/src/services/search/search-query-backend-imap.es6 @@ -116,6 +116,10 @@ class IMAPSearchQueryExpressionVisitor extends SearchQueryExpressionVisitor { this._result = ['FROM', text]; } + visitDate(node) { + throw new Error('Function not implemented!', node); + } + visitTo(node) { const text = this.visitAndGetResult(node.text); this._result = ['TO', text]; diff --git a/app/src/services/search/search-query-backend-local.es6 b/app/src/services/search/search-query-backend-local.es6 index 6a8ad9ffd..c8b5ca125 100644 --- a/app/src/services/search/search-query-backend-local.es6 +++ b/app/src/services/search/search-query-backend-local.es6 @@ -2,11 +2,13 @@ import { SearchQueryExpressionVisitor, OrQueryExpression, AndQueryExpression, + DateQueryExpression, UnreadStatusQueryExpression, StarredStatusQueryExpression, HasAttachmentQueryExpression, MatchQueryExpression, } from './search-query-ast'; +import { DateUtils } from 'mailspring-exports'; /* * This class visits a match-compatible subtree and condenses it into a single @@ -37,6 +39,8 @@ class MatchQueryExpressionVisitor extends SearchQueryExpressionVisitor { this._result = `(${lhs} OR ${rhs})`; } + visitDate(node) {} + visitFrom(node) { const text = this.visitAndGetResult(node.text); this._result = `(from_ : "${text}"*)`; @@ -144,6 +148,10 @@ class MatchCompatibleQueryCondenser extends SearchQueryExpressionVisitor { this._result = new UnreadStatusQueryExpression(node.status); } + visitDate(node) { + this._result = new DateQueryExpression(node.text, node.direction); + } + visitStarred(node) { this._result = new StarredStatusQueryExpression(node.status); } @@ -218,6 +226,17 @@ class StructuredSearchQueryVisitor extends SearchQueryExpressionVisitor { this._result = `(\`${this._className}\`.\`data\` LIKE '%"has_attachments":true%')`; } + visitDate(node) { + const comparator = node.direction === 'before' ? '<' : '>'; + const date = DateUtils.getChronoPast().parseDate(node.text.token.s); + if (!date) { + this._result = ''; + return; + } + const ts = Math.floor(date.getTime() / 1000); + this._result = `(\`${this._className}\`.\`lastMessageReceivedTimestamp\` ${comparator} ${ts})`; + } + visitMatch(node) { const searchTable = `${this._className}Search`; diff --git a/app/src/services/search/search-query-parser.es6 b/app/src/services/search/search-query-parser.es6 index 283443622..132dc34d1 100644 --- a/app/src/services/search/search-query-parser.es6 +++ b/app/src/services/search/search-query-parser.es6 @@ -9,6 +9,7 @@ import { TextQueryExpression, UnreadStatusQueryExpression, StarredStatusQueryExpression, + DateQueryExpression, InQueryExpression, HasAttachmentQueryExpression, } from './search-query-ast'; @@ -66,6 +67,9 @@ const reserved = [ 'in', 'has', 'attachment', + 'before', + 'since', + 'after', ]; const mightBeReserved = text => { @@ -272,6 +276,18 @@ const parseSimpleQuery = text => { } } + if (tok.s.toUpperCase() === 'SINCE' || tok.s.toUpperCase() === 'AFTER') { + const afterColon = consumeExpectedToken(afterTok, ':'); + const [txt, afterTxt] = parseText(afterColon); + return [new DateQueryExpression(txt, 'after'), afterTxt]; + } + + if (tok.s.toUpperCase() === 'BEFORE') { + const afterColon = consumeExpectedToken(afterTok, ':'); + const [txt, afterTxt] = parseText(afterColon); + return [new DateQueryExpression(txt, 'before'), afterTxt]; + } + if (tok.s.toUpperCase() === 'IN') { const afterColon = consumeExpectedToken(afterTok, ':'); const [txt, afterTxt] = parseText(afterColon); diff --git a/app/static/components/search-bar.less b/app/static/components/search-bar.less index 8eff9534c..e4cba9d43 100644 --- a/app/static/components/search-bar.less +++ b/app/static/components/search-bar.less @@ -64,6 +64,7 @@ } .search-accessory { + &.search { position: absolute; top: 8px;