Build ContactBook concept to track which accounts have sync running

This commit is contained in:
Ben Gotow 2019-10-07 14:00:34 -05:00
parent 1f6aab1083
commit 96c6a64e46
19 changed files with 267 additions and 159 deletions

View file

@ -110,7 +110,7 @@ export function registerMenuItems(accounts: Account[], sidebarAccountIds: string
return;
}
const idx = submenu.findIndex(({ type }) => type === 'separator');
const idx = submenu.findIndex(({ id }) => id === 'account-shortcuts-separator');
if (!(idx > 0)) {
return;
}

View file

@ -31,7 +31,11 @@ class AddContactToolbarWithData extends React.Component<AddContactToolbarProps>
tabIndex={-1}
disabled={!enabled}
className={`btn btn-toolbar btn-new-contact ${!enabled && 'btn-disabled'}`}
title={localized('New contact in %@', acct ? acct.label : 'account')}
title={
acct
? localized('New contact in %@', acct.label)
: localized('Select an account to add a contact.')
}
onClick={enabled ? this.onAdd : undefined}
>
<Icons.NewPerson />

View file

@ -86,6 +86,7 @@ class ContactDetailToolbarWithData extends React.Component<ContactDetailToolbarP
{perspective && perspective.type === 'group' && (
<button
tabIndex={-1}
title={localized('Remove from Group')}
className={`btn btn-toolbar ${actionSet.length === 0 && 'btn-disabled'}`}
onClick={actionSet.length > 0 ? this._onRemoveFromSource : undefined}
>
@ -94,14 +95,15 @@ class ContactDetailToolbarWithData extends React.Component<ContactDetailToolbarP
)}
<button
tabIndex={-1}
className={`btn btn-toolbar ${actionSet.length === 0 && 'btn-disabled'}`}
title={localized('Delete')}
className={`btn btn-toolbar ${actionSet.length === 0 && 'btn-disabled'}`}
onClick={actionSet.length > 0 ? this._onDelete : undefined}
>
<RetinaImg name="toolbar-trash.png" mode={RetinaImg.Mode.ContentIsMask} />
</button>
<button
tabIndex={-1}
title={localized('Edit')}
className={`btn btn-toolbar ${!editable && 'btn-disabled'}`}
onClick={editable ? () => Store.setEditing(actionSet[0].id) : undefined}
>

View file

@ -3,11 +3,13 @@ import {
Account,
AccountStore,
ContactGroup,
ContactBook,
Rx,
Actions,
DestroyContactGroupTask,
SyncbackContactGroupTask,
ChangeContactGroupMembershipTask,
localized,
} from 'mailspring-exports';
import { ContactsPerspective, Store } from './Store';
import {
@ -16,12 +18,14 @@ import {
OutlineViewItem,
ListensToFluxStore,
ListensToObservable,
IOutlineViewItem,
} from 'mailspring-component-kit';
import { isEqual } from 'underscore';
interface ContactsPerspectivesProps {
accounts: Account[];
groups: ContactGroup[];
books: ContactBook[];
findInMailDisabled: string[];
selected: ContactsPerspective | null;
onSelect: (item: ContactsPerspective | null) => void;
@ -36,9 +40,115 @@ function perspectiveForGroup(g: ContactGroup): ContactsPerspective {
};
}
interface OutlineViewForAccountProps {
account: Account;
groups: ContactGroup[];
books: ContactBook[];
findInMailDisabled: boolean;
selected: ContactsPerspective | null;
onSelect: (item: ContactsPerspective | null) => void;
}
const OutlineViewForAccount = ({
account,
groups,
books,
selected,
onSelect,
findInMailDisabled,
}: OutlineViewForAccountProps) => {
const items: IOutlineViewItem[] = [];
if (books.length) {
items.push({
id: 'all-contacts',
name: localized('All Contacts'),
iconName: 'person.png',
children: [],
selected: selected && selected.type === 'all',
onSelect: () =>
onSelect({ accountId: account.id, type: 'all', label: localized('All Contacts') }),
shouldAcceptDrop: () => false,
});
for (const group of groups) {
const perspective = perspectiveForGroup(group);
items.push({
id: `${perspective.accountId}-${perspective.label}`,
name: perspective.label,
iconName: 'label.png',
children: [],
selected: isEqual(selected, perspective),
onSelect: () => onSelect(perspective),
onEdited: (item, value: string) => {
Actions.queueTask(SyncbackContactGroupTask.forRenaming(group, value));
},
onDelete: () => {
Actions.queueTask(DestroyContactGroupTask.forRemoving(group));
},
onDrop: (item, { dataTransfer }) => {
const data = JSON.parse(dataTransfer.getData('mailspring-contacts-data'));
const contacts = data.ids.map(i => Store.filteredContacts().find(c => c.id === i));
Actions.queueTask(
ChangeContactGroupMembershipTask.forMoving({
direction: 'add',
contacts,
group,
})
);
},
shouldAcceptDrop: (item, { dataTransfer }) => {
if (!dataTransfer.types.includes('mailspring-contacts-data')) {
return false;
}
if (isEqual(selected, perspective)) {
return false;
}
// We can't inspect the drag payload until drop, so we use a dataTransfer
// type to encode the account IDs of threads currently being dragged.
const accountsType = dataTransfer.types.find(t => t.startsWith('mailspring-accounts='));
const accountIds = (accountsType || '').replace('mailspring-accounts=', '').split(',');
return isEqual(accountIds, [perspective.accountId]);
},
});
}
}
items.push({
id: 'found-in-mail',
name: localized('Found in Mail'),
iconName: 'inbox.png',
children: [],
className: findInMailDisabled ? 'found-in-mail-disabled' : '',
selected: selected && selected.type === 'found-in-mail',
shouldAcceptDrop: () => false,
onSelect: () =>
onSelect({
accountId: account.id,
type: 'found-in-mail',
label: `${localized('Found in Mail')} (${account.label})`,
}),
});
return (
<OutlineView
title={account.label}
items={items}
onItemCreated={
books.length > 0
? name => Actions.queueTask(SyncbackContactGroupTask.forCreating(account.id, name))
: undefined
}
/>
);
};
const ContactsPerspectivesWithData: React.FunctionComponent<ContactsPerspectivesProps> = ({
findInMailDisabled,
groups,
books,
accounts,
selected,
onSelect,
@ -57,87 +167,14 @@ const ContactsPerspectivesWithData: React.FunctionComponent<ContactsPerspectives
/>
</section>
{accounts.map(a => (
<OutlineView
<OutlineViewForAccount
key={a.id}
title={a.label}
onItemCreated={name => Actions.queueTask(SyncbackContactGroupTask.forCreating(a.id, name))}
items={[
{
id: 'all-contacts',
name: 'All Contacts',
iconName: 'person.png',
children: [],
selected: selected && selected.accountId == a.id && selected.type === 'all',
onSelect: () => onSelect({ accountId: a.id, type: 'all', label: 'All Contacts' }),
shouldAcceptDrop: () => false,
},
...groups
.filter(g => g.accountId === a.id)
.map(group => {
const perspective = perspectiveForGroup(group);
return {
id: `${perspective.accountId}-${perspective.label}`,
name: perspective.label,
iconName: 'label.png',
children: [],
selected: isEqual(selected, perspective),
onSelect: () => onSelect(perspective),
onEdited: (item, value: string) => {
Actions.queueTask(SyncbackContactGroupTask.forRenaming(group, value));
},
onDelete: () => {
Actions.queueTask(DestroyContactGroupTask.forRemoving(group));
},
onDrop: (item, { dataTransfer }) => {
const data = JSON.parse(dataTransfer.getData('mailspring-contacts-data'));
const contacts = data.ids.map(i =>
Store.filteredContacts().find(c => c.id === i)
);
Actions.queueTask(
ChangeContactGroupMembershipTask.forMoving({
direction: 'add',
contacts,
group,
})
);
},
shouldAcceptDrop: (item, { dataTransfer }) => {
if (!dataTransfer.types.includes('mailspring-contacts-data')) {
return false;
}
if (isEqual(selected, perspective)) {
return false;
}
// We can't inspect the drag payload until drop, so we use a dataTransfer
// type to encode the account IDs of threads currently being dragged.
const accountsType = dataTransfer.types.find(t =>
t.startsWith('mailspring-accounts=')
);
const accountIds = (accountsType || '')
.replace('mailspring-accounts=', '')
.split(',');
return isEqual(accountIds, [perspective.accountId]);
},
};
}),
{
id: 'found-in-mail',
name: 'Found in Mail',
iconName: 'inbox.png',
children: [],
className: findInMailDisabled.includes(a.id) ? 'found-in-mail-disabled' : '',
selected: selected && selected.accountId == a.id && selected.type === 'found-in-mail',
shouldAcceptDrop: () => false,
onSelect: () =>
onSelect({
accountId: a.id,
type: 'found-in-mail',
label: `Found in Mail (${a.label})`,
}),
},
]}
account={a}
findInMailDisabled={findInMailDisabled.includes(a.id)}
books={books.filter(b => b.accountId === a.id)}
groups={groups.filter(b => b.accountId === a.id)}
selected={selected && selected.accountId === a.id ? selected : null}
onSelect={onSelect}
/>
))}
</ScrollRegion>
@ -148,6 +185,7 @@ export const ContactPerspectivesList = ListensToObservable(
stores: [AccountStore, Store],
getStateFromStores: () => ({
accounts: AccountStore.accounts(),
books: Store.books(),
groups: Store.groups(),
selected: Store.perspective(),
onSelect: s => Store.setPerspective(s),

View file

@ -1,5 +1,11 @@
import Rx from 'rx-lite';
import { DatabaseStore, Contact, ContactGroup, MutableQuerySubscription } from 'mailspring-exports';
import {
DatabaseStore,
Contact,
ContactGroup,
ContactBook,
MutableQuerySubscription,
} from 'mailspring-exports';
import MailspringStore from 'mailspring-store';
import { ListTabular } from 'mailspring-component-kit';
@ -10,6 +16,7 @@ class ContactsWindowStore extends MailspringStore {
_contacts: Contact[] = [];
_contactsSubscription: MutableQuerySubscription<Contact>;
_groups: ContactGroup[] = [];
_books: ContactBook[] = [];
_search: string = '';
_filtered: Contact[] | null = null;
_editing: string | 'new' | false = false;
@ -24,6 +31,7 @@ class ContactsWindowStore extends MailspringStore {
.where(Contact.attributes.refs.greaterThan(0))
.where(Contact.attributes.hidden.equal(false));
this._contactsSubscription = new MutableQuerySubscription<Contact>(contacts);
Rx.Observable.fromNamedQuerySubscription('contacts', this._contactsSubscription).subscribe(
contacts => {
this._contacts = contacts as Contact[];
@ -32,14 +40,24 @@ class ContactsWindowStore extends MailspringStore {
}
);
const groups = Rx.Observable.fromQuery(DatabaseStore.findAll<ContactGroup>(ContactGroup));
groups.subscribe(groups => {
this._groups = groups;
Rx.Observable.fromQuery(DatabaseStore.findAll<ContactGroup>(ContactGroup)).subscribe(
groups => {
this._groups = groups;
this.trigger();
}
);
Rx.Observable.fromQuery(DatabaseStore.findAll<ContactBook>(ContactBook)).subscribe(books => {
this._books = books;
this.trigger();
});
});
}
books() {
return this._books;
}
groups() {
return this._groups;
}

View file

@ -8,7 +8,7 @@ import { FoundInMailEnabledBar } from './FoundInMailEnabledBar';
function adjustMenus() {
const contactMenu: typeof AppEnv.menu.template[0] = {
key: 'Contact',
id: 'Contact',
label: localized('Contact'),
submenu: [
{
@ -32,10 +32,8 @@ function adjustMenus() {
],
};
const template = AppEnv.menu.template.filter(
item => item.key !== 'Thread' && item.key !== 'View'
);
const editIndex = template.findIndex(item => item.key === 'Edit');
const template = AppEnv.menu.template.filter(item => item.id !== 'Thread' && item.id !== 'View');
const editIndex = template.findIndex(item => item.id === 'Edit');
template.splice(editIndex + 1, 0, contactMenu);
AppEnv.menu.template = template;

View file

@ -44,6 +44,7 @@
"window:select-account-8": "mod+9",
"window:sync-mail-now": "f5",
"application:show-main-window": "mod+0",
"contenteditable:underline": "mod+u",
"contenteditable:bold": "mod+b",

View file

@ -3,7 +3,7 @@ const { localized } = require('../src/intl');
module.exports = {
menu: [
{
key: 'Mailspring',
id: 'Mailspring',
label: 'Mailspring',
submenu: [
{ label: localized('About Mailspring'), command: 'application:about' },
@ -42,7 +42,7 @@ module.exports = {
],
},
{
key: 'File',
id: 'File',
label: localized('File'),
submenu: [
{ label: localized('Sync New Mail Now'), command: 'window:sync-mail-now' },
@ -56,7 +56,7 @@ module.exports = {
},
{
key: 'Edit',
id: 'Edit',
label: localized('Edit'),
submenu: [
{ label: localized('Undo'), command: 'core:undo' },
@ -86,7 +86,7 @@ module.exports = {
},
{
key: 'View',
id: 'View',
label: localized('View'),
submenu: [
{
@ -140,7 +140,7 @@ module.exports = {
},
{
key: 'Thread',
id: 'Thread',
label: localized('Thread'),
submenu: [
{ label: localized('Reply'), command: 'core:reply' },
@ -192,7 +192,7 @@ module.exports = {
},
{
key: 'Developer',
id: 'Developer',
label: localized('Developer'),
submenu: [
{
@ -202,7 +202,6 @@ module.exports = {
},
{ type: 'separator' },
{ label: localized('Calendar Preview'), command: 'application:show-calendar' },
{ label: localized('Contacts Preview'), command: 'application:show-contacts' },
{ type: 'separator' },
{ label: localized('Create a Plugin') + '...', command: 'window:create-package' },
{ label: localized('Install a Plugin') + '...', command: 'window:install-package' },
@ -221,13 +220,22 @@ module.exports = {
],
},
{
key: 'Window',
id: 'Window',
label: localized('Window'),
submenu: [
{ label: localized('Minimize'), command: 'application:minimize' },
{ label: localized('Zoom'), command: 'application:zoom' },
{ type: 'separator', id: 'window-list-separator' },
{ type: 'separator' },
{
label: localized('Message Viewer'),
command: 'application:show-main-window',
},
{
label: localized('Contacts'),
command: 'application:show-contacts',
},
{ type: 'separator', id: 'window-list-separator' },
{ type: 'separator', id: 'account-shortcuts-separator' },
{
label: localized('Bring All to Front'),
command: 'application:bring-all-windows-to-front',
@ -236,7 +244,7 @@ module.exports = {
},
{
key: 'Help',
id: 'Help',
label: localized('Help'),
submenu: [
{ label: localized('Mailspring Help'), command: 'application:view-help' },

View file

@ -3,7 +3,7 @@ const { localized } = require('../src/intl');
module.exports = {
menu: [
{
key: 'File',
id: 'File',
label: localized('File'),
submenu: [
{ label: localized('Sync New Mail Now'), command: 'window:sync-mail-now' },
@ -24,7 +24,7 @@ module.exports = {
},
{
key: 'Edit',
id: 'Edit',
label: localized('Edit'),
submenu: [
{ label: localized('Undo'), command: 'core:undo' },
@ -58,7 +58,7 @@ module.exports = {
},
{
key: 'View',
id: 'View',
label: localized('View'),
submenu: [
{
@ -110,7 +110,7 @@ module.exports = {
},
{
key: 'Thread',
id: 'Thread',
label: localized('Thread'),
submenu: [
{ label: localized('Reply'), command: 'core:reply' },
@ -161,7 +161,7 @@ module.exports = {
],
},
{
key: 'Developer',
id: 'Developer',
label: localized('Developer'),
submenu: [
{
@ -171,7 +171,6 @@ module.exports = {
},
{ type: 'separator' },
{ label: localized('Calendar Preview'), command: 'application:show-calendar' },
{ label: localized('Contacts Preview'), command: 'application:show-contacts' },
{ type: 'separator' },
{ label: localized('Create a Plugin') + '...', command: 'window:create-package' },
{ label: localized('Install a Plugin') + '...', command: 'window:install-package' },
@ -189,16 +188,27 @@ module.exports = {
],
},
{
key: 'Window',
id: 'Window',
label: localized('Window'),
submenu: [
{ label: localized('Minimize'), command: 'application:minimize' },
{ label: localized('Zoom'), command: 'application:zoom' },
{ type: 'separator' },
{
label: localized('Message Viewer'),
command: 'application:show-main-window',
accelerator: 'CmdOrCtrl+0',
},
{
label: localized('Contacts'),
command: 'application:show-contacts',
},
{ type: 'separator', id: 'window-list-separator' },
{ type: 'separator', id: 'account-shortcuts-separator' },
],
},
{
key: 'Help',
id: 'Help',
label: localized('Help'),
submenu: [
{ label: 'VERSION', enabled: false },

View file

@ -3,7 +3,7 @@ const { localized } = require('../src/intl');
module.exports = {
menu: [
{
key: 'Edit',
id: 'Edit',
label: localized('Edit'),
submenu: [
{ label: localized('Undo'), command: 'core:undo' },
@ -33,7 +33,7 @@ module.exports = {
},
{
key: 'View',
id: 'View',
label: localized('View'),
submenu: [
{
@ -87,7 +87,7 @@ module.exports = {
},
{
key: 'Thread',
id: 'Thread',
label: localized('Thread'),
submenu: [
{ label: localized('Reply'), command: 'core:reply' },
@ -138,7 +138,7 @@ module.exports = {
],
},
{
key: 'Developer',
id: 'Developer',
label: localized('Developer'),
submenu: [
{
@ -148,7 +148,6 @@ module.exports = {
},
{ type: 'separator' },
{ label: localized('Calendar Preview'), command: 'application:show-calendar' },
{ label: localized('Contacts Preview'), command: 'application:show-contacts' },
{ type: 'separator' },
{ label: localized('Create a Plugin') + '...', command: 'window:create-package' },
{ label: localized('Install a Plugin') + '...', command: 'window:install-package' },
@ -167,12 +166,23 @@ module.exports = {
],
},
{
key: 'Window',
id: 'Window',
label: localized('Window'),
submenu: [
{ label: localized('Minimize'), command: 'application:minimize' },
{ label: localized('Zoom'), command: 'application:zoom' },
{ type: 'separator' },
{
label: localized('Message Viewer'),
command: 'application:show-main-window',
accelerator: 'CmdOrCtrl+0',
},
{
label: localized('Contacts'),
command: 'application:show-contacts',
},
{ type: 'separator', id: 'window-list-separator' },
{ type: 'separator', id: 'account-shortcuts-separator' },
],
},
{ type: 'separator' },

View file

@ -154,24 +154,17 @@ export default class ApplicationMenu {
}
const idx = windowMenu.submenu.findIndex(({ id }) => id === 'window-list-separator');
let workShortcut = 'CmdOrCtrl+alt+w';
if (process.platform === 'win32') {
workShortcut = 'ctrl+shift+w';
}
const accelerators = {
default: 'CmdOrCtrl+0',
work: workShortcut,
};
const windows = global.application.windowManager.getOpenWindows();
const windowsItems = windows.map(w => ({
label: w.loadSettings().title || 'Window',
accelerator: accelerators[w.windowType],
click() {
w.show();
w.focus();
},
}));
const windowsItems = windows
.filter(w => w.windowType !== 'default' && w.windowType !== 'contacts')
.map(w => ({
label: w.loadSettings().title || 'Window',
click() {
w.show();
w.focus();
},
}));
return windowMenu.submenu.splice(idx, 0, { type: 'separator' }, ...windowsItems);
}

View file

@ -67,7 +67,7 @@ interface OutlineViewState {
* @param {props.onCollapseToggled} props.onCollapseToggled
* @class OutlineView
*/
class OutlineView extends Component<OutlineViewProps, OutlineViewState> {
export class OutlineView extends Component<OutlineViewProps, OutlineViewState> {
static displayName = 'OutlineView';
/*
@ -232,5 +232,3 @@ class OutlineView extends Component<OutlineViewProps, OutlineViewState> {
);
}
}
export default OutlineView;

View file

@ -0,0 +1,32 @@
/* eslint global-require: 0 */
import { Model, AttributeValues } from './model';
import Attributes from '../attributes';
export class ContactBook extends Model {
static attributes = {
...Model.attributes,
readonly: Attributes.String({
modelKey: 'readonly',
}),
source: Attributes.String({
modelKey: 'source',
}),
};
readonly: boolean;
source: 'carddav' | 'gpeople';
static sortOrderAttribute = () => {
return ContactBook.attributes.id;
};
static naturalSortOrder = () => {
return ContactBook.sortOrderAttribute().ascending();
};
constructor(data: AttributeValues<typeof ContactBook.attributes>) {
super(data);
}
}

View file

@ -1,5 +1,4 @@
/* eslint global-require: 0 */
import _str from 'underscore.string';
import { Model, AttributeValues } from './model';
import Attributes from '../attributes';
@ -12,6 +11,8 @@ export class ContactGroup extends Model {
}),
};
public name: string;
static sortOrderAttribute = () => {
return ContactGroup.attributes.name;
};
@ -20,8 +21,6 @@ export class ContactGroup extends Model {
return ContactGroup.sortOrderAttribute().ascending();
};
public name: string;
constructor(data: AttributeValues<typeof ContactGroup.attributes>) {
super(data);
}

View file

@ -40,7 +40,7 @@ export * from '../components/disclosure-triangle';
export const EditableList: typeof import('../components/editable-list').default;
export const DropdownMenu: typeof import('../components/dropdown-menu').default;
export const OutlineViewItem: typeof import('../components/outline-view-item').default;
export const OutlineView: typeof import('../components/outline-view').default;
export * from '../components/outline-view';
export const DateInput: typeof import('../components/date-input').default;
export const DatePicker: typeof import('../components/date-picker').default;
export const TimePicker: typeof import('../components/time-picker').default;

View file

@ -44,6 +44,7 @@ export * from '../flux/models/thread';
export * from '../flux/models/account';
export * from '../flux/models/message';
export * from '../flux/models/contact';
export * from '../flux/models/contact-book';
export * from '../flux/models/contact-group';
export * from '../flux/models/category';
export * from '../flux/models/calendar';
@ -80,6 +81,8 @@ export * from '../flux/tasks/change-role-mapping-task';
export * from '../flux/tasks/send-feature-usage-event-task';
export * from '../flux/tasks/syncback-contact-task';
export * from '../flux/tasks/destroy-contact-task';
export * from '../flux/tasks/destroy-contactgroup-task';
export * from '../flux/tasks/syncback-contactgroup-task';
export * from '../flux/tasks/change-contactgroup-membership-task';
// Stores

View file

@ -81,6 +81,7 @@ lazyLoadAndRegisterModel(`Thread`, 'thread');
lazyLoadAndRegisterModel(`Account`, 'account');
lazyLoadAndRegisterModel(`Message`, 'message');
lazyLoadAndRegisterModel(`Contact`, 'contact');
lazyLoadAndRegisterModel(`ContactBook`, 'contact-book');
lazyLoadAndRegisterModel(`ContactGroup`, 'contact-group');
lazyLoadAndRegisterModel(`Category`, 'category');
lazyLoadAndRegisterModel(`Calendar`, 'calendar');

View file

@ -8,24 +8,17 @@ import _ from 'underscore';
const ItemSpecificities = new WeakMap();
export type IMenuItem =
| {
label: string;
submenu?: IMenuItem[];
type?: string;
export type IMenuItem = {
label?: string;
submenu?: IMenuItem[];
type?: 'separator';
key?: string; //unlocalized label
command?: string;
enabled?: boolean;
hideWhenDisabled?: boolean;
visible?: boolean;
}
| {
key?: string; //unlocalized label
label?: string;
submenu?: IMenuItem[];
type: 'separator';
};
id?: string; //unlocalized label
command?: string;
enabled?: boolean;
hideWhenDisabled?: boolean;
visible?: boolean;
};
export function merge(menu: IMenuItem[], item: IMenuItem, itemSpecificity?: number) {
let matchingItem;

@ -1 +1 @@
Subproject commit e78951655f7bc194029c709549c0f5c1a29e9c85
Subproject commit 8c3e2ad5ba07b19fd96fe18cbe589b8c05c72e8c