diff --git a/app/internal_packages/thread-list/lib/thread-list-store.ts b/app/internal_packages/thread-list/lib/thread-list-store.ts index 956c72ab1f..61e114a009 100644 --- a/app/internal_packages/thread-list/lib/thread-list-store.ts +++ b/app/internal_packages/thread-list/lib/thread-list-store.ts @@ -20,6 +20,8 @@ class ThreadListStore extends MailspringStore { super(); this.listenTo(FocusedPerspectiveStore, this._onPerspectiveChanged); this.createListDataSource(); + + AppEnv.config.observe('core.lastUsedOrder', () => this.createListDataSource()); } dataSource = () => { diff --git a/app/internal_packages/thread-search/lib/search-query-subscription.ts b/app/internal_packages/thread-search/lib/search-query-subscription.ts index e169e1776b..9833278622 100644 --- a/app/internal_packages/thread-search/lib/search-query-subscription.ts +++ b/app/internal_packages/thread-search/lib/search-query-subscription.ts @@ -7,6 +7,7 @@ import { ComponentRegistry, MutableQuerySubscription, } from 'mailspring-exports'; +import { SortOrder } from 'src/flux/attributes'; class SearchQuerySubscription extends MutableQuerySubscription { _searchQuery: string; @@ -46,9 +47,29 @@ class SearchQuerySubscription extends MutableQuerySubscription { console.info('Failed to parse local search query, falling back to generic query', e); dbQuery = dbQuery.search(this._searchQuery); } + + let order = Thread.attributes.lastMessageReceivedTimestamp.descending(); + + const orderBy: string | undefined = AppEnv.config.get('core.lastUsedOrder'); + if (orderBy) { + switch (orderBy) { + case '2': + order = Thread.attributes.subject.ascending(); + break; + + case '3': + order = Thread.attributes.subject.descending(); + break; + + case '0': + order = Thread.attributes.lastMessageReceivedTimestamp.ascending(); + break; + } + } + dbQuery = dbQuery .background() - .order(Thread.attributes.lastMessageReceivedTimestamp.descending()) + .order(order) .limit(1000); this.replaceQuery(dbQuery); diff --git a/app/internal_packages/thread-search/lib/thread-search-bar.tsx b/app/internal_packages/thread-search/lib/thread-search-bar.tsx index 5657df1972..547f78cf59 100644 --- a/app/internal_packages/thread-search/lib/thread-search-bar.tsx +++ b/app/internal_packages/thread-search/lib/thread-search-bar.tsx @@ -1,6 +1,12 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { ListensToFluxStore, RetinaImg, KeyCommandsRegion } from 'mailspring-component-kit'; +import { + ListensToFluxStore, + RetinaImg, + KeyCommandsRegion, + DropdownMenu, + Flexbox, +} from 'mailspring-component-kit'; import { localized, Actions, @@ -41,6 +47,7 @@ interface ThreadSearchBarState { description: any; }; selectedIdx: number; + lastSortBy: string; } class ThreadSearchBar extends Component { @@ -61,6 +68,7 @@ class ThreadSearchBar extends Component { - // If the user is in list mode, we need to clear the selection because the - // thread action bar appears over the search bar. Kind of a hack. - if (WorkspaceStore.layoutMode() === 'list') { - AppEnv.commands.dispatch('multiselect-list:deselect-all'); - } - Actions.popSheet(); - this._fieldEl.focus(); - }, - }} - > - {isSearching ? ( - - ) : ( - this._fieldEl.focus()} - /> - )} - (this._fieldEl = el)} - value={showPlaceholder ? this._placeholder() : query} - onKeyDown={this._onKeyDown} - onFocus={this._onFocus} - onBlur={this._onBlur} - onChange={this._onSearchQueryChanged} - /> - {showX && ( - + { + // If the user is in list mode, we need to clear the selection because the + // thread action bar appears over the search bar. Kind of a hack. + if (WorkspaceStore.layoutMode() === 'list') { + AppEnv.commands.dispatch('multiselect-list:deselect-all'); + } + Actions.popSheet(); + this._fieldEl.focus(); + }, + }} + > + {isSearching ? ( + + ) : ( + this._fieldEl.focus()} + /> + )} + (this._fieldEl = el)} + value={showPlaceholder ? this._placeholder() : query} + onKeyDown={this._onKeyDown} + onFocus={this._onFocus} + onBlur={this._onBlur} + onChange={this._onSearchQueryChanged} /> - )} - {this.state.suggestions.length > 0 && - this.state.focused && ( + {showX && ( + + )} + {this.state.suggestions.length > 0 && this.state.focused && (
{suggestions.map((s, idx) => (
)} - + + x.id === (this.state.lastSortBy || '1'))[0]} + defaultSelectedIndex={Number.parseInt(this.state.lastSortBy) || -1} + itemKey={item => item.id} + itemContent={item => item.name} + onSelect={this._onSortSelect} + style={{ order: 100 }} + /> + ); } + + private _onSortSelect = (item: any) => { + this.setState({ lastSortBy: item.id }); + AppEnv.config.set('core.lastUsedOrder', item.id); + }; } export default ListensToFluxStore(ThreadSearchBar, { diff --git a/app/internal_packages/thread-search/styles/thread-search-bar.less b/app/internal_packages/thread-search/styles/thread-search-bar.less index 59d564351c..24efad0746 100644 --- a/app/internal_packages/thread-search/styles/thread-search-bar.less +++ b/app/internal_packages/thread-search/styles/thread-search-bar.less @@ -3,9 +3,14 @@ @token-color: @accent-primary; +.thread-search-container { + order: -100; + width: 100%; + flex: 1; +} + .thread-search-bar { position: relative; - order: -100; overflow: visible; z-index: 100; width: 450px; @@ -125,3 +130,11 @@ } } } + +.btn.thread-search-sort { + box-shadow: @shadow-border !important; + height: 23px; + margin-left: 5px; + margin-top: 5px; + white-space: nowrap; +} diff --git a/app/lang/en.json b/app/lang/en.json index 6caf3cee3c..7e2e98a158 100644 --- a/app/lang/en.json +++ b/app/lang/en.json @@ -61,6 +61,7 @@ "Archive": "Archive", "Archived %@": "Archived %@", "Are you sure?": "Are you sure?", + "ASC": "ASC", "Attach File": "Attach File", "Attach Mailsync to Xcode": "Attach Mailsync to Xcode", "Attachment name": "Attachment name", @@ -178,6 +179,7 @@ "Deleting %@": "Deleting %@", "Deleting all messages in %@": "Deleting all messages in %@", "Deleting draft": "Deleting draft", + "DESC": "DESC", "Deselect all conversations": "Deselect all conversations", "Developer": "Developer", "Disable": "Disable", diff --git a/app/src/flux/attributes/attribute-string.ts b/app/src/flux/attributes/attribute-string.ts index cf1b6e746d..91a34ad2f7 100644 --- a/app/src/flux/attributes/attribute-string.ts +++ b/app/src/flux/attributes/attribute-string.ts @@ -10,6 +10,8 @@ String attributes can be queries using `equal`, `not`, and `startsWith`. Matchin Section: Database */ export class AttributeString extends Attribute { + applyCaseInsensitivity = true; + toJSON(val) { return val; } diff --git a/app/src/flux/attributes/attribute.ts b/app/src/flux/attributes/attribute.ts index 7af09417ce..8bde610a40 100644 --- a/app/src/flux/attributes/attribute.ts +++ b/app/src/flux/attributes/attribute.ts @@ -15,6 +15,7 @@ export class Attribute { public jsonKey: string; public queryable: boolean; public loadFromColumn: boolean; + public applyCaseInsensitivity = false; constructor({ modelKey, diff --git a/app/src/flux/attributes/sort-order.ts b/app/src/flux/attributes/sort-order.ts index 042d3b2d6f..aef3be37f4 100644 --- a/app/src/flux/attributes/sort-order.ts +++ b/app/src/flux/attributes/sort-order.ts @@ -18,6 +18,7 @@ Section: Database export class SortOrder { public attr: Attribute; public direction: 'ASC' | 'DESC'; + public collation: string; constructor(attr: Attribute, direction: 'ASC' | 'DESC' = 'DESC') { this.attr = attr; @@ -25,7 +26,9 @@ export class SortOrder { } orderBySQL(klass: typeof Model) { - return `\`${klass.name}\`.\`${this.attr.tableColumn}\` ${this.direction}`; + return `\`${klass.name}\`.\`${this.attr.tableColumn}\` ${ + this.attr.applyCaseInsensitivity ? 'COLLATE NOCASE' : '' + } ${this.direction}`; } attribute() { diff --git a/app/src/mailbox-perspective.ts b/app/src/mailbox-perspective.ts index 3309484532..944b18490c 100644 --- a/app/src/mailbox-perspective.ts +++ b/app/src/mailbox-perspective.ts @@ -20,6 +20,7 @@ import { Folder } from './flux/models/folder'; import { Task } from './flux/tasks/task'; import * as Actions from './flux/actions'; import { QuerySubscription } from 'mailspring-exports'; +import { SortOrder } from './flux/attributes'; let WorkspaceStore = null; let ChangeStarredTask = null; @@ -384,7 +385,31 @@ class CategoryMailboxPerspective extends MailboxPerspective { .where([Thread.attributes.categories.containsAny(this.categories().map(c => c.id))]) .limit(0); - if (this.isSent()) { + const orderBy: string | undefined = AppEnv.config.get('core.lastUsedOrder'); + + if (orderBy) { + let order: SortOrder; + + switch (orderBy) { + case '2': + order = Thread.attributes.subject.ascending(); + break; + + case '3': + order = Thread.attributes.subject.descending(); + break; + + case '0': + order = Thread.attributes.lastMessageReceivedTimestamp.ascending(); + break; + + default: + order = Thread.attributes.lastMessageReceivedTimestamp.descending(); + break; + } + + query.order(order); + } else if (this.isSent()) { query.order(Thread.attributes.lastMessageSentTimestamp.descending()); }